@walwal-harness/cli 4.0.0-beta.4 → 4.0.0-beta.6
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/bin/init.js +27 -63
- package/package.json +1 -1
- package/scripts/harness-studio-setup.sh +89 -34
- package/skills/team-action/SKILL.md +17 -3
package/bin/init.js
CHANGED
|
@@ -225,25 +225,24 @@ function installSkills() {
|
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
// harness- 프리픽스 스킬 전체 삭제 후 재복사 — 잔류 방지
|
|
229
|
+
if (fs.existsSync(CLAUDE_SKILLS_DIR)) {
|
|
230
|
+
const existing = fs.readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true });
|
|
231
|
+
for (const entry of existing) {
|
|
232
|
+
if (entry.isDirectory() && entry.name.startsWith('harness-')) {
|
|
233
|
+
fs.rmSync(path.join(CLAUDE_SKILLS_DIR, entry.name), { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
log(' Cleared existing harness-* skills');
|
|
237
|
+
}
|
|
238
|
+
|
|
228
239
|
const skills = fs.readdirSync(skillsSrc, { withFileTypes: true })
|
|
229
240
|
.filter(d => d.isDirectory())
|
|
230
241
|
.map(d => d.name);
|
|
231
242
|
|
|
232
|
-
// Remove obsolete skills (cleaned up in v4)
|
|
233
|
-
const obsoleteSkills = ['harness-generator-frontend-flutter', 'harness-evaluator-functional-flutter', 'harness-team'];
|
|
234
|
-
for (const obs of obsoleteSkills) {
|
|
235
|
-
const obsPath = path.join(CLAUDE_SKILLS_DIR, obs);
|
|
236
|
-
if (fs.existsSync(obsPath)) {
|
|
237
|
-
fs.rmSync(obsPath, { recursive: true, force: true });
|
|
238
|
-
log(` Removed obsolete: ${obs}`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
243
|
for (const skill of skills) {
|
|
243
244
|
const src = path.join(skillsSrc, skill);
|
|
244
245
|
const dest = path.join(CLAUDE_SKILLS_DIR, `harness-${skill}`);
|
|
245
|
-
|
|
246
|
-
// Skills are ALWAYS overwritten — they are harness-managed, not user-editable
|
|
247
246
|
copyDir(src, dest);
|
|
248
247
|
log(` Installed: harness-${skill}`);
|
|
249
248
|
}
|
|
@@ -260,63 +259,28 @@ function installScripts() {
|
|
|
260
259
|
const scriptsSrc = path.join(PKG_ROOT, 'scripts');
|
|
261
260
|
const scriptsDest = path.join(PROJECT_ROOT, 'scripts');
|
|
262
261
|
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
'
|
|
267
|
-
'harness-prompts-v4.sh',
|
|
268
|
-
'harness-team-worker.sh',
|
|
269
|
-
];
|
|
270
|
-
for (const obs of obsoleteScripts) {
|
|
271
|
-
const obsPath = path.join(scriptsDest, obs);
|
|
272
|
-
if (fs.existsSync(obsPath)) {
|
|
273
|
-
fs.unlinkSync(obsPath);
|
|
274
|
-
log(` Removed obsolete: ${obs}`);
|
|
275
|
-
}
|
|
262
|
+
// 전체 삭제 후 재복사 — 버전 간 잔류 파일 방지
|
|
263
|
+
if (fs.existsSync(scriptsDest)) {
|
|
264
|
+
fs.rmSync(scriptsDest, { recursive: true, force: true });
|
|
265
|
+
log(' Cleared existing scripts/');
|
|
276
266
|
}
|
|
277
267
|
|
|
278
|
-
// Core scripts are ALWAYS overwritten on update (not user-editable)
|
|
279
|
-
// These contain harness logic that must stay in sync with the CLI version
|
|
280
|
-
const coreScripts = new Set([
|
|
281
|
-
'harness-next.sh',
|
|
282
|
-
'harness-session-start.sh',
|
|
283
|
-
'harness-statusline.sh',
|
|
284
|
-
'harness-user-prompt-submit.sh',
|
|
285
|
-
'harness-dashboard.sh',
|
|
286
|
-
'harness-monitor.sh',
|
|
287
|
-
'harness-eval-watcher.sh',
|
|
288
|
-
'harness-tmux.sh',
|
|
289
|
-
'harness-control.sh',
|
|
290
|
-
'harness-dashboard-v4.sh',
|
|
291
|
-
'harness-queue-manager.sh',
|
|
292
|
-
]);
|
|
293
|
-
|
|
294
268
|
if (fs.existsSync(scriptsSrc)) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
for
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (f.endsWith('.sh')) {
|
|
307
|
-
fs.chmodSync(path.join(destPath, f), '755');
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
} catch (e) {}
|
|
311
|
-
} else {
|
|
312
|
-
// Core scripts: always overwrite. Others: skip if exists (unless --force)
|
|
313
|
-
const isCore = coreScripts.has(entry.name);
|
|
314
|
-
if (isCore || !fileExists(destPath) || isForce) {
|
|
315
|
-
copyFile(srcPath, destPath);
|
|
316
|
-
try { fs.chmodSync(destPath, '755'); } catch (e) {}
|
|
269
|
+
copyDir(scriptsSrc, scriptsDest);
|
|
270
|
+
|
|
271
|
+
// chmod +x for all .sh files (recursive)
|
|
272
|
+
function chmodRecursive(dir) {
|
|
273
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
const full = path.join(dir, entry.name);
|
|
276
|
+
if (entry.isDirectory()) {
|
|
277
|
+
chmodRecursive(full);
|
|
278
|
+
} else if (entry.name.endsWith('.sh')) {
|
|
279
|
+
try { fs.chmodSync(full, '755'); } catch (e) {}
|
|
317
280
|
}
|
|
318
281
|
}
|
|
319
282
|
}
|
|
283
|
+
chmodRecursive(scriptsDest);
|
|
320
284
|
}
|
|
321
285
|
|
|
322
286
|
log('Scripts installation complete');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.6",
|
|
4
4
|
"description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer (requirements refinement). Supports React and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-studio-setup.sh — Claude
|
|
3
|
-
#
|
|
4
|
-
# Claude가 이미 실행 중인 tmux pane에서 호출됨.
|
|
5
|
-
# 현재 pane(Claude가 있는 곳)을 Left로 유지하고,
|
|
6
|
-
# Center(Dashboard + History)와 Right(Monitor)를 split으로 생성.
|
|
2
|
+
# harness-studio-setup.sh — Claude 세션에서 3-column 레이아웃 자동 구축
|
|
7
3
|
#
|
|
8
4
|
# ┌──────────────┬──────────────┬──────────────┐
|
|
9
5
|
# │ │ Dashboard │ │
|
|
10
|
-
# │
|
|
11
|
-
# │
|
|
6
|
+
# │ Claude │ (v4 queue) │ Team Monitor│
|
|
7
|
+
# │ (Lead) ├──────────────┤ (lifecycle) │
|
|
12
8
|
# │ │ Command │ │
|
|
13
9
|
# │ │ History │ │
|
|
14
10
|
# └──────────────┴──────────────┴──────────────┘
|
|
15
11
|
#
|
|
16
|
-
#
|
|
12
|
+
# 두 가지 상황을 모두 처리:
|
|
13
|
+
# A) tmux 안에서 실행 → 현재 pane을 split하여 레이아웃 구축
|
|
14
|
+
# B) tmux 밖에서 실행 → 새 tmux 세션 생성, Claude를 좌측 pane에서 재실행
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
17
|
# bash scripts/harness-studio-setup.sh [project-root]
|
|
18
18
|
#
|
|
19
19
|
# 이미 구축됐으면 skip (멱등성 보장)
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
set -euo pipefail
|
|
22
22
|
|
|
23
23
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
24
|
+
SESSION_NAME="harness-studio"
|
|
24
25
|
|
|
25
26
|
PROJECT_ROOT="${1:-}"
|
|
26
27
|
if [ -z "$PROJECT_ROOT" ]; then
|
|
@@ -36,53 +37,107 @@ if [ -z "$PROJECT_ROOT" ] || [ ! -d "$PROJECT_ROOT/.harness" ]; then
|
|
|
36
37
|
exit 1
|
|
37
38
|
fi
|
|
38
39
|
|
|
39
|
-
# ──
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
# ── Resolve Claude command ──
|
|
41
|
+
CLAUDE_CMD="claude --dangerously-skip-permissions"
|
|
42
|
+
if [ -f "$PROJECT_ROOT/.harness/handoff.json" ]; then
|
|
43
|
+
_model=$(jq -r '.model // empty' "$PROJECT_ROOT/.harness/handoff.json" 2>/dev/null)
|
|
44
|
+
if [ -n "$_model" ] && [ "$_model" != "null" ]; then
|
|
45
|
+
CLAUDE_CMD="$CLAUDE_CMD --model $_model"
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# ══════════════════════════════════════════
|
|
50
|
+
# Case A: 이미 tmux 안에 있음 → pane split
|
|
51
|
+
# ══════════════════════════════════════════
|
|
52
|
+
if [ -n "${TMUX:-}" ]; then
|
|
53
|
+
# 멱등성: 이미 pane이 3개 이상이면 skip
|
|
54
|
+
PANE_COUNT=$(tmux list-panes | wc -l | tr -d ' ')
|
|
55
|
+
if [ "$PANE_COUNT" -ge 3 ]; then
|
|
56
|
+
echo "[studio] Layout already set up ($PANE_COUNT panes). Skipping."
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
PANE_CLAUDE=$(tmux display-message -p '#{pane_id}')
|
|
61
|
+
echo "[studio] Setting up 3-column layout (in-place split)..."
|
|
62
|
+
|
|
63
|
+
# Left 35% | Right 65%
|
|
64
|
+
PANE_MID=$(tmux split-window -h -p 65 -t "$PANE_CLAUDE" -c "$PROJECT_ROOT" \
|
|
65
|
+
-P -F '#{pane_id}' \
|
|
66
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
67
|
+
|
|
68
|
+
# Middle 45% | Right 55%
|
|
69
|
+
PANE_RIGHT=$(tmux split-window -h -p 55 -t "$PANE_MID" -c "$PROJECT_ROOT" \
|
|
70
|
+
-P -F '#{pane_id}' \
|
|
71
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\"'")
|
|
72
|
+
|
|
73
|
+
# Dashboard top 45% | History bottom 55%
|
|
74
|
+
PANE_HISTORY=$(tmux split-window -v -p 55 -t "$PANE_MID" -c "$PROJECT_ROOT" \
|
|
75
|
+
-P -F '#{pane_id}' \
|
|
76
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"'")
|
|
77
|
+
|
|
78
|
+
# Pane titles
|
|
79
|
+
tmux select-pane -t "$PANE_CLAUDE" -T "Lead (Claude)"
|
|
80
|
+
tmux select-pane -t "$PANE_MID" -T "Dashboard"
|
|
81
|
+
tmux select-pane -t "$PANE_HISTORY" -T "Command History"
|
|
82
|
+
tmux select-pane -t "$PANE_RIGHT" -T "Team Monitor"
|
|
83
|
+
|
|
84
|
+
tmux set-option pane-border-status top 2>/dev/null || true
|
|
85
|
+
tmux set-option pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
86
|
+
|
|
87
|
+
# 포커스를 Claude pane으로 복귀
|
|
88
|
+
tmux select-pane -t "$PANE_CLAUDE"
|
|
89
|
+
|
|
90
|
+
echo "[studio] Layout ready (in-place)."
|
|
91
|
+
exit 0
|
|
44
92
|
fi
|
|
45
93
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
94
|
+
# ══════════════════════════════════════════
|
|
95
|
+
# Case B: tmux 밖에 있음 → 새 세션 생성
|
|
96
|
+
# ══════════════════════════════════════════
|
|
97
|
+
|
|
98
|
+
# 이미 세션이 있으면 attach만
|
|
99
|
+
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
100
|
+
echo "[studio] Session '$SESSION_NAME' already exists. Attaching..."
|
|
101
|
+
echo "[studio] ATTACH_TMUX=$SESSION_NAME"
|
|
50
102
|
exit 0
|
|
51
103
|
fi
|
|
52
104
|
|
|
53
|
-
|
|
54
|
-
PANE_CLAUDE=$(tmux display-message -p '#{pane_id}')
|
|
105
|
+
echo "[studio] Creating new tmux session with 3-column layout..."
|
|
55
106
|
|
|
56
|
-
|
|
107
|
+
# 1. 새 세션 → Left pane (Claude 실행)
|
|
108
|
+
PANE_MAIN=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 \
|
|
109
|
+
-P -F '#{pane_id}')
|
|
57
110
|
|
|
58
|
-
#
|
|
59
|
-
PANE_MID=$(tmux split-window -h -p 65 -t "$
|
|
111
|
+
# 2. Left 35% | Right 65%
|
|
112
|
+
PANE_MID=$(tmux split-window -h -p 65 -t "$PANE_MAIN" -c "$PROJECT_ROOT" \
|
|
60
113
|
-P -F '#{pane_id}' \
|
|
61
114
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
62
115
|
|
|
63
|
-
#
|
|
116
|
+
# 3. Middle 45% | Right 55%
|
|
64
117
|
PANE_RIGHT=$(tmux split-window -h -p 55 -t "$PANE_MID" -c "$PROJECT_ROOT" \
|
|
65
118
|
-P -F '#{pane_id}' \
|
|
66
119
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-monitor.sh\" \"${PROJECT_ROOT}\"'")
|
|
67
120
|
|
|
68
|
-
#
|
|
121
|
+
# 4. Dashboard top 45% | History bottom 55%
|
|
69
122
|
PANE_HISTORY=$(tmux split-window -v -p 55 -t "$PANE_MID" -c "$PROJECT_ROOT" \
|
|
70
123
|
-P -F '#{pane_id}' \
|
|
71
124
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompt-history.sh\" \"${PROJECT_ROOT}\"'")
|
|
72
125
|
|
|
73
|
-
#
|
|
74
|
-
tmux
|
|
126
|
+
# 5. Left pane에서 Claude 자동 실행
|
|
127
|
+
tmux send-keys -t "$PANE_MAIN" "unset npm_config_prefix 2>/dev/null" Enter
|
|
128
|
+
tmux send-keys -t "$PANE_MAIN" "clear && $CLAUDE_CMD" Enter
|
|
129
|
+
|
|
130
|
+
# Pane titles
|
|
131
|
+
tmux select-pane -t "$PANE_MAIN" -T "Lead (Claude)"
|
|
75
132
|
tmux select-pane -t "$PANE_MID" -T "Dashboard"
|
|
76
133
|
tmux select-pane -t "$PANE_HISTORY" -T "Command History"
|
|
77
134
|
tmux select-pane -t "$PANE_RIGHT" -T "Team Monitor"
|
|
78
135
|
|
|
79
|
-
tmux set-option pane-border-status top 2>/dev/null || true
|
|
80
|
-
tmux set-option pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
136
|
+
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
137
|
+
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
81
138
|
|
|
82
|
-
#
|
|
83
|
-
tmux select-pane -t "$
|
|
139
|
+
# Focus on Claude pane
|
|
140
|
+
tmux select-pane -t "$PANE_MAIN"
|
|
84
141
|
|
|
85
|
-
echo "[studio]
|
|
86
|
-
echo "[studio]
|
|
87
|
-
echo "[studio] Center : Dashboard + Command History"
|
|
88
|
-
echo "[studio] Right : Team Monitor"
|
|
142
|
+
echo "[studio] Session '$SESSION_NAME' created."
|
|
143
|
+
echo "[studio] ATTACH_TMUX=$SESSION_NAME"
|
|
@@ -8,14 +8,28 @@ disable-model-invocation: false
|
|
|
8
8
|
|
|
9
9
|
## Step 0: Studio 레이아웃 자동 구축
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
3-column 대시보드 레이아웃을 자동 구축합니다:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
bash scripts/harness-studio-setup.sh .
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
스크립트 출력을 확인합니다:
|
|
18
|
+
|
|
19
|
+
- **`Layout ready`** → 현재 터미널에 split 완료. 바로 Step 1로.
|
|
20
|
+
- **`ATTACH_TMUX=harness-studio`** → 새 tmux 세션이 생성됨 (tmux 밖에서 실행한 경우).
|
|
21
|
+
사용자에게 아래 안내를 출력하고 **STOP**합니다:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Studio 레이아웃이 준비되었습니다!
|
|
25
|
+
다른 터미널에서 아래 명령을 실행하세요:
|
|
26
|
+
|
|
27
|
+
tmux attach -t harness-studio
|
|
28
|
+
|
|
29
|
+
새 창에서 Claude가 자동 실행됩니다. 거기서 "팀 가동"을 입력하면 Teams가 시작됩니다.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **`already set up`** → 이미 구축됨. 바로 Step 1로.
|
|
19
33
|
|
|
20
34
|
## Step 1: Queue 초기화
|
|
21
35
|
|