@walwal-harness/cli 5.2.0 → 5.3.1

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,3 +1,17 @@
1
+ ---
2
+ docmeta:
3
+ id: harness-team
4
+ title: /harness-team — Team Mode 시작/재개
5
+ type: input
6
+ createdAt: 2026-04-20T00:00:00Z
7
+ updatedAt: 2026-04-20T00:00:00Z
8
+ source:
9
+ producer: user
10
+ skillId: harness
11
+ inputs: []
12
+ tags: [harness, team-mode, tmux, command]
13
+ ---
14
+
1
15
  # /harness-team — Team Mode 시작/재개
2
16
 
3
17
  Planner가 완료한 feature-list.json의 피처들을 최대 3개 팀이 병렬로 Gen→Eval 사이클을 수행합니다.
@@ -40,9 +54,11 @@ bash scripts/harness-queue-manager.sh status .
40
54
  ### Step 2: tmux Studio 레이아웃 구축
41
55
 
42
56
  ```bash
43
- bash scripts/harness-tmux.sh --team
57
+ bash scripts/harness-tmux.sh --team --force-tmux
44
58
  ```
45
59
 
60
+ **`--force-tmux` 필수**: iTerm2 감지 경로는 백그라운드에 iTerm2가 떠 있기만 해도 활성화되어 AppleScript 실패 시 팀 레이아웃이 조용히 사라짐. Team Mode는 항상 tmux로 강제하여 재현 가능한 레이아웃을 보장.
61
+
46
62
  ### Step 3: 초기 Worker 생성
47
63
 
48
64
  Queue에서 ready 피처를 최대 3개 dequeue하고, **각각 background Agent로 생성**합니다.
@@ -162,18 +178,28 @@ LOG="$HARNESS_ROOT/.harness/progress.log"
162
178
  logev() { echo "$(date +'%Y-%m-%d %H:%M') | team-{N} | $1 | $2" >> "$LOG"; }
163
179
  ```
164
180
 
165
- | ACTION | 사용 시점 | DETAIL 예시 |
166
- |--------|-----------|-------------|
167
- | `gen-start` | Gen Phase 시작 | `F-001 start — 6 AC` |
168
- | `gen-read` | 소스/계약 읽기 | `read api-contract.json (POST /users)` |
169
- | `gen-write` | 파일 생성/수정 | `write apps/service-user/src/user.controller.ts` |
170
- | `gen-test` | 자체 게이트 | `tsc OK · eslint 0 · jest 12/12` |
171
- | `gen-done` | Gen Phase 종료 | `F-001 done — 5 files, 142 LOC` |
172
- | `eval-start` | Evaluator 시작 | `F-001 spawning evaluator` |
173
- | `eval-check` | AC 검증 | `AC-3 — verify POST /users returns 201` |
174
- | `eval-done` | Eval 결과 | `verdict=PASS score=2.95` |
175
- | `result` | PASS 확정 | `F-001 PASS` |
176
- | `fail` | FAIL 확정 | `FAIL #1 — AC-2 missing` |
181
+ | ACTION | 사용 시점 | DETAIL 예시 (필수 포함 정보) |
182
+ |--------|-----------|------------------------------|
183
+ | `gen-start` | Gen Phase 시작 (1회) | `F-001 "사용자 회원가입 API" start — goal=POST /users, 6 AC` — **Feature 제목+목표** 포함 필수 |
184
+ | `gen-plan` | 작업 계획 공표 (1회) | `plan: create controller+service+dto, wire module, add 3 unit tests` |
185
+ | `gen-read` | 소스/계약 읽기 (매 파일) | `read api-contract.json#/paths/~1users` |
186
+ | `gen-write` | 파일 생성/수정 (**매 파일**) | `write apps/service-user/src/user.controller.ts (+82 LOC, create)` — **경로+LOC+action(create/edit/delete)** 필수 |
187
+ | `gen-test` | 자체 게이트 | `tsc OK · eslint 0 warn · jest 12/12 pass` |
188
+ | `gen-done` | Gen Phase 종료 | `F-001 done — 5 files: controller.ts, service.ts, dto.ts, module.ts, spec.ts (total +142 LOC)` — **변경 파일 전체 나열** 필수 |
189
+ | `eval-start` | Evaluator 시작 | `F-001 evaluating — 6 ACs + regression + security` — **AC 개수+검증 축** 필수 |
190
+ | `eval-ac` | AC 본문 선언 (**매 AC 시작 시 1회**) | `AC-3: "POST /users returns 201 with created user id"` — **AC 원문** 필수 |
191
+ | `eval-check` | AC 검증 수행/증거 | `AC-3 [PASS] — curl POST /users → 201, body.id matches` — **판정+증거** 필수 |
192
+ | `eval-gate` | 자동 게이트 | `gate: tsc OK, eslint OK, security scan 0 high` |
193
+ | `eval-done` | Eval 결과 | `F-001 VERDICT=PASS SCORE=2.95/3.00 (AC 6/6, gates OK)` |
194
+ | `result` | PASS 확정 (**SCORE ≥ 2.80**) | `F-001 PASS score=2.95` |
195
+ | `fail` | FAIL 확정 | `F-001 FAIL #1 — AC-2 "email uniqueness" missing DB constraint` |
196
+
197
+ **중요: 로깅 가독성 규칙 (필수)**
198
+ 1. `gen-start`에는 반드시 Feature **제목과 목표**를 함께 기록 (무슨 일을 시작하는가 명확히).
199
+ 2. `gen-write`는 **변경되는 파일마다 1건씩** 기록. 묶어서 요약 금지 ("2 files edit" 같은 표기 금지).
200
+ 3. `gen-done`은 **변경 파일 전체 목록**을 나열. "(2 files)" 같은 개수만 기록 금지.
201
+ 4. `eval-ac`로 **AC 원문을 먼저 선언**한 뒤 `eval-check`로 증거/판정 기록. "AC-1 count=0" 같은 수치 단독 기록 금지.
202
+ 5. `result` PASS는 **SCORE ≥ 2.80** 인 경우에만 기록. score=1.00인데 PASS로 기록하는 실수 금지.
177
203
 
178
204
  **queue phase 업데이트:**
179
205
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "5.2.0",
3
+ "version": "5.3.1",
4
4
  "description": "Production harness for AI agent engineering — Solo/Team mode, Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer. Supports React, Next.js, and Flutter FE stacks.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"
@@ -0,0 +1,187 @@
1
+ #!/bin/bash
2
+ # harness-gotcha-memory.sh — Gotcha & Memory pane (index view)
3
+ # 활성 에이전트의 gotcha를 최상단으로, 나머지는 요약(최근 3항목+총개수)으로 표시
4
+ # Usage: bash scripts/harness-gotcha-memory.sh [project-root]
5
+
6
+ set -uo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
10
+
11
+ PROJECT_ROOT="${1:-}"
12
+ if [ -z "$PROJECT_ROOT" ]; then
13
+ PROJECT_ROOT="$(resolve_harness_root ".")" || { echo "[gotcha] .harness/ not found."; exit 1; }
14
+ fi
15
+
16
+ MEMORY_FILE="$PROJECT_ROOT/.harness/memory.md"
17
+ GOTCHAS_DIR="$PROJECT_ROOT/.harness/gotchas"
18
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
19
+
20
+ BOLD="\033[1m"
21
+ DIM="\033[2m"
22
+ GREEN="\033[32m"
23
+ YELLOW="\033[33m"
24
+ RED="\033[31m"
25
+ CYAN="\033[36m"
26
+ MAGENTA="\033[35m"
27
+ RESET="\033[0m"
28
+ BG_YELLOW="\033[43m"
29
+
30
+ REFRESH_SEC="${HARNESS_REFRESH:-5}"
31
+ RECENT_N="${HARNESS_GOTCHA_RECENT:-3}"
32
+
33
+ # 렌더 캐시 — 내용이 바뀌지 않으면 redraw 스킵
34
+ LAST_SIG=""
35
+
36
+ # 입력 시그니처: gotchas + memory + 활성 에이전트의 mtime/size 해시
37
+ compute_signature() {
38
+ {
39
+ [ -f "$PROGRESS" ] && stat -f '%m %z' "$PROGRESS" 2>/dev/null
40
+ [ -f "$MEMORY_FILE" ] && stat -f '%m %z' "$MEMORY_FILE" 2>/dev/null
41
+ if [ -d "$GOTCHAS_DIR" ]; then
42
+ for f in "$GOTCHAS_DIR"/*.md; do
43
+ [ -e "$f" ] || continue
44
+ stat -f '%N %m %z' "$f" 2>/dev/null
45
+ done
46
+ fi
47
+ } | md5 -q 2>/dev/null || echo "$RANDOM"
48
+ }
49
+
50
+ # 활성 에이전트 결정: current_agent > next_agent
51
+ get_active_agent() {
52
+ [ -f "$PROGRESS" ] || { echo ""; return; }
53
+ command -v jq &>/dev/null || { echo ""; return; }
54
+ local cur nxt
55
+ cur=$(jq -r '.current_agent // ""' "$PROGRESS" 2>/dev/null)
56
+ nxt=$(jq -r '.next_agent // ""' "$PROGRESS" 2>/dev/null)
57
+ if [ -n "$cur" ] && [ "$cur" != "null" ] && [ "$cur" != "none" ]; then
58
+ echo "$cur"
59
+ elif [ -n "$nxt" ] && [ "$nxt" != "null" ] && [ "$nxt" != "none" ]; then
60
+ echo "$nxt"
61
+ else
62
+ echo ""
63
+ fi
64
+ }
65
+
66
+ # gotcha 파일에서 '### ' 헤딩 개수 = 항목 총수
67
+ count_items() {
68
+ local f="$1"
69
+ local n
70
+ n=$(grep -c '^### ' "$f" 2>/dev/null)
71
+ echo "${n:-0}"
72
+ }
73
+
74
+ # 최근 N개 항목 제목 추출 (### 헤딩 — 파일 하단부가 최신이라 tail)
75
+ recent_titles() {
76
+ local f="$1" n="$2"
77
+ grep '^### ' "$f" 2>/dev/null | tail -n "$n" | sed 's/^### //'
78
+ }
79
+
80
+ # agent 이름 → 파일 basename 매핑 (정확/접두 일치)
81
+ file_for_agent() {
82
+ local agent="$1"
83
+ local f="$GOTCHAS_DIR/${agent}.md"
84
+ [ -f "$f" ] && { echo "$f"; return; }
85
+ # 접두 일치 (예: current_agent=generator → generator-backend/frontend 중 하나)
86
+ for cand in "$GOTCHAS_DIR"/*.md; do
87
+ [ -e "$cand" ] || continue
88
+ local base
89
+ base=$(basename "$cand" .md)
90
+ [ "$base" = "README" ] && continue
91
+ case "$base" in
92
+ "$agent"*|*"$agent"*) echo "$cand"; return ;;
93
+ esac
94
+ done
95
+ echo ""
96
+ }
97
+
98
+ render_gotcha_entry() {
99
+ local f="$1" active="$2"
100
+ local name count
101
+ name=$(basename "$f" .md)
102
+ count=$(count_items "$f")
103
+ if [ "$active" = "true" ]; then
104
+ echo -e "${BOLD}${BG_YELLOW} ▸ ${name} ${RESET} ${DIM}(${count} items · ACTIVE)${RESET}"
105
+ else
106
+ echo -e "${BOLD}${GREEN}▸ ${name}${RESET} ${DIM}(${count} items)${RESET}"
107
+ fi
108
+ if [ "$count" -eq 0 ]; then
109
+ echo -e " ${DIM}(항목 없음)${RESET}"
110
+ else
111
+ recent_titles "$f" "$RECENT_N" | while IFS= read -r t; do
112
+ echo -e " ${DIM}·${RESET} $t"
113
+ done
114
+ fi
115
+ }
116
+
117
+ render_memory_summary() {
118
+ if [ ! -f "$MEMORY_FILE" ]; then
119
+ echo -e "${DIM}(memory.md 없음)${RESET}"
120
+ return
121
+ fi
122
+ local count
123
+ count=$(grep -c '^### ' "$MEMORY_FILE" 2>/dev/null)
124
+ count="${count:-0}"
125
+ echo -e "${BOLD}${CYAN}▣ SHARED MEMORY${RESET} ${DIM}(${count} items · memory.md)${RESET}"
126
+ if [ "$count" -eq 0 ]; then
127
+ echo -e " ${DIM}(항목 없음)${RESET}"
128
+ else
129
+ grep '^### ' "$MEMORY_FILE" | tail -n "$RECENT_N" | sed 's/^### //' | while IFS= read -r t; do
130
+ echo -e " ${DIM}·${RESET} $t"
131
+ done
132
+ fi
133
+ }
134
+
135
+ render() {
136
+ # clear 대신 커서 홈 + 화면 끝까지 지우기 — 깜빡임 최소화
137
+ printf '\033[H\033[2J\033[3J'
138
+ local cols hr
139
+ cols=$(tput cols 2>/dev/null || echo 80)
140
+ hr=$(printf '─%.0s' $(seq 1 "$cols"))
141
+
142
+ local active active_file
143
+ active=$(get_active_agent)
144
+ active_file=""
145
+ [ -n "$active" ] && active_file=$(file_for_agent "$active")
146
+
147
+ echo -e "${BOLD}${MAGENTA}GOTCHA INDEX${RESET} ${DIM}$(date +%H:%M:%S) · recent=${RECENT_N} · refresh ${REFRESH_SEC}s${RESET}"
148
+ if [ -n "$active" ]; then
149
+ echo -e "${DIM}active agent:${RESET} ${BOLD}${YELLOW}${active}${RESET}"
150
+ else
151
+ echo -e "${DIM}active agent: (none)${RESET}"
152
+ fi
153
+ echo -e "${DIM}${hr}${RESET}"
154
+
155
+ # 활성 에이전트 gotcha 최상단
156
+ if [ -n "$active_file" ]; then
157
+ render_gotcha_entry "$active_file" "true"
158
+ echo ""
159
+ fi
160
+
161
+ # 나머지 gotcha
162
+ if [ -d "$GOTCHAS_DIR" ]; then
163
+ for f in "$GOTCHAS_DIR"/*.md; do
164
+ [ -e "$f" ] || continue
165
+ [ "$f" = "$active_file" ] && continue
166
+ local base
167
+ base=$(basename "$f" .md)
168
+ [ "$base" = "README" ] && continue
169
+ render_gotcha_entry "$f" "false"
170
+ done
171
+ fi
172
+
173
+ echo ""
174
+ echo -e "${DIM}${hr}${RESET}"
175
+ render_memory_summary
176
+ }
177
+
178
+ trap 'exit 0' INT TERM
179
+
180
+ while true; do
181
+ sig=$(compute_signature)
182
+ if [ "$sig" != "$LAST_SIG" ]; then
183
+ render
184
+ LAST_SIG="$sig"
185
+ fi
186
+ sleep "$REFRESH_SEC"
187
+ done
@@ -4,10 +4,10 @@
4
4
  # Team Mode (--team):
5
5
  # ┌──────────────┬──────────────┬──────────────┐
6
6
  # │ │ │ TEAM 1 │
7
- # │ Prompt │ Dashboard │ Gen | Eval │
8
- # │ History │ (queue + ├──────────────┤
9
- # │ │ status + │ TEAM 2 │
10
- # │ │ archive) │ Gen | Eval │
7
+ # │ Dashboard │ Gotcha & │ │
8
+ # │ (queue + │ Memory ├──────────────┤
9
+ # │ status + │ │ TEAM 2 │
10
+ # │ archive) │ │ │
11
11
  # │ │ ├──────────────┤
12
12
  # │ │ │ TEAM 3 │
13
13
  # └──────────────┴──────────────┴──────────────┘
@@ -20,6 +20,7 @@
20
20
  # Usage:
21
21
  # bash scripts/harness-tmux.sh [project-root] --team
22
22
  # bash scripts/harness-tmux.sh [project-root] --solo
23
+ # bash scripts/harness-tmux.sh [project-root] --team --force-tmux # skip iTerm2, use tmux
23
24
  # bash scripts/harness-tmux.sh --kill
24
25
 
25
26
  set -euo pipefail
@@ -30,12 +31,14 @@ SESSION_NAME="harness-studio"
30
31
  PROJECT_ROOT=""
31
32
  MODE=""
32
33
  DETACH=false
34
+ FORCE_TMUX=false
33
35
 
34
36
  for arg in "$@"; do
35
37
  case "$arg" in
36
38
  --team) MODE="team" ;;
37
39
  --solo) MODE="solo" ;;
38
40
  --detach) DETACH=true ;;
41
+ --force-tmux) FORCE_TMUX=true ;;
39
42
  --kill)
40
43
  tmux kill-session -t "$SESSION_NAME" 2>/dev/null && echo "tmux session killed." || true
41
44
  # Also try to close iTerm2 studio tab if exists
@@ -99,12 +102,12 @@ launch_iterm2_team() {
99
102
  tell studioWindow
100
103
  tell current session of current tab
101
104
  set name to "harness-studio"
102
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-prompt-history.sh' '${PROJECT_ROOT}'"
105
+ write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-dashboard.sh' '${PROJECT_ROOT}'"
103
106
 
104
- -- Split right → Dashboard
107
+ -- Split right → Gotcha & Memory
105
108
  set dashPane to (split vertically with default profile)
106
109
  tell dashPane
107
- write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-dashboard.sh' '${PROJECT_ROOT}'"
110
+ write text "cd '${PROJECT_ROOT}' && bash '${SCRIPT_DIR}/harness-gotcha-memory.sh' '${PROJECT_ROOT}'"
108
111
 
109
112
  -- Split right → Team 1
110
113
  set t1Pane to (split vertically with default profile)
@@ -171,24 +174,24 @@ APPLESCRIPT
171
174
  launch_tmux_team() {
172
175
  tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
173
176
 
174
- PANE_LEFT=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 -P -F '#{pane_id}')
175
- PANE_MID=$(tmux split-window -h -p 70 -t "$PANE_LEFT" -c "$PROJECT_ROOT" -P -F '#{pane_id}')
176
- PANE_T1=$(tmux split-window -h -p 60 -t "$PANE_MID" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
177
+ PANE_DASH=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 -P -F '#{pane_id}')
178
+ PANE_GOTCHA=$(tmux split-window -h -p 70 -t "$PANE_DASH" -c "$PROJECT_ROOT" -P -F '#{pane_id}')
179
+ PANE_T1=$(tmux split-window -h -p 60 -t "$PANE_GOTCHA" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
177
180
  "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 1'")
178
181
  PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
179
182
  "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 2'")
180
183
  PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" -P -F '#{pane_id}' \
181
184
  "bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\" --team 3'")
182
185
 
183
- tmux send-keys -t "$PANE_LEFT" "bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"" Enter
184
- tmux send-keys -t "$PANE_MID" "bash \"${SCRIPT_DIR}/harness-dashboard.sh\" \"${PROJECT_ROOT}\"" Enter
186
+ tmux send-keys -t "$PANE_DASH" "bash \"${SCRIPT_DIR}/harness-dashboard.sh\" \"${PROJECT_ROOT}\"" Enter
187
+ tmux send-keys -t "$PANE_GOTCHA" "bash \"${SCRIPT_DIR}/harness-gotcha-memory.sh\" \"${PROJECT_ROOT}\"" Enter
185
188
 
186
- tmux select-pane -t "$PANE_LEFT" -T "Prompt History"
187
- tmux select-pane -t "$PANE_MID" -T "Dashboard"
188
- tmux select-pane -t "$PANE_T1" -T "TEAM 1"
189
- tmux select-pane -t "$PANE_T2" -T "TEAM 2"
190
- tmux select-pane -t "$PANE_T3" -T "TEAM 3"
191
- tmux select-pane -t "$PANE_LEFT"
189
+ tmux select-pane -t "$PANE_DASH" -T "Dashboard"
190
+ tmux select-pane -t "$PANE_GOTCHA" -T "Gotcha & Memory"
191
+ tmux select-pane -t "$PANE_T1" -T "TEAM 1"
192
+ tmux select-pane -t "$PANE_T2" -T "TEAM 2"
193
+ tmux select-pane -t "$PANE_T3" -T "TEAM 3"
194
+ tmux select-pane -t "$PANE_DASH"
192
195
 
193
196
  tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
194
197
  tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
@@ -237,6 +240,15 @@ if command -v tmux &>/dev/null; then
237
240
  HAS_TMUX=true
238
241
  fi
239
242
 
243
+ # --force-tmux overrides iTerm2 detection
244
+ if [ "$FORCE_TMUX" = true ]; then
245
+ if [ "$HAS_TMUX" != true ]; then
246
+ echo "ERROR: --force-tmux specified but tmux not installed. Run: brew install tmux"
247
+ exit 1
248
+ fi
249
+ IS_ITERM=false
250
+ fi
251
+
240
252
  if [ "$IS_ITERM" = true ]; then
241
253
  echo "Strategy: iTerm2 native split panes"
242
254
  if [ "$MODE" = "team" ]; then