@hdwebsoft/hdcode-agent-team 0.1.0 → 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/dist/index.js +459 -435
- 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,500 @@ 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
|
+
}
|
|
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
|
+
}
|
|
262
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
|
-
|
|
339
|
-
|
|
316
|
+
function parseStringArray(raw, label) {
|
|
317
|
+
if (!raw.trimStart().startsWith("[")) {
|
|
318
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
319
|
+
}
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = JSON.parse(raw);
|
|
323
|
+
} catch {
|
|
324
|
+
return `Error: Invalid ${label} — use comma-separated IDs (e.g. "1,2") or JSON array (e.g. ["1","2"])`;
|
|
325
|
+
}
|
|
326
|
+
if (!Array.isArray(parsed) || !parsed.every((v) => typeof v === "string" || typeof v === "number")) {
|
|
327
|
+
return `Error: ${label} must be comma-separated IDs (e.g. "1,2") or a JSON array (e.g. ["1","2"])`;
|
|
328
|
+
}
|
|
329
|
+
return parsed.map((v) => String(v));
|
|
330
|
+
}
|
|
331
|
+
function createTaskTools(directory) {
|
|
332
|
+
return {
|
|
333
|
+
task_create: tool2({
|
|
334
|
+
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.",
|
|
335
|
+
args: {
|
|
336
|
+
teamName: tool2.schema.string().describe("Team name"),
|
|
337
|
+
subject: tool2.schema.string().describe("Task subject, <60 chars, imperative"),
|
|
338
|
+
description: tool2.schema.string().describe("Task description"),
|
|
339
|
+
activeForm: tool2.schema.string().optional().describe("Spinner text when in_progress"),
|
|
340
|
+
addBlocks: tool2.schema.string().optional().describe('JSON array of task IDs this task blocks, e.g. ["3","4"]'),
|
|
341
|
+
addBlockedBy: tool2.schema.string().optional().describe('JSON array of task IDs that block this task, e.g. ["1"]'),
|
|
342
|
+
metadata: tool2.schema.string().optional().describe("JSON object of arbitrary metadata")
|
|
343
|
+
},
|
|
344
|
+
async execute(args) {
|
|
345
|
+
const dir = teamDir(directory, args.teamName);
|
|
346
|
+
if (!await exists(join5(dir, "config.json"))) {
|
|
347
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
340
348
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
349
|
+
const tasksDir = join5(dir, "tasks");
|
|
350
|
+
await ensureDir(tasksDir);
|
|
351
|
+
const tasksLockPath = join5(tasksDir, ".lock");
|
|
352
|
+
return withLock(tasksLockPath, async () => {
|
|
353
|
+
let addBlocks = [];
|
|
354
|
+
let addBlockedBy = [];
|
|
355
|
+
let metadata;
|
|
356
|
+
if (args.addBlocks) {
|
|
357
|
+
const result = parseStringArray(args.addBlocks, "addBlocks");
|
|
358
|
+
if (typeof result === "string")
|
|
359
|
+
return result;
|
|
360
|
+
addBlocks = result;
|
|
346
361
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return "Error: Invalid metadata JSON";
|
|
362
|
+
if (args.addBlockedBy) {
|
|
363
|
+
const result = parseStringArray(args.addBlockedBy, "addBlockedBy");
|
|
364
|
+
if (typeof result === "string")
|
|
365
|
+
return result;
|
|
366
|
+
addBlockedBy = result;
|
|
353
367
|
}
|
|
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);
|
|
368
|
+
if (args.metadata) {
|
|
369
|
+
try {
|
|
370
|
+
metadata = JSON.parse(args.metadata);
|
|
371
|
+
} catch {
|
|
372
|
+
return "Error: Invalid metadata JSON";
|
|
373
|
+
}
|
|
385
374
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
target.blockedBy.push(id);
|
|
392
|
-
await writeJsonAtomic(targetPath, target);
|
|
375
|
+
const allTasks = await loadAllTasks(directory, args.teamName);
|
|
376
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
377
|
+
for (const bid of addBlockedBy) {
|
|
378
|
+
if (!taskMap.has(bid))
|
|
379
|
+
return `Error: Blocker task "${bid}" not found`;
|
|
393
380
|
}
|
|
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}`;
|
|
381
|
+
for (const bid of addBlocks) {
|
|
382
|
+
if (!taskMap.has(bid))
|
|
383
|
+
return `Error: Blocked task "${bid}" not found`;
|
|
435
384
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
385
|
+
const id = await nextTaskId(tasksDir);
|
|
386
|
+
if (addBlockedBy.length > 0 && hasCycle(id, addBlockedBy, taskMap)) {
|
|
387
|
+
return `Error: Circular dependency detected for task ${id}`;
|
|
388
|
+
}
|
|
389
|
+
const task = {
|
|
390
|
+
id,
|
|
391
|
+
subject: args.subject,
|
|
392
|
+
description: args.description,
|
|
393
|
+
...args.activeForm && { activeForm: args.activeForm },
|
|
394
|
+
status: "pending",
|
|
395
|
+
blocks: addBlocks,
|
|
396
|
+
blockedBy: addBlockedBy,
|
|
397
|
+
...metadata && { metadata }
|
|
398
|
+
};
|
|
399
|
+
await writeJsonAtomic(join5(tasksDir, `${id}.json`), task);
|
|
400
|
+
for (const targetId of addBlockedBy) {
|
|
401
|
+
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
402
|
+
const target = await readJson(targetPath);
|
|
403
|
+
if (!target.blocks.includes(id)) {
|
|
404
|
+
target.blocks.push(id);
|
|
405
|
+
await writeJsonAtomic(targetPath, target);
|
|
449
406
|
}
|
|
450
407
|
}
|
|
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";
|
|
408
|
+
for (const targetId of addBlocks) {
|
|
409
|
+
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
410
|
+
const target = await readJson(targetPath);
|
|
411
|
+
if (!target.blockedBy.includes(id)) {
|
|
412
|
+
target.blockedBy.push(id);
|
|
413
|
+
await writeJsonAtomic(targetPath, target);
|
|
414
|
+
}
|
|
467
415
|
}
|
|
468
|
-
|
|
416
|
+
return JSON.stringify(task, null, 2);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}),
|
|
420
|
+
task_update: tool2({
|
|
421
|
+
description: "Update task fields, manage dependencies, auto-unblock. Status transitions: pending→in_progress, in_progress→completed. On completion: auto-unblocks dependents. Returns {task, unblocked[]}.",
|
|
422
|
+
args: {
|
|
423
|
+
teamName: tool2.schema.string().describe("Team name"),
|
|
424
|
+
taskId: tool2.schema.string().describe("Task ID (integer string)"),
|
|
425
|
+
status: tool2.schema.enum(["pending", "in_progress", "completed"]).optional().describe("New status"),
|
|
426
|
+
subject: tool2.schema.string().optional().describe("New subject"),
|
|
427
|
+
description: tool2.schema.string().optional().describe("New description"),
|
|
428
|
+
activeForm: tool2.schema.string().optional().describe("Spinner text when in_progress"),
|
|
429
|
+
owner: tool2.schema.string().optional().describe("Agent name claiming this task"),
|
|
430
|
+
addBlocks: tool2.schema.string().optional().describe("JSON array of task IDs to add to blocks[]"),
|
|
431
|
+
addBlockedBy: tool2.schema.string().optional().describe("JSON array of task IDs to add to blockedBy[]"),
|
|
432
|
+
metadata: tool2.schema.string().optional().describe("JSON object to merge into metadata")
|
|
433
|
+
},
|
|
434
|
+
async execute(args) {
|
|
435
|
+
const dir = teamDir(directory, args.teamName);
|
|
436
|
+
if (!await exists(join5(dir, "config.json"))) {
|
|
437
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
469
438
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return
|
|
439
|
+
const tasksDir = join5(dir, "tasks");
|
|
440
|
+
const tasksLockPath = join5(tasksDir, ".lock");
|
|
441
|
+
return withLock(tasksLockPath, async () => {
|
|
442
|
+
const tPath = taskFilePath(directory, args.teamName, args.taskId);
|
|
443
|
+
if (!await exists(tPath)) {
|
|
444
|
+
return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
|
|
476
445
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
446
|
+
const task = await readJson(tPath);
|
|
447
|
+
if (args.status) {
|
|
448
|
+
const validTransitions = {
|
|
449
|
+
pending: ["in_progress"],
|
|
450
|
+
in_progress: ["completed"],
|
|
451
|
+
completed: []
|
|
452
|
+
};
|
|
453
|
+
const allowed = validTransitions[task.status];
|
|
454
|
+
if (!allowed || !allowed.includes(args.status)) {
|
|
455
|
+
return `Error: Invalid transition ${task.status} → ${args.status}`;
|
|
480
456
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
457
|
+
if (args.status === "in_progress" && task.blockedBy.length > 0) {
|
|
458
|
+
const activeBlockers = [];
|
|
459
|
+
for (const bid of task.blockedBy) {
|
|
460
|
+
const blockerPath = taskFilePath(directory, args.teamName, bid);
|
|
461
|
+
if (await exists(blockerPath)) {
|
|
462
|
+
const blocker = await readJson(blockerPath);
|
|
463
|
+
if (blocker.status !== "completed") {
|
|
464
|
+
activeBlockers.push(bid);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (activeBlockers.length > 0) {
|
|
469
|
+
return `Error: Cannot start task with unresolved blockers: ${activeBlockers.join(", ")}`;
|
|
487
470
|
}
|
|
488
471
|
}
|
|
472
|
+
task.status = args.status;
|
|
489
473
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
474
|
+
if (args.subject !== undefined)
|
|
475
|
+
task.subject = args.subject;
|
|
476
|
+
if (args.description !== undefined)
|
|
477
|
+
task.description = args.description;
|
|
478
|
+
if (args.activeForm !== undefined)
|
|
479
|
+
task.activeForm = args.activeForm;
|
|
480
|
+
if (args.owner !== undefined)
|
|
481
|
+
task.owner = args.owner;
|
|
482
|
+
if (args.metadata) {
|
|
483
|
+
let newMeta;
|
|
484
|
+
try {
|
|
485
|
+
newMeta = JSON.parse(args.metadata);
|
|
486
|
+
} catch {
|
|
487
|
+
return "Error: Invalid metadata JSON";
|
|
488
|
+
}
|
|
489
|
+
task.metadata = { ...task.metadata, ...newMeta };
|
|
497
490
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
491
|
+
if (args.addBlocks) {
|
|
492
|
+
const result = parseStringArray(args.addBlocks, "addBlocks");
|
|
493
|
+
if (typeof result === "string")
|
|
494
|
+
return result;
|
|
495
|
+
for (const targetId of result) {
|
|
496
|
+
if (!task.blocks.includes(targetId)) {
|
|
497
|
+
task.blocks.push(targetId);
|
|
498
|
+
}
|
|
499
|
+
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
500
|
+
if (await exists(targetPath)) {
|
|
501
|
+
const target = await readJson(targetPath);
|
|
502
|
+
if (!target.blockedBy.includes(task.id)) {
|
|
503
|
+
target.blockedBy.push(task.id);
|
|
504
|
+
await writeJsonAtomic(targetPath, target);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
501
507
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
+
}
|
|
509
|
+
if (args.addBlockedBy) {
|
|
510
|
+
const result = parseStringArray(args.addBlockedBy, "addBlockedBy");
|
|
511
|
+
if (typeof result === "string")
|
|
512
|
+
return result;
|
|
513
|
+
for (const targetId of result) {
|
|
514
|
+
if (!task.blockedBy.includes(targetId)) {
|
|
515
|
+
task.blockedBy.push(targetId);
|
|
516
|
+
}
|
|
517
|
+
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
518
|
+
if (await exists(targetPath)) {
|
|
519
|
+
const target = await readJson(targetPath);
|
|
520
|
+
if (!target.blocks.includes(task.id)) {
|
|
521
|
+
target.blocks.push(task.id);
|
|
522
|
+
await writeJsonAtomic(targetPath, target);
|
|
523
|
+
}
|
|
508
524
|
}
|
|
509
525
|
}
|
|
510
526
|
}
|
|
527
|
+
await writeJsonAtomic(tPath, task);
|
|
528
|
+
let unblocked = [];
|
|
529
|
+
if (task.status === "completed") {
|
|
530
|
+
unblocked = await unblockDependents(directory, args.teamName, task.id);
|
|
531
|
+
}
|
|
532
|
+
return JSON.stringify({ task, unblocked }, null, 2);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}),
|
|
536
|
+
task_get: tool2({
|
|
537
|
+
description: "Get a single task by ID.",
|
|
538
|
+
args: {
|
|
539
|
+
teamName: tool2.schema.string().describe("Team name"),
|
|
540
|
+
taskId: tool2.schema.string().describe("Task ID (integer string)")
|
|
541
|
+
},
|
|
542
|
+
async execute(args) {
|
|
543
|
+
const tPath = taskFilePath(directory, args.teamName, args.taskId);
|
|
544
|
+
if (!await exists(tPath)) {
|
|
545
|
+
return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
|
|
511
546
|
}
|
|
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}"`;
|
|
547
|
+
return JSON.stringify(await readJson(tPath), null, 2);
|
|
531
548
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
549
|
+
}),
|
|
550
|
+
task_list: tool2({
|
|
551
|
+
description: "List all tasks in a team with full details, sorted by integer ID.",
|
|
552
|
+
args: {
|
|
553
|
+
teamName: tool2.schema.string().describe("Team name")
|
|
554
|
+
},
|
|
555
|
+
async execute(args) {
|
|
556
|
+
const dir = teamDir(directory, args.teamName);
|
|
557
|
+
if (!await exists(join5(dir, "config.json"))) {
|
|
558
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
559
|
+
}
|
|
560
|
+
const tasks = await loadAllTasks(directory, args.teamName);
|
|
561
|
+
tasks.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
|
|
562
|
+
return tasks.length > 0 ? JSON.stringify(tasks, null, 2) : "No tasks found";
|
|
544
563
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
})
|
|
550
|
-
};
|
|
564
|
+
})
|
|
565
|
+
};
|
|
566
|
+
}
|
|
551
567
|
|
|
552
568
|
// src/tools/message-tools.ts
|
|
553
569
|
import { join as join6 } from "node:path";
|
|
554
570
|
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
|
-
|
|
571
|
+
function createMessageTools(directory) {
|
|
572
|
+
return {
|
|
573
|
+
message_send: tool3({
|
|
574
|
+
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: [...]}.",
|
|
575
|
+
args: {
|
|
576
|
+
teamName: tool3.schema.string().describe("Team name"),
|
|
577
|
+
type: tool3.schema.enum([
|
|
578
|
+
"message",
|
|
579
|
+
"broadcast",
|
|
580
|
+
"shutdown_request",
|
|
581
|
+
"shutdown_response",
|
|
582
|
+
"plan_approval_response"
|
|
583
|
+
]).describe("Message type"),
|
|
584
|
+
recipient: tool3.schema.string().optional().describe("Recipient agent name (required for non-broadcast types)"),
|
|
585
|
+
content: tool3.schema.string().describe("Message content (markdown)"),
|
|
586
|
+
summary: tool3.schema.string().optional().describe("5-10 word preview"),
|
|
587
|
+
from: tool3.schema.string().optional().describe("Sender name (defaults to team-lead)")
|
|
588
|
+
},
|
|
589
|
+
async execute(args) {
|
|
590
|
+
const dir = teamDir(directory, args.teamName);
|
|
591
|
+
const configPath = join6(dir, "config.json");
|
|
592
|
+
if (!await exists(configPath)) {
|
|
593
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
594
|
+
}
|
|
595
|
+
const sender = args.from || "team-lead";
|
|
596
|
+
const inboxesDir = join6(dir, "inboxes");
|
|
597
|
+
await ensureDir(inboxesDir);
|
|
598
|
+
const inboxesLockPath = join6(inboxesDir, ".lock");
|
|
599
|
+
const message = {
|
|
600
|
+
from: sender,
|
|
601
|
+
text: args.content,
|
|
602
|
+
...args.summary && { summary: args.summary },
|
|
603
|
+
timestamp: new Date().toISOString(),
|
|
604
|
+
read: false
|
|
605
|
+
};
|
|
590
606
|
const config = await readJson(configPath);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
607
|
+
if (args.type === "broadcast") {
|
|
608
|
+
const recipients = [];
|
|
609
|
+
return withLock(inboxesLockPath, async () => {
|
|
610
|
+
for (const member of config.members) {
|
|
611
|
+
if (member.name === sender)
|
|
612
|
+
continue;
|
|
613
|
+
const inboxPath = join6(inboxesDir, `${member.name}.json`);
|
|
614
|
+
await appendToInbox(inboxPath, message);
|
|
615
|
+
recipients.push(member.name);
|
|
616
|
+
}
|
|
617
|
+
return JSON.stringify({ sent: true, recipients }, null, 2);
|
|
618
|
+
});
|
|
619
|
+
} else {
|
|
620
|
+
if (!args.recipient) {
|
|
621
|
+
return "Error: recipient is required for non-broadcast messages";
|
|
622
|
+
}
|
|
623
|
+
const recipientName = args.recipient.includes("@") ? args.recipient.split("@")[0] : args.recipient;
|
|
624
|
+
const member = config.members.find((m) => m.name === recipientName || m.agentId === args.recipient);
|
|
625
|
+
if (!member) {
|
|
626
|
+
return `Error: Recipient "${args.recipient}" not found in team "${args.teamName}"`;
|
|
627
|
+
}
|
|
628
|
+
return withLock(inboxesLockPath, async () => {
|
|
596
629
|
const inboxPath = join6(inboxesDir, `${member.name}.json`);
|
|
597
630
|
await appendToInbox(inboxPath, message);
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
return JSON.stringify({ sent: true, recipients }, null, 2);
|
|
601
|
-
});
|
|
602
|
-
} else {
|
|
603
|
-
if (!args.recipient) {
|
|
604
|
-
return "Error: recipient is required for non-broadcast messages";
|
|
631
|
+
return JSON.stringify({ sent: true, recipients: [member.name] }, null, 2);
|
|
632
|
+
});
|
|
605
633
|
}
|
|
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
634
|
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
if (args.markRead && messages.length > 0) {
|
|
634
|
-
const allMessages = await readInbox(inboxPath);
|
|
635
|
-
const sinceDate = args.since ? new Date(args.since) : null;
|
|
636
|
-
let changed = false;
|
|
637
|
-
for (const m of allMessages) {
|
|
638
|
-
if (sinceDate && new Date(m.timestamp) <= sinceDate)
|
|
639
|
-
continue;
|
|
640
|
-
if (!m.read) {
|
|
641
|
-
m.read = true;
|
|
642
|
-
changed = true;
|
|
643
|
-
}
|
|
635
|
+
}),
|
|
636
|
+
message_fetch: tool3({
|
|
637
|
+
description: "Fetch messages from an agent's inbox. Optionally filter by timestamp and mark as read.",
|
|
638
|
+
args: {
|
|
639
|
+
teamName: tool3.schema.string().describe("Team name"),
|
|
640
|
+
agent: tool3.schema.string().describe("Agent name whose inbox to read"),
|
|
641
|
+
since: tool3.schema.string().optional().describe("ISO timestamp — return only messages after this time"),
|
|
642
|
+
markRead: tool3.schema.boolean().optional().describe("Mark fetched messages as read (default false)")
|
|
643
|
+
},
|
|
644
|
+
async execute(args) {
|
|
645
|
+
const dir = teamDir(directory, args.teamName);
|
|
646
|
+
if (!await exists(join6(dir, "config.json"))) {
|
|
647
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
648
|
+
}
|
|
649
|
+
const agentName = args.agent.includes("@") ? args.agent.split("@")[0] : args.agent;
|
|
650
|
+
const inboxPath = join6(dir, "inboxes", `${agentName}.json`);
|
|
651
|
+
let messages = await readInbox(inboxPath);
|
|
652
|
+
if (args.since) {
|
|
653
|
+
const sinceDate = new Date(args.since);
|
|
654
|
+
messages = messages.filter((m) => new Date(m.timestamp) > sinceDate);
|
|
644
655
|
}
|
|
645
|
-
if (
|
|
646
|
-
await
|
|
656
|
+
if (args.markRead && messages.length > 0) {
|
|
657
|
+
const allMessages = await readInbox(inboxPath);
|
|
658
|
+
const sinceDate = args.since ? new Date(args.since) : null;
|
|
659
|
+
let changed = false;
|
|
660
|
+
for (const m of allMessages) {
|
|
661
|
+
if (sinceDate && new Date(m.timestamp) <= sinceDate)
|
|
662
|
+
continue;
|
|
663
|
+
if (!m.read) {
|
|
664
|
+
m.read = true;
|
|
665
|
+
changed = true;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (changed) {
|
|
669
|
+
await writeJsonAtomic(inboxPath, allMessages);
|
|
670
|
+
}
|
|
647
671
|
}
|
|
672
|
+
messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
673
|
+
return messages.length > 0 ? JSON.stringify(messages, null, 2) : "No messages found";
|
|
648
674
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
})
|
|
653
|
-
};
|
|
675
|
+
})
|
|
676
|
+
};
|
|
677
|
+
}
|
|
654
678
|
|
|
655
679
|
// src/index.ts
|
|
656
680
|
var HDTeamPlugin = async ({ directory }) => {
|
|
@@ -670,9 +694,9 @@ Use team_status(teamName) to get full details. Continue any in-progress tasks.
|
|
|
670
694
|
}
|
|
671
695
|
},
|
|
672
696
|
tool: {
|
|
673
|
-
...
|
|
674
|
-
...
|
|
675
|
-
...
|
|
697
|
+
...createTeamTools(directory),
|
|
698
|
+
...createTaskTools(directory),
|
|
699
|
+
...createMessageTools(directory)
|
|
676
700
|
}
|
|
677
701
|
};
|
|
678
702
|
};
|
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.2",
|
|
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",
|