@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.
- package/.github/workflows/test.yml +47 -0
- package/README.md +257 -0
- package/bun.lock +587 -0
- package/examples/services/board/board.test.ts +215 -0
- package/examples/services/board/index.ts +155 -0
- package/examples/services/board/routes.ts +335 -0
- package/examples/services/board/store.ts +329 -0
- package/examples/services/board/tools.ts +214 -0
- package/examples/services/commits/commits.test.ts +74 -0
- package/examples/services/commits/index.ts +14 -0
- package/examples/services/commits/routes.ts +43 -0
- package/examples/services/commits/store.ts +114 -0
- package/examples/services/feed/behaviors.ts +23 -0
- package/examples/services/feed/feed.test.ts +101 -0
- package/examples/services/feed/index.ts +117 -0
- package/examples/services/feed/routes.ts +224 -0
- package/examples/services/feed/store.ts +194 -0
- package/examples/services/feed/tools.ts +83 -0
- package/examples/services/journal/index.ts +15 -0
- package/examples/services/journal/journal.test.ts +57 -0
- package/examples/services/journal/routes.ts +45 -0
- package/examples/services/journal/store.ts +119 -0
- package/examples/services/journal/tools.ts +32 -0
- package/examples/services/log/index.ts +15 -0
- package/examples/services/log/log.test.ts +70 -0
- package/examples/services/log/routes.ts +44 -0
- package/examples/services/log/store.ts +105 -0
- package/examples/services/log/tools.ts +57 -0
- package/examples/services/registry/behaviors.ts +128 -0
- package/examples/services/registry/index.ts +37 -0
- package/examples/services/registry/registry.test.ts +135 -0
- package/examples/services/registry/routes.ts +76 -0
- package/examples/services/registry/store.ts +224 -0
- package/examples/services/registry/tools.ts +116 -0
- package/examples/services/reports/index.ts +14 -0
- package/examples/services/reports/reports.test.ts +75 -0
- package/examples/services/reports/routes.ts +42 -0
- package/examples/services/reports/store.ts +110 -0
- package/examples/services/ui/auth.ts +61 -0
- package/examples/services/ui/index.ts +16 -0
- package/examples/services/ui/routes.ts +160 -0
- package/examples/services/ui/static/app.js +369 -0
- package/examples/services/ui/static/index.html +42 -0
- package/examples/services/ui/static/style.css +157 -0
- package/examples/services/usage/behaviors.ts +166 -0
- package/examples/services/usage/index.ts +19 -0
- package/examples/services/usage/routes.ts +53 -0
- package/examples/services/usage/store.ts +341 -0
- package/examples/services/usage/tools.ts +75 -0
- package/examples/services/usage/usage.test.ts +91 -0
- package/package.json +29 -0
- package/services/agent/index.ts +465 -0
- package/services/board/index.ts +155 -0
- package/services/board/routes.ts +335 -0
- package/services/board/store.ts +329 -0
- package/services/board/tools.ts +214 -0
- package/services/docs/index.ts +391 -0
- package/services/feed/behaviors.ts +23 -0
- package/services/feed/index.ts +117 -0
- package/services/feed/routes.ts +224 -0
- package/services/feed/store.ts +194 -0
- package/services/feed/tools.ts +83 -0
- package/services/installer/index.ts +574 -0
- package/services/services/index.ts +165 -0
- package/services/ui/auth.ts +61 -0
- package/services/ui/index.ts +16 -0
- package/services/ui/routes.ts +160 -0
- package/services/ui/static/app.js +369 -0
- package/services/ui/static/index.html +42 -0
- package/services/ui/static/style.css +157 -0
- package/skills/create-service/SKILL.md +698 -0
- package/src/core/auth.ts +28 -0
- package/src/core/client.ts +99 -0
- package/src/core/discover.ts +152 -0
- package/src/core/events.ts +44 -0
- package/src/core/extension.ts +66 -0
- package/src/core/server.ts +262 -0
- package/src/core/testing.ts +155 -0
- package/src/core/types.ts +194 -0
- package/src/extension.ts +16 -0
- package/src/main.ts +11 -0
- package/tests/server.test.ts +1338 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
}
|