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.
- package/AGENTS.md +21 -0
- package/CHANGELOG.md +13 -0
- package/README.md +4 -4
- package/README.tr.md +4 -4
- package/agents/AGENTS.md +91 -0
- package/agents/auditor.md +59 -17
- package/agents/{worker.md → coder.md} +12 -10
- package/agents/{scribe.md → docmaster.md} +16 -8
- package/agents/executor.md +45 -62
- package/agents/planner.md +59 -47
- package/agents/reviewer.md +22 -9
- package/agents/scout.md +16 -12
- package/agents/sec-coder.md +83 -0
- package/agents/ui-coder.md +77 -0
- package/commands/board.md +17 -0
- package/commands/execute.md +8 -7
- package/commands/init-deep.md +6 -6
- package/commands/init.md +4 -5
- package/commands/inspect.md +5 -5
- package/commands/plan.md +7 -6
- package/commands/quality.md +3 -3
- package/commands/review.md +4 -3
- package/commands/status.md +4 -3
- package/defaults/AGENTS.md +48 -0
- package/defaults/opencode-multiagent.json +24 -67
- package/defaults/opencode-multiagent.schema.json +16 -0
- package/dist/index.js +464 -131
- package/dist/opencode-multiagent/compiler.d.ts +8 -2
- package/dist/opencode-multiagent/compiler.d.ts.map +1 -1
- package/dist/opencode-multiagent/constants.d.ts +12 -0
- package/dist/opencode-multiagent/constants.d.ts.map +1 -1
- package/dist/opencode-multiagent/correlation.d.ts +21 -0
- package/dist/opencode-multiagent/correlation.d.ts.map +1 -0
- package/dist/opencode-multiagent/hooks.d.ts.map +1 -1
- package/dist/opencode-multiagent/log.d.ts.map +1 -1
- package/dist/opencode-multiagent/quality.d.ts +4 -0
- package/dist/opencode-multiagent/quality.d.ts.map +1 -1
- package/dist/opencode-multiagent/supervision.d.ts +14 -0
- package/dist/opencode-multiagent/supervision.d.ts.map +1 -1
- package/dist/opencode-multiagent/task-manager.d.ts +8 -2
- package/dist/opencode-multiagent/task-manager.d.ts.map +1 -1
- package/dist/opencode-multiagent/telemetry.d.ts +2 -0
- package/dist/opencode-multiagent/telemetry.d.ts.map +1 -1
- package/dist/opencode-multiagent/tools.d.ts +32 -1
- package/dist/opencode-multiagent/tools.d.ts.map +1 -1
- package/docs/agents.md +67 -179
- package/docs/agents.tr.md +68 -179
- package/docs/configuration.md +14 -25
- package/docs/configuration.tr.md +14 -25
- package/docs/usage-guide.md +31 -33
- package/docs/usage-guide.tr.md +31 -33
- package/examples/opencode.with-overrides.json +2 -2
- package/package.json +1 -1
- package/skills/AGENTS.md +51 -0
- package/skills/advanced-evaluation/manifest.json +1 -1
- package/skills/cek-context-engineering/manifest.json +1 -1
- package/skills/cek-prompt-engineering/manifest.json +1 -1
- package/skills/cek-test-prompt/manifest.json +1 -1
- package/skills/cek-thought-based-reasoning/manifest.json +1 -1
- package/skills/context-degradation/manifest.json +1 -1
- package/skills/debate/manifest.json +1 -1
- package/skills/design-first/manifest.json +1 -1
- package/skills/dispatching-parallel-agents/manifest.json +1 -1
- package/skills/drift-analysis/manifest.json +1 -1
- package/skills/evaluation/manifest.json +1 -1
- package/skills/parallel-investigation/manifest.json +1 -1
- package/skills/reflexion-critique/manifest.json +1 -1
- package/skills/reflexion-reflect/manifest.json +1 -1
- package/skills/root-cause-analysis/manifest.json +1 -1
- package/skills/sadd-judge-with-debate/manifest.json +1 -1
- package/skills/structured-code-review/manifest.json +1 -1
- package/skills/task-decomposition/manifest.json +1 -1
- package/skills/verification-before-completion/manifest.json +1 -1
- package/skills/verification-gates/manifest.json +1 -1
- package/agents/advisor.md +0 -60
- package/agents/critic.md +0 -136
- package/agents/deep-worker.md +0 -69
- package/agents/devil.md +0 -38
- package/agents/heavy-worker.md +0 -72
- package/agents/lead.md +0 -147
- package/agents/librarian.md +0 -66
- package/agents/qa.md +0 -53
- package/agents/quick.md +0 -70
- package/agents/strategist.md +0 -66
- package/agents/ui-heavy-worker.md +0 -66
- package/agents/ui-worker.md +0 -74
- package/agents/validator.md +0 -50
- package/dist/opencode-multiagent/file-lock.d.ts +0 -15
- 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
|
|
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
|
-
|
|
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.
|
|
450
|
-
const invalidCurrent = !current || disabledNativeAgents.includes(current) || !currentAgent || currentAgent.disable === true || currentAgent.hidden === true || currentAgent.mode === "subagent" || currentAgent.mode === "sub" || preferred === "
|
|
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/
|
|
497
|
-
var
|
|
498
|
-
var
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
521
|
-
|
|
551
|
+
intentQueues.get(parentSessionID).push({
|
|
552
|
+
taskID,
|
|
553
|
+
parentSessionID,
|
|
554
|
+
targetAgent,
|
|
555
|
+
createdAt: Date.now()
|
|
556
|
+
});
|
|
522
557
|
};
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
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(),
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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, ".
|
|
909
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
-
|
|
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 === "
|
|
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:
|
|
1426
|
-
|
|
1427
|
-
reason: "
|
|
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]
|
|
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
|
|