@walwal-harness/cli 4.0.0-beta.1 → 4.0.0-beta.11
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 +97 -49
- package/package.json +4 -2
- package/scripts/harness-dashboard-v4.sh +45 -0
- package/scripts/harness-monitor.sh +202 -67
- 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 -10
- package/skills/team-action/SKILL.md +192 -39
- package/skills/team-stop/SKILL.md +17 -6
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,48 +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-dashboard-v4.sh',
|
|
230
|
-
'harness-queue-manager.sh',
|
|
231
|
-
]);
|
|
262
|
+
// 전체 삭제 후 재복사 — 버전 간 잔류 파일 방지
|
|
263
|
+
if (fs.existsSync(scriptsDest)) {
|
|
264
|
+
fs.rmSync(scriptsDest, { recursive: true, force: true });
|
|
265
|
+
log(' Cleared existing scripts/');
|
|
266
|
+
}
|
|
232
267
|
|
|
233
268
|
if (fs.existsSync(scriptsSrc)) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
for
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (f.endsWith('.sh')) {
|
|
246
|
-
fs.chmodSync(path.join(destPath, f), '755');
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
} catch (e) {}
|
|
250
|
-
} else {
|
|
251
|
-
// Core scripts: always overwrite. Others: skip if exists (unless --force)
|
|
252
|
-
const isCore = coreScripts.has(entry.name);
|
|
253
|
-
if (isCore || !fileExists(destPath) || isForce) {
|
|
254
|
-
copyFile(srcPath, destPath);
|
|
255
|
-
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) {}
|
|
256
280
|
}
|
|
257
281
|
}
|
|
258
282
|
}
|
|
283
|
+
chmodRecursive(scriptsDest);
|
|
259
284
|
}
|
|
260
285
|
|
|
261
286
|
log('Scripts installation complete');
|
|
@@ -421,6 +446,28 @@ function installUserPromptSubmitHook() {
|
|
|
421
446
|
// ─────────────────────────────────────────
|
|
422
447
|
// 4. AGENTS.md + CLAUDE.md
|
|
423
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
|
+
|
|
424
471
|
function setupAgentsMd() {
|
|
425
472
|
const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
|
|
426
473
|
const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
@@ -703,6 +750,7 @@ function main() {
|
|
|
703
750
|
installSessionHook();
|
|
704
751
|
installStatusline();
|
|
705
752
|
installUserPromptSubmitHook();
|
|
753
|
+
installAgentTeamsEnv();
|
|
706
754
|
setupAgentsMd();
|
|
707
755
|
checkPlaywrightMcp();
|
|
708
756
|
checkRecommendedSkills();
|
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.11",
|
|
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",
|
|
@@ -133,10 +133,55 @@ render_features() {
|
|
|
133
133
|
done
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
render_bottleneck_alert() {
|
|
137
|
+
if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
|
|
138
|
+
|
|
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
|
|
144
|
+
|
|
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
|
|
152
|
+
done
|
|
153
|
+
|
|
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
|
|
178
|
+
}
|
|
179
|
+
|
|
136
180
|
render_all() {
|
|
137
181
|
render_header
|
|
138
182
|
render_queue_summary
|
|
139
183
|
render_teams
|
|
184
|
+
render_bottleneck_alert
|
|
140
185
|
echo ""
|
|
141
186
|
render_features
|
|
142
187
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-monitor.sh —
|
|
3
|
-
#
|
|
2
|
+
# harness-monitor.sh — Agent Lifecycle Monitor
|
|
3
|
+
#
|
|
4
|
+
# v3 모드: 단일 이벤트 스트림 (progress.json 변경 감지 + audit.log)
|
|
5
|
+
# v4 모드: 팀별 섹션 분리 (feature-queue.json + progress.log 기반)
|
|
6
|
+
#
|
|
4
7
|
# Usage: bash scripts/harness-monitor.sh [project-root]
|
|
5
8
|
|
|
6
9
|
set -uo pipefail
|
|
@@ -17,8 +20,9 @@ if [ -z "$PROJECT_ROOT" ]; then
|
|
|
17
20
|
fi
|
|
18
21
|
|
|
19
22
|
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
23
|
+
PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
|
|
20
24
|
AUDIT_LOG="$PROJECT_ROOT/.harness/actions/audit.log"
|
|
21
|
-
|
|
25
|
+
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
22
26
|
|
|
23
27
|
# ── ANSI helpers ──
|
|
24
28
|
BOLD="\033[1m"
|
|
@@ -28,28 +32,169 @@ YELLOW="\033[33m"
|
|
|
28
32
|
RED="\033[31m"
|
|
29
33
|
CYAN="\033[36m"
|
|
30
34
|
MAGENTA="\033[35m"
|
|
35
|
+
BLUE="\033[34m"
|
|
31
36
|
RESET="\033[0m"
|
|
32
37
|
|
|
38
|
+
# ── Team colors ──
|
|
39
|
+
T1_COLOR="$CYAN"
|
|
40
|
+
T2_COLOR="$MAGENTA"
|
|
41
|
+
T3_COLOR="$YELLOW"
|
|
42
|
+
|
|
43
|
+
team_color() {
|
|
44
|
+
case "$1" in
|
|
45
|
+
1|team-1) echo "$T1_COLOR" ;;
|
|
46
|
+
2|team-2) echo "$T2_COLOR" ;;
|
|
47
|
+
3|team-3) echo "$T3_COLOR" ;;
|
|
48
|
+
*) echo "$DIM" ;;
|
|
49
|
+
esac
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ══════════════════════════════════════════
|
|
53
|
+
# v4 모드: 팀별 섹션 렌더링
|
|
54
|
+
# ══════════════════════════════════════════
|
|
55
|
+
|
|
56
|
+
render_v4_header() {
|
|
57
|
+
echo -e "${BOLD}TEAM MONITOR${RESET} ${DIM}$(date +%H:%M:%S)${RESET}"
|
|
58
|
+
echo ""
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render_team_section() {
|
|
62
|
+
local team_num="$1"
|
|
63
|
+
local color
|
|
64
|
+
color=$(team_color "$team_num")
|
|
65
|
+
|
|
66
|
+
# 팀 상태 from queue
|
|
67
|
+
local t_status="idle" t_feature="—" t_phase="—" t_attempt=""
|
|
68
|
+
if [ -f "$QUEUE" ]; then
|
|
69
|
+
t_status=$(jq -r ".teams[\"$team_num\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
|
|
70
|
+
t_feature=$(jq -r ".teams[\"$team_num\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
|
|
71
|
+
if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
|
|
72
|
+
t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
|
|
73
|
+
t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# 상태 아이콘
|
|
78
|
+
local icon="○"
|
|
79
|
+
case "$t_status" in
|
|
80
|
+
busy) icon="▶" ;;
|
|
81
|
+
idle) icon="○" ;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
# Phase 표시
|
|
85
|
+
local phase_str=""
|
|
86
|
+
case "$t_phase" in
|
|
87
|
+
gen) phase_str="Gen" ;;
|
|
88
|
+
gate) phase_str="Gate" ;;
|
|
89
|
+
eval) phase_str="Eval" ;;
|
|
90
|
+
*) phase_str="" ;;
|
|
91
|
+
esac
|
|
92
|
+
|
|
93
|
+
# 헤더 라인
|
|
94
|
+
printf "%b%b T%s%b " "$color" "$icon" "$team_num" "$RESET"
|
|
95
|
+
if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
|
|
96
|
+
printf "%s " "$t_feature"
|
|
97
|
+
if [ -n "$phase_str" ]; then
|
|
98
|
+
printf "%b%s%b" "$color" "$phase_str" "$RESET"
|
|
99
|
+
fi
|
|
100
|
+
if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ] && [ "$t_attempt" != "1" ]; then
|
|
101
|
+
printf " %b#%s%b" "$DIM" "$t_attempt" "$RESET"
|
|
102
|
+
fi
|
|
103
|
+
else
|
|
104
|
+
printf "%bidle%b" "$DIM" "$RESET"
|
|
105
|
+
fi
|
|
106
|
+
echo ""
|
|
107
|
+
|
|
108
|
+
# 팀 로그 (progress.log에서 team-N 엔트리만 추출)
|
|
109
|
+
if [ -f "$PROGRESS_LOG" ]; then
|
|
110
|
+
grep -i "team-${team_num}\|team_${team_num}" "$PROGRESS_LOG" 2>/dev/null | tail -5 | while IFS= read -r line; do
|
|
111
|
+
local ts action detail
|
|
112
|
+
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
113
|
+
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
114
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
115
|
+
|
|
116
|
+
local short_ts
|
|
117
|
+
short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
|
|
118
|
+
|
|
119
|
+
if [ ${#detail} -gt 35 ]; then detail="${detail:0:33}.."; fi
|
|
120
|
+
|
|
121
|
+
# 액션별 아이콘
|
|
122
|
+
local a_icon="·" a_color="$DIM"
|
|
123
|
+
case "$action" in
|
|
124
|
+
gen) a_icon="▶"; a_color="$GREEN" ;;
|
|
125
|
+
eval) a_icon="✦"; a_color="$BLUE" ;;
|
|
126
|
+
pass) a_icon="✓"; a_color="$GREEN" ;;
|
|
127
|
+
fail) a_icon="✗"; a_color="$RED" ;;
|
|
128
|
+
dequeue) a_icon="→"; a_color="$CYAN" ;;
|
|
129
|
+
gate) a_icon="◆"; a_color="$YELLOW" ;;
|
|
130
|
+
*) a_icon="·"; a_color="$DIM" ;;
|
|
131
|
+
esac
|
|
132
|
+
|
|
133
|
+
printf " %b%s%b %b%s%b %b%s%b\n" \
|
|
134
|
+
"$DIM" "$short_ts" "$RESET" \
|
|
135
|
+
"$a_color" "$a_icon" "$RESET" \
|
|
136
|
+
"$DIM" "$detail" "$RESET"
|
|
137
|
+
done
|
|
138
|
+
fi
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
render_v4() {
|
|
142
|
+
render_v4_header
|
|
143
|
+
|
|
144
|
+
# 팀 수 확인
|
|
145
|
+
local team_count=3
|
|
146
|
+
if [ -f "$QUEUE" ]; then
|
|
147
|
+
team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null || echo 3)
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
for i in $(seq 1 "$team_count"); do
|
|
151
|
+
render_team_section "$i"
|
|
152
|
+
echo ""
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
# Lead/시스템 이벤트 (team이 아닌 엔트리)
|
|
156
|
+
echo -e "${BOLD}SYSTEM${RESET}"
|
|
157
|
+
if [ -f "$PROGRESS_LOG" ]; then
|
|
158
|
+
grep -v 'team-[0-9]' "$PROGRESS_LOG" 2>/dev/null | grep -v '^#' | grep -v '^$' | tail -5 | while IFS= read -r line; do
|
|
159
|
+
local ts agent action detail
|
|
160
|
+
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
161
|
+
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
162
|
+
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
163
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
164
|
+
|
|
165
|
+
local short_ts
|
|
166
|
+
short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
|
|
167
|
+
|
|
168
|
+
if [ ${#detail} -gt 35 ]; then detail="${detail:0:33}.."; fi
|
|
169
|
+
|
|
170
|
+
printf " %b%s%b %s %b%s%b\n" \
|
|
171
|
+
"$DIM" "$short_ts" "$RESET" \
|
|
172
|
+
"$agent" \
|
|
173
|
+
"$DIM" "$detail" "$RESET"
|
|
174
|
+
done
|
|
175
|
+
fi
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# ══════════════════════════════════════════
|
|
179
|
+
# v3 모드: 단일 스트림 (기존 동작)
|
|
180
|
+
# ══════════════════════════════════════════
|
|
181
|
+
|
|
33
182
|
LAST_AGENT=""
|
|
34
183
|
LAST_STATUS=""
|
|
35
184
|
LAST_SPRINT=""
|
|
36
|
-
EVENT_COUNT=0
|
|
37
185
|
|
|
38
186
|
print_event() {
|
|
39
187
|
local ts="$1" icon="$2" color="$3" msg="$4"
|
|
40
|
-
EVENT_COUNT=$((EVENT_COUNT + 1))
|
|
41
188
|
echo -e " ${DIM}${ts}${RESET} ${color}${icon}${RESET} ${msg}"
|
|
42
189
|
}
|
|
43
190
|
|
|
44
|
-
|
|
45
|
-
print_header() {
|
|
191
|
+
print_v3_header() {
|
|
46
192
|
echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
|
|
47
193
|
echo -e "${BOLD}║ AGENT LIFECYCLE MONITOR ║${RESET}"
|
|
48
194
|
echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
|
|
49
195
|
echo ""
|
|
50
196
|
}
|
|
51
197
|
|
|
52
|
-
# ── Detect agent transitions from progress.json ──
|
|
53
198
|
check_transitions() {
|
|
54
199
|
if [ ! -f "$PROGRESS" ]; then return; fi
|
|
55
200
|
|
|
@@ -61,39 +206,22 @@ check_transitions() {
|
|
|
61
206
|
local now
|
|
62
207
|
now=$(date +"%H:%M:%S")
|
|
63
208
|
|
|
64
|
-
# Sprint change
|
|
65
209
|
if [ "$sprint" != "$LAST_SPRINT" ] && [ -n "$LAST_SPRINT" ]; then
|
|
66
210
|
print_event "$now" ">>>" "$MAGENTA" "${BOLD}Sprint ${sprint} started${RESET}"
|
|
67
211
|
echo ""
|
|
68
212
|
fi
|
|
69
213
|
|
|
70
|
-
# Agent change
|
|
71
214
|
if [ "$agent" != "$LAST_AGENT" ] && [ -n "$LAST_AGENT" ]; then
|
|
72
215
|
if [ "$LAST_AGENT" != "none" ] && [ "$LAST_AGENT" != "null" ]; then
|
|
73
216
|
print_event "$now" " ✓ " "$GREEN" "${LAST_AGENT} → ${BOLD}done${RESET}"
|
|
74
217
|
fi
|
|
75
218
|
if [ "$agent" != "none" ] && [ "$agent" != "null" ]; then
|
|
76
219
|
print_event "$now" " ▶ " "$CYAN" "${BOLD}${agent}${RESET} started"
|
|
77
|
-
|
|
78
|
-
# Show handoff context if available
|
|
79
|
-
if [ -f "$HANDOFF" ]; then
|
|
80
|
-
local from focus
|
|
81
|
-
from=$(jq -r '.from // empty' "$HANDOFF" 2>/dev/null)
|
|
82
|
-
focus=$(jq -r '.focus_features // [] | join(", ")' "$HANDOFF" 2>/dev/null)
|
|
83
|
-
if [ -n "$from" ] && [ "$from" != "null" ]; then
|
|
84
|
-
echo -e " ${DIM}handoff from: ${from}${RESET}"
|
|
85
|
-
fi
|
|
86
|
-
if [ -n "$focus" ] && [ "$focus" != "null" ]; then
|
|
87
|
-
echo -e " ${DIM}focus: ${focus}${RESET}"
|
|
88
|
-
fi
|
|
89
|
-
fi
|
|
90
220
|
fi
|
|
91
221
|
fi
|
|
92
222
|
|
|
93
|
-
# Status change (same agent)
|
|
94
223
|
if [ "$agent" = "$LAST_AGENT" ] && [ "$status" != "$LAST_STATUS" ] && [ -n "$LAST_STATUS" ]; then
|
|
95
|
-
local status_color="$RESET"
|
|
96
|
-
local status_icon="•"
|
|
224
|
+
local status_color="$RESET" status_icon="•"
|
|
97
225
|
case "$status" in
|
|
98
226
|
running) status_color="$GREEN"; status_icon=" ▶ " ;;
|
|
99
227
|
completed) status_color="$CYAN"; status_icon=" ✓ " ;;
|
|
@@ -103,7 +231,6 @@ check_transitions() {
|
|
|
103
231
|
esac
|
|
104
232
|
print_event "$now" "$status_icon" "$status_color" "${agent} → ${BOLD}${status}${RESET}"
|
|
105
233
|
|
|
106
|
-
# Show failure detail
|
|
107
234
|
if [ "$status" = "failed" ]; then
|
|
108
235
|
local fail_msg
|
|
109
236
|
fail_msg=$(jq -r '.failure.message // empty' "$PROGRESS" 2>/dev/null)
|
|
@@ -118,76 +245,84 @@ check_transitions() {
|
|
|
118
245
|
LAST_SPRINT="$sprint"
|
|
119
246
|
}
|
|
120
247
|
|
|
121
|
-
# ── Stream audit.log new lines ──
|
|
122
248
|
stream_audit() {
|
|
123
249
|
if [ ! -f "$AUDIT_LOG" ]; then return; fi
|
|
124
|
-
|
|
125
|
-
# Use tail -f in background, parse each new line
|
|
126
250
|
tail -n 0 -f "$AUDIT_LOG" 2>/dev/null | while IFS= read -r line; do
|
|
127
|
-
# Skip comments/headers
|
|
128
251
|
if [[ "$line" == "#"* ]] || [[ -z "$line" ]]; then continue; fi
|
|
129
|
-
|
|
130
|
-
# Parse: TIMESTAMP | AGENT | ACTION | STATUS | TARGET | DETAIL
|
|
131
|
-
local ts agent action status target detail
|
|
252
|
+
local ts agent action status target
|
|
132
253
|
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
133
254
|
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
134
255
|
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
135
256
|
status=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
136
257
|
target=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
|
|
137
|
-
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$6); print $6}')
|
|
138
258
|
|
|
139
|
-
# Color by status
|
|
140
259
|
local color="$RESET" icon="•"
|
|
141
260
|
case "$status" in
|
|
142
|
-
start) color="$CYAN";
|
|
143
|
-
complete) color="$GREEN";
|
|
144
|
-
fail) color="$RED";
|
|
145
|
-
pass) color="$GREEN";
|
|
146
|
-
skip) color="$DIM";
|
|
261
|
+
start) color="$CYAN"; icon="▶" ;;
|
|
262
|
+
complete) color="$GREEN"; icon="✓" ;;
|
|
263
|
+
fail) color="$RED"; icon="✗" ;;
|
|
264
|
+
pass) color="$GREEN"; icon="✓" ;;
|
|
265
|
+
skip) color="$DIM"; icon="–" ;;
|
|
147
266
|
esac
|
|
148
267
|
|
|
149
|
-
# Short timestamp (HH:MM:SS from ISO)
|
|
150
268
|
local short_ts
|
|
151
269
|
short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}:[0-9]{2}' || echo "$ts")
|
|
152
|
-
|
|
153
|
-
echo -e " ${DIM}${short_ts}${RESET} ${color}${icon}${RESET} ${BOLD}${agent}${RESET} ${action} ${target} ${DIM}${detail}${RESET}"
|
|
270
|
+
echo -e " ${DIM}${short_ts}${RESET} ${color}${icon}${RESET} ${BOLD}${agent}${RESET} ${action} ${target}"
|
|
154
271
|
done &
|
|
155
272
|
AUDIT_TAIL_PID=$!
|
|
156
273
|
}
|
|
157
274
|
|
|
158
|
-
#
|
|
275
|
+
# ══════════════════════════════════════════
|
|
276
|
+
# Mode detection & main loop
|
|
277
|
+
# ══════════════════════════════════════════
|
|
278
|
+
|
|
159
279
|
cleanup() {
|
|
160
280
|
if [ -n "${AUDIT_TAIL_PID:-}" ]; then
|
|
161
281
|
kill "$AUDIT_TAIL_PID" 2>/dev/null || true
|
|
162
282
|
fi
|
|
283
|
+
tput cnorm 2>/dev/null
|
|
163
284
|
exit 0
|
|
164
285
|
}
|
|
165
286
|
trap cleanup EXIT INT TERM
|
|
166
287
|
|
|
167
|
-
#
|
|
168
|
-
|
|
288
|
+
# 매 루프마다 v4/v3 동적 감지 — 시작 후에도 모드 전환 가능
|
|
289
|
+
tput civis 2>/dev/null
|
|
290
|
+
clear
|
|
169
291
|
|
|
170
|
-
|
|
171
|
-
if [ -f "$PROGRESS" ]; then
|
|
172
|
-
LAST_AGENT=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
173
|
-
LAST_STATUS=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
174
|
-
LAST_SPRINT=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
|
|
175
|
-
fi
|
|
292
|
+
CURRENT_MODE=""
|
|
176
293
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
fi
|
|
294
|
+
while true; do
|
|
295
|
+
# 동적 모드 감지
|
|
296
|
+
if [ -f "$QUEUE" ]; then
|
|
297
|
+
NEW_MODE="v4"
|
|
298
|
+
else
|
|
299
|
+
NEW_MODE="v3"
|
|
300
|
+
fi
|
|
185
301
|
|
|
186
|
-
#
|
|
187
|
-
|
|
302
|
+
# 모드 전환 시 화면 초기화
|
|
303
|
+
if [ "$NEW_MODE" != "$CURRENT_MODE" ]; then
|
|
304
|
+
clear
|
|
305
|
+
CURRENT_MODE="$NEW_MODE"
|
|
306
|
+
if [ "$CURRENT_MODE" = "v3" ]; then
|
|
307
|
+
# v3 초기화
|
|
308
|
+
if [ -f "$PROGRESS" ]; then
|
|
309
|
+
LAST_AGENT=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
310
|
+
LAST_STATUS=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
311
|
+
LAST_SPRINT=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
|
|
312
|
+
fi
|
|
313
|
+
stream_audit
|
|
314
|
+
fi
|
|
315
|
+
fi
|
|
188
316
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
317
|
+
if [ "$CURRENT_MODE" = "v4" ]; then
|
|
318
|
+
buf=$(render_v4 2>&1)
|
|
319
|
+
tput cup 0 0 2>/dev/null
|
|
320
|
+
echo "$buf"
|
|
321
|
+
tput ed 2>/dev/null
|
|
322
|
+
else
|
|
323
|
+
check_transitions
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
sleep 3
|
|
193
327
|
done
|
|
328
|
+
|