@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.
Files changed (2) hide show
  1. package/dist/index.js +175 -71
  2. 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
- const dir = teamDir(directory, args.teamName);
237
- if (await exists(join4(dir, "config.json"))) {
238
- return `Error: Team "${args.teamName}" already exists`;
239
- }
240
- await ensureDir(join4(dir, "tasks"));
241
- await ensureDir(join4(dir, "inboxes"));
242
- await ensureDir(join4(dir, "reports"));
243
- const now = Date.now();
244
- const leadId = `team-lead@${args.teamName}`;
245
- const config = {
246
- name: args.teamName,
247
- description: args.description,
248
- createdAt: now,
249
- leadAgentId: leadId,
250
- members: [
251
- {
252
- agentId: leadId,
253
- name: "team-lead",
254
- agentType: "team-lead",
255
- model: "unknown",
256
- planModeRequired: false,
257
- joinedAt: now,
258
- cwd: directory,
259
- isActive: true
260
- }
261
- ]
262
- };
263
- await writeJsonAtomic(join4(dir, "config.json"), config);
264
- await writeJsonAtomic(join4(dir, "inboxes", "team-lead.json"), []);
265
- return JSON.stringify(config, null, 2);
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
- const dir = teamDir(directory, args.teamName);
275
- if (!await exists(join4(dir, "config.json"))) {
276
- return `Error: Team "${args.teamName}" not found`;
277
- }
278
- await rm2(dir, { recursive: true, force: true });
279
- return `Team "${args.teamName}" deleted successfully`;
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 = args.owner || task.owner || "unknown";
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
- const agentId = args.owner || task.owner;
768
- if (agentId) {
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
- if (await exists(targetPath)) {
806
- const target = await readJson(targetPath);
807
- if (!target.blockedBy.includes(task.id)) {
808
- target.blockedBy.push(task.id);
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
- if (await exists(targetPath)) {
829
- const target = await readJson(targetPath);
830
- if (!target.blocks.includes(task.id)) {
831
- target.blocks.push(task.id);
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
- if (!await exists(join7(dir, "config.json"))) {
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 inboxPath = join7(dir, "inboxes", `${agentName}.json`);
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.0",
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",