@hdwebsoft/hdcode-agent-team 0.1.0 → 0.1.1
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/dist/index.js +452 -437
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readdir as readdir2 } from "node:fs/promises";
|
|
|
3
3
|
import { join as join3 } from "node:path";
|
|
4
4
|
|
|
5
5
|
// src/utils.ts
|
|
6
|
-
import { readdir, mkdir, rename, unlink } from "node:fs/promises";
|
|
6
|
+
import { readdir, mkdir, rename, unlink, stat } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
var TEAM_ROOT = ".team";
|
|
9
9
|
var SAFE_NAME = /^[a-z0-9-]+$/;
|
|
@@ -20,7 +20,12 @@ function teamDir(base, teamName) {
|
|
|
20
20
|
return join(base, TEAM_ROOT, teamName);
|
|
21
21
|
}
|
|
22
22
|
async function exists(path) {
|
|
23
|
-
|
|
23
|
+
try {
|
|
24
|
+
await stat(path);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
24
29
|
}
|
|
25
30
|
async function writeJsonAtomic(path, data) {
|
|
26
31
|
const tmp = path + ".tmp";
|
|
@@ -176,481 +181,491 @@ async function appendToInbox(inboxPath, message) {
|
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
// src/tools/team-tools.ts
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}),
|
|
219
|
-
team_delete: tool({
|
|
220
|
-
description: "Delete a team and all its data (tasks, inboxes, reports).",
|
|
221
|
-
args: {
|
|
222
|
-
teamName: tool.schema.string().describe("Team name to delete")
|
|
223
|
-
},
|
|
224
|
-
async execute(args, context) {
|
|
225
|
-
const dir = teamDir(context.directory, args.teamName);
|
|
226
|
-
if (!await exists(join4(dir, "config.json"))) {
|
|
227
|
-
return `Error: Team "${args.teamName}" not found`;
|
|
228
|
-
}
|
|
229
|
-
await rm(dir, { recursive: true, force: true });
|
|
230
|
-
return `Team "${args.teamName}" deleted successfully`;
|
|
231
|
-
}
|
|
232
|
-
}),
|
|
233
|
-
team_status: tool({
|
|
234
|
-
description: "Get team status: config, task summary by status, message counts per agent, reports, completion flag.",
|
|
235
|
-
args: {
|
|
236
|
-
teamName: tool.schema.string().describe("Team name")
|
|
237
|
-
},
|
|
238
|
-
async execute(args, context) {
|
|
239
|
-
const dir = teamDir(context.directory, args.teamName);
|
|
240
|
-
if (!await exists(join4(dir, "config.json"))) {
|
|
241
|
-
return `Error: Team "${args.teamName}" not found`;
|
|
184
|
+
function createTeamTools(directory) {
|
|
185
|
+
return {
|
|
186
|
+
team_create: tool({
|
|
187
|
+
description: "Create a new agent team with directory structure and CC-aligned config. Lead is automatically added as first member. Returns the team config.",
|
|
188
|
+
args: {
|
|
189
|
+
teamName: tool.schema.string().regex(/^[a-z0-9-]+$/).describe("Team name in kebab-case"),
|
|
190
|
+
description: tool.schema.string().describe("Team description")
|
|
191
|
+
},
|
|
192
|
+
async execute(args) {
|
|
193
|
+
const dir = teamDir(directory, args.teamName);
|
|
194
|
+
if (await exists(join4(dir, "config.json"))) {
|
|
195
|
+
return `Error: Team "${args.teamName}" already exists`;
|
|
196
|
+
}
|
|
197
|
+
await ensureDir(join4(dir, "tasks"));
|
|
198
|
+
await ensureDir(join4(dir, "inboxes"));
|
|
199
|
+
await ensureDir(join4(dir, "reports"));
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
const leadId = `team-lead@${args.teamName}`;
|
|
202
|
+
const config = {
|
|
203
|
+
name: args.teamName,
|
|
204
|
+
description: args.description,
|
|
205
|
+
createdAt: now,
|
|
206
|
+
leadAgentId: leadId,
|
|
207
|
+
members: [
|
|
208
|
+
{
|
|
209
|
+
agentId: leadId,
|
|
210
|
+
name: "team-lead",
|
|
211
|
+
agentType: "team-lead",
|
|
212
|
+
model: "unknown",
|
|
213
|
+
planModeRequired: false,
|
|
214
|
+
joinedAt: now,
|
|
215
|
+
cwd: directory,
|
|
216
|
+
isActive: true
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
};
|
|
220
|
+
await writeJsonAtomic(join4(dir, "config.json"), config);
|
|
221
|
+
await writeJsonAtomic(join4(dir, "inboxes", "team-lead.json"), []);
|
|
222
|
+
return JSON.stringify(config, null, 2);
|
|
242
223
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
224
|
+
}),
|
|
225
|
+
team_delete: tool({
|
|
226
|
+
description: "Delete a team and all its data (tasks, inboxes, reports).",
|
|
227
|
+
args: {
|
|
228
|
+
teamName: tool.schema.string().describe("Team name to delete")
|
|
229
|
+
},
|
|
230
|
+
async execute(args) {
|
|
231
|
+
const dir = teamDir(directory, args.teamName);
|
|
232
|
+
if (!await exists(join4(dir, "config.json"))) {
|
|
233
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
249
234
|
}
|
|
235
|
+
await rm(dir, { recursive: true, force: true });
|
|
236
|
+
return `Team "${args.teamName}" deleted successfully`;
|
|
250
237
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
238
|
+
}),
|
|
239
|
+
team_status: tool({
|
|
240
|
+
description: "Get team status: config, task summary by status, message counts per agent, reports, completion flag.",
|
|
241
|
+
args: {
|
|
242
|
+
teamName: tool.schema.string().describe("Team name")
|
|
243
|
+
},
|
|
244
|
+
async execute(args) {
|
|
245
|
+
const dir = teamDir(directory, args.teamName);
|
|
246
|
+
if (!await exists(join4(dir, "config.json"))) {
|
|
247
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
248
|
+
}
|
|
249
|
+
const config = await readJson(join4(dir, "config.json"));
|
|
250
|
+
const tasks = await loadAllTasks(directory, args.teamName);
|
|
251
|
+
const summary = { pending: 0, in_progress: 0, completed: 0 };
|
|
252
|
+
for (const t of tasks) {
|
|
253
|
+
if (t.status in summary) {
|
|
254
|
+
summary[t.status]++;
|
|
255
|
+
}
|
|
262
256
|
}
|
|
257
|
+
const inboxesDir = join4(dir, "inboxes");
|
|
258
|
+
const messageCounts = {};
|
|
259
|
+
if (await exists(inboxesDir)) {
|
|
260
|
+
const inboxFiles = await listJsonFiles(inboxesDir);
|
|
261
|
+
for (const f of inboxFiles) {
|
|
262
|
+
const agentName = f.split("/").pop().replace(".json", "");
|
|
263
|
+
const messages = await readInbox(f);
|
|
264
|
+
messageCounts[agentName] = {
|
|
265
|
+
total: messages.length,
|
|
266
|
+
unread: messages.filter((m) => !m.read).length
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const reportDir = join4(dir, "reports");
|
|
271
|
+
const reports = await exists(reportDir) ? (await readdir3(reportDir)).filter((f) => !f.startsWith(".")) : [];
|
|
272
|
+
const total = Object.values(summary).reduce((a, b) => a + b, 0);
|
|
273
|
+
const isComplete = total > 0 && summary.pending === 0 && summary.in_progress === 0;
|
|
274
|
+
return JSON.stringify({ config, taskSummary: summary, messageCounts, reports, isComplete }, null, 2);
|
|
263
275
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
memberCount: config.members.length,
|
|
297
|
-
taskSummary: { pending, in_progress: inProgress, completed },
|
|
298
|
-
isComplete
|
|
299
|
-
});
|
|
276
|
+
}),
|
|
277
|
+
team_list: tool({
|
|
278
|
+
description: "List all teams with basic info.",
|
|
279
|
+
args: {},
|
|
280
|
+
async execute() {
|
|
281
|
+
const root = join4(directory, TEAM_ROOT);
|
|
282
|
+
if (!await exists(root))
|
|
283
|
+
return "No teams found";
|
|
284
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
285
|
+
const teams = [];
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
if (!entry.isDirectory())
|
|
288
|
+
continue;
|
|
289
|
+
const configPath = join4(root, entry.name, "config.json");
|
|
290
|
+
if (!await exists(configPath))
|
|
291
|
+
continue;
|
|
292
|
+
const config = await readJson(configPath);
|
|
293
|
+
const tasks = await loadAllTasks(directory, entry.name);
|
|
294
|
+
const pending = tasks.filter((t) => t.status === "pending").length;
|
|
295
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress").length;
|
|
296
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
297
|
+
const isComplete = tasks.length > 0 && pending === 0 && inProgress === 0;
|
|
298
|
+
teams.push({
|
|
299
|
+
name: config.name,
|
|
300
|
+
description: config.description,
|
|
301
|
+
createdAt: config.createdAt,
|
|
302
|
+
memberCount: config.members.length,
|
|
303
|
+
taskSummary: { pending, in_progress: inProgress, completed },
|
|
304
|
+
isComplete
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return teams.length > 0 ? JSON.stringify(teams, null, 2) : "No teams found";
|
|
300
308
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
};
|
|
309
|
+
})
|
|
310
|
+
};
|
|
311
|
+
}
|
|
305
312
|
|
|
306
313
|
// src/tools/task-tools.ts
|
|
307
314
|
import { join as join5 } from "node:path";
|
|
308
315
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
return "Error: Invalid addBlocks JSON";
|
|
339
|
-
}
|
|
316
|
+
function parseStringArray(raw, label) {
|
|
317
|
+
let parsed;
|
|
318
|
+
try {
|
|
319
|
+
parsed = JSON.parse(raw);
|
|
320
|
+
} catch {
|
|
321
|
+
return `Error: Invalid ${label} JSON`;
|
|
322
|
+
}
|
|
323
|
+
if (!Array.isArray(parsed) || !parsed.every((v) => typeof v === "string")) {
|
|
324
|
+
return `Error: ${label} must be a JSON array of strings, e.g. ["1","2"]`;
|
|
325
|
+
}
|
|
326
|
+
return parsed;
|
|
327
|
+
}
|
|
328
|
+
function createTaskTools(directory) {
|
|
329
|
+
return {
|
|
330
|
+
task_create: tool2({
|
|
331
|
+
description: "Create a task with auto-increment integer ID. Supports optional dependencies (addBlocks/addBlockedBy) at creation. If blockedBy tasks are not completed, status stays pending. Returns created task.",
|
|
332
|
+
args: {
|
|
333
|
+
teamName: tool2.schema.string().describe("Team name"),
|
|
334
|
+
subject: tool2.schema.string().describe("Task subject, <60 chars, imperative"),
|
|
335
|
+
description: tool2.schema.string().describe("Task description"),
|
|
336
|
+
activeForm: tool2.schema.string().optional().describe("Spinner text when in_progress"),
|
|
337
|
+
addBlocks: tool2.schema.string().optional().describe('JSON array of task IDs this task blocks, e.g. ["3","4"]'),
|
|
338
|
+
addBlockedBy: tool2.schema.string().optional().describe('JSON array of task IDs that block this task, e.g. ["1"]'),
|
|
339
|
+
metadata: tool2.schema.string().optional().describe("JSON object of arbitrary metadata")
|
|
340
|
+
},
|
|
341
|
+
async execute(args) {
|
|
342
|
+
const dir = teamDir(directory, args.teamName);
|
|
343
|
+
if (!await exists(join5(dir, "config.json"))) {
|
|
344
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
340
345
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
+
const tasksDir = join5(dir, "tasks");
|
|
347
|
+
await ensureDir(tasksDir);
|
|
348
|
+
const tasksLockPath = join5(tasksDir, ".lock");
|
|
349
|
+
return withLock(tasksLockPath, async () => {
|
|
350
|
+
let addBlocks = [];
|
|
351
|
+
let addBlockedBy = [];
|
|
352
|
+
let metadata;
|
|
353
|
+
if (args.addBlocks) {
|
|
354
|
+
const result = parseStringArray(args.addBlocks, "addBlocks");
|
|
355
|
+
if (typeof result === "string")
|
|
356
|
+
return result;
|
|
357
|
+
addBlocks = result;
|
|
346
358
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return "Error: Invalid metadata JSON";
|
|
359
|
+
if (args.addBlockedBy) {
|
|
360
|
+
const result = parseStringArray(args.addBlockedBy, "addBlockedBy");
|
|
361
|
+
if (typeof result === "string")
|
|
362
|
+
return result;
|
|
363
|
+
addBlockedBy = result;
|
|
353
364
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
for (const bid of addBlocks) {
|
|
362
|
-
if (!taskMap.has(bid))
|
|
363
|
-
return `Error: Blocked task "${bid}" not found`;
|
|
364
|
-
}
|
|
365
|
-
if (addBlockedBy.length > 0 && hasCycle(id, addBlockedBy, taskMap)) {
|
|
366
|
-
return `Error: Circular dependency detected for task ${id}`;
|
|
367
|
-
}
|
|
368
|
-
const task = {
|
|
369
|
-
id,
|
|
370
|
-
subject: args.subject,
|
|
371
|
-
description: args.description,
|
|
372
|
-
...args.activeForm && { activeForm: args.activeForm },
|
|
373
|
-
status: "pending",
|
|
374
|
-
blocks: addBlocks,
|
|
375
|
-
blockedBy: addBlockedBy,
|
|
376
|
-
...metadata && { metadata }
|
|
377
|
-
};
|
|
378
|
-
await writeJsonAtomic(join5(tasksDir, `${id}.json`), task);
|
|
379
|
-
for (const targetId of addBlockedBy) {
|
|
380
|
-
const targetPath = taskFilePath(context.directory, args.teamName, targetId);
|
|
381
|
-
const target = await readJson(targetPath);
|
|
382
|
-
if (!target.blocks.includes(id)) {
|
|
383
|
-
target.blocks.push(id);
|
|
384
|
-
await writeJsonAtomic(targetPath, target);
|
|
365
|
+
if (args.metadata) {
|
|
366
|
+
try {
|
|
367
|
+
metadata = JSON.parse(args.metadata);
|
|
368
|
+
} catch {
|
|
369
|
+
return "Error: Invalid metadata JSON";
|
|
370
|
+
}
|
|
385
371
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
target.blockedBy.push(id);
|
|
392
|
-
await writeJsonAtomic(targetPath, target);
|
|
372
|
+
const allTasks = await loadAllTasks(directory, args.teamName);
|
|
373
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
374
|
+
for (const bid of addBlockedBy) {
|
|
375
|
+
if (!taskMap.has(bid))
|
|
376
|
+
return `Error: Blocker task "${bid}" not found`;
|
|
393
377
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
}),
|
|
399
|
-
task_update: tool2({
|
|
400
|
-
description: "Update task fields, manage dependencies, auto-unblock. Status transitions: pending→in_progress, pending→completed, in_progress→completed. On completion: auto-unblocks dependents. Returns {task, unblocked[]}.",
|
|
401
|
-
args: {
|
|
402
|
-
teamName: tool2.schema.string().describe("Team name"),
|
|
403
|
-
taskId: tool2.schema.string().describe("Task ID (integer string)"),
|
|
404
|
-
status: tool2.schema.enum(["pending", "in_progress", "completed"]).optional().describe("New status"),
|
|
405
|
-
subject: tool2.schema.string().optional().describe("New subject"),
|
|
406
|
-
description: tool2.schema.string().optional().describe("New description"),
|
|
407
|
-
activeForm: tool2.schema.string().optional().describe("Spinner text when in_progress"),
|
|
408
|
-
owner: tool2.schema.string().optional().describe("Agent name claiming this task"),
|
|
409
|
-
addBlocks: tool2.schema.string().optional().describe("JSON array of task IDs to add to blocks[]"),
|
|
410
|
-
addBlockedBy: tool2.schema.string().optional().describe("JSON array of task IDs to add to blockedBy[]"),
|
|
411
|
-
metadata: tool2.schema.string().optional().describe("JSON object to merge into metadata")
|
|
412
|
-
},
|
|
413
|
-
async execute(args, context) {
|
|
414
|
-
const dir = teamDir(context.directory, args.teamName);
|
|
415
|
-
if (!await exists(join5(dir, "config.json"))) {
|
|
416
|
-
return `Error: Team "${args.teamName}" not found`;
|
|
417
|
-
}
|
|
418
|
-
const tasksDir = join5(dir, "tasks");
|
|
419
|
-
const tasksLockPath = join5(tasksDir, ".lock");
|
|
420
|
-
return withLock(tasksLockPath, async () => {
|
|
421
|
-
const tPath = taskFilePath(context.directory, args.teamName, args.taskId);
|
|
422
|
-
if (!await exists(tPath)) {
|
|
423
|
-
return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
|
|
424
|
-
}
|
|
425
|
-
const task = await readJson(tPath);
|
|
426
|
-
if (args.status) {
|
|
427
|
-
const validTransitions = {
|
|
428
|
-
pending: ["in_progress", "completed"],
|
|
429
|
-
in_progress: ["completed"],
|
|
430
|
-
completed: []
|
|
431
|
-
};
|
|
432
|
-
const allowed = validTransitions[task.status];
|
|
433
|
-
if (!allowed || !allowed.includes(args.status)) {
|
|
434
|
-
return `Error: Invalid transition ${task.status} → ${args.status}`;
|
|
378
|
+
for (const bid of addBlocks) {
|
|
379
|
+
if (!taskMap.has(bid))
|
|
380
|
+
return `Error: Blocked task "${bid}" not found`;
|
|
435
381
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
382
|
+
const id = await nextTaskId(tasksDir);
|
|
383
|
+
if (addBlockedBy.length > 0 && hasCycle(id, addBlockedBy, taskMap)) {
|
|
384
|
+
return `Error: Circular dependency detected for task ${id}`;
|
|
385
|
+
}
|
|
386
|
+
const task = {
|
|
387
|
+
id,
|
|
388
|
+
subject: args.subject,
|
|
389
|
+
description: args.description,
|
|
390
|
+
...args.activeForm && { activeForm: args.activeForm },
|
|
391
|
+
status: "pending",
|
|
392
|
+
blocks: addBlocks,
|
|
393
|
+
blockedBy: addBlockedBy,
|
|
394
|
+
...metadata && { metadata }
|
|
395
|
+
};
|
|
396
|
+
await writeJsonAtomic(join5(tasksDir, `${id}.json`), task);
|
|
397
|
+
for (const targetId of addBlockedBy) {
|
|
398
|
+
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
399
|
+
const target = await readJson(targetPath);
|
|
400
|
+
if (!target.blocks.includes(id)) {
|
|
401
|
+
target.blocks.push(id);
|
|
402
|
+
await writeJsonAtomic(targetPath, target);
|
|
449
403
|
}
|
|
450
404
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
task.activeForm = args.activeForm;
|
|
459
|
-
if (args.owner !== undefined)
|
|
460
|
-
task.owner = args.owner;
|
|
461
|
-
if (args.metadata) {
|
|
462
|
-
let newMeta;
|
|
463
|
-
try {
|
|
464
|
-
newMeta = JSON.parse(args.metadata);
|
|
465
|
-
} catch {
|
|
466
|
-
return "Error: Invalid metadata JSON";
|
|
405
|
+
for (const targetId of addBlocks) {
|
|
406
|
+
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
407
|
+
const target = await readJson(targetPath);
|
|
408
|
+
if (!target.blockedBy.includes(id)) {
|
|
409
|
+
target.blockedBy.push(id);
|
|
410
|
+
await writeJsonAtomic(targetPath, target);
|
|
411
|
+
}
|
|
467
412
|
}
|
|
468
|
-
|
|
413
|
+
return JSON.stringify(task, null, 2);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}),
|
|
417
|
+
task_update: tool2({
|
|
418
|
+
description: "Update task fields, manage dependencies, auto-unblock. Status transitions: pending→in_progress, pending→completed, in_progress→completed. On completion: auto-unblocks dependents. Returns {task, unblocked[]}.",
|
|
419
|
+
args: {
|
|
420
|
+
teamName: tool2.schema.string().describe("Team name"),
|
|
421
|
+
taskId: tool2.schema.string().describe("Task ID (integer string)"),
|
|
422
|
+
status: tool2.schema.enum(["pending", "in_progress", "completed"]).optional().describe("New status"),
|
|
423
|
+
subject: tool2.schema.string().optional().describe("New subject"),
|
|
424
|
+
description: tool2.schema.string().optional().describe("New description"),
|
|
425
|
+
activeForm: tool2.schema.string().optional().describe("Spinner text when in_progress"),
|
|
426
|
+
owner: tool2.schema.string().optional().describe("Agent name claiming this task"),
|
|
427
|
+
addBlocks: tool2.schema.string().optional().describe("JSON array of task IDs to add to blocks[]"),
|
|
428
|
+
addBlockedBy: tool2.schema.string().optional().describe("JSON array of task IDs to add to blockedBy[]"),
|
|
429
|
+
metadata: tool2.schema.string().optional().describe("JSON object to merge into metadata")
|
|
430
|
+
},
|
|
431
|
+
async execute(args) {
|
|
432
|
+
const dir = teamDir(directory, args.teamName);
|
|
433
|
+
if (!await exists(join5(dir, "config.json"))) {
|
|
434
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
469
435
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return
|
|
436
|
+
const tasksDir = join5(dir, "tasks");
|
|
437
|
+
const tasksLockPath = join5(tasksDir, ".lock");
|
|
438
|
+
return withLock(tasksLockPath, async () => {
|
|
439
|
+
const tPath = taskFilePath(directory, args.teamName, args.taskId);
|
|
440
|
+
if (!await exists(tPath)) {
|
|
441
|
+
return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
|
|
476
442
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
443
|
+
const task = await readJson(tPath);
|
|
444
|
+
if (args.status) {
|
|
445
|
+
const validTransitions = {
|
|
446
|
+
pending: ["in_progress", "completed"],
|
|
447
|
+
in_progress: ["completed"],
|
|
448
|
+
completed: []
|
|
449
|
+
};
|
|
450
|
+
const allowed = validTransitions[task.status];
|
|
451
|
+
if (!allowed || !allowed.includes(args.status)) {
|
|
452
|
+
return `Error: Invalid transition ${task.status} → ${args.status}`;
|
|
480
453
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
454
|
+
if (args.status === "in_progress" && task.blockedBy.length > 0) {
|
|
455
|
+
const activeBlockers = [];
|
|
456
|
+
for (const bid of task.blockedBy) {
|
|
457
|
+
const blockerPath = taskFilePath(directory, args.teamName, bid);
|
|
458
|
+
if (await exists(blockerPath)) {
|
|
459
|
+
const blocker = await readJson(blockerPath);
|
|
460
|
+
if (blocker.status !== "completed") {
|
|
461
|
+
activeBlockers.push(bid);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (activeBlockers.length > 0) {
|
|
466
|
+
return `Error: Cannot start task with unresolved blockers: ${activeBlockers.join(", ")}`;
|
|
487
467
|
}
|
|
488
468
|
}
|
|
469
|
+
task.status = args.status;
|
|
489
470
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
471
|
+
if (args.subject !== undefined)
|
|
472
|
+
task.subject = args.subject;
|
|
473
|
+
if (args.description !== undefined)
|
|
474
|
+
task.description = args.description;
|
|
475
|
+
if (args.activeForm !== undefined)
|
|
476
|
+
task.activeForm = args.activeForm;
|
|
477
|
+
if (args.owner !== undefined)
|
|
478
|
+
task.owner = args.owner;
|
|
479
|
+
if (args.metadata) {
|
|
480
|
+
let newMeta;
|
|
481
|
+
try {
|
|
482
|
+
newMeta = JSON.parse(args.metadata);
|
|
483
|
+
} catch {
|
|
484
|
+
return "Error: Invalid metadata JSON";
|
|
485
|
+
}
|
|
486
|
+
task.metadata = { ...task.metadata, ...newMeta };
|
|
497
487
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
488
|
+
if (args.addBlocks) {
|
|
489
|
+
const result = parseStringArray(args.addBlocks, "addBlocks");
|
|
490
|
+
if (typeof result === "string")
|
|
491
|
+
return result;
|
|
492
|
+
for (const targetId of result) {
|
|
493
|
+
if (!task.blocks.includes(targetId)) {
|
|
494
|
+
task.blocks.push(targetId);
|
|
495
|
+
}
|
|
496
|
+
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
497
|
+
if (await exists(targetPath)) {
|
|
498
|
+
const target = await readJson(targetPath);
|
|
499
|
+
if (!target.blockedBy.includes(task.id)) {
|
|
500
|
+
target.blockedBy.push(task.id);
|
|
501
|
+
await writeJsonAtomic(targetPath, target);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
501
504
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
505
|
+
}
|
|
506
|
+
if (args.addBlockedBy) {
|
|
507
|
+
const result = parseStringArray(args.addBlockedBy, "addBlockedBy");
|
|
508
|
+
if (typeof result === "string")
|
|
509
|
+
return result;
|
|
510
|
+
for (const targetId of result) {
|
|
511
|
+
if (!task.blockedBy.includes(targetId)) {
|
|
512
|
+
task.blockedBy.push(targetId);
|
|
513
|
+
}
|
|
514
|
+
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
515
|
+
if (await exists(targetPath)) {
|
|
516
|
+
const target = await readJson(targetPath);
|
|
517
|
+
if (!target.blocks.includes(task.id)) {
|
|
518
|
+
target.blocks.push(task.id);
|
|
519
|
+
await writeJsonAtomic(targetPath, target);
|
|
520
|
+
}
|
|
508
521
|
}
|
|
509
522
|
}
|
|
510
523
|
}
|
|
524
|
+
await writeJsonAtomic(tPath, task);
|
|
525
|
+
let unblocked = [];
|
|
526
|
+
if (task.status === "completed") {
|
|
527
|
+
unblocked = await unblockDependents(directory, args.teamName, task.id);
|
|
528
|
+
}
|
|
529
|
+
return JSON.stringify({ task, unblocked }, null, 2);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}),
|
|
533
|
+
task_get: tool2({
|
|
534
|
+
description: "Get a single task by ID.",
|
|
535
|
+
args: {
|
|
536
|
+
teamName: tool2.schema.string().describe("Team name"),
|
|
537
|
+
taskId: tool2.schema.string().describe("Task ID (integer string)")
|
|
538
|
+
},
|
|
539
|
+
async execute(args) {
|
|
540
|
+
const tPath = taskFilePath(directory, args.teamName, args.taskId);
|
|
541
|
+
if (!await exists(tPath)) {
|
|
542
|
+
return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
|
|
511
543
|
}
|
|
512
|
-
await
|
|
513
|
-
let unblocked = [];
|
|
514
|
-
if (task.status === "completed") {
|
|
515
|
-
unblocked = await unblockDependents(context.directory, args.teamName, task.id);
|
|
516
|
-
}
|
|
517
|
-
return JSON.stringify({ task, unblocked }, null, 2);
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
}),
|
|
521
|
-
task_get: tool2({
|
|
522
|
-
description: "Get a single task by ID.",
|
|
523
|
-
args: {
|
|
524
|
-
teamName: tool2.schema.string().describe("Team name"),
|
|
525
|
-
taskId: tool2.schema.string().describe("Task ID (integer string)")
|
|
526
|
-
},
|
|
527
|
-
async execute(args, context) {
|
|
528
|
-
const tPath = taskFilePath(context.directory, args.teamName, args.taskId);
|
|
529
|
-
if (!await exists(tPath)) {
|
|
530
|
-
return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
|
|
544
|
+
return JSON.stringify(await readJson(tPath), null, 2);
|
|
531
545
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
546
|
+
}),
|
|
547
|
+
task_list: tool2({
|
|
548
|
+
description: "List all tasks in a team with full details, sorted by integer ID.",
|
|
549
|
+
args: {
|
|
550
|
+
teamName: tool2.schema.string().describe("Team name")
|
|
551
|
+
},
|
|
552
|
+
async execute(args) {
|
|
553
|
+
const dir = teamDir(directory, args.teamName);
|
|
554
|
+
if (!await exists(join5(dir, "config.json"))) {
|
|
555
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
556
|
+
}
|
|
557
|
+
const tasks = await loadAllTasks(directory, args.teamName);
|
|
558
|
+
tasks.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
|
|
559
|
+
return tasks.length > 0 ? JSON.stringify(tasks, null, 2) : "No tasks found";
|
|
544
560
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
})
|
|
550
|
-
};
|
|
561
|
+
})
|
|
562
|
+
};
|
|
563
|
+
}
|
|
551
564
|
|
|
552
565
|
// src/tools/message-tools.ts
|
|
553
566
|
import { join as join6 } from "node:path";
|
|
554
567
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
568
|
+
function createMessageTools(directory) {
|
|
569
|
+
return {
|
|
570
|
+
message_send: tool3({
|
|
571
|
+
description: "Send a message to a recipient's inbox or broadcast to all agents. Types: message, broadcast, shutdown_request, shutdown_response, plan_approval_response. Returns {sent: true, recipients: [...]}.",
|
|
572
|
+
args: {
|
|
573
|
+
teamName: tool3.schema.string().describe("Team name"),
|
|
574
|
+
type: tool3.schema.enum([
|
|
575
|
+
"message",
|
|
576
|
+
"broadcast",
|
|
577
|
+
"shutdown_request",
|
|
578
|
+
"shutdown_response",
|
|
579
|
+
"plan_approval_response"
|
|
580
|
+
]).describe("Message type"),
|
|
581
|
+
recipient: tool3.schema.string().optional().describe("Recipient agent name (required for non-broadcast types)"),
|
|
582
|
+
content: tool3.schema.string().describe("Message content (markdown)"),
|
|
583
|
+
summary: tool3.schema.string().optional().describe("5-10 word preview"),
|
|
584
|
+
from: tool3.schema.string().optional().describe("Sender name (defaults to team-lead)")
|
|
585
|
+
},
|
|
586
|
+
async execute(args) {
|
|
587
|
+
const dir = teamDir(directory, args.teamName);
|
|
588
|
+
const configPath = join6(dir, "config.json");
|
|
589
|
+
if (!await exists(configPath)) {
|
|
590
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
591
|
+
}
|
|
592
|
+
const sender = args.from || "team-lead";
|
|
593
|
+
const inboxesDir = join6(dir, "inboxes");
|
|
594
|
+
await ensureDir(inboxesDir);
|
|
595
|
+
const inboxesLockPath = join6(inboxesDir, ".lock");
|
|
596
|
+
const message = {
|
|
597
|
+
from: sender,
|
|
598
|
+
text: args.content,
|
|
599
|
+
...args.summary && { summary: args.summary },
|
|
600
|
+
timestamp: new Date().toISOString(),
|
|
601
|
+
read: false
|
|
602
|
+
};
|
|
603
|
+
if (args.type === "broadcast") {
|
|
604
|
+
const config = await readJson(configPath);
|
|
605
|
+
const recipients = [];
|
|
606
|
+
return withLock(inboxesLockPath, async () => {
|
|
607
|
+
for (const member of config.members) {
|
|
608
|
+
if (member.name === sender)
|
|
609
|
+
continue;
|
|
610
|
+
const inboxPath = join6(inboxesDir, `${member.name}.json`);
|
|
611
|
+
await appendToInbox(inboxPath, message);
|
|
612
|
+
recipients.push(member.name);
|
|
613
|
+
}
|
|
614
|
+
return JSON.stringify({ sent: true, recipients }, null, 2);
|
|
615
|
+
});
|
|
616
|
+
} else {
|
|
617
|
+
if (!args.recipient) {
|
|
618
|
+
return "Error: recipient is required for non-broadcast messages";
|
|
599
619
|
}
|
|
600
|
-
return
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
620
|
+
return withLock(inboxesLockPath, async () => {
|
|
621
|
+
const inboxPath = join6(inboxesDir, `${args.recipient}.json`);
|
|
622
|
+
await appendToInbox(inboxPath, message);
|
|
623
|
+
return JSON.stringify({ sent: true, recipients: [args.recipient] }, null, 2);
|
|
624
|
+
});
|
|
605
625
|
}
|
|
606
|
-
return withLock(inboxesLockPath, async () => {
|
|
607
|
-
const inboxPath = join6(inboxesDir, `${args.recipient}.json`);
|
|
608
|
-
await appendToInbox(inboxPath, message);
|
|
609
|
-
return JSON.stringify({ sent: true, recipients: [args.recipient] }, null, 2);
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}),
|
|
614
|
-
message_fetch: tool3({
|
|
615
|
-
description: "Fetch messages from an agent's inbox. Optionally filter by timestamp and mark as read.",
|
|
616
|
-
args: {
|
|
617
|
-
teamName: tool3.schema.string().describe("Team name"),
|
|
618
|
-
agent: tool3.schema.string().describe("Agent name whose inbox to read"),
|
|
619
|
-
since: tool3.schema.string().optional().describe("ISO timestamp — return only messages after this time"),
|
|
620
|
-
markRead: tool3.schema.boolean().optional().describe("Mark fetched messages as read (default false)")
|
|
621
|
-
},
|
|
622
|
-
async execute(args, context) {
|
|
623
|
-
const dir = teamDir(context.directory, args.teamName);
|
|
624
|
-
if (!await exists(join6(dir, "config.json"))) {
|
|
625
|
-
return `Error: Team "${args.teamName}" not found`;
|
|
626
626
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
627
|
+
}),
|
|
628
|
+
message_fetch: tool3({
|
|
629
|
+
description: "Fetch messages from an agent's inbox. Optionally filter by timestamp and mark as read.",
|
|
630
|
+
args: {
|
|
631
|
+
teamName: tool3.schema.string().describe("Team name"),
|
|
632
|
+
agent: tool3.schema.string().describe("Agent name whose inbox to read"),
|
|
633
|
+
since: tool3.schema.string().optional().describe("ISO timestamp — return only messages after this time"),
|
|
634
|
+
markRead: tool3.schema.boolean().optional().describe("Mark fetched messages as read (default false)")
|
|
635
|
+
},
|
|
636
|
+
async execute(args) {
|
|
637
|
+
const dir = teamDir(directory, args.teamName);
|
|
638
|
+
if (!await exists(join6(dir, "config.json"))) {
|
|
639
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
640
|
+
}
|
|
641
|
+
const inboxPath = join6(dir, "inboxes", `${args.agent}.json`);
|
|
642
|
+
let messages = await readInbox(inboxPath);
|
|
643
|
+
if (args.since) {
|
|
644
|
+
const sinceDate = new Date(args.since);
|
|
645
|
+
messages = messages.filter((m) => new Date(m.timestamp) > sinceDate);
|
|
644
646
|
}
|
|
645
|
-
if (
|
|
646
|
-
await
|
|
647
|
+
if (args.markRead && messages.length > 0) {
|
|
648
|
+
const allMessages = await readInbox(inboxPath);
|
|
649
|
+
const sinceDate = args.since ? new Date(args.since) : null;
|
|
650
|
+
let changed = false;
|
|
651
|
+
for (const m of allMessages) {
|
|
652
|
+
if (sinceDate && new Date(m.timestamp) <= sinceDate)
|
|
653
|
+
continue;
|
|
654
|
+
if (!m.read) {
|
|
655
|
+
m.read = true;
|
|
656
|
+
changed = true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (changed) {
|
|
660
|
+
await writeJsonAtomic(inboxPath, allMessages);
|
|
661
|
+
}
|
|
647
662
|
}
|
|
663
|
+
messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
664
|
+
return messages.length > 0 ? JSON.stringify(messages, null, 2) : "No messages found";
|
|
648
665
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
})
|
|
653
|
-
};
|
|
666
|
+
})
|
|
667
|
+
};
|
|
668
|
+
}
|
|
654
669
|
|
|
655
670
|
// src/index.ts
|
|
656
671
|
var HDTeamPlugin = async ({ directory }) => {
|
|
@@ -670,9 +685,9 @@ Use team_status(teamName) to get full details. Continue any in-progress tasks.
|
|
|
670
685
|
}
|
|
671
686
|
},
|
|
672
687
|
tool: {
|
|
673
|
-
...
|
|
674
|
-
...
|
|
675
|
-
...
|
|
688
|
+
...createTeamTools(directory),
|
|
689
|
+
...createTaskTools(directory),
|
|
690
|
+
...createMessageTools(directory)
|
|
676
691
|
}
|
|
677
692
|
};
|
|
678
693
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hdwebsoft/hdcode-agent-team",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenCode plugin for multi-agent team coordination — per-agent inboxes, task management with dependency tracking, and file locking.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|