@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.
- package/dist/index.js +104 -10
- 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
|
|
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,
|
|
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"
|
|
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, `${
|
|
714
|
+
const inboxPath = join6(inboxesDir, `${member.name}.json`);
|
|
622
715
|
await appendToInbox(inboxPath, message);
|
|
623
|
-
return JSON.stringify({ sent: true, recipients: [
|
|
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
|
|
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.
|
|
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",
|