@hdwebsoft/hdcode-agent-team 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +175 -71
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { relative, isAbsolute } from "node:path";
|
|
3
|
+
|
|
1
4
|
// src/teams.ts
|
|
2
5
|
import { readdir as readdir2 } from "node:fs/promises";
|
|
3
6
|
import { join as join3 } from "node:path";
|
|
@@ -233,36 +236,38 @@ function createTeamTools(directory) {
|
|
|
233
236
|
description: tool.schema.string().describe("Team description")
|
|
234
237
|
},
|
|
235
238
|
async execute(args) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
239
|
+
return withTeamLock(directory, args.teamName, async () => {
|
|
240
|
+
const dir = teamDir(directory, args.teamName);
|
|
241
|
+
if (await exists(join4(dir, "config.json"))) {
|
|
242
|
+
return `Error: Team "${args.teamName}" already exists`;
|
|
243
|
+
}
|
|
244
|
+
await ensureDir(join4(dir, "tasks"));
|
|
245
|
+
await ensureDir(join4(dir, "inboxes"));
|
|
246
|
+
await ensureDir(join4(dir, "reports"));
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const leadId = `team-lead@${args.teamName}`;
|
|
249
|
+
const config = {
|
|
250
|
+
name: args.teamName,
|
|
251
|
+
description: args.description,
|
|
252
|
+
createdAt: now,
|
|
253
|
+
leadAgentId: leadId,
|
|
254
|
+
members: [
|
|
255
|
+
{
|
|
256
|
+
agentId: leadId,
|
|
257
|
+
name: "team-lead",
|
|
258
|
+
agentType: "team-lead",
|
|
259
|
+
model: "unknown",
|
|
260
|
+
planModeRequired: false,
|
|
261
|
+
joinedAt: now,
|
|
262
|
+
cwd: directory,
|
|
263
|
+
isActive: true
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
await writeJsonAtomic(join4(dir, "config.json"), config);
|
|
268
|
+
await writeJsonAtomic(join4(dir, "inboxes", "team-lead.json"), []);
|
|
269
|
+
return JSON.stringify(config, null, 2);
|
|
270
|
+
});
|
|
266
271
|
}
|
|
267
272
|
}),
|
|
268
273
|
team_delete: tool({
|
|
@@ -271,12 +276,14 @@ function createTeamTools(directory) {
|
|
|
271
276
|
teamName: tool.schema.string().describe("Team name to delete")
|
|
272
277
|
},
|
|
273
278
|
async execute(args) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
return withTeamLock(directory, args.teamName, async () => {
|
|
280
|
+
const dir = teamDir(directory, args.teamName);
|
|
281
|
+
if (!await exists(join4(dir, "config.json"))) {
|
|
282
|
+
return `Error: Team "${args.teamName}" not found`;
|
|
283
|
+
}
|
|
284
|
+
await rm2(dir, { recursive: true, force: true });
|
|
285
|
+
return `Team "${args.teamName}" deleted successfully`;
|
|
286
|
+
});
|
|
280
287
|
}
|
|
281
288
|
}),
|
|
282
289
|
team_status: tool({
|
|
@@ -360,6 +367,7 @@ import { tool as tool2 } from "@opencode-ai/plugin";
|
|
|
360
367
|
|
|
361
368
|
// src/reservations.ts
|
|
362
369
|
import { join as join5 } from "node:path";
|
|
370
|
+
import { readdir as readdir4 } from "node:fs/promises";
|
|
363
371
|
import { randomUUID } from "node:crypto";
|
|
364
372
|
var DEFAULT_TTL_SECONDS = 1800;
|
|
365
373
|
var MAX_TTL_SECONDS = 14400;
|
|
@@ -590,6 +598,70 @@ async function checkFile(base, teamName, path, agentId) {
|
|
|
590
598
|
const canEdit = !reserved || !!agentId && holders.every((h) => h.agentId === agentId);
|
|
591
599
|
return { reserved, canEdit, holders };
|
|
592
600
|
}
|
|
601
|
+
async function checkFileAcrossTeams(base, filePath, sessionId) {
|
|
602
|
+
const root = join5(base, TEAM_ROOT);
|
|
603
|
+
if (!await exists(root))
|
|
604
|
+
return null;
|
|
605
|
+
let normalized;
|
|
606
|
+
try {
|
|
607
|
+
normalized = normalizeRepoPath(filePath);
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
let entries;
|
|
612
|
+
try {
|
|
613
|
+
entries = await readdir4(root, { withFileTypes: true });
|
|
614
|
+
} catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
for (const entry of entries) {
|
|
618
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
619
|
+
continue;
|
|
620
|
+
const storePath = reservationStorePath(base, entry.name);
|
|
621
|
+
if (!await exists(storePath))
|
|
622
|
+
continue;
|
|
623
|
+
let store;
|
|
624
|
+
try {
|
|
625
|
+
store = await readJson(storePath);
|
|
626
|
+
} catch {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
pruneExpired(store);
|
|
630
|
+
for (const r of store.reservations) {
|
|
631
|
+
if (r.files.includes(normalized)) {
|
|
632
|
+
return {
|
|
633
|
+
teamName: entry.name,
|
|
634
|
+
holder: {
|
|
635
|
+
reservationId: r.id,
|
|
636
|
+
agentId: r.agentId,
|
|
637
|
+
taskId: r.taskId,
|
|
638
|
+
reason: r.reason,
|
|
639
|
+
expiresAt: r.expiresAt,
|
|
640
|
+
matchedBy: "file",
|
|
641
|
+
value: normalized
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
for (const p of r.prefixes) {
|
|
646
|
+
if (isPathInsidePrefix(normalized, p)) {
|
|
647
|
+
return {
|
|
648
|
+
teamName: entry.name,
|
|
649
|
+
holder: {
|
|
650
|
+
reservationId: r.id,
|
|
651
|
+
agentId: r.agentId,
|
|
652
|
+
taskId: r.taskId,
|
|
653
|
+
reason: r.reason,
|
|
654
|
+
expiresAt: r.expiresAt,
|
|
655
|
+
matchedBy: "prefix",
|
|
656
|
+
value: p
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
593
665
|
|
|
594
666
|
// src/tools/task-tools.ts
|
|
595
667
|
function parseStringArray(raw, label) {
|
|
@@ -723,6 +795,24 @@ function createTaskTools(directory) {
|
|
|
723
795
|
return `Error: Task "${args.taskId}" not found in team "${args.teamName}"`;
|
|
724
796
|
}
|
|
725
797
|
const task = await readJson(tPath);
|
|
798
|
+
const originalOwner = task.owner;
|
|
799
|
+
if (args.subject !== undefined)
|
|
800
|
+
task.subject = args.subject;
|
|
801
|
+
if (args.description !== undefined)
|
|
802
|
+
task.description = args.description;
|
|
803
|
+
if (args.activeForm !== undefined)
|
|
804
|
+
task.activeForm = args.activeForm;
|
|
805
|
+
if (args.owner !== undefined)
|
|
806
|
+
task.owner = args.owner;
|
|
807
|
+
if (args.metadata) {
|
|
808
|
+
let newMeta;
|
|
809
|
+
try {
|
|
810
|
+
newMeta = JSON.parse(args.metadata);
|
|
811
|
+
} catch {
|
|
812
|
+
return "Error: Invalid metadata JSON";
|
|
813
|
+
}
|
|
814
|
+
task.metadata = { ...task.metadata, ...newMeta };
|
|
815
|
+
}
|
|
726
816
|
if (args.status) {
|
|
727
817
|
const validTransitions = {
|
|
728
818
|
pending: ["in_progress"],
|
|
@@ -751,7 +841,7 @@ function createTaskTools(directory) {
|
|
|
751
841
|
if (args.status === "in_progress") {
|
|
752
842
|
const fileScope = task.metadata?.fileScope;
|
|
753
843
|
if (fileScope) {
|
|
754
|
-
const agentId =
|
|
844
|
+
const agentId = task.owner || "unknown";
|
|
755
845
|
const reserveResult = await reserveFiles(directory, args.teamName, {
|
|
756
846
|
agentId,
|
|
757
847
|
taskId: task.id,
|
|
@@ -764,36 +854,22 @@ function createTaskTools(directory) {
|
|
|
764
854
|
}
|
|
765
855
|
}
|
|
766
856
|
if (args.status === "completed") {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
await releaseByTask(directory, args.teamName, agentId, task.id);
|
|
857
|
+
if (originalOwner) {
|
|
858
|
+
await releaseByTask(directory, args.teamName, originalOwner, task.id);
|
|
770
859
|
}
|
|
771
860
|
}
|
|
772
861
|
task.status = args.status;
|
|
773
862
|
}
|
|
774
|
-
if (args.subject !== undefined)
|
|
775
|
-
task.subject = args.subject;
|
|
776
|
-
if (args.description !== undefined)
|
|
777
|
-
task.description = args.description;
|
|
778
|
-
if (args.activeForm !== undefined)
|
|
779
|
-
task.activeForm = args.activeForm;
|
|
780
|
-
if (args.owner !== undefined)
|
|
781
|
-
task.owner = args.owner;
|
|
782
|
-
if (args.metadata) {
|
|
783
|
-
let newMeta;
|
|
784
|
-
try {
|
|
785
|
-
newMeta = JSON.parse(args.metadata);
|
|
786
|
-
} catch {
|
|
787
|
-
return "Error: Invalid metadata JSON";
|
|
788
|
-
}
|
|
789
|
-
task.metadata = { ...task.metadata, ...newMeta };
|
|
790
|
-
}
|
|
791
863
|
if (args.addBlocks) {
|
|
792
864
|
const result = parseStringArray(args.addBlocks, "addBlocks");
|
|
793
865
|
if (typeof result === "string")
|
|
794
866
|
return result;
|
|
795
867
|
const allTasks = await loadAllTasks(directory, args.teamName);
|
|
796
868
|
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
869
|
+
for (const targetId of result) {
|
|
870
|
+
if (!taskMap.has(targetId))
|
|
871
|
+
return `Error: Task "${targetId}" not found — cannot add to blocks`;
|
|
872
|
+
}
|
|
797
873
|
if (hasCycleViaBlocks(task.id, result, task.blockedBy, taskMap)) {
|
|
798
874
|
return `Error: Circular dependency detected — cannot add blocks`;
|
|
799
875
|
}
|
|
@@ -802,12 +878,10 @@ function createTaskTools(directory) {
|
|
|
802
878
|
task.blocks.push(targetId);
|
|
803
879
|
}
|
|
804
880
|
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
await writeJsonAtomic(targetPath, target);
|
|
810
|
-
}
|
|
881
|
+
const target = await readJson(targetPath);
|
|
882
|
+
if (!target.blockedBy.includes(task.id)) {
|
|
883
|
+
target.blockedBy.push(task.id);
|
|
884
|
+
await writeJsonAtomic(targetPath, target);
|
|
811
885
|
}
|
|
812
886
|
}
|
|
813
887
|
}
|
|
@@ -817,6 +891,10 @@ function createTaskTools(directory) {
|
|
|
817
891
|
return result;
|
|
818
892
|
const allTasks = await loadAllTasks(directory, args.teamName);
|
|
819
893
|
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
894
|
+
for (const targetId of result) {
|
|
895
|
+
if (!taskMap.has(targetId))
|
|
896
|
+
return `Error: Task "${targetId}" not found — cannot add to blockedBy`;
|
|
897
|
+
}
|
|
820
898
|
if (hasCycle(task.id, result, taskMap)) {
|
|
821
899
|
return `Error: Circular dependency detected — cannot add blockedBy`;
|
|
822
900
|
}
|
|
@@ -825,12 +903,10 @@ function createTaskTools(directory) {
|
|
|
825
903
|
task.blockedBy.push(targetId);
|
|
826
904
|
}
|
|
827
905
|
const targetPath = taskFilePath(directory, args.teamName, targetId);
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
await writeJsonAtomic(targetPath, target);
|
|
833
|
-
}
|
|
906
|
+
const target = await readJson(targetPath);
|
|
907
|
+
if (!target.blocks.includes(task.id)) {
|
|
908
|
+
target.blocks.push(task.id);
|
|
909
|
+
await writeJsonAtomic(targetPath, target);
|
|
834
910
|
}
|
|
835
911
|
}
|
|
836
912
|
}
|
|
@@ -947,6 +1023,7 @@ function createMessageTools(directory) {
|
|
|
947
1023
|
const inboxesDir = join7(dir, "inboxes");
|
|
948
1024
|
await ensureDir(inboxesDir);
|
|
949
1025
|
const message = {
|
|
1026
|
+
type: args.type,
|
|
950
1027
|
from: sender,
|
|
951
1028
|
text: args.content,
|
|
952
1029
|
...args.summary && { summary: args.summary },
|
|
@@ -993,11 +1070,17 @@ function createMessageTools(directory) {
|
|
|
993
1070
|
},
|
|
994
1071
|
async execute(args) {
|
|
995
1072
|
const dir = teamDir(directory, args.teamName);
|
|
996
|
-
|
|
1073
|
+
const configPath = join7(dir, "config.json");
|
|
1074
|
+
if (!await exists(configPath)) {
|
|
997
1075
|
return `Error: Team "${args.teamName}" not found`;
|
|
998
1076
|
}
|
|
999
1077
|
const agentName = args.agent.includes("@") ? args.agent.split("@")[0] : args.agent;
|
|
1000
|
-
const
|
|
1078
|
+
const config = await readJson(configPath);
|
|
1079
|
+
const member = config.members.find((m) => m.name === agentName || m.agentId === args.agent);
|
|
1080
|
+
if (!member) {
|
|
1081
|
+
return `Error: Agent "${args.agent}" not found in team "${args.teamName}"`;
|
|
1082
|
+
}
|
|
1083
|
+
const inboxPath = join7(dir, "inboxes", `${member.name}.json`);
|
|
1001
1084
|
if (args.markRead) {
|
|
1002
1085
|
return withTeamLock(directory, args.teamName, async () => {
|
|
1003
1086
|
const allMessages = await readInbox(inboxPath);
|
|
@@ -1143,6 +1226,7 @@ function createReservationTools(directory) {
|
|
|
1143
1226
|
}
|
|
1144
1227
|
|
|
1145
1228
|
// src/index.ts
|
|
1229
|
+
var WRITE_TOOLS = new Set(["edit", "write", "create"]);
|
|
1146
1230
|
var HDTeamPlugin = async ({ directory }) => {
|
|
1147
1231
|
return {
|
|
1148
1232
|
"experimental.session.compacting": async (_input, output) => {
|
|
@@ -1159,6 +1243,26 @@ Use team_status(teamName) to get full details. Continue any in-progress tasks.
|
|
|
1159
1243
|
`);
|
|
1160
1244
|
}
|
|
1161
1245
|
},
|
|
1246
|
+
"tool.execute.before": async (input, output) => {
|
|
1247
|
+
if (!WRITE_TOOLS.has(input.tool))
|
|
1248
|
+
return;
|
|
1249
|
+
const filePath = output.args.filePath;
|
|
1250
|
+
if (!filePath)
|
|
1251
|
+
return;
|
|
1252
|
+
let repoPath = filePath;
|
|
1253
|
+
if (isAbsolute(filePath)) {
|
|
1254
|
+
repoPath = relative(directory, filePath);
|
|
1255
|
+
if (repoPath.startsWith(".."))
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const match = await checkFileAcrossTeams(directory, repoPath);
|
|
1259
|
+
if (match) {
|
|
1260
|
+
const h = match.holder;
|
|
1261
|
+
const who = h.taskId ? `${h.agentId} (task ${h.taskId})` : h.agentId;
|
|
1262
|
+
const reason = h.reason ? ` — ${h.reason}` : "";
|
|
1263
|
+
throw new Error(`\uD83D\uDD12 File "${repoPath}" is reserved by ${who} in team "${match.teamName}"${reason}. ` + `Use file_check to see details or wait for the reservation to expire (${h.expiresAt}).`);
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1162
1266
|
tool: {
|
|
1163
1267
|
...createTeamTools(directory),
|
|
1164
1268
|
...createTaskTools(directory),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hdwebsoft/hdcode-agent-team",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "OpenCode plugin for multi-agent team coordination — per-agent inboxes, task management with dependency tracking, file reservation/locking, and atomic mkdir-based concurrency control.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|