opencode-multiagent 0.3.0-next.1 → 0.4.0

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 (89) hide show
  1. package/AGENTS.md +21 -0
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +4 -4
  4. package/README.tr.md +4 -4
  5. package/agents/AGENTS.md +91 -0
  6. package/agents/auditor.md +59 -17
  7. package/agents/{worker.md → coder.md} +12 -10
  8. package/agents/{scribe.md → docmaster.md} +16 -8
  9. package/agents/executor.md +45 -62
  10. package/agents/planner.md +59 -47
  11. package/agents/reviewer.md +22 -9
  12. package/agents/scout.md +16 -12
  13. package/agents/sec-coder.md +83 -0
  14. package/agents/ui-coder.md +77 -0
  15. package/commands/board.md +17 -0
  16. package/commands/execute.md +8 -7
  17. package/commands/init-deep.md +6 -6
  18. package/commands/init.md +4 -5
  19. package/commands/inspect.md +5 -5
  20. package/commands/plan.md +7 -6
  21. package/commands/quality.md +3 -3
  22. package/commands/review.md +4 -3
  23. package/commands/status.md +4 -3
  24. package/defaults/AGENTS.md +48 -0
  25. package/defaults/opencode-multiagent.json +24 -67
  26. package/defaults/opencode-multiagent.schema.json +16 -0
  27. package/dist/index.js +464 -131
  28. package/dist/opencode-multiagent/compiler.d.ts +8 -2
  29. package/dist/opencode-multiagent/compiler.d.ts.map +1 -1
  30. package/dist/opencode-multiagent/constants.d.ts +12 -0
  31. package/dist/opencode-multiagent/constants.d.ts.map +1 -1
  32. package/dist/opencode-multiagent/correlation.d.ts +21 -0
  33. package/dist/opencode-multiagent/correlation.d.ts.map +1 -0
  34. package/dist/opencode-multiagent/hooks.d.ts.map +1 -1
  35. package/dist/opencode-multiagent/log.d.ts.map +1 -1
  36. package/dist/opencode-multiagent/quality.d.ts +4 -0
  37. package/dist/opencode-multiagent/quality.d.ts.map +1 -1
  38. package/dist/opencode-multiagent/supervision.d.ts +14 -0
  39. package/dist/opencode-multiagent/supervision.d.ts.map +1 -1
  40. package/dist/opencode-multiagent/task-manager.d.ts +8 -2
  41. package/dist/opencode-multiagent/task-manager.d.ts.map +1 -1
  42. package/dist/opencode-multiagent/telemetry.d.ts +2 -0
  43. package/dist/opencode-multiagent/telemetry.d.ts.map +1 -1
  44. package/dist/opencode-multiagent/tools.d.ts +32 -1
  45. package/dist/opencode-multiagent/tools.d.ts.map +1 -1
  46. package/docs/agents.md +67 -179
  47. package/docs/agents.tr.md +68 -179
  48. package/docs/configuration.md +14 -25
  49. package/docs/configuration.tr.md +14 -25
  50. package/docs/usage-guide.md +31 -33
  51. package/docs/usage-guide.tr.md +31 -33
  52. package/examples/opencode.with-overrides.json +2 -2
  53. package/package.json +1 -1
  54. package/skills/AGENTS.md +51 -0
  55. package/skills/advanced-evaluation/manifest.json +1 -1
  56. package/skills/cek-context-engineering/manifest.json +1 -1
  57. package/skills/cek-prompt-engineering/manifest.json +1 -1
  58. package/skills/cek-test-prompt/manifest.json +1 -1
  59. package/skills/cek-thought-based-reasoning/manifest.json +1 -1
  60. package/skills/context-degradation/manifest.json +1 -1
  61. package/skills/debate/manifest.json +1 -1
  62. package/skills/design-first/manifest.json +1 -1
  63. package/skills/dispatching-parallel-agents/manifest.json +1 -1
  64. package/skills/drift-analysis/manifest.json +1 -1
  65. package/skills/evaluation/manifest.json +1 -1
  66. package/skills/parallel-investigation/manifest.json +1 -1
  67. package/skills/reflexion-critique/manifest.json +1 -1
  68. package/skills/reflexion-reflect/manifest.json +1 -1
  69. package/skills/root-cause-analysis/manifest.json +1 -1
  70. package/skills/sadd-judge-with-debate/manifest.json +1 -1
  71. package/skills/structured-code-review/manifest.json +1 -1
  72. package/skills/task-decomposition/manifest.json +1 -1
  73. package/skills/verification-before-completion/manifest.json +1 -1
  74. package/skills/verification-gates/manifest.json +1 -1
  75. package/agents/advisor.md +0 -60
  76. package/agents/critic.md +0 -136
  77. package/agents/deep-worker.md +0 -69
  78. package/agents/devil.md +0 -38
  79. package/agents/heavy-worker.md +0 -72
  80. package/agents/lead.md +0 -147
  81. package/agents/librarian.md +0 -66
  82. package/agents/qa.md +0 -53
  83. package/agents/quick.md +0 -70
  84. package/agents/strategist.md +0 -66
  85. package/agents/ui-heavy-worker.md +0 -66
  86. package/agents/ui-worker.md +0 -74
  87. package/agents/validator.md +0 -50
  88. package/dist/opencode-multiagent/file-lock.d.ts +0 -15
  89. package/dist/opencode-multiagent/file-lock.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // @bun
2
2
  // src/opencode-multiagent/hooks.ts
3
+ import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
3
4
  import { join as join4 } from "path";
4
5
 
5
6
  // src/opencode-multiagent/constants.ts
@@ -49,15 +50,11 @@ var trackedEventTypes = new Set([
49
50
  "session.deleted",
50
51
  "session.idle",
51
52
  "message.updated",
52
- "message.part.updated",
53
- "message.part.delta",
54
53
  "command.executed"
55
54
  ]);
56
55
  var supervisionEventTypes = new Set([
57
56
  "session.created",
58
57
  "message.updated",
59
- "message.part.updated",
60
- "message.part.delta",
61
58
  "session.idle",
62
59
  "session.deleted"
63
60
  ]);
@@ -83,6 +80,9 @@ var defaultProfiles = {
83
80
  telemetry: false,
84
81
  supervision: false,
85
82
  quality_gate: false,
83
+ task_lifecycle: false,
84
+ quality_gate_enforcement: false,
85
+ concurrency_limit: 0,
86
86
  experimental: {
87
87
  chat_system_transform: false,
88
88
  chat_messages_transform: false,
@@ -100,6 +100,9 @@ var defaultProfiles = {
100
100
  telemetry: true,
101
101
  supervision: true,
102
102
  quality_gate: true,
103
+ task_lifecycle: true,
104
+ quality_gate_enforcement: false,
105
+ concurrency_limit: 5,
103
106
  skill_sources: [
104
107
  bundledSkillsDir,
105
108
  join(homedir(), ".agents", "skills"),
@@ -123,6 +126,9 @@ var defaultProfiles = {
123
126
  telemetry: true,
124
127
  supervision: true,
125
128
  quality_gate: true,
129
+ task_lifecycle: true,
130
+ quality_gate_enforcement: true,
131
+ concurrency_limit: 5,
126
132
  experimental: {
127
133
  chat_system_transform: true,
128
134
  chat_messages_transform: true,
@@ -142,6 +148,9 @@ var defaultFlags = {
142
148
  telemetry: true,
143
149
  supervision: true,
144
150
  quality_gate: true,
151
+ task_lifecycle: true,
152
+ quality_gate_enforcement: false,
153
+ concurrency_limit: 5,
145
154
  skill_sources: [
146
155
  bundledSkillsDir,
147
156
  join(homedir(), ".agents", "skills"),
@@ -212,9 +221,18 @@ var experimentalText = "[opencode-multiagent experimental] Experimental control-
212
221
 
213
222
  // src/opencode-multiagent/log.ts
214
223
  import { appendFile, mkdir } from "fs/promises";
224
+ var _initPromise = null;
225
+ var ensureLogDir = () => {
226
+ if (_initPromise === null) {
227
+ _initPromise = mkdir(logDirPath, { recursive: true }).then(() => {
228
+ return;
229
+ });
230
+ }
231
+ return _initPromise;
232
+ };
215
233
  var note = async (kind, payload) => {
216
234
  try {
217
- await mkdir(logDirPath, { recursive: true });
235
+ await ensureLogDir();
218
236
  await appendFile(logFilePath, `${JSON.stringify({ ts: Date.now(), kind, ...payload })}
219
237
  `);
220
238
  } catch {}
@@ -369,6 +387,22 @@ var normalizeDefinitionData = (data) => {
369
387
  return result;
370
388
  };
371
389
  var isMcpPermissionKey = (key) => mcpToolPrefixes.some((prefix) => key === prefix || key.startsWith(prefix));
390
+ var buildTaskRoutingAllowSet = (permission = {}) => {
391
+ const allowed = new Set;
392
+ const taskPerm = permission.task;
393
+ if (taskPerm === "allow" || taskPerm === true)
394
+ return new Set(["*"]);
395
+ if (taskPerm === "deny" || taskPerm === false || !taskPerm || typeof taskPerm !== "object")
396
+ return allowed;
397
+ const taskObj = taskPerm;
398
+ for (const [key, value] of Object.entries(taskObj)) {
399
+ if (key === "*")
400
+ continue;
401
+ if (value === "allow" || value === true)
402
+ allowed.add(key);
403
+ }
404
+ return allowed;
405
+ };
372
406
  var buildMcpPermissionRegistry = (permission = {}) => {
373
407
  const allowed = [];
374
408
  const denied = [];
@@ -387,6 +421,7 @@ async function compileAgents(cfg, dirs, agentSettings = {}) {
387
421
  if (!cfg.agent || typeof cfg.agent !== "object")
388
422
  cfg.agent = {};
389
423
  const mcpRegistry = new Map;
424
+ const taskRouting = new Map;
390
425
  for (const [name, raw] of defs.entries()) {
391
426
  const data = normalizeDefinitionData(raw.data);
392
427
  if (!own(cfg.agent, name)) {
@@ -412,10 +447,20 @@ async function compileAgents(cfg, dirs, agentSettings = {}) {
412
447
  if (!own(target, "prompt") && typeof raw.body === "string") {
413
448
  target.prompt = raw.body.trim();
414
449
  }
415
- mcpRegistry.set(name, buildMcpPermissionRegistry(target.permission ?? {}));
450
+ const perm = target.permission ?? {};
451
+ mcpRegistry.set(name, buildMcpPermissionRegistry(perm));
452
+ taskRouting.set(name, buildTaskRoutingAllowSet(perm));
416
453
  }
417
- return mcpRegistry;
454
+ return { mcpRegistry, taskRouting };
418
455
  }
456
+ var isTaskRoutingAllowed = (agent, target, routing) => {
457
+ const allowed = routing.get(agent);
458
+ if (!allowed)
459
+ return true;
460
+ if (allowed.has("*"))
461
+ return true;
462
+ return allowed.has(target);
463
+ };
419
464
  async function compileCommands(cfg, dirs) {
420
465
  const defs = await loadMarkdownDefs(dirs);
421
466
  if (!cfg.command || typeof cfg.command !== "object")
@@ -446,8 +491,8 @@ var applyBuiltInAgentPolicy = async (cfg) => {
446
491
  }
447
492
  const current = typeof cfg.default_agent === "string" ? cfg.default_agent : undefined;
448
493
  const currentAgent = current ? cfg.agent?.[current] : undefined;
449
- const preferred = cfg.agent.lead && cfg.agent.lead.disable !== true ? "lead" : cfg.agent.critic && cfg.agent.critic.disable !== true ? "critic" : undefined;
450
- const invalidCurrent = !current || disabledNativeAgents.includes(current) || !currentAgent || currentAgent.disable === true || currentAgent.hidden === true || currentAgent.mode === "subagent" || currentAgent.mode === "sub" || preferred === "lead" && current !== "lead";
494
+ const preferred = cfg.agent.planner && cfg.agent.planner.disable !== true ? "planner" : cfg.agent.executor && cfg.agent.executor.disable !== true ? "executor" : undefined;
495
+ const invalidCurrent = !current || disabledNativeAgents.includes(current) || !currentAgent || currentAgent.disable === true || currentAgent.hidden === true || currentAgent.mode === "subagent" || currentAgent.mode === "sub" || preferred === "planner" && current !== "planner";
451
496
  if (invalidCurrent && preferred) {
452
497
  const previous = current;
453
498
  cfg.default_agent = preferred;
@@ -493,67 +538,72 @@ var matchesMcpPermission = (tool, registry) => {
493
538
  return registry.fallback === "allow";
494
539
  };
495
540
 
496
- // src/opencode-multiagent/file-lock.ts
497
- var cleanupIntervalMs = 5 * 60 * 1000;
498
- var staleLockTtlMs = 30 * 60 * 1000;
499
- var normalizeFilePath = (filePath) => typeof filePath === "string" ? filePath.replace(/\\/g, "/") : undefined;
500
- var createFileLockController = () => {
501
- const locks = new Map;
502
- const sessionFiles = new Map;
503
- const trackSessionFile = (sessionID, filePath) => {
504
- if (!sessionFiles.has(sessionID))
505
- sessionFiles.set(sessionID, new Set);
506
- sessionFiles.get(sessionID)?.add(filePath);
507
- };
508
- const releaseAll = (sessionID) => {
509
- const files = sessionFiles.get(sessionID);
510
- if (!files)
511
- return 0;
512
- let count = 0;
513
- for (const filePath of files) {
514
- const existing = locks.get(filePath);
515
- if (existing?.sessionID === sessionID) {
516
- locks.delete(filePath);
517
- count += 1;
518
- }
541
+ // src/opencode-multiagent/correlation.ts
542
+ var INTENT_TTL_MS = 60000;
543
+ var createCorrelationController = () => {
544
+ const intentQueues = new Map;
545
+ const links = new Map;
546
+ const taskIndex = new Map;
547
+ const recordIntent = (parentSessionID, taskID, targetAgent) => {
548
+ if (!intentQueues.has(parentSessionID)) {
549
+ intentQueues.set(parentSessionID, []);
519
550
  }
520
- sessionFiles.delete(sessionID);
521
- return count;
551
+ intentQueues.get(parentSessionID).push({
552
+ taskID,
553
+ parentSessionID,
554
+ targetAgent,
555
+ createdAt: Date.now()
556
+ });
522
557
  };
523
- const cleanupStaleLocks = (now = Date.now()) => {
524
- for (const [filePath, info] of locks.entries()) {
525
- if (now - info.lastTouchedAt <= staleLockTtlMs)
526
- continue;
527
- locks.delete(filePath);
528
- const files = sessionFiles.get(info.sessionID);
529
- if (files) {
530
- files.delete(filePath);
531
- if (files.size === 0)
532
- sessionFiles.delete(info.sessionID);
533
- }
558
+ const tryCorrelate = (parentSessionID, childSessionID, agentHint) => {
559
+ const queue = intentQueues.get(parentSessionID);
560
+ if (!queue || queue.length === 0)
561
+ return;
562
+ const now = Date.now();
563
+ const fresh = queue.filter((i) => now - i.createdAt < INTENT_TTL_MS);
564
+ if (fresh.length === 0) {
565
+ intentQueues.delete(parentSessionID);
566
+ return;
534
567
  }
535
- };
536
- const interval = setInterval(() => cleanupStaleLocks(), cleanupIntervalMs);
537
- interval.unref?.();
538
- return {
539
- acquire(sessionID, filePath) {
540
- const normalized = normalizeFilePath(filePath);
541
- if (!sessionID || !normalized)
542
- return { ok: true, filePath: normalized };
543
- const existing = locks.get(normalized);
544
- if (existing && existing.sessionID !== sessionID) {
545
- return { ok: false, filePath: normalized, ownerSessionID: existing.sessionID };
546
- }
547
- locks.set(normalized, { sessionID, lastTouchedAt: Date.now() });
548
- trackSessionFile(sessionID, normalized);
549
- return { ok: true, filePath: normalized };
550
- },
551
- releaseAll,
552
- cleanup() {
553
- cleanupStaleLocks();
554
- clearInterval(interval);
568
+ intentQueues.set(parentSessionID, fresh);
569
+ let matchIdx = -1;
570
+ if (agentHint) {
571
+ matchIdx = fresh.findIndex((i) => i.targetAgent === agentHint);
572
+ }
573
+ if (matchIdx < 0) {
574
+ matchIdx = 0;
555
575
  }
576
+ const intent = fresh.splice(matchIdx, 1)[0];
577
+ if (fresh.length === 0)
578
+ intentQueues.delete(parentSessionID);
579
+ const link = {
580
+ taskID: intent.taskID,
581
+ childSessionID,
582
+ parentSessionID,
583
+ linkedAt: now
584
+ };
585
+ links.set(childSessionID, link);
586
+ taskIndex.set(intent.taskID, childSessionID);
587
+ return link;
588
+ };
589
+ const getLink = (childSessionID) => {
590
+ return links.get(childSessionID);
591
+ };
592
+ const getLinkByTask = (taskID) => {
593
+ const childID = taskIndex.get(taskID);
594
+ if (!childID)
595
+ return;
596
+ return links.get(childID);
556
597
  };
598
+ const removeLink = (childSessionID) => {
599
+ const link = links.get(childSessionID);
600
+ if (!link)
601
+ return;
602
+ links.delete(childSessionID);
603
+ taskIndex.delete(link.taskID);
604
+ return link;
605
+ };
606
+ return { recordIntent, tryCorrelate, getLink, getLinkByTask, removeLink };
557
607
  };
558
608
 
559
609
  // src/opencode-multiagent/defaults.ts
@@ -653,7 +703,7 @@ function createSessionTracker(config) {
653
703
  const {
654
704
  getActivityTime,
655
705
  onRemove,
656
- cleanupIntervalMs: cleanupIntervalMs2 = 5 * 60 * 1000,
706
+ cleanupIntervalMs = 5 * 60 * 1000,
657
707
  staleTtlMs = 30 * 60 * 1000,
658
708
  maxTracked = 200,
659
709
  evictionFraction = 0.2,
@@ -679,7 +729,7 @@ function createSessionTracker(config) {
679
729
  };
680
730
  let interval = null;
681
731
  if (enabled) {
682
- interval = setInterval(() => cleanupStale(), cleanupIntervalMs2);
732
+ interval = setInterval(() => cleanupStale(), cleanupIntervalMs);
683
733
  interval.unref?.();
684
734
  }
685
735
  const cleanup = () => {
@@ -779,10 +829,22 @@ var createQualityController = ({
779
829
  tracker.entries.delete(sessionID);
780
830
  }
781
831
  };
832
+ const hasQualityEvidence = (sessionID) => {
833
+ const state = tracker.entries.get(sessionID);
834
+ if (!state || state.editedFiles.size === 0)
835
+ return { passed: true };
836
+ if (state.lastQualityEvidenceAt >= state.lastEditAt)
837
+ return { passed: true };
838
+ return {
839
+ passed: false,
840
+ reason: `${state.editedFiles.size} file(s) edited, no verification evidence found`
841
+ };
842
+ };
782
843
  return {
783
844
  handleQualityEvent,
784
845
  recordQualityEvidence,
785
846
  trackEdit,
847
+ hasQualityEvidence,
786
848
  cleanup: tracker.cleanup
787
849
  };
788
850
  };
@@ -808,7 +870,7 @@ var createSupervisionController = ({
808
870
  getActivityTime: (entry) => entry.lastActivity,
809
871
  maxTracked: 100,
810
872
  onRemove: cleanupChildMap,
811
- enabled: Boolean(flags.supervision)
873
+ enabled: true
812
874
  });
813
875
  const removeChild = (childID) => {
814
876
  const info = tracker.entries.get(childID);
@@ -824,7 +886,7 @@ var createSupervisionController = ({
824
886
  return true;
825
887
  };
826
888
  const handleSupervision = async (event) => {
827
- if (!flags.supervision || !supervisionEventTypes.has(event?.type ?? ""))
889
+ if (!supervisionEventTypes.has(event?.type ?? ""))
828
890
  return;
829
891
  const props = event.properties ?? {};
830
892
  if (event.type === "session.created") {
@@ -840,12 +902,23 @@ var createSupervisionController = ({
840
902
  parentID,
841
903
  agentName: null,
842
904
  lastActivity: Date.now(),
843
- remindedAt: 0
905
+ remindedAt: 0,
906
+ lastToolErrorAt: 0,
907
+ hasUnverifiedEdits: false
844
908
  });
845
909
  tracker.enforceLimit();
846
910
  await note("supervision", { event: "child_tracked", parentID, childID });
847
911
  return;
848
912
  }
913
+ if (event.type === "session.deleted") {
914
+ const sessionID = typeof props.info?.id === "string" ? props.info.id : typeof props.sessionID === "string" ? props.sessionID : undefined;
915
+ if (!sessionID || !removeChild(sessionID))
916
+ return;
917
+ await note("supervision", { event: "child_removed", sessionID });
918
+ return;
919
+ }
920
+ if (!flags.supervision)
921
+ return;
849
922
  if (["message.updated", "message.part.updated", "message.part.delta"].includes(event.type ?? "")) {
850
923
  const sessionID = event.type === "message.updated" ? typeof props.info?.sessionID === "string" ? props.info.sessionID : undefined : event.type === "message.part.updated" ? typeof props.part?.sessionID === "string" ? props.part.sessionID : undefined : typeof props.sessionID === "string" ? props.sessionID : undefined;
851
924
  if (!sessionID || !tracker.entries.has(sessionID))
@@ -885,14 +958,33 @@ var createSupervisionController = ({
885
958
  });
886
959
  return;
887
960
  }
888
- if (event.type === "session.deleted") {
889
- const sessionID = typeof props.info?.id === "string" ? props.info.id : undefined;
890
- if (!sessionID || !removeChild(sessionID))
891
- return;
892
- await note("supervision", { event: "child_removed", sessionID });
961
+ };
962
+ const recordToolError = (sessionID) => {
963
+ const info = tracker.entries.get(sessionID);
964
+ if (info)
965
+ info.lastToolErrorAt = Date.now();
966
+ };
967
+ const recordVerification = (sessionID) => {
968
+ const info = tracker.entries.get(sessionID);
969
+ if (info)
970
+ info.hasUnverifiedEdits = false;
971
+ };
972
+ const getActiveChildCount = (parentID) => {
973
+ return childMap.get(parentID)?.size ?? 0;
974
+ };
975
+ return {
976
+ handleSupervision,
977
+ cleanup: tracker.cleanup,
978
+ recordToolError,
979
+ recordVerification,
980
+ getActiveChildCount,
981
+ getChildInfo(sessionID) {
982
+ return tracker.entries.get(sessionID);
983
+ },
984
+ getParentID(sessionID) {
985
+ return tracker.entries.get(sessionID)?.parentID;
893
986
  }
894
987
  };
895
- return { handleSupervision, cleanup: tracker.cleanup };
896
988
  };
897
989
 
898
990
  // src/opencode-multiagent/task-manager.ts
@@ -905,24 +997,83 @@ var createTaskManager = (projectRoot) => {
905
997
  return `T-${Date.now()}-${taskCounter.toString().padStart(4, "0")}`;
906
998
  };
907
999
  const tasks = new Map;
908
- const boardPath = projectRoot ? join3(projectRoot, ".opencode", "tasks", "taskboard.json") : undefined;
909
- const persist = async () => {
1000
+ const boardPath = projectRoot ? join3(projectRoot, ".magent", "board.json") : undefined;
1001
+ const legacyBoardPath = projectRoot ? join3(projectRoot, ".opencode", "tasks", "taskboard.json") : undefined;
1002
+ let persistTimer = null;
1003
+ let persistInFlight = false;
1004
+ let persistQueued = false;
1005
+ const doPersist = async () => {
910
1006
  if (!boardPath)
911
1007
  return;
1008
+ if (persistInFlight) {
1009
+ persistQueued = true;
1010
+ return;
1011
+ }
1012
+ persistInFlight = true;
912
1013
  try {
913
1014
  await mkdir2(join3(boardPath, ".."), { recursive: true });
914
1015
  await writeFile(boardPath, JSON.stringify([...tasks.values()], null, 2), "utf8");
915
- } catch {}
1016
+ } catch {} finally {
1017
+ persistInFlight = false;
1018
+ if (persistQueued) {
1019
+ persistQueued = false;
1020
+ doPersist();
1021
+ }
1022
+ }
1023
+ };
1024
+ const schedulePersist = () => {
1025
+ if (persistTimer)
1026
+ clearTimeout(persistTimer);
1027
+ persistTimer = setTimeout(() => {
1028
+ persistTimer = null;
1029
+ doPersist();
1030
+ }, 100);
916
1031
  };
917
1032
  const load = async () => {
918
1033
  if (!boardPath)
919
1034
  return;
1035
+ let loaded = false;
920
1036
  try {
921
1037
  const raw = await readFile4(boardPath, "utf8");
922
1038
  const items = JSON.parse(raw);
923
1039
  for (const item of items)
924
1040
  tasks.set(item.id, item);
1041
+ loaded = true;
925
1042
  } catch {}
1043
+ if (!loaded && legacyBoardPath) {
1044
+ try {
1045
+ const raw = await readFile4(legacyBoardPath, "utf8");
1046
+ const items = JSON.parse(raw);
1047
+ for (const item of items)
1048
+ tasks.set(item.id, item);
1049
+ loaded = true;
1050
+ schedulePersist();
1051
+ } catch {}
1052
+ }
1053
+ if (loaded) {
1054
+ for (const task of tasks.values()) {
1055
+ if (task.status === "in_progress" || task.status === "claimed") {
1056
+ task.status = "pending";
1057
+ task.updatedAt = Date.now();
1058
+ task.result = (task.result ? task.result + " | " : "") + "Reset from stale state on load";
1059
+ }
1060
+ }
1061
+ if (tasks.size > 0)
1062
+ schedulePersist();
1063
+ }
1064
+ };
1065
+ const areDependenciesMet = (taskID) => {
1066
+ const task = tasks.get(taskID);
1067
+ if (!task || task.dependencies.length === 0)
1068
+ return { met: true, unmet: [] };
1069
+ const unmet = [];
1070
+ for (const depID of task.dependencies) {
1071
+ const dep = tasks.get(depID);
1072
+ if (!dep || dep.status !== "completed") {
1073
+ unmet.push(depID);
1074
+ }
1075
+ }
1076
+ return { met: unmet.length === 0, unmet };
926
1077
  };
927
1078
  const create = (input) => {
928
1079
  const task = {
@@ -938,13 +1089,21 @@ var createTaskManager = (projectRoot) => {
938
1089
  updatedAt: Date.now()
939
1090
  };
940
1091
  tasks.set(task.id, task);
941
- persist();
1092
+ schedulePersist();
942
1093
  return task;
943
1094
  };
944
1095
  const update = (taskID, input) => {
945
1096
  const task = tasks.get(taskID);
946
1097
  if (!task)
947
1098
  return { error: `Task ${taskID} not found` };
1099
+ if (input.status && (input.status === "in_progress" || input.status === "claimed") && task.dependencies.length > 0) {
1100
+ const { met, unmet } = areDependenciesMet(taskID);
1101
+ if (!met) {
1102
+ return {
1103
+ error: `Cannot transition ${taskID} to ${input.status}: unmet dependencies [${unmet.join(", ")}]`
1104
+ };
1105
+ }
1106
+ }
948
1107
  if (input.status !== undefined)
949
1108
  task.status = input.status;
950
1109
  if (input.result !== undefined)
@@ -952,7 +1111,13 @@ var createTaskManager = (projectRoot) => {
952
1111
  if (input.assignedAgent !== undefined)
953
1112
  task.assignedAgent = input.assignedAgent;
954
1113
  task.updatedAt = Date.now();
955
- persist();
1114
+ schedulePersist();
1115
+ return task;
1116
+ };
1117
+ const get = (taskID) => {
1118
+ const task = tasks.get(taskID);
1119
+ if (!task)
1120
+ return { error: `Task ${taskID} not found` };
956
1121
  return task;
957
1122
  };
958
1123
  const list = (filter) => {
@@ -967,7 +1132,25 @@ var createTaskManager = (projectRoot) => {
967
1132
  return true;
968
1133
  });
969
1134
  };
970
- return { create, update, list, load };
1135
+ const getActiveSummary = () => {
1136
+ const active = [...tasks.values()].filter((t) => t.status === "pending" || t.status === "claimed" || t.status === "in_progress");
1137
+ if (active.length === 0)
1138
+ return "";
1139
+ const lines = active.slice(0, 10).map((t) => `- [${t.status}] ${t.title}${t.assignedAgent ? ` (${t.assignedAgent})` : ""}`);
1140
+ return `Active tasks (${active.length}):
1141
+ ${lines.join(`
1142
+ `)}`;
1143
+ };
1144
+ const linkSession = (taskID, sessionID) => {
1145
+ const task = tasks.get(taskID);
1146
+ if (!task)
1147
+ return false;
1148
+ task.sessionID = sessionID;
1149
+ task.updatedAt = Date.now();
1150
+ schedulePersist();
1151
+ return true;
1152
+ };
1153
+ return { create, update, get, list, load, getActiveSummary, linkSession };
971
1154
  };
972
1155
 
973
1156
  // src/opencode-multiagent/telemetry.ts
@@ -992,11 +1175,13 @@ var createTelemetryController = ({ flags }) => {
992
1175
  filesEdited: 0,
993
1176
  tasksDispatched: 0,
994
1177
  permissionDenied: 0,
1178
+ flushed: false,
995
1179
  ...extra
996
1180
  });
997
1181
  }
998
1182
  const state = tracker.entries.get(sessionID);
999
- Object.assign(state, extra);
1183
+ const safeExtra = Object.fromEntries(Object.entries(extra).filter(([, v]) => v !== undefined));
1184
+ Object.assign(state, safeExtra);
1000
1185
  state.lastActivityAt = now;
1001
1186
  return state;
1002
1187
  };
@@ -1038,8 +1223,9 @@ var createTelemetryController = ({ flags }) => {
1038
1223
  },
1039
1224
  async flushSession(sessionID, reason = "session_deleted") {
1040
1225
  const state = tracker.entries.get(sessionID);
1041
- if (!state)
1226
+ if (!state || state.flushed)
1042
1227
  return;
1228
+ state.flushed = true;
1043
1229
  tracker.entries.delete(sessionID);
1044
1230
  await note("session_metrics", {
1045
1231
  observation: true,
@@ -1054,13 +1240,19 @@ var createTelemetryController = ({ flags }) => {
1054
1240
  permission_denied: state.permissionDenied,
1055
1241
  reason
1056
1242
  });
1243
+ },
1244
+ async flushAll() {
1245
+ const ids = [...tracker.entries.keys()];
1246
+ for (const id of ids) {
1247
+ await this.flushSession(id, "cleanup");
1248
+ }
1057
1249
  }
1058
1250
  };
1059
1251
  };
1060
1252
 
1061
1253
  // src/opencode-multiagent/tools.ts
1062
1254
  import { tool as pluginTool } from "@opencode-ai/plugin";
1063
- var createTaskTools = (taskManager, taskManagerReady) => ({
1255
+ var createTaskTools = (taskManager, taskManagerReady, correlation, quality, flags) => ({
1064
1256
  task_create: pluginTool({
1065
1257
  description: "Create a new task on the shared task board",
1066
1258
  args: {
@@ -1075,18 +1267,84 @@ var createTaskTools = (taskManager, taskManagerReady) => ({
1075
1267
  return JSON.stringify(taskManager.create({ ...args, createdBy: ctx.agent }));
1076
1268
  }
1077
1269
  }),
1270
+ task_dispatch: pluginTool({
1271
+ description: "Record a dispatch intent linking a task board entry to the next child session. " + "Call this immediately before dispatching work via the task tool. " + "Sets the task status to in_progress and records the correlation intent.",
1272
+ args: {
1273
+ taskID: pluginTool.schema.string().describe("ID of the task being dispatched"),
1274
+ notes: pluginTool.schema.string().optional().describe("Optional dispatch notes or context")
1275
+ },
1276
+ async execute(args, ctx) {
1277
+ await taskManagerReady;
1278
+ const task = taskManager.get(args.taskID);
1279
+ if ("error" in task)
1280
+ return JSON.stringify(task);
1281
+ const updated = taskManager.update(args.taskID, { status: "in_progress" });
1282
+ if ("error" in updated)
1283
+ return JSON.stringify(updated);
1284
+ if (correlation) {
1285
+ const parentSessionID = ctx.sessionID ?? "";
1286
+ const targetAgent = task.assignedAgent ?? "unknown";
1287
+ correlation.recordIntent(parentSessionID, args.taskID, targetAgent);
1288
+ }
1289
+ await note("task_dispatch_intent", {
1290
+ taskID: args.taskID,
1291
+ agent: ctx.agent,
1292
+ notes: args.notes
1293
+ });
1294
+ return JSON.stringify({
1295
+ ...updated,
1296
+ dispatch: "intent_recorded",
1297
+ notes: args.notes
1298
+ });
1299
+ }
1300
+ }),
1078
1301
  task_update: pluginTool({
1079
- description: "Update a task's status or result on the shared task board",
1302
+ description: "Update a task's status or result on the shared task board. " + "Dependency enforcement: transitions to in_progress or claimed are blocked if dependencies are not completed.",
1080
1303
  args: {
1081
1304
  taskID: pluginTool.schema.string().describe("ID of the task to update"),
1082
1305
  status: pluginTool.schema.enum(["pending", "claimed", "in_progress", "completed", "failed", "blocked"]).describe("New status for the task"),
1083
- result: pluginTool.schema.string().optional().describe("Result summary or notes")
1306
+ result: pluginTool.schema.string().optional().describe("Result summary or notes"),
1307
+ force: pluginTool.schema.boolean().optional().describe("Force completion even without quality evidence (bypass quality gate)")
1084
1308
  },
1085
- async execute(args) {
1309
+ async execute(args, ctx) {
1086
1310
  await taskManagerReady;
1311
+ if (flags?.quality_gate_enforcement && args.status === "completed" && !args.force && quality) {
1312
+ const link = correlation?.getLinkByTask(args.taskID);
1313
+ if (link) {
1314
+ const evidence = quality.hasQualityEvidence(link.childSessionID);
1315
+ if (!evidence.passed) {
1316
+ await note("quality_gate_blocked", {
1317
+ taskID: args.taskID,
1318
+ sessionID: link.childSessionID,
1319
+ reason: evidence.reason
1320
+ });
1321
+ return JSON.stringify({
1322
+ error: `Quality gate: ${evidence.reason}. Run verification commands (test/lint/build) before completing, or pass force: true to bypass.`,
1323
+ taskID: args.taskID,
1324
+ quality_gate: "blocked"
1325
+ });
1326
+ }
1327
+ }
1328
+ }
1329
+ if (args.force && args.status === "completed") {
1330
+ await note("quality_gate_bypassed", {
1331
+ taskID: args.taskID,
1332
+ agent: ctx.agent
1333
+ });
1334
+ }
1087
1335
  return JSON.stringify(taskManager.update(args.taskID, { status: args.status, result: args.result }));
1088
1336
  }
1089
1337
  }),
1338
+ task_get: pluginTool({
1339
+ description: "Get a single task by ID from the shared task board",
1340
+ args: {
1341
+ taskID: pluginTool.schema.string().describe("ID of the task to retrieve")
1342
+ },
1343
+ async execute(args) {
1344
+ await taskManagerReady;
1345
+ return JSON.stringify(taskManager.get(args.taskID));
1346
+ }
1347
+ }),
1090
1348
  task_list: pluginTool({
1091
1349
  description: "List tasks on the shared task board with optional filters",
1092
1350
  args: {
@@ -1119,17 +1377,18 @@ var createPluginHooks = ({
1119
1377
  }) => {
1120
1378
  const projectAgentsDir = projectRoot ? join4(projectRoot, ".opencode", "agents") : undefined;
1121
1379
  const projectCommandsDir = projectRoot ? join4(projectRoot, ".opencode", "commands") : undefined;
1122
- const { handleSupervision, cleanup } = createSupervisionController({ flags, client });
1380
+ const supervision = createSupervisionController({ flags, client });
1381
+ const { handleSupervision, cleanup, getChildInfo } = supervision;
1123
1382
  const quality = createQualityController({ flags, client });
1124
1383
  const telemetry = createTelemetryController({ flags });
1125
- const fileLocks = createFileLockController();
1384
+ const correlation = createCorrelationController();
1126
1385
  const sessionAgentMap = new Map;
1127
1386
  const qaDispatchState = new Map;
1128
1387
  let mcpDefaults = null;
1129
1388
  let mcpRegistry = null;
1389
+ let taskRoutingRegistry = null;
1130
1390
  const taskManager = createTaskManager(projectRoot);
1131
1391
  const taskManagerReady = taskManager.load();
1132
- const childSessionInfo = new Map;
1133
1392
  const rememberSession = (sessionID, extra = {}) => {
1134
1393
  if (!sessionID)
1135
1394
  return;
@@ -1158,7 +1417,7 @@ var createPluginHooks = ({
1158
1417
  parts: [
1159
1418
  {
1160
1419
  type: "text",
1161
- text: `[opencode-multiagent qa] This session has dispatched QA ${state.count} times. ` + "Reassess the brief, consider planner repair, or narrow the defect before sending QA again."
1420
+ text: `[opencode-multiagent review] This session has dispatched reviewer ${state.count} times. ` + "Reassess the brief, consider planner repair, or narrow the defect before sending reviewer again."
1162
1421
  }
1163
1422
  ]
1164
1423
  }
@@ -1174,9 +1433,10 @@ var createPluginHooks = ({
1174
1433
  qa_dispatch_count: state.count
1175
1434
  });
1176
1435
  };
1177
- const notifyParentOnChildCompletion = async (childSessionID, parentSessionID, agentName) => {
1436
+ const notifyParentOnChildCompletion = async (childSessionID, parentSessionID, agentName, taskLabel) => {
1178
1437
  const agentLabel = agentName ? ` (agent: ${agentName})` : "";
1179
- const text2 = `Child session ${childSessionID}${agentLabel} has completed. ` + "Review its output and proceed with the next task.";
1438
+ const taskInfo = taskLabel ? `, task ${taskLabel}` : "";
1439
+ const text2 = `Child session ${childSessionID}${agentLabel}${taskInfo} has completed. ` + "Review its output and proceed with the next task.";
1180
1440
  try {
1181
1441
  if (client.session?.prompt) {
1182
1442
  await client.session.prompt({
@@ -1202,13 +1462,31 @@ var createPluginHooks = ({
1202
1462
  return;
1203
1463
  }
1204
1464
  };
1465
+ const loadActivePlanSummary = async () => {
1466
+ if (!projectRoot)
1467
+ return "";
1468
+ const plansDir = join4(projectRoot, ".magent", "plans");
1469
+ try {
1470
+ const entries = await readdir2(plansDir);
1471
+ const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
1472
+ if (mdFiles.length === 0)
1473
+ return "";
1474
+ const lastPlan = mdFiles[mdFiles.length - 1];
1475
+ const content = await readFile5(join4(plansDir, lastPlan), "utf8");
1476
+ const objectiveMatch = content.match(/##\s+Objective\s*\n([\s\S]*?)(?=\n##|\s*$)/);
1477
+ const objective = objectiveMatch ? objectiveMatch[1].trim().split(`
1478
+ `)[0].slice(0, 200) : "";
1479
+ return objective ? `Active plan: ${lastPlan} \u2014 ${objective}` : `Active plan: ${lastPlan}`;
1480
+ } catch {
1481
+ return "";
1482
+ }
1483
+ };
1205
1484
  return {
1206
- cleanup() {
1485
+ async cleanup() {
1486
+ await telemetry.flushAll();
1207
1487
  cleanup?.();
1208
1488
  quality.cleanup?.();
1209
1489
  telemetry.cleanup?.();
1210
- fileLocks.cleanup?.();
1211
- childSessionInfo.clear();
1212
1490
  },
1213
1491
  async event(input) {
1214
1492
  const type = input.event?.type;
@@ -1233,35 +1511,65 @@ var createPluginHooks = ({
1233
1511
  if (parentID && childID) {
1234
1512
  const parent = sessionAgentMap.get(parentID) ?? {};
1235
1513
  telemetry.trackTaskDispatch(parentID, { agent: parent.agent, model: parent.model });
1514
+ const agentHint = typeof props.info?.agent === "string" ? props.info.agent : undefined;
1515
+ const link = correlation.tryCorrelate(parentID, childID, agentHint);
1516
+ if (link) {
1517
+ taskManager.linkSession(link.taskID, childID);
1518
+ await note("task_correlation", {
1519
+ observation: true,
1520
+ taskID: link.taskID,
1521
+ parentSessionID: parentID,
1522
+ childSessionID: childID,
1523
+ agentHint
1524
+ });
1525
+ }
1236
1526
  await note("task_dispatch", {
1237
1527
  observation: true,
1238
1528
  parent_sessionID: parentID,
1239
1529
  parent_agent: parent.agent,
1240
1530
  child_sessionID: childID
1241
1531
  });
1242
- childSessionInfo.set(childID, { parentID, agentName: null });
1243
1532
  }
1244
1533
  } else if (type === "message.updated") {
1245
1534
  const sessionID = typeof props.info?.sessionID === "string" ? props.info.sessionID : undefined;
1246
1535
  const agent = typeof props.info?.agent === "string" ? props.info.agent : undefined;
1247
1536
  if (sessionID && agent) {
1248
1537
  rememberSession(sessionID, { agent });
1249
- const childInfo = childSessionInfo.get(sessionID);
1250
- if (childInfo)
1251
- childInfo.agentName = agent;
1252
1538
  }
1253
1539
  } else if (type === "session.deleted") {
1254
1540
  const sessionID = typeof props.info?.id === "string" ? props.info.id : typeof props.sessionID === "string" ? props.sessionID : undefined;
1255
1541
  if (sessionID) {
1256
- const childInfo = childSessionInfo.get(sessionID);
1542
+ const childInfo = getChildInfo(sessionID);
1257
1543
  sessionAgentMap.delete(sessionID);
1258
1544
  qaDispatchState.delete(sessionID);
1259
- fileLocks.releaseAll(sessionID);
1260
1545
  await telemetry.flushSession(sessionID);
1546
+ let taskLabel;
1547
+ const link = correlation.getLink(sessionID);
1548
+ if (link && flags.task_lifecycle) {
1549
+ const recentErrorMs = 30000;
1550
+ const hasRecentError = childInfo?.lastToolErrorAt != null && childInfo.lastToolErrorAt > 0 && Date.now() - childInfo.lastToolErrorAt < recentErrorMs;
1551
+ const autoStatus = hasRecentError ? "failed" : "completed";
1552
+ const autoResult = hasRecentError ? "Auto-failed: tool error detected near session end" : "Auto-completed: session ended without recent errors";
1553
+ await taskManagerReady;
1554
+ taskManager.update(link.taskID, { status: autoStatus, result: autoResult });
1555
+ taskLabel = link.taskID;
1556
+ await note("task_lifecycle_auto", {
1557
+ observation: true,
1558
+ taskID: link.taskID,
1559
+ childSessionID: sessionID,
1560
+ autoStatus
1561
+ });
1562
+ }
1563
+ if (link)
1564
+ correlation.removeLink(sessionID);
1261
1565
  if (childInfo?.parentID) {
1262
- await notifyParentOnChildCompletion(sessionID, childInfo.parentID, childInfo.agentName);
1566
+ await notifyParentOnChildCompletion(sessionID, childInfo.parentID, childInfo.agentName, taskLabel);
1263
1567
  }
1264
- childSessionInfo.delete(sessionID);
1568
+ }
1569
+ } else if (type === "session.idle") {
1570
+ const sessionID = typeof props.sessionID === "string" ? props.sessionID : typeof props.info?.id === "string" ? props.info.id : undefined;
1571
+ if (sessionID) {
1572
+ await telemetry.flushSession(sessionID, "session_idle");
1265
1573
  }
1266
1574
  }
1267
1575
  await handleSupervision(input.event);
@@ -1324,7 +1632,8 @@ var createPluginHooks = ({
1324
1632
  });
1325
1633
  },
1326
1634
  async "chat.message"(input, output) {
1327
- if (!flags.prompt_controls || !risky(text(output.parts)))
1635
+ const userParts = Array.isArray(output.parts) ? output.parts.filter((p) => p?.type === "text" && p?.role !== "tool") : output.parts;
1636
+ if (!flags.prompt_controls || !risky(text(userParts)))
1328
1637
  return;
1329
1638
  await note("chat_risk", compact({
1330
1639
  observation: true,
@@ -1363,9 +1672,22 @@ var createPluginHooks = ({
1363
1672
  async "experimental.session.compacting"(_input, output) {
1364
1673
  if (flags.experimental?.session_compacting !== true)
1365
1674
  return;
1366
- if (output.context.includes(experimentalText))
1367
- return;
1368
- output.context.push(experimentalText);
1675
+ const parts = [];
1676
+ const planSummary = await loadActivePlanSummary();
1677
+ if (planSummary)
1678
+ parts.push(planSummary);
1679
+ await taskManagerReady;
1680
+ const taskSummary = taskManager.getActiveSummary();
1681
+ if (taskSummary)
1682
+ parts.push(taskSummary);
1683
+ if (parts.length > 0) {
1684
+ output.context.push(`[opencode-multiagent workflow state]
1685
+ ${parts.join(`
1686
+
1687
+ `)}`);
1688
+ } else if (!output.context.includes(experimentalText)) {
1689
+ output.context.push(experimentalText);
1690
+ }
1369
1691
  },
1370
1692
  async "experimental.text.complete"(_input, output) {
1371
1693
  if (flags.experimental?.text_complete !== true)
@@ -1378,7 +1700,9 @@ ${experimentalText}`;
1378
1700
  async config(sdkCfg) {
1379
1701
  const cfg = sdkCfg;
1380
1702
  if (flags.agent_compilation) {
1381
- mcpRegistry = await compileAgents(cfg, [bundledAgentsDir, globalAgentsDir, projectAgentsDir], agentSettings);
1703
+ const result = await compileAgents(cfg, [bundledAgentsDir, globalAgentsDir, projectAgentsDir], agentSettings);
1704
+ mcpRegistry = result.mcpRegistry;
1705
+ taskRoutingRegistry = result.taskRouting;
1382
1706
  }
1383
1707
  if (flags.command_compilation) {
1384
1708
  await compileCommands(cfg, [bundledCommandsDir, globalCommandsDir, projectCommandsDir]);
@@ -1396,23 +1720,24 @@ ${experimentalText}`;
1396
1720
  const agentInfo = sessionAgentMap.get(input.sessionID) ?? {};
1397
1721
  const agentName = agentInfo.agent;
1398
1722
  if (input.tool === "task") {
1723
+ const limit = flags.concurrency_limit;
1724
+ if (typeof limit === "number" && limit > 0 && input.sessionID) {
1725
+ const activeCount = supervision.getActiveChildCount(input.sessionID);
1726
+ if (activeCount >= limit) {
1727
+ await note("concurrency_blocked", {
1728
+ observation: true,
1729
+ sessionID: input.sessionID,
1730
+ activeCount,
1731
+ limit
1732
+ });
1733
+ throw new Error(`[opencode-multiagent] Concurrency limit reached: ${activeCount}/${limit} active child sessions. ` + "Wait for existing sessions to complete before dispatching new work.");
1734
+ }
1735
+ }
1399
1736
  const targetAgent = getTaskTarget(output.args ?? input.args ?? {});
1400
- if (targetAgent === "qa") {
1737
+ if (targetAgent === "reviewer") {
1401
1738
  await trackQaDispatch(input.sessionID, agentInfo);
1402
1739
  }
1403
- }
1404
- if (input.tool === "webfetch" && typeof output.args?.url === "string" && output.args.url.startsWith("http://")) {
1405
- output.args.url = output.args.url.replace("http://", "https://");
1406
- }
1407
- if (flags.enforcement && ["read", "edit"].includes(input.tool) && typeof output.args?.filePath === "string" && blocked(output.args.filePath)) {
1408
- throw new Error("[opencode-multiagent] blocked sensitive path access");
1409
- }
1410
- if (flags.enforcement && input.tool === "bash" && typeof output.args?.command === "string" && tokenizedBashBlocked(output.args.command)) {
1411
- throw new Error("[opencode-multiagent] blocked destructive bash command");
1412
- }
1413
- if (input.tool === "edit" && typeof output.args?.filePath === "string") {
1414
- const lock = fileLocks.acquire(input.sessionID, output.args.filePath);
1415
- if (!lock.ok) {
1740
+ if (flags.enforcement && agentName && targetAgent && taskRoutingRegistry && !isTaskRoutingAllowed(agentName, targetAgent, taskRoutingRegistry)) {
1416
1741
  telemetry.trackPermissionDenied(input.sessionID, {
1417
1742
  agent: agentName,
1418
1743
  model: agentInfo.model
@@ -1422,15 +1747,23 @@ ${experimentalText}`;
1422
1747
  sessionID: input.sessionID,
1423
1748
  agent: agentName,
1424
1749
  model: agentInfo.model,
1425
- tool: input.tool,
1426
- file: lock.filePath,
1427
- reason: "file_locked_by_other_session",
1428
- owner_sessionID: lock.ownerSessionID,
1750
+ tool: "task",
1751
+ target: targetAgent,
1752
+ reason: "task_routing_not_allowed",
1429
1753
  enforcement_layer: "plugin_hook"
1430
1754
  });
1431
- throw new Error(`[opencode-multiagent] File lock conflict for ${lock.filePath}; active session ${lock.ownerSessionID}`);
1755
+ throw new Error(`[opencode-multiagent] Agent ${agentName} is not allowed to delegate to ${targetAgent}`);
1432
1756
  }
1433
1757
  }
1758
+ if (input.tool === "webfetch" && typeof output.args?.url === "string" && output.args.url.startsWith("http://")) {
1759
+ output.args.url = output.args.url.replace("http://", "https://");
1760
+ }
1761
+ if (flags.enforcement && ["read", "edit"].includes(input.tool) && typeof output.args?.filePath === "string" && blocked(output.args.filePath)) {
1762
+ throw new Error("[opencode-multiagent] blocked sensitive path access");
1763
+ }
1764
+ if (flags.enforcement && input.tool === "bash" && typeof output.args?.command === "string" && tokenizedBashBlocked(output.args.command)) {
1765
+ throw new Error("[opencode-multiagent] blocked destructive bash command");
1766
+ }
1434
1767
  if (!flags.enforcement)
1435
1768
  return;
1436
1769
  if (isMcpTool(input.tool) && agentName && mcpRegistry?.has(agentName)) {
@@ -1491,7 +1824,6 @@ ${experimentalText}`;
1491
1824
  if (input.tool === "edit" && typeof input.args?.filePath === "string") {
1492
1825
  quality.trackEdit(input.sessionID, input.args.filePath);
1493
1826
  telemetry.trackEdit(input.sessionID);
1494
- fileLocks.acquire(input.sessionID, input.args.filePath);
1495
1827
  }
1496
1828
  if (input.tool === "bash" && typeof input.args?.command === "string") {
1497
1829
  quality.recordQualityEvidence(input.sessionID, input.args.command);
@@ -1504,6 +1836,7 @@ ${experimentalText}`;
1504
1836
  agent: agentInfo.agent,
1505
1837
  model: agentInfo.model
1506
1838
  });
1839
+ supervision.recordToolError(input.sessionID);
1507
1840
  }
1508
1841
  if (!flags.observation)
1509
1842
  return;
@@ -1525,7 +1858,7 @@ ${String(output.output ?? "")}`.toLowerCase();
1525
1858
  ...isNonzeroExit ? { exit_code: output.metadata.exit, error_type: "nonzero_exit" } : {}
1526
1859
  }));
1527
1860
  },
1528
- tool: createTaskTools(taskManager, taskManagerReady)
1861
+ tool: createTaskTools(taskManager, taskManagerReady, correlation, quality, flags)
1529
1862
  };
1530
1863
  };
1531
1864