@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.
Files changed (2) hide show
  1. package/dist/index.js +459 -435
  2. 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
- return Bun.file(path).exists();
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
- var teamTools = {
180
- team_create: tool({
181
- description: "Create a new agent team with directory structure and CC-aligned config. Lead is automatically added as first member. Returns the team config.",
182
- args: {
183
- teamName: tool.schema.string().regex(/^[a-z0-9-]+$/).describe("Team name in kebab-case"),
184
- description: tool.schema.string().describe("Team description")
185
- },
186
- async execute(args, context) {
187
- const dir = teamDir(context.directory, args.teamName);
188
- if (await exists(join4(dir, "config.json"))) {
189
- return `Error: Team "${args.teamName}" already exists`;
190
- }
191
- await ensureDir(join4(dir, "tasks"));
192
- await ensureDir(join4(dir, "inboxes"));
193
- await ensureDir(join4(dir, "reports"));
194
- const now = Date.now();
195
- const leadId = `team-lead@${args.teamName}`;
196
- const config = {
197
- name: args.teamName,
198
- description: args.description,
199
- createdAt: now,
200
- leadAgentId: leadId,
201
- members: [
202
- {
203
- agentId: leadId,
204
- name: "team-lead",
205
- agentType: "team-lead",
206
- model: "unknown",
207
- planModeRequired: false,
208
- joinedAt: now,
209
- cwd: context.directory,
210
- isActive: true
211
- }
212
- ]
213
- };
214
- await writeJsonAtomic(join4(dir, "config.json"), config);
215
- await writeJsonAtomic(join4(dir, "inboxes", "team-lead.json"), []);
216
- return JSON.stringify(config, null, 2);
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
- const config = await readJson(join4(dir, "config.json"));
244
- const tasks = await loadAllTasks(context.directory, args.teamName);
245
- const summary = { pending: 0, in_progress: 0, completed: 0 };
246
- for (const t of tasks) {
247
- if (t.status in summary) {
248
- summary[t.status]++;
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
- const inboxesDir = join4(dir, "inboxes");
252
- const messageCounts = {};
253
- if (await exists(inboxesDir)) {
254
- const inboxFiles = await listJsonFiles(inboxesDir);
255
- for (const f of inboxFiles) {
256
- const agentName = f.split("/").pop().replace(".json", "");
257
- const messages = await readInbox(f);
258
- messageCounts[agentName] = {
259
- total: messages.length,
260
- unread: messages.filter((m) => !m.read).length
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
- const reportDir = join4(dir, "reports");
265
- const reports = await exists(reportDir) ? (await readdir3(reportDir)).filter((f) => !f.startsWith(".")) : [];
266
- const total = Object.values(summary).reduce((a, b) => a + b, 0);
267
- const isComplete = total > 0 && summary.pending === 0 && summary.in_progress === 0;
268
- return JSON.stringify({ config, taskSummary: summary, messageCounts, reports, isComplete }, null, 2);
269
- }
270
- }),
271
- team_list: tool({
272
- description: "List all teams with basic info.",
273
- args: {},
274
- async execute(_args, context) {
275
- const root = join4(context.directory, TEAM_ROOT);
276
- if (!await exists(root))
277
- return "No teams found";
278
- const entries = await readdir3(root, { withFileTypes: true });
279
- const teams = [];
280
- for (const entry of entries) {
281
- if (!entry.isDirectory())
282
- continue;
283
- const configPath = join4(root, entry.name, "config.json");
284
- if (!await exists(configPath))
285
- continue;
286
- const config = await readJson(configPath);
287
- const tasks = await loadAllTasks(context.directory, entry.name);
288
- const pending = tasks.filter((t) => t.status === "pending").length;
289
- const inProgress = tasks.filter((t) => t.status === "in_progress").length;
290
- const completed = tasks.filter((t) => t.status === "completed").length;
291
- const isComplete = tasks.length > 0 && pending === 0 && inProgress === 0;
292
- teams.push({
293
- name: config.name,
294
- description: config.description,
295
- createdAt: config.createdAt,
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
- return teams.length > 0 ? JSON.stringify(teams, null, 2) : "No teams found";
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
- var taskTools = {
310
- task_create: tool2({
311
- 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.",
312
- args: {
313
- teamName: tool2.schema.string().describe("Team name"),
314
- subject: tool2.schema.string().describe("Task subject, <60 chars, imperative"),
315
- description: tool2.schema.string().describe("Task description"),
316
- activeForm: tool2.schema.string().optional().describe("Spinner text when in_progress"),
317
- addBlocks: tool2.schema.string().optional().describe('JSON array of task IDs this task blocks, e.g. ["3","4"]'),
318
- addBlockedBy: tool2.schema.string().optional().describe('JSON array of task IDs that block this task, e.g. ["1"]'),
319
- metadata: tool2.schema.string().optional().describe("JSON object of arbitrary metadata")
320
- },
321
- async execute(args, context) {
322
- const dir = teamDir(context.directory, args.teamName);
323
- if (!await exists(join5(dir, "config.json"))) {
324
- return `Error: Team "${args.teamName}" not found`;
325
- }
326
- const tasksDir = join5(dir, "tasks");
327
- await ensureDir(tasksDir);
328
- const tasksLockPath = join5(tasksDir, ".lock");
329
- return withLock(tasksLockPath, async () => {
330
- const id = await nextTaskId(tasksDir);
331
- let addBlocks = [];
332
- let addBlockedBy = [];
333
- let metadata;
334
- if (args.addBlocks) {
335
- try {
336
- addBlocks = JSON.parse(args.addBlocks);
337
- } catch {
338
- return "Error: Invalid addBlocks JSON";
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
- if (args.addBlockedBy) {
342
- try {
343
- addBlockedBy = JSON.parse(args.addBlockedBy);
344
- } catch {
345
- return "Error: Invalid addBlockedBy JSON";
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
- if (args.metadata) {
349
- try {
350
- metadata = JSON.parse(args.metadata);
351
- } catch {
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
- const allTasks = await loadAllTasks(context.directory, args.teamName);
356
- const taskMap = new Map(allTasks.map((t) => [t.id, t]));
357
- for (const bid of addBlockedBy) {
358
- if (!taskMap.has(bid))
359
- return `Error: Blocker task "${bid}" not found`;
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
- for (const targetId of addBlocks) {
388
- const targetPath = taskFilePath(context.directory, args.teamName, targetId);
389
- const target = await readJson(targetPath);
390
- if (!target.blockedBy.includes(id)) {
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
- return JSON.stringify(task, null, 2);
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
- if (args.status === "in_progress" && task.blockedBy.length > 0) {
437
- const activeBlockers = [];
438
- for (const bid of task.blockedBy) {
439
- const blockerPath = taskFilePath(context.directory, args.teamName, bid);
440
- if (await exists(blockerPath)) {
441
- const blocker = await readJson(blockerPath);
442
- if (blocker.status !== "completed") {
443
- activeBlockers.push(bid);
444
- }
445
- }
446
- }
447
- if (activeBlockers.length > 0) {
448
- return `Error: Cannot start task with unresolved blockers: ${activeBlockers.join(", ")}`;
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
- task.status = args.status;
452
- }
453
- if (args.subject !== undefined)
454
- task.subject = args.subject;
455
- if (args.description !== undefined)
456
- task.description = args.description;
457
- if (args.activeForm !== undefined)
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
- task.metadata = { ...task.metadata, ...newMeta };
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
- if (args.addBlocks) {
471
- let newBlocks;
472
- try {
473
- newBlocks = JSON.parse(args.addBlocks);
474
- } catch {
475
- return "Error: Invalid addBlocks JSON";
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
- for (const targetId of newBlocks) {
478
- if (!task.blocks.includes(targetId)) {
479
- task.blocks.push(targetId);
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
- const targetPath = taskFilePath(context.directory, args.teamName, targetId);
482
- if (await exists(targetPath)) {
483
- const target = await readJson(targetPath);
484
- if (!target.blockedBy.includes(task.id)) {
485
- target.blockedBy.push(task.id);
486
- await writeJsonAtomic(targetPath, target);
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
- if (args.addBlockedBy) {
492
- let newBlockedBy;
493
- try {
494
- newBlockedBy = JSON.parse(args.addBlockedBy);
495
- } catch {
496
- return "Error: Invalid addBlockedBy JSON";
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
- for (const targetId of newBlockedBy) {
499
- if (!task.blockedBy.includes(targetId)) {
500
- task.blockedBy.push(targetId);
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
- const targetPath = taskFilePath(context.directory, args.teamName, targetId);
503
- if (await exists(targetPath)) {
504
- const target = await readJson(targetPath);
505
- if (!target.blocks.includes(task.id)) {
506
- target.blocks.push(task.id);
507
- await writeJsonAtomic(targetPath, target);
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 writeJsonAtomic(tPath, task);
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
- return JSON.stringify(await readJson(tPath), null, 2);
533
- }
534
- }),
535
- task_list: tool2({
536
- description: "List all tasks in a team with full details, sorted by integer ID.",
537
- args: {
538
- teamName: tool2.schema.string().describe("Team name")
539
- },
540
- async execute(args, context) {
541
- const dir = teamDir(context.directory, args.teamName);
542
- if (!await exists(join5(dir, "config.json"))) {
543
- return `Error: Team "${args.teamName}" not found`;
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
- const tasks = await loadAllTasks(context.directory, args.teamName);
546
- tasks.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
547
- return tasks.length > 0 ? JSON.stringify(tasks, null, 2) : "No tasks found";
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
- var messageTools = {
556
- message_send: tool3({
557
- 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: [...]}.",
558
- args: {
559
- teamName: tool3.schema.string().describe("Team name"),
560
- type: tool3.schema.enum([
561
- "message",
562
- "broadcast",
563
- "shutdown_request",
564
- "shutdown_response",
565
- "plan_approval_response"
566
- ]).describe("Message type"),
567
- recipient: tool3.schema.string().optional().describe("Recipient agent name (required for non-broadcast types)"),
568
- content: tool3.schema.string().describe("Message content (markdown)"),
569
- summary: tool3.schema.string().optional().describe("5-10 word preview"),
570
- from: tool3.schema.string().optional().describe("Sender name (defaults to team-lead)")
571
- },
572
- async execute(args, context) {
573
- const dir = teamDir(context.directory, args.teamName);
574
- const configPath = join6(dir, "config.json");
575
- if (!await exists(configPath)) {
576
- return `Error: Team "${args.teamName}" not found`;
577
- }
578
- const sender = args.from || "team-lead";
579
- const inboxesDir = join6(dir, "inboxes");
580
- await ensureDir(inboxesDir);
581
- const inboxesLockPath = join6(inboxesDir, ".lock");
582
- const message = {
583
- from: sender,
584
- text: args.content,
585
- ...args.summary && { summary: args.summary },
586
- timestamp: new Date().toISOString(),
587
- read: false
588
- };
589
- if (args.type === "broadcast") {
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
- const recipients = [];
592
- return withLock(inboxesLockPath, async () => {
593
- for (const member of config.members) {
594
- if (member.name === sender)
595
- continue;
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
- recipients.push(member.name);
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
- 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
- }
627
- const inboxPath = join6(dir, "inboxes", `${args.agent}.json`);
628
- let messages = await readInbox(inboxPath);
629
- if (args.since) {
630
- const sinceDate = new Date(args.since);
631
- messages = messages.filter((m) => new Date(m.timestamp) > sinceDate);
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 (changed) {
646
- await writeJsonAtomic(inboxPath, allMessages);
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
- messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
650
- return messages.length > 0 ? JSON.stringify(messages, null, 2) : "No messages found";
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
- ...teamTools,
674
- ...taskTools,
675
- ...messageTools
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.0",
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",