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.
- package/AGENTS.md +21 -0
- package/CHANGELOG.md +25 -0
- package/README.md +4 -4
- package/README.tr.md +4 -4
- package/agents/AGENTS.md +95 -0
- package/agents/auditor.md +59 -17
- package/agents/brainstormer.md +113 -0
- 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/brainstorm-conclude.md +14 -0
- package/commands/brainstorm.md +14 -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 +16 -150
- package/defaults/opencode-multiagent.schema.json +16 -190
- package/dist/index.js +471 -218
- 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 +3 -57
- 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/defaults.d.ts +0 -2
- package/dist/opencode-multiagent/defaults.d.ts.map +1 -1
- package/dist/opencode-multiagent/hooks.d.ts.map +1 -1
- package/dist/opencode-multiagent/log.d.ts.map +1 -1
- package/dist/opencode-multiagent/markdown.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/runtime.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 +77 -175
- package/docs/agents.tr.md +78 -175
- package/docs/configuration.md +17 -27
- package/docs/configuration.tr.md +17 -27
- package/docs/usage-guide.md +35 -34
- package/docs/usage-guide.tr.md +36 -35
- 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
|
]);
|
|
@@ -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:
|
|
156
|
-
chat_messages_transform:
|
|
157
|
-
session_compacting:
|
|
158
|
-
text_complete:
|
|
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
|
|
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
|
-
|
|
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.
|
|
450
|
-
const invalidCurrent = !current || disabledNativeAgents.includes(current) || !currentAgent || currentAgent.disable === true || currentAgent.hidden === true || currentAgent.mode === "subagent" || currentAgent.mode === "sub" || preferred === "
|
|
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/
|
|
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
|
-
}
|
|
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
|
-
|
|
521
|
-
|
|
482
|
+
intentQueues.get(parentSessionID).push({
|
|
483
|
+
taskID,
|
|
484
|
+
parentSessionID,
|
|
485
|
+
targetAgent,
|
|
486
|
+
createdAt: Date.now()
|
|
487
|
+
});
|
|
522
488
|
};
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
|
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(),
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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, ".
|
|
909
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
-
|
|
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 === "
|
|
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:
|
|
1426
|
-
|
|
1427
|
-
reason: "
|
|
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]
|
|
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:
|
|
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,
|
|
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
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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(
|
|
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
|
}
|