@hdwebsoft/hdcode-agent-team 0.1.1 → 0.1.3

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 +104 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -117,6 +117,37 @@ function hasCycle(taskId, blockedBy, allTasks) {
117
117
  }
118
118
  return false;
119
119
  }
120
+ function hasCycleViaBlocks(taskId, blockTargets, blockedBySources, allTasks) {
121
+ const upstream = new Set;
122
+ const queue = [...blockedBySources];
123
+ while (queue.length > 0) {
124
+ const current = queue.shift();
125
+ if (upstream.has(current))
126
+ continue;
127
+ upstream.add(current);
128
+ const task = allTasks.get(current);
129
+ if (task)
130
+ queue.push(...task.blockedBy);
131
+ }
132
+ for (const targetId of blockTargets) {
133
+ if (upstream.has(targetId))
134
+ return true;
135
+ const targetQueue = [targetId];
136
+ const visited = new Set;
137
+ while (targetQueue.length > 0) {
138
+ const cur = targetQueue.shift();
139
+ if (visited.has(cur))
140
+ continue;
141
+ visited.add(cur);
142
+ if (upstream.has(cur))
143
+ return true;
144
+ const t = allTasks.get(cur);
145
+ if (t)
146
+ targetQueue.push(...t.blockedBy);
147
+ }
148
+ }
149
+ return false;
150
+ }
120
151
  async function unblockDependents(base, teamName, completedTaskId) {
121
152
  const unblocked = [];
122
153
  const allTasks = await loadAllTasks(base, teamName);
@@ -312,18 +343,22 @@ function createTeamTools(directory) {
312
343
 
313
344
  // src/tools/task-tools.ts
314
345
  import { join as join5 } from "node:path";
346
+ import { unlink as unlink2 } from "node:fs/promises";
315
347
  import { tool as tool2 } from "@opencode-ai/plugin";
316
348
  function parseStringArray(raw, label) {
349
+ if (!raw.trimStart().startsWith("[")) {
350
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
351
+ }
317
352
  let parsed;
318
353
  try {
319
354
  parsed = JSON.parse(raw);
320
355
  } catch {
321
- return `Error: Invalid ${label} JSON`;
356
+ return `Error: Invalid ${label} — use comma-separated IDs (e.g. "1,2") or JSON array (e.g. ["1","2"])`;
322
357
  }
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"]`;
358
+ if (!Array.isArray(parsed) || !parsed.every((v) => typeof v === "string" || typeof v === "number")) {
359
+ return `Error: ${label} must be comma-separated IDs (e.g. "1,2") or a JSON array (e.g. ["1","2"])`;
325
360
  }
326
- return parsed;
361
+ return parsed.map((v) => String(v));
327
362
  }
328
363
  function createTaskTools(directory) {
329
364
  return {
@@ -383,6 +418,9 @@ function createTaskTools(directory) {
383
418
  if (addBlockedBy.length > 0 && hasCycle(id, addBlockedBy, taskMap)) {
384
419
  return `Error: Circular dependency detected for task ${id}`;
385
420
  }
421
+ if (addBlocks.length > 0 && hasCycleViaBlocks(id, addBlocks, addBlockedBy, taskMap)) {
422
+ return `Error: Circular dependency detected for task ${id}`;
423
+ }
386
424
  const task = {
387
425
  id,
388
426
  subject: args.subject,
@@ -415,7 +453,7 @@ function createTaskTools(directory) {
415
453
  }
416
454
  }),
417
455
  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[]}.",
456
+ description: "Update task fields, manage dependencies, auto-unblock. Status transitions: pending→in_progress, in_progress→completed. On completion: auto-unblocks dependents. Returns {task, unblocked[]}.",
419
457
  args: {
420
458
  teamName: tool2.schema.string().describe("Team name"),
421
459
  taskId: tool2.schema.string().describe("Task ID (integer string)"),
@@ -443,7 +481,7 @@ function createTaskTools(directory) {
443
481
  const task = await readJson(tPath);
444
482
  if (args.status) {
445
483
  const validTransitions = {
446
- pending: ["in_progress", "completed"],
484
+ pending: ["in_progress"],
447
485
  in_progress: ["completed"],
448
486
  completed: []
449
487
  };
@@ -489,6 +527,11 @@ function createTaskTools(directory) {
489
527
  const result = parseStringArray(args.addBlocks, "addBlocks");
490
528
  if (typeof result === "string")
491
529
  return result;
530
+ const allTasks = await loadAllTasks(directory, args.teamName);
531
+ const taskMap = new Map(allTasks.map((t) => [t.id, t]));
532
+ if (hasCycleViaBlocks(task.id, result, task.blockedBy, taskMap)) {
533
+ return `Error: Circular dependency detected — cannot add blocks`;
534
+ }
492
535
  for (const targetId of result) {
493
536
  if (!task.blocks.includes(targetId)) {
494
537
  task.blocks.push(targetId);
@@ -507,6 +550,11 @@ function createTaskTools(directory) {
507
550
  const result = parseStringArray(args.addBlockedBy, "addBlockedBy");
508
551
  if (typeof result === "string")
509
552
  return result;
553
+ const allTasks = await loadAllTasks(directory, args.teamName);
554
+ const taskMap = new Map(allTasks.map((t) => [t.id, t]));
555
+ if (hasCycle(task.id, result, taskMap)) {
556
+ return `Error: Circular dependency detected — cannot add blockedBy`;
557
+ }
510
558
  for (const targetId of result) {
511
559
  if (!task.blockedBy.includes(targetId)) {
512
560
  task.blockedBy.push(targetId);
@@ -530,6 +578,46 @@ function createTaskTools(directory) {
530
578
  });
531
579
  }
532
580
  }),
581
+ task_delete: tool2({
582
+ description: "Delete a task and clean up all bidirectional dependency links. Removes this task from blocks[] and blockedBy[] of all related tasks.",
583
+ args: {
584
+ teamName: tool2.schema.string().describe("Team name"),
585
+ taskId: tool2.schema.string().describe("Task ID to delete")
586
+ },
587
+ async execute(args) {
588
+ const dir = teamDir(directory, args.teamName);
589
+ if (!await exists(join5(dir, "config.json"))) {
590
+ return `Error: Team "${args.teamName}" not found`;
591
+ }
592
+ const tasksDir = join5(dir, "tasks");
593
+ const tasksLockPath = join5(tasksDir, ".lock");
594
+ return withLock(tasksLockPath, async () => {
595
+ const tPath = taskFilePath(directory, args.teamName, args.taskId);
596
+ if (!await exists(tPath)) {
597
+ return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
598
+ }
599
+ const task = await readJson(tPath);
600
+ for (const blockerId of task.blockedBy) {
601
+ const blockerPath = taskFilePath(directory, args.teamName, blockerId);
602
+ if (await exists(blockerPath)) {
603
+ const blocker = await readJson(blockerPath);
604
+ blocker.blocks = blocker.blocks.filter((id) => id !== args.taskId);
605
+ await writeJsonAtomic(blockerPath, blocker);
606
+ }
607
+ }
608
+ for (const blockedId of task.blocks) {
609
+ const blockedPath = taskFilePath(directory, args.teamName, blockedId);
610
+ if (await exists(blockedPath)) {
611
+ const blocked = await readJson(blockedPath);
612
+ blocked.blockedBy = blocked.blockedBy.filter((id) => id !== args.taskId);
613
+ await writeJsonAtomic(blockedPath, blocked);
614
+ }
615
+ }
616
+ await unlink2(tPath);
617
+ return `Task "${args.taskId}" deleted and all dependency links cleaned up`;
618
+ });
619
+ }
620
+ }),
533
621
  task_get: tool2({
534
622
  description: "Get a single task by ID.",
535
623
  args: {
@@ -600,8 +688,8 @@ function createMessageTools(directory) {
600
688
  timestamp: new Date().toISOString(),
601
689
  read: false
602
690
  };
691
+ const config = await readJson(configPath);
603
692
  if (args.type === "broadcast") {
604
- const config = await readJson(configPath);
605
693
  const recipients = [];
606
694
  return withLock(inboxesLockPath, async () => {
607
695
  for (const member of config.members) {
@@ -617,10 +705,15 @@ function createMessageTools(directory) {
617
705
  if (!args.recipient) {
618
706
  return "Error: recipient is required for non-broadcast messages";
619
707
  }
708
+ const recipientName = args.recipient.includes("@") ? args.recipient.split("@")[0] : args.recipient;
709
+ const member = config.members.find((m) => m.name === recipientName || m.agentId === args.recipient);
710
+ if (!member) {
711
+ return `Error: Recipient "${args.recipient}" not found in team "${args.teamName}"`;
712
+ }
620
713
  return withLock(inboxesLockPath, async () => {
621
- const inboxPath = join6(inboxesDir, `${args.recipient}.json`);
714
+ const inboxPath = join6(inboxesDir, `${member.name}.json`);
622
715
  await appendToInbox(inboxPath, message);
623
- return JSON.stringify({ sent: true, recipients: [args.recipient] }, null, 2);
716
+ return JSON.stringify({ sent: true, recipients: [member.name] }, null, 2);
624
717
  });
625
718
  }
626
719
  }
@@ -638,7 +731,8 @@ function createMessageTools(directory) {
638
731
  if (!await exists(join6(dir, "config.json"))) {
639
732
  return `Error: Team "${args.teamName}" not found`;
640
733
  }
641
- const inboxPath = join6(dir, "inboxes", `${args.agent}.json`);
734
+ const agentName = args.agent.includes("@") ? args.agent.split("@")[0] : args.agent;
735
+ const inboxPath = join6(dir, "inboxes", `${agentName}.json`);
642
736
  let messages = await readInbox(inboxPath);
643
737
  if (args.since) {
644
738
  const sinceDate = new Date(args.since);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hdwebsoft/hdcode-agent-team",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",