@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.
Files changed (2) hide show
  1. package/dist/index.js +452 -437
  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,491 @@ 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
+ }
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
- 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
+ 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
- if (args.addBlockedBy) {
342
- try {
343
- addBlockedBy = JSON.parse(args.addBlockedBy);
344
- } catch {
345
- return "Error: Invalid addBlockedBy JSON";
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
- if (args.metadata) {
349
- try {
350
- metadata = JSON.parse(args.metadata);
351
- } catch {
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
- 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);
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
- 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);
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
- 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}`;
378
+ for (const bid of addBlocks) {
379
+ if (!taskMap.has(bid))
380
+ return `Error: Blocked task "${bid}" not found`;
435
381
  }
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(", ")}`;
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
- 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";
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
- task.metadata = { ...task.metadata, ...newMeta };
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
- if (args.addBlocks) {
471
- let newBlocks;
472
- try {
473
- newBlocks = JSON.parse(args.addBlocks);
474
- } catch {
475
- return "Error: Invalid addBlocks JSON";
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
- for (const targetId of newBlocks) {
478
- if (!task.blocks.includes(targetId)) {
479
- task.blocks.push(targetId);
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
- 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);
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
- if (args.addBlockedBy) {
492
- let newBlockedBy;
493
- try {
494
- newBlockedBy = JSON.parse(args.addBlockedBy);
495
- } catch {
496
- return "Error: Invalid addBlockedBy JSON";
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
- for (const targetId of newBlockedBy) {
499
- if (!task.blockedBy.includes(targetId)) {
500
- task.blockedBy.push(targetId);
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
- 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);
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 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}"`;
544
+ return JSON.stringify(await readJson(tPath), null, 2);
531
545
  }
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`;
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
- 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
- };
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
- 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") {
590
- 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;
596
- const inboxPath = join6(inboxesDir, `${member.name}.json`);
597
- await appendToInbox(inboxPath, message);
598
- recipients.push(member.name);
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 JSON.stringify({ sent: true, recipients }, null, 2);
601
- });
602
- } else {
603
- if (!args.recipient) {
604
- return "Error: recipient is required for non-broadcast messages";
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
- 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
- }
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 (changed) {
646
- await writeJsonAtomic(inboxPath, allMessages);
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
- 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
- };
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
- ...teamTools,
674
- ...taskTools,
675
- ...messageTools
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.0",
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",