@versdotsh/reef 0.1.2

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.
Files changed (83) hide show
  1. package/.github/workflows/test.yml +47 -0
  2. package/README.md +257 -0
  3. package/bun.lock +587 -0
  4. package/examples/services/board/board.test.ts +215 -0
  5. package/examples/services/board/index.ts +155 -0
  6. package/examples/services/board/routes.ts +335 -0
  7. package/examples/services/board/store.ts +329 -0
  8. package/examples/services/board/tools.ts +214 -0
  9. package/examples/services/commits/commits.test.ts +74 -0
  10. package/examples/services/commits/index.ts +14 -0
  11. package/examples/services/commits/routes.ts +43 -0
  12. package/examples/services/commits/store.ts +114 -0
  13. package/examples/services/feed/behaviors.ts +23 -0
  14. package/examples/services/feed/feed.test.ts +101 -0
  15. package/examples/services/feed/index.ts +117 -0
  16. package/examples/services/feed/routes.ts +224 -0
  17. package/examples/services/feed/store.ts +194 -0
  18. package/examples/services/feed/tools.ts +83 -0
  19. package/examples/services/journal/index.ts +15 -0
  20. package/examples/services/journal/journal.test.ts +57 -0
  21. package/examples/services/journal/routes.ts +45 -0
  22. package/examples/services/journal/store.ts +119 -0
  23. package/examples/services/journal/tools.ts +32 -0
  24. package/examples/services/log/index.ts +15 -0
  25. package/examples/services/log/log.test.ts +70 -0
  26. package/examples/services/log/routes.ts +44 -0
  27. package/examples/services/log/store.ts +105 -0
  28. package/examples/services/log/tools.ts +57 -0
  29. package/examples/services/registry/behaviors.ts +128 -0
  30. package/examples/services/registry/index.ts +37 -0
  31. package/examples/services/registry/registry.test.ts +135 -0
  32. package/examples/services/registry/routes.ts +76 -0
  33. package/examples/services/registry/store.ts +224 -0
  34. package/examples/services/registry/tools.ts +116 -0
  35. package/examples/services/reports/index.ts +14 -0
  36. package/examples/services/reports/reports.test.ts +75 -0
  37. package/examples/services/reports/routes.ts +42 -0
  38. package/examples/services/reports/store.ts +110 -0
  39. package/examples/services/ui/auth.ts +61 -0
  40. package/examples/services/ui/index.ts +16 -0
  41. package/examples/services/ui/routes.ts +160 -0
  42. package/examples/services/ui/static/app.js +369 -0
  43. package/examples/services/ui/static/index.html +42 -0
  44. package/examples/services/ui/static/style.css +157 -0
  45. package/examples/services/usage/behaviors.ts +166 -0
  46. package/examples/services/usage/index.ts +19 -0
  47. package/examples/services/usage/routes.ts +53 -0
  48. package/examples/services/usage/store.ts +341 -0
  49. package/examples/services/usage/tools.ts +75 -0
  50. package/examples/services/usage/usage.test.ts +91 -0
  51. package/package.json +29 -0
  52. package/services/agent/index.ts +465 -0
  53. package/services/board/index.ts +155 -0
  54. package/services/board/routes.ts +335 -0
  55. package/services/board/store.ts +329 -0
  56. package/services/board/tools.ts +214 -0
  57. package/services/docs/index.ts +391 -0
  58. package/services/feed/behaviors.ts +23 -0
  59. package/services/feed/index.ts +117 -0
  60. package/services/feed/routes.ts +224 -0
  61. package/services/feed/store.ts +194 -0
  62. package/services/feed/tools.ts +83 -0
  63. package/services/installer/index.ts +574 -0
  64. package/services/services/index.ts +165 -0
  65. package/services/ui/auth.ts +61 -0
  66. package/services/ui/index.ts +16 -0
  67. package/services/ui/routes.ts +160 -0
  68. package/services/ui/static/app.js +369 -0
  69. package/services/ui/static/index.html +42 -0
  70. package/services/ui/static/style.css +157 -0
  71. package/skills/create-service/SKILL.md +698 -0
  72. package/src/core/auth.ts +28 -0
  73. package/src/core/client.ts +99 -0
  74. package/src/core/discover.ts +152 -0
  75. package/src/core/events.ts +44 -0
  76. package/src/core/extension.ts +66 -0
  77. package/src/core/server.ts +262 -0
  78. package/src/core/testing.ts +155 -0
  79. package/src/core/types.ts +194 -0
  80. package/src/extension.ts +16 -0
  81. package/src/main.ts +11 -0
  82. package/tests/server.test.ts +1338 -0
  83. package/tsconfig.json +29 -0
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Docs service module — auto-generated API documentation.
3
+ *
4
+ * Introspects all loaded service modules to produce endpoint documentation.
5
+ * Reads routes from Hono instances and combines with routeDocs metadata.
6
+ *
7
+ * GET /docs — full API docs (JSON)
8
+ * GET /docs/:service — docs for a specific service
9
+ * GET /docs/ui — browsable HTML docs
10
+ */
11
+
12
+ import { Hono } from "hono";
13
+ import type {
14
+ ServiceModule,
15
+ ServiceContext,
16
+ RouteDocs,
17
+ ParamDoc,
18
+ } from "../src/core/types.js";
19
+
20
+ let ctx: ServiceContext;
21
+
22
+ // =============================================================================
23
+ // Types
24
+ // =============================================================================
25
+
26
+ interface RouteDoc {
27
+ method: string;
28
+ path: string;
29
+ summary?: string;
30
+ detail?: string;
31
+ params?: Record<string, ParamDoc>;
32
+ query?: Record<string, ParamDoc>;
33
+ body?: Record<string, ParamDoc>;
34
+ response?: string;
35
+ }
36
+
37
+ interface ServiceDoc {
38
+ name: string;
39
+ description: string | undefined;
40
+ auth: boolean;
41
+ routes: RouteDoc[];
42
+ capabilities: {
43
+ hasTools: boolean;
44
+ hasBehaviors: boolean;
45
+ hasWidget: boolean;
46
+ hasStore: boolean;
47
+ };
48
+ dependencies: string[];
49
+ }
50
+
51
+ // =============================================================================
52
+ // Introspection
53
+ // =============================================================================
54
+
55
+ function documentService(mod: ServiceModule): ServiceDoc {
56
+ const routes: RouteDoc[] = [];
57
+ const docMap = mod.routeDocs ?? {};
58
+
59
+ if (mod.routes) {
60
+ for (const r of (mod.routes as any).routes || []) {
61
+ if (r.method === "ALL" && (r.path === "/*" || r.path === "*")) continue;
62
+
63
+ const prefix = mod.mountAtRoot ? "" : `/${mod.name}`;
64
+
65
+ // Look up docs by "METHOD /path" (path relative to module, as declared)
66
+ const key = `${r.method} ${r.path}`;
67
+ const docs = docMap[key];
68
+
69
+ routes.push({
70
+ method: r.method,
71
+ path: prefix + r.path,
72
+ ...(docs ?? {}),
73
+ });
74
+ }
75
+ }
76
+
77
+ return {
78
+ name: mod.name,
79
+ description: mod.description,
80
+ auth: mod.requiresAuth !== false,
81
+ routes,
82
+ capabilities: {
83
+ hasTools: !!mod.registerTools,
84
+ hasBehaviors: !!mod.registerBehaviors,
85
+ hasWidget: !!mod.widget,
86
+ hasStore: !!mod.store,
87
+ },
88
+ dependencies: mod.dependencies ?? [],
89
+ };
90
+ }
91
+
92
+ function documentAll(): ServiceDoc[] {
93
+ return ctx
94
+ .getModules()
95
+ .filter((m) => m.name !== "docs")
96
+ .map(documentService)
97
+ .sort((a, b) => a.name.localeCompare(b.name));
98
+ }
99
+
100
+ // =============================================================================
101
+ // HTML renderer
102
+ // =============================================================================
103
+
104
+ function renderHTML(docs: ServiceDoc[]): string {
105
+ const methodColor: Record<string, string> = {
106
+ GET: "#4f9",
107
+ POST: "#5af",
108
+ PATCH: "#fd0",
109
+ PUT: "#fd0",
110
+ DELETE: "#f55",
111
+ ALL: "#888",
112
+ };
113
+
114
+ const services = docs
115
+ .map((svc) => {
116
+ const routes = svc.routes
117
+ .map((r) => {
118
+ const color = methodColor[r.method] || "#ccc";
119
+ const summary = r.summary
120
+ ? `<span class="summary">${esc(r.summary)}</span>`
121
+ : "";
122
+ const detail = r.detail
123
+ ? `<div class="detail">${esc(r.detail)}</div>`
124
+ : "";
125
+ const params = renderParamTable("Path params", r.params);
126
+ const query = renderParamTable("Query params", r.query);
127
+ const body = renderParamTable("Body", r.body);
128
+ const response = r.response
129
+ ? `<div class="response-doc"><span class="field-label">Response:</span> <span class="response-text">${esc(r.response)}</span></div>`
130
+ : "";
131
+ const hasDetails = r.summary || r.params || r.query || r.body || r.response;
132
+ const expandClass = hasDetails ? " expandable" : "";
133
+ const detailBlock =
134
+ detail || params || query || body || response
135
+ ? `<div class="route-details">${detail}${params}${query}${body}${response}</div>`
136
+ : "";
137
+
138
+ return `<div class="route${expandClass}"${hasDetails ? ' onclick="this.classList.toggle(\'open\')"' : ""}>
139
+ <div class="route-header">
140
+ <span class="method" style="color:${color}">${esc(r.method)}</span>
141
+ <span class="path">${esc(r.path)}</span>
142
+ ${summary}
143
+ </div>
144
+ ${detailBlock}
145
+ </div>`;
146
+ })
147
+ .join("\n");
148
+
149
+ const badges = [
150
+ svc.auth
151
+ ? '<span class="badge auth">auth</span>'
152
+ : '<span class="badge public">public</span>',
153
+ svc.capabilities.hasTools
154
+ ? '<span class="badge tools">tools</span>'
155
+ : "",
156
+ svc.capabilities.hasBehaviors
157
+ ? '<span class="badge behaviors">behaviors</span>'
158
+ : "",
159
+ svc.capabilities.hasWidget
160
+ ? '<span class="badge widget">widget</span>'
161
+ : "",
162
+ svc.capabilities.hasStore
163
+ ? '<span class="badge store">store</span>'
164
+ : "",
165
+ ]
166
+ .filter(Boolean)
167
+ .join(" ");
168
+
169
+ const deps = svc.dependencies.length
170
+ ? `<div class="deps">depends on: ${svc.dependencies.map((d) => `<code>${esc(d)}</code>`).join(", ")}</div>`
171
+ : "";
172
+
173
+ return `<div class="service" id="svc-${esc(svc.name)}">
174
+ <div class="service-header">
175
+ <h2><a href="#svc-${esc(svc.name)}">/${esc(svc.name)}</a></h2>
176
+ <div class="badges">${badges}</div>
177
+ </div>
178
+ ${svc.description ? `<p class="desc">${esc(svc.description)}</p>` : ""}
179
+ ${deps}
180
+ <div class="routes">${routes || '<div class="empty">No routes</div>'}</div>
181
+ </div>`;
182
+ })
183
+ .join("\n");
184
+
185
+ const nav = docs
186
+ .map(
187
+ (svc) =>
188
+ `<a href="#svc-${esc(svc.name)}" class="nav-item">/${esc(svc.name)} <span class="route-count">${svc.routes.length}</span></a>`,
189
+ )
190
+ .join("\n");
191
+
192
+ const totalRoutes = docs.reduce((sum, s) => sum + s.routes.length, 0);
193
+ const documented = docs.reduce(
194
+ (sum, s) => sum + s.routes.filter((r) => r.summary).length,
195
+ 0,
196
+ );
197
+
198
+ return `<!DOCTYPE html>
199
+ <html lang="en">
200
+ <head>
201
+ <meta charset="UTF-8">
202
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
203
+ <title>Fleet Services — API Docs</title>
204
+ <style>
205
+ * { margin: 0; padding: 0; box-sizing: border-box; }
206
+ :root {
207
+ --bg: #0a0a0a; --bg-panel: #111; --bg-card: #1a1a1a;
208
+ --border: #2a2a2a; --text: #ccc; --text-dim: #666;
209
+ --text-bright: #eee; --accent: #4f9;
210
+ }
211
+ html, body {
212
+ height: 100%; background: var(--bg); color: var(--text);
213
+ font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
214
+ font-size: 13px; line-height: 1.6;
215
+ }
216
+ .layout { display: flex; height: 100vh; }
217
+ .sidebar {
218
+ width: 220px; background: var(--bg-panel); border-right: 1px solid var(--border);
219
+ padding: 16px 0; overflow-y: auto; flex-shrink: 0;
220
+ }
221
+ .sidebar h1 {
222
+ font-size: 13px; color: var(--accent); padding: 0 16px 12px;
223
+ border-bottom: 1px solid var(--border); margin-bottom: 8px; font-weight: 600;
224
+ }
225
+ .sidebar .summary { font-size: 11px; color: var(--text-dim); padding: 0 16px 12px; }
226
+ .nav-item {
227
+ display: flex; justify-content: space-between; align-items: center;
228
+ padding: 4px 16px; color: var(--text-dim); text-decoration: none;
229
+ font-size: 12px; transition: all 0.1s;
230
+ }
231
+ .nav-item:hover { color: var(--text); background: var(--bg-card); }
232
+ .route-count {
233
+ font-size: 10px; background: var(--bg-card); padding: 1px 6px;
234
+ border-radius: 3px; color: var(--text-dim);
235
+ }
236
+ .content { flex: 1; overflow-y: auto; padding: 24px 32px; }
237
+ .service {
238
+ margin-bottom: 32px; padding-bottom: 24px;
239
+ border-bottom: 1px solid var(--border);
240
+ }
241
+ .service:last-child { border-bottom: none; }
242
+ .service-header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
243
+ .service-header h2 { font-size: 16px; font-weight: 600; }
244
+ .service-header h2 a { color: var(--text-bright); text-decoration: none; }
245
+ .service-header h2 a:hover { color: var(--accent); }
246
+ .desc { color: var(--text-dim); margin-bottom: 8px; font-size: 12px; }
247
+ .deps { color: var(--text-dim); font-size: 11px; margin-bottom: 8px; }
248
+ .deps code { color: var(--accent); }
249
+ .badges { display: flex; gap: 4px; }
250
+ .badge {
251
+ font-size: 10px; padding: 2px 8px; border-radius: 3px;
252
+ text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;
253
+ }
254
+ .badge.auth { background: #2a1a1a; color: #f93; }
255
+ .badge.public { background: #1a2a1a; color: var(--accent); }
256
+ .badge.tools { background: #1a1a2a; color: #5af; }
257
+ .badge.behaviors { background: #2a1a2a; color: #a7f; }
258
+ .badge.widget { background: #2a2a1a; color: #fd0; }
259
+ .badge.store { background: #1a2a2a; color: #5cc; }
260
+ .routes { margin-top: 8px; }
261
+ .route {
262
+ margin: 2px 0; border-left: 2px solid var(--border);
263
+ transition: border-color 0.15s;
264
+ }
265
+ .route:hover { border-left-color: #444; }
266
+ .route.expandable { cursor: pointer; }
267
+ .route.expandable .route-header::after {
268
+ content: "▸"; color: var(--text-dim); font-size: 10px; margin-left: auto;
269
+ transition: transform 0.15s;
270
+ }
271
+ .route.open .route-header::after { transform: rotate(90deg); }
272
+ .route-header {
273
+ display: flex; align-items: center; gap: 8px;
274
+ padding: 6px 12px; font-size: 13px;
275
+ }
276
+ .route-header:hover { background: var(--bg-card); }
277
+ .method { display: inline-block; width: 65px; font-weight: 700; flex-shrink: 0; }
278
+ .path { color: var(--text-bright); }
279
+ .summary { color: var(--text-dim); font-size: 12px; margin-left: 8px; }
280
+ .route-details {
281
+ display: none; padding: 8px 12px 12px 24px;
282
+ border-top: 1px solid var(--border); background: var(--bg-panel);
283
+ font-size: 12px;
284
+ }
285
+ .route.open .route-details { display: block; }
286
+ .detail { color: var(--text-dim); margin-bottom: 8px; font-style: italic; }
287
+ .param-table { margin: 8px 0; }
288
+ .param-table-title {
289
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;
290
+ color: var(--text-dim); margin-bottom: 4px; font-weight: 600;
291
+ }
292
+ .param-row {
293
+ display: flex; gap: 8px; padding: 3px 0;
294
+ border-bottom: 1px solid #1a1a1a; align-items: baseline;
295
+ }
296
+ .param-row:last-child { border-bottom: none; }
297
+ .param-name { color: var(--accent); font-weight: 600; min-width: 120px; flex-shrink: 0; }
298
+ .param-type { color: #5af; font-size: 11px; min-width: 80px; flex-shrink: 0; }
299
+ .param-required { color: #f93; font-size: 10px; font-weight: 600; }
300
+ .param-desc { color: var(--text-dim); }
301
+ .field-label { color: var(--text-dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
302
+ .response-doc { margin-top: 8px; }
303
+ .response-text { color: var(--text); }
304
+ .empty { color: var(--text-dim); font-style: italic; padding: 8px 12px; }
305
+ @media (max-width: 700px) {
306
+ .sidebar { display: none; }
307
+ .content { padding: 16px; }
308
+ }
309
+ </style>
310
+ </head>
311
+ <body>
312
+ <div class="layout">
313
+ <div class="sidebar">
314
+ <h1>▸ API Docs</h1>
315
+ <div class="summary">${docs.length} services · ${totalRoutes} routes · ${documented} documented</div>
316
+ ${nav}
317
+ </div>
318
+ <div class="content">
319
+ ${services}
320
+ </div>
321
+ </div>
322
+ </body>
323
+ </html>`;
324
+ }
325
+
326
+ function renderParamTable(
327
+ title: string,
328
+ params?: Record<string, ParamDoc>,
329
+ ): string {
330
+ if (!params || Object.keys(params).length === 0) return "";
331
+
332
+ const rows = Object.entries(params)
333
+ .map(([name, p]) => {
334
+ const req = p.required
335
+ ? ' <span class="param-required">required</span>'
336
+ : "";
337
+ return `<div class="param-row">
338
+ <span class="param-name">${esc(name)}${req}</span>
339
+ <span class="param-type">${esc(p.type)}</span>
340
+ <span class="param-desc">${esc(p.description ?? "")}</span>
341
+ </div>`;
342
+ })
343
+ .join("\n");
344
+
345
+ return `<div class="param-table">
346
+ <div class="param-table-title">${esc(title)}</div>
347
+ ${rows}
348
+ </div>`;
349
+ }
350
+
351
+ function esc(s: string): string {
352
+ return s
353
+ .replace(/&/g, "&amp;")
354
+ .replace(/</g, "&lt;")
355
+ .replace(/>/g, "&gt;")
356
+ .replace(/"/g, "&quot;");
357
+ }
358
+
359
+ // =============================================================================
360
+ // Routes
361
+ // =============================================================================
362
+
363
+ const routes = new Hono();
364
+
365
+ routes.get("/", (c) => c.json({ services: documentAll() }));
366
+
367
+ routes.get("/ui", (c) => c.html(renderHTML(documentAll())));
368
+
369
+ routes.get("/:service", (c) => {
370
+ const name = c.req.param("service");
371
+ const mod = ctx.getModule(name);
372
+ if (!mod) return c.json({ error: `Service "${name}" not found` }, 404);
373
+ return c.json(documentService(mod));
374
+ });
375
+
376
+ // =============================================================================
377
+ // Module
378
+ // =============================================================================
379
+
380
+ const docs: ServiceModule = {
381
+ name: "docs",
382
+ description: "Auto-generated API documentation",
383
+ routes,
384
+ requiresAuth: false,
385
+
386
+ init(serviceCtx: ServiceContext) {
387
+ ctx = serviceCtx;
388
+ },
389
+ };
390
+
391
+ export default docs;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Feed behaviors — auto-publish agent lifecycle events.
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import type { FleetClient } from "../src/core/types.js";
7
+
8
+ export function registerBehaviors(pi: ExtensionAPI, client: FleetClient) {
9
+ pi.on("agent_start", async () => {
10
+ if (!client.getBaseUrl()) return;
11
+ try {
12
+ await client.api("POST", "/feed/events", {
13
+ agent: client.agentName,
14
+ type: "agent_started",
15
+ summary: `Agent ${client.agentName} started processing`,
16
+ });
17
+ } catch {
18
+ // best-effort
19
+ }
20
+ });
21
+
22
+ // agent_end publish is handled by the usage service (it has cost data to include)
23
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Feed service module — activity event stream for coordination and observability.
3
+ *
4
+ * Listens to server-side events from other modules and auto-publishes
5
+ * them as feed events:
6
+ * board:task_created → feed event "task_started"
7
+ * board:task_updated → feed event "task_completed" (if status=done)
8
+ * board:task_deleted → (ignored)
9
+ */
10
+
11
+ import type { ServiceModule, ServiceContext, FleetClient } from "../src/core/types.js";
12
+ import { FeedStore } from "./store.js";
13
+ import { createRoutes } from "./routes.js";
14
+ import { registerTools } from "./tools.js";
15
+ import { registerBehaviors } from "./behaviors.js";
16
+
17
+ const store = new FeedStore();
18
+
19
+ const feed: ServiceModule = {
20
+ name: "feed",
21
+ description: "Activity event stream",
22
+ routes: createRoutes(store),
23
+ registerTools,
24
+ registerBehaviors,
25
+
26
+ routeDocs: {
27
+ "POST /events": {
28
+ summary: "Publish an event to the feed",
29
+ body: {
30
+ agent: { type: "string", required: true, description: "Agent name that produced the event" },
31
+ type: { type: "string", required: true, description: "task_started | task_completed | task_failed | blocker_found | question | finding | skill_proposed | file_changed | cost_update | agent_started | agent_stopped | token_update | custom" },
32
+ summary: { type: "string", required: true, description: "Short human-readable summary" },
33
+ detail: { type: "string", description: "Longer detail or structured JSON string" },
34
+ metadata: { type: "object", description: "Arbitrary key-value metadata" },
35
+ },
36
+ response: "The created event with generated ID and timestamp",
37
+ },
38
+ "GET /events": {
39
+ summary: "List recent events with optional filters",
40
+ query: {
41
+ agent: { type: "string", description: "Filter by agent name" },
42
+ type: { type: "string", description: "Filter by event type" },
43
+ since: { type: "string", description: "ULID or ISO timestamp — return events after this point" },
44
+ limit: { type: "number", description: "Max events to return. Default: 50" },
45
+ },
46
+ response: "Array of feed events, most recent last",
47
+ },
48
+ "GET /events/:id": {
49
+ summary: "Get a single event by ID",
50
+ params: { id: { type: "string", required: true, description: "Event ID (ULID)" } },
51
+ },
52
+ "DELETE /events": {
53
+ summary: "Clear all events",
54
+ detail: "Destructive — removes all events from memory and disk. Use with caution.",
55
+ response: "{ ok: true }",
56
+ },
57
+ "GET /stats": {
58
+ summary: "Get feed statistics",
59
+ response: "{ total, byAgent: { [name]: count }, byType: { [type]: count }, latestPerAgent: { [name]: Event } }",
60
+ },
61
+ "GET /stream": {
62
+ summary: "Server-Sent Events stream of new events in real time",
63
+ detail: "Connect with EventSource. Pass ?since=<ulid> to replay missed events on reconnect. Pass ?agent=<name> to filter by agent. Sends heartbeat comments every 15s.",
64
+ query: {
65
+ agent: { type: "string", description: "Filter to events from this agent only" },
66
+ since: { type: "string", description: "ULID — replay events since this ID before streaming" },
67
+ },
68
+ response: "SSE stream. Each message data is a JSON-encoded FeedEvent.",
69
+ },
70
+ },
71
+
72
+ init(ctx: ServiceContext) {
73
+ ctx.events.on("board:task_created", (data: any) => {
74
+ const task = data.task;
75
+ store.publish({
76
+ agent: task.createdBy || "unknown",
77
+ type: "task_started",
78
+ summary: `Task created: ${task.title}`,
79
+ metadata: { taskId: task.id, status: task.status },
80
+ });
81
+ });
82
+
83
+ ctx.events.on("board:task_updated", (data: any) => {
84
+ const task = data.task;
85
+ const changes = data.changes || {};
86
+
87
+ if (changes.status === "done") {
88
+ store.publish({
89
+ agent: task.assignee || task.createdBy || "unknown",
90
+ type: "task_completed",
91
+ summary: `Task completed: ${task.title}`,
92
+ metadata: { taskId: task.id },
93
+ });
94
+ } else if (changes.status === "blocked") {
95
+ store.publish({
96
+ agent: task.assignee || task.createdBy || "unknown",
97
+ type: "blocker_found",
98
+ summary: `Task blocked: ${task.title}`,
99
+ metadata: { taskId: task.id },
100
+ });
101
+ }
102
+ });
103
+ },
104
+
105
+ widget: {
106
+ async getLines(client: FleetClient) {
107
+ try {
108
+ const stats = await client.api<{ total: number }>("GET", "/feed/stats");
109
+ return [`Feed: ${stats.total} events`];
110
+ } catch {
111
+ return [];
112
+ }
113
+ },
114
+ },
115
+ };
116
+
117
+ export default feed;