@versdotsh/reef 0.1.4 → 0.1.6

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.
@@ -1,329 +0,0 @@
1
- /**
2
- * Board store — task tracking with notes and artifacts.
3
- *
4
- * Uses Bun's file APIs for persistence. JSON file with debounced writes.
5
- */
6
-
7
- import { ulid } from "ulid";
8
- import { existsSync, mkdirSync } from "node:fs";
9
- import { dirname } from "node:path";
10
-
11
- // =============================================================================
12
- // Types
13
- // =============================================================================
14
-
15
- export type TaskStatus = "open" | "in_progress" | "in_review" | "blocked" | "done";
16
- export type NoteType = "finding" | "blocker" | "question" | "update";
17
- export type ArtifactType = "branch" | "report" | "deploy" | "diff" | "file" | "url";
18
-
19
- export interface Note {
20
- id: string;
21
- author: string;
22
- content: string;
23
- type: NoteType;
24
- createdAt: string;
25
- }
26
-
27
- export interface Artifact {
28
- type: ArtifactType;
29
- url: string;
30
- label: string;
31
- addedAt: string;
32
- addedBy?: string;
33
- }
34
-
35
- export interface Task {
36
- id: string;
37
- title: string;
38
- description?: string;
39
- status: TaskStatus;
40
- assignee?: string;
41
- tags: string[];
42
- dependencies: string[];
43
- createdBy: string;
44
- createdAt: string;
45
- updatedAt: string;
46
- notes: Note[];
47
- artifacts: Artifact[];
48
- score: number;
49
- }
50
-
51
- export interface CreateTaskInput {
52
- title: string;
53
- description?: string;
54
- status?: TaskStatus;
55
- assignee?: string;
56
- tags?: string[];
57
- dependencies?: string[];
58
- createdBy: string;
59
- }
60
-
61
- export interface UpdateTaskInput {
62
- title?: string;
63
- description?: string;
64
- status?: TaskStatus;
65
- assignee?: string | null;
66
- tags?: string[];
67
- dependencies?: string[];
68
- }
69
-
70
- export interface AddNoteInput {
71
- author: string;
72
- content: string;
73
- type: NoteType;
74
- }
75
-
76
- export interface AddArtifactInput {
77
- type: ArtifactType;
78
- url: string;
79
- label: string;
80
- addedBy?: string;
81
- }
82
-
83
- export interface TaskFilters {
84
- status?: TaskStatus;
85
- assignee?: string;
86
- tag?: string;
87
- }
88
-
89
- // =============================================================================
90
- // Validation
91
- // =============================================================================
92
-
93
- const VALID_STATUSES = new Set<string>(["open", "in_progress", "in_review", "blocked", "done"]);
94
- const VALID_NOTE_TYPES = new Set<string>(["finding", "blocker", "question", "update"]);
95
- const VALID_ARTIFACT_TYPES = new Set<string>(["branch", "report", "deploy", "diff", "file", "url"]);
96
-
97
- // =============================================================================
98
- // Errors
99
- // =============================================================================
100
-
101
- export class NotFoundError extends Error {
102
- constructor(message: string) {
103
- super(message);
104
- this.name = "NotFoundError";
105
- }
106
- }
107
-
108
- export class ValidationError extends Error {
109
- constructor(message: string) {
110
- super(message);
111
- this.name = "ValidationError";
112
- }
113
- }
114
-
115
- // =============================================================================
116
- // Store
117
- // =============================================================================
118
-
119
- export class BoardStore {
120
- private tasks = new Map<string, Task>();
121
- private filePath: string;
122
- private writeTimer: ReturnType<typeof setTimeout> | null = null;
123
-
124
- constructor(filePath = "data/board.json") {
125
- this.filePath = filePath;
126
- this.load();
127
- }
128
-
129
- private load(): void {
130
- try {
131
- const file = Bun.file(this.filePath);
132
- if (existsSync(this.filePath)) {
133
- // Synchronous read at startup — Bun.file().text() is async,
134
- // so we use the fs import path for the initial load
135
- const raw = require("node:fs").readFileSync(this.filePath, "utf-8");
136
- const data = JSON.parse(raw);
137
- if (Array.isArray(data.tasks)) {
138
- for (const t of data.tasks) {
139
- if (!t.artifacts) t.artifacts = [];
140
- if (t.score === undefined) t.score = 0;
141
- this.tasks.set(t.id, t);
142
- }
143
- }
144
- }
145
- } catch {
146
- this.tasks = new Map();
147
- }
148
- }
149
-
150
- private scheduleSave(): void {
151
- if (this.writeTimer) return;
152
- this.writeTimer = setTimeout(() => {
153
- this.writeTimer = null;
154
- this.flushAsync();
155
- }, 100);
156
- }
157
-
158
- private async flushAsync(): Promise<void> {
159
- const dir = dirname(this.filePath);
160
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
161
- const data = JSON.stringify({ tasks: Array.from(this.tasks.values()) }, null, 2);
162
- await Bun.write(this.filePath, data);
163
- }
164
-
165
- flush(): void {
166
- if (this.writeTimer) {
167
- clearTimeout(this.writeTimer);
168
- this.writeTimer = null;
169
- }
170
- const dir = dirname(this.filePath);
171
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
172
- const data = JSON.stringify({ tasks: Array.from(this.tasks.values()) }, null, 2);
173
- require("node:fs").writeFileSync(this.filePath, data, "utf-8");
174
- }
175
-
176
- // --- CRUD ---
177
-
178
- createTask(input: CreateTaskInput): Task {
179
- if (!input.title?.trim()) throw new ValidationError("title is required");
180
- if (!input.createdBy?.trim()) throw new ValidationError("createdBy is required");
181
- if (input.status && !VALID_STATUSES.has(input.status)) {
182
- throw new ValidationError(`invalid status: ${input.status}`);
183
- }
184
-
185
- const now = new Date().toISOString();
186
- const task: Task = {
187
- id: ulid(),
188
- title: input.title.trim(),
189
- description: input.description?.trim(),
190
- status: input.status || "open",
191
- assignee: input.assignee?.trim(),
192
- tags: input.tags || [],
193
- dependencies: input.dependencies || [],
194
- createdBy: input.createdBy.trim(),
195
- createdAt: now,
196
- updatedAt: now,
197
- notes: [],
198
- artifacts: [],
199
- score: 0,
200
- };
201
-
202
- this.tasks.set(task.id, task);
203
- this.scheduleSave();
204
- return task;
205
- }
206
-
207
- getTask(id: string): Task | undefined {
208
- return this.tasks.get(id);
209
- }
210
-
211
- listTasks(filters?: TaskFilters): Task[] {
212
- let results = Array.from(this.tasks.values());
213
-
214
- if (filters?.status) results = results.filter((t) => t.status === filters.status);
215
- if (filters?.assignee) results = results.filter((t) => t.assignee === filters.assignee);
216
- if (filters?.tag) results = results.filter((t) => t.tags.includes(filters.tag!));
217
-
218
- results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
219
- return results;
220
- }
221
-
222
- updateTask(id: string, input: UpdateTaskInput): Task {
223
- const task = this.tasks.get(id);
224
- if (!task) throw new NotFoundError("task not found");
225
-
226
- if (input.status !== undefined && !VALID_STATUSES.has(input.status)) {
227
- throw new ValidationError(`invalid status: ${input.status}`);
228
- }
229
- if (input.title !== undefined) {
230
- if (!input.title.trim()) throw new ValidationError("title cannot be empty");
231
- task.title = input.title.trim();
232
- }
233
- if (input.description !== undefined) task.description = input.description?.trim();
234
- if (input.status !== undefined) task.status = input.status;
235
- if (input.assignee !== undefined) {
236
- task.assignee = input.assignee === null ? undefined : input.assignee?.trim();
237
- }
238
- if (input.tags !== undefined) task.tags = input.tags;
239
- if (input.dependencies !== undefined) task.dependencies = input.dependencies;
240
-
241
- task.updatedAt = new Date().toISOString();
242
- this.tasks.set(id, task);
243
- this.scheduleSave();
244
- return task;
245
- }
246
-
247
- deleteTask(id: string): boolean {
248
- const existed = this.tasks.delete(id);
249
- if (existed) this.scheduleSave();
250
- return existed;
251
- }
252
-
253
- bumpTask(id: string): Task {
254
- const task = this.tasks.get(id);
255
- if (!task) throw new NotFoundError("task not found");
256
-
257
- task.score = (task.score || 0) + 1;
258
- task.updatedAt = new Date().toISOString();
259
- this.tasks.set(id, task);
260
- this.scheduleSave();
261
- return task;
262
- }
263
-
264
- // --- Notes ---
265
-
266
- addNote(taskId: string, input: AddNoteInput): Note {
267
- const task = this.tasks.get(taskId);
268
- if (!task) throw new NotFoundError("task not found");
269
-
270
- if (!input.author?.trim()) throw new ValidationError("author is required");
271
- if (!input.content?.trim()) throw new ValidationError("content is required");
272
- if (!VALID_NOTE_TYPES.has(input.type)) throw new ValidationError(`invalid note type: ${input.type}`);
273
-
274
- const note: Note = {
275
- id: ulid(),
276
- author: input.author.trim(),
277
- content: input.content.trim(),
278
- type: input.type,
279
- createdAt: new Date().toISOString(),
280
- };
281
-
282
- task.notes.push(note);
283
- task.updatedAt = new Date().toISOString();
284
- this.tasks.set(taskId, task);
285
- this.scheduleSave();
286
- return note;
287
- }
288
-
289
- getNotes(taskId: string): Note[] {
290
- const task = this.tasks.get(taskId);
291
- if (!task) throw new NotFoundError("task not found");
292
- return task.notes;
293
- }
294
-
295
- // --- Artifacts ---
296
-
297
- addArtifacts(taskId: string, artifacts: AddArtifactInput[]): Artifact[] {
298
- const task = this.tasks.get(taskId);
299
- if (!task) throw new NotFoundError("task not found");
300
-
301
- if (!Array.isArray(artifacts) || artifacts.length === 0) {
302
- throw new ValidationError("artifacts array is required and must not be empty");
303
- }
304
-
305
- const now = new Date().toISOString();
306
- const added: Artifact[] = [];
307
-
308
- for (const a of artifacts) {
309
- if (!VALID_ARTIFACT_TYPES.has(a.type)) throw new ValidationError(`invalid artifact type: ${a.type}`);
310
- if (!a.url?.trim()) throw new ValidationError("artifact url is required");
311
- if (!a.label?.trim()) throw new ValidationError("artifact label is required");
312
-
313
- const artifact: Artifact = {
314
- type: a.type,
315
- url: a.url.trim(),
316
- label: a.label.trim(),
317
- addedAt: now,
318
- addedBy: a.addedBy?.trim(),
319
- };
320
- task.artifacts.push(artifact);
321
- added.push(artifact);
322
- }
323
-
324
- task.updatedAt = now;
325
- this.tasks.set(taskId, task);
326
- this.scheduleSave();
327
- return added;
328
- }
329
- }
@@ -1,214 +0,0 @@
1
- /**
2
- * Board tools — registered on the pi extension so the LLM can manage tasks.
3
- */
4
-
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
- import type { FleetClient } from "../src/core/types.js";
7
- import { Type } from "@sinclair/typebox";
8
- import { StringEnum } from "@mariozechner/pi-ai";
9
-
10
- const STATUS_ENUM = StringEnum(
11
- ["open", "in_progress", "in_review", "blocked", "done"] as const,
12
- { description: "Task status" },
13
- );
14
-
15
- const ARTIFACT_TYPE_ENUM = StringEnum(
16
- ["branch", "report", "deploy", "diff", "file", "url"] as const,
17
- { description: "Artifact type" },
18
- );
19
-
20
- export function registerTools(pi: ExtensionAPI, client: FleetClient) {
21
- pi.registerTool({
22
- name: "board_create_task",
23
- label: "Board: Create Task",
24
- description:
25
- "Create a new task on the shared coordination board. Returns the created task with its ID.",
26
- parameters: Type.Object({
27
- title: Type.String({ description: "Task title" }),
28
- description: Type.Optional(Type.String({ description: "Detailed task description" })),
29
- assignee: Type.Optional(Type.String({ description: "Agent or user to assign to" })),
30
- tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorization" })),
31
- }),
32
- async execute(_id, params) {
33
- if (!client.getBaseUrl()) return client.noUrl();
34
- try {
35
- const task = await client.api("POST", "/board/tasks", {
36
- ...params,
37
- createdBy: client.agentName,
38
- });
39
- return client.ok(JSON.stringify(task, null, 2), { task });
40
- } catch (e: any) {
41
- return client.err(e.message);
42
- }
43
- },
44
- });
45
-
46
- pi.registerTool({
47
- name: "board_list_tasks",
48
- label: "Board: List Tasks",
49
- description:
50
- "List tasks on the shared board. Optionally filter by status, assignee, or tag.",
51
- parameters: Type.Object({
52
- status: Type.Optional(STATUS_ENUM),
53
- assignee: Type.Optional(Type.String({ description: "Filter by assignee" })),
54
- tag: Type.Optional(Type.String({ description: "Filter by tag" })),
55
- }),
56
- async execute(_id, params) {
57
- if (!client.getBaseUrl()) return client.noUrl();
58
- try {
59
- const qs = new URLSearchParams();
60
- if (params.status) qs.set("status", params.status);
61
- if (params.assignee) qs.set("assignee", params.assignee);
62
- if (params.tag) qs.set("tag", params.tag);
63
- const query = qs.toString();
64
- const result = await client.api("GET", `/board/tasks${query ? `?${query}` : ""}`);
65
- return client.ok(JSON.stringify(result, null, 2), { result });
66
- } catch (e: any) {
67
- return client.err(e.message);
68
- }
69
- },
70
- });
71
-
72
- pi.registerTool({
73
- name: "board_update_task",
74
- label: "Board: Update Task",
75
- description:
76
- "Update a task — change status, reassign, rename, or update tags.",
77
- parameters: Type.Object({
78
- id: Type.String({ description: "Task ID to update" }),
79
- status: Type.Optional(STATUS_ENUM),
80
- assignee: Type.Optional(Type.String({ description: "New assignee" })),
81
- title: Type.Optional(Type.String({ description: "New title" })),
82
- tags: Type.Optional(Type.Array(Type.String(), { description: "New tags" })),
83
- }),
84
- async execute(_toolCallId, params) {
85
- if (!client.getBaseUrl()) return client.noUrl();
86
- try {
87
- const { id, ...updates } = params;
88
- const task = await client.api("PATCH", `/board/tasks/${encodeURIComponent(id)}`, updates);
89
- return client.ok(JSON.stringify(task, null, 2), { task });
90
- } catch (e: any) {
91
- return client.err(e.message);
92
- }
93
- },
94
- });
95
-
96
- pi.registerTool({
97
- name: "board_add_note",
98
- label: "Board: Add Note",
99
- description:
100
- "Add a note to a task — findings, blockers, questions, or status updates.",
101
- parameters: Type.Object({
102
- taskId: Type.String({ description: "Task ID to add the note to" }),
103
- content: Type.String({ description: "Note content" }),
104
- type: StringEnum(["finding", "blocker", "question", "update"] as const, {
105
- description: "Note type",
106
- }),
107
- }),
108
- async execute(_id, params) {
109
- if (!client.getBaseUrl()) return client.noUrl();
110
- try {
111
- const { taskId, ...body } = params;
112
- const note = await client.api(
113
- "POST",
114
- `/board/tasks/${encodeURIComponent(taskId)}/notes`,
115
- { ...body, author: client.agentName },
116
- );
117
- return client.ok(JSON.stringify(note, null, 2), { note });
118
- } catch (e: any) {
119
- return client.err(e.message);
120
- }
121
- },
122
- });
123
-
124
- pi.registerTool({
125
- name: "board_submit_for_review",
126
- label: "Board: Submit for Review",
127
- description:
128
- "Submit a task for review — sets status to in_review, adds a summary note, and optionally attaches artifacts.",
129
- parameters: Type.Object({
130
- taskId: Type.String({ description: "Task ID to submit for review" }),
131
- summary: Type.String({ description: "Review summary describing what was done" }),
132
- artifacts: Type.Optional(
133
- Type.Array(
134
- Type.Object({
135
- type: ARTIFACT_TYPE_ENUM,
136
- url: Type.String({ description: "URL or path to the artifact" }),
137
- label: Type.String({ description: "Human-readable label" }),
138
- }),
139
- { description: "Artifacts to attach" },
140
- ),
141
- ),
142
- }),
143
- async execute(_id, params) {
144
- if (!client.getBaseUrl()) return client.noUrl();
145
- try {
146
- const body: Record<string, unknown> = {
147
- summary: params.summary,
148
- reviewedBy: client.agentName,
149
- };
150
- if (params.artifacts) body.artifacts = params.artifacts;
151
- const task = await client.api(
152
- "POST",
153
- `/board/tasks/${encodeURIComponent(params.taskId)}/review`,
154
- body,
155
- );
156
- return client.ok(JSON.stringify(task, null, 2), { task });
157
- } catch (e: any) {
158
- return client.err(e.message);
159
- }
160
- },
161
- });
162
-
163
- pi.registerTool({
164
- name: "board_add_artifact",
165
- label: "Board: Add Artifact",
166
- description:
167
- "Add artifact link(s) to any task — branches, reports, deploys, diffs, files, or URLs.",
168
- parameters: Type.Object({
169
- taskId: Type.String({ description: "Task ID" }),
170
- artifacts: Type.Array(
171
- Type.Object({
172
- type: ARTIFACT_TYPE_ENUM,
173
- url: Type.String({ description: "URL or path" }),
174
- label: Type.String({ description: "Human-readable label" }),
175
- }),
176
- { description: "Artifacts to attach" },
177
- ),
178
- }),
179
- async execute(_id, params) {
180
- if (!client.getBaseUrl()) return client.noUrl();
181
- try {
182
- const task = await client.api(
183
- "POST",
184
- `/board/tasks/${encodeURIComponent(params.taskId)}/artifacts`,
185
- { artifacts: params.artifacts.map((a) => ({ ...a, addedBy: client.agentName })) },
186
- );
187
- return client.ok(JSON.stringify(task, null, 2), { task });
188
- } catch (e: any) {
189
- return client.err(e.message);
190
- }
191
- },
192
- });
193
-
194
- pi.registerTool({
195
- name: "board_bump",
196
- label: "Board: Bump Task",
197
- description: "Bump a task's score by 1. Use to signal priority or upvote.",
198
- parameters: Type.Object({
199
- taskId: Type.String({ description: "Task ID to bump" }),
200
- }),
201
- async execute(_id, params) {
202
- if (!client.getBaseUrl()) return client.noUrl();
203
- try {
204
- const task = await client.api(
205
- "POST",
206
- `/board/tasks/${encodeURIComponent(params.taskId)}/bump`,
207
- );
208
- return client.ok(JSON.stringify(task, null, 2), { task });
209
- } catch (e: any) {
210
- return client.err(e.message);
211
- }
212
- },
213
- });
214
- }
@@ -1,23 +0,0 @@
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
- }
@@ -1,117 +0,0 @@
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;