@cleocode/cleo-os 2026.4.17 → 2026.4.19

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.
@@ -0,0 +1,611 @@
1
+ /**
2
+ * CleoOS agent monitor — agent activity TUI + Circle of Ten status.
3
+ *
4
+ * CANONICAL LOCATION: `packages/cleo-os/extensions/cleo-agent-monitor.ts`
5
+ *
6
+ * Installed to: $XDG_DATA_HOME/cleo/extensions/cleo-agent-monitor.js
7
+ * Loaded by: Pi via `--extension <path>` injected by CleoOS cli.ts
8
+ *
9
+ * T442 deliverables:
10
+ * - `/cleo:agents` command: shows current agent activity in a TUI panel
11
+ * - `before_agent_start` hook: tracks agent spawns with tier-aware prefixes
12
+ * - `/cleo:circle` command: renders Circle of Ten status from CLEO CLI data
13
+ *
14
+ * Requirements:
15
+ * - Pi coding agent runtime (`@mariozechner/pi-coding-agent`)
16
+ * - Optional: `cleo` CLI on PATH for `/cleo:circle` data
17
+ *
18
+ * Guardrails:
19
+ * - Best-effort: if `cleo` CLI is not available, `/cleo:circle` shows
20
+ * static fallback data. NEVER crash Pi.
21
+ * - NO top-level await; all work happens inside event handlers.
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+
26
+ import { execFile } from "node:child_process";
27
+ import { promisify } from "node:util";
28
+ import type {
29
+ ExtensionAPI,
30
+ ExtensionCommandContext,
31
+ ExtensionContext,
32
+ } from "@mariozechner/pi-coding-agent";
33
+ import {
34
+ accentPrimary,
35
+ accentSuccess,
36
+ accentWarning,
37
+ accentError,
38
+ textSecondary,
39
+ textTertiary,
40
+ tierWorker,
41
+ bold,
42
+ DOT_FILLED,
43
+ DOT_HOLLOW,
44
+ ICON_FORGE,
45
+ LINE_HORIZONTAL,
46
+ } from "./tui-theme.js";
47
+
48
+ const execFileAsync = promisify(execFile);
49
+
50
+ // ============================================================================
51
+ // ANSI color helpers — imported from shared tui-theme.ts
52
+ // ============================================================================
53
+ // All styling functions come from tui-theme.ts. The mapping to design tokens:
54
+ // accentPrimary → #a855f7 (ANSI 135) — headers, Circle of Ten title
55
+ // accentSuccess → #22c55e (ANSI 35) — active dots, orchestrator [O]
56
+ // accentWarning → #f59e0b (ANSI 214) — paused dots, lead [L]
57
+ // accentError → #ef4444 (ANSI 196) — error dots, failed states
58
+ // tierWorker → #5fafff (ANSI 75) — worker [W]
59
+ // textSecondary → #94a3b8 (ANSI 245) — dim/muted text
60
+ // textTertiary → #64748b (ANSI 243) — disabled/very muted text
61
+ // bold → ANSI bold — headings, agent names
62
+
63
+ // ============================================================================
64
+ // Agent activity tracking
65
+ // ============================================================================
66
+
67
+ /** Tier role of an agent in the 3-tier hierarchy. */
68
+ type AgentTierRole = "orchestrator" | "lead" | "worker";
69
+
70
+ /** A tracked agent activity entry. */
71
+ interface AgentActivity {
72
+ /** ISO-8601 timestamp of the activity. */
73
+ timestamp: string;
74
+ /** Agent name. */
75
+ name: string;
76
+ /** Agent tier role. */
77
+ role: AgentTierRole;
78
+ /** Activity description (e.g. "spawned", "completed"). */
79
+ action: string;
80
+ }
81
+
82
+ /** Maximum number of agent activities to keep in the ring buffer. */
83
+ const MAX_ACTIVITIES = 5;
84
+
85
+ /** In-memory ring buffer of recent agent activities. */
86
+ const activities: AgentActivity[] = [];
87
+
88
+ /** Widget key for the agent monitor panel. */
89
+ const WIDGET_KEY = "cleo-agent-monitor";
90
+
91
+ /**
92
+ * Cached CANT tool count from the bridge extension's status bar.
93
+ * Updated when agent spawns are tracked (best-effort cross-extension state).
94
+ */
95
+ let cachedCantTools = 0;
96
+
97
+ /**
98
+ * Cached CANT agent count from agent spawn tracking.
99
+ * Incremented as agents are seen in the current session.
100
+ */
101
+ let cachedCantAgents = 0;
102
+
103
+ /**
104
+ * Return the tier prefix for an agent role.
105
+ *
106
+ * Uses design system colors:
107
+ * - `[O]` orchestrator — accent-success (#22c55e)
108
+ * - `[L]` lead — accent-warning (#f59e0b)
109
+ * - `[W]` worker — tier-worker blue (#5fafff)
110
+ *
111
+ * @param role - The agent's tier role.
112
+ * @returns The styled tier prefix string.
113
+ */
114
+ export function tierPrefix(role: AgentTierRole): string {
115
+ switch (role) {
116
+ case "orchestrator":
117
+ return accentSuccess("[O]");
118
+ case "lead":
119
+ return accentWarning("[L]");
120
+ default:
121
+ return tierWorker("[W]");
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Format a single agent activity for TUI display.
127
+ *
128
+ * @param activity - The activity to format.
129
+ * @returns A formatted single-line string with ANSI styling.
130
+ */
131
+ export function formatActivity(activity: AgentActivity): string {
132
+ const time = activity.timestamp.slice(11, 19);
133
+ const prefix = tierPrefix(activity.role);
134
+ return `${prefix} ${textSecondary(`[${time}]`)} ${bold(activity.name)} ${textSecondary(activity.action)}`;
135
+ }
136
+
137
+ /**
138
+ * Record an agent activity and trim the buffer to MAX_ACTIVITIES.
139
+ *
140
+ * @param activity - The activity to record.
141
+ */
142
+ function recordActivity(activity: AgentActivity): void {
143
+ activities.push(activity);
144
+ while (activities.length > MAX_ACTIVITIES) {
145
+ activities.shift();
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Render the agent activity widget.
151
+ *
152
+ * @param ctx - The Pi extension context.
153
+ */
154
+ function renderAgentWidget(ctx: ExtensionContext): void {
155
+ if (!ctx.hasUI) return;
156
+
157
+ if (activities.length === 0) {
158
+ ctx.ui.setWidget(WIDGET_KEY, [textSecondary(" No agent activity yet")], {
159
+ placement: "belowEditor",
160
+ });
161
+ return;
162
+ }
163
+
164
+ const header = accentPrimary(` ${ICON_FORGE} Agent Activity`);
165
+ const separator = textSecondary(" " + LINE_HORIZONTAL.repeat(30));
166
+ const lines = [header, separator, ...activities.map(formatActivity)];
167
+ ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "belowEditor" });
168
+ }
169
+
170
+ // ============================================================================
171
+ // Circle of Ten status
172
+ // ============================================================================
173
+
174
+ /**
175
+ * Parsed dashboard data from `cleo dash --json`.
176
+ *
177
+ * Each field is optional because the CLI output format may vary or the
178
+ * CLI may not be available. Fields are sourced from the `data` envelope
179
+ * of `cleo dash --json` output.
180
+ */
181
+ interface DashData {
182
+ /** Number of active tasks (summary.active). */
183
+ activeTasks?: number;
184
+ /** Number of pending tasks (summary.pending). */
185
+ pendingTasks?: number;
186
+ /** Number of completed tasks (summary.done). */
187
+ doneTasks?: number;
188
+ /** Total tasks across all statuses. */
189
+ totalTasks?: number;
190
+ /** Number of blocked tasks (blockedTasks.count). */
191
+ blockedTasks?: number;
192
+ /** Number of high-priority tasks (highPriority.count). */
193
+ highPriorityTasks?: number;
194
+ /** Top labels from the project (topLabels[].label). */
195
+ topLabels?: string[];
196
+ }
197
+
198
+ /**
199
+ * Parsed session data from `cleo session status --json`.
200
+ *
201
+ * Each field is optional; the CLI may not be available or the
202
+ * response format may vary.
203
+ */
204
+ interface SessionData {
205
+ /** Whether a session is currently active. */
206
+ hasActiveSession?: boolean;
207
+ /** Active session ID. */
208
+ sessionId?: string;
209
+ /** Active session name. */
210
+ sessionName?: string;
211
+ /** Number of tasks completed in the current session. */
212
+ tasksCompleted?: number;
213
+ /** Number of focus changes in the current session. */
214
+ focusChanges?: number;
215
+ }
216
+
217
+ /**
218
+ * Combined data for Circle of Ten rendering.
219
+ *
220
+ * Merges dashboard data, session data, and any in-memory
221
+ * extension state (e.g. CANT bundle counts from the bridge).
222
+ */
223
+ interface CircleData {
224
+ dash: DashData;
225
+ session: SessionData;
226
+ /** Number of tools from the CANT bundle (populated by bridge). */
227
+ cantTools?: number;
228
+ /** Number of agents from the CANT bundle (populated by bridge). */
229
+ cantAgents?: number;
230
+ }
231
+
232
+ /**
233
+ * Parse `cleo dash --json` output for Circle of Ten status data.
234
+ *
235
+ * Extracts task summary counts, blocked/high-priority stats, and top labels.
236
+ * Best-effort: returns empty object on any parse failure.
237
+ *
238
+ * @param output - Raw stdout from `cleo dash --json`.
239
+ * @returns Parsed dashboard data.
240
+ */
241
+ function parseDashOutput(output: string): DashData {
242
+ try {
243
+ const parsed = JSON.parse(output) as Record<string, unknown>;
244
+ const data = (parsed["data"] ?? parsed) as Record<string, unknown>;
245
+
246
+ // Extract summary counts
247
+ const summary = data["summary"] as Record<string, unknown> | undefined;
248
+ const activeTasks = typeof summary?.["active"] === "number" ? summary["active"] : undefined;
249
+ const pendingTasks = typeof summary?.["pending"] === "number" ? summary["pending"] : undefined;
250
+ const doneTasks = typeof summary?.["done"] === "number" ? summary["done"] : undefined;
251
+ const totalTasks = typeof summary?.["total"] === "number" ? summary["total"] : undefined;
252
+
253
+ // Extract blocked tasks count
254
+ const blocked = data["blockedTasks"] as Record<string, unknown> | undefined;
255
+ const blockedCount = typeof blocked?.["count"] === "number" ? blocked["count"] : undefined;
256
+
257
+ // Extract high-priority count
258
+ const highPri = data["highPriority"] as Record<string, unknown> | undefined;
259
+ const highPriCount = typeof highPri?.["count"] === "number" ? highPri["count"] : undefined;
260
+
261
+ // Extract top labels
262
+ const rawLabels = data["topLabels"];
263
+ let topLabels: string[] | undefined;
264
+ if (Array.isArray(rawLabels)) {
265
+ topLabels = rawLabels
266
+ .slice(0, 5)
267
+ .map((l) => {
268
+ const label = (l as Record<string, unknown>)["label"];
269
+ return typeof label === "string" ? label : "";
270
+ })
271
+ .filter((l) => l.length > 0);
272
+ }
273
+
274
+ return {
275
+ activeTasks,
276
+ pendingTasks,
277
+ doneTasks,
278
+ totalTasks,
279
+ blockedTasks: blockedCount,
280
+ highPriorityTasks: highPriCount,
281
+ topLabels,
282
+ };
283
+ } catch {
284
+ return {};
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Parse `cleo session status --json` output for session data.
290
+ *
291
+ * Extracts active session state, ID, name, and completion stats.
292
+ * Best-effort: returns empty object on any parse failure.
293
+ *
294
+ * @param output - Raw stdout from `cleo session status --json`.
295
+ * @returns Parsed session data.
296
+ */
297
+ function parseSessionOutput(output: string): SessionData {
298
+ try {
299
+ const parsed = JSON.parse(output) as Record<string, unknown>;
300
+ const data = (parsed["data"] ?? parsed) as Record<string, unknown>;
301
+ const sessionWrapper = data["session"] as Record<string, unknown> | undefined;
302
+
303
+ if (!sessionWrapper) return {};
304
+
305
+ const hasActive = sessionWrapper["hasActiveSession"] === true;
306
+ const session = sessionWrapper["session"] as Record<string, unknown> | undefined;
307
+
308
+ if (!hasActive || !session) {
309
+ return { hasActiveSession: false };
310
+ }
311
+
312
+ const stats = session["stats"] as Record<string, unknown> | undefined;
313
+
314
+ return {
315
+ hasActiveSession: true,
316
+ sessionId: typeof session["id"] === "string" ? session["id"] : undefined,
317
+ sessionName: typeof session["name"] === "string" ? session["name"] : undefined,
318
+ tasksCompleted: typeof stats?.["tasksCompleted"] === "number" ? stats["tasksCompleted"] : undefined,
319
+ focusChanges: typeof stats?.["focusChanges"] === "number" ? stats["focusChanges"] : undefined,
320
+ };
321
+ } catch {
322
+ return {};
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Build the Circle of Ten status display lines.
328
+ *
329
+ * Each Circle aspect is shown with a filled (active) or hollow (inactive)
330
+ * dot and a brief status summary. Data is wired from live CLI sources
331
+ * where available, with explicit "not wired" labels where the data
332
+ * source does not yet exist.
333
+ *
334
+ * Design system colors:
335
+ * - Filled dot: accent-success (#22c55e) for active zones
336
+ * - Warning dot: accent-warning (#f59e0b) for zones with alerts
337
+ * - Error dot: accent-error (#ef4444) for blocked/failed zones
338
+ * - Hollow dot: text-secondary (#94a3b8) for inactive/offline zones
339
+ *
340
+ * @param data - Combined Circle data from CLI sources and extension state.
341
+ * @returns Array of ANSI-styled lines for TUI display.
342
+ */
343
+ export function buildCircleOfTenStatus(data: CircleData): string[] {
344
+ const dot = accentSuccess(DOT_FILLED);
345
+ const warnDot = accentWarning(DOT_FILLED);
346
+ const errDot = accentError(DOT_FILLED);
347
+ const hollow = textSecondary(DOT_HOLLOW);
348
+
349
+ const { dash, session } = data;
350
+
351
+ // ── Smiths (tasks) — wired to cleo dash summary ──
352
+ const activeCount = dash.activeTasks ?? 0;
353
+ const pendingCount = dash.pendingTasks ?? 0;
354
+ const doneCount = dash.doneTasks ?? 0;
355
+ const smithsDot = activeCount > 0 ? dot : (pendingCount > 0 ? warnDot : hollow);
356
+ const smithsDetail = `${activeCount} active, ${pendingCount} pending, ${doneCount} done`;
357
+
358
+ // ── Weavers (pipeline) — not wired (no pipeline CLI endpoint) ──
359
+ const weaversDot = hollow;
360
+ const weaversDetail = textTertiary("not wired");
361
+
362
+ // ── Conductors (orchestrate) — wired to cleo session status ──
363
+ const hasSession = session.hasActiveSession === true;
364
+ const conductorsDot = hasSession ? dot : hollow;
365
+ const conductorsDetail = hasSession
366
+ ? `active: ${session.sessionName ?? session.sessionId ?? "unnamed"}`
367
+ : "no session";
368
+
369
+ // ── Artificers (tools) — wired to CANT bundle counts ──
370
+ const toolCount = data.cantTools ?? 0;
371
+ const agentCount = data.cantAgents ?? 0;
372
+ const artificersDot = toolCount > 0 || agentCount > 0 ? dot : hollow;
373
+ const artificersDetail = `${toolCount} tools, ${agentCount} agents`;
374
+
375
+ // ── Archivists (memory) — wired to cleo dash (done count as proxy) ──
376
+ // The dash endpoint does not directly expose observation counts,
377
+ // but done tasks indicate archived work history.
378
+ const archivistsDot = doneCount > 0 ? dot : hollow;
379
+ const archivistsDetail = `${doneCount} archived tasks`;
380
+
381
+ // ── Scribes (session) — wired to session stats ──
382
+ const scribesCompleted = session.tasksCompleted ?? 0;
383
+ const scribesDot = hasSession ? dot : hollow;
384
+ const scribesDetail = hasSession
385
+ ? `${scribesCompleted} completed this session`
386
+ : "no session";
387
+
388
+ // ── Wardens (check) — wired to blocked + high-priority counts ──
389
+ const blockedCount = dash.blockedTasks ?? 0;
390
+ const highPriCount = dash.highPriorityTasks ?? 0;
391
+ const wardensDot = blockedCount > 0 ? errDot : (highPriCount > 0 ? warnDot : dot);
392
+ const wardensDetail = `${blockedCount} blocked, ${highPriCount} high-pri`;
393
+
394
+ // ── Wayfinders (nexus) — not wired (Nexus deferred to Phase 3) ──
395
+ const wayfindersDot = hollow;
396
+ const wayfindersDetail = textTertiary("not wired");
397
+
398
+ // ── Catchers (sticky) — not wired (no sticky notes API) ──
399
+ const catchersDot = hollow;
400
+ const catchersDetail = textTertiary("not wired");
401
+
402
+ // ── Keepers (admin) — wired to overall health from dash totals ──
403
+ const totalCount = dash.totalTasks ?? 0;
404
+ const keepersDot = totalCount > 0 ? dot : hollow;
405
+ const keepersDetail = `${totalCount} total tasks tracked`;
406
+
407
+ // ── Labels line (bonus data from dash) ──
408
+ const labelsLine = dash.topLabels && dash.topLabels.length > 0
409
+ ? ` ${textSecondary("Labels:")} ${textTertiary(dash.topLabels.join(", "))}`
410
+ : null;
411
+
412
+ const lines = [
413
+ "",
414
+ bold(accentPrimary(" The Circle of Ten")),
415
+ textSecondary(" " + LINE_HORIZONTAL.repeat(36)),
416
+ ` ${bold("Smiths")} ${textSecondary("(tasks)")} ${smithsDot} ${smithsDetail}`,
417
+ ` ${bold("Weavers")} ${textSecondary("(pipeline)")} ${weaversDot} ${weaversDetail}`,
418
+ ` ${bold("Conductors")} ${textSecondary("(orch)")} ${conductorsDot} ${conductorsDetail}`,
419
+ ` ${bold("Artificers")} ${textSecondary("(tools)")} ${artificersDot} ${artificersDetail}`,
420
+ ` ${bold("Archivists")} ${textSecondary("(memory)")} ${archivistsDot} ${archivistsDetail}`,
421
+ ` ${bold("Scribes")} ${textSecondary("(session)")} ${scribesDot} ${scribesDetail}`,
422
+ ` ${bold("Wardens")} ${textSecondary("(check)")} ${wardensDot} ${wardensDetail}`,
423
+ ` ${bold("Wayfinders")} ${textSecondary("(nexus)")} ${wayfindersDot} ${wayfindersDetail}`,
424
+ ` ${bold("Catchers")} ${textSecondary("(sticky)")} ${catchersDot} ${catchersDetail}`,
425
+ ` ${bold("Keepers")} ${textSecondary("(admin)")} ${keepersDot} ${keepersDetail}`,
426
+ ];
427
+
428
+ if (labelsLine) {
429
+ lines.push(textSecondary(" " + LINE_HORIZONTAL.repeat(36)));
430
+ lines.push(labelsLine);
431
+ }
432
+
433
+ lines.push("");
434
+
435
+ return lines;
436
+ }
437
+
438
+ // ============================================================================
439
+ // Pi extension factory
440
+ // ============================================================================
441
+
442
+ /**
443
+ * Pi extension factory for the CleoOS agent monitor.
444
+ *
445
+ * Registers agent activity tracking, the `/cleo:agents` command, and the
446
+ * `/cleo:circle` Circle of Ten status command.
447
+ *
448
+ * @param pi - The Pi extension API instance.
449
+ */
450
+ export default function (pi: ExtensionAPI): void {
451
+ // -------------------------------------------------------------------------
452
+ // session_start: initialize widget
453
+ // -------------------------------------------------------------------------
454
+ pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
455
+ activities.length = 0;
456
+ cachedCantTools = 0;
457
+ cachedCantAgents = 0;
458
+ renderAgentWidget(ctx);
459
+ });
460
+
461
+ // -------------------------------------------------------------------------
462
+ // before_agent_start: track agent spawn
463
+ // -------------------------------------------------------------------------
464
+ pi.on(
465
+ "before_agent_start",
466
+ async (
467
+ event: {
468
+ systemPrompt?: string;
469
+ agentName?: string;
470
+ agentDef?: {
471
+ role?: string;
472
+ name?: string;
473
+ };
474
+ },
475
+ ctx: ExtensionContext,
476
+ ) => {
477
+ const agentName = event.agentName ?? event.agentDef?.name ?? "unknown";
478
+ const roleStr = event.agentDef?.role ?? "worker";
479
+
480
+ // Normalise the role to a valid AgentTierRole
481
+ let role: AgentTierRole;
482
+ switch (roleStr) {
483
+ case "orchestrator":
484
+ role = "orchestrator";
485
+ break;
486
+ case "lead":
487
+ role = "lead";
488
+ break;
489
+ default:
490
+ role = "worker";
491
+ break;
492
+ }
493
+
494
+ recordActivity({
495
+ timestamp: new Date().toISOString(),
496
+ name: agentName,
497
+ role,
498
+ action: "spawned",
499
+ });
500
+
501
+ // Track unique agents seen for Circle of Ten Artificers zone
502
+ cachedCantAgents = activities.length;
503
+
504
+ renderAgentWidget(ctx);
505
+
506
+ // Return empty object (do not modify system prompt)
507
+ return {};
508
+ },
509
+ );
510
+
511
+ // -------------------------------------------------------------------------
512
+ // Command: /cleo:agents — show agent activity
513
+ // -------------------------------------------------------------------------
514
+ pi.registerCommand("cleo:agents", {
515
+ description: "Show current agent activity and recent spawn history",
516
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
517
+ const lines: string[] = [
518
+ bold(accentPrimary(`${ICON_FORGE} CleoOS Agent Monitor`)),
519
+ textSecondary(" " + LINE_HORIZONTAL.repeat(28)),
520
+ "",
521
+ ` Total tracked activities: ${activities.length}`,
522
+ "",
523
+ ];
524
+
525
+ if (activities.length === 0) {
526
+ lines.push(textSecondary(" No agent activity recorded this session."));
527
+ } else {
528
+ lines.push(bold(" Recent Agent Activity:"));
529
+ for (const activity of activities) {
530
+ lines.push(" " + formatActivity(activity));
531
+ }
532
+ }
533
+
534
+ lines.push("");
535
+ lines.push(textSecondary(` Legend: ${accentSuccess("[O]")} orchestrator ${accentWarning("[L]")} lead ${tierWorker("[W]")} worker`));
536
+
537
+ pi.sendMessage(
538
+ {
539
+ customType: "cleo-agent-monitor",
540
+ content: lines.join("\n"),
541
+ display: true,
542
+ },
543
+ { triggerTurn: false },
544
+ );
545
+
546
+ if (ctx.hasUI) {
547
+ ctx.ui.notify(`Agent monitor: ${activities.length} activities`, "info");
548
+ }
549
+ },
550
+ });
551
+
552
+ // -------------------------------------------------------------------------
553
+ // Command: /cleo:circle — Circle of Ten status
554
+ // -------------------------------------------------------------------------
555
+ pi.registerCommand("cleo:circle", {
556
+ description: "Show Circle of Ten operational status from CLEO CLI",
557
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
558
+ let dashData: DashData = {};
559
+ let sessionData: SessionData = {};
560
+
561
+ // Best-effort: fetch data from both `cleo dash` and `cleo session status`
562
+ // in parallel. Either or both may fail if the CLI is not available.
563
+ const [dashResult, sessionResult] = await Promise.allSettled([
564
+ execFileAsync("cleo", ["dash", "--json"], { timeout: 5000, cwd: ctx.cwd }),
565
+ execFileAsync("cleo", ["session", "status", "--json"], { timeout: 5000, cwd: ctx.cwd }),
566
+ ]);
567
+
568
+ if (dashResult.status === "fulfilled") {
569
+ dashData = parseDashOutput(dashResult.value.stdout);
570
+ }
571
+ if (sessionResult.status === "fulfilled") {
572
+ sessionData = parseSessionOutput(sessionResult.value.stdout);
573
+ }
574
+
575
+ // Build combined Circle data, including CANT bundle state if available.
576
+ // The CANT bridge stores counts in its own module scope — we read them
577
+ // from the status bar entries as a cross-extension communication channel.
578
+ // For now, we pass defaults and let the bridge contribute via status bar.
579
+ const circleData: CircleData = {
580
+ dash: dashData,
581
+ session: sessionData,
582
+ cantTools: cachedCantTools,
583
+ cantAgents: cachedCantAgents,
584
+ };
585
+
586
+ const lines = buildCircleOfTenStatus(circleData);
587
+
588
+ pi.sendMessage(
589
+ {
590
+ customType: "cleo-circle-of-ten",
591
+ content: lines.join("\n"),
592
+ display: true,
593
+ },
594
+ { triggerTurn: false },
595
+ );
596
+
597
+ if (ctx.hasUI) {
598
+ ctx.ui.notify("Circle of Ten status", "info");
599
+ }
600
+ },
601
+ });
602
+
603
+ // -------------------------------------------------------------------------
604
+ // session_shutdown: clear state
605
+ // -------------------------------------------------------------------------
606
+ pi.on("session_shutdown", async () => {
607
+ activities.length = 0;
608
+ cachedCantTools = 0;
609
+ cachedCantAgents = 0;
610
+ });
611
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CleoOS CANT bridge — Wave 2 Pi extension.
2
+ * CleoOS CANT bridge — Wave 2 + Wave 5 Pi extension.
3
3
  *
4
4
  * CANONICAL LOCATION: `packages/cleo-os/extensions/cleo-cant-bridge.ts`
5
5
  *
@@ -12,16 +12,17 @@
12
12
  * Installed to: $XDG_DATA_HOME/cleo/extensions/cleo-cant-bridge.js
13
13
  * Loaded by: Pi via `--extension <path>` injected by CleoOS cli.ts
14
14
  *
15
- * This bridge discovers `.cant` files in the project's `.cleo/cant/`
16
- * directory at session start, compiles them via `@cleocode/cant`'s
17
- * `compileBundle()`, and appends the compiled declarations to Pi's
18
- * system prompt on `before_agent_start`. This gives the LLM awareness
19
- * of all declared agents, teams, and tools without hand-authored
20
- * protocol text.
15
+ * This bridge discovers `.cant` files across the 3-tier hierarchy at
16
+ * session start, compiles them via `@cleocode/cant`'s `compileBundle()`,
17
+ * and appends the compiled declarations to Pi's system prompt on
18
+ * `before_agent_start`. This gives the LLM awareness of all declared
19
+ * agents, teams, and tools without hand-authored protocol text.
21
20
  *
22
- * Wave 2 scope:
23
- * - Scans project tier only: `<cwd>/.cleo/cant/` (recursive)
24
- * - Three-tier resolution (global, user, project) is Wave 5
21
+ * 3-tier resolution (T438, ULTRAPLAN Section 2.4):
22
+ * - Global: `~/.local/share/cleo/cant/` (lowest precedence)
23
+ * - User: `~/.config/cleo/cant/`
24
+ * - Project: `<cwd>/.cleo/cant/` (highest precedence)
25
+ * - Override semantics: project > user > global, matched by basename
25
26
  * - Prompt strategy: APPEND (per ULTRAPLAN L6, never replace)
26
27
  *
27
28
  * Wave 8 additions (T420):
@@ -71,6 +72,31 @@ export interface MentalModelObservation {
71
72
  * or an empty string when `observations` is empty.
72
73
  */
73
74
  export declare function buildMentalModelInjection(agentName: string, observations: MentalModelObservation[]): string;
75
+ /**
76
+ * Cached bundle counts from the last session_start compilation.
77
+ * Used by the banner and status bar entries.
78
+ */
79
+ export interface BundleCounts {
80
+ /** Number of agents declared in the CANT bundle. */
81
+ agents: number;
82
+ /** Number of teams declared in the CANT bundle. */
83
+ teams: number;
84
+ /** Number of tools declared in the CANT bundle. */
85
+ tools: number;
86
+ /** Name of the first team in the bundle, or "none" if no teams. */
87
+ teamName: string;
88
+ }
89
+ /**
90
+ * Build the CleoOS branded session banner lines.
91
+ *
92
+ * Renders a box-drawing banner with the forge aesthetic using purple
93
+ * ANSI accents and the compiled CANT bundle counts.
94
+ *
95
+ * @param counts - Agent, team, and tool counts from the CANT bundle.
96
+ * @param sessionId - The current session ID, if available.
97
+ * @returns An array of pre-formatted ANSI lines for the widget.
98
+ */
99
+ export declare function buildSessionBanner(counts: BundleCounts, sessionId: string): string[];
74
100
  /**
75
101
  * Pi extension factory for the CleoOS CANT bridge.
76
102
  *
@@ -1 +1 @@
1
- {"version":3,"file":"cleo-cant-bridge.d.ts","sourceRoot":"","sources":["cleo-cant-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAIH,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,+BAA+B,CAAC;AAMvC;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,QAIiC,CAAC;AAExE,6EAA6E;AAC7E,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,sBAAsB,EAAE,GACrC,MAAM,CAmBR;AA+OD;;;;;;;;;GASG;AACH,MAAM,CAAC,OAAO,WAAW,EAAE,EAAE,YAAY,GAAG,IAAI,CA6O/C"}
1
+ {"version":3,"file":"cleo-cant-bridge.d.ts","sourceRoot":"","sources":["cleo-cant-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAKH,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,+BAA+B,CAAC;AAuBvC;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,QAIiC,CAAC;AAExE,6EAA6E;AAC7E,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,sBAAsB,EAAE,GACrC,MAAM,CAmBR;AAwJD;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,GAChB,MAAM,EAAE,CAgCV;AAmMD;;;;;;;;;GASG;AACH,MAAM,CAAC,OAAO,WAAW,EAAE,EAAE,YAAY,GAAG,IAAI,CAuQ/C"}