claude-comms 1.0.46 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/README.md +12 -13
- package/package.json +6 -10
- package/.agents/AGENTS.md +0 -61
- package/.agents/repo.md +0 -18
- package/.agents/tools/README.md +0 -45
- package/.agents/tools/agent-job/README.md +0 -208
- package/.agents/tools/agent-job/__pycache__/agent_monitor.cpython-312.pyc +0 -0
- package/.agents/tools/agent-job/agent_job.py +0 -936
- package/.agents/tools/agent-job/agent_monitor.py +0 -575
- package/.agents/tools/agent-job/consultant_template.txt +0 -73
- package/.agents/tools/chrome-devtools/README.md +0 -101
- package/.agents/tools/chrome-devtools/browsertools.py +0 -839
- package/.agents/tools/chrome-devtools/config.json +0 -24
- package/.agents/tools/workflow/CONSUMER_SETUP.md +0 -114
- package/.agents/tools/workflow/ONBOARD.md +0 -167
- package/.agents/tools/workflow/README.md +0 -169
- package/.agents/tools/workflow/cli.ts +0 -726
- package/.agents/tools/workflow/config.example.json +0 -16
- package/.agents/tools/workflow/init.ts +0 -76
- package/.agents/tools/workflow/lib/file-tracker.test.ts +0 -81
- package/.agents/tools/workflow/lib/file-tracker.ts +0 -387
- package/.agents/tools/workflow/lib/harness-executor.ts +0 -649
- package/.agents/tools/workflow/lib/prompts.scenario.test.ts +0 -262
- package/.agents/tools/workflow/lib/prompts.ts +0 -548
- package/.agents/tools/workflow/lib/streams.test.ts +0 -140
- package/.agents/tools/workflow/lib/streams.ts +0 -254
- package/.agents/tools/workflow/runner.ts +0 -994
- package/.agents/tools/workflow/templates/document.md +0 -42
- package/.agents/tools/workflow/templates/implement.md +0 -117
- package/.agents/tools/workflow/templates/plan.md +0 -54
- package/.agents/tools/workflow/templates/pm.md +0 -84
- package/.agents/tools/workflow/templates/product-owner.md +0 -211
- package/.agents/tools/workflow/templates/review.md +0 -56
- package/.agents/tools/workflow/templates/uat.md +0 -71
- package/.claude/agents/core/engineer.md +0 -146
- package/.claude/commands/action-feedback-item.md +0 -27
- package/.claude/commands/install-feedback-widget.md +0 -599
- package/.claude/commands/learn.md +0 -32
- package/.claude/hooks/comms/__pycache__/register_subagent.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/__pycache__/update_subagent_completion.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/get_unread_messages.py +0 -71
- package/.claude/hooks/comms/register_subagent.py +0 -74
- package/.claude/hooks/comms/send_message.py +0 -87
- package/.claude/hooks/comms/update_subagent_completion.py +0 -126
- package/.claude/hooks/context/session_id.py +0 -35
- package/.claude/hooks/observability/send_event.py +0 -110
- package/.claude/hooks/session-data/get_session_data.py +0 -261
- package/.claude/hooks/session-data/intercept_session_id.py +0 -69
- package/.claude/hooks/utils/__pycache__/server_config.cpython-312.pyc +0 -0
- package/.claude/hooks/utils/server_config.py +0 -17
- package/.claude/settings.json +0 -108
- package/.claude/settings.local.json +0 -46
- package/CLAUDE.md +0 -240
|
@@ -1,575 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.11"
|
|
4
|
-
# dependencies = []
|
|
5
|
-
# ///
|
|
6
|
-
"""
|
|
7
|
-
Agent Job Monitor - TUI for monitoring agent jobs.
|
|
8
|
-
|
|
9
|
-
Real-time display of all agent jobs with status indicators.
|
|
10
|
-
Similar to `top` but for agent jobs.
|
|
11
|
-
|
|
12
|
-
Usage:
|
|
13
|
-
uv run agent_monitor.py [--interval SECONDS]
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import curses
|
|
18
|
-
import json
|
|
19
|
-
import os
|
|
20
|
-
import signal
|
|
21
|
-
import subprocess
|
|
22
|
-
import sys
|
|
23
|
-
import time
|
|
24
|
-
from dataclasses import dataclass
|
|
25
|
-
from datetime import datetime, timezone
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Optional
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@dataclass
|
|
31
|
-
class JobInfo:
|
|
32
|
-
job_id: str
|
|
33
|
-
harness: str
|
|
34
|
-
status: str
|
|
35
|
-
status_reason: Optional[str]
|
|
36
|
-
runtime_sec: float
|
|
37
|
-
idle_sec: Optional[float]
|
|
38
|
-
operations: int
|
|
39
|
-
start_time: Optional[str]
|
|
40
|
-
logs: Optional[str] = None
|
|
41
|
-
pid: Optional[int] = None
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def get_jobs() -> list[JobInfo]:
|
|
45
|
-
"""Fetch current job list via agent_job.py"""
|
|
46
|
-
script_dir = Path(__file__).parent
|
|
47
|
-
agent_job = script_dir / "agent_job.py"
|
|
48
|
-
|
|
49
|
-
try:
|
|
50
|
-
result = subprocess.run(
|
|
51
|
-
["uv", "run", str(agent_job), "list"],
|
|
52
|
-
capture_output=True,
|
|
53
|
-
text=True,
|
|
54
|
-
timeout=10,
|
|
55
|
-
)
|
|
56
|
-
if result.returncode != 0:
|
|
57
|
-
return []
|
|
58
|
-
|
|
59
|
-
jobs_data = json.loads(result.stdout)
|
|
60
|
-
jobs = []
|
|
61
|
-
for job in jobs_data:
|
|
62
|
-
jobs.append(JobInfo(
|
|
63
|
-
job_id=job.get("job_id", ""),
|
|
64
|
-
harness=job.get("harness", ""),
|
|
65
|
-
status=job.get("status", "unknown"),
|
|
66
|
-
status_reason=job.get("status_reason"),
|
|
67
|
-
runtime_sec=job.get("runtime_sec", 0),
|
|
68
|
-
idle_sec=job.get("idle_sec"),
|
|
69
|
-
operations=job.get("operations", 0),
|
|
70
|
-
start_time=job.get("start_time"),
|
|
71
|
-
logs=job.get("logs"),
|
|
72
|
-
pid=job.get("pid"),
|
|
73
|
-
))
|
|
74
|
-
return jobs
|
|
75
|
-
except Exception:
|
|
76
|
-
return []
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def read_log_file(log_path: str) -> list[str]:
|
|
80
|
-
"""Read log file and return lines"""
|
|
81
|
-
try:
|
|
82
|
-
with open(log_path, "r") as f:
|
|
83
|
-
return f.readlines()
|
|
84
|
-
except Exception:
|
|
85
|
-
return ["(Unable to read log file)"]
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def get_job_status(job_id: str) -> Optional[dict]:
|
|
89
|
-
"""Fetch detailed status for a single job"""
|
|
90
|
-
script_dir = Path(__file__).parent
|
|
91
|
-
agent_job = script_dir / "agent_job.py"
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
result = subprocess.run(
|
|
95
|
-
["uv", "run", str(agent_job), "status", job_id],
|
|
96
|
-
capture_output=True,
|
|
97
|
-
text=True,
|
|
98
|
-
timeout=10,
|
|
99
|
-
)
|
|
100
|
-
if result.returncode != 0:
|
|
101
|
-
return None
|
|
102
|
-
return json.loads(result.stdout)
|
|
103
|
-
except Exception:
|
|
104
|
-
return None
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def kill_job(pid: int) -> bool:
|
|
108
|
-
"""Kill a job by PID. Returns True if successful."""
|
|
109
|
-
if not pid:
|
|
110
|
-
return False
|
|
111
|
-
try:
|
|
112
|
-
os.kill(pid, signal.SIGTERM)
|
|
113
|
-
# Give it a moment, then force kill if still running
|
|
114
|
-
time.sleep(0.5)
|
|
115
|
-
try:
|
|
116
|
-
os.kill(pid, 0) # Check if still running
|
|
117
|
-
os.kill(pid, signal.SIGKILL) # Force kill
|
|
118
|
-
except ProcessLookupError:
|
|
119
|
-
pass # Already dead
|
|
120
|
-
return True
|
|
121
|
-
except ProcessLookupError:
|
|
122
|
-
return True # Already dead
|
|
123
|
-
except PermissionError:
|
|
124
|
-
return False
|
|
125
|
-
except Exception:
|
|
126
|
-
return False
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def format_duration(seconds: float) -> str:
|
|
130
|
-
"""Format seconds as human-readable duration"""
|
|
131
|
-
if seconds < 60:
|
|
132
|
-
return f"{seconds:.1f}s"
|
|
133
|
-
elif seconds < 3600:
|
|
134
|
-
mins = int(seconds // 60)
|
|
135
|
-
secs = int(seconds % 60)
|
|
136
|
-
return f"{mins}m{secs:02d}s"
|
|
137
|
-
else:
|
|
138
|
-
hours = int(seconds // 3600)
|
|
139
|
-
mins = int((seconds % 3600) // 60)
|
|
140
|
-
return f"{hours}h{mins:02d}m"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def format_time_ago(iso_time: Optional[str]) -> str:
|
|
144
|
-
"""Format ISO time as 'X ago'"""
|
|
145
|
-
if not iso_time:
|
|
146
|
-
return "?"
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
if iso_time.endswith("Z"):
|
|
150
|
-
iso_time = iso_time[:-1] + "+00:00"
|
|
151
|
-
dt = datetime.fromisoformat(iso_time)
|
|
152
|
-
now = datetime.now(timezone.utc)
|
|
153
|
-
delta = (now - dt).total_seconds()
|
|
154
|
-
return format_duration(delta) + " ago"
|
|
155
|
-
except Exception:
|
|
156
|
-
return "?"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def truncate(s: str, max_len: int) -> str:
|
|
160
|
-
"""Truncate string with ellipsis if too long"""
|
|
161
|
-
if len(s) <= max_len:
|
|
162
|
-
return s
|
|
163
|
-
return s[:max_len-1] + "…"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def wrap_line(line: str, width: int, max_lines: int = 5) -> list[str]:
|
|
167
|
-
"""Wrap a line to fit within width, max number of wrapped lines.
|
|
168
|
-
|
|
169
|
-
Returns a list of display lines. If content exceeds max_lines,
|
|
170
|
-
the last line is truncated with ellipsis.
|
|
171
|
-
"""
|
|
172
|
-
line = line.rstrip('\n\r')
|
|
173
|
-
if not line:
|
|
174
|
-
return [""]
|
|
175
|
-
|
|
176
|
-
if len(line) <= width:
|
|
177
|
-
return [line]
|
|
178
|
-
|
|
179
|
-
wrapped = []
|
|
180
|
-
remaining = line
|
|
181
|
-
|
|
182
|
-
while remaining and len(wrapped) < max_lines:
|
|
183
|
-
if len(remaining) <= width:
|
|
184
|
-
wrapped.append(remaining)
|
|
185
|
-
remaining = ""
|
|
186
|
-
else:
|
|
187
|
-
# Check if this will be the last allowed line
|
|
188
|
-
if len(wrapped) == max_lines - 1:
|
|
189
|
-
# Truncate with ellipsis
|
|
190
|
-
wrapped.append(remaining[:width-1] + "…")
|
|
191
|
-
remaining = ""
|
|
192
|
-
else:
|
|
193
|
-
wrapped.append(remaining[:width])
|
|
194
|
-
remaining = remaining[width:]
|
|
195
|
-
|
|
196
|
-
return wrapped if wrapped else [""]
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
class Monitor:
|
|
200
|
-
def __init__(self, stdscr, interval: float = 2.0):
|
|
201
|
-
self.stdscr = stdscr
|
|
202
|
-
self.interval = interval
|
|
203
|
-
self.jobs: list[JobInfo] = []
|
|
204
|
-
self.selected_idx = 0
|
|
205
|
-
self.scroll_offset = 0
|
|
206
|
-
self.show_log = False
|
|
207
|
-
self.log_job: Optional[JobInfo] = None
|
|
208
|
-
self.log_lines: list[str] = []
|
|
209
|
-
self.log_scroll = 0
|
|
210
|
-
self.log_display_line_count = 0 # Cached count of wrapped display lines
|
|
211
|
-
|
|
212
|
-
# Setup colors
|
|
213
|
-
curses.start_color()
|
|
214
|
-
curses.use_default_colors()
|
|
215
|
-
curses.init_pair(1, curses.COLOR_GREEN, -1) # complete
|
|
216
|
-
curses.init_pair(2, curses.COLOR_YELLOW, -1) # running
|
|
217
|
-
curses.init_pair(3, curses.COLOR_RED, -1) # error/timeout
|
|
218
|
-
curses.init_pair(4, curses.COLOR_CYAN, -1) # header
|
|
219
|
-
curses.init_pair(5, curses.COLOR_MAGENTA, -1) # selected
|
|
220
|
-
curses.init_pair(6, curses.COLOR_WHITE, -1) # normal
|
|
221
|
-
|
|
222
|
-
# Hide cursor
|
|
223
|
-
curses.curs_set(0)
|
|
224
|
-
|
|
225
|
-
# Non-blocking input
|
|
226
|
-
self.stdscr.nodelay(True)
|
|
227
|
-
self.stdscr.keypad(True)
|
|
228
|
-
|
|
229
|
-
def get_status_color(self, status: str) -> int:
|
|
230
|
-
"""Get color pair for status"""
|
|
231
|
-
if status == "complete":
|
|
232
|
-
return curses.color_pair(1)
|
|
233
|
-
elif status == "running":
|
|
234
|
-
return curses.color_pair(2)
|
|
235
|
-
elif status in ("error", "timeout"):
|
|
236
|
-
return curses.color_pair(3)
|
|
237
|
-
return curses.color_pair(6)
|
|
238
|
-
|
|
239
|
-
def get_status_indicator(self, status: str) -> str:
|
|
240
|
-
"""Get status indicator symbol"""
|
|
241
|
-
if status == "complete":
|
|
242
|
-
return "✓"
|
|
243
|
-
elif status == "running":
|
|
244
|
-
return "●"
|
|
245
|
-
elif status == "error":
|
|
246
|
-
return "✗"
|
|
247
|
-
elif status == "timeout":
|
|
248
|
-
return "⏱"
|
|
249
|
-
return "?"
|
|
250
|
-
|
|
251
|
-
def draw_header(self):
|
|
252
|
-
"""Draw the header bar"""
|
|
253
|
-
height, width = self.stdscr.getmaxyx()
|
|
254
|
-
|
|
255
|
-
# Title
|
|
256
|
-
title = " Agent Job Monitor "
|
|
257
|
-
now = datetime.now().strftime("%H:%M:%S")
|
|
258
|
-
header = f"{'─' * 2}{title}{'─' * (width - len(title) - len(now) - 6)} {now} ─"
|
|
259
|
-
|
|
260
|
-
self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
|
|
261
|
-
self.stdscr.addnstr(0, 0, header, width - 1)
|
|
262
|
-
self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
|
|
263
|
-
|
|
264
|
-
def draw_column_headers(self, y: int):
|
|
265
|
-
"""Draw column headers"""
|
|
266
|
-
height, width = self.stdscr.getmaxyx()
|
|
267
|
-
|
|
268
|
-
# Column layout
|
|
269
|
-
headers = f"{'ST':<3} {'HARNESS':<7} {'PID':<7} {'JOB ID':<20} {'STATUS':<26} {'STARTED':<10} {'RUNTIME':<8} {'OPS':<4}"
|
|
270
|
-
|
|
271
|
-
self.stdscr.attron(curses.color_pair(4) | curses.A_REVERSE)
|
|
272
|
-
self.stdscr.addnstr(y, 0, headers.ljust(width - 1), width - 1)
|
|
273
|
-
self.stdscr.attroff(curses.color_pair(4) | curses.A_REVERSE)
|
|
274
|
-
|
|
275
|
-
def draw_job_row(self, y: int, job: JobInfo, selected: bool):
|
|
276
|
-
"""Draw a single job row"""
|
|
277
|
-
height, width = self.stdscr.getmaxyx()
|
|
278
|
-
|
|
279
|
-
indicator = self.get_status_indicator(job.status)
|
|
280
|
-
color = self.get_status_color(job.status)
|
|
281
|
-
|
|
282
|
-
# Build status reason display
|
|
283
|
-
reason = job.status_reason or job.status
|
|
284
|
-
if job.status == "running" and job.idle_sec is not None:
|
|
285
|
-
reason = f"{reason} (idle {format_duration(job.idle_sec)})"
|
|
286
|
-
|
|
287
|
-
# Format start time as HH:MM:SS
|
|
288
|
-
start_display = "?"
|
|
289
|
-
if job.start_time:
|
|
290
|
-
try:
|
|
291
|
-
# Parse ISO time and format as local time
|
|
292
|
-
ts = job.start_time
|
|
293
|
-
if ts.endswith("Z"):
|
|
294
|
-
ts = ts[:-1] + "+00:00"
|
|
295
|
-
dt = datetime.fromisoformat(ts)
|
|
296
|
-
start_display = dt.strftime("%H:%M:%S")
|
|
297
|
-
except Exception:
|
|
298
|
-
start_display = "?"
|
|
299
|
-
|
|
300
|
-
# Format PID
|
|
301
|
-
pid_display = str(job.pid) if job.pid else "?"
|
|
302
|
-
|
|
303
|
-
# Format row
|
|
304
|
-
job_id_short = job.job_id[:18] if len(job.job_id) > 18 else job.job_id
|
|
305
|
-
row = f" {indicator} {job.harness:<7} {pid_display:<7} {job_id_short:<20} {truncate(reason, 24):<26} {start_display:<10} {format_duration(job.runtime_sec):<8} {job.operations:<4}"
|
|
306
|
-
|
|
307
|
-
if selected:
|
|
308
|
-
self.stdscr.attron(curses.color_pair(5) | curses.A_REVERSE)
|
|
309
|
-
self.stdscr.addnstr(y, 0, row.ljust(width - 1), width - 1)
|
|
310
|
-
self.stdscr.attroff(curses.color_pair(5) | curses.A_REVERSE)
|
|
311
|
-
else:
|
|
312
|
-
self.stdscr.attron(color)
|
|
313
|
-
self.stdscr.addnstr(y, 0, row.ljust(width - 1), width - 1)
|
|
314
|
-
self.stdscr.attroff(color)
|
|
315
|
-
|
|
316
|
-
def draw_job_list(self):
|
|
317
|
-
"""Draw the job list"""
|
|
318
|
-
height, width = self.stdscr.getmaxyx()
|
|
319
|
-
|
|
320
|
-
# Draw column headers
|
|
321
|
-
self.draw_column_headers(2)
|
|
322
|
-
|
|
323
|
-
# Calculate visible area
|
|
324
|
-
list_start = 3
|
|
325
|
-
list_height = height - 5 # Leave room for header, columns, and footer
|
|
326
|
-
|
|
327
|
-
# Adjust scroll offset
|
|
328
|
-
if self.selected_idx < self.scroll_offset:
|
|
329
|
-
self.scroll_offset = self.selected_idx
|
|
330
|
-
elif self.selected_idx >= self.scroll_offset + list_height:
|
|
331
|
-
self.scroll_offset = self.selected_idx - list_height + 1
|
|
332
|
-
|
|
333
|
-
# Draw visible jobs
|
|
334
|
-
for i, job in enumerate(self.jobs[self.scroll_offset:self.scroll_offset + list_height]):
|
|
335
|
-
actual_idx = i + self.scroll_offset
|
|
336
|
-
y = list_start + i
|
|
337
|
-
if y >= height - 2:
|
|
338
|
-
break
|
|
339
|
-
self.draw_job_row(y, job, actual_idx == self.selected_idx)
|
|
340
|
-
|
|
341
|
-
# Show if more items above/below
|
|
342
|
-
if self.scroll_offset > 0:
|
|
343
|
-
self.stdscr.addstr(list_start, width - 3, "▲")
|
|
344
|
-
if self.scroll_offset + list_height < len(self.jobs):
|
|
345
|
-
self.stdscr.addstr(min(list_start + list_height - 1, height - 3), width - 3, "▼")
|
|
346
|
-
|
|
347
|
-
def draw_log_view(self):
|
|
348
|
-
"""Draw scrollable log view for selected job"""
|
|
349
|
-
if not self.log_job:
|
|
350
|
-
return
|
|
351
|
-
|
|
352
|
-
height, width = self.stdscr.getmaxyx()
|
|
353
|
-
|
|
354
|
-
# Header with job info
|
|
355
|
-
job = self.log_job
|
|
356
|
-
indicator = self.get_status_indicator(job.status)
|
|
357
|
-
color = self.get_status_color(job.status)
|
|
358
|
-
|
|
359
|
-
# Job header line
|
|
360
|
-
self.stdscr.attron(curses.A_BOLD)
|
|
361
|
-
header = f" {indicator} {job.harness} | {job.job_id} | {job.status_reason or job.status} | {format_duration(job.runtime_sec)}"
|
|
362
|
-
self.stdscr.addnstr(2, 0, header, width - 1)
|
|
363
|
-
self.stdscr.attroff(curses.A_BOLD)
|
|
364
|
-
|
|
365
|
-
# Log file path
|
|
366
|
-
self.stdscr.attron(curses.color_pair(4))
|
|
367
|
-
self.stdscr.addnstr(3, 0, f" Log: {job.logs or '?'}", width - 1)
|
|
368
|
-
self.stdscr.attroff(curses.color_pair(4))
|
|
369
|
-
|
|
370
|
-
# Separator
|
|
371
|
-
self.stdscr.addnstr(4, 0, "─" * (width - 1), width - 1)
|
|
372
|
-
|
|
373
|
-
# Log content area
|
|
374
|
-
log_start = 5
|
|
375
|
-
log_height = height - log_start - 2
|
|
376
|
-
content_width = width - 4 # Leave margin for wrap indicator
|
|
377
|
-
|
|
378
|
-
# Build display lines (wrapped) from source lines
|
|
379
|
-
display_lines = []
|
|
380
|
-
for source_line in self.log_lines:
|
|
381
|
-
wrapped = wrap_line(source_line, content_width, max_lines=5)
|
|
382
|
-
display_lines.extend(wrapped)
|
|
383
|
-
|
|
384
|
-
# Cache display line count for scroll handling
|
|
385
|
-
self.log_display_line_count = len(display_lines)
|
|
386
|
-
|
|
387
|
-
# Clamp scroll to display lines
|
|
388
|
-
max_scroll = max(0, len(display_lines) - log_height)
|
|
389
|
-
self.log_scroll = max(0, min(self.log_scroll, max_scroll))
|
|
390
|
-
|
|
391
|
-
# Draw display lines
|
|
392
|
-
visible_lines = display_lines[self.log_scroll:self.log_scroll + log_height]
|
|
393
|
-
for i, line in enumerate(visible_lines):
|
|
394
|
-
y = log_start + i
|
|
395
|
-
if y >= height - 2:
|
|
396
|
-
break
|
|
397
|
-
self.stdscr.addnstr(y, 1, line, width - 2)
|
|
398
|
-
|
|
399
|
-
# Scroll indicators
|
|
400
|
-
if self.log_scroll > 0:
|
|
401
|
-
self.stdscr.attron(curses.color_pair(4))
|
|
402
|
-
self.stdscr.addstr(log_start, width - 2, "▲")
|
|
403
|
-
self.stdscr.attroff(curses.color_pair(4))
|
|
404
|
-
if self.log_scroll + log_height < len(display_lines):
|
|
405
|
-
self.stdscr.attron(curses.color_pair(4))
|
|
406
|
-
self.stdscr.addstr(min(log_start + log_height - 1, height - 3), width - 2, "▼")
|
|
407
|
-
self.stdscr.attroff(curses.color_pair(4))
|
|
408
|
-
|
|
409
|
-
# Line count
|
|
410
|
-
total = len(display_lines)
|
|
411
|
-
showing = f"Lines {self.log_scroll + 1}-{min(self.log_scroll + log_height, total)} of {total}"
|
|
412
|
-
self.stdscr.attron(curses.color_pair(4))
|
|
413
|
-
self.stdscr.addnstr(height - 2, width - len(showing) - 2, showing, len(showing))
|
|
414
|
-
self.stdscr.attroff(curses.color_pair(4))
|
|
415
|
-
|
|
416
|
-
def draw_footer(self):
|
|
417
|
-
"""Draw the footer with help"""
|
|
418
|
-
height, width = self.stdscr.getmaxyx()
|
|
419
|
-
|
|
420
|
-
# Summary
|
|
421
|
-
running = sum(1 for j in self.jobs if j.status == "running")
|
|
422
|
-
complete = sum(1 for j in self.jobs if j.status == "complete")
|
|
423
|
-
failed = sum(1 for j in self.jobs if j.status in ("error", "timeout"))
|
|
424
|
-
|
|
425
|
-
summary = f" Jobs: {len(self.jobs)} total, {running} running, {complete} complete, {failed} failed "
|
|
426
|
-
|
|
427
|
-
# Help
|
|
428
|
-
if self.show_log:
|
|
429
|
-
help_text = " [↑↓/PgUp/PgDn] Scroll [r] Reload [ESC] Back [q] Quit "
|
|
430
|
-
else:
|
|
431
|
-
help_text = " [↑↓] Select [Enter] Log [x] Kill [r] Refresh [q] Quit "
|
|
432
|
-
|
|
433
|
-
# Draw footer bar
|
|
434
|
-
footer = f"{summary}{'─' * (width - len(summary) - len(help_text) - 2)}{help_text}"
|
|
435
|
-
|
|
436
|
-
self.stdscr.attron(curses.color_pair(4))
|
|
437
|
-
self.stdscr.addnstr(height - 1, 0, footer, width - 1)
|
|
438
|
-
self.stdscr.attroff(curses.color_pair(4))
|
|
439
|
-
|
|
440
|
-
def draw(self):
|
|
441
|
-
"""Draw the full screen"""
|
|
442
|
-
self.stdscr.clear()
|
|
443
|
-
|
|
444
|
-
self.draw_header()
|
|
445
|
-
|
|
446
|
-
if self.show_log:
|
|
447
|
-
self.draw_log_view()
|
|
448
|
-
else:
|
|
449
|
-
self.draw_job_list()
|
|
450
|
-
|
|
451
|
-
self.draw_footer()
|
|
452
|
-
|
|
453
|
-
self.stdscr.refresh()
|
|
454
|
-
|
|
455
|
-
def handle_input(self) -> bool:
|
|
456
|
-
"""Handle keyboard input. Returns False to quit."""
|
|
457
|
-
try:
|
|
458
|
-
key = self.stdscr.getch()
|
|
459
|
-
except Exception:
|
|
460
|
-
return True
|
|
461
|
-
|
|
462
|
-
if key == -1:
|
|
463
|
-
return True
|
|
464
|
-
|
|
465
|
-
if key == ord('q') or key == ord('Q'):
|
|
466
|
-
return False
|
|
467
|
-
|
|
468
|
-
if self.show_log:
|
|
469
|
-
# Log view controls
|
|
470
|
-
height, _ = self.stdscr.getmaxyx()
|
|
471
|
-
log_height = height - 7 # Visible log lines
|
|
472
|
-
|
|
473
|
-
if key == 27 or key == curses.KEY_BACKSPACE: # ESC or Backspace
|
|
474
|
-
self.show_log = False
|
|
475
|
-
self.log_job = None
|
|
476
|
-
self.log_lines = []
|
|
477
|
-
self.log_scroll = 0
|
|
478
|
-
elif key == curses.KEY_UP or key == ord('k'):
|
|
479
|
-
self.log_scroll = max(0, self.log_scroll - 1)
|
|
480
|
-
elif key == curses.KEY_DOWN or key == ord('j'):
|
|
481
|
-
self.log_scroll += 1
|
|
482
|
-
elif key == curses.KEY_PPAGE: # Page Up
|
|
483
|
-
self.log_scroll = max(0, self.log_scroll - log_height)
|
|
484
|
-
elif key == curses.KEY_NPAGE: # Page Down
|
|
485
|
-
self.log_scroll += log_height
|
|
486
|
-
elif key == curses.KEY_HOME:
|
|
487
|
-
self.log_scroll = 0
|
|
488
|
-
elif key == curses.KEY_END:
|
|
489
|
-
self.log_scroll = max(0, self.log_display_line_count - log_height)
|
|
490
|
-
elif key == ord('r') or key == ord('R'):
|
|
491
|
-
# Reload log file
|
|
492
|
-
if self.log_job and self.log_job.logs:
|
|
493
|
-
self.log_lines = read_log_file(self.log_job.logs)
|
|
494
|
-
else:
|
|
495
|
-
# Job list controls
|
|
496
|
-
if key == curses.KEY_UP or key == ord('k'):
|
|
497
|
-
self.selected_idx = max(0, self.selected_idx - 1)
|
|
498
|
-
elif key == curses.KEY_DOWN or key == ord('j'):
|
|
499
|
-
if self.selected_idx < len(self.jobs) - 1:
|
|
500
|
-
self.selected_idx += 1
|
|
501
|
-
elif key == ord('\n') or key == curses.KEY_ENTER:
|
|
502
|
-
if self.jobs and 0 <= self.selected_idx < len(self.jobs):
|
|
503
|
-
job = self.jobs[self.selected_idx]
|
|
504
|
-
self.show_log = True
|
|
505
|
-
self.log_job = job
|
|
506
|
-
self.log_scroll = 0
|
|
507
|
-
if job.logs:
|
|
508
|
-
self.log_lines = read_log_file(job.logs)
|
|
509
|
-
else:
|
|
510
|
-
self.log_lines = ["(No log file available)"]
|
|
511
|
-
elif key == ord('r') or key == ord('R'):
|
|
512
|
-
self.jobs = get_jobs()
|
|
513
|
-
elif key == ord('x') or key == ord('X'):
|
|
514
|
-
# Kill selected job
|
|
515
|
-
if self.jobs and 0 <= self.selected_idx < len(self.jobs):
|
|
516
|
-
job = self.jobs[self.selected_idx]
|
|
517
|
-
if job.pid and job.status == "running":
|
|
518
|
-
kill_job(job.pid)
|
|
519
|
-
# Refresh job list after kill
|
|
520
|
-
time.sleep(0.2)
|
|
521
|
-
self.jobs = get_jobs()
|
|
522
|
-
elif key == curses.KEY_HOME:
|
|
523
|
-
self.selected_idx = 0
|
|
524
|
-
elif key == curses.KEY_END:
|
|
525
|
-
self.selected_idx = max(0, len(self.jobs) - 1)
|
|
526
|
-
|
|
527
|
-
return True
|
|
528
|
-
|
|
529
|
-
def run(self):
|
|
530
|
-
"""Main loop"""
|
|
531
|
-
last_refresh = 0
|
|
532
|
-
|
|
533
|
-
while True:
|
|
534
|
-
# Refresh data periodically
|
|
535
|
-
now = time.time()
|
|
536
|
-
if now - last_refresh >= self.interval:
|
|
537
|
-
self.jobs = get_jobs()
|
|
538
|
-
# Clamp selected index
|
|
539
|
-
if self.jobs:
|
|
540
|
-
self.selected_idx = min(self.selected_idx, len(self.jobs) - 1)
|
|
541
|
-
else:
|
|
542
|
-
self.selected_idx = 0
|
|
543
|
-
last_refresh = now
|
|
544
|
-
|
|
545
|
-
# Draw
|
|
546
|
-
self.draw()
|
|
547
|
-
|
|
548
|
-
# Handle input
|
|
549
|
-
if not self.handle_input():
|
|
550
|
-
break
|
|
551
|
-
|
|
552
|
-
# Small sleep to reduce CPU usage
|
|
553
|
-
time.sleep(0.05)
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
def main(stdscr, interval: float):
|
|
557
|
-
"""Main entry point for curses"""
|
|
558
|
-
monitor = Monitor(stdscr, interval)
|
|
559
|
-
monitor.run()
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if __name__ == "__main__":
|
|
563
|
-
parser = argparse.ArgumentParser(description="Agent Job Monitor TUI")
|
|
564
|
-
parser.add_argument(
|
|
565
|
-
"--interval", "-i",
|
|
566
|
-
type=float,
|
|
567
|
-
default=2.0,
|
|
568
|
-
help="Refresh interval in seconds (default: 2.0)"
|
|
569
|
-
)
|
|
570
|
-
args = parser.parse_args()
|
|
571
|
-
|
|
572
|
-
try:
|
|
573
|
-
curses.wrapper(lambda stdscr: main(stdscr, args.interval))
|
|
574
|
-
except KeyboardInterrupt:
|
|
575
|
-
pass
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
To deliver on your Assignment/Task You must manage and maintain your approach plan dynamically, refine your plan after every decision, and when new information presents itself.
|
|
5
|
-
Populate your initial plan with your step by step WORKFLOW:
|
|
6
|
-
|
|
7
|
-
[WORKFLOW]
|
|
8
|
-
🤝 Periodically Inbox Check after every few action, and dynamically add TEAMWORK Broadcast as per Communication Protocols 🤝
|
|
9
|
-
|
|
10
|
-
1. Consume AGENT OPERATING PROCEDURES (AOP) `.agents/AGENTS.md`. *You will execute 1 or more of Procedures in the following steps*
|
|
11
|
-
2. ALWAYS Execute AOP.CALIBRATE
|
|
12
|
-
3. *Decision:* Based on your Assignment/Task/Loop you may be either Implementing New, Assessing Only, or Review+Refine Existing. Choose your path accordingly:
|
|
13
|
-
- *Implementing New:* Execute AOP.IMPLEMENT
|
|
14
|
-
- *Assessing Only:* Execute AOP.ASSESS
|
|
15
|
-
- *Review+Refine Existing:* Execute AOP.ASSESS then AOP.IMPLEMENT
|
|
16
|
-
4. Execute AOP.VALIDATE
|
|
17
|
-
5. Decision: If there are any failures or errors from AOP.VALIDATE: Loop through steps 3 & 4 Review+Refine Existing (AOP ASSESS -> IMPLEMENT -> VALIDATE). Continue itterating until 100% pass/green. *skip this loop step for Assessing Only path*
|
|
18
|
-
6. Finally, document implementation and/or assessment in `docs/project/phases/<phase-id>/`
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*Remember:* 🤝 Broadcast ASAP when you discover, before making decisions, and immidiatly after new teammate message recieved if you have critical feedback
|
|
22
|
-
|
|
23
|
-
[/WORKFLOW]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
[TEAMWORK]
|
|
28
|
-
You are part of a cross-disciplined team, and concurrently working with team-mates toward a common objective. Team communication is critical for success.
|
|
29
|
-
You can Broadcast to and Check messages from your team-mates.
|
|
30
|
-
You MUST promptly Broadcast information that may impact their trajectory, and Inbox Check for new Broadcasts from your team-mates frequently.
|
|
31
|
-
|
|
32
|
-
🤝 Communication Protocols
|
|
33
|
-
|
|
34
|
-
**Inbox Check:**
|
|
35
|
-
- After every few reads, before every write, and definitely before making a solution approach decision, run an Inbox Check command
|
|
36
|
-
- If you don't Inbox Check frequently, you may be missing critical context from your team-mates!
|
|
37
|
-
- PONDER every message recieved from your team-mates. Does it contradict, support, or suppliment your mental model? Should you change you approach?
|
|
38
|
-
- Read source reference files provided when relevant to your task, to verify your team-mate's claims. Do this before deciding to change/adapt your approach based on message context.
|
|
39
|
-
- If the verification proves your team-mate incorrect, you must IMMEDIATLY Broadcast feedback with reference files as proof.
|
|
40
|
-
|
|
41
|
-
Inbox Check Command (run this in shell):
|
|
42
|
-
```
|
|
43
|
-
uv run .claude/hooks/comms/get_unread_messages.py --name "YourAgentName"
|
|
44
|
-
```
|
|
45
|
-
Replace "YourAgentName" with your actual agent name from your assignment.
|
|
46
|
-
|
|
47
|
-
**Broadcast:**
|
|
48
|
-
Keep your Broadcasts consice, unambiguous, and factually grounded in context you have gathered while operating.
|
|
49
|
-
|
|
50
|
-
You MUST Broadcast:
|
|
51
|
-
- Learnings from external research after searching web or using perplexity ask.
|
|
52
|
-
- System relationships, patterns and issues you have discoverd through deep codebase analysis. Include file references
|
|
53
|
-
- Decisions you make about your solution approach. Include your rationalle
|
|
54
|
-
- Change summary after implmenting, documenting, fixing, or writing to file(s). Include purpose of change and file references
|
|
55
|
-
- Status of lint, build, test, dev after running any of these commands. Detail failures and your suspected cause.
|
|
56
|
-
- When you encounter an issue, Broadcast after each step in the fix cycle. initial issue, fix attempt, outcome, additional fix cycle loops.
|
|
57
|
-
- Critical Feedback to teammate Broadcasts when their system understanding, decisions, approach, or changes, conflict with your mental model of the system or project requirements, will introduce issues, or have broader implications. Include file references as proof
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
Broadcast Command (run this in shell):
|
|
61
|
-
```
|
|
62
|
-
uv run .claude/hooks/comms/send_message.py --sender "YourAgentName" --message "Your message content"
|
|
63
|
-
```
|
|
64
|
-
Replace "YourAgentName" with your actual agent name and "Your message content" with your broadcast.
|
|
65
|
-
|
|
66
|
-
*Broadcast ASAP when you discover, before making decisions, and immidiatly to provide critical feedback to teammates*
|
|
67
|
-
|
|
68
|
-
If your Team Role is support:
|
|
69
|
-
1. Monitor inbox for architectural and testing questions from engineers
|
|
70
|
-
2. Sleep 60 seconds, then Inbox Check. Loop until no new messages for 5 sequential checks.
|
|
71
|
-
3. When a new message is recieved, review, analyse, assess then Broadcast feedback.
|
|
72
|
-
|
|
73
|
-
[/TEAMWORK]
|