opencode-multiagent 0.2.0 → 0.3.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +18 -0
  3. package/CONTRIBUTING.md +36 -0
  4. package/README.md +41 -165
  5. package/README.tr.md +84 -0
  6. package/RELEASE.md +68 -0
  7. package/agents/advisor.md +9 -6
  8. package/agents/auditor.md +8 -6
  9. package/agents/critic.md +19 -10
  10. package/agents/deep-worker.md +11 -7
  11. package/agents/devil.md +3 -1
  12. package/agents/executor.md +20 -19
  13. package/agents/heavy-worker.md +11 -7
  14. package/agents/lead.md +22 -30
  15. package/agents/librarian.md +6 -2
  16. package/agents/planner.md +18 -10
  17. package/agents/qa.md +9 -6
  18. package/agents/quick.md +12 -7
  19. package/agents/reviewer.md +9 -6
  20. package/agents/scout.md +9 -5
  21. package/agents/scribe.md +33 -28
  22. package/agents/strategist.md +10 -7
  23. package/agents/ui-heavy-worker.md +11 -7
  24. package/agents/ui-worker.md +12 -7
  25. package/agents/validator.md +8 -5
  26. package/agents/worker.md +12 -7
  27. package/commands/execute.md +1 -0
  28. package/commands/init-deep.md +1 -0
  29. package/commands/init.md +1 -0
  30. package/commands/inspect.md +1 -0
  31. package/commands/plan.md +1 -0
  32. package/commands/quality.md +1 -0
  33. package/commands/review.md +1 -0
  34. package/commands/status.md +1 -0
  35. package/defaults/opencode-multiagent.json +223 -0
  36. package/defaults/opencode-multiagent.schema.json +249 -0
  37. package/dist/control-plane.d.ts +4 -0
  38. package/dist/control-plane.d.ts.map +1 -0
  39. package/dist/index.d.ts +5 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +1583 -0
  42. package/dist/opencode-multiagent/compiler.d.ts +19 -0
  43. package/dist/opencode-multiagent/compiler.d.ts.map +1 -0
  44. package/dist/opencode-multiagent/constants.d.ts +116 -0
  45. package/dist/opencode-multiagent/constants.d.ts.map +1 -0
  46. package/dist/opencode-multiagent/defaults.d.ts +10 -0
  47. package/dist/opencode-multiagent/defaults.d.ts.map +1 -0
  48. package/dist/opencode-multiagent/file-lock.d.ts +15 -0
  49. package/dist/opencode-multiagent/file-lock.d.ts.map +1 -0
  50. package/dist/opencode-multiagent/hooks.d.ts +62 -0
  51. package/dist/opencode-multiagent/hooks.d.ts.map +1 -0
  52. package/dist/opencode-multiagent/log.d.ts +2 -0
  53. package/dist/opencode-multiagent/log.d.ts.map +1 -0
  54. package/dist/opencode-multiagent/markdown.d.ts +8 -0
  55. package/dist/opencode-multiagent/markdown.d.ts.map +1 -0
  56. package/dist/opencode-multiagent/mcp.d.ts +3 -0
  57. package/dist/opencode-multiagent/mcp.d.ts.map +1 -0
  58. package/dist/opencode-multiagent/policy.d.ts +5 -0
  59. package/dist/opencode-multiagent/policy.d.ts.map +1 -0
  60. package/dist/opencode-multiagent/quality.d.ts +14 -0
  61. package/dist/opencode-multiagent/quality.d.ts.map +1 -0
  62. package/dist/opencode-multiagent/runtime.d.ts +7 -0
  63. package/dist/opencode-multiagent/runtime.d.ts.map +1 -0
  64. package/dist/opencode-multiagent/session-tracker.d.ts +32 -0
  65. package/dist/opencode-multiagent/session-tracker.d.ts.map +1 -0
  66. package/dist/opencode-multiagent/skills.d.ts +17 -0
  67. package/dist/opencode-multiagent/skills.d.ts.map +1 -0
  68. package/dist/opencode-multiagent/supervision.d.ts +12 -0
  69. package/dist/opencode-multiagent/supervision.d.ts.map +1 -0
  70. package/dist/opencode-multiagent/task-manager.d.ts +48 -0
  71. package/dist/opencode-multiagent/task-manager.d.ts.map +1 -0
  72. package/dist/opencode-multiagent/telemetry.d.ts +26 -0
  73. package/dist/opencode-multiagent/telemetry.d.ts.map +1 -0
  74. package/dist/opencode-multiagent/tools.d.ts +56 -0
  75. package/dist/opencode-multiagent/tools.d.ts.map +1 -0
  76. package/dist/opencode-multiagent/types.d.ts +36 -0
  77. package/dist/opencode-multiagent/types.d.ts.map +1 -0
  78. package/dist/opencode-multiagent/utils.d.ts +9 -0
  79. package/dist/opencode-multiagent/utils.d.ts.map +1 -0
  80. package/docs/agents.md +260 -0
  81. package/docs/agents.tr.md +260 -0
  82. package/docs/configuration.md +255 -0
  83. package/docs/configuration.tr.md +255 -0
  84. package/docs/usage-guide.md +226 -0
  85. package/docs/usage-guide.tr.md +227 -0
  86. package/examples/opencode.with-overrides.json +1 -5
  87. package/package.json +23 -13
  88. package/skills/advanced-evaluation/SKILL.md +37 -21
  89. package/skills/advanced-evaluation/manifest.json +2 -13
  90. package/skills/cek-context-engineering/SKILL.md +159 -87
  91. package/skills/cek-context-engineering/manifest.json +1 -3
  92. package/skills/cek-prompt-engineering/SKILL.md +13 -10
  93. package/skills/cek-prompt-engineering/manifest.json +1 -3
  94. package/skills/cek-test-prompt/SKILL.md +38 -28
  95. package/skills/cek-test-prompt/manifest.json +1 -3
  96. package/skills/cek-thought-based-reasoning/SKILL.md +75 -21
  97. package/skills/cek-thought-based-reasoning/manifest.json +1 -3
  98. package/skills/context-degradation/SKILL.md +14 -13
  99. package/skills/context-degradation/manifest.json +1 -3
  100. package/skills/debate/SKILL.md +23 -78
  101. package/skills/debate/manifest.json +2 -12
  102. package/skills/design-first/manifest.json +2 -13
  103. package/skills/dispatching-parallel-agents/SKILL.md +14 -3
  104. package/skills/dispatching-parallel-agents/manifest.json +1 -4
  105. package/skills/drift-analysis/SKILL.md +50 -29
  106. package/skills/drift-analysis/manifest.json +2 -12
  107. package/skills/evaluation/manifest.json +2 -12
  108. package/skills/executing-plans/SKILL.md +15 -8
  109. package/skills/executing-plans/manifest.json +1 -3
  110. package/skills/handoff-protocols/manifest.json +2 -12
  111. package/skills/parallel-investigation/SKILL.md +25 -12
  112. package/skills/parallel-investigation/manifest.json +1 -4
  113. package/skills/reflexion-critique/SKILL.md +21 -10
  114. package/skills/reflexion-critique/manifest.json +1 -3
  115. package/skills/reflexion-reflect/SKILL.md +36 -34
  116. package/skills/reflexion-reflect/manifest.json +2 -10
  117. package/skills/root-cause-analysis/manifest.json +2 -13
  118. package/skills/sadd-judge-with-debate/SKILL.md +50 -26
  119. package/skills/sadd-judge-with-debate/manifest.json +1 -3
  120. package/skills/structured-code-review/manifest.json +2 -11
  121. package/skills/task-decomposition/manifest.json +2 -13
  122. package/skills/verification-before-completion/manifest.json +2 -15
  123. package/skills/verification-gates/SKILL.md +27 -19
  124. package/skills/verification-gates/manifest.json +2 -12
  125. package/defaults/agent-settings.json +0 -102
  126. package/defaults/agent-settings.schema.json +0 -25
  127. package/defaults/flags.json +0 -35
  128. package/defaults/flags.schema.json +0 -119
  129. package/defaults/mcp-defaults.json +0 -47
  130. package/defaults/mcp-defaults.schema.json +0 -38
  131. package/defaults/profiles.json +0 -53
  132. package/defaults/profiles.schema.json +0 -60
  133. package/defaults/team-profiles.json +0 -83
  134. package/src/control-plane.ts +0 -21
  135. package/src/index.ts +0 -8
  136. package/src/opencode-multiagent/compiler.ts +0 -168
  137. package/src/opencode-multiagent/constants.ts +0 -178
  138. package/src/opencode-multiagent/file-lock.ts +0 -90
  139. package/src/opencode-multiagent/hooks.ts +0 -599
  140. package/src/opencode-multiagent/log.ts +0 -12
  141. package/src/opencode-multiagent/mailbox.ts +0 -287
  142. package/src/opencode-multiagent/markdown.ts +0 -99
  143. package/src/opencode-multiagent/mcp.ts +0 -35
  144. package/src/opencode-multiagent/policy.ts +0 -67
  145. package/src/opencode-multiagent/quality.ts +0 -140
  146. package/src/opencode-multiagent/runtime.ts +0 -55
  147. package/src/opencode-multiagent/skills.ts +0 -144
  148. package/src/opencode-multiagent/supervision.ts +0 -156
  149. package/src/opencode-multiagent/task-manager.ts +0 -148
  150. package/src/opencode-multiagent/team-manager.ts +0 -219
  151. package/src/opencode-multiagent/team-tools.ts +0 -359
  152. package/src/opencode-multiagent/telemetry.ts +0 -124
  153. package/src/opencode-multiagent/utils.ts +0 -54
@@ -1,287 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
-
3
- import { note } from "./log.ts";
4
-
5
- /** A single inter-agent message stored in the mailbox. */
6
- export type Message = {
7
- id: string;
8
- /** Sender sessionID or agent name. */
9
- from: string;
10
- /** Target sessionID or agent name (used as inbox key). */
11
- to: string;
12
- content: string;
13
- timestamp: number;
14
- read: boolean;
15
- delivered: boolean;
16
- };
17
-
18
- /**
19
- * Minimal client interface required for mailbox delivery.
20
- *
21
- * Uses the typed SDK `session.prompt({ path, body })` single-object form.
22
- * Falls back to the runtime-only `session.promptAsync()` used elsewhere
23
- * in the plugin when the typed form is unavailable.
24
- */
25
- export type MailboxClient = {
26
- session?: {
27
- /**
28
- * Typed SDK method — single-object form:
29
- * `prompt({ path: { id }, body: { agent?, parts, noReply } })`
30
- *
31
- * `noReply: false` causes the LLM to process and act on the message.
32
- * `noReply: true` injects silently (used for system-level notes only).
33
- */
34
- prompt?: (input: {
35
- path: { id: string };
36
- body: {
37
- agent?: string;
38
- parts: Array<{ type: string; text?: string; name?: string }>;
39
- noReply?: boolean;
40
- };
41
- }) => Promise<unknown>;
42
- /** Runtime-only method used in supervision/quality controllers. */
43
- promptAsync?: (input: {
44
- sessionID: string;
45
- noReply?: boolean;
46
- parts: Array<{ type: string; text: string }>;
47
- }) => Promise<unknown>;
48
- };
49
- };
50
-
51
- /**
52
- * Creates an in-memory mailbox for inter-agent messaging.
53
- *
54
- * Messages are keyed by the `to` field (target sessionID/name).
55
- * Delivery is attempted via `client.session.prompt({ path, body })` (preferred)
56
- * or `client.session.promptAsync()` (fallback). Messages are delivered with
57
- * `noReply: false` so the receiving agent's LLM actively processes each message.
58
- *
59
- * @example
60
- * ```ts
61
- * const mailbox = createMailbox();
62
- * mailbox.send("lead-session-id", "worker-session-id", "Hello!");
63
- * await mailbox.deliverPending("worker-session-id", client);
64
- * ```
65
- */
66
- export const createMailbox = () => {
67
- /** inbox keyed by sessionID (or agent name used as `to`) */
68
- const messagesBySession = new Map<string, Message[]>();
69
- /** all messages by ID for fast mutation */
70
- const allMessages = new Map<string, Message>();
71
-
72
- const getInbox = (sessionID: string): Message[] => {
73
- if (!messagesBySession.has(sessionID)) messagesBySession.set(sessionID, []);
74
- return messagesBySession.get(sessionID)!;
75
- };
76
-
77
- /**
78
- * Enqueue a new message from `from` to `to`.
79
- * The message is stored undelivered and unread.
80
- */
81
- const send = (from: string, to: string, content: string): Message => {
82
- const msg: Message = {
83
- id: randomUUID(),
84
- from,
85
- to,
86
- content,
87
- timestamp: Date.now(),
88
- read: false,
89
- delivered: false,
90
- };
91
- getInbox(to).push(msg);
92
- allMessages.set(msg.id, msg);
93
- return msg;
94
- };
95
-
96
- /** Return all unread messages for `sessionID`. */
97
- const getUnread = (sessionID: string): Message[] =>
98
- (messagesBySession.get(sessionID) ?? []).filter((m) => !m.read);
99
-
100
- /** Return all messages (read + unread) for `sessionID`. */
101
- const getAll = (sessionID: string): Message[] =>
102
- [...(messagesBySession.get(sessionID) ?? [])];
103
-
104
- /** Return messages that have not yet been delivered to the session. */
105
- const getPending = (sessionID: string): Message[] =>
106
- (messagesBySession.get(sessionID) ?? []).filter((m) => !m.delivered);
107
-
108
- /** Mark a single message as read. */
109
- const markRead = (messageID: string): void => {
110
- const msg = allMessages.get(messageID);
111
- if (msg) msg.read = true;
112
- };
113
-
114
- /** Mark a single message as delivered. */
115
- const markDelivered = (messageID: string): void => {
116
- const msg = allMessages.get(messageID);
117
- if (msg) msg.delivered = true;
118
- };
119
-
120
- /**
121
- * Attempt to deliver all pending messages for `sessionID` by
122
- * injecting them into the target session via the OpenCode client.
123
- *
124
- * Uses `client.session.prompt({ path, body })` (typed SDK, preferred) with
125
- * `noReply: false` so the receiving agent's LLM processes and acts on each
126
- * message. Falls back to `client.session.promptAsync()` when unavailable.
127
- *
128
- * Failed deliveries are silently skipped and retried on the next
129
- * `session.idle` event for the target session.
130
- *
131
- * @returns The number of messages successfully delivered.
132
- */
133
- const deliverPending = async (sessionID: string, client: MailboxClient): Promise<number> => {
134
- const pending = getPending(sessionID);
135
- if (pending.length === 0) return 0;
136
-
137
- const canDeliver = Boolean(client.session?.prompt ?? client.session?.promptAsync);
138
- if (!canDeliver) return 0;
139
-
140
- let delivered = 0;
141
- for (const msg of pending) {
142
- const text = `[opencode-multiagent mailbox] From: ${msg.from}\n${msg.content}`;
143
- try {
144
- if (client.session?.prompt) {
145
- // noReply: false — the receiving agent processes and responds to the message.
146
- await client.session.prompt({
147
- path: { id: sessionID },
148
- body: { parts: [{ type: "text", text }], noReply: false },
149
- });
150
- } else if (client.session?.promptAsync) {
151
- await client.session.promptAsync({
152
- sessionID,
153
- noReply: false,
154
- parts: [{ type: "text", text }],
155
- });
156
- }
157
- markDelivered(msg.id);
158
- delivered++;
159
- } catch {
160
- // Best-effort: skip failed deliveries, will retry on next idle event.
161
- }
162
- }
163
- return delivered;
164
- };
165
-
166
- /**
167
- * Send a lead-notification message when a child session completes.
168
- * Enqueues the notification and attempts immediate delivery.
169
- *
170
- * @param childSessionID The completed child session.
171
- * @param parentSessionID The lead / parent session to notify.
172
- * @param agentName Optional agent name for the completion message.
173
- * @param client OpenCode client for delivery.
174
- */
175
- const notifyLeadOnCompletion = async (
176
- childSessionID: string,
177
- parentSessionID: string,
178
- agentName: string | null,
179
- client: MailboxClient,
180
- ): Promise<void> => {
181
- const agentLabel = agentName ? ` (agent: ${agentName})` : "";
182
- const content =
183
- `Child session ${childSessionID}${agentLabel} has completed. ` +
184
- "Review its output and proceed with the next task.";
185
- try {
186
- send(childSessionID, parentSessionID, content);
187
- const delivered = await deliverPending(parentSessionID, client);
188
- await note("mailbox_lead_notify", {
189
- observation: true,
190
- childSessionID,
191
- parentSessionID,
192
- agentName,
193
- delivered,
194
- });
195
- } catch {
196
- // Best-effort notification; do not let delivery failure propagate.
197
- }
198
- };
199
-
200
- /**
201
- * Release all in-memory state for `sessionID`.
202
- * Call this when a session is deleted to avoid memory leaks.
203
- */
204
- const cleanup = (sessionID: string): void => {
205
- const msgs = messagesBySession.get(sessionID) ?? [];
206
- for (const msg of msgs) allMessages.delete(msg.id);
207
- messagesBySession.delete(sessionID);
208
- };
209
-
210
- return {
211
- send,
212
- getUnread,
213
- getAll,
214
- getPending,
215
- markRead,
216
- markDelivered,
217
- deliverPending,
218
- notifyLeadOnCompletion,
219
- cleanup,
220
- };
221
- };
222
-
223
- export type Mailbox = ReturnType<typeof createMailbox>;
224
-
225
- // ---------------------------------------------------------------------------
226
- // Event-hook integration helpers
227
- // ---------------------------------------------------------------------------
228
-
229
- /**
230
- * Creates a mailbox event handler to be called from the plugin's `event()` hook.
231
- *
232
- * Handles two lifecycle events:
233
- * - `session.idle` → deliver any pending messages to the idle session.
234
- * - `session.deleted` → release mailbox state for the terminated session.
235
- *
236
- * For `session.deleted` on a child session it also notifies the parent (lead).
237
- *
238
- * @example Integration (in hooks.ts `event()` handler):
239
- * ```ts
240
- * const mailboxHandler = createMailboxEventHandler(mailbox, client, childInfo);
241
- * // …inside event():
242
- * await mailboxHandler(input.event);
243
- * ```
244
- */
245
- export const createMailboxEventHandler = (
246
- mailbox: Mailbox,
247
- client: MailboxClient,
248
- /** Lookup function: given a childSessionID returns its parentSessionID + agentName or undefined. */
249
- getChildInfo: (sessionID: string) => { parentID: string; agentName: string | null } | undefined,
250
- ) => {
251
- return async (event: { type?: string; properties?: unknown }): Promise<void> => {
252
- const props = (event.properties ?? {}) as Record<string, unknown>;
253
- const type = event.type ?? "";
254
-
255
- if (type === "session.idle") {
256
- const sessionID = typeof props.sessionID === "string" ? props.sessionID : undefined;
257
- if (!sessionID) return;
258
- const pending = mailbox.getPending(sessionID);
259
- if (pending.length === 0) return;
260
- await mailbox.deliverPending(sessionID, client);
261
- await note("mailbox_idle_delivery", {
262
- observation: true,
263
- sessionID,
264
- pending: pending.length,
265
- });
266
- return;
267
- }
268
-
269
- if (type === "session.deleted") {
270
- const sessionID =
271
- typeof (props.info as Record<string, unknown> | undefined)?.id === "string"
272
- ? (props.info as Record<string, unknown>).id as string
273
- : typeof props.sessionID === "string"
274
- ? props.sessionID
275
- : undefined;
276
- if (!sessionID) return;
277
-
278
- // Notify parent (lead) that this child finished before cleaning up.
279
- const info = getChildInfo(sessionID);
280
- if (info?.parentID) {
281
- await mailbox.notifyLeadOnCompletion(sessionID, info.parentID, info.agentName, client);
282
- }
283
-
284
- mailbox.cleanup(sessionID);
285
- }
286
- };
287
- };
@@ -1,99 +0,0 @@
1
- import { readFile, readdir } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
3
-
4
- import { clone } from "./utils.ts";
5
-
6
- type FrontmatterData = Record<string, unknown>;
7
-
8
- const unquote = (value: unknown): unknown => {
9
- if (typeof value !== "string" || value.length < 2) return value;
10
- const first = value[0];
11
- const last = value[value.length - 1];
12
- if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
13
- return value.slice(1, -1);
14
- }
15
- return value;
16
- };
17
-
18
- const parseScalar = (value: string): unknown => {
19
- const trimmed = value.trim();
20
- if (trimmed === "") return "";
21
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
22
- return unquote(trimmed);
23
- }
24
- if (trimmed === "true") return true;
25
- if (trimmed === "false") return false;
26
- if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
27
- return trimmed;
28
- };
29
-
30
- export function parseFrontmatter(value: string): { data: FrontmatterData; body: string } {
31
- if (typeof value !== "string" || !value.startsWith("---")) {
32
- return { data: {}, body: value };
33
- }
34
- const openingMatch = value.match(/^---\r?\n/);
35
- if (!openingMatch) return { data: {}, body: value };
36
- const rest = value.slice(openingMatch[0].length);
37
- const closingIndex = rest.search(/\r?\n---(?:\r?\n|$)/);
38
- if (closingIndex < 0) return { data: {}, body: value };
39
- const rawFrontmatter = rest.slice(0, closingIndex);
40
- const closingSlice = rest.slice(closingIndex);
41
- const closingMatch = closingSlice.match(/^\r?\n---(?:\r?\n|$)/);
42
- if (!closingMatch) return { data: {}, body: value };
43
- const body = closingSlice.slice(closingMatch[0].length);
44
- const data: FrontmatterData = {};
45
- const stack: Array<{ indent: number; obj: FrontmatterData }> = [{ indent: -1, obj: data }];
46
-
47
- for (const line of rawFrontmatter.split(/\r?\n/)) {
48
- if (!line.trim()) continue;
49
- const indent = line.match(/^ */)?.[0].length ?? 0;
50
- const trimmed = line.trim();
51
- const colonIndex = trimmed.indexOf(":");
52
- if (colonIndex < 0) continue;
53
- const key = String(unquote(trimmed.slice(0, colonIndex).trim()));
54
- const rawValue = trimmed.slice(colonIndex + 1).trim();
55
-
56
- while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
57
- stack.pop();
58
- }
59
-
60
- const parent = stack[stack.length - 1].obj;
61
- if (rawValue === "") {
62
- parent[key] = {};
63
- stack.push({ indent, obj: parent[key] as FrontmatterData });
64
- continue;
65
- }
66
-
67
- parent[key] = parseScalar(rawValue);
68
- }
69
-
70
- return { data, body };
71
- }
72
-
73
- export const readJSON = async <T>(path: string, fallback: T): Promise<T> => {
74
- try {
75
- return JSON.parse(await readFile(path, "utf8")) as T;
76
- } catch {
77
- return clone(fallback);
78
- }
79
- };
80
-
81
- export const dedupe = (values: Array<string | undefined>): string[] =>
82
- [...new Set(values.filter(Boolean).map((value) => resolve(value as string)))];
83
-
84
- export async function loadMarkdownDefs(dirs: Array<string | undefined>) {
85
- const defs = new Map<string, { data: FrontmatterData; body: string; source: string }>();
86
- for (const dir of dedupe(dirs)) {
87
- const files = await readdir(dir).catch(() => []);
88
- for (const file of files.filter((name) => name.endsWith(".md")).sort()) {
89
- try {
90
- const value = await readFile(join(dir, file), "utf8");
91
- const { data, body } = parseFrontmatter(value);
92
- defs.set(file.slice(0, -3), { data, body, source: join(dir, file) });
93
- } catch {
94
- // Ignore unreadable definitions.
95
- }
96
- }
97
- }
98
- return defs;
99
- }
@@ -1,35 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
-
4
- import { packageRoot } from "./constants.ts";
5
- import { note } from "./log.ts";
6
- import { clone, own } from "./utils.ts";
7
-
8
- const defaultsFilePath = join(packageRoot, "defaults", "mcp-defaults.json");
9
-
10
- let cachedDefaults: Record<string, unknown> | undefined;
11
-
12
- export async function loadMcpDefaults(): Promise<Record<string, unknown>> {
13
- if (cachedDefaults) return clone(cachedDefaults);
14
- try {
15
- const parsed = JSON.parse(await readFile(defaultsFilePath, "utf8")) as Record<string, unknown>;
16
- cachedDefaults = parsed && typeof parsed === "object" ? parsed : {};
17
- } catch {
18
- cachedDefaults = {};
19
- await note("config_warning", {
20
- observation: true,
21
- warning: "mcp_defaults_unreadable",
22
- path: defaultsFilePath,
23
- });
24
- }
25
- return clone(cachedDefaults);
26
- }
27
-
28
- export function applyMcpDefaults(cfg: Record<string, unknown>, defaults: Record<string, unknown>): void {
29
- if (!cfg.mcp || typeof cfg.mcp !== "object") cfg.mcp = {};
30
- const mcp = cfg.mcp as Record<string, unknown>;
31
- for (const [name, definition] of Object.entries(defaults ?? {})) {
32
- if (own(mcp, name)) continue;
33
- mcp[name] = clone(definition);
34
- }
35
- }
@@ -1,67 +0,0 @@
1
- import {
2
- blockedPathFragments,
3
- blockedPathPrefixRegex,
4
- blockedPathSuffixes,
5
- destructiveBashFragments,
6
- experimentalText,
7
- noteText,
8
- sensitiveMentions,
9
- suspiciousTerms,
10
- } from "./constants.ts";
11
-
12
- export { destructiveBashFragments, experimentalText, noteText };
13
-
14
- const normalize = (value: unknown): string => String(value ?? "").toLowerCase();
15
-
16
- const hasBlockedEnvName = (value: string): boolean => {
17
- const name = value.split("/").pop() ?? "";
18
- if (name === ".env.example") return false;
19
- return name.endsWith(".env") || name.includes(".env.");
20
- };
21
-
22
- export const blocked = (value: unknown): boolean => {
23
- const normalized = normalize(value);
24
- return (
25
- blockedPathPrefixRegex.test(normalized) ||
26
- blockedPathFragments.some((fragment) => normalized.includes(fragment)) ||
27
- blockedPathSuffixes.some((suffix) => normalized.endsWith(suffix)) ||
28
- hasBlockedEnvName(normalized)
29
- );
30
- };
31
-
32
- export const tokenizedBashBlocked = (command: unknown): boolean => {
33
- const normalized = normalize(command).replace(/\s+/g, " ").trim();
34
- if (normalized.length === 0) return false;
35
-
36
- const forkBombPattern =
37
- /(?:^|[;&|])\s*(?::|[a-z_][a-z0-9_]*)\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*(?::|[a-z_][a-z0-9_]*)\s*(?:$|[;&|])/;
38
- if (forkBombPattern.test(normalized)) return true;
39
-
40
- const segments = normalized
41
- .split(/&&|\||;/)
42
- .map((segment) => segment.trim())
43
- .filter(Boolean);
44
-
45
- if (segments.some((segment) => destructiveBashFragments.some((fragment) => segment.includes(fragment)))) {
46
- return true;
47
- }
48
-
49
- for (let index = 0; index < segments.length - 1; index += 1) {
50
- if (!/^(curl|wget)(?:\s|$)/.test(segments[index]!)) continue;
51
- if (/\b(?:bash|sh)\b/.test(segments[index + 1]!)) return true;
52
- }
53
-
54
- return false;
55
- };
56
-
57
- export const mentions = (value: string): boolean => sensitiveMentions.some((fragment) => value.includes(fragment));
58
-
59
- export const flagged = (value: string): string[] =>
60
- suspiciousTerms.filter((term, index, list) => value.includes(term) && list.indexOf(term) === index);
61
-
62
- export const risky = (value: unknown): boolean => {
63
- const lower = normalize(value);
64
- return mentions(lower) || blocked(lower) || tokenizedBashBlocked(lower);
65
- };
66
-
67
- export const nextSyntheticId = (): string => `plugin-part-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -1,140 +0,0 @@
1
- import { qualityEventTypes, qualitySignalRegex } from "./constants.ts";
2
- import { note } from "./log.ts";
3
-
4
- const cleanupIntervalMs = 5 * 60 * 1000;
5
- const staleSessionTtlMs = 30 * 60 * 1000;
6
- const maxTrackedSessions = 200;
7
- const evictionFraction = 0.2;
8
-
9
- type SessionQualityState = {
10
- editedFiles: Set<string>;
11
- lastEditAt: number;
12
- qualityRemindedAt: number;
13
- lastQualityEvidenceAt: number;
14
- lastActivityAt: number;
15
- };
16
-
17
- type PromptClient = {
18
- session?: {
19
- prompt?: (options: {
20
- path: { id: string };
21
- body?: { parts: Array<{ type: string; text: string }>; noReply?: boolean };
22
- }) => Promise<unknown>;
23
- };
24
- };
25
-
26
- export const createQualityController = ({ flags, client }: { flags: Record<string, any>; client: PromptClient }) => {
27
- const sessionState = new Map<string, SessionQualityState>();
28
-
29
- const getSessionState = (sessionID: string): SessionQualityState => {
30
- if (!sessionState.has(sessionID)) {
31
- sessionState.set(sessionID, {
32
- editedFiles: new Set(),
33
- lastEditAt: 0,
34
- qualityRemindedAt: 0,
35
- lastQualityEvidenceAt: 0,
36
- lastActivityAt: Date.now(),
37
- });
38
- }
39
- return sessionState.get(sessionID)!;
40
- };
41
-
42
- const cleanupStaleSessions = (now = Date.now()): void => {
43
- for (const [sessionID, state] of sessionState.entries()) {
44
- if (now - state.lastActivityAt > staleSessionTtlMs) sessionState.delete(sessionID);
45
- }
46
- };
47
-
48
- const enforceSessionLimit = (): void => {
49
- if (sessionState.size <= maxTrackedSessions) return;
50
- const toRemove = [...sessionState.entries()]
51
- .sort((left, right) => left[1].lastActivityAt - right[1].lastActivityAt)
52
- .slice(0, Math.max(1, Math.ceil(sessionState.size * evictionFraction)));
53
- for (const [sessionID] of toRemove) sessionState.delete(sessionID);
54
- };
55
-
56
- let interval: ReturnType<typeof setInterval> | null = null;
57
- if (flags.quality_gate) {
58
- interval = setInterval(() => cleanupStaleSessions(), cleanupIntervalMs);
59
- interval.unref?.();
60
- }
61
-
62
- const trackEdit = (sessionID: string | undefined, filePath: string | undefined): void => {
63
- if (!sessionID || !filePath) return;
64
- const state = getSessionState(sessionID);
65
- state.editedFiles.add(filePath);
66
- state.lastEditAt = Date.now();
67
- state.lastActivityAt = Date.now();
68
- enforceSessionLimit();
69
- };
70
-
71
- const recordQualityEvidence = (sessionID: string | undefined, command: unknown): void => {
72
- if (!sessionID || typeof command !== "string") return;
73
- if (!qualitySignalRegex.test(command)) return;
74
- const state = getSessionState(sessionID);
75
- state.lastQualityEvidenceAt = Date.now();
76
- state.lastActivityAt = Date.now();
77
- };
78
-
79
- const handleQualityEvent = async (event: { type?: string; properties?: any }): Promise<void> => {
80
- if (!flags.quality_gate || !qualityEventTypes.has(event?.type ?? "")) return;
81
- const props = event.properties ?? {};
82
-
83
- if (event.type === "file.edited") {
84
- const sessionID =
85
- typeof props.sessionID === "string" ? props.sessionID : typeof props.info?.sessionID === "string" ? props.info.sessionID : undefined;
86
- const file = typeof props.file === "string" ? props.file : typeof props.info?.file === "string" ? props.info.file : undefined;
87
- trackEdit(sessionID, file);
88
- return;
89
- }
90
-
91
- if (event.type === "session.idle") {
92
- const sessionID = typeof props.sessionID === "string" ? props.sessionID : undefined;
93
- if (!sessionID || !client?.session?.prompt) return;
94
- const state = sessionState.get(sessionID);
95
- if (!state || state.editedFiles.size === 0) return;
96
- state.lastActivityAt = Date.now();
97
- if (state.lastQualityEvidenceAt >= state.lastEditAt) return;
98
- const now = Date.now();
99
- const idleMs = flags.quality_config?.reminder_idle_ms ?? 120000;
100
- const cooldownMs = flags.quality_config?.reminder_cooldown_ms ?? 300000;
101
- if (now - state.lastEditAt < idleMs || now - state.qualityRemindedAt < cooldownMs) return;
102
- state.qualityRemindedAt = now;
103
- const files = [...state.editedFiles].slice(0, 6).join(", ");
104
- const reminder =
105
- "[opencode-multiagent quality] This session edited files and no later verification signal was observed. " +
106
- "Run `/quality` or equivalent repo-native checks before treating the work as done." +
107
- (files ? ` Tracked files: ${files}` : "");
108
- try {
109
- await client.session.prompt({
110
- path: { id: sessionID },
111
- body: { parts: [{ type: "text", text: reminder }], noReply: true },
112
- });
113
- } catch {
114
- return;
115
- }
116
- await note("quality_gate", {
117
- event: "idle_reminder",
118
- sessionID,
119
- files: [...state.editedFiles].slice(0, 12),
120
- });
121
- return;
122
- }
123
-
124
- if (event.type === "session.deleted") {
125
- const sessionID = typeof props.info?.id === "string" ? props.info.id : typeof props.sessionID === "string" ? props.sessionID : undefined;
126
- if (sessionID) sessionState.delete(sessionID);
127
- }
128
- };
129
-
130
- return {
131
- handleQualityEvent,
132
- recordQualityEvidence,
133
- trackEdit,
134
- cleanup(): void {
135
- if (!interval) return;
136
- clearInterval(interval);
137
- interval = null;
138
- },
139
- };
140
- };
@@ -1,55 +0,0 @@
1
- import {
2
- agentSettingsPath,
3
- bundledAgentSettingsPath,
4
- defaultFlags,
5
- defaultProfiles,
6
- flagsPath,
7
- profilesPath,
8
- } from "./constants.ts";
9
- import { readJSON } from "./markdown.ts";
10
- import { merge } from "./utils.ts";
11
-
12
- type GenericRecord = Record<string, unknown>;
13
-
14
- export const resolveTier = (value: unknown, modelTiers: Record<string, string>): unknown => {
15
- if (typeof value !== "string" || !value.startsWith("$")) return value;
16
- const key = value.slice(1);
17
- return modelTiers[key] ?? value;
18
- };
19
-
20
- export const resolveAlias = resolveTier;
21
-
22
- export const resolveExistingTierReferences = (cfg: GenericRecord | undefined, modelTiers: Record<string, string>): void => {
23
- if (!cfg || typeof cfg !== "object") return;
24
- if (typeof cfg.model === "string") cfg.model = resolveTier(cfg.model, modelTiers);
25
- if (typeof cfg.small_model === "string") cfg.small_model = resolveTier(cfg.small_model, modelTiers);
26
-
27
- for (const item of Object.values((cfg.agent as GenericRecord | undefined) ?? {})) {
28
- if (item && typeof item === "object" && typeof (item as GenericRecord).model === "string") {
29
- (item as GenericRecord).model = resolveTier((item as GenericRecord).model, modelTiers);
30
- }
31
- }
32
-
33
- for (const item of Object.values((cfg.command as GenericRecord | undefined) ?? {})) {
34
- if (item && typeof item === "object" && typeof (item as GenericRecord).model === "string") {
35
- (item as GenericRecord).model = resolveTier((item as GenericRecord).model, modelTiers);
36
- }
37
- }
38
- };
39
-
40
- export async function loadRuntimeSettings(): Promise<{
41
- flags: typeof defaultFlags;
42
- modelTiers: Record<string, string>;
43
- agentSettings: GenericRecord;
44
- }> {
45
- const defaultAgentSettings = await readJSON<GenericRecord>(bundledAgentSettingsPath, {});
46
- const userFlags = await readJSON<Partial<typeof defaultFlags>>(flagsPath, {});
47
- const userAgentSettings = await readJSON<GenericRecord>(agentSettingsPath, {});
48
- const userProfiles = await readJSON<Partial<typeof defaultProfiles>>(profilesPath, {});
49
- const profiles = merge(defaultProfiles, userProfiles);
50
- const profileName = typeof userFlags.profile === "string" ? userFlags.profile : defaultFlags.profile;
51
- const profile = profiles[profileName as keyof typeof profiles] ?? {};
52
- const flags = merge(merge(defaultFlags, profile), userFlags);
53
- const agentSettings = merge(defaultAgentSettings, userAgentSettings);
54
- return { flags, modelTiers: {}, agentSettings };
55
- }