@walwal-harness/cli 7.1.43 → 7.1.45
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/apps/harness-dashboard/app/brick.css +25 -3
- package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +36 -16
- package/apps/harness-dashboard/components/brick/ui.tsx +4 -0
- package/apps/harness-dashboard/components/brick/views-grid.tsx +3 -2
- package/apps/harness-dashboard/lib/metrics/sampler.ts +61 -17
- package/package.json +1 -1
|
@@ -52,6 +52,25 @@
|
|
|
52
52
|
.viewscroll{ flex:1; overflow:auto; padding:var(--gap); }
|
|
53
53
|
.viewscroll-graph{ overflow:hidden; }
|
|
54
54
|
|
|
55
|
+
/* Consolidated single-screen layout: COMMAND + ORCHESTRATION + TIMELINE stacked. */
|
|
56
|
+
.viewstack{ flex:1; min-height:0; overflow:auto; display:flex; flex-direction:column; }
|
|
57
|
+
.viewsection{ flex:none; display:flex; flex-direction:column; border-top:1px solid var(--bd2); }
|
|
58
|
+
.viewsection:first-child{ border-top:none; }
|
|
59
|
+
.viewsection-head{ position:sticky; top:0; z-index:6; display:flex; align-items:baseline; gap:10px;
|
|
60
|
+
padding:7px var(--gap); background:rgba(13,17,24,.92); backdrop-filter:blur(4px);
|
|
61
|
+
border-bottom:1px solid var(--bd);
|
|
62
|
+
font-family:var(--mono); font-size:10px; letter-spacing:.16em; text-transform:uppercase; color:var(--tx-lo); }
|
|
63
|
+
.viewsection-head b{ color:var(--tx-hi); font-weight:600; letter-spacing:.18em; }
|
|
64
|
+
.viewsection-body{ flex:1; min-height:0; overflow:auto; padding:var(--gap); }
|
|
65
|
+
/* Content-driven height so a short agents grid / empty WORK·IN·FLIGHT doesn't
|
|
66
|
+
leave a tall blank gap. The stream rail is capped so many events scroll
|
|
67
|
+
instead of stretching the section. */
|
|
68
|
+
.viewsection-command{ height:auto; }
|
|
69
|
+
.viewsection-command .gridview-rail{ max-height:60vh; }
|
|
70
|
+
.viewsection-graph{ height:80vh; }
|
|
71
|
+
.viewsection-graph .viewsection-body{ overflow:hidden; }
|
|
72
|
+
.viewsection-timeline{ height:80vh; }
|
|
73
|
+
|
|
55
74
|
/* ===================== HEADER ===================== */
|
|
56
75
|
.hdr{
|
|
57
76
|
height:46px; flex:0 0 auto; display:flex; align-items:center; gap:18px;
|
|
@@ -105,7 +124,9 @@
|
|
|
105
124
|
.sb-sub{ font-family:var(--mono); font-size:10px; color:var(--tx-lo); }
|
|
106
125
|
.sb-toggle{ color:var(--tx-lo); font-size:15px; padding:2px 6px; border-radius:4px; }
|
|
107
126
|
.sb-toggle:hover{ color:var(--tx-hi); background:var(--bg2); }
|
|
108
|
-
.sb-list{ flex:1; overflow:auto; padding:8px; display:flex; flex-direction:column; gap:1px; }
|
|
127
|
+
.sb-list{ flex:1 1 0; min-height:0; overflow:auto; padding:8px; display:flex; flex-direction:column; gap:1px; }
|
|
128
|
+
.sb-footer{ flex:1 1 0; min-height:0; display:flex; flex-direction:column; border-top:1px solid var(--bd2); overflow:hidden; padding:8px; }
|
|
129
|
+
.sb-footer .alertpanel{ flex:1 1 0; min-height:0; }
|
|
109
130
|
.sb-item{ display:flex; flex-direction:column; gap:3px; text-align:left; padding:7px 9px; border-radius:5px; transition:.12s; border:1px solid transparent; }
|
|
110
131
|
.sb-item:hover{ background:var(--bg2); }
|
|
111
132
|
.sb-item.is-active{ background:var(--bg2); border-color:var(--bd2); }
|
|
@@ -206,7 +227,8 @@
|
|
|
206
227
|
.sl-done{ color:var(--ok); }
|
|
207
228
|
|
|
208
229
|
/* alerts */
|
|
209
|
-
.alertpanel{ flex:0 0
|
|
230
|
+
.alertpanel{ flex:1.3 1 0; min-height:0; }
|
|
231
|
+
.alertpanel .panel-body{ flex:1; min-height:0; }
|
|
210
232
|
.alertlist{ display:flex; flex-direction:column; gap:7px; }
|
|
211
233
|
.alertitem{ position:relative; display:flex; gap:9px; background:var(--bg2); border:1px solid var(--bd); border-radius:5px; padding:8px 10px 8px 9px; overflow:hidden; }
|
|
212
234
|
.alert-bar{ position:absolute; left:0; top:0; bottom:0; width:2px; }
|
|
@@ -249,7 +271,7 @@
|
|
|
249
271
|
@keyframes spawnpop{ from{ opacity:0; transform:translate(-10px,-50%) scale(.8); } to{ opacity:1; transform:translate(0,-50%) scale(1); } }
|
|
250
272
|
.graph-legend{ position:absolute; left:12px; bottom:10px; font-family:var(--mono); font-size:9.5px; color:var(--tx-faint); }
|
|
251
273
|
|
|
252
|
-
.graph-detail{ flex:0 0 312px; background:var(--bg1); border:1px solid var(--bd); border-radius:var(--radius); padding:var(--pad); overflow:auto; }
|
|
274
|
+
.graph-detail{ flex:0 0 312px; background:var(--bg1); border:1px solid var(--bd); border-radius:var(--radius); padding:var(--pad); overflow:auto; min-height:0; }
|
|
253
275
|
.gd-head{ display:flex; align-items:center; gap:11px; margin-bottom:12px; }
|
|
254
276
|
.gd-role{ font-size:12px; color:var(--tx-mid); }
|
|
255
277
|
.gd-status{ display:flex; align-items:center; gap:6px; font-family:var(--mono); font-size:10.5px; margin-top:3px; }
|
|
@@ -27,7 +27,7 @@ import type { AgentRole, DocTarget } from "../../lib/brick/contract";
|
|
|
27
27
|
import type { MetricsSample } from "../../lib/metrics/sampler";
|
|
28
28
|
|
|
29
29
|
import { Header, Sidebar, type BrickView } from "./ui";
|
|
30
|
-
import { GridView } from "./views-grid";
|
|
30
|
+
import { GridView, AlertRail } from "./views-grid";
|
|
31
31
|
import { GraphView } from "./views-graph";
|
|
32
32
|
import { TimelineView } from "./views-timeline";
|
|
33
33
|
import { DocViewer } from "./reports";
|
|
@@ -100,6 +100,13 @@ export function BrickDashboard({
|
|
|
100
100
|
s.ctrend = metrics.ctrend ?? [];
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// All three views render on one screen; the header buttons jump to a section.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const id =
|
|
106
|
+
view === "grid" ? "view-command" : view === "graph" ? "view-orchestration" : "view-timeline";
|
|
107
|
+
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
108
|
+
}, [view]);
|
|
109
|
+
|
|
103
110
|
const openDoc = (target: DocTarget): void => setDoc(target);
|
|
104
111
|
const closeDoc = (): void => setDoc(null);
|
|
105
112
|
|
|
@@ -113,23 +120,36 @@ export function BrickDashboard({
|
|
|
113
120
|
onToggle={() => setCollapsed((c) => !c)}
|
|
114
121
|
missions={snapshot.missions}
|
|
115
122
|
ownerHistory={snapshot.ownerHistory}
|
|
123
|
+
footer={
|
|
124
|
+
s.alerts.length ? (
|
|
125
|
+
<AlertRail alerts={s.alerts} now={s.now} openDoc={openDoc} />
|
|
126
|
+
) : undefined
|
|
127
|
+
}
|
|
116
128
|
/>
|
|
117
129
|
<div className="app-main">
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
{/* All three views consolidated on one scrolling screen — no
|
|
131
|
+
switching. The header COMMAND/ORCHESTRATION/TIMELINE buttons
|
|
132
|
+
jump to the matching section. */}
|
|
133
|
+
<div className="viewstack">
|
|
134
|
+
<section className="viewsection viewsection-command" id="view-command">
|
|
135
|
+
<div className="viewsection-head"><b>Command</b><span>agents · work in flight · live stream</span></div>
|
|
136
|
+
<div className="viewsection-body">
|
|
137
|
+
<GridView s={s} layout={t.agentLayout} openDoc={openDoc} />
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
<section className="viewsection viewsection-timeline" id="view-timeline">
|
|
141
|
+
<div className="viewsection-head"><b>Heatmap</b><span>cadence · swimlanes · last 12h</span></div>
|
|
142
|
+
<div className="viewsection-body">
|
|
143
|
+
<TimelineView s={s} openDoc={openDoc} />
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
<section className="viewsection viewsection-graph" id="view-orchestration">
|
|
147
|
+
<div className="viewsection-head"><b>Orchestration</b><span>CEO → CXX → workers</span></div>
|
|
148
|
+
<div className="viewsection-body">
|
|
149
|
+
<GraphView s={s} sel={sel} setSel={setSel} openDoc={openDoc} />
|
|
150
|
+
</div>
|
|
151
|
+
</section>
|
|
152
|
+
</div>
|
|
133
153
|
</div>
|
|
134
154
|
</div>
|
|
135
155
|
|
|
@@ -418,6 +418,8 @@ export interface SidebarProps {
|
|
|
418
418
|
missions?: MissionDoc[];
|
|
419
419
|
/** Real owner prompt history; used when missions are absent. */
|
|
420
420
|
ownerHistory?: OwnerPromptEntry[];
|
|
421
|
+
/** Optional bottom-half content (e.g. the ATTENTION rail), split 1:1 under the list. */
|
|
422
|
+
footer?: React.ReactNode;
|
|
421
423
|
}
|
|
422
424
|
|
|
423
425
|
export function Sidebar({
|
|
@@ -425,6 +427,7 @@ export function Sidebar({
|
|
|
425
427
|
onToggle,
|
|
426
428
|
missions,
|
|
427
429
|
ownerHistory,
|
|
430
|
+
footer,
|
|
428
431
|
}: SidebarProps): React.JSX.Element {
|
|
429
432
|
const entries = buildSidebarEntries(missions, ownerHistory);
|
|
430
433
|
const [sel, setSel] = useState<string>(entries[0]?.id ?? "");
|
|
@@ -458,6 +461,7 @@ export function Sidebar({
|
|
|
458
461
|
))}
|
|
459
462
|
</div>
|
|
460
463
|
)}
|
|
464
|
+
{!collapsed && footer && <div className="sb-footer">{footer}</div>}
|
|
461
465
|
</aside>
|
|
462
466
|
);
|
|
463
467
|
}
|
|
@@ -455,7 +455,7 @@ export interface AlertRailProps {
|
|
|
455
455
|
export function AlertRail({ alerts, now, openDoc }: AlertRailProps) {
|
|
456
456
|
if (!alerts.length) return null;
|
|
457
457
|
return (
|
|
458
|
-
<Panel title="ATTENTION · IN-LOOP" right={alerts.length} className="alertpanel">
|
|
458
|
+
<Panel title="ATTENTION · IN-LOOP" right={alerts.length} className="alertpanel" scroll>
|
|
459
459
|
<div className="alertlist">
|
|
460
460
|
{alerts.map((al) => {
|
|
461
461
|
const a = defOf(al.agent);
|
|
@@ -534,7 +534,8 @@ export function GridView({ s, layout = "card", openDoc }: GridViewProps) {
|
|
|
534
534
|
<TaskRail s={s} openDoc={openDoc} />
|
|
535
535
|
</div>
|
|
536
536
|
<div className="gridview-rail">
|
|
537
|
-
|
|
537
|
+
{/* ATTENTION rail moved to the sidebar footer; the command rail now
|
|
538
|
+
carries just the live stream (shorter section). */}
|
|
538
539
|
<LiveStream events={s.events} openDoc={openDoc} />
|
|
539
540
|
</div>
|
|
540
541
|
</div>
|
|
@@ -308,21 +308,67 @@ function collectPids(value: unknown, into: Set<number>): void {
|
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
/**
|
|
312
|
-
function
|
|
311
|
+
/** One `ps` snapshot: pid -> %cpu, and ppid -> [children]. */
|
|
312
|
+
function readProcTable(): { cpu: Map<number, number>; kids: Map<number, number[]> } {
|
|
313
|
+
const cpu = new Map<number, number>();
|
|
314
|
+
const kids = new Map<number, number[]>();
|
|
313
315
|
try {
|
|
314
|
-
const out = execFileSync("ps", ["-
|
|
316
|
+
const out = execFileSync("ps", ["-axo", "pid=,ppid=,%cpu="], {
|
|
315
317
|
encoding: "utf8",
|
|
316
318
|
timeout: 4000,
|
|
317
319
|
stdio: ["ignore", "pipe", "ignore"],
|
|
318
320
|
});
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
+
for (const line of out.split("\n")) {
|
|
322
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+([\d.]+)$/);
|
|
323
|
+
if (!m) continue;
|
|
324
|
+
const pid = Number(m[1]);
|
|
325
|
+
const ppid = Number(m[2]);
|
|
326
|
+
const c = parseFloat(m[3]);
|
|
327
|
+
if (Number.isFinite(c)) cpu.set(pid, c);
|
|
328
|
+
const arr = kids.get(ppid);
|
|
329
|
+
if (arr) arr.push(pid);
|
|
330
|
+
else kids.set(ppid, [pid]);
|
|
331
|
+
}
|
|
321
332
|
} catch {
|
|
322
|
-
|
|
333
|
+
/* ps absent/guarded */
|
|
334
|
+
}
|
|
335
|
+
return { cpu, kids };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Collect a pid plus ALL its descendants into `into`. tmux/background workers
|
|
339
|
+
* run as CHILDREN of the recorded pane/shell pid, so sampling the root pid alone
|
|
340
|
+
* reads ~0% (the shell idles while the worker burns cpu in a child). */
|
|
341
|
+
function collectSubtree(
|
|
342
|
+
root: number,
|
|
343
|
+
table: { cpu: Map<number, number>; kids: Map<number, number[]> },
|
|
344
|
+
into: Set<number>
|
|
345
|
+
): void {
|
|
346
|
+
const stack = [root];
|
|
347
|
+
while (stack.length) {
|
|
348
|
+
const pid = stack.pop() as number;
|
|
349
|
+
if (into.has(pid)) continue;
|
|
350
|
+
into.add(pid);
|
|
351
|
+
for (const k of table.kids.get(pid) ?? []) stack.push(k);
|
|
323
352
|
}
|
|
324
353
|
}
|
|
325
354
|
|
|
355
|
+
/** Sum %cpu of a set of pids from the table. */
|
|
356
|
+
function sumCpu(
|
|
357
|
+
pids: Set<number>,
|
|
358
|
+
table: { cpu: Map<number, number>; kids: Map<number, number[]> }
|
|
359
|
+
): number | null {
|
|
360
|
+
let total = 0;
|
|
361
|
+
let any = false;
|
|
362
|
+
for (const pid of pids) {
|
|
363
|
+
const c = table.cpu.get(pid);
|
|
364
|
+
if (c != null) {
|
|
365
|
+
total += c;
|
|
366
|
+
any = true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return any ? Math.round(total * 10) / 10 : null;
|
|
370
|
+
}
|
|
371
|
+
|
|
326
372
|
/**
|
|
327
373
|
* Discover tmux company panes: pane_pid keyed by session name, for sessions
|
|
328
374
|
* matching `walwal_<projbasename>_*`. tmux may be absent → returns {}.
|
|
@@ -473,18 +519,14 @@ export async function sampleMetrics(harnessRoot: string): Promise<MetricsSample>
|
|
|
473
519
|
const panes = tmuxCompanyPanes(projBase);
|
|
474
520
|
for (const p of panes) pidSet.add(p.pid);
|
|
475
521
|
|
|
522
|
+
const procTable = readProcTable();
|
|
476
523
|
let cpu: number | null = null;
|
|
477
524
|
if (pidSet.size > 0) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
total += c;
|
|
484
|
-
any = true;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
cpu = any ? Math.round(total * 10) / 10 : null;
|
|
525
|
+
// Sum each discovered pid's whole process subtree (deduped), so a busy
|
|
526
|
+
// worker running as a child of its tmux/background shell is counted.
|
|
527
|
+
const counted = new Set<number>();
|
|
528
|
+
for (const pid of pidSet) collectSubtree(pid, procTable, counted);
|
|
529
|
+
cpu = sumCpu(counted, procTable);
|
|
488
530
|
}
|
|
489
531
|
|
|
490
532
|
/* ---- 3. perAgent: attribute via tmux session name ---------------- */
|
|
@@ -495,7 +537,9 @@ export async function sampleMetrics(harnessRoot: string): Promise<MetricsSample>
|
|
|
495
537
|
for (const p of panes) {
|
|
496
538
|
// session name: walwal_<projBase>_<stamp> — stamp is the agent tag.
|
|
497
539
|
const tag = p.session.slice(prefix.length) || p.session;
|
|
498
|
-
const
|
|
540
|
+
const sub = new Set<number>();
|
|
541
|
+
collectSubtree(p.pid, procTable, sub);
|
|
542
|
+
const c = sumCpu(sub, procTable);
|
|
499
543
|
const cur = acc[tag] ?? {};
|
|
500
544
|
if (c != null) cur.cpu = Math.round(((cur.cpu ?? 0) + c) * 10) / 10;
|
|
501
545
|
acc[tag] = cur;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.45",
|
|
4
4
|
"description": "Company-style AI agent harness for Claude and Codex. Installs commands, CXX agents, skills, HR-Resource hiring pool, and project-local .harness runtime state.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|