@walwal-harness/cli 5.6.4 → 5.7.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,8 +1,12 @@
1
1
  {
2
- "version": 2,
2
+ "version": 3,
3
3
  "mode": "solo",
4
4
  "project_name": "",
5
5
  "pipeline": null,
6
+ "dispatch": {
7
+ "counter": 0,
8
+ "id": null
9
+ },
6
10
  "sprint": {
7
11
  "number": 0,
8
12
  "status": "init",
package/bin/init.js CHANGED
@@ -346,6 +346,14 @@ function scaffoldHarness() {
346
346
  fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2) + '\n');
347
347
  log('progress.json migrated to v2 (mode + team_state added)');
348
348
  }
349
+ if (progress.version < 3) {
350
+ progress.version = 3;
351
+ if (!progress.dispatch) {
352
+ progress.dispatch = { counter: 0, id: null };
353
+ }
354
+ fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2) + '\n');
355
+ log('progress.json migrated to v3 (dispatch counter added)');
356
+ }
349
357
  } catch (e) {
350
358
  log('WARNING: Could not migrate progress.json');
351
359
  }
@@ -59,6 +59,39 @@ bash scripts/harness-tmux.sh --team --force-tmux
59
59
 
60
60
  **`--force-tmux` 필수**: iTerm2 감지 경로는 백그라운드에 iTerm2가 떠 있기만 해도 활성화되어 AppleScript 실패 시 팀 레이아웃이 조용히 사라짐. Team Mode는 항상 tmux로 강제하여 재현 가능한 레이아웃을 보장.
61
61
 
62
+ ### Step 2.5: Worker Pre-flight Bundle 빌드 (v5.6.6+)
63
+
64
+ Worker 는 plain Agent 로 실행되어 SKILL.md Startup 체크리스트를 자동 주입받지 못한다. Lead 가 Worker spawn 직전에 **역할별 바인딩 문서를 프롬프트에 직접 주입**한다. 이렇게 하면 "Worker 가 읽어야 함" → "이미 읽은 상태로 시작" 으로 전환되어 스킵이 구조적으로 불가능해진다.
65
+
66
+ ```bash
67
+ # Generator-{be|fe} / Evaluator-{functional|visual|code-quality} 별 번들 빌드
68
+ build_preflight_bundle() {
69
+ local role="$1" # generator-frontend | generator-backend | evaluator-functional | ...
70
+ local hroot="$2" # HARNESS_ROOT (worktree 가 아닌 원본 루트)
71
+ {
72
+ echo "===== ROOT CONVENTIONS.md ====="
73
+ [ -f "$hroot/CONVENTIONS.md" ] && cat "$hroot/CONVENTIONS.md" || echo "(none)"
74
+ echo
75
+ echo "===== AGENTS.md ====="
76
+ [ -f "$hroot/AGENTS.md" ] && cat "$hroot/AGENTS.md" || echo "(none)"
77
+ echo
78
+ echo "===== .harness/conventions/shared.md ====="
79
+ [ -f "$hroot/.harness/conventions/shared.md" ] && cat "$hroot/.harness/conventions/shared.md" || echo "(empty)"
80
+ echo
81
+ echo "===== .harness/conventions/$role.md ====="
82
+ [ -f "$hroot/.harness/conventions/$role.md" ] && cat "$hroot/.harness/conventions/$role.md" || echo "(empty)"
83
+ echo
84
+ echo "===== .harness/gotchas/$role.md ====="
85
+ [ -f "$hroot/.harness/gotchas/$role.md" ] && cat "$hroot/.harness/gotchas/$role.md" || echo "(empty)"
86
+ echo
87
+ echo "===== .harness/memory.md ====="
88
+ [ -f "$hroot/.harness/memory.md" ] && cat "$hroot/.harness/memory.md" || echo "(empty)"
89
+ }
90
+ }
91
+ ```
92
+
93
+ Worker/내부 Evaluator Agent 프롬프트 상단에 이 번들 출력을 `## Binding Rules (pre-loaded)` 섹션으로 삽입한다. Worker 는 이를 **추가 조회 없이 이미 적용되는 규범**으로 취급한다.
94
+
62
95
  ### Step 3: 초기 Worker 생성 (Auto-Dispatch)
63
96
 
64
97
  **v5.6.4+**: 개별 dequeue 대신 **`auto-dispatch`** 한 번으로 모든 idle team 에 ready feature 를 원자적으로 배정합니다. 의존성 없는 작업은 병렬로 즉시 시작됩니다.
@@ -97,10 +130,11 @@ ORCHESTRATION LOOP:
97
130
 
98
131
  Background Agent 완료 알림을 받으면:
99
132
 
100
- 1. Worker 결과 분석:
101
- - PASS인 경우 → Step 4a (Merge + Unblock)
102
- - FAIL (재시도 가능)인 경우 → Step 4b (Retry)
103
- - ESCALATED인 경우 → 사용자에게 알림, 해당 팀 유휴
133
+ 1. Worker 결과 분석 (반환 메시지 첫 줄 태그로 분기):
134
+ - `PASS` → Step 4a (Merge + Unblock)
135
+ - `FAIL` (재시도 가능) → Step 4b (Retry)
136
+ - `RATE_LIMIT` → Step 4c (Rate-Limit Hold, 10m probe)
137
+ - `ESCALATED` → 사용자에게 알림, 해당 팀 유휴
104
138
 
105
139
  2. **Auto-Dispatch (필수)** — worker 완료 직후 idle 이 된 팀뿐 아니라
106
140
  모든 idle team 에 ready feature 를 즉시 재배정:
@@ -145,6 +179,47 @@ Worker가 PASS로 반환되면:
145
179
  echo "$(date +'%Y-%m-%d %H:%M') | lead | pass | {FEATURE_ID} merged + unblocked deps" >> .harness/progress.log
146
180
  ```
147
181
 
182
+ #### Step 4c: Rate-Limit Hold (토큰 한도 대응 · v5.6.7+)
183
+
184
+ Worker 반환 첫 줄이 `RATE_LIMIT` 으로 시작하면 Lead 는 **에러 아닌 hold 모드**로 전환한다. 나머지 Worker 들은 자연 완료까지 계속 실행되고, 그 결과도 RATE_LIMIT 이면 합쳐서 hold 상태에 누적된다.
185
+
186
+ ```bash
187
+ # 1) Checkpoint 기록 (current in_progress + ready 스냅샷 저장)
188
+ bash "$HARNESS_ROOT/scripts/harness-queue-manager.sh" hold rate_limit 600 .
189
+
190
+ # 2) 로그 + tmux pane 타이틀 변경
191
+ echo "$(date +'%Y-%m-%d %H:%M') | lead | hold | rate-limit detected, pausing 10m" >> .harness/progress.log
192
+ tmux rename-window "⏸ HOLD (resume ~$(date -v+10M +%H:%M 2>/dev/null || date -d '+10 min' +%H:%M))" 2>/dev/null || true
193
+
194
+ # 3) 실패한 feature 는 requeue (WIP worktree 는 유지 — merge 없이 재사용)
195
+ bash "$HARNESS_ROOT/scripts/harness-queue-manager.sh" requeue {FEATURE_ID} .
196
+ ```
197
+
198
+ 4) **ScheduleWakeup 으로 10분 뒤 재진입 스케줄**:
199
+ ```
200
+ ScheduleWakeup({
201
+ delaySeconds: 600,
202
+ prompt: "/harness-team resume",
203
+ reason: "rate-limit hold — 10m probe"
204
+ })
205
+ ```
206
+
207
+ 5) Lead LOOP return (중단 아님 — wake-up 이 재진입 트리거).
208
+
209
+ **Wake-up 재진입 시 Lead 동작** (`/harness-team resume` 처리):
210
+
211
+ ```bash
212
+ # Probe: claude CLI 가 실제로 응답하는지 최소 호출로 확인
213
+ bash "$HARNESS_ROOT/scripts/harness-queue-manager.sh" resume-probe .
214
+ # 종료 코드: 0=clear, 1=still held, 2=escalated(>12h)
215
+ ```
216
+
217
+ - **0 (clear)** → 체크포인트 삭제됨. 즉시 `auto-dispatch` 실행 → Step 4 LOOP 복귀.
218
+ - **1 (still held)** → 다시 `ScheduleWakeup(600, "/harness-team resume", "rate-limit still held — cycle N")` 스케줄. hold_count 증가.
219
+ - **2 (escalated)** → 72 사이클(12시간) 초과. 사용자 개입 알림 후 LOOP 종료. 체크포인트 파일 (`.harness/actions/team-checkpoint.json`) 에 전체 상태가 남아있으므로 사용자가 수동 복구 가능.
220
+
221
+ **핵심 원칙**: 토큰 리밋은 "실패"가 아닌 "일시 정지". 진행 중이던 worktree/queue 상태는 그대로 보존되고, 10분 간격 probe 로 해제 즉시 이어서 진행한다. Session 을 닫아도 이어지길 원한다면 `ScheduleWakeup` 대신 `schedule` 스킬(CronCreate) 로 cron-backed 재시도 설정 가능.
222
+
148
223
  #### Step 4b: Retry (FAIL 처리)
149
224
 
150
225
  Worker가 FAIL (재시도 가능)로 반환되면:
@@ -171,6 +246,15 @@ Worker가 FAIL (재시도 가능)로 반환되면:
171
246
  당신은 Harness Team-{N} 워커입니다. **단일 Feature**에 대해 Gen→Eval 사이클을 수행합니다.
172
247
  완료 후 결과를 반환합니다. 다음 Feature는 Lead가 할당합니다.
173
248
 
249
+ ## Binding Rules (pre-loaded — 스킵 금지, 이미 적용됨)
250
+
251
+ Lead 가 Step 2.5 에서 build_preflight_bundle 로 생성한 번들이 아래에 주입됩니다.
252
+ 당신은 이 규칙을 이미 읽은 상태로 시작합니다. 추가 조회 불필요:
253
+
254
+ {PREFLIGHT_BUNDLE}
255
+
256
+ **작업 시작 전 필수 출력**: 위 번들에서 이번 Feature 작업에 **적용되는 규칙**을 3~8 줄로 요약한 뒤 진행하라. 비어있으면 "(empty)" 로 명시. 이 요약 없이 Phase 1 로 진입하면 Self-FAIL 처리하고 재시작한다. 내부 Evaluator Agent 를 생성할 때도 같은 번들을 `{PREFLIGHT_BUNDLE}` 자리에 그대로 전달하라 (Evaluator 도 plain Agent 이므로 자동주입 없음).
257
+
174
258
  ## 할당된 Feature
175
259
  - Feature ID: {FEATURE_ID}
176
260
  - 프로젝트 루트: 현재 디렉토리 (worktree 복사본)
@@ -301,6 +385,18 @@ logev fail "{FEATURE_ID} FAIL #{ATTEMPT} — {사유}"
301
385
  logev fail "{FEATURE_ID} FINAL FAIL after 5 attempts"
302
386
  ```
303
387
  Lead에게 반환: `ESCALATED | {FEATURE_ID} | attempts=5 | last_feedback={마지막_피드백}`
388
+
389
+ ### RATE_LIMIT (토큰 한도 감지 시 — v5.6.7+)
390
+
391
+ Gen 또는 Eval Phase 중 429 / "rate_limit" / "quota" / "overloaded_error" / "token limit" / "usage limit" 메시지를 만나면:
392
+
393
+ ```bash
394
+ logev hold "{FEATURE_ID} rate-limit hit — returning RATE_LIMIT to Lead"
395
+ ```
396
+ Lead에게 반환 **첫 줄에 반드시 `RATE_LIMIT` 태그 포함**:
397
+ `RATE_LIMIT | {FEATURE_ID} | phase={gen|eval} | attempt={N} | err={원문요약}`
398
+
399
+ 작업을 **포기하지 말고** 현재까지의 변경분을 worktree 에 그대로 commit (WIP). Lead 가 hold 해제 후 같은 worktree 로 resume.
304
400
  ```
305
401
 
306
402
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "5.6.4",
3
+ "version": "5.7.0",
4
4
  "description": "Production harness for AI agent engineering — Solo/Team mode, Planner, Generator(BE/FE), Evaluator chain (Code-Quality → Functional → Visual), optional Brainstormer. Supports React, Next.js, and Flutter FE stacks.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"
@@ -0,0 +1,109 @@
1
+ #!/bin/bash
2
+ # harness-archive.sh — 스프린트 종료 시 자동 아카이빙
3
+ #
4
+ # 동작:
5
+ # 1. .harness/actions/ 의 스프린트 산출물을 .harness/archive/D-NNN/S-NNN/ 로 이동
6
+ # 2. progress.json 의 sprint 상태를 초기화 (신규 dispatch 준비)
7
+ # 3. dispatch.id 가 없으면 counter++ 로 새 dispatch 시작
8
+ #
9
+ # 유지되는 파일 (이동하지 않음):
10
+ # - .harness/gotchas/**, .harness/conventions/**, .harness/ref/**
11
+ # - .harness/config.json, .harness/memory.md
12
+ # - .harness/progress.json (초기화만)
13
+ #
14
+ # 호출 주체: harness-next.sh (next_agent="archive" 도달 시 자동)
15
+
16
+ set -e
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh" 2>/dev/null || true
20
+
21
+ PROJECT_ROOT="${1:-.}"
22
+ PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"
23
+
24
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
25
+ ACTIONS_DIR="$PROJECT_ROOT/.harness/actions"
26
+ ARCHIVE_ROOT="$PROJECT_ROOT/.harness/archive"
27
+
28
+ if [ ! -f "$PROGRESS" ]; then
29
+ echo "[archive] ERROR: progress.json not found" >&2
30
+ exit 1
31
+ fi
32
+
33
+ command -v jq >/dev/null 2>&1 || { echo "[archive] ERROR: jq required" >&2; exit 1; }
34
+
35
+ # ── Read current dispatch/sprint numbers ──
36
+ dispatch_counter=$(jq -r '.dispatch.counter // 0' "$PROGRESS")
37
+ dispatch_id=$(jq -r '.dispatch.id // empty' "$PROGRESS")
38
+ sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS")
39
+
40
+ # Ensure dispatch id exists (fallback for legacy progress.json without dispatch)
41
+ if [ -z "$dispatch_id" ] || [ "$dispatch_id" = "null" ]; then
42
+ if [ "$dispatch_counter" -lt 1 ]; then
43
+ dispatch_counter=1
44
+ fi
45
+ dispatch_id=$(printf 'D-%03d' "$dispatch_counter")
46
+ fi
47
+
48
+ sprint_id=$(printf 'S-%03d' "$sprint_num")
49
+ target_dir="$ARCHIVE_ROOT/$dispatch_id/$sprint_id"
50
+
51
+ echo ""
52
+ echo " ── Archive ────────────────────────────"
53
+ echo " Dispatch : $dispatch_id"
54
+ echo " Sprint : $sprint_id"
55
+ echo " Target : ${target_dir#$PROJECT_ROOT/}"
56
+
57
+ # ── Move actions/ contents into archive ──
58
+ if [ -d "$ACTIONS_DIR" ] && [ "$(ls -A "$ACTIONS_DIR" 2>/dev/null)" ]; then
59
+ mkdir -p "$target_dir"
60
+ moved=0
61
+ for f in "$ACTIONS_DIR"/*; do
62
+ [ -e "$f" ] || continue
63
+ name="$(basename "$f")"
64
+ if [ -e "$target_dir/$name" ]; then
65
+ # Collision — suffix with timestamp to avoid overwrite
66
+ ts=$(date +%Y%m%d-%H%M%S)
67
+ mv "$f" "$target_dir/${name%.}.${ts}"
68
+ else
69
+ mv "$f" "$target_dir/"
70
+ fi
71
+ moved=$((moved + 1))
72
+ done
73
+ echo " Moved : $moved file(s)"
74
+ else
75
+ echo " Moved : 0 file(s) (actions/ empty)"
76
+ fi
77
+
78
+ # ── Reset progress.json for next dispatch ──
79
+ # - sprint → init
80
+ # - agents → cleared
81
+ # - artifacts → pending
82
+ # - dispatch.id cleared (next dispatcher run will allocate new D-NNN)
83
+ # - failure → cleared
84
+ jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
85
+ .pipeline = null
86
+ | .dispatch.id = null
87
+ | .sprint = { number: 0, status: "init", retry_count: 0 }
88
+ | .current_agent = null
89
+ | .agent_status = "pending"
90
+ | .completed_agents = []
91
+ | .next_agent = "dispatcher"
92
+ | .failure = { agent: null, location: null, message: null, retry_target: null }
93
+ | (.artifacts // {}) as $a
94
+ | .artifacts = ($a | with_entries(
95
+ if (.value | type) == "object"
96
+ then .value = { status: "pending", updated_by: null, updated_at: null }
97
+ else .
98
+ end))
99
+ | .updated_at = $now
100
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
101
+
102
+ # ── Clear handoff so next session starts fresh ──
103
+ HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
104
+ echo '{}' > "$HANDOFF"
105
+
106
+ echo " Status : archived, progress reset"
107
+ echo ""
108
+ echo " ✓ 다음 요청은 새로운 dispatch 로 시작합니다."
109
+ echo ""
@@ -11,6 +11,7 @@ set -uo pipefail
11
11
 
12
12
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
13
  source "$SCRIPT_DIR/lib/harness-render-progress.sh"
14
+ source "$SCRIPT_DIR/lib/harness-keywait.sh"
14
15
 
15
16
  PROJECT_ROOT="${1:-}"
16
17
  if [ -z "$PROJECT_ROOT" ]; then
@@ -420,5 +421,6 @@ while true; do
420
421
  # from any previous frame (fixes wrapped shell-prompt bleed-through).
421
422
  printf '%s\n' "$buf" | awk '{printf "%s\033[K\n", $0}'
422
423
  tput ed 2>/dev/null
423
- sleep 3
424
+ printf "${DIM} [r] refresh [q] quit${RESET}\033[K\n"
425
+ wait_or_refresh 3 || true
424
426
  done
@@ -7,6 +7,7 @@ set -uo pipefail
7
7
 
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  source "$SCRIPT_DIR/lib/harness-render-progress.sh"
10
+ source "$SCRIPT_DIR/lib/harness-keywait.sh"
10
11
 
11
12
  PROJECT_ROOT="${1:-}"
12
13
  if [ -z "$PROJECT_ROOT" ]; then
@@ -181,7 +182,11 @@ while true; do
181
182
  sig=$(compute_signature)
182
183
  if [ "$sig" != "$LAST_SIG" ]; then
183
184
  render
185
+ printf "${DIM} [r] refresh [q] quit${RESET}\033[K\n"
184
186
  LAST_SIG="$sig"
185
187
  fi
186
- sleep "$REFRESH_SEC"
188
+ if ! wait_or_refresh "$REFRESH_SEC"; then
189
+ # 'r' 키 → 캐시 무효화해서 다음 루프에서 강제 렌더
190
+ LAST_SIG=""
191
+ fi
187
192
  done
@@ -9,6 +9,7 @@
9
9
  set -uo pipefail
10
10
 
11
11
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ source "$SCRIPT_DIR/lib/harness-keywait.sh"
12
13
 
13
14
  # ── Args ──
14
15
  # Usage: harness-monitor.sh [project-root] [--team N]
@@ -382,6 +383,7 @@ while true; do
382
383
  check_transitions
383
384
  fi
384
385
 
385
- sleep 3
386
+ printf "${DIM} [r] refresh [q] quit${RESET}\033[K\n"
387
+ wait_or_refresh 3 || true
386
388
  done
387
389
 
@@ -54,7 +54,7 @@ if [ -f "$PIPELINE_JSON" ]; then
54
54
  fe_target=$(jq -r '.fe_target // empty' "$PIPELINE_JSON" 2>/dev/null || true)
55
55
  if [ -z "$fe_target" ]; then
56
56
  # pipeline.json 에 fe_target 미지정 시 config.json 의 _default_target 사용
57
- fe_target=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}._default_target // \"web\"" "$CONFIG" 2>/dev/null || echo "web")
57
+ fe_target="web" # v5.6.5+: 치환 로직 제거. fe_target 은 pipeline.json 에서 명시하거나 web 기본.
58
58
  fi
59
59
  fi
60
60
 
@@ -499,19 +499,29 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
499
499
 
500
500
  elif [ "$next_agent" = "archive" ]; then
501
501
  audit_log "system" "archive" "start" "sprint-${sprint_num}" "sprint cycle complete"
502
- jq -n \
503
- --arg from "${current_agent:-evaluator}" \
504
- --argjson sprint "$sprint_num" \
505
- --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
506
- '{
507
- from: $from,
508
- to: "archive",
509
- prompt: "Sprint 문서를 아카이브하세요.\n.harness/actions/의 스프린트 문서를 .harness/archive/sprint-NNN/으로 이동합니다.\n.harness/handoff.json을 읽고 sprint 번호를 확인하세요.",
510
- sprint: $sprint,
511
- model: "opus",
512
- thinking_mode: null,
513
- timestamp: $timestamp
514
- }' > "$HANDOFF"
502
+
503
+ # ── Auto-archive: run archive script synchronously ──
504
+ # harness-archive.sh moves .harness/actions/* to .harness/archive/D-NNN/S-NNN/
505
+ # and resets progress.json. On next user prompt the flow starts fresh as a new dispatch.
506
+ if bash "$SCRIPT_DIR/harness-archive.sh" "$PROJECT_ROOT"; then
507
+ audit_log "system" "archive" "complete" "sprint-${sprint_num}" "auto-archived"
508
+ else
509
+ echo " ⚠ archive script failed — falling back to manual handoff" >&2
510
+ audit_log "system" "archive" "fail" "sprint-${sprint_num}" "script failed"
511
+ jq -n \
512
+ --arg from "${current_agent:-evaluator}" \
513
+ --argjson sprint "$sprint_num" \
514
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
515
+ '{
516
+ from: $from,
517
+ to: "archive",
518
+ prompt: "Sprint 문서를 아카이브하세요. harness-archive.sh 가 실패했습니다 — 수동으로 .harness/actions/* 를 .harness/archive/D-NNN/S-NNN/ 로 이동하세요.",
519
+ sprint: $sprint,
520
+ model: "opus",
521
+ thinking_mode: null,
522
+ timestamp: $timestamp
523
+ }' > "$HANDOFF"
524
+ fi
515
525
 
516
526
  else
517
527
  echo '{}' > "$HANDOFF"
@@ -7,6 +7,7 @@ set -uo pipefail
7
7
 
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  source "$SCRIPT_DIR/lib/harness-render-progress.sh"
10
+ source "$SCRIPT_DIR/lib/harness-keywait.sh"
10
11
 
11
12
  PROJECT_ROOT="${1:-}"
12
13
  if [ -z "$PROJECT_ROOT" ]; then
@@ -158,5 +159,6 @@ while true; do
158
159
  tput cup 0 0 2>/dev/null
159
160
  echo "$buf"
160
161
  tput ed 2>/dev/null
161
- sleep 3
162
+ printf "${DIM} [r] refresh [q] quit${RESET}\033[K\n"
163
+ wait_or_refresh 3 || true
162
164
  done
@@ -487,6 +487,68 @@ cmd_idle_slots() {
487
487
  }' "$QUEUE"
488
488
  }
489
489
 
490
+ # ── Rate-Limit Hold checkpoint (v5.6.7+) ──
491
+ CHECKPOINT="$PROJECT_ROOT/.harness/actions/team-checkpoint.json"
492
+
493
+ cmd_hold() {
494
+ # Args: <reason> [retry_seconds=600]
495
+ local reason="${1:-rate_limit}"
496
+ local retry_secs="${2:-600}"
497
+ local now_iso retry_iso
498
+ now_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
499
+ retry_iso="$(date -u -v+"${retry_secs}"S +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
500
+ || date -u -d "+${retry_secs} seconds" +%Y-%m-%dT%H:%M:%SZ)"
501
+ acquire_queue_lock
502
+ trap release_queue_lock EXIT
503
+ local in_progress pending
504
+ in_progress="$(jq '[.teams[] | select(.current_feature != null) |
505
+ {team: .team_id, feature: .current_feature, phase: (.current_phase // "gen")}]' "$QUEUE")"
506
+ pending="$(jq '[.features[] | select(.status == "ready") | .id]' "$QUEUE")"
507
+ local tmp="${CHECKPOINT}.tmp"
508
+ jq -n --arg ts "$now_iso" --arg retry "$retry_iso" --arg reason "$reason" \
509
+ --argjson inprog "$in_progress" --argjson ready "$pending" \
510
+ '{timestamp:$ts, retry_at:$retry, last_error:$reason,
511
+ in_progress:$inprog, ready_features:$ready,
512
+ hold_count: 1, escalate_after: 72}' > "$tmp" && mv "$tmp" "$CHECKPOINT"
513
+ release_queue_lock
514
+ trap - EXIT
515
+ echo "[hold] checkpoint written — retry_at=$retry_iso reason=$reason"
516
+ }
517
+
518
+ cmd_resume_probe() {
519
+ # Lightweight probe — called on ScheduleWakeup resume.
520
+ # Exits 0 if ready to resume, 1 if still held, 2 if escalated.
521
+ [ ! -f "$CHECKPOINT" ] && { echo "[resume] no checkpoint — nothing to resume"; exit 0; }
522
+ local hold_count escalate_after
523
+ hold_count="$(jq -r '.hold_count // 1' "$CHECKPOINT")"
524
+ escalate_after="$(jq -r '.escalate_after // 72' "$CHECKPOINT")"
525
+ if [ "$hold_count" -ge "$escalate_after" ]; then
526
+ echo "[resume] ESCALATED — held $hold_count cycles (max=$escalate_after). Manual intervention required."
527
+ exit 2
528
+ fi
529
+ # Probe: minimal claude call — if rate-limited, exits non-zero quickly.
530
+ if command -v claude >/dev/null 2>&1; then
531
+ if echo "ping" | timeout 30 claude -p "reply only: pong" >/dev/null 2>&1; then
532
+ echo "[resume] probe OK — clearing hold"
533
+ rm -f "$CHECKPOINT"
534
+ exit 0
535
+ fi
536
+ fi
537
+ # Still held — increment and report
538
+ local tmp="${CHECKPOINT}.tmp"
539
+ jq '.hold_count = (.hold_count + 1) | .last_probe_at = now | todate' "$CHECKPOINT" > "$tmp" && mv "$tmp" "$CHECKPOINT"
540
+ echo "[resume] still held (cycle $((hold_count+1))/$escalate_after) — schedule another wake-up"
541
+ exit 1
542
+ }
543
+
544
+ cmd_hold_status() {
545
+ if [ ! -f "$CHECKPOINT" ]; then
546
+ echo '{"held":false}'
547
+ return
548
+ fi
549
+ jq '. + {held:true}' "$CHECKPOINT"
550
+ }
551
+
490
552
  # ── Dispatch ──
491
553
  case "$CMD" in
492
554
  init) cmd_init "$@" ;;
@@ -500,8 +562,11 @@ case "$CMD" in
500
562
  recover) cmd_recover ;;
501
563
  next-sprint) cmd_next_sprint ;;
502
564
  status) cmd_status ;;
565
+ hold) cmd_hold "$@" ;;
566
+ resume-probe) cmd_resume_probe ;;
567
+ hold-status) cmd_hold_status ;;
503
568
  *)
504
- echo "Usage: harness-queue-manager.sh <init|dequeue|auto-dispatch|idle-slots|pass|fail|requeue|recover|next-sprint|update_phase|status> [args]"
569
+ echo "Usage: harness-queue-manager.sh <init|dequeue|auto-dispatch|idle-slots|pass|fail|requeue|recover|next-sprint|update_phase|status|hold|resume-probe|hold-status> [args]"
505
570
  exit 1
506
571
  ;;
507
572
  esac
@@ -29,9 +29,10 @@ get_allowed_paths() {
29
29
  echo "tsconfig"
30
30
  echo "docker-compose"
31
31
  ;;
32
- generator-frontend|generator-frontend-flutter)
32
+ generator-frontend)
33
33
  echo "apps/web/"
34
34
  echo "apps/flutter/"
35
+ echo "lib/"
35
36
  echo ".harness/actions/sprint-contract.md"
36
37
  echo ".harness/actions/feature-list.json"
37
38
  echo ".harness/progress.json"
@@ -43,7 +44,7 @@ get_allowed_paths() {
43
44
  echo ".harness/actions/evaluation-code-quality.md"
44
45
  echo ".harness/progress.json"
45
46
  ;;
46
- evaluator-functional|evaluator-functional-flutter)
47
+ evaluator-functional)
47
48
  echo ".harness/actions/evaluation-functional.md"
48
49
  echo ".harness/progress.json"
49
50
  echo "tests/"
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # harness-keywait.sh — 패널 refresh/quit 키 대기 헬퍼
3
+ #
4
+ # 사용법:
5
+ # source scripts/lib/harness-keywait.sh
6
+ # while true; do
7
+ # render_panel
8
+ # echo " [r] refresh [q] quit"
9
+ # if wait_or_refresh 3; then
10
+ # # 타임아웃 → 일반 주기 리프레시
11
+ # :
12
+ # else
13
+ # # 'r' 키 즉시 리프레시 (반환값 != 0)
14
+ # :
15
+ # fi
16
+ # done
17
+ #
18
+ # 키 처리:
19
+ # 'r' / 'R' / Enter → 즉시 복귀 (return 1 — 강제 리프레시 신호)
20
+ # 'q' / 'Q' → exit 0 (패널 종료)
21
+ # 그 외 / 타임아웃 → return 0 (정상 주기 리프레시)
22
+ #
23
+ # TTY 가 아니면 plain sleep 으로 동작 (Claude Code Bash 도구 등에서도 안전).
24
+
25
+ wait_or_refresh() {
26
+ local secs="${1:-3}"
27
+
28
+ # Non-TTY fallback
29
+ if ! [ -t 0 ]; then
30
+ sleep "$secs"
31
+ return 0
32
+ fi
33
+
34
+ local key=""
35
+ # -t: timeout, -n 1: 1글자, -s: 에코 안함
36
+ # read 는 Enter 에서도 타임아웃 전에 즉시 복귀 (빈 키)
37
+ if IFS= read -rsn 1 -t "$secs" key 2>/dev/null; then
38
+ case "$key" in
39
+ q|Q)
40
+ # 커서 복원 후 종료
41
+ tput cnorm 2>/dev/null || true
42
+ clear
43
+ exit 0
44
+ ;;
45
+ r|R|"")
46
+ # 강제 리프레시 시그널
47
+ return 1
48
+ ;;
49
+ *)
50
+ # 기타 키는 무시하고 일반 복귀
51
+ return 0
52
+ ;;
53
+ esac
54
+ fi
55
+ # 타임아웃
56
+ return 0
57
+ }
@@ -311,7 +311,7 @@ render_agent_bar() {
311
311
  fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
312
312
  fe_target=$(jq -r '.fe_target // empty' "$PIPELINE_JSON" 2>/dev/null || true)
313
313
  if [ -z "$fe_target" ]; then
314
- fe_target=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}._default_target // \"web\"" "$CONFIG" 2>/dev/null || echo "web")
314
+ fe_target="web" # v5.6.5+: 치환 로직 제거. 기본 web.
315
315
  fi
316
316
  fi
317
317
 
@@ -325,14 +325,7 @@ render_agent_bar() {
325
325
 
326
326
  while IFS= read -r agent; do
327
327
  agent=$(echo "$agent" | sed 's/:.*//') # strip mode suffix like :light, :api-only
328
-
329
- # fe_stack + fe_target 치환 적용
330
- if [ "$fe_stack" = "flutter" ]; then
331
- local sub
332
- sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter.by_target[\"${fe_target}\"][\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
333
- if [ "$sub" = "__skip__" ]; then continue; fi
334
- agent="$sub"
335
- fi
328
+ # v5.6.5+: fe_stack 에이전트 치환 제거. 스택별 동작은 ref-docs 로 조절.
336
329
 
337
330
  if [ "$first" = true ]; then
338
331
  first=false
@@ -42,6 +42,18 @@ jq '.agent_status = "completed" | .completed_agents += ["planner"]' .harness/p
42
42
  - Gotcha 교정 후 재작업 → `failure.retry_target` (해당 에이전트)
43
43
  - `pipeline` → 선택된 파이프라인 (FULLSTACK/FE-ONLY/BE-ONLY)
44
44
  - `sprint.number` → `1`, `sprint.status` → `"in_progress"` (신규 파이프라인인 경우에만)
45
+ - **신규 파이프라인인 경우** `dispatch.id` 가 `null` 이면 counter 를 올리고 새 ID 를 발급 (v5.7+):
46
+ ```bash
47
+ # dispatch.id 가 이미 있으면 기존 dispatch 유지, 없으면 새로 발급
48
+ cur=$(jq -r '.dispatch.id // ""' .harness/progress.json)
49
+ if [ -z "$cur" ]; then
50
+ next=$(jq -r '((.dispatch.counter // 0) + 1)' .harness/progress.json)
51
+ new_id=$(printf 'D-%03d' "$next")
52
+ bash scripts/harness-progress-set.sh . \
53
+ ".dispatch.counter = $next | .dispatch.id = \"$new_id\""
54
+ fi
55
+ ```
56
+ 아카이빙 후 `dispatch.id` 는 `null` 로 리셋되므로, 다음 dispatcher 실행 시 새 D-NNN 이 할당된다.
45
57
  2. `.harness/progress.log`에 요약 한 줄 추가
46
58
  3. **STOP. 다음 에이전트를 직접 호출하지 않는다.**
47
59
  4. 출력: `"✓ Dispatcher 완료. bash scripts/harness-next.sh 실행하여 다음 단계 확인."`
@@ -147,7 +159,7 @@ AGENTS.md 비하네스 → 기존 백업 + 리빌드
147
159
  - FULLSTACK / FE-ONLY: `["evaluator-code-quality", "evaluator-functional", "evaluator-visual"]`
148
160
  - BE-ONLY: `["evaluator-code-quality", "evaluator-functional"]` (functional 은 api-only 모드)
149
161
 
150
- Flutter 치환 규칙은 chain 배열의 각 원소에도 동일 적용. `fe_stack == "flutter"` + `fe_target in (mobile, desktop)` 이면 `evaluator-visual` 을 chain 에서 제거하고 `evaluator-functional` → `evaluator-functional-flutter` 로 치환한다. `evaluator-code-quality` 는 스택/타겟 무관 공통.
162
+ 스택 특성(예: Flutter mobile 에서 Visual skip) 이 필요하면 해당 스택 ref-docs (`.harness/ref/fe-<stack>.md`) 의 `validation.visual.enabled` 를 false 로 두면 evaluator-visual 이 MANUAL_REQUIRED 로 우아하게 우회한다. 별도 치환 에이전트는 사용하지 않는다.
151
163
 
152
164
  ### Evaluator 체인 라우팅 규칙
153
165
 
@@ -163,11 +175,9 @@ Gotcha retry 시에도 체인 시작점은 chain[0] 부터 재실행.
163
175
 
164
176
  FE-ONLY 또는 FULLSTACK 선택 시, `pipeline.json`에 **`fe_stack`** 필드를 포함해야 한다:
165
177
 
166
- - `scan-result.json.tech_stack.fe_stack` 값을 기본으로 사용 (`react` | `flutter`)
178
+ - `scan-result.json.tech_stack.fe_stack` 값을 기본으로 사용 (예: `react`, `nextjs`, `vue`, `flutter`, `swift` 등)
167
179
  - 값이 없거나 불명확하면 Planner가 확정하도록 위임 (Dispatcher는 `"unknown"` 기록 + `notes` 에 메모)
168
- - Flutter 선택 시 `agents_active`/`agents_skipped`에 치환된 에이전트명을 기록
169
- - active: `generator-frontend-flutter`, `evaluator-functional-flutter`
170
- - skipped: `generator-frontend`, `evaluator-functional`, `evaluator-visual`
180
+ - **에이전트 이름 치환은 하지 않는다** (v5.6.5+). 모든 FE 스택은 공통 `generator-frontend` / `evaluator-functional` / `evaluator-visual` 을 사용하고, 스택 특성은 `.harness/ref/fe-<stack>.md` (adaptive ref-docs) 에서 로드한다.
171
181
 
172
182
  ## 6. Brainstormer Routing Decision
173
183
 
@@ -222,7 +232,7 @@ Planner 를 호출해야 한다고 판단되면, **사용자에게 단 하나의
222
232
  | 상황 | next_agent |
223
233
  |------|-----------|
224
234
  | "Eval, X 다시 검증해" | `evaluator-functional` (또는 `evaluator-visual`) |
225
- | "Generator-FE, Y 버그 고쳐" | `generator-frontend` (또는 Flutter 변형) |
235
+ | "Generator-FE, Y 버그 고쳐" | `generator-frontend` |
226
236
  | "Generator-BE, API 재생성해" | `generator-backend` |
227
237
  | Eval FAIL → retry | `failure.retry_target` |
228
238
  | Gotcha 수정 | `failure.retry_target` 또는 현재 에이전트 |
@@ -240,14 +250,8 @@ Planner 를 호출해야 한다고 판단되면, **사용자에게 단 하나의
240
250
  이 경우 기존 `.harness/actions/brainstorm-spec.md` 는 Brainstormer 의 On Start 에서
241
251
  `.harness/archive/brainstorm-spec-<timestamp>.md` 로 백업된다.
242
252
 
243
- ## 7. Handoff 라우팅 (fe_stack 반영)
253
+ ## 7. Handoff 라우팅
244
254
 
245
- Dispatcher가 `next_agent` 를 세팅할 때 pipeline.json.fe_stack 을 참조해 치환:
255
+ Dispatcher 가 `next_agent` 를 세팅할 때 **스택별 에이전트 이름 치환은 하지 않는다** (v5.6.5+). 모든 FE 스택이 공통 `generator-frontend` / `evaluator-functional` / `evaluator-visual` 을 사용하고, 스택 특성은 adaptive ref-docs(`.harness/ref/fe-<stack>.md`) 에서 로드한다.
246
256
 
247
- | 원본 next_agent | fe_stack=react | fe_stack=flutter |
248
- |-----------------|----------------|------------------|
249
- | generator-frontend | generator-frontend | generator-frontend-flutter |
250
- | evaluator-functional (FE 단계) | evaluator-functional | evaluator-functional-flutter |
251
- | evaluator-visual | evaluator-visual | (skip → 다음 단계로 이동) |
252
-
253
- **Brainstormer 는 fe_stack 치환 대상이 아니다** — 언어/스택 무관 공통 에이전트.
257
+ 예외적 스킵 규칙은 ref-docs 의 `validation.visual.enabled` 플래그로 제어 — false 면 evaluator-visual 이 MANUAL_REQUIRED 로 우아하게 우회한다 (별도 에이전트 이름 변경 없음).
@@ -28,35 +28,19 @@ docmeta:
28
28
 
29
29
  # Pipeline Definitions
30
30
 
31
- > **FE Stack 차원**: 모든 FE 관련 파이프라인은 `fe_stack` 필드로 React/Flutter를 구분한다.
31
+ > **FE Stack 차원**: 모든 FE 관련 파이프라인은 `fe_stack` 필드로 스택을 기록한다.
32
32
  > Planner가 scan-result.json(`tech_stack.fe_stack`) 또는 사용자 질문으로 확정한다.
33
33
 
34
- ## fe_stack + fe_target 스위치 매트릭스
34
+ ## 단일 에이전트 체인 (v5.6.5+)
35
35
 
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** |
36
+ **에이전트 이름 치환은 하지 않는다**. 모든 FE 스택(React, Next.js, Vue, Svelte, Flutter, Swift, 기타) 은 공통 `generator-frontend` / `evaluator-functional` / `evaluator-visual` 을 사용하며, 스택 특성(runner, paths, API, validation) 은 **adaptive ref-docs** (`.harness/ref/fe-<stack>.md`) 에서 로드한다.
42
37
 
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 됨이 전제.
38
+ | 스택별 스킵/우회 | 방법 |
39
+ |------------------|------|
40
+ | Visual 렌더 검증이 불가능한 네이티브 모바일/데스크톱 | ref-docs 의 `validation.visual.enabled = false` — `evaluator-visual` 이 MANUAL_REQUIRED 로 우아하게 우회 |
41
+ | Playwright 대신 스택 네이티브 E2E (예: XCUITest, Flutter `flutter_driver`) | ref-docs 의 `validation.functional_tests` 에 해당 명령 나열 — `evaluator-functional` 이 로드 실행 |
46
42
 
47
- Dispatcher는 `next_agent`를 설정할 때 `pipeline.json` 의 `fe_stack` + `fe_target` 두 값을 보고 치환한다:
48
-
49
- ```
50
- if pipeline.json.fe_stack == "flutter":
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__
59
- ```
43
+ Dispatcher 는 `fe_stack` 을 `pipeline.json` 에 기록만 하고, 에이전트 이름은 변경하지 않는다. Generator/Evaluator 는 세션 시작 시 스스로 ref-docs 를 로드해 스택에 맞는 동작을 한다.
60
44
 
61
45
  ## Evaluator Chain (공통)
62
46
 
@@ -83,10 +67,10 @@ agents:
83
67
  - planner (light):
84
68
  skip: MSA 서비스 설계, BE 기능 목록
85
69
  do: OpenAPI → api-contract.json 변환, FE 컴포넌트 설계, feature-list (layer: frontend만), fe_stack 확정
86
- - generator-frontend OR generator-frontend-flutter # fe_stack에 따라
70
+ - generator-frontend # 모든 스택 공통 (ref-docs 로드)
87
71
  - evaluator-code-quality # 공통 — 브라우저 없음
88
- - evaluator-functional OR evaluator-functional-flutter
89
- - evaluator-visual # fe_stack == "flutter" + fe_target in (mobile,desktop) 이면 SKIP
72
+ - evaluator-functional # 모든 스택 공통
73
+ - evaluator-visual # ref.validation.visual.enabled=false 면 MANUAL_REQUIRED 로 우회
90
74
  evaluator_chain:
91
75
  - evaluator-code-quality
92
76
  - evaluator-functional
@@ -95,9 +79,9 @@ skip:
95
79
  - generator-backend
96
80
  notes:
97
81
  - api-contract.json은 OpenAPI에서 파생 (Planner가 변환)
98
- - Eval-Func(React)의 API Health Check는 외부 서버 대상
82
+ - Eval-Func 의 API Health Check는 외부 서버 대상 (ref.api.base_url)
99
83
  - AGENTS.md IA-MAP에 BE 경로 없음 (외부 서버)
100
- - fe_stack=flutter (mobile/desktop) 인 경우 evaluator-visual 생략
84
+ - 네이티브 모바일/데스크톱 스택은 ref-docs 에서 visual.enabled=false 로 설정
101
85
  ```
102
86
 
103
87
  ## BE-ONLY
@@ -76,14 +76,15 @@ jq '.agent_status = "completed" | .completed_agents += ["planner"]' .harness/p
76
76
 
77
77
  ## fe_stack (FE 파이프라인 분기)
78
78
 
79
- `pipeline.json.fe_stack`은 FE Generator/Evaluator 선택을 결정한다:
79
+ `pipeline.json.fe_stack` 은 스택 기록용 메타데이터이며, **에이전트 이름 치환에는 사용되지 않는다** (v5.6.5+). 모든 스택이 공통 `generator-frontend` / `evaluator-functional` / `evaluator-visual` 을 사용하고, 스택별 동작(runner, paths, API, validation) 은 adaptive ref-docs(`.harness/ref/fe-<stack>.md`) 에서 로드한다.
80
80
 
81
- | 값 | FE Generator | FE Evaluator | 비고 |
82
- |----|--------------|--------------|------|
83
- | `react` (기본) | `generator-frontend` | `evaluator-functional` + `evaluator-visual` | Vercel/Next.js/Tailwind |
84
- | `flutter` | `generator-frontend-flutter` | `evaluator-functional-flutter` | Riverpod + integrated_data_layer, Eval-Visual 생략 |
81
+ | 스택 예 | ref-docs 경로 | 비고 |
82
+ |---------|--------------|------|
83
+ | `react`, `nextjs` | `.harness/ref/fe-react.md`, `.harness/ref/fe-nextjs.md` | Vercel/Next.js/Tailwind |
84
+ | `flutter` | `.harness/ref/fe-flutter.md` | Riverpod · validation.visual.enabled=false (모바일/데스크톱) |
85
+ | `vue`, `svelte`, `swift` 등 | `.harness/ref/fe-<stack>.md` | 각 스택 관례 |
85
86
 
86
- **Planner는 `pipeline.json`에 `fe_stack`을 반드시 기록해야 한다.** Dispatcher가 이 값으로 `next_agent`를 라우팅한다.
87
+ **Planner는 `pipeline.json`에 `fe_stack`을 반드시 기록해야 한다.** Generator/Evaluator 는 세션 시작 시 이 값으로 올바른 ref-docs 를 로드한다.
87
88
 
88
89
  ## Process
89
90
 
@@ -107,72 +107,58 @@ Flutter 프로젝트의 타깃을 확인합니다:
107
107
  이번 스프린트의 타깃은? (A/B/C)
108
108
  ```
109
109
 
110
- ### fe_target → Eval 흐름
110
+ ### fe_target 과 Eval 흐름 (v5.6.5+)
111
111
 
112
- | fe_target | Generator | Eval-Functional | Eval-Visual |
113
- |-----------|-----------|----------------|-------------|
114
- | `web` | `generator-frontend-flutter` | `evaluator-functional` (Playwright!) | `evaluator-visual` (Playwright!) |
115
- | `mobile` | `generator-frontend-flutter` | `evaluator-functional-flutter` (정적 분석) | SKIP |
116
- | `desktop` | `generator-frontend-flutter` | `evaluator-functional-flutter` (정적 분석) | SKIP |
112
+ 에이전트 이름 치환은 하지 않는다. 모든 스택이 공통 `generator-frontend` / `evaluator-functional` / `evaluator-visual` 을 사용하고, 스택 특성은 `.harness/ref/fe-<stack>.md` (adaptive ref-docs) 의 `validation` 블록으로 조절된다.
117
113
 
118
- **핵심**: Flutter Web 의 빌드 결과물(HTML/JS/CSS)은 일반 웹앱과 동일하므로 Playwright 기반
119
- React 경로의 evaluator 를 그대로 재사용한다. Generator-Frontend-Flutter 의 Self-Verification
120
- 단계에서 `flutter analyze`/`flutter test`/`flutter build web` 정적 검증이 이미 통과한 상태로 handoff 된다.
114
+ | fe_target | 전략 | 방법 |
115
+ |-----------|------|------|
116
+ | `web` (Flutter Web, Next.js 등) | 일반 Playwright 평가 | ref.validation.visual.enabled = true |
117
+ | `mobile` (iOS/Android 네이티브) | Visual 은 MANUAL_REQUIRED | ref.validation.visual.enabled = false + functional_tests 에 스택 네이티브 명령 |
118
+ | `desktop` (macOS/Windows/Linux 네이티브) | Visual MANUAL_REQUIRED | 동일 |
119
+
120
+ Generator 는 `ref.runner.dev_command` / `ref.paths.*` 를 로드해 스택별 빌드·테스트·구조를 따른다.
121
121
 
122
122
  ## 3. pipeline.json 갱신
123
123
 
124
- `fe_stack` + `fe_target` 확정 후 `pipeline.json`에 반드시 추가:
124
+ `fe_stack` + `fe_target` 확정 후 `pipeline.json` 에 기록:
125
125
 
126
126
  ```json
127
127
  {
128
128
  "pipeline": "FULLSTACK",
129
129
  "planner_mode": "full",
130
130
  "fe_stack": "flutter",
131
- "fe_target": "web",
131
+ "fe_target": "mobile",
132
132
  "agents_active": [
133
133
  "planner",
134
134
  "generator-backend",
135
- "generator-frontend-flutter",
136
- "evaluator-functional",
137
- "evaluator-visual"
138
- ],
139
- "agents_skipped": [
140
135
  "generator-frontend",
141
- "evaluator-functional-flutter"
136
+ "evaluator-code-quality",
137
+ "evaluator-functional"
142
138
  ],
143
- "evaluator_mode": "playwright-web",
144
- "notes": "Flutter Web — 컴파일 결과가 HTML+JS+CSS 이므로 React 경로의 Playwright evaluator 사용. Generator 의 Self-Verification 에서 flutter analyze/test 통과 전제."
139
+ "agents_skipped": [],
140
+ "evaluator_mode": "native",
141
+ "notes": "Flutter mobile — ref.validation.visual.enabled=false, evaluator-visual 은 MANUAL_REQUIRED 로 우회."
145
142
  }
146
143
  ```
147
144
 
148
145
  ### fe_stack + fe_target → 파이프라인 매핑
149
146
 
150
- | pipeline | fe_stack | fe_target | agents_active 예시 |
151
- |----------|----------|-----------|-------------------|
152
- | FULLSTACK | react | (n/a) | planner, generator-backend, generator-frontend, evaluator-functional, evaluator-visual |
153
- | FULLSTACK | flutter | **web** | planner, generator-backend, generator-frontend-flutter, evaluator-functional, evaluator-visual |
154
- | FULLSTACK | flutter | mobile | planner, generator-backend, generator-frontend-flutter, evaluator-functional-flutter |
155
- | FULLSTACK | flutter | desktop | planner, generator-backend, generator-frontend-flutter, evaluator-functional-flutter |
156
- | FE-ONLY | react | (n/a) | planner, generator-frontend, evaluator-functional, evaluator-visual |
157
- | FE-ONLY | flutter | **web** | planner, generator-frontend-flutter, evaluator-functional, evaluator-visual |
158
- | FE-ONLY | flutter | mobile | planner, generator-frontend-flutter, evaluator-functional-flutter |
159
- | BE-ONLY | (무관) | (n/a) | planner, generator-backend, evaluator-functional |
160
-
161
- ## 4. Flutter 선택 시 추가 작업
162
-
163
- Flutter로 확정되면 Planner는:
164
-
165
- 1. **AGENTS.md IA-MAP** 의 `[FE]` 섹션을 Flutter 구조로 바꿔야 한다.
166
- - `apps/web/` → `lib/ui/pages/`, `lib/ui/component/`
167
- - `libs/shared-dto/` 대신 `integrated_data_layer/` 경로 등록
168
- - 소유자: `→ Generator-Frontend-Flutter`
147
+ | pipeline | 에이전트 체인 (스택 무관) | ref-docs 로 조절되는 부분 |
148
+ |----------|--------------------------|---------------------------|
149
+ | FULLSTACK | planner → gen-be → gen-fe → eval-code-quality → eval-func → eval-visual | Gen/Eval 모두 ref.* 로드 |
150
+ | FE-ONLY | planner(light) → gen-fe → eval-code-quality → eval-func → eval-visual | 동일 |
151
+ | BE-ONLY | planner → gen-be → eval-code-quality → eval-func | visual 체인 없음 |
169
152
 
170
- 2. **api-contract.json** 은 언어 중립적이어야 한다 — Planner는 TypeScript 타입이 아니라 **스키마 JSON** 으로만 작성. Flutter Generator가 Retrofit/JsonSerializable로 변환한다.
153
+ ## 4. Flutter 등 네이티브 스택 선택 시 추가 작업
171
154
 
172
- 3. **feature-list.json** 의 `layer: "frontend"` feature들은 `fe_stack: "flutter"` 태그를 달아서 Evaluator가 구분하도록 한다.
155
+ 1. **AGENTS.md IA-MAP** 의 `[FE]` 섹션에 해당 스택 구조 반영 (예: Flutter 면 `lib/ui/pages/`, `integrated_data_layer/` 등).
156
+ 2. **api-contract.json** 은 언어 중립 스키마 JSON — Generator 가 스택 타입으로 변환.
157
+ 3. **feature-list.json** 의 `layer: "frontend"` feature 에 `fe_stack` 태그 유지 (필터링 용도).
158
+ 4. **`.harness/ref/fe-<stack>.md`** 의 `validation` 블록을 스택에 맞게 채워 두기 — 이게 실제 동작을 바꾸는 유일한 지점.
173
159
 
174
160
  ## 5. 금지
175
161
 
176
162
  - 파이프라인 실행 도중 `fe_stack` 변경 금지 — 스프린트 경계에서만 가능
177
- - React/Flutter 코드 혼재 생성 금지 — Generator는 하나의 stack만 담당
163
+ - 서로 다른 스택 코드 혼재 생성 금지 — Generator 는 하나의 stack만 담당
178
164
  - 사용자가 명시적으로 한 스택을 지시했는데 감지 결과로 다른 스택을 강제하지 말 것