opencode-multiagent 0.3.0-next.1 → 0.5.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 (96) hide show
  1. package/AGENTS.md +21 -0
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +4 -4
  4. package/README.tr.md +4 -4
  5. package/agents/AGENTS.md +95 -0
  6. package/agents/auditor.md +59 -17
  7. package/agents/brainstormer.md +113 -0
  8. package/agents/{worker.md → coder.md} +12 -10
  9. package/agents/{scribe.md → docmaster.md} +16 -8
  10. package/agents/executor.md +45 -62
  11. package/agents/planner.md +59 -47
  12. package/agents/reviewer.md +22 -9
  13. package/agents/scout.md +16 -12
  14. package/agents/sec-coder.md +83 -0
  15. package/agents/ui-coder.md +77 -0
  16. package/commands/board.md +17 -0
  17. package/commands/brainstorm-conclude.md +14 -0
  18. package/commands/brainstorm.md +14 -0
  19. package/commands/execute.md +8 -7
  20. package/commands/init-deep.md +6 -6
  21. package/commands/init.md +4 -5
  22. package/commands/inspect.md +5 -5
  23. package/commands/plan.md +7 -6
  24. package/commands/quality.md +3 -3
  25. package/commands/review.md +4 -3
  26. package/commands/status.md +4 -3
  27. package/defaults/AGENTS.md +48 -0
  28. package/defaults/opencode-multiagent.json +16 -150
  29. package/defaults/opencode-multiagent.schema.json +16 -190
  30. package/dist/index.js +471 -218
  31. package/dist/opencode-multiagent/compiler.d.ts +8 -2
  32. package/dist/opencode-multiagent/compiler.d.ts.map +1 -1
  33. package/dist/opencode-multiagent/constants.d.ts +3 -57
  34. package/dist/opencode-multiagent/constants.d.ts.map +1 -1
  35. package/dist/opencode-multiagent/correlation.d.ts +21 -0
  36. package/dist/opencode-multiagent/correlation.d.ts.map +1 -0
  37. package/dist/opencode-multiagent/defaults.d.ts +0 -2
  38. package/dist/opencode-multiagent/defaults.d.ts.map +1 -1
  39. package/dist/opencode-multiagent/hooks.d.ts.map +1 -1
  40. package/dist/opencode-multiagent/log.d.ts.map +1 -1
  41. package/dist/opencode-multiagent/markdown.d.ts.map +1 -1
  42. package/dist/opencode-multiagent/quality.d.ts +4 -0
  43. package/dist/opencode-multiagent/quality.d.ts.map +1 -1
  44. package/dist/opencode-multiagent/runtime.d.ts.map +1 -1
  45. package/dist/opencode-multiagent/supervision.d.ts +14 -0
  46. package/dist/opencode-multiagent/supervision.d.ts.map +1 -1
  47. package/dist/opencode-multiagent/task-manager.d.ts +8 -2
  48. package/dist/opencode-multiagent/task-manager.d.ts.map +1 -1
  49. package/dist/opencode-multiagent/telemetry.d.ts +2 -0
  50. package/dist/opencode-multiagent/telemetry.d.ts.map +1 -1
  51. package/dist/opencode-multiagent/tools.d.ts +32 -1
  52. package/dist/opencode-multiagent/tools.d.ts.map +1 -1
  53. package/docs/agents.md +77 -175
  54. package/docs/agents.tr.md +78 -175
  55. package/docs/configuration.md +17 -27
  56. package/docs/configuration.tr.md +17 -27
  57. package/docs/usage-guide.md +35 -34
  58. package/docs/usage-guide.tr.md +36 -35
  59. package/examples/opencode.with-overrides.json +2 -2
  60. package/package.json +1 -1
  61. package/skills/AGENTS.md +51 -0
  62. package/skills/advanced-evaluation/manifest.json +1 -1
  63. package/skills/cek-context-engineering/manifest.json +1 -1
  64. package/skills/cek-prompt-engineering/manifest.json +1 -1
  65. package/skills/cek-test-prompt/manifest.json +1 -1
  66. package/skills/cek-thought-based-reasoning/manifest.json +1 -1
  67. package/skills/context-degradation/manifest.json +1 -1
  68. package/skills/debate/manifest.json +1 -1
  69. package/skills/design-first/manifest.json +1 -1
  70. package/skills/dispatching-parallel-agents/manifest.json +1 -1
  71. package/skills/drift-analysis/manifest.json +1 -1
  72. package/skills/evaluation/manifest.json +1 -1
  73. package/skills/parallel-investigation/manifest.json +1 -1
  74. package/skills/reflexion-critique/manifest.json +1 -1
  75. package/skills/reflexion-reflect/manifest.json +1 -1
  76. package/skills/root-cause-analysis/manifest.json +1 -1
  77. package/skills/sadd-judge-with-debate/manifest.json +1 -1
  78. package/skills/structured-code-review/manifest.json +1 -1
  79. package/skills/task-decomposition/manifest.json +1 -1
  80. package/skills/verification-before-completion/manifest.json +1 -1
  81. package/skills/verification-gates/manifest.json +1 -1
  82. package/agents/advisor.md +0 -60
  83. package/agents/critic.md +0 -136
  84. package/agents/deep-worker.md +0 -69
  85. package/agents/devil.md +0 -38
  86. package/agents/heavy-worker.md +0 -72
  87. package/agents/lead.md +0 -147
  88. package/agents/librarian.md +0 -66
  89. package/agents/qa.md +0 -53
  90. package/agents/quick.md +0 -70
  91. package/agents/strategist.md +0 -66
  92. package/agents/ui-heavy-worker.md +0 -66
  93. package/agents/ui-worker.md +0 -74
  94. package/agents/validator.md +0 -50
  95. package/dist/opencode-multiagent/file-lock.d.ts +0 -15
  96. 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
  ]);
@@ -72,67 +69,7 @@ var mcpToolPrefixes = [
72
69
  "code_index_",
73
70
  "repo_git_"
74
71
  ];
75
- var defaultProfiles = {
76
- minimal: {
77
- enforcement: true,
78
- observation: false,
79
- prompt_controls: false,
80
- agent_compilation: true,
81
- command_compilation: true,
82
- mcp_compilation: true,
83
- telemetry: false,
84
- supervision: false,
85
- quality_gate: false,
86
- experimental: {
87
- chat_system_transform: false,
88
- chat_messages_transform: false,
89
- session_compacting: false,
90
- text_complete: false
91
- }
92
- },
93
- standard: {
94
- enforcement: true,
95
- observation: true,
96
- prompt_controls: true,
97
- agent_compilation: true,
98
- command_compilation: true,
99
- mcp_compilation: true,
100
- telemetry: true,
101
- supervision: true,
102
- quality_gate: true,
103
- skill_sources: [
104
- bundledSkillsDir,
105
- join(homedir(), ".agents", "skills"),
106
- join(homedir(), "skills")
107
- ],
108
- skill_injection: false,
109
- experimental: {
110
- chat_system_transform: false,
111
- chat_messages_transform: false,
112
- session_compacting: false,
113
- text_complete: false
114
- }
115
- },
116
- strict: {
117
- enforcement: true,
118
- observation: true,
119
- prompt_controls: true,
120
- agent_compilation: true,
121
- command_compilation: true,
122
- mcp_compilation: true,
123
- telemetry: true,
124
- supervision: true,
125
- quality_gate: true,
126
- experimental: {
127
- chat_system_transform: true,
128
- chat_messages_transform: true,
129
- session_compacting: true,
130
- text_complete: true
131
- }
132
- }
133
- };
134
72
  var defaultFlags = {
135
- profile: "standard",
136
73
  enforcement: true,
137
74
  observation: true,
138
75
  prompt_controls: true,
@@ -142,6 +79,9 @@ var defaultFlags = {
142
79
  telemetry: true,
143
80
  supervision: true,
144
81
  quality_gate: true,
82
+ task_lifecycle: true,
83
+ quality_gate_enforcement: true,
84
+ concurrency_limit: 5,
145
85
  skill_sources: [
146
86
  bundledSkillsDir,
147
87
  join(homedir(), ".agents", "skills"),
@@ -152,10 +92,10 @@ var defaultFlags = {
152
92
  permission_compilation: true
153
93
  },
154
94
  experimental: {
155
- chat_system_transform: false,
156
- chat_messages_transform: false,
157
- session_compacting: false,
158
- text_complete: false
95
+ chat_system_transform: true,
96
+ chat_messages_transform: true,
97
+ session_compacting: true,
98
+ text_complete: true
159
99
  },
160
100
  supervision_config: {
161
101
  idle_timeout_ms: 180000,
@@ -212,9 +152,18 @@ var experimentalText = "[opencode-multiagent experimental] Experimental control-
212
152
 
213
153
  // src/opencode-multiagent/log.ts
214
154
  import { appendFile, mkdir } from "fs/promises";
155
+ var _initPromise = null;
156
+ var ensureLogDir = () => {
157
+ if (_initPromise === null) {
158
+ _initPromise = mkdir(logDirPath, { recursive: true }).then(() => {
159
+ return;
160
+ });
161
+ }
162
+ return _initPromise;
163
+ };
215
164
  var note = async (kind, payload) => {
216
165
  try {
217
- await mkdir(logDirPath, { recursive: true });
166
+ await ensureLogDir();
218
167
  await appendFile(logFilePath, `${JSON.stringify({ ts: Date.now(), kind, ...payload })}
219
168
  `);
220
169
  } catch {}
@@ -297,7 +246,7 @@ async function loadMarkdownDefs(dirs) {
297
246
  const defs = new Map;
298
247
  for (const dir of dedupe(dirs)) {
299
248
  const files = await readdir(dir).catch(() => []);
300
- for (const file of files.filter((name) => name.endsWith(".md")).sort()) {
249
+ for (const file of files.filter((name) => name.endsWith(".md") && name !== name.toUpperCase()).sort()) {
301
250
  try {
302
251
  const value = await readFile(join2(dir, file), "utf8");
303
252
  const { data, body } = parseFrontmatter(value);
@@ -369,6 +318,22 @@ var normalizeDefinitionData = (data) => {
369
318
  return result;
370
319
  };
371
320
  var isMcpPermissionKey = (key) => mcpToolPrefixes.some((prefix) => key === prefix || key.startsWith(prefix));
321
+ var buildTaskRoutingAllowSet = (permission = {}) => {
322
+ const allowed = new Set;
323
+ const taskPerm = permission.task;
324
+ if (taskPerm === "allow" || taskPerm === true)
325
+ return new Set(["*"]);
326
+ if (taskPerm === "deny" || taskPerm === false || !taskPerm || typeof taskPerm !== "object")
327
+ return allowed;
328
+ const taskObj = taskPerm;
329
+ for (const [key, value] of Object.entries(taskObj)) {
330
+ if (key === "*")
331
+ continue;
332
+ if (value === "allow" || value === true)
333
+ allowed.add(key);
334
+ }
335
+ return allowed;
336
+ };
372
337
  var buildMcpPermissionRegistry = (permission = {}) => {
373
338
  const allowed = [];
374
339
  const denied = [];
@@ -387,6 +352,7 @@ async function compileAgents(cfg, dirs, agentSettings = {}) {
387
352
  if (!cfg.agent || typeof cfg.agent !== "object")
388
353
  cfg.agent = {};
389
354
  const mcpRegistry = new Map;
355
+ const taskRouting = new Map;
390
356
  for (const [name, raw] of defs.entries()) {
391
357
  const data = normalizeDefinitionData(raw.data);
392
358
  if (!own(cfg.agent, name)) {
@@ -412,10 +378,20 @@ async function compileAgents(cfg, dirs, agentSettings = {}) {
412
378
  if (!own(target, "prompt") && typeof raw.body === "string") {
413
379
  target.prompt = raw.body.trim();
414
380
  }
415
- mcpRegistry.set(name, buildMcpPermissionRegistry(target.permission ?? {}));
381
+ const perm = target.permission ?? {};
382
+ mcpRegistry.set(name, buildMcpPermissionRegistry(perm));
383
+ taskRouting.set(name, buildTaskRoutingAllowSet(perm));
416
384
  }
417
- return mcpRegistry;
385
+ return { mcpRegistry, taskRouting };
418
386
  }
387
+ var isTaskRoutingAllowed = (agent, target, routing) => {
388
+ const allowed = routing.get(agent);
389
+ if (!allowed)
390
+ return true;
391
+ if (allowed.has("*"))
392
+ return true;
393
+ return allowed.has(target);
394
+ };
419
395
  async function compileCommands(cfg, dirs) {
420
396
  const defs = await loadMarkdownDefs(dirs);
421
397
  if (!cfg.command || typeof cfg.command !== "object")
@@ -446,8 +422,8 @@ var applyBuiltInAgentPolicy = async (cfg) => {
446
422
  }
447
423
  const current = typeof cfg.default_agent === "string" ? cfg.default_agent : undefined;
448
424
  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";
425
+ const preferred = cfg.agent.planner && cfg.agent.planner.disable !== true ? "planner" : cfg.agent.executor && cfg.agent.executor.disable !== true ? "executor" : undefined;
426
+ const invalidCurrent = !current || disabledNativeAgents.includes(current) || !currentAgent || currentAgent.disable === true || currentAgent.hidden === true || currentAgent.mode === "subagent" || currentAgent.mode === "sub" || preferred === "planner" && current !== "planner";
451
427
  if (invalidCurrent && preferred) {
452
428
  const previous = current;
453
429
  cfg.default_agent = preferred;
@@ -493,67 +469,72 @@ var matchesMcpPermission = (tool, registry) => {
493
469
  return registry.fallback === "allow";
494
470
  };
495
471
 
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
- }
472
+ // src/opencode-multiagent/correlation.ts
473
+ var INTENT_TTL_MS = 60000;
474
+ var createCorrelationController = () => {
475
+ const intentQueues = new Map;
476
+ const links = new Map;
477
+ const taskIndex = new Map;
478
+ const recordIntent = (parentSessionID, taskID, targetAgent) => {
479
+ if (!intentQueues.has(parentSessionID)) {
480
+ intentQueues.set(parentSessionID, []);
519
481
  }
520
- sessionFiles.delete(sessionID);
521
- return count;
482
+ intentQueues.get(parentSessionID).push({
483
+ taskID,
484
+ parentSessionID,
485
+ targetAgent,
486
+ createdAt: Date.now()
487
+ });
522
488
  };
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
- }
489
+ const tryCorrelate = (parentSessionID, childSessionID, agentHint) => {
490
+ const queue = intentQueues.get(parentSessionID);
491
+ if (!queue || queue.length === 0)
492
+ return;
493
+ const now = Date.now();
494
+ const fresh = queue.filter((i) => now - i.createdAt < INTENT_TTL_MS);
495
+ if (fresh.length === 0) {
496
+ intentQueues.delete(parentSessionID);
497
+ return;
534
498
  }
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);
499
+ intentQueues.set(parentSessionID, fresh);
500
+ let matchIdx = -1;
501
+ if (agentHint) {
502
+ matchIdx = fresh.findIndex((i) => i.targetAgent === agentHint);
503
+ }
504
+ if (matchIdx < 0) {
505
+ matchIdx = 0;
555
506
  }
507
+ const intent = fresh.splice(matchIdx, 1)[0];
508
+ if (fresh.length === 0)
509
+ intentQueues.delete(parentSessionID);
510
+ const link = {
511
+ taskID: intent.taskID,
512
+ childSessionID,
513
+ parentSessionID,
514
+ linkedAt: now
515
+ };
516
+ links.set(childSessionID, link);
517
+ taskIndex.set(intent.taskID, childSessionID);
518
+ return link;
519
+ };
520
+ const getLink = (childSessionID) => {
521
+ return links.get(childSessionID);
556
522
  };
523
+ const getLinkByTask = (taskID) => {
524
+ const childID = taskIndex.get(taskID);
525
+ if (!childID)
526
+ return;
527
+ return links.get(childID);
528
+ };
529
+ const removeLink = (childSessionID) => {
530
+ const link = links.get(childSessionID);
531
+ if (!link)
532
+ return;
533
+ links.delete(childSessionID);
534
+ taskIndex.delete(link.taskID);
535
+ return link;
536
+ };
537
+ return { recordIntent, tryCorrelate, getLink, getLinkByTask, removeLink };
557
538
  };
558
539
 
559
540
  // src/opencode-multiagent/defaults.ts
@@ -653,7 +634,7 @@ function createSessionTracker(config) {
653
634
  const {
654
635
  getActivityTime,
655
636
  onRemove,
656
- cleanupIntervalMs: cleanupIntervalMs2 = 5 * 60 * 1000,
637
+ cleanupIntervalMs = 5 * 60 * 1000,
657
638
  staleTtlMs = 30 * 60 * 1000,
658
639
  maxTracked = 200,
659
640
  evictionFraction = 0.2,
@@ -679,7 +660,7 @@ function createSessionTracker(config) {
679
660
  };
680
661
  let interval = null;
681
662
  if (enabled) {
682
- interval = setInterval(() => cleanupStale(), cleanupIntervalMs2);
663
+ interval = setInterval(() => cleanupStale(), cleanupIntervalMs);
683
664
  interval.unref?.();
684
665
  }
685
666
  const cleanup = () => {
@@ -779,10 +760,22 @@ var createQualityController = ({
779
760
  tracker.entries.delete(sessionID);
780
761
  }
781
762
  };
763
+ const hasQualityEvidence = (sessionID) => {
764
+ const state = tracker.entries.get(sessionID);
765
+ if (!state || state.editedFiles.size === 0)
766
+ return { passed: true };
767
+ if (state.lastQualityEvidenceAt >= state.lastEditAt)
768
+ return { passed: true };
769
+ return {
770
+ passed: false,
771
+ reason: `${state.editedFiles.size} file(s) edited, no verification evidence found`
772
+ };
773
+ };
782
774
  return {
783
775
  handleQualityEvent,
784
776
  recordQualityEvidence,
785
777
  trackEdit,
778
+ hasQualityEvidence,
786
779
  cleanup: tracker.cleanup
787
780
  };
788
781
  };
@@ -808,7 +801,7 @@ var createSupervisionController = ({
808
801
  getActivityTime: (entry) => entry.lastActivity,
809
802
  maxTracked: 100,
810
803
  onRemove: cleanupChildMap,
811
- enabled: Boolean(flags.supervision)
804
+ enabled: true
812
805
  });
813
806
  const removeChild = (childID) => {
814
807
  const info = tracker.entries.get(childID);
@@ -824,7 +817,7 @@ var createSupervisionController = ({
824
817
  return true;
825
818
  };
826
819
  const handleSupervision = async (event) => {
827
- if (!flags.supervision || !supervisionEventTypes.has(event?.type ?? ""))
820
+ if (!supervisionEventTypes.has(event?.type ?? ""))
828
821
  return;
829
822
  const props = event.properties ?? {};
830
823
  if (event.type === "session.created") {
@@ -840,12 +833,23 @@ var createSupervisionController = ({
840
833
  parentID,
841
834
  agentName: null,
842
835
  lastActivity: Date.now(),
843
- remindedAt: 0
836
+ remindedAt: 0,
837
+ lastToolErrorAt: 0,
838
+ hasUnverifiedEdits: false
844
839
  });
845
840
  tracker.enforceLimit();
846
841
  await note("supervision", { event: "child_tracked", parentID, childID });
847
842
  return;
848
843
  }
844
+ if (event.type === "session.deleted") {
845
+ const sessionID = typeof props.info?.id === "string" ? props.info.id : typeof props.sessionID === "string" ? props.sessionID : undefined;
846
+ if (!sessionID || !removeChild(sessionID))
847
+ return;
848
+ await note("supervision", { event: "child_removed", sessionID });
849
+ return;
850
+ }
851
+ if (!flags.supervision)
852
+ return;
849
853
  if (["message.updated", "message.part.updated", "message.part.delta"].includes(event.type ?? "")) {
850
854
  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
855
  if (!sessionID || !tracker.entries.has(sessionID))
@@ -885,14 +889,33 @@ var createSupervisionController = ({
885
889
  });
886
890
  return;
887
891
  }
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 });
892
+ };
893
+ const recordToolError = (sessionID) => {
894
+ const info = tracker.entries.get(sessionID);
895
+ if (info)
896
+ info.lastToolErrorAt = Date.now();
897
+ };
898
+ const recordVerification = (sessionID) => {
899
+ const info = tracker.entries.get(sessionID);
900
+ if (info)
901
+ info.hasUnverifiedEdits = false;
902
+ };
903
+ const getActiveChildCount = (parentID) => {
904
+ return childMap.get(parentID)?.size ?? 0;
905
+ };
906
+ return {
907
+ handleSupervision,
908
+ cleanup: tracker.cleanup,
909
+ recordToolError,
910
+ recordVerification,
911
+ getActiveChildCount,
912
+ getChildInfo(sessionID) {
913
+ return tracker.entries.get(sessionID);
914
+ },
915
+ getParentID(sessionID) {
916
+ return tracker.entries.get(sessionID)?.parentID;
893
917
  }
894
918
  };
895
- return { handleSupervision, cleanup: tracker.cleanup };
896
919
  };
897
920
 
898
921
  // src/opencode-multiagent/task-manager.ts
@@ -905,24 +928,83 @@ var createTaskManager = (projectRoot) => {
905
928
  return `T-${Date.now()}-${taskCounter.toString().padStart(4, "0")}`;
906
929
  };
907
930
  const tasks = new Map;
908
- const boardPath = projectRoot ? join3(projectRoot, ".opencode", "tasks", "taskboard.json") : undefined;
909
- const persist = async () => {
931
+ const boardPath = projectRoot ? join3(projectRoot, ".magent", "board.json") : undefined;
932
+ const legacyBoardPath = projectRoot ? join3(projectRoot, ".opencode", "tasks", "taskboard.json") : undefined;
933
+ let persistTimer = null;
934
+ let persistInFlight = false;
935
+ let persistQueued = false;
936
+ const doPersist = async () => {
910
937
  if (!boardPath)
911
938
  return;
939
+ if (persistInFlight) {
940
+ persistQueued = true;
941
+ return;
942
+ }
943
+ persistInFlight = true;
912
944
  try {
913
945
  await mkdir2(join3(boardPath, ".."), { recursive: true });
914
946
  await writeFile(boardPath, JSON.stringify([...tasks.values()], null, 2), "utf8");
915
- } catch {}
947
+ } catch {} finally {
948
+ persistInFlight = false;
949
+ if (persistQueued) {
950
+ persistQueued = false;
951
+ doPersist();
952
+ }
953
+ }
954
+ };
955
+ const schedulePersist = () => {
956
+ if (persistTimer)
957
+ clearTimeout(persistTimer);
958
+ persistTimer = setTimeout(() => {
959
+ persistTimer = null;
960
+ doPersist();
961
+ }, 100);
916
962
  };
917
963
  const load = async () => {
918
964
  if (!boardPath)
919
965
  return;
966
+ let loaded = false;
920
967
  try {
921
968
  const raw = await readFile4(boardPath, "utf8");
922
969
  const items = JSON.parse(raw);
923
970
  for (const item of items)
924
971
  tasks.set(item.id, item);
972
+ loaded = true;
925
973
  } catch {}
974
+ if (!loaded && legacyBoardPath) {
975
+ try {
976
+ const raw = await readFile4(legacyBoardPath, "utf8");
977
+ const items = JSON.parse(raw);
978
+ for (const item of items)
979
+ tasks.set(item.id, item);
980
+ loaded = true;
981
+ schedulePersist();
982
+ } catch {}
983
+ }
984
+ if (loaded) {
985
+ for (const task of tasks.values()) {
986
+ if (task.status === "in_progress" || task.status === "claimed") {
987
+ task.status = "pending";
988
+ task.updatedAt = Date.now();
989
+ task.result = (task.result ? task.result + " | " : "") + "Reset from stale state on load";
990
+ }
991
+ }
992
+ if (tasks.size > 0)
993
+ schedulePersist();
994
+ }
995
+ };
996
+ const areDependenciesMet = (taskID) => {
997
+ const task = tasks.get(taskID);
998
+ if (!task || task.dependencies.length === 0)
999
+ return { met: true, unmet: [] };
1000
+ const unmet = [];
1001
+ for (const depID of task.dependencies) {
1002
+ const dep = tasks.get(depID);
1003
+ if (!dep || dep.status !== "completed") {
1004
+ unmet.push(depID);
1005
+ }
1006
+ }
1007
+ return { met: unmet.length === 0, unmet };
926
1008
  };
927
1009
  const create = (input) => {
928
1010
  const task = {
@@ -938,13 +1020,21 @@ var createTaskManager = (projectRoot) => {
938
1020
  updatedAt: Date.now()
939
1021
  };
940
1022
  tasks.set(task.id, task);
941
- persist();
1023
+ schedulePersist();
942
1024
  return task;
943
1025
  };
944
1026
  const update = (taskID, input) => {
945
1027
  const task = tasks.get(taskID);
946
1028
  if (!task)
947
1029
  return { error: `Task ${taskID} not found` };
1030
+ if (input.status && (input.status === "in_progress" || input.status === "claimed") && task.dependencies.length > 0) {
1031
+ const { met, unmet } = areDependenciesMet(taskID);
1032
+ if (!met) {
1033
+ return {
1034
+ error: `Cannot transition ${taskID} to ${input.status}: unmet dependencies [${unmet.join(", ")}]`
1035
+ };
1036
+ }
1037
+ }
948
1038
  if (input.status !== undefined)
949
1039
  task.status = input.status;
950
1040
  if (input.result !== undefined)
@@ -952,7 +1042,13 @@ var createTaskManager = (projectRoot) => {
952
1042
  if (input.assignedAgent !== undefined)
953
1043
  task.assignedAgent = input.assignedAgent;
954
1044
  task.updatedAt = Date.now();
955
- persist();
1045
+ schedulePersist();
1046
+ return task;
1047
+ };
1048
+ const get = (taskID) => {
1049
+ const task = tasks.get(taskID);
1050
+ if (!task)
1051
+ return { error: `Task ${taskID} not found` };
956
1052
  return task;
957
1053
  };
958
1054
  const list = (filter) => {
@@ -967,7 +1063,25 @@ var createTaskManager = (projectRoot) => {
967
1063
  return true;
968
1064
  });
969
1065
  };
970
- return { create, update, list, load };
1066
+ const getActiveSummary = () => {
1067
+ const active = [...tasks.values()].filter((t) => t.status === "pending" || t.status === "claimed" || t.status === "in_progress");
1068
+ if (active.length === 0)
1069
+ return "";
1070
+ const lines = active.slice(0, 10).map((t) => `- [${t.status}] ${t.title}${t.assignedAgent ? ` (${t.assignedAgent})` : ""}`);
1071
+ return `Active tasks (${active.length}):
1072
+ ${lines.join(`
1073
+ `)}`;
1074
+ };
1075
+ const linkSession = (taskID, sessionID) => {
1076
+ const task = tasks.get(taskID);
1077
+ if (!task)
1078
+ return false;
1079
+ task.sessionID = sessionID;
1080
+ task.updatedAt = Date.now();
1081
+ schedulePersist();
1082
+ return true;
1083
+ };
1084
+ return { create, update, get, list, load, getActiveSummary, linkSession };
971
1085
  };
972
1086
 
973
1087
  // src/opencode-multiagent/telemetry.ts
@@ -992,11 +1106,13 @@ var createTelemetryController = ({ flags }) => {
992
1106
  filesEdited: 0,
993
1107
  tasksDispatched: 0,
994
1108
  permissionDenied: 0,
1109
+ flushed: false,
995
1110
  ...extra
996
1111
  });
997
1112
  }
998
1113
  const state = tracker.entries.get(sessionID);
999
- Object.assign(state, extra);
1114
+ const safeExtra = Object.fromEntries(Object.entries(extra).filter(([, v]) => v !== undefined));
1115
+ Object.assign(state, safeExtra);
1000
1116
  state.lastActivityAt = now;
1001
1117
  return state;
1002
1118
  };
@@ -1038,8 +1154,9 @@ var createTelemetryController = ({ flags }) => {
1038
1154
  },
1039
1155
  async flushSession(sessionID, reason = "session_deleted") {
1040
1156
  const state = tracker.entries.get(sessionID);
1041
- if (!state)
1157
+ if (!state || state.flushed)
1042
1158
  return;
1159
+ state.flushed = true;
1043
1160
  tracker.entries.delete(sessionID);
1044
1161
  await note("session_metrics", {
1045
1162
  observation: true,
@@ -1054,13 +1171,19 @@ var createTelemetryController = ({ flags }) => {
1054
1171
  permission_denied: state.permissionDenied,
1055
1172
  reason
1056
1173
  });
1174
+ },
1175
+ async flushAll() {
1176
+ const ids = [...tracker.entries.keys()];
1177
+ for (const id of ids) {
1178
+ await this.flushSession(id, "cleanup");
1179
+ }
1057
1180
  }
1058
1181
  };
1059
1182
  };
1060
1183
 
1061
1184
  // src/opencode-multiagent/tools.ts
1062
1185
  import { tool as pluginTool } from "@opencode-ai/plugin";
1063
- var createTaskTools = (taskManager, taskManagerReady) => ({
1186
+ var createTaskTools = (taskManager, taskManagerReady, correlation, quality, flags) => ({
1064
1187
  task_create: pluginTool({
1065
1188
  description: "Create a new task on the shared task board",
1066
1189
  args: {
@@ -1075,18 +1198,84 @@ var createTaskTools = (taskManager, taskManagerReady) => ({
1075
1198
  return JSON.stringify(taskManager.create({ ...args, createdBy: ctx.agent }));
1076
1199
  }
1077
1200
  }),
1201
+ task_dispatch: pluginTool({
1202
+ 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.",
1203
+ args: {
1204
+ taskID: pluginTool.schema.string().describe("ID of the task being dispatched"),
1205
+ notes: pluginTool.schema.string().optional().describe("Optional dispatch notes or context")
1206
+ },
1207
+ async execute(args, ctx) {
1208
+ await taskManagerReady;
1209
+ const task = taskManager.get(args.taskID);
1210
+ if ("error" in task)
1211
+ return JSON.stringify(task);
1212
+ const updated = taskManager.update(args.taskID, { status: "in_progress" });
1213
+ if ("error" in updated)
1214
+ return JSON.stringify(updated);
1215
+ if (correlation) {
1216
+ const parentSessionID = ctx.sessionID ?? "";
1217
+ const targetAgent = task.assignedAgent ?? "unknown";
1218
+ correlation.recordIntent(parentSessionID, args.taskID, targetAgent);
1219
+ }
1220
+ await note("task_dispatch_intent", {
1221
+ taskID: args.taskID,
1222
+ agent: ctx.agent,
1223
+ notes: args.notes
1224
+ });
1225
+ return JSON.stringify({
1226
+ ...updated,
1227
+ dispatch: "intent_recorded",
1228
+ notes: args.notes
1229
+ });
1230
+ }
1231
+ }),
1078
1232
  task_update: pluginTool({
1079
- description: "Update a task's status or result on the shared task board",
1233
+ 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
1234
  args: {
1081
1235
  taskID: pluginTool.schema.string().describe("ID of the task to update"),
1082
1236
  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")
1237
+ result: pluginTool.schema.string().optional().describe("Result summary or notes"),
1238
+ force: pluginTool.schema.boolean().optional().describe("Force completion even without quality evidence (bypass quality gate)")
1084
1239
  },
1085
- async execute(args) {
1240
+ async execute(args, ctx) {
1086
1241
  await taskManagerReady;
1242
+ if (flags?.quality_gate_enforcement && args.status === "completed" && !args.force && quality) {
1243
+ const link = correlation?.getLinkByTask(args.taskID);
1244
+ if (link) {
1245
+ const evidence = quality.hasQualityEvidence(link.childSessionID);
1246
+ if (!evidence.passed) {
1247
+ await note("quality_gate_blocked", {
1248
+ taskID: args.taskID,
1249
+ sessionID: link.childSessionID,
1250
+ reason: evidence.reason
1251
+ });
1252
+ return JSON.stringify({
1253
+ error: `Quality gate: ${evidence.reason}. Run verification commands (test/lint/build) before completing, or pass force: true to bypass.`,
1254
+ taskID: args.taskID,
1255
+ quality_gate: "blocked"
1256
+ });
1257
+ }
1258
+ }
1259
+ }
1260
+ if (args.force && args.status === "completed") {
1261
+ await note("quality_gate_bypassed", {
1262
+ taskID: args.taskID,
1263
+ agent: ctx.agent
1264
+ });
1265
+ }
1087
1266
  return JSON.stringify(taskManager.update(args.taskID, { status: args.status, result: args.result }));
1088
1267
  }
1089
1268
  }),
1269
+ task_get: pluginTool({
1270
+ description: "Get a single task by ID from the shared task board",
1271
+ args: {
1272
+ taskID: pluginTool.schema.string().describe("ID of the task to retrieve")
1273
+ },
1274
+ async execute(args) {
1275
+ await taskManagerReady;
1276
+ return JSON.stringify(taskManager.get(args.taskID));
1277
+ }
1278
+ }),
1090
1279
  task_list: pluginTool({
1091
1280
  description: "List tasks on the shared task board with optional filters",
1092
1281
  args: {
@@ -1119,17 +1308,18 @@ var createPluginHooks = ({
1119
1308
  }) => {
1120
1309
  const projectAgentsDir = projectRoot ? join4(projectRoot, ".opencode", "agents") : undefined;
1121
1310
  const projectCommandsDir = projectRoot ? join4(projectRoot, ".opencode", "commands") : undefined;
1122
- const { handleSupervision, cleanup } = createSupervisionController({ flags, client });
1311
+ const supervision = createSupervisionController({ flags, client });
1312
+ const { handleSupervision, cleanup, getChildInfo } = supervision;
1123
1313
  const quality = createQualityController({ flags, client });
1124
1314
  const telemetry = createTelemetryController({ flags });
1125
- const fileLocks = createFileLockController();
1315
+ const correlation = createCorrelationController();
1126
1316
  const sessionAgentMap = new Map;
1127
1317
  const qaDispatchState = new Map;
1128
1318
  let mcpDefaults = null;
1129
1319
  let mcpRegistry = null;
1320
+ let taskRoutingRegistry = null;
1130
1321
  const taskManager = createTaskManager(projectRoot);
1131
1322
  const taskManagerReady = taskManager.load();
1132
- const childSessionInfo = new Map;
1133
1323
  const rememberSession = (sessionID, extra = {}) => {
1134
1324
  if (!sessionID)
1135
1325
  return;
@@ -1158,7 +1348,7 @@ var createPluginHooks = ({
1158
1348
  parts: [
1159
1349
  {
1160
1350
  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."
1351
+ 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
1352
  }
1163
1353
  ]
1164
1354
  }
@@ -1174,9 +1364,10 @@ var createPluginHooks = ({
1174
1364
  qa_dispatch_count: state.count
1175
1365
  });
1176
1366
  };
1177
- const notifyParentOnChildCompletion = async (childSessionID, parentSessionID, agentName) => {
1367
+ const notifyParentOnChildCompletion = async (childSessionID, parentSessionID, agentName, taskLabel) => {
1178
1368
  const agentLabel = agentName ? ` (agent: ${agentName})` : "";
1179
- const text2 = `Child session ${childSessionID}${agentLabel} has completed. ` + "Review its output and proceed with the next task.";
1369
+ const taskInfo = taskLabel ? `, task ${taskLabel}` : "";
1370
+ const text2 = `Child session ${childSessionID}${agentLabel}${taskInfo} has completed. ` + "Review its output and proceed with the next task.";
1180
1371
  try {
1181
1372
  if (client.session?.prompt) {
1182
1373
  await client.session.prompt({
@@ -1202,13 +1393,31 @@ var createPluginHooks = ({
1202
1393
  return;
1203
1394
  }
1204
1395
  };
1396
+ const loadActivePlanSummary = async () => {
1397
+ if (!projectRoot)
1398
+ return "";
1399
+ const plansDir = join4(projectRoot, ".magent", "plans");
1400
+ try {
1401
+ const entries = await readdir2(plansDir);
1402
+ const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
1403
+ if (mdFiles.length === 0)
1404
+ return "";
1405
+ const lastPlan = mdFiles[mdFiles.length - 1];
1406
+ const content = await readFile5(join4(plansDir, lastPlan), "utf8");
1407
+ const objectiveMatch = content.match(/##\s+Objective\s*\n([\s\S]*?)(?=\n##|\s*$)/);
1408
+ const objective = objectiveMatch ? objectiveMatch[1].trim().split(`
1409
+ `)[0].slice(0, 200) : "";
1410
+ return objective ? `Active plan: ${lastPlan} \u2014 ${objective}` : `Active plan: ${lastPlan}`;
1411
+ } catch {
1412
+ return "";
1413
+ }
1414
+ };
1205
1415
  return {
1206
- cleanup() {
1416
+ async cleanup() {
1417
+ await telemetry.flushAll();
1207
1418
  cleanup?.();
1208
1419
  quality.cleanup?.();
1209
1420
  telemetry.cleanup?.();
1210
- fileLocks.cleanup?.();
1211
- childSessionInfo.clear();
1212
1421
  },
1213
1422
  async event(input) {
1214
1423
  const type = input.event?.type;
@@ -1233,35 +1442,65 @@ var createPluginHooks = ({
1233
1442
  if (parentID && childID) {
1234
1443
  const parent = sessionAgentMap.get(parentID) ?? {};
1235
1444
  telemetry.trackTaskDispatch(parentID, { agent: parent.agent, model: parent.model });
1445
+ const agentHint = typeof props.info?.agent === "string" ? props.info.agent : undefined;
1446
+ const link = correlation.tryCorrelate(parentID, childID, agentHint);
1447
+ if (link) {
1448
+ taskManager.linkSession(link.taskID, childID);
1449
+ await note("task_correlation", {
1450
+ observation: true,
1451
+ taskID: link.taskID,
1452
+ parentSessionID: parentID,
1453
+ childSessionID: childID,
1454
+ agentHint
1455
+ });
1456
+ }
1236
1457
  await note("task_dispatch", {
1237
1458
  observation: true,
1238
1459
  parent_sessionID: parentID,
1239
1460
  parent_agent: parent.agent,
1240
1461
  child_sessionID: childID
1241
1462
  });
1242
- childSessionInfo.set(childID, { parentID, agentName: null });
1243
1463
  }
1244
1464
  } else if (type === "message.updated") {
1245
1465
  const sessionID = typeof props.info?.sessionID === "string" ? props.info.sessionID : undefined;
1246
1466
  const agent = typeof props.info?.agent === "string" ? props.info.agent : undefined;
1247
1467
  if (sessionID && agent) {
1248
1468
  rememberSession(sessionID, { agent });
1249
- const childInfo = childSessionInfo.get(sessionID);
1250
- if (childInfo)
1251
- childInfo.agentName = agent;
1252
1469
  }
1253
1470
  } else if (type === "session.deleted") {
1254
1471
  const sessionID = typeof props.info?.id === "string" ? props.info.id : typeof props.sessionID === "string" ? props.sessionID : undefined;
1255
1472
  if (sessionID) {
1256
- const childInfo = childSessionInfo.get(sessionID);
1473
+ const childInfo = getChildInfo(sessionID);
1257
1474
  sessionAgentMap.delete(sessionID);
1258
1475
  qaDispatchState.delete(sessionID);
1259
- fileLocks.releaseAll(sessionID);
1260
1476
  await telemetry.flushSession(sessionID);
1477
+ let taskLabel;
1478
+ const link = correlation.getLink(sessionID);
1479
+ if (link && flags.task_lifecycle) {
1480
+ const recentErrorMs = 30000;
1481
+ const hasRecentError = childInfo?.lastToolErrorAt != null && childInfo.lastToolErrorAt > 0 && Date.now() - childInfo.lastToolErrorAt < recentErrorMs;
1482
+ const autoStatus = hasRecentError ? "failed" : "completed";
1483
+ const autoResult = hasRecentError ? "Auto-failed: tool error detected near session end" : "Auto-completed: session ended without recent errors";
1484
+ await taskManagerReady;
1485
+ taskManager.update(link.taskID, { status: autoStatus, result: autoResult });
1486
+ taskLabel = link.taskID;
1487
+ await note("task_lifecycle_auto", {
1488
+ observation: true,
1489
+ taskID: link.taskID,
1490
+ childSessionID: sessionID,
1491
+ autoStatus
1492
+ });
1493
+ }
1494
+ if (link)
1495
+ correlation.removeLink(sessionID);
1261
1496
  if (childInfo?.parentID) {
1262
- await notifyParentOnChildCompletion(sessionID, childInfo.parentID, childInfo.agentName);
1497
+ await notifyParentOnChildCompletion(sessionID, childInfo.parentID, childInfo.agentName, taskLabel);
1263
1498
  }
1264
- childSessionInfo.delete(sessionID);
1499
+ }
1500
+ } else if (type === "session.idle") {
1501
+ const sessionID = typeof props.sessionID === "string" ? props.sessionID : typeof props.info?.id === "string" ? props.info.id : undefined;
1502
+ if (sessionID) {
1503
+ await telemetry.flushSession(sessionID, "session_idle");
1265
1504
  }
1266
1505
  }
1267
1506
  await handleSupervision(input.event);
@@ -1319,12 +1558,13 @@ var createPluginHooks = ({
1319
1558
  return;
1320
1559
  Object.assign(output.headers, {
1321
1560
  "x-opencode-multiagent": "1",
1322
- "x-opencode-multiagent-profile": flags.profile ?? "standard",
1561
+ "x-opencode-multiagent-profile": pluginMode,
1323
1562
  "x-opencode-agent": label(input.agent) ?? "unknown"
1324
1563
  });
1325
1564
  },
1326
1565
  async "chat.message"(input, output) {
1327
- if (!flags.prompt_controls || !risky(text(output.parts)))
1566
+ const userParts = Array.isArray(output.parts) ? output.parts.filter((p) => p?.type === "text" && p?.role !== "tool") : output.parts;
1567
+ if (!flags.prompt_controls || !risky(text(userParts)))
1328
1568
  return;
1329
1569
  await note("chat_risk", compact({
1330
1570
  observation: true,
@@ -1363,9 +1603,22 @@ var createPluginHooks = ({
1363
1603
  async "experimental.session.compacting"(_input, output) {
1364
1604
  if (flags.experimental?.session_compacting !== true)
1365
1605
  return;
1366
- if (output.context.includes(experimentalText))
1367
- return;
1368
- output.context.push(experimentalText);
1606
+ const parts = [];
1607
+ const planSummary = await loadActivePlanSummary();
1608
+ if (planSummary)
1609
+ parts.push(planSummary);
1610
+ await taskManagerReady;
1611
+ const taskSummary = taskManager.getActiveSummary();
1612
+ if (taskSummary)
1613
+ parts.push(taskSummary);
1614
+ if (parts.length > 0) {
1615
+ output.context.push(`[opencode-multiagent workflow state]
1616
+ ${parts.join(`
1617
+
1618
+ `)}`);
1619
+ } else if (!output.context.includes(experimentalText)) {
1620
+ output.context.push(experimentalText);
1621
+ }
1369
1622
  },
1370
1623
  async "experimental.text.complete"(_input, output) {
1371
1624
  if (flags.experimental?.text_complete !== true)
@@ -1378,7 +1631,9 @@ ${experimentalText}`;
1378
1631
  async config(sdkCfg) {
1379
1632
  const cfg = sdkCfg;
1380
1633
  if (flags.agent_compilation) {
1381
- mcpRegistry = await compileAgents(cfg, [bundledAgentsDir, globalAgentsDir, projectAgentsDir], agentSettings);
1634
+ const result = await compileAgents(cfg, [bundledAgentsDir, globalAgentsDir, projectAgentsDir], agentSettings);
1635
+ mcpRegistry = result.mcpRegistry;
1636
+ taskRoutingRegistry = result.taskRouting;
1382
1637
  }
1383
1638
  if (flags.command_compilation) {
1384
1639
  await compileCommands(cfg, [bundledCommandsDir, globalCommandsDir, projectCommandsDir]);
@@ -1396,23 +1651,24 @@ ${experimentalText}`;
1396
1651
  const agentInfo = sessionAgentMap.get(input.sessionID) ?? {};
1397
1652
  const agentName = agentInfo.agent;
1398
1653
  if (input.tool === "task") {
1654
+ const limit = flags.concurrency_limit;
1655
+ if (typeof limit === "number" && limit > 0 && input.sessionID) {
1656
+ const activeCount = supervision.getActiveChildCount(input.sessionID);
1657
+ if (activeCount >= limit) {
1658
+ await note("concurrency_blocked", {
1659
+ observation: true,
1660
+ sessionID: input.sessionID,
1661
+ activeCount,
1662
+ limit
1663
+ });
1664
+ throw new Error(`[opencode-multiagent] Concurrency limit reached: ${activeCount}/${limit} active child sessions. ` + "Wait for existing sessions to complete before dispatching new work.");
1665
+ }
1666
+ }
1399
1667
  const targetAgent = getTaskTarget(output.args ?? input.args ?? {});
1400
- if (targetAgent === "qa") {
1668
+ if (targetAgent === "reviewer") {
1401
1669
  await trackQaDispatch(input.sessionID, agentInfo);
1402
1670
  }
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) {
1671
+ if (flags.enforcement && agentName && targetAgent && taskRoutingRegistry && !isTaskRoutingAllowed(agentName, targetAgent, taskRoutingRegistry)) {
1416
1672
  telemetry.trackPermissionDenied(input.sessionID, {
1417
1673
  agent: agentName,
1418
1674
  model: agentInfo.model
@@ -1422,15 +1678,23 @@ ${experimentalText}`;
1422
1678
  sessionID: input.sessionID,
1423
1679
  agent: agentName,
1424
1680
  model: agentInfo.model,
1425
- tool: input.tool,
1426
- file: lock.filePath,
1427
- reason: "file_locked_by_other_session",
1428
- owner_sessionID: lock.ownerSessionID,
1681
+ tool: "task",
1682
+ target: targetAgent,
1683
+ reason: "task_routing_not_allowed",
1429
1684
  enforcement_layer: "plugin_hook"
1430
1685
  });
1431
- throw new Error(`[opencode-multiagent] File lock conflict for ${lock.filePath}; active session ${lock.ownerSessionID}`);
1686
+ throw new Error(`[opencode-multiagent] Agent ${agentName} is not allowed to delegate to ${targetAgent}`);
1432
1687
  }
1433
1688
  }
1689
+ if (input.tool === "webfetch" && typeof output.args?.url === "string" && output.args.url.startsWith("http://")) {
1690
+ output.args.url = output.args.url.replace("http://", "https://");
1691
+ }
1692
+ if (flags.enforcement && ["read", "edit"].includes(input.tool) && typeof output.args?.filePath === "string" && blocked(output.args.filePath)) {
1693
+ throw new Error("[opencode-multiagent] blocked sensitive path access");
1694
+ }
1695
+ if (flags.enforcement && input.tool === "bash" && typeof output.args?.command === "string" && tokenizedBashBlocked(output.args.command)) {
1696
+ throw new Error("[opencode-multiagent] blocked destructive bash command");
1697
+ }
1434
1698
  if (!flags.enforcement)
1435
1699
  return;
1436
1700
  if (isMcpTool(input.tool) && agentName && mcpRegistry?.has(agentName)) {
@@ -1483,7 +1747,7 @@ ${experimentalText}`;
1483
1747
  Object.assign(output.env, {
1484
1748
  OPENCODE_MULTIAGENT: "1",
1485
1749
  OPENCODE_MULTIAGENT_MODE: pluginMode,
1486
- OPENCODE_MULTIAGENT_PROFILE: flags.profile ?? "standard",
1750
+ OPENCODE_MULTIAGENT_PROFILE: pluginMode,
1487
1751
  OPENCODE_CONTROL_PLANE: "1"
1488
1752
  });
1489
1753
  },
@@ -1491,7 +1755,6 @@ ${experimentalText}`;
1491
1755
  if (input.tool === "edit" && typeof input.args?.filePath === "string") {
1492
1756
  quality.trackEdit(input.sessionID, input.args.filePath);
1493
1757
  telemetry.trackEdit(input.sessionID);
1494
- fileLocks.acquire(input.sessionID, input.args.filePath);
1495
1758
  }
1496
1759
  if (input.tool === "bash" && typeof input.args?.command === "string") {
1497
1760
  quality.recordQualityEvidence(input.sessionID, input.args.command);
@@ -1504,6 +1767,7 @@ ${experimentalText}`;
1504
1767
  agent: agentInfo.agent,
1505
1768
  model: agentInfo.model
1506
1769
  });
1770
+ supervision.recordToolError(input.sessionID);
1507
1771
  }
1508
1772
  if (!flags.observation)
1509
1773
  return;
@@ -1525,37 +1789,26 @@ ${String(output.output ?? "")}`.toLowerCase();
1525
1789
  ...isNonzeroExit ? { exit_code: output.metadata.exit, error_type: "nonzero_exit" } : {}
1526
1790
  }));
1527
1791
  },
1528
- tool: createTaskTools(taskManager, taskManagerReady)
1792
+ tool: createTaskTools(taskManager, taskManagerReady, correlation, quality, flags)
1529
1793
  };
1530
1794
  };
1531
1795
 
1532
1796
  // src/opencode-multiagent/runtime.ts
1533
- var readSettingsSection = (value) => {
1534
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1535
- return {};
1536
- }
1537
- return value;
1538
- };
1539
1797
  async function loadRuntimeSettings() {
1540
- const [bundledDefaults, unifiedUserSettings] = await Promise.all([
1798
+ const [bundledDefaults, userSettings] = await Promise.all([
1541
1799
  loadBundledDefaults().catch(() => ({})),
1542
1800
  readJSON(settingsPath, {})
1543
1801
  ]);
1544
1802
  const defaultAgentSettings = readBundledDefaultsSection(bundledDefaults, "agentSettings", {});
1545
- const userFlags = readSettingsSection(unifiedUserSettings.flags);
1546
- const userAgentSettings = readSettingsSection(unifiedUserSettings.agentSettings);
1547
- const userProfiles = readSettingsSection(unifiedUserSettings.profiles);
1548
- const profiles = merge(defaultProfiles, userProfiles);
1549
- const profileName = typeof userFlags.profile === "string" ? userFlags.profile : defaultFlags.profile;
1550
- const profile = profiles[profileName] ?? {};
1551
- if (!(profileName in profiles)) {
1552
- await note("config_warning", {
1553
- observation: true,
1554
- warning: "unknown_profile",
1555
- profile: profileName
1556
- });
1803
+ const userFlagOverrides = {};
1804
+ if (typeof userSettings.telemetry === "boolean") {
1805
+ userFlagOverrides.telemetry = userSettings.telemetry;
1806
+ }
1807
+ if (typeof userSettings.concurrency_limit === "number") {
1808
+ userFlagOverrides.concurrency_limit = userSettings.concurrency_limit;
1557
1809
  }
1558
- const flags = merge(merge(defaultFlags, profile), userFlags);
1810
+ const flags = merge(defaultFlags, userFlagOverrides);
1811
+ const userAgentSettings = userSettings.agentSettings ?? {};
1559
1812
  const agentSettings = merge(defaultAgentSettings, userAgentSettings);
1560
1813
  return { flags, agentSettings };
1561
1814
  }