@walwal-harness/cli 2.0.0 → 2.3.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/assets/templates/config.json +41 -1
- package/bin/init.js +93 -7
- package/package.json +5 -2
- package/scripts/harness-next.sh +36 -1
- package/scripts/harness-user-prompt-submit.sh +106 -0
- package/scripts/lib/harness-render-progress.sh +15 -1
- package/scripts/scan-project.sh +24 -2
- package/skills/brainstorming/SKILL.md +200 -0
- package/skills/brainstorming/references/attribution.md +109 -0
- package/skills/brainstorming/references/spec-document-reviewer-prompt.md +49 -0
- package/skills/brainstorming/references/visual-companion.md +287 -0
- package/skills/brainstorming/scripts/frame-template.html +214 -0
- package/skills/brainstorming/scripts/helper.js +88 -0
- package/skills/brainstorming/scripts/server.cjs +354 -0
- package/skills/brainstorming/scripts/start-server.sh +148 -0
- package/skills/brainstorming/scripts/stop-server.sh +56 -0
- package/skills/dispatcher/SKILL.md +114 -2
- package/skills/dispatcher/references/pipeline-definitions.md +53 -5
- package/skills/evaluator-functional-flutter/SKILL.md +198 -0
- package/skills/evaluator-functional-flutter/references/ia-compliance.md +77 -0
- package/skills/evaluator-functional-flutter/references/scoring-rubric.md +132 -0
- package/skills/evaluator-functional-flutter/references/static-check-rules.md +99 -0
- package/skills/generator-frontend-flutter/SKILL.md +138 -0
- package/skills/generator-frontend-flutter/references/anti-patterns.md +288 -0
- package/skills/generator-frontend-flutter/references/api-layer-pattern.md +233 -0
- package/skills/generator-frontend-flutter/references/i18n-pattern.md +102 -0
- package/skills/generator-frontend-flutter/references/riverpod-pattern.md +199 -0
- package/skills/planner/SKILL.md +23 -1
- package/skills/planner/references/fe-stack-detection.md +131 -0
|
@@ -12,9 +12,20 @@
|
|
|
12
12
|
"outputs": ["actions/pipeline.json"],
|
|
13
13
|
"always_first": true
|
|
14
14
|
},
|
|
15
|
+
"brainstorming": {
|
|
16
|
+
"role": "사용자의 러프한 요구사항을 대화형으로 구체화하여 Planner가 바로 쓸 수 있는 brainstorm-spec.md로 변환. Dispatcher가 사용자에게 '브레인스토밍 필요?' 확인 후에만 호출되는 조건부 에이전트.",
|
|
17
|
+
"skill": "harness-brainstorming",
|
|
18
|
+
"inputs": ["(user conversation)"],
|
|
19
|
+
"outputs": ["actions/brainstorm-spec.md"],
|
|
20
|
+
"conditional": true,
|
|
21
|
+
"invoked_by": "dispatcher (user opt-in)",
|
|
22
|
+
"next_on_complete": "planner",
|
|
23
|
+
"attribution": "Derived from obra/superpowers skills/brainstorming (MIT License)"
|
|
24
|
+
},
|
|
15
25
|
"planner": {
|
|
16
26
|
"role": "제품 사양 + API 계약서 + 서비스 분할 설계",
|
|
17
27
|
"skill": "harness-planner",
|
|
28
|
+
"inputs": ["actions/brainstorm-spec.md (optional)"],
|
|
18
29
|
"outputs": [
|
|
19
30
|
"actions/plan.md",
|
|
20
31
|
"actions/feature-list.json",
|
|
@@ -48,16 +59,40 @@
|
|
|
48
59
|
"tools": ["playwright:browser_take_screenshot", "playwright:browser_resize", "playwright:browser_snapshot"],
|
|
49
60
|
"inputs": ["actions/sprint-contract.md", "actions/evaluation-functional.md"],
|
|
50
61
|
"outputs": ["actions/evaluation-visual.md"]
|
|
62
|
+
},
|
|
63
|
+
"generator-frontend-flutter": {
|
|
64
|
+
"role": "Flutter 앱 개발 — Riverpod, integrated_data_layer(Retrofit), i18n(ARB), build_runner",
|
|
65
|
+
"skill": "harness-generator-frontend-flutter",
|
|
66
|
+
"inputs": ["actions/plan.md", "actions/feature-list.json", "actions/api-contract.json", "actions/sprint-contract.md"],
|
|
67
|
+
"outputs": ["code:flutter", "actions/sprint-contract.md"],
|
|
68
|
+
"order": 2,
|
|
69
|
+
"fe_stack": "flutter"
|
|
70
|
+
},
|
|
71
|
+
"evaluator-functional-flutter": {
|
|
72
|
+
"role": "Flutter 앱 검증 — flutter analyze, flutter test, build_runner 일관성, 안티패턴 정적 검증",
|
|
73
|
+
"skill": "harness-evaluator-functional-flutter",
|
|
74
|
+
"tools": ["bash:flutter", "bash:dart"],
|
|
75
|
+
"inputs": ["actions/sprint-contract.md"],
|
|
76
|
+
"outputs": ["actions/evaluation-functional.md"],
|
|
77
|
+
"fe_stack": "flutter"
|
|
51
78
|
}
|
|
52
79
|
},
|
|
53
80
|
"flow": {
|
|
54
81
|
"sequence": ["dispatcher", "planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
55
82
|
"pipeline_selection": {
|
|
56
|
-
"comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정.
|
|
83
|
+
"comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정. fe_stack 필드로 React/Flutter를 구분. harness-next.sh가 pipeline.json.fe_stack 을 읽어 generator-frontend/evaluator-functional 을 Flutter 변형으로 치환한다.",
|
|
57
84
|
"pipelines": {
|
|
58
85
|
"FULLSTACK": ["planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
59
86
|
"FE-ONLY": ["planner:light", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
60
87
|
"BE-ONLY": ["planner", "generator-backend", "evaluator-functional:api-only"]
|
|
88
|
+
},
|
|
89
|
+
"fe_stack_substitution": {
|
|
90
|
+
"comment": "pipeline.json.fe_stack=flutter 이면 아래 치환을 적용. evaluator-visual은 skip.",
|
|
91
|
+
"flutter": {
|
|
92
|
+
"generator-frontend": "generator-frontend-flutter",
|
|
93
|
+
"evaluator-functional": "evaluator-functional-flutter",
|
|
94
|
+
"evaluator-visual": "__skip__"
|
|
95
|
+
}
|
|
61
96
|
}
|
|
62
97
|
},
|
|
63
98
|
"sprint_execution": {
|
|
@@ -75,6 +110,11 @@
|
|
|
75
110
|
"state_log": ".harness/progress.log",
|
|
76
111
|
"next_prompt_file": ".harness/next-prompt.txt"
|
|
77
112
|
},
|
|
113
|
+
"behavior": {
|
|
114
|
+
"comment": "하네스 동작 플래그. UserPromptSubmit 훅이 이 값을 읽어 자동 라우팅을 결정한다.",
|
|
115
|
+
"auto_route_dispatcher": true,
|
|
116
|
+
"auto_route_dispatcher_description": "true 이면 사용자의 모든 프롬프트가 harness-dispatcher 경유로 분류/라우팅 된다. 사용자가 'harness skip' 등을 말하면 단일 메시지 한정으로 건너뛴다."
|
|
117
|
+
},
|
|
78
118
|
"paths": {
|
|
79
119
|
"actions": ".harness/actions",
|
|
80
120
|
"archive": ".harness/archive",
|
package/bin/init.js
CHANGED
|
@@ -253,24 +253,102 @@ function installSessionHook() {
|
|
|
253
253
|
if (!settings.hooks) settings.hooks = {};
|
|
254
254
|
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
255
255
|
|
|
256
|
-
//
|
|
256
|
+
// Migrate any legacy flat entries (created by older walwal-harness versions)
|
|
257
|
+
// into the correct matcher + hooks-array shape that Claude Code expects.
|
|
257
258
|
const hookCmd = 'bash scripts/harness-session-start.sh';
|
|
259
|
+
let migrated = false;
|
|
260
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart
|
|
261
|
+
.map((entry) => {
|
|
262
|
+
if (entry && typeof entry === 'object' && Array.isArray(entry.hooks)) {
|
|
263
|
+
return entry; // already in correct shape
|
|
264
|
+
}
|
|
265
|
+
if (entry && typeof entry === 'object' && entry.type === 'command' && entry.command) {
|
|
266
|
+
migrated = true;
|
|
267
|
+
return { matcher: '', hooks: [{ type: entry.type, command: entry.command }] };
|
|
268
|
+
}
|
|
269
|
+
return entry;
|
|
270
|
+
})
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
|
|
273
|
+
// Check if our hook is already installed (inside any matcher group)
|
|
258
274
|
const alreadyInstalled = settings.hooks.SessionStart.some(
|
|
259
|
-
|
|
275
|
+
(entry) =>
|
|
276
|
+
entry &&
|
|
277
|
+
Array.isArray(entry.hooks) &&
|
|
278
|
+
entry.hooks.some(
|
|
279
|
+
(h) => h && h.command && h.command.includes('harness-session-start')
|
|
280
|
+
)
|
|
260
281
|
);
|
|
261
282
|
|
|
262
283
|
if (!alreadyInstalled) {
|
|
263
284
|
settings.hooks.SessionStart.push({
|
|
264
|
-
|
|
265
|
-
command: hookCmd
|
|
285
|
+
matcher: '',
|
|
286
|
+
hooks: [{ type: 'command', command: hookCmd }]
|
|
266
287
|
});
|
|
267
288
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
268
289
|
log('SessionStart hook installed in .claude/settings.json');
|
|
290
|
+
} else if (migrated) {
|
|
291
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
292
|
+
log('SessionStart hook migrated to matcher + hooks-array format');
|
|
269
293
|
} else {
|
|
270
294
|
log('SessionStart hook already installed');
|
|
271
295
|
}
|
|
272
296
|
}
|
|
273
297
|
|
|
298
|
+
// ─────────────────────────────────────────
|
|
299
|
+
// 3c. UserPromptSubmit hook (auto dispatcher routing)
|
|
300
|
+
// ─────────────────────────────────────────
|
|
301
|
+
function installUserPromptSubmitHook() {
|
|
302
|
+
log('Installing UserPromptSubmit hook (auto dispatcher routing)...');
|
|
303
|
+
|
|
304
|
+
const settingsDir = path.join(PROJECT_ROOT, '.claude');
|
|
305
|
+
const settingsFile = path.join(settingsDir, 'settings.json');
|
|
306
|
+
|
|
307
|
+
ensureDir(settingsDir);
|
|
308
|
+
|
|
309
|
+
let settings = {};
|
|
310
|
+
if (fileExists(settingsFile)) {
|
|
311
|
+
try {
|
|
312
|
+
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
313
|
+
} catch (e) {
|
|
314
|
+
log('WARNING: Could not parse existing .claude/settings.json, creating new');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!settings.hooks) settings.hooks = {};
|
|
319
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
320
|
+
|
|
321
|
+
const hookCmd = 'bash scripts/harness-user-prompt-submit.sh';
|
|
322
|
+
|
|
323
|
+
// Detect existing harness UserPromptSubmit hook (any shape)
|
|
324
|
+
const alreadyInstalled = settings.hooks.UserPromptSubmit.some((entry) => {
|
|
325
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
326
|
+
if (Array.isArray(entry.hooks)) {
|
|
327
|
+
return entry.hooks.some(
|
|
328
|
+
(h) => h && h.command && h.command.includes('harness-user-prompt-submit')
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (entry.type === 'command' && entry.command) {
|
|
332
|
+
return entry.command.includes('harness-user-prompt-submit');
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (!alreadyInstalled) {
|
|
338
|
+
settings.hooks.UserPromptSubmit.push({
|
|
339
|
+
matcher: '',
|
|
340
|
+
hooks: [{ type: 'command', command: hookCmd }]
|
|
341
|
+
});
|
|
342
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
343
|
+
log('UserPromptSubmit hook installed in .claude/settings.json');
|
|
344
|
+
log(' → All prompts will be routed through harness-dispatcher');
|
|
345
|
+
log(' → Opt-out per message: say "harness skip" or "without harness"');
|
|
346
|
+
log(' → Disable globally: set .harness/config.json behavior.auto_route_dispatcher = false');
|
|
347
|
+
} else {
|
|
348
|
+
log('UserPromptSubmit hook already installed');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
274
352
|
// ─────────────────────────────────────────
|
|
275
353
|
// 4. AGENTS.md + CLAUDE.md
|
|
276
354
|
// ─────────────────────────────────────────
|
|
@@ -429,9 +507,16 @@ What it does:
|
|
|
429
507
|
1. Scaffolds .harness/ directory (actions, archive, gotchas, config)
|
|
430
508
|
2. Installs skills to .claude/skills/ (dispatcher, planner, generators, evaluators)
|
|
431
509
|
3. Copies helper scripts to scripts/
|
|
432
|
-
4.
|
|
433
|
-
5.
|
|
434
|
-
6.
|
|
510
|
+
4. Registers SessionStart hook (boot-time progress render)
|
|
511
|
+
5. Registers UserPromptSubmit hook (auto-route every prompt through harness-dispatcher)
|
|
512
|
+
6. Creates AGENTS.md + CLAUDE.md symlink
|
|
513
|
+
7. Checks Playwright MCP configuration
|
|
514
|
+
8. Checks recommended external skills (Vercel, design skills)
|
|
515
|
+
|
|
516
|
+
Auto routing:
|
|
517
|
+
Every user prompt is routed through harness-dispatcher.
|
|
518
|
+
Per-message opt-out: say "harness skip" or "harness 없이".
|
|
519
|
+
Global disable: edit .harness/config.json → behavior.auto_route_dispatcher = false
|
|
435
520
|
|
|
436
521
|
After init:
|
|
437
522
|
1. Restart Claude Code session (exit and re-enter) for skills to load
|
|
@@ -463,6 +548,7 @@ function main() {
|
|
|
463
548
|
installSkills();
|
|
464
549
|
installScripts();
|
|
465
550
|
installSessionHook();
|
|
551
|
+
installUserPromptSubmitHook();
|
|
466
552
|
setupAgentsMd();
|
|
467
553
|
checkPlaywrightMcp();
|
|
468
554
|
checkRecommendedSkills();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual)
|
|
3
|
+
"version": "2.3.0",
|
|
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"
|
|
7
7
|
},
|
|
@@ -16,11 +16,14 @@
|
|
|
16
16
|
"ai-agent",
|
|
17
17
|
"claude-code",
|
|
18
18
|
"skill",
|
|
19
|
+
"brainstorming",
|
|
19
20
|
"planner",
|
|
20
21
|
"generator",
|
|
21
22
|
"evaluator",
|
|
22
23
|
"nestjs",
|
|
23
24
|
"react",
|
|
25
|
+
"flutter",
|
|
26
|
+
"riverpod",
|
|
24
27
|
"playwright"
|
|
25
28
|
],
|
|
26
29
|
"author": "",
|
package/scripts/harness-next.sh
CHANGED
|
@@ -18,6 +18,7 @@ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || {
|
|
|
18
18
|
|
|
19
19
|
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
20
20
|
CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
21
|
+
PIPELINE_JSON="$PROJECT_ROOT/.harness/actions/pipeline.json"
|
|
21
22
|
NEXT_PROMPT="$PROJECT_ROOT/.harness/next-prompt.txt"
|
|
22
23
|
|
|
23
24
|
check_jq || exit 1
|
|
@@ -40,6 +41,29 @@ next_agent=$(jq -r '.next_agent // "null"' "$PROGRESS")
|
|
|
40
41
|
retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS")
|
|
41
42
|
max_retries=$(jq -r '.flow.max_retries_per_sprint // 10' "$CONFIG" 2>/dev/null || echo 10)
|
|
42
43
|
|
|
44
|
+
# fe_stack 치환 (Flutter 지원) — pipeline.json 에서 읽음
|
|
45
|
+
fe_stack="react"
|
|
46
|
+
if [ -f "$PIPELINE_JSON" ]; then
|
|
47
|
+
fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# ─────────────────────────────────────────
|
|
51
|
+
# fe_stack 치환 헬퍼
|
|
52
|
+
# pipeline_selection.pipelines에서 읽은 에이전트명을 fe_stack에 따라 치환
|
|
53
|
+
# - react: 그대로
|
|
54
|
+
# - flutter: generator-frontend → generator-frontend-flutter 등, __skip__ 은 건너뜀
|
|
55
|
+
# ─────────────────────────────────────────
|
|
56
|
+
substitute_fe_stack() {
|
|
57
|
+
local agent="$1"
|
|
58
|
+
if [ "$fe_stack" != "flutter" ]; then
|
|
59
|
+
echo "$agent"
|
|
60
|
+
return
|
|
61
|
+
fi
|
|
62
|
+
local sub
|
|
63
|
+
sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter[\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
|
|
64
|
+
echo "$sub"
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
# ─────────────────────────────────────────
|
|
44
68
|
# Determine next agent
|
|
45
69
|
# ─────────────────────────────────────────
|
|
@@ -73,8 +97,18 @@ compute_next_agent() {
|
|
|
73
97
|
return
|
|
74
98
|
fi
|
|
75
99
|
|
|
100
|
+
local -a raw_agents
|
|
101
|
+
mapfile -t raw_agents < <(jq -r ".flow.pipeline_selection.pipelines[\"${pipeline}\"][]" "$CONFIG" 2>/dev/null | sed 's/:.*//')
|
|
102
|
+
|
|
103
|
+
# fe_stack 치환 + __skip__ 필터링
|
|
76
104
|
local -a agents
|
|
77
|
-
|
|
105
|
+
for a in "${raw_agents[@]}"; do
|
|
106
|
+
local sub
|
|
107
|
+
sub=$(substitute_fe_stack "$a")
|
|
108
|
+
if [ "$sub" != "__skip__" ]; then
|
|
109
|
+
agents+=("$sub")
|
|
110
|
+
fi
|
|
111
|
+
done
|
|
78
112
|
|
|
79
113
|
local found=false
|
|
80
114
|
for agent in "${agents[@]}"; do
|
|
@@ -82,6 +116,7 @@ compute_next_agent() {
|
|
|
82
116
|
echo "$agent"
|
|
83
117
|
return
|
|
84
118
|
fi
|
|
119
|
+
# current 비교 시에도 치환된 이름으로 (Flutter 변형 에이전트가 실행되는 경우)
|
|
85
120
|
if [ "$agent" = "$current" ]; then
|
|
86
121
|
found=true
|
|
87
122
|
fi
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
# ─────────────────────────────────────────
|
|
14
|
+
set -e
|
|
15
|
+
|
|
16
|
+
# stdin 의 JSON payload 읽기 (Claude Code 가 {prompt, cwd, session_id, ...} 전달)
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
|
|
19
|
+
# cwd 추출 (payload 에 없으면 PWD fallback)
|
|
20
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
|
|
21
|
+
if [ -z "$CWD" ]; then
|
|
22
|
+
CWD="$PWD"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# 조건 1: 하네스 초기화 확인
|
|
26
|
+
if [ ! -f "$CWD/.harness/config.json" ]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# 조건 2: opt-out 플래그 확인 (기본값 true)
|
|
31
|
+
AUTO_ROUTE="true"
|
|
32
|
+
if command -v jq >/dev/null 2>&1; then
|
|
33
|
+
AUTO_ROUTE=$(jq -r '.behavior.auto_route_dispatcher // true' "$CWD/.harness/config.json" 2>/dev/null || echo "true")
|
|
34
|
+
fi
|
|
35
|
+
if [ "$AUTO_ROUTE" != "true" ]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# 프롬프트 내용 추출 (opt-out 문구 감지용)
|
|
40
|
+
PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null || true)
|
|
41
|
+
# 사용자가 명시적으로 건너뛰기 요청하면 pass-through
|
|
42
|
+
if echo "$PROMPT" | grep -qiE "harness\s*(skip|off|bypass|없이)|without\s*harness|just\s*(answer|chat|reply)"; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# 현재 세션 상태 읽기
|
|
47
|
+
PIPELINE="none"
|
|
48
|
+
CURRENT_AGENT="none"
|
|
49
|
+
NEXT_AGENT="none"
|
|
50
|
+
SPRINT_NUM="0"
|
|
51
|
+
SPRINT_STATUS="init"
|
|
52
|
+
FE_STACK="react"
|
|
53
|
+
|
|
54
|
+
if [ -f "$CWD/.harness/progress.json" ] && command -v jq >/dev/null 2>&1; then
|
|
55
|
+
PIPELINE=$(jq -r '.pipeline // "none"' "$CWD/.harness/progress.json" 2>/dev/null || echo "none")
|
|
56
|
+
CURRENT_AGENT=$(jq -r '.current_agent // "none"' "$CWD/.harness/progress.json" 2>/dev/null || echo "none")
|
|
57
|
+
NEXT_AGENT=$(jq -r '.next_agent // "none"' "$CWD/.harness/progress.json" 2>/dev/null || echo "none")
|
|
58
|
+
SPRINT_NUM=$(jq -r '.sprint.number // 0' "$CWD/.harness/progress.json" 2>/dev/null || echo "0")
|
|
59
|
+
SPRINT_STATUS=$(jq -r '.sprint.status // "init"' "$CWD/.harness/progress.json" 2>/dev/null || echo "init")
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ -f "$CWD/.harness/actions/pipeline.json" ] && command -v jq >/dev/null 2>&1; then
|
|
63
|
+
FE_STACK=$(jq -r '.fe_stack // "react"' "$CWD/.harness/actions/pipeline.json" 2>/dev/null || echo "react")
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# ─────────────────────────────────────────
|
|
67
|
+
# Context 주입 (stdout → Claude 컨텍스트)
|
|
68
|
+
# ─────────────────────────────────────────
|
|
69
|
+
cat <<EOF
|
|
70
|
+
[walwal-harness] Auto-routing is ACTIVE for this project.
|
|
71
|
+
|
|
72
|
+
이 프로젝트는 walwal-harness 가 초기화되어 있으며, auto_route_dispatcher 플래그가
|
|
73
|
+
켜져 있습니다. 사용자의 모든 프롬프트는 **harness-dispatcher** 스킬을 통해
|
|
74
|
+
분류/라우팅된 뒤 처리되어야 합니다.
|
|
75
|
+
|
|
76
|
+
## 분류 규칙 (Dispatcher Request Classification)
|
|
77
|
+
- **기능 요청** (만들어줘, 추가, 고쳐줘, PRD, OpenAPI) → Pipeline Flow
|
|
78
|
+
- **실수 지적** (아니, 잘못됐어, 그렇게 하면 안 돼, X로 해야지) → Gotcha Flow
|
|
79
|
+
- **혼합** → Gotcha 먼저 기록 → Pipeline 이어서
|
|
80
|
+
- **메타/인사/Claude 자체 질문** → 짧게 일반 응답 (dispatcher skip 허용)
|
|
81
|
+
|
|
82
|
+
## 현재 harness 세션 상태
|
|
83
|
+
- pipeline: $PIPELINE
|
|
84
|
+
- current_agent: $CURRENT_AGENT
|
|
85
|
+
- next_agent: $NEXT_AGENT
|
|
86
|
+
- sprint: $SPRINT_NUM ($SPRINT_STATUS)
|
|
87
|
+
- fe_stack: $FE_STACK
|
|
88
|
+
|
|
89
|
+
## 지시
|
|
90
|
+
1. 먼저 위 상태를 읽고, 사용자 메시지가 어떤 분류에 해당하는지 판단
|
|
91
|
+
2. **pipeline 이 'none' 또는 dispatcher 미실행 상태**면 harness-dispatcher 스킬 호출
|
|
92
|
+
3. **pipeline 이 이미 활성**이면:
|
|
93
|
+
- 실수 지적이면 → dispatcher 로 gotcha 기록
|
|
94
|
+
- 기능 연속 작업이면 → next_agent 또는 current_agent 의 컨텍스트로 계속
|
|
95
|
+
4. 응답 전 반드시 \`.harness/progress.json\` 을 읽고 세션 경계 프로토콜 준수
|
|
96
|
+
|
|
97
|
+
## 이 라우팅을 건너뛰려면
|
|
98
|
+
- **단일 메시지**: 사용자가 "harness skip", "without harness", "harness 없이", "just answer" 등을 명시
|
|
99
|
+
- **전역 비활성**: \`.harness/config.json\` 에서 \`behavior.auto_route_dispatcher = false\` 설정
|
|
100
|
+
|
|
101
|
+
## 참고
|
|
102
|
+
이 안내는 \`scripts/harness-user-prompt-submit.sh\` UserPromptSubmit 훅에 의해
|
|
103
|
+
매 프롬프트마다 자동 주입됩니다. 중복 안내처럼 보여도 무시하지 말고 위 규칙을 따르세요.
|
|
104
|
+
EOF
|
|
105
|
+
|
|
106
|
+
exit 0
|
|
@@ -282,12 +282,17 @@ render_progress() {
|
|
|
282
282
|
render_agent_bar() {
|
|
283
283
|
local PROGRESS="${1:-.}/.harness/progress.json"
|
|
284
284
|
local CONFIG="${1:-.}/.harness/config.json"
|
|
285
|
+
local PIPELINE_JSON="${1:-.}/.harness/actions/pipeline.json"
|
|
285
286
|
|
|
286
287
|
if [ ! -f "$PROGRESS" ] || [ ! -f "$CONFIG" ]; then return 1; fi
|
|
287
288
|
|
|
288
|
-
local pipeline current_agent
|
|
289
|
+
local pipeline current_agent fe_stack
|
|
289
290
|
pipeline=$(jq -r '.pipeline // "unknown"' "$PROGRESS")
|
|
290
291
|
current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS")
|
|
292
|
+
fe_stack="react"
|
|
293
|
+
if [ -f "$PIPELINE_JSON" ]; then
|
|
294
|
+
fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
|
|
295
|
+
fi
|
|
291
296
|
|
|
292
297
|
local completed_agents
|
|
293
298
|
completed_agents=$(jq -r '.completed_agents // [] | .[]' "$PROGRESS")
|
|
@@ -299,6 +304,15 @@ render_agent_bar() {
|
|
|
299
304
|
|
|
300
305
|
while IFS= read -r agent; do
|
|
301
306
|
agent=$(echo "$agent" | sed 's/:.*//') # strip mode suffix like :light, :api-only
|
|
307
|
+
|
|
308
|
+
# fe_stack 치환 적용
|
|
309
|
+
if [ "$fe_stack" = "flutter" ]; then
|
|
310
|
+
local sub
|
|
311
|
+
sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter[\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
|
|
312
|
+
if [ "$sub" = "__skip__" ]; then continue; fi
|
|
313
|
+
agent="$sub"
|
|
314
|
+
fi
|
|
315
|
+
|
|
302
316
|
if [ "$first" = true ]; then
|
|
303
317
|
first=false
|
|
304
318
|
else
|
package/scripts/scan-project.sh
CHANGED
|
@@ -61,7 +61,13 @@ elif [ -f "${PROJECT_ROOT}/pom.xml" ] || [ -f "${PROJECT_ROOT}/build.gradle" ];
|
|
|
61
61
|
fi
|
|
62
62
|
|
|
63
63
|
# Frontend
|
|
64
|
-
|
|
64
|
+
# Flutter 우선 감지 — pubspec.yaml 존재하면 Flutter 프로젝트로 판정 (TECH_LANG도 보정)
|
|
65
|
+
FE_STACK="react" # react | flutter (기본값은 react 계열)
|
|
66
|
+
if [ -f "${PROJECT_ROOT}/pubspec.yaml" ] && grep -q "flutter:" "${PROJECT_ROOT}/pubspec.yaml" 2>/dev/null; then
|
|
67
|
+
TECH_FRONTEND="flutter"
|
|
68
|
+
TECH_LANG="dart"
|
|
69
|
+
FE_STACK="flutter"
|
|
70
|
+
elif [ -f "${PROJECT_ROOT}/next.config.js" ] || [ -f "${PROJECT_ROOT}/next.config.ts" ] || [ -f "${PROJECT_ROOT}/next.config.mjs" ]; then
|
|
65
71
|
TECH_FRONTEND="nextjs"
|
|
66
72
|
elif [ -f "${PROJECT_ROOT}/vite.config.ts" ] || [ -f "${PROJECT_ROOT}/vite.config.js" ]; then
|
|
67
73
|
TECH_FRONTEND="vite-react"
|
|
@@ -75,11 +81,26 @@ fi
|
|
|
75
81
|
if [ -d "${PROJECT_ROOT}/apps/web" ]; then
|
|
76
82
|
if [ -f "${PROJECT_ROOT}/apps/web/next.config.js" ] || [ -f "${PROJECT_ROOT}/apps/web/next.config.ts" ]; then
|
|
77
83
|
TECH_FRONTEND="nextjs"
|
|
84
|
+
FE_STACK="react"
|
|
78
85
|
elif [ -f "${PROJECT_ROOT}/apps/web/vite.config.ts" ]; then
|
|
79
86
|
TECH_FRONTEND="vite-react"
|
|
87
|
+
FE_STACK="react"
|
|
80
88
|
fi
|
|
81
89
|
fi
|
|
82
90
|
|
|
91
|
+
# Flutter 서브디렉토리 감지 (monorepo 또는 서브 프로젝트 케이스)
|
|
92
|
+
if [ "$TECH_FRONTEND" = "unknown" ]; then
|
|
93
|
+
# 대표적인 Flutter 서브폴더 이름을 얕게 탐색
|
|
94
|
+
for d in apps/mobile mobile clue_mobile_app flutter_app; do
|
|
95
|
+
if [ -f "${PROJECT_ROOT}/${d}/pubspec.yaml" ] && grep -q "flutter:" "${PROJECT_ROOT}/${d}/pubspec.yaml" 2>/dev/null; then
|
|
96
|
+
TECH_FRONTEND="flutter"
|
|
97
|
+
TECH_LANG="dart"
|
|
98
|
+
FE_STACK="flutter"
|
|
99
|
+
break
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
fi
|
|
103
|
+
|
|
83
104
|
# Database
|
|
84
105
|
if grep -rq "typeorm\|prisma\|sequelize\|knex" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
85
106
|
if grep -q "pg\|postgres" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
@@ -203,6 +224,7 @@ cat > "$OUTPUT" << JSONEOF
|
|
|
203
224
|
"tech_stack": {
|
|
204
225
|
"backend": "${TECH_BACKEND}",
|
|
205
226
|
"frontend": "${TECH_FRONTEND}",
|
|
227
|
+
"fe_stack": "${FE_STACK}",
|
|
206
228
|
"database": "${TECH_DB}",
|
|
207
229
|
"monorepo": "${TECH_MONOREPO}",
|
|
208
230
|
"language": "${TECH_LANG}"
|
|
@@ -260,7 +282,7 @@ echo "=== Scan Complete ==="
|
|
|
260
282
|
echo "Output: ${OUTPUT}"
|
|
261
283
|
echo ""
|
|
262
284
|
echo "--- Summary ---"
|
|
263
|
-
echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} / ${TECH_DB}"
|
|
285
|
+
echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} (fe_stack=${FE_STACK}) / ${TECH_DB}"
|
|
264
286
|
echo "Monorepo: ${TECH_MONOREPO}"
|
|
265
287
|
echo "OpenAPI: ${OPENAPI}"
|
|
266
288
|
echo "Git: ${GIT_INIT} (${GIT_COMMITS} commits, branch: ${GIT_BRANCH})"
|