@walwal-harness/cli 4.0.0-alpha.9 → 4.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +235 -273
- package/assets/templates/config.json +1 -48
- package/assets/templates/gitignore-append.txt +1 -0
- package/bin/init.js +139 -67
- package/package.json +4 -2
- package/scripts/harness-dashboard-v4.sh +66 -82
- package/scripts/harness-monitor.sh +202 -67
- package/scripts/harness-next.sh +4 -15
- package/scripts/harness-prompt-history.sh +126 -0
- package/scripts/harness-studio-setup.sh +143 -0
- package/scripts/harness-studio.sh +66 -0
- package/scripts/harness-tmux-v4.sh +136 -0
- package/scripts/harness-user-prompt-submit.sh +13 -0
- package/skills/dispatcher/SKILL.md +7 -2
- package/skills/team-action/SKILL.md +217 -0
- package/skills/team-stop/SKILL.md +19 -0
- package/scripts/harness-control-v4.sh +0 -97
- package/scripts/harness-studio-v4.sh +0 -122
- package/scripts/harness-team-worker.sh +0 -415
- package/skills/evaluator-functional-flutter/SKILL.md +0 -206
- package/skills/evaluator-functional-flutter/references/ia-compliance.md +0 -77
- package/skills/evaluator-functional-flutter/references/scoring-rubric.md +0 -132
- package/skills/evaluator-functional-flutter/references/static-check-rules.md +0 -99
- package/skills/generator-frontend-flutter/SKILL.md +0 -173
- package/skills/generator-frontend-flutter/references/anti-patterns.md +0 -320
- package/skills/generator-frontend-flutter/references/api-layer-pattern.md +0 -233
- package/skills/generator-frontend-flutter/references/flutter-web-pattern.md +0 -273
- package/skills/generator-frontend-flutter/references/i18n-pattern.md +0 -102
- package/skills/generator-frontend-flutter/references/riverpod-pattern.md +0 -199
|
@@ -133,62 +133,15 @@
|
|
|
133
133
|
]
|
|
134
134
|
}
|
|
135
135
|
},
|
|
136
|
-
"generator-frontend-flutter": {
|
|
137
|
-
"role": "Flutter 앱 개발 — Riverpod, integrated_data_layer(Retrofit), i18n(ARB), build_runner",
|
|
138
|
-
"skill": "harness-generator-frontend-flutter",
|
|
139
|
-
"inputs": ["actions/plan.md", "actions/feature-list.json", "actions/api-contract.json", "actions/sprint-contract.md"],
|
|
140
|
-
"outputs": ["code:flutter", "actions/sprint-contract.md"],
|
|
141
|
-
"order": 2,
|
|
142
|
-
"fe_stack": "flutter",
|
|
143
|
-
"model": "sonnet",
|
|
144
|
-
"thinking_mode": null
|
|
145
|
-
},
|
|
146
|
-
"evaluator-functional-flutter": {
|
|
147
|
-
"role": "Flutter 앱 검증 — flutter analyze, flutter test, build_runner 일관성, 안티패턴 정적 검증",
|
|
148
|
-
"skill": "harness-evaluator-functional-flutter",
|
|
149
|
-
"tools": ["bash:flutter", "bash:dart"],
|
|
150
|
-
"inputs": ["actions/sprint-contract.md"],
|
|
151
|
-
"outputs": ["actions/evaluation-functional.md"],
|
|
152
|
-
"fe_stack": "flutter",
|
|
153
|
-
"model": "opus",
|
|
154
|
-
"thinking_mode": "ultrathink"
|
|
155
|
-
}
|
|
156
136
|
},
|
|
157
137
|
"flow": {
|
|
158
138
|
"sequence": ["dispatcher", "planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
159
139
|
"pipeline_selection": {
|
|
160
|
-
"comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정.
|
|
140
|
+
"comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정.",
|
|
161
141
|
"pipelines": {
|
|
162
142
|
"FULLSTACK": ["planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
163
143
|
"FE-ONLY": ["planner:light", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
164
144
|
"BE-ONLY": ["planner", "generator-backend", "evaluator-functional:api-only"]
|
|
165
|
-
},
|
|
166
|
-
"fe_stack_substitution": {
|
|
167
|
-
"comment": "pipeline.json.fe_stack + fe_target 에 따라 FE 에이전트 치환. Flutter Web 은 React 와 동일한 Playwright 기반 evaluator 를 사용한다.",
|
|
168
|
-
"flutter": {
|
|
169
|
-
"_doc": "fe_target = web | mobile | desktop. by_target 으로 분기.",
|
|
170
|
-
"by_target": {
|
|
171
|
-
"web": {
|
|
172
|
-
"_doc": "Flutter Web — 컴파일 결과가 HTML+JS+CSS 이므로 Playwright evaluator 사용 가능",
|
|
173
|
-
"generator-frontend": "generator-frontend-flutter",
|
|
174
|
-
"evaluator-functional": "evaluator-functional",
|
|
175
|
-
"evaluator-visual": "evaluator-visual"
|
|
176
|
-
},
|
|
177
|
-
"mobile": {
|
|
178
|
-
"_doc": "Flutter Mobile (Android/iOS) — 브라우저 없음, 정적 분석 evaluator 사용",
|
|
179
|
-
"generator-frontend": "generator-frontend-flutter",
|
|
180
|
-
"evaluator-functional": "evaluator-functional-flutter",
|
|
181
|
-
"evaluator-visual": "__skip__"
|
|
182
|
-
},
|
|
183
|
-
"desktop": {
|
|
184
|
-
"_doc": "Flutter Desktop (macOS/Windows/Linux) — 브라우저 없음, 정적 분석 evaluator 사용",
|
|
185
|
-
"generator-frontend": "generator-frontend-flutter",
|
|
186
|
-
"evaluator-functional": "evaluator-functional-flutter",
|
|
187
|
-
"evaluator-visual": "__skip__"
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
"_default_target": "mobile"
|
|
191
|
-
}
|
|
192
145
|
}
|
|
193
146
|
},
|
|
194
147
|
"sprint_execution": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# For consumer projects, add .worktrees/ to .gitignore
|
package/bin/init.js
CHANGED
|
@@ -129,17 +129,56 @@ function scaffoldHarness() {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
//
|
|
132
|
+
// config.json — ALWAYS update (harness system file, not user data)
|
|
133
|
+
// But preserve user's custom settings (pre_eval_gate.frontend_cwd, behavior, etc.)
|
|
133
134
|
const configSrc = path.join(PKG_ROOT, 'assets', 'templates', 'config.json');
|
|
134
135
|
const configDest = path.join(HARNESS_DIR, 'config.json');
|
|
135
|
-
if (fs.existsSync(configSrc)
|
|
136
|
-
|
|
136
|
+
if (fs.existsSync(configSrc)) {
|
|
137
|
+
if (fileExists(configDest) && !isForce) {
|
|
138
|
+
// Merge: keep user's customizations, update harness structure
|
|
139
|
+
try {
|
|
140
|
+
const existing = JSON.parse(fs.readFileSync(configDest, 'utf8'));
|
|
141
|
+
const template = JSON.parse(fs.readFileSync(configSrc, 'utf8'));
|
|
142
|
+
// Preserve user customizations
|
|
143
|
+
const userPreserve = {
|
|
144
|
+
behavior: existing.behavior,
|
|
145
|
+
'flow.pre_eval_gate.frontend_cwd': existing?.flow?.pre_eval_gate?.frontend_cwd,
|
|
146
|
+
'flow.pre_eval_gate.backend_cwd': existing?.flow?.pre_eval_gate?.backend_cwd,
|
|
147
|
+
'flow.pre_eval_gate.frontend_checks': existing?.flow?.pre_eval_gate?.frontend_checks,
|
|
148
|
+
'flow.pre_eval_gate.backend_checks': existing?.flow?.pre_eval_gate?.backend_checks,
|
|
149
|
+
};
|
|
150
|
+
// Write template, then re-apply user settings
|
|
151
|
+
fs.writeFileSync(configDest, JSON.stringify(template, null, 2) + '\n');
|
|
152
|
+
// Re-apply preserved user settings
|
|
153
|
+
const merged = JSON.parse(fs.readFileSync(configDest, 'utf8'));
|
|
154
|
+
if (userPreserve.behavior) merged.behavior = userPreserve.behavior;
|
|
155
|
+
if (userPreserve['flow.pre_eval_gate.frontend_cwd']) {
|
|
156
|
+
merged.flow.pre_eval_gate.frontend_cwd = userPreserve['flow.pre_eval_gate.frontend_cwd'];
|
|
157
|
+
}
|
|
158
|
+
if (userPreserve['flow.pre_eval_gate.backend_cwd']) {
|
|
159
|
+
merged.flow.pre_eval_gate.backend_cwd = userPreserve['flow.pre_eval_gate.backend_cwd'];
|
|
160
|
+
}
|
|
161
|
+
if (userPreserve['flow.pre_eval_gate.frontend_checks']) {
|
|
162
|
+
merged.flow.pre_eval_gate.frontend_checks = userPreserve['flow.pre_eval_gate.frontend_checks'];
|
|
163
|
+
}
|
|
164
|
+
if (userPreserve['flow.pre_eval_gate.backend_checks']) {
|
|
165
|
+
merged.flow.pre_eval_gate.backend_checks = userPreserve['flow.pre_eval_gate.backend_checks'];
|
|
166
|
+
}
|
|
167
|
+
fs.writeFileSync(configDest, JSON.stringify(merged, null, 2) + '\n');
|
|
168
|
+
log('config.json updated (user settings preserved)');
|
|
169
|
+
} catch (e) {
|
|
170
|
+
copyFile(configSrc, configDest);
|
|
171
|
+
log('config.json replaced (merge failed)');
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
copyFile(configSrc, configDest);
|
|
175
|
+
}
|
|
137
176
|
}
|
|
138
177
|
|
|
139
|
-
//
|
|
178
|
+
// HARNESS.md — ALWAYS update
|
|
140
179
|
const harnessMdSrc = path.join(PKG_ROOT, 'assets', 'templates', 'HARNESS.md');
|
|
141
180
|
const harnessMdDest = path.join(HARNESS_DIR, 'HARNESS.md');
|
|
142
|
-
if (fs.existsSync(harnessMdSrc)
|
|
181
|
+
if (fs.existsSync(harnessMdSrc)) {
|
|
143
182
|
copyFile(harnessMdSrc, harnessMdDest);
|
|
144
183
|
}
|
|
145
184
|
|
|
@@ -186,6 +225,17 @@ function installSkills() {
|
|
|
186
225
|
return;
|
|
187
226
|
}
|
|
188
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
|
+
|
|
189
239
|
const skills = fs.readdirSync(skillsSrc, { withFileTypes: true })
|
|
190
240
|
.filter(d => d.isDirectory())
|
|
191
241
|
.map(d => d.name);
|
|
@@ -193,13 +243,8 @@ function installSkills() {
|
|
|
193
243
|
for (const skill of skills) {
|
|
194
244
|
const src = path.join(skillsSrc, skill);
|
|
195
245
|
const dest = path.join(CLAUDE_SKILLS_DIR, `harness-${skill}`);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
copyDir(src, dest);
|
|
199
|
-
log(` Installed: harness-${skill}`);
|
|
200
|
-
} else {
|
|
201
|
-
log(` Skipped (exists): harness-${skill}`);
|
|
202
|
-
}
|
|
246
|
+
copyDir(src, dest);
|
|
247
|
+
log(` Installed: harness-${skill}`);
|
|
203
248
|
}
|
|
204
249
|
|
|
205
250
|
log('Skills installation complete');
|
|
@@ -214,51 +259,28 @@ function installScripts() {
|
|
|
214
259
|
const scriptsSrc = path.join(PKG_ROOT, 'scripts');
|
|
215
260
|
const scriptsDest = path.join(PROJECT_ROOT, 'scripts');
|
|
216
261
|
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
'
|
|
221
|
-
|
|
222
|
-
'harness-statusline.sh',
|
|
223
|
-
'harness-user-prompt-submit.sh',
|
|
224
|
-
'harness-dashboard.sh',
|
|
225
|
-
'harness-monitor.sh',
|
|
226
|
-
'harness-eval-watcher.sh',
|
|
227
|
-
'harness-tmux.sh',
|
|
228
|
-
'harness-control.sh',
|
|
229
|
-
'harness-studio-v4.sh',
|
|
230
|
-
'harness-dashboard-v4.sh',
|
|
231
|
-
'harness-control-v4.sh',
|
|
232
|
-
'harness-queue-manager.sh',
|
|
233
|
-
'harness-team-worker.sh',
|
|
234
|
-
]);
|
|
262
|
+
// 전체 삭제 후 재복사 — 버전 간 잔류 파일 방지
|
|
263
|
+
if (fs.existsSync(scriptsDest)) {
|
|
264
|
+
fs.rmSync(scriptsDest, { recursive: true, force: true });
|
|
265
|
+
log(' Cleared existing scripts/');
|
|
266
|
+
}
|
|
235
267
|
|
|
236
268
|
if (fs.existsSync(scriptsSrc)) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
for
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (f.endsWith('.sh')) {
|
|
249
|
-
fs.chmodSync(path.join(destPath, f), '755');
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} catch (e) {}
|
|
253
|
-
} else {
|
|
254
|
-
// Core scripts: always overwrite. Others: skip if exists (unless --force)
|
|
255
|
-
const isCore = coreScripts.has(entry.name);
|
|
256
|
-
if (isCore || !fileExists(destPath) || isForce) {
|
|
257
|
-
copyFile(srcPath, destPath);
|
|
258
|
-
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) {}
|
|
259
280
|
}
|
|
260
281
|
}
|
|
261
282
|
}
|
|
283
|
+
chmodRecursive(scriptsDest);
|
|
262
284
|
}
|
|
263
285
|
|
|
264
286
|
log('Scripts installation complete');
|
|
@@ -424,6 +446,28 @@ function installUserPromptSubmitHook() {
|
|
|
424
446
|
// ─────────────────────────────────────────
|
|
425
447
|
// 4. AGENTS.md + CLAUDE.md
|
|
426
448
|
// ─────────────────────────────────────────
|
|
449
|
+
// ─────────────────────────────────────────
|
|
450
|
+
// 3d. Agent Teams env var
|
|
451
|
+
// ─────────────────────────────────────────
|
|
452
|
+
function installAgentTeamsEnv() {
|
|
453
|
+
const settingsPath = path.join(PROJECT_ROOT, '.claude', 'settings.json');
|
|
454
|
+
let settings = {};
|
|
455
|
+
if (fileExists(settingsPath)) {
|
|
456
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!settings.env) settings.env = {};
|
|
460
|
+
|
|
461
|
+
if (settings.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] !== '1') {
|
|
462
|
+
settings.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] = '1';
|
|
463
|
+
ensureDir(path.dirname(settingsPath));
|
|
464
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
465
|
+
log('Agent Teams enabled (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)');
|
|
466
|
+
} else {
|
|
467
|
+
log('Agent Teams already enabled');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
427
471
|
function setupAgentsMd() {
|
|
428
472
|
const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
|
|
429
473
|
const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
@@ -575,7 +619,7 @@ Usage:
|
|
|
575
619
|
npx walwal-harness --force Re-initialize (overwrites existing files)
|
|
576
620
|
npx walwal-harness studio Launch Harness Studio v3 (tmux 5-pane)
|
|
577
621
|
npx walwal-harness studio --ai Studio v3 + AI eval summary
|
|
578
|
-
npx walwal-harness v4
|
|
622
|
+
npx walwal-harness v4 Enable Agent Teams (set env var + init queue)
|
|
579
623
|
npx walwal-harness --help Show this help
|
|
580
624
|
|
|
581
625
|
What it does:
|
|
@@ -626,24 +670,51 @@ function runStudio() {
|
|
|
626
670
|
}
|
|
627
671
|
|
|
628
672
|
function runStudioV4() {
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
process.exit(1);
|
|
673
|
+
// Enable Agent Teams in project settings
|
|
674
|
+
const settingsPath = path.join(PROJECT_ROOT, '.claude', 'settings.json');
|
|
675
|
+
let settings = {};
|
|
676
|
+
if (fileExists(settingsPath)) {
|
|
677
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
|
|
635
678
|
}
|
|
636
679
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
680
|
+
if (!settings.env) settings.env = {};
|
|
681
|
+
settings.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] = '1';
|
|
682
|
+
ensureDir(path.dirname(settingsPath));
|
|
683
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
684
|
+
|
|
685
|
+
// Initialize feature queue if feature-list.json exists
|
|
686
|
+
const featureList = path.join(PROJECT_ROOT, '.harness', 'actions', 'feature-list.json');
|
|
687
|
+
const featureQueue = path.join(PROJECT_ROOT, '.harness', 'actions', 'feature-queue.json');
|
|
688
|
+
const queueMgr = path.join(PKG_ROOT, 'scripts', 'harness-queue-manager.sh');
|
|
689
|
+
|
|
690
|
+
if (fs.existsSync(featureList) && fs.existsSync(queueMgr)) {
|
|
691
|
+
if (!fs.existsSync(featureQueue)) {
|
|
692
|
+
log('Initializing feature queue...');
|
|
693
|
+
try { execSync(`bash "${queueMgr}" init "${PROJECT_ROOT}"`, { stdio: 'inherit' }); } catch (e) {}
|
|
694
|
+
} else {
|
|
695
|
+
log('Recovering feature queue...');
|
|
696
|
+
try { execSync(`bash "${queueMgr}" recover "${PROJECT_ROOT}"`, { stdio: 'inherit' }); } catch (e) {}
|
|
697
|
+
}
|
|
642
698
|
}
|
|
643
699
|
|
|
644
|
-
|
|
645
|
-
log('
|
|
646
|
-
|
|
700
|
+
console.log('');
|
|
701
|
+
log('╔═══════════════════════════════════════════════════════════╗');
|
|
702
|
+
log('║ Agent Teams v4 ENABLED ║');
|
|
703
|
+
log('╠═══════════════════════════════════════════════════════════╣');
|
|
704
|
+
log('║ ║');
|
|
705
|
+
log('║ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 set in ║');
|
|
706
|
+
log('║ .claude/settings.json ║');
|
|
707
|
+
log('║ ║');
|
|
708
|
+
log('║ Next steps: ║');
|
|
709
|
+
log('║ 1. Restart Claude Code (/exit → re-enter) ║');
|
|
710
|
+
log('║ 2. Run Planner: "하네스 엔지니어링 시작" ║');
|
|
711
|
+
log('║ 3. Start Teams: /harness-team-action ║');
|
|
712
|
+
log('║ ║');
|
|
713
|
+
log('║ Or use --teammate-mode tmux for split panes: ║');
|
|
714
|
+
log('║ $ claude --teammate-mode tmux ║');
|
|
715
|
+
log('║ ║');
|
|
716
|
+
log('╚═══════════════════════════════════════════════════════════╝');
|
|
717
|
+
console.log('');
|
|
647
718
|
}
|
|
648
719
|
|
|
649
720
|
function main() {
|
|
@@ -679,6 +750,7 @@ function main() {
|
|
|
679
750
|
installSessionHook();
|
|
680
751
|
installStatusline();
|
|
681
752
|
installUserPromptSubmitHook();
|
|
753
|
+
installAgentTeamsEnv();
|
|
682
754
|
setupAgentsMd();
|
|
683
755
|
checkPlaywrightMcp();
|
|
684
756
|
checkRecommendedSkills();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "4.0.0-
|
|
3
|
+
"version": "4.0.0-beta.10",
|
|
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"
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"postinstall": "node bin/init.js --auto",
|
|
10
10
|
"init": "node bin/init.js",
|
|
11
11
|
"scan": "bash scripts/scan-project.sh .",
|
|
12
|
-
"reset": "node bin/init.js --force"
|
|
12
|
+
"reset": "node bin/init.js --force",
|
|
13
|
+
"studio": "bash scripts/harness-studio.sh",
|
|
14
|
+
"studio:kill": "bash scripts/harness-studio.sh --kill"
|
|
13
15
|
},
|
|
14
16
|
"keywords": [
|
|
15
17
|
"harness-engineering",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-dashboard-v4.sh — v4 Dashboard
|
|
3
|
-
#
|
|
2
|
+
# harness-dashboard-v4.sh — v4 Dashboard 상단: Planner Progress
|
|
3
|
+
# Queue + Teams + Features 를 auto-refresh. 고정 영역, 스크롤 없음.
|
|
4
4
|
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
@@ -30,42 +30,34 @@ render_header() {
|
|
|
30
30
|
now=$(date +"%H:%M:%S")
|
|
31
31
|
project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
|
|
32
32
|
|
|
33
|
-
echo -e "${BOLD}
|
|
34
|
-
echo -e "${BOLD}║ HARNESS v4 — Parallel Agent Teams ║${RESET}"
|
|
35
|
-
echo -e "${BOLD}╚════════════════════════════════════════════════╝${RESET}"
|
|
36
|
-
echo -e " ${DIM}${project_name} | ${now}${RESET}"
|
|
37
|
-
echo ""
|
|
33
|
+
echo -e "${BOLD}HARNESS v4${RESET} ${DIM}${project_name} | ${now}${RESET}"
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
render_queue_summary() {
|
|
41
37
|
if [ ! -f "$QUEUE" ]; then
|
|
42
|
-
echo -e " ${DIM}(queue not initialized
|
|
38
|
+
echo -e " ${DIM}(queue not initialized)${RESET}"
|
|
43
39
|
return
|
|
44
40
|
fi
|
|
45
41
|
|
|
46
|
-
local ready blocked in_prog passed failed total
|
|
42
|
+
local ready blocked in_prog passed failed total
|
|
47
43
|
ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
48
44
|
blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
49
45
|
in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
50
46
|
passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
51
47
|
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
52
|
-
concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null || echo 3)
|
|
53
48
|
ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
|
|
54
49
|
total=$((ready + blocked + in_prog + passed + failed))
|
|
55
50
|
|
|
56
|
-
# Progress bar
|
|
57
51
|
local pct=0
|
|
58
52
|
if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
|
|
59
|
-
local bar_w=
|
|
53
|
+
local bar_w=16
|
|
60
54
|
local filled=$(( pct * bar_w / 100 ))
|
|
61
55
|
local empty=$(( bar_w - filled ))
|
|
62
56
|
local bar=""
|
|
63
57
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
64
58
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
65
59
|
|
|
66
|
-
echo -e " ${
|
|
67
|
-
echo -e " Ready:${GREEN}${ready}${RESET} Blocked:${YELLOW}${blocked}${RESET} Progress:${CYAN}${in_prog}${RESET} Pass:${GREEN}${passed}${RESET} Fail:${RED}${failed}${RESET}"
|
|
68
|
-
echo ""
|
|
60
|
+
echo -e " ${bar} ${passed}/${total} (${pct}%) R:${GREEN}${ready}${RESET} B:${YELLOW}${blocked}${RESET} P:${CYAN}${in_prog}${RESET} ${GREEN}✓${passed}${RESET} ${RED}✗${failed}${RESET}"
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
render_teams() {
|
|
@@ -75,49 +67,44 @@ render_teams() {
|
|
|
75
67
|
team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
|
|
76
68
|
if [ "${team_count:-0}" -eq 0 ]; then return; fi
|
|
77
69
|
|
|
78
|
-
echo -e " ${BOLD}Teams${RESET}"
|
|
79
|
-
|
|
80
70
|
for i in $(seq 1 "$team_count"); do
|
|
81
71
|
local t_status t_feature t_phase t_attempt
|
|
82
72
|
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
|
|
83
73
|
t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
|
|
84
74
|
|
|
85
|
-
# Get phase from in_progress
|
|
86
75
|
if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
|
|
87
76
|
t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
|
|
88
77
|
t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
|
|
89
78
|
else
|
|
90
|
-
t_phase="—"
|
|
91
|
-
t_attempt="—"
|
|
79
|
+
t_phase="—"; t_attempt=""
|
|
92
80
|
fi
|
|
93
81
|
|
|
94
82
|
local icon color
|
|
95
83
|
case "$t_status" in
|
|
96
|
-
busy) icon="▶"
|
|
97
|
-
idle) icon="○"
|
|
98
|
-
|
|
99
|
-
*) icon="?" ; color="$RESET" ;;
|
|
84
|
+
busy) icon="▶"; color="$GREEN" ;;
|
|
85
|
+
idle) icon="○"; color="$DIM" ;;
|
|
86
|
+
*) icon="?"; color="$RESET" ;;
|
|
100
87
|
esac
|
|
101
88
|
|
|
102
|
-
local
|
|
89
|
+
local phase_short=""
|
|
103
90
|
case "$t_phase" in
|
|
104
|
-
gen)
|
|
105
|
-
gate)
|
|
106
|
-
eval)
|
|
107
|
-
*)
|
|
91
|
+
gen) phase_short="${CYAN}G${RESET}" ;;
|
|
92
|
+
gate) phase_short="${YELLOW}K${RESET}" ;;
|
|
93
|
+
eval) phase_short="${MAGENTA}E${RESET}" ;;
|
|
94
|
+
*) phase_short="${DIM}-${RESET}" ;;
|
|
108
95
|
esac
|
|
109
96
|
|
|
110
|
-
printf " %b
|
|
97
|
+
printf " %b%b T%d %-7s %b" "$color" "$icon" "$i" "$t_feature" "$phase_short"
|
|
98
|
+
if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ]; then
|
|
99
|
+
printf " #%s" "$t_attempt"
|
|
100
|
+
fi
|
|
101
|
+
echo ""
|
|
111
102
|
done
|
|
112
|
-
echo ""
|
|
113
103
|
}
|
|
114
104
|
|
|
115
|
-
|
|
105
|
+
render_features() {
|
|
116
106
|
if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
|
|
117
107
|
|
|
118
|
-
echo -e " ${BOLD}Features${RESET}"
|
|
119
|
-
|
|
120
|
-
# Single jq call: merge feature-list + queue state → pre-formatted lines
|
|
121
108
|
jq -r --slurpfile q "$QUEUE" '
|
|
122
109
|
($q[0].queue.passed // []) as $passed |
|
|
123
110
|
($q[0].queue.failed // []) as $failed |
|
|
@@ -125,7 +112,7 @@ render_feature_list() {
|
|
|
125
112
|
($q[0].queue.in_progress // {}) as $prog |
|
|
126
113
|
.features[] |
|
|
127
114
|
.id as $fid |
|
|
128
|
-
(.name // .description // "?" | if length >
|
|
115
|
+
(.name // .description // "?" | if length > 18 then .[0:16] + ".." else . end) as $fname |
|
|
129
116
|
(if ($fid | IN($passed[])) then "P"
|
|
130
117
|
elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
|
|
131
118
|
elif ($fid | IN($failed[])) then "F"
|
|
@@ -138,73 +125,70 @@ render_feature_list() {
|
|
|
138
125
|
F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
139
126
|
R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
140
127
|
B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
141
|
-
I\|*)
|
|
142
|
-
team=$(echo "$st" | cut -d'|' -f2)
|
|
128
|
+
I\|*) team=$(echo "$st" | cut -d'|' -f2)
|
|
143
129
|
phase=$(echo "$st" | cut -d'|' -f3)
|
|
144
|
-
printf " ${CYAN}◐${RESET} %-6s %-
|
|
130
|
+
printf " ${CYAN}◐${RESET} %-6s %-14s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
|
|
145
131
|
*) printf " ? %-6s %s\n" "$fid" "$fname" ;;
|
|
146
132
|
esac
|
|
147
133
|
done
|
|
148
|
-
|
|
149
|
-
echo ""
|
|
150
134
|
}
|
|
151
135
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if [ ! -f "$log_file" ]; then return; fi
|
|
155
|
-
|
|
156
|
-
# Get terminal height to limit display
|
|
157
|
-
local term_h
|
|
158
|
-
term_h=$(tput lines 2>/dev/null || echo 50)
|
|
159
|
-
local max_lines=10 # show latest 10 entries
|
|
160
|
-
|
|
161
|
-
echo -e " ${BOLD}Prompt History${RESET} ${DIM}(newest first)${RESET}"
|
|
162
|
-
|
|
163
|
-
# Read non-comment lines, reverse (newest first), take max_lines
|
|
164
|
-
grep -v '^#' "$log_file" 2>/dev/null | grep -v '^$' | tail -r 2>/dev/null | head -"$max_lines" | \
|
|
165
|
-
while IFS= read -r line; do
|
|
166
|
-
# Parse: date | agent | action | detail
|
|
167
|
-
local ts agent action detail
|
|
168
|
-
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
169
|
-
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
170
|
-
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
171
|
-
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
172
|
-
|
|
173
|
-
local short_ts icon color
|
|
174
|
-
short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
|
|
175
|
-
|
|
176
|
-
case "$agent" in
|
|
177
|
-
dispatcher*|dispatch) icon="▸"; color="$MAGENTA" ;;
|
|
178
|
-
team-*) icon="⚡"; color="$CYAN" ;;
|
|
179
|
-
manual|user) icon="★"; color="$BOLD" ;;
|
|
180
|
-
planner*) icon="□"; color="$YELLOW" ;;
|
|
181
|
-
generator*|gen*) icon="▶"; color="$GREEN" ;;
|
|
182
|
-
eval*) icon="✦"; color="$RED" ;;
|
|
183
|
-
system) icon="⚙"; color="$DIM" ;;
|
|
184
|
-
*) icon="·"; color="$DIM" ;;
|
|
185
|
-
esac
|
|
136
|
+
render_bottleneck_alert() {
|
|
137
|
+
if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
|
|
186
138
|
|
|
187
|
-
|
|
139
|
+
local failed_list blocked_count idle_teams in_prog
|
|
140
|
+
failed_list=$(jq -r '.queue.failed[]' "$QUEUE" 2>/dev/null)
|
|
141
|
+
blocked_count=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
142
|
+
in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
143
|
+
idle_teams=0
|
|
188
144
|
|
|
189
|
-
|
|
145
|
+
# Count idle teams
|
|
146
|
+
local team_count
|
|
147
|
+
team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
148
|
+
for i in $(seq 1 "$team_count"); do
|
|
149
|
+
local ts
|
|
150
|
+
ts=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
|
|
151
|
+
if [ "$ts" = "idle" ]; then idle_teams=$((idle_teams + 1)); fi
|
|
190
152
|
done
|
|
191
153
|
|
|
192
|
-
|
|
154
|
+
if [ -z "$failed_list" ]; then return; fi
|
|
155
|
+
|
|
156
|
+
# Check each failed feature for blocking impact
|
|
157
|
+
while IFS= read -r fid; do
|
|
158
|
+
[ -z "$fid" ] && continue
|
|
159
|
+
# Count how many blocked features depend on this fid
|
|
160
|
+
local deps_on_this
|
|
161
|
+
deps_on_this=$(jq --arg fid "$fid" '[.queue.blocked | to_entries[] | select(.value | index($fid))] | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
162
|
+
|
|
163
|
+
if [ "$deps_on_this" -gt 0 ]; then
|
|
164
|
+
echo ""
|
|
165
|
+
echo -e " ${RED}${BOLD}⚠ BOTTLENECK${RESET} ${RED}${fid}${RESET} failed → ${YELLOW}${deps_on_this} features blocked${RESET}"
|
|
166
|
+
if [ "$idle_teams" -gt 0 ]; then
|
|
167
|
+
echo -e " ${BOLD}→ requeue:${RESET} bash scripts/harness-queue-manager.sh requeue ${fid} ."
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
done <<< "$failed_list"
|
|
171
|
+
|
|
172
|
+
# All teams idle + nothing in progress = stalled
|
|
173
|
+
if [ "$idle_teams" -eq "$team_count" ] && [ "$in_prog" -eq 0 ] && [ "$blocked_count" -gt 0 ]; then
|
|
174
|
+
echo ""
|
|
175
|
+
echo -e " ${RED}${BOLD}!! STALLED${RESET} — All teams idle, ${blocked_count} features blocked"
|
|
176
|
+
echo -e " ${BOLD}→ requeue failed features or check dependency graph${RESET}"
|
|
177
|
+
fi
|
|
193
178
|
}
|
|
194
179
|
|
|
195
180
|
render_all() {
|
|
196
181
|
render_header
|
|
197
182
|
render_queue_summary
|
|
198
183
|
render_teams
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
184
|
+
render_bottleneck_alert
|
|
185
|
+
echo ""
|
|
186
|
+
render_features
|
|
202
187
|
}
|
|
203
188
|
|
|
204
189
|
# ── Main loop ──
|
|
205
190
|
tput civis 2>/dev/null
|
|
206
191
|
trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
207
|
-
|
|
208
192
|
clear
|
|
209
193
|
|
|
210
194
|
while true; do
|