thoth-agents 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +5 -3
  2. package/dist/agents/agent-config.d.ts +13 -0
  3. package/dist/agents/index.d.ts +1 -1
  4. package/dist/agents/orchestrator.d.ts +1 -1
  5. package/dist/{chunk-VHQMTZ5O.js → chunk-7F33WUSD.js} +1 -1
  6. package/dist/{chunk-RWQPMWEM.js → chunk-JLU3FSBT.js} +17 -2
  7. package/dist/chunk-MIDGZE34.js +107 -0
  8. package/dist/{chunk-RVQ6IIBR.js → chunk-NY2IPJ7V.js} +361 -118
  9. package/dist/{chunk-RN2ERZLZ.js → chunk-QOA2AETX.js} +2 -228
  10. package/dist/{chunk-6NZTNWI4.js → chunk-SEF6PFM6.js} +504 -211
  11. package/dist/cli/config-io.d.ts +2 -2
  12. package/dist/cli/index.js +92 -39
  13. package/dist/cli/install.d.ts +1 -0
  14. package/dist/cli/opencode-skill-consent.d.ts +5 -0
  15. package/dist/cli/operations/opencode.d.ts +2 -1
  16. package/dist/cli/operations/types.d.ts +1 -0
  17. package/dist/cli/owned-skills.d.ts +16 -0
  18. package/dist/cli/paths.d.ts +1 -1
  19. package/dist/cli/providers.d.ts +1 -1
  20. package/dist/cli/system.d.ts +3 -1
  21. package/dist/cli/thoth-mem-install.d.ts +10 -10
  22. package/dist/cli/tui/index.js +53 -5
  23. package/dist/cli/tui/operations.d.ts +2 -0
  24. package/dist/cli/types.d.ts +2 -4
  25. package/dist/config/loader.d.ts +1 -1
  26. package/dist/config/schema.d.ts +0 -31
  27. package/dist/harness/adapters/opencode.d.ts +1 -1
  28. package/dist/harness/core/agent-pack.d.ts +1 -0
  29. package/dist/harness/core/sdd.d.ts +6 -6
  30. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -8
  31. package/dist/hooks/foreground-fallback/index.d.ts +14 -69
  32. package/dist/hooks/index.d.ts +0 -2
  33. package/dist/hooks/json-error-recovery/hook.d.ts +2 -15
  34. package/dist/hooks/oracle-gate-verdict/hook.d.ts +4 -11
  35. package/dist/index.d.ts +7 -4
  36. package/dist/index.js +822 -1421
  37. package/dist/opencode/agents.d.ts +3 -0
  38. package/dist/opencode/command.d.ts +3 -0
  39. package/dist/opencode/context.d.ts +20 -0
  40. package/dist/opencode/delegation-hooks.d.ts +3 -0
  41. package/dist/opencode/mcp.d.ts +3 -0
  42. package/dist/opencode/permission-rules.d.ts +9 -0
  43. package/dist/opencode/permissions.d.ts +3 -0
  44. package/dist/opencode/session-hooks.d.ts +3 -0
  45. package/dist/opencode/session-state.d.ts +39 -0
  46. package/dist/opencode/skills.d.ts +3 -0
  47. package/dist/opencode/tools.d.ts +3 -0
  48. package/dist/pi.js +3 -3
  49. package/dist/tools/ast-grep/cli.d.ts +1 -0
  50. package/dist/tools/ast-grep/index.d.ts +108 -2
  51. package/dist/tools/ast-grep/tools.d.ts +107 -3
  52. package/dist/tools/define-tool.d.ts +23 -0
  53. package/dist/tools/lsp/tools.d.ts +97 -5
  54. package/dist/utils/index.d.ts +0 -1
  55. package/package.json +14 -3
  56. package/pi/.thoth-agents-assets.json +2 -2
  57. package/pi/agents/thoth-designer.md +1 -1
  58. package/pi/agents/thoth-worker.md +1 -1
  59. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +3 -3
  60. package/plugin/.claude-plugin/plugin.json +1 -1
  61. package/plugin/.codex-plugin/.thoth-agents-plugin-assets.json +3 -3
  62. package/plugin/.codex-plugin/plugin.json +1 -1
  63. package/plugin/agents/designer.md +1 -1
  64. package/plugin/agents/orchestrator.md +32 -32
  65. package/plugin/agents/worker.md +1 -1
  66. package/plugin/codex-hooks/payloads/designer.md +1 -1
  67. package/plugin/codex-hooks/payloads/root.md +32 -32
  68. package/plugin/codex-hooks/payloads/worker.md +1 -1
  69. package/plugin/skills/plan-reviewer/SKILL.md +5 -5
  70. package/plugin/skills/thoth-sdd/SKILL.md +33 -9
  71. package/plugin/skills/thoth-sdd/references/phases/checklist.md +1 -1
  72. package/plugin/skills/thoth-sdd/references/phases/clarify.md +1 -1
  73. package/plugin/skills/thoth-sdd/references/phases/converge.md +8 -1
  74. package/plugin/skills/thoth-sdd/references/phases/implement.md +5 -1
  75. package/plugin/skills/thoth-sdd/references/phases/plan.md +1 -1
  76. package/plugin/skills/thoth-sdd/references/phases/specify.md +1 -1
  77. package/plugin/skills/thoth-sdd/references/phases/tasks.md +1 -1
  78. package/plugin/skills/thoth-sdd/references/phases/verify.md +10 -3
  79. package/plugin/skills/thoth-sdd/scripts/validate-result.mjs +28 -2
  80. package/plugin/skills/thoth-sdd/templates/change.md +2 -1
  81. package/skills/plan-reviewer/SKILL.md +5 -5
  82. package/skills/thoth-sdd/SKILL.md +33 -9
  83. package/skills/thoth-sdd/references/phases/checklist.md +1 -1
  84. package/skills/thoth-sdd/references/phases/clarify.md +1 -1
  85. package/skills/thoth-sdd/references/phases/converge.md +8 -1
  86. package/skills/thoth-sdd/references/phases/implement.md +5 -1
  87. package/skills/thoth-sdd/references/phases/plan.md +1 -1
  88. package/skills/thoth-sdd/references/phases/specify.md +1 -1
  89. package/skills/thoth-sdd/references/phases/tasks.md +1 -1
  90. package/skills/thoth-sdd/references/phases/verify.md +10 -3
  91. package/skills/thoth-sdd/scripts/validate-result.mjs +28 -2
  92. package/skills/thoth-sdd/templates/change.md +2 -1
  93. package/thoth-agents.schema.json +0 -26
  94. package/dist/chunk-62L3ORXN.js +0 -30
  95. package/dist/delegation/project-id.d.ts +0 -10
  96. package/dist/hooks/auto-update-checker/checker.d.ts +0 -23
  97. package/dist/hooks/auto-update-checker/constants.d.ts +0 -11
  98. package/dist/hooks/auto-update-checker/index.d.ts +0 -17
  99. package/dist/hooks/auto-update-checker/types.d.ts +0 -22
  100. package/dist/utils/tmux-session-manager.d.ts +0 -63
  101. package/dist/utils/tmux.d.ts +0 -32
package/dist/index.js CHANGED
@@ -1,28 +1,107 @@
1
1
  import {
2
2
  formatGateVerdict,
3
3
  summarizeGateResult
4
- } from "./chunk-RWQPMWEM.js";
4
+ } from "./chunk-JLU3FSBT.js";
5
5
  import {
6
6
  createAgents,
7
- renderOpenCodeAgentConfigs,
8
- spawn,
9
- spawnSync
10
- } from "./chunk-RN2ERZLZ.js";
7
+ renderOpenCodeAgentConfigs
8
+ } from "./chunk-QOA2AETX.js";
11
9
  import {
12
10
  context7,
13
11
  exa,
14
- grep_app
15
- } from "./chunk-62L3ORXN.js";
12
+ grep_app,
13
+ spawn,
14
+ spawnSync
15
+ } from "./chunk-MIDGZE34.js";
16
16
  import {
17
- POLL_INTERVAL_BACKGROUND_MS,
18
- getAgentRole,
19
- getOpenCodeConfigPaths,
20
- loadPluginConfig,
21
- stripJsonComments
22
- } from "./chunk-RVQ6IIBR.js";
17
+ loadPluginConfig
18
+ } from "./chunk-NY2IPJ7V.js";
23
19
 
24
- // src/index.ts
25
- import { fileURLToPath as fileURLToPath3 } from "url";
20
+ // src/opencode/permission-rules.ts
21
+ var ACTIONS = {
22
+ read: "read",
23
+ glob: "glob",
24
+ grep: "grep",
25
+ list: "read",
26
+ codesearch: "read",
27
+ lsp: "read",
28
+ edit: "edit",
29
+ write: "edit",
30
+ patch: "edit",
31
+ bash: "shell",
32
+ task: "subagent",
33
+ webfetch: "webfetch",
34
+ exa: "exa_*",
35
+ external_directory: "external_directory",
36
+ skill: "skill",
37
+ question: "question",
38
+ todowrite: "todowrite"
39
+ };
40
+ var RESTRICTION = { allow: 0, ask: 1, deny: 2 };
41
+ function specificity(rule) {
42
+ return Number(rule.action !== "*") + 2 * Number(rule.resource !== "*");
43
+ }
44
+ function translatePermissionRules(permission) {
45
+ if (typeof permission === "string") {
46
+ return [{ action: "*", resource: "*", effect: permission }];
47
+ }
48
+ const rules = [];
49
+ for (const [key, value] of Object.entries(permission ?? {})) {
50
+ if (!value) continue;
51
+ const action = ACTIONS[key] ?? key;
52
+ const entries = typeof value === "string" ? [["*", value]] : Object.entries(value);
53
+ for (const [resource, effect] of entries) {
54
+ const existing = rules.find(
55
+ (rule) => rule.action === action && rule.resource === resource
56
+ );
57
+ if (!existing) rules.push({ action, resource, effect });
58
+ else if (RESTRICTION[effect] > RESTRICTION[existing.effect])
59
+ existing.effect = effect;
60
+ }
61
+ }
62
+ return [
63
+ { action: "*", resource: "*", effect: "ask" },
64
+ ...rules.sort((a, b) => specificity(a) - specificity(b))
65
+ ];
66
+ }
67
+
68
+ // src/opencode/agents.ts
69
+ var ROLE_COLORS = {
70
+ orchestrator: "#7c3aed",
71
+ explorer: "#0ea5e9",
72
+ librarian: "#0284c7",
73
+ oracle: "#f59e0b",
74
+ designer: "#ec4899",
75
+ worker: "#10b981"
76
+ };
77
+ async function registerAgents(ctx, deps) {
78
+ await ctx.agent.transform((editor) => {
79
+ for (const { name } of deps.agentDefs) {
80
+ const config = deps.agents[name];
81
+ const first = deps.effectiveModelArrays[name]?.[0];
82
+ const model = first?.id ?? config.model;
83
+ const variant = first ? first.variant : config.variant;
84
+ editor.update(name, (agent) => {
85
+ agent.description = config.description;
86
+ agent.system = config.prompt;
87
+ agent.mode = name === "orchestrator" ? "primary" : "subagent";
88
+ agent.hidden = config.hidden ?? false;
89
+ agent.color = /^#[0-9a-f]{6}$/i.test(config.color ?? "") ? config.color : ROLE_COLORS[name];
90
+ agent.steps = config.steps;
91
+ agent.permissions = translatePermissionRules(config.permission);
92
+ const separator = model?.indexOf("/") ?? -1;
93
+ agent.model = model && separator > 0 && separator < model.length - 1 ? {
94
+ providerID: model.slice(0, separator),
95
+ id: model.slice(separator + 1),
96
+ ...variant ? { variant } : {}
97
+ } : void 0;
98
+ });
99
+ }
100
+ editor.default("orchestrator");
101
+ editor.remove("explore");
102
+ editor.remove("general");
103
+ });
104
+ }
26
105
 
27
106
  // src/harness/opencode-init-command.ts
28
107
  import { existsSync, readFileSync } from "fs";
@@ -62,664 +141,160 @@ ${contract}`
62
141
  };
63
142
  }
64
143
 
65
- // src/utils/logger.ts
66
- import { createRequire } from "module";
67
- var require2 = createRequire(import.meta.url);
68
- function getLogFile() {
69
- const os2 = require2("node:os");
70
- const path3 = require2("node:path");
71
- const configuredPath = process.env.THOTH_AGENTS_LOG_FILE?.trim();
72
- return configuredPath || path3.join(os2.tmpdir(), "thoth-agents.log");
73
- }
74
- function log(message, data) {
75
- try {
76
- const fs2 = require2("node:fs");
77
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
78
- const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
79
- `;
80
- fs2.appendFileSync(getLogFile(), logEntry);
81
- } catch {
82
- }
83
- }
84
-
85
- // src/hooks/auto-update-checker/checker.ts
86
- import * as fs from "fs";
87
- import * as path2 from "path";
88
- import { fileURLToPath } from "url";
89
-
90
- // src/hooks/auto-update-checker/constants.ts
91
- import * as os from "os";
92
- import * as path from "path";
93
- var PACKAGE_NAME = "thoth-agents";
94
- var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
95
- var NPM_FETCH_TIMEOUT = 5e3;
96
- function getCacheDir() {
97
- if (process.platform === "win32") {
98
- return path.join(process.env.LOCALAPPDATA ?? os.homedir(), "opencode");
99
- }
100
- return path.join(os.homedir(), ".cache", "opencode");
101
- }
102
- var CACHE_DIR = getCacheDir();
103
- var INSTALLED_PACKAGE_JSON = path.join(
104
- CACHE_DIR,
105
- "node_modules",
106
- PACKAGE_NAME,
107
- "package.json"
108
- );
109
- var configPaths = getOpenCodeConfigPaths();
110
- var USER_OPENCODE_CONFIG = configPaths[0];
111
- var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
144
+ // src/opencode/command.ts
145
+ async function registerCommand(ctx, deps) {
146
+ const command = createOpenCodeInitCommand({
147
+ projectRoot: deps.directory,
148
+ packageRoot: deps.packageRoot
149
+ });
150
+ await ctx.command.transform((editor) => {
151
+ editor.add({
152
+ name: "thoth-init",
153
+ description: command.description,
154
+ async execute({ sessionID, prompt, delivery }) {
155
+ const args = prompt.text.trim();
156
+ const text = command.template + (args ? `
112
157
 
113
- // src/hooks/auto-update-checker/checker.ts
114
- function isPrereleaseVersion(version) {
115
- return version.includes("-");
116
- }
117
- function isDistTag(version) {
118
- return !/^\d/.test(version);
119
- }
120
- function extractChannel(version) {
121
- if (!version) return "latest";
122
- if (isDistTag(version)) return version;
123
- if (isPrereleaseVersion(version)) {
124
- const prereleasePart = version.split("-")[1];
125
- if (prereleasePart) {
126
- const channelMatch = prereleasePart.match(/^(alpha|beta|rc|canary|next)/);
127
- if (channelMatch) return channelMatch[1];
128
- }
129
- }
130
- return "latest";
131
- }
132
- function getConfigPaths(directory) {
133
- return [
134
- path2.join(directory, ".opencode", "opencode.json"),
135
- path2.join(directory, ".opencode", "opencode.jsonc"),
136
- USER_OPENCODE_CONFIG,
137
- USER_OPENCODE_CONFIG_JSONC
138
- ];
139
- }
140
- function getLocalDevPath(directory) {
141
- for (const configPath of getConfigPaths(directory)) {
142
- try {
143
- if (!fs.existsSync(configPath)) continue;
144
- const content = fs.readFileSync(configPath, "utf-8");
145
- const config = JSON.parse(stripJsonComments(content));
146
- const plugins = config.plugin ?? [];
147
- for (const entry of plugins) {
148
- if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
149
- try {
150
- return fileURLToPath(entry);
151
- } catch {
152
- return entry.replace("file://", "");
153
- }
154
- }
158
+ ${args}` : "");
159
+ await ctx.session.switchAgent({ sessionID, agent: "orchestrator" });
160
+ await ctx.session.prompt({ sessionID, text, delivery });
155
161
  }
156
- } catch {
157
- }
158
- }
159
- return null;
160
- }
161
- function findPackageJsonUp(startPath) {
162
- try {
163
- const stat = fs.statSync(startPath);
164
- let dir = stat.isDirectory() ? startPath : path2.dirname(startPath);
165
- for (let i = 0; i < 10; i++) {
166
- const pkgPath = path2.join(dir, "package.json");
167
- if (fs.existsSync(pkgPath)) {
168
- try {
169
- const content = fs.readFileSync(pkgPath, "utf-8");
170
- const pkg = JSON.parse(content);
171
- if (pkg.name === PACKAGE_NAME) return pkgPath;
172
- } catch {
173
- }
174
- }
175
- const parent = path2.dirname(dir);
176
- if (parent === dir) break;
177
- dir = parent;
178
- }
179
- } catch {
180
- }
181
- return null;
182
- }
183
- function getLocalDevVersion(directory) {
184
- const localPath = getLocalDevPath(directory);
185
- if (!localPath) return null;
186
- try {
187
- const pkgPath = findPackageJsonUp(localPath);
188
- if (!pkgPath) return null;
189
- const content = fs.readFileSync(pkgPath, "utf-8");
190
- const pkg = JSON.parse(content);
191
- return pkg.version ?? null;
192
- } catch {
193
- return null;
194
- }
195
- }
196
- function findPluginEntry(directory) {
197
- for (const configPath of getConfigPaths(directory)) {
198
- try {
199
- if (!fs.existsSync(configPath)) continue;
200
- const content = fs.readFileSync(configPath, "utf-8");
201
- const config = JSON.parse(stripJsonComments(content));
202
- const plugins = config.plugin ?? [];
203
- for (const entry of plugins) {
204
- if (entry === PACKAGE_NAME) {
205
- return { entry, isPinned: false, pinnedVersion: null, configPath };
206
- }
207
- if (entry.startsWith(`${PACKAGE_NAME}@`)) {
208
- const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1);
209
- const isPinned = pinnedVersion !== "latest";
210
- return {
211
- entry,
212
- isPinned,
213
- pinnedVersion: isPinned ? pinnedVersion : null,
214
- configPath
215
- };
216
- }
217
- }
218
- } catch {
219
- }
220
- }
221
- return null;
162
+ });
163
+ });
222
164
  }
223
- var cachedPackageVersion = null;
224
- function getCachedVersion() {
225
- if (cachedPackageVersion) return cachedPackageVersion;
226
- try {
227
- if (fs.existsSync(INSTALLED_PACKAGE_JSON)) {
228
- const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8");
229
- const pkg = JSON.parse(content);
230
- if (pkg.version) {
231
- cachedPackageVersion = pkg.version;
232
- return pkg.version;
233
- }
234
- }
235
- } catch {
165
+
166
+ // src/opencode/context.ts
167
+ import { fileURLToPath } from "url";
168
+ function loadOpenCodeDependencies(ctx) {
169
+ const directory = ctx.location.directory;
170
+ const config = loadPluginConfig(directory);
171
+ const agentDefs = createAgents(config);
172
+ const agents = renderOpenCodeAgentConfigs(config);
173
+ const modelArrayMap = {};
174
+ const runtimeChains = {};
175
+ const effectiveModelArrays = {};
176
+ for (const agent of agentDefs) {
177
+ if (!agent._modelArray?.length) continue;
178
+ modelArrayMap[agent.name] = [...agent._modelArray];
179
+ effectiveModelArrays[agent.name] = [...agent._modelArray];
180
+ runtimeChains[agent.name] = agent._modelArray.map((model) => model.id);
236
181
  }
237
- try {
238
- const currentDir = path2.dirname(fileURLToPath(import.meta.url));
239
- const pkgPath = findPackageJsonUp(currentDir);
240
- if (pkgPath) {
241
- const content = fs.readFileSync(pkgPath, "utf-8");
242
- const pkg = JSON.parse(content);
243
- if (pkg.version) {
244
- cachedPackageVersion = pkg.version;
245
- return pkg.version;
182
+ if (config.fallback?.enabled !== false) {
183
+ for (const [name, models] of Object.entries(
184
+ config.fallback?.chains ?? {}
185
+ )) {
186
+ if (!models?.length) continue;
187
+ const currentModel = agents[name]?.model;
188
+ const effective = effectiveModelArrays[name] ?? (currentModel ? [{ id: currentModel, variant: agents[name]?.variant }] : []);
189
+ for (const id of models) {
190
+ if (!effective.some((model) => model.id === id)) effective.push({ id });
246
191
  }
192
+ runtimeChains[name] = effective.map((model) => model.id);
193
+ effectiveModelArrays[name] = effective;
247
194
  }
248
- } catch (err) {
249
- log(
250
- "[auto-update-checker] Failed to resolve version from current directory:",
251
- err
252
- );
253
- }
254
- return null;
255
- }
256
- async function getLatestVersion(channel = "latest") {
257
- const controller = new AbortController();
258
- const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT);
259
- try {
260
- const response = await fetch(NPM_REGISTRY_URL, {
261
- signal: controller.signal,
262
- headers: { Accept: "application/json" }
263
- });
264
- if (!response.ok) return null;
265
- const data = await response.json();
266
- return data[channel] ?? data.latest ?? null;
267
- } catch {
268
- return null;
269
- } finally {
270
- clearTimeout(timeoutId);
271
195
  }
272
- }
273
-
274
- // src/hooks/auto-update-checker/index.ts
275
- function createAutoUpdateCheckerHook(ctx, options = {}) {
276
- const { showStartupToast = true } = options;
277
- let hasChecked = false;
278
196
  return {
279
- event: ({ event }) => {
280
- if (event.type !== "session.created") return;
281
- if (hasChecked) return;
282
- const props = event.properties;
283
- if (props?.info?.parentID) return;
284
- hasChecked = true;
285
- setTimeout(async () => {
286
- const cachedVersion = getCachedVersion();
287
- const localDevVersion = getLocalDevVersion(ctx.directory);
288
- const displayVersion = localDevVersion ?? cachedVersion;
289
- if (localDevVersion) {
290
- if (showStartupToast) {
291
- showToast(
292
- ctx,
293
- `thoth-agents ${displayVersion} (dev)`,
294
- "Running in local development mode.",
295
- "info"
296
- );
297
- }
298
- log("[auto-update-checker] Local development mode");
299
- return;
300
- }
301
- if (showStartupToast) {
302
- showToast(
303
- ctx,
304
- `thoth-agents ${displayVersion ?? "unknown"}`,
305
- "thoth-agents is active.",
306
- "info"
307
- );
308
- }
309
- runBackgroundUpdateCheck(ctx).catch((err) => {
310
- log("[auto-update-checker] Background update check failed:", err);
311
- });
312
- }, 0);
313
- }
197
+ directory,
198
+ // Both src/opencode/context.ts and dist/index.js (or a root chunk) resolve
199
+ // the package root here; nested source modules need one extra parent.
200
+ packageRoot: fileURLToPath(
201
+ new URL(
202
+ import.meta.url.endsWith(".ts") ? "../.." : "..",
203
+ import.meta.url
204
+ )
205
+ ),
206
+ config,
207
+ agentDefs,
208
+ agents,
209
+ modelArrayMap,
210
+ effectiveModelArrays,
211
+ runtimeChains
314
212
  };
315
213
  }
316
- async function runBackgroundUpdateCheck(ctx) {
317
- const pluginInfo = findPluginEntry(ctx.directory);
318
- if (!pluginInfo) {
319
- log("[auto-update-checker] Plugin not found in config");
320
- return;
321
- }
322
- const cachedVersion = getCachedVersion();
323
- const currentVersion = cachedVersion ?? pluginInfo.pinnedVersion;
324
- if (!currentVersion) {
325
- log("[auto-update-checker] No version found (cached or pinned)");
326
- return;
327
- }
328
- const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion);
329
- const latestVersion = await getLatestVersion(channel);
330
- if (!latestVersion) {
331
- log(
332
- "[auto-update-checker] Failed to fetch latest version for channel:",
333
- channel
334
- );
335
- return;
336
- }
337
- if (currentVersion === latestVersion) {
338
- log(
339
- "[auto-update-checker] Already on latest version for channel:",
340
- channel
341
- );
342
- return;
343
- }
344
- log(
345
- `[auto-update-checker] Update available (${channel}): ${currentVersion} \u2192 ${latestVersion}`
346
- );
347
- showToast(
348
- ctx,
349
- `thoth-agents ${latestVersion} available`,
350
- `v${currentVersion} \u2192 v${latestVersion}. Run npx thoth-agents@latest install --agent=opencode or use interactive CLI Update.`,
351
- "info",
352
- 1e4
353
- );
354
- log("[auto-update-checker] Notification only; explicit CLI update required");
355
- }
356
- function showToast(ctx, title, message, variant = "info", duration = 3e3) {
357
- ctx.client.tui.showToast({
358
- body: { title, message, variant, duration }
359
- }).catch(() => {
360
- });
361
- }
362
214
 
363
215
  // src/hooks/delegate-task-retry/patterns.ts
364
216
  var DELEGATE_TASK_ERROR_PATTERNS = [
365
217
  {
366
- pattern: "load_skills",
367
- errorType: "missing_load_skills",
368
- fixHint: "Add load_skills=[] (empty array when no skill is needed)."
369
- },
370
- {
371
- pattern: "category OR subagent_type",
372
- errorType: "mutual_exclusion",
373
- fixHint: 'Provide only one: category (e.g., "unspecified-low") OR subagent_type (e.g., "explorer").'
374
- },
375
- {
376
- pattern: "Must provide either category or subagent_type",
377
- errorType: "missing_category_or_agent",
378
- fixHint: 'Add either category="unspecified-low" or subagent_type="explorer".'
218
+ pattern: "Unknown agent:",
219
+ errorType: "unknown_agent",
220
+ fixHint: "Use an available agent name in the agent argument."
379
221
  },
380
222
  {
381
- pattern: "Unknown category",
382
- errorType: "unknown_category",
383
- fixHint: "Use a valid category listed in the error output."
223
+ pattern: "cannot run as a subagent",
224
+ errorType: "primary_agent",
225
+ fixHint: "Choose a specialist agent, not a primary agent."
384
226
  },
385
227
  {
386
- pattern: "Unknown agent",
387
- errorType: "unknown_agent",
388
- fixHint: "Use a valid agent name from the available list."
228
+ pattern: "Subagent depth limit reached",
229
+ errorType: "depth_limit",
230
+ fixHint: "Return to the root coordinator; nesting depth is 1. Do not retry nested delegation."
389
231
  },
390
232
  {
391
- pattern: "Skills not found",
392
- errorType: "unknown_skills",
393
- fixHint: "Use valid skill names listed in the error output."
233
+ pattern: "Subagent denied:",
234
+ errorType: "permission_denied",
235
+ fixHint: "Return the permission blocker to the root coordinator; do not bypass the denial."
394
236
  },
395
237
  {
396
- pattern: "is not allowed. Allowed agents:",
397
- errorType: "background_agent_not_allowed",
398
- fixHint: "Use one of the allowed agents shown in the error or delegate from a parent agent that can call this subagent."
238
+ pattern: "Invalid arguments",
239
+ errorType: "invalid_arguments",
240
+ fixHint: "Supply agent, description and prompt; optional model, sessionID and background must have the native types."
399
241
  }
400
242
  ];
401
243
  function detectDelegateTaskError(output) {
402
- if (!output || typeof output !== "string") return null;
403
- const hasErrorSignal = output.includes("[ERROR]") || output.includes("Invalid arguments") || output.includes("is not allowed. Allowed agents:");
404
- if (!hasErrorSignal) return null;
405
- for (const pattern of DELEGATE_TASK_ERROR_PATTERNS) {
406
- if (output.includes(pattern.pattern)) {
407
- return {
408
- errorType: pattern.errorType,
409
- originalOutput: output
410
- };
411
- }
412
- }
413
- return null;
244
+ const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
245
+ (entry) => output.includes(entry.pattern)
246
+ );
247
+ return pattern ? { errorType: pattern.errorType, originalOutput: output } : null;
414
248
  }
415
249
 
416
250
  // src/hooks/delegate-task-retry/guidance.ts
417
- function extractAvailableList(output) {
418
- const match = output.match(/Allowed agents:\s*(.+)$/m);
419
- if (match) return match[1].trim();
420
- const available = output.match(/Available[^:]*:\s*(.+)$/m);
421
- if (available) return available[1].trim();
422
- return null;
423
- }
424
251
  function buildRetryGuidance(errorInfo) {
425
252
  const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
426
- (p) => p.errorType === errorInfo.errorType
253
+ (entry) => entry.errorType === errorInfo.errorType
427
254
  );
428
- if (!pattern) {
429
- return "\n[delegate-task retry] Fix parameters and retry with corrected arguments.";
430
- }
431
- const available = extractAvailableList(errorInfo.originalOutput);
432
255
  const lines = [
433
- "",
434
- "[delegate-task retry suggestion]",
256
+ "[subagent retry suggestion]",
435
257
  `Error type: ${errorInfo.errorType}`,
436
- `Fix: ${pattern.fixHint}`
258
+ `Fix: ${pattern?.fixHint ?? "Correct the native subagent arguments before retrying."}`
437
259
  ];
438
- if (available) {
439
- lines.push(`Available: ${available}`);
440
- }
441
- if (errorInfo.errorType === "background_agent_not_allowed") {
260
+ if (errorInfo.errorType !== "depth_limit" && errorInfo.errorType !== "permission_denied") {
442
261
  lines.push(
443
- "Retry now with corrected parameters.",
444
- "Use `background=true` only for `explorer` or `librarian`; otherwise retry without background.",
445
- "If the host enables `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, collect results with `task_status` before reporting completion.",
446
- "Examples:",
447
- 'task(description="...", prompt="...", subagent_type="explorer", background=true)',
448
- 'task_status(task_id="ses_...", wait=true)'
262
+ 'Example: subagent({agent:"explorer", description:"Locate entrypoint", prompt:"Find the bounded entrypoint"})',
263
+ "Use background:true for independent work; completion arrives automatically. Use sessionID to clarify or continue the same child, including while running. Do not poll."
449
264
  );
450
- return lines.join("\n");
451
265
  }
452
- lines.push(
453
- "Retry now with corrected parameters. Example:",
454
- 'task(description="...", prompt="...", subagent_type="explorer")'
455
- );
456
266
  return lines.join("\n");
457
267
  }
458
268
 
459
269
  // src/hooks/delegate-task-retry/hook.ts
460
- function createDelegateTaskRetryHook(_ctx) {
461
- return {
462
- "tool.execute.after": async (input, output) => {
463
- const toolName = input.tool.toLowerCase();
464
- const isDelegateTool = toolName === "task";
465
- if (!isDelegateTool) return;
466
- if (typeof output.output !== "string") return;
467
- const detected = detectDelegateTaskError(output.output);
468
- if (!detected) return;
469
- output.output += `
470
- ${buildRetryGuidance(detected)}`;
471
- }
270
+ function appendErrorGuidance(event, guidance) {
271
+ const properties = Object.getOwnPropertyDescriptors(event.error);
272
+ properties.message = {
273
+ value: `${event.error.message}
274
+ ${guidance}`,
275
+ configurable: true,
276
+ writable: true,
277
+ enumerable: true
278
+ };
279
+ properties.stack = {
280
+ value: event.error.stack,
281
+ configurable: true,
282
+ writable: true,
283
+ enumerable: false
284
+ };
285
+ event.error = Object.create(Object.getPrototypeOf(event.error), properties);
286
+ }
287
+ function createDelegateTaskRetryHook() {
288
+ return async (event) => {
289
+ if (event.tool !== "subagent" || event.status !== "error") return;
290
+ if (event.error.message.includes("[subagent retry suggestion]")) return;
291
+ const detected = detectDelegateTaskError(event.error.message);
292
+ if (!detected) return;
293
+ appendErrorGuidance(event, buildRetryGuidance(detected));
472
294
  };
473
295
  }
474
296
 
475
- // src/hooks/foreground-fallback/index.ts
476
- var RATE_LIMIT_PATTERNS = [
477
- /\b429\b/,
478
- /rate.?limit/i,
479
- /too many requests/i,
480
- /quota.?exceeded/i,
481
- /usage.?exceeded/i,
482
- /usage limit/i,
483
- /overloaded/i,
484
- /resource.?exhausted/i,
485
- /insufficient.?quota/i,
486
- /high concurrency/i,
487
- /reduce concurrency/i
488
- ];
489
- function isRateLimitError(error) {
490
- if (!error || typeof error !== "object") return false;
491
- const err = error;
492
- const text = [
493
- err.message ?? "",
494
- String(err.data?.statusCode ?? ""),
495
- err.data?.message ?? "",
496
- err.data?.responseBody ?? ""
497
- ].join(" ");
498
- return RATE_LIMIT_PATTERNS.some((p) => p.test(text));
499
- }
500
- function parseModel(model) {
501
- const slash = model.indexOf("/");
502
- if (slash <= 0 || slash >= model.length - 1) return null;
503
- return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
504
- }
505
- var DEDUP_WINDOW_MS = 5e3;
506
- var ForegroundFallbackManager = class {
507
- constructor(client, chains, enabled) {
508
- this.client = client;
509
- this.chains = chains;
510
- this.enabled = enabled;
511
- }
512
- client;
513
- chains;
514
- enabled;
515
- /** sessionID → last observed model string ("providerID/modelID") */
516
- sessionModel = /* @__PURE__ */ new Map();
517
- /** sessionID → agent name (populated from message.updated info.agent field) */
518
- sessionAgent = /* @__PURE__ */ new Map();
519
- /** sessionID → set of models already attempted this session */
520
- sessionTried = /* @__PURE__ */ new Map();
521
- /** Sessions with an active fallback switch in flight */
522
- inProgress = /* @__PURE__ */ new Set();
523
- /** sessionID → timestamp of last trigger (for deduplication) */
524
- lastTrigger = /* @__PURE__ */ new Map();
525
- /**
526
- * Process an OpenCode plugin event.
527
- * Call this from the plugin's `event` hook for every event received.
528
- */
529
- async handleEvent(rawEvent) {
530
- if (!this.enabled) return;
531
- const event = rawEvent;
532
- if (!event?.type) return;
533
- switch (event.type) {
534
- case "message.updated": {
535
- const info = event.properties?.info;
536
- if (!info) break;
537
- const sessionID = info.sessionID;
538
- if (!sessionID) break;
539
- if (typeof info.agent === "string") {
540
- this.sessionAgent.set(sessionID, info.agent);
541
- }
542
- if (typeof info.providerID === "string" && typeof info.modelID === "string") {
543
- this.sessionModel.set(
544
- sessionID,
545
- `${info.providerID}/${info.modelID}`
546
- );
547
- }
548
- if (info.error && isRateLimitError(info.error)) {
549
- await this.tryFallback(sessionID);
550
- }
551
- break;
552
- }
553
- case "session.error": {
554
- const props = event.properties;
555
- if (props?.sessionID && props.error && isRateLimitError(props.error)) {
556
- await this.tryFallback(props.sessionID);
557
- }
558
- break;
559
- }
560
- case "session.status": {
561
- const props = event.properties;
562
- if (!props?.sessionID || props.status?.type !== "retry") break;
563
- const msg = props.status.message?.toLowerCase() ?? "";
564
- if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
565
- await this.tryFallback(props.sessionID);
566
- }
567
- break;
568
- }
569
- case "subagent.session.created": {
570
- const props = event.properties;
571
- if (props?.sessionID && typeof props.agentName === "string") {
572
- this.sessionAgent.set(props.sessionID, props.agentName);
573
- }
574
- break;
575
- }
576
- case "session.deleted": {
577
- const props = event.properties;
578
- const id = props?.info?.id ?? props?.sessionID;
579
- if (id) {
580
- this.sessionModel.delete(id);
581
- this.sessionAgent.delete(id);
582
- this.sessionTried.delete(id);
583
- this.inProgress.delete(id);
584
- this.lastTrigger.delete(id);
585
- }
586
- break;
587
- }
588
- }
589
- }
590
- getSessionModel(sessionId) {
591
- return this.sessionModel.get(sessionId);
592
- }
593
- // ---------------------------------------------------------------------------
594
- // Core fallback logic
595
- // ---------------------------------------------------------------------------
596
- async tryFallback(sessionID) {
597
- if (!sessionID) return;
598
- if (this.inProgress.has(sessionID)) return;
599
- const now = Date.now();
600
- if (now - (this.lastTrigger.get(sessionID) ?? 0) < DEDUP_WINDOW_MS) return;
601
- this.lastTrigger.set(sessionID, now);
602
- this.inProgress.add(sessionID);
603
- try {
604
- const currentModel = this.sessionModel.get(sessionID);
605
- const agentName = this.sessionAgent.get(sessionID);
606
- const chain = this.resolveChain(agentName, currentModel);
607
- if (!chain.length) {
608
- log("[foreground-fallback] no chain configured", {
609
- sessionID,
610
- agentName
611
- });
612
- return;
613
- }
614
- if (!this.sessionTried.has(sessionID)) {
615
- this.sessionTried.set(sessionID, /* @__PURE__ */ new Set());
616
- }
617
- const tried = this.sessionTried.get(sessionID);
618
- if (currentModel) tried.add(currentModel);
619
- const nextModel = chain.find((m) => !tried.has(m));
620
- if (!nextModel) {
621
- log("[foreground-fallback] fallback chain exhausted", {
622
- sessionID,
623
- agentName,
624
- tried: [...tried]
625
- });
626
- return;
627
- }
628
- tried.add(nextModel);
629
- const ref = parseModel(nextModel);
630
- if (!ref) {
631
- log("[foreground-fallback] invalid model format", {
632
- sessionID,
633
- nextModel
634
- });
635
- return;
636
- }
637
- const result = await this.client.session.messages({
638
- path: { id: sessionID }
639
- });
640
- const messages = result.data ?? [];
641
- const lastUser = [...messages].reverse().find((m) => m.info.role === "user");
642
- if (!lastUser) {
643
- log("[foreground-fallback] no user message found", { sessionID });
644
- return;
645
- }
646
- try {
647
- await this.client.session.abort({ path: { id: sessionID } });
648
- } catch {
649
- }
650
- await new Promise((r) => setTimeout(r, 500));
651
- await this.client.session.promptAsync({
652
- path: { id: sessionID },
653
- body: {
654
- // biome-ignore lint/suspicious/noExplicitAny: parts array from message history
655
- parts: lastUser.parts,
656
- model: ref
657
- }
658
- });
659
- this.sessionModel.set(sessionID, nextModel);
660
- log("[foreground-fallback] switched to fallback model", {
661
- sessionID,
662
- agentName,
663
- from: currentModel,
664
- to: nextModel
665
- });
666
- } catch (err) {
667
- log("[foreground-fallback] fallback attempt failed", {
668
- sessionID,
669
- error: err instanceof Error ? err.message : String(err)
670
- });
671
- } finally {
672
- this.inProgress.delete(sessionID);
673
- }
674
- }
675
- // ---------------------------------------------------------------------------
676
- // Chain resolution
677
- // ---------------------------------------------------------------------------
678
- /**
679
- * Determine the fallback chain to use for a session.
680
- *
681
- * Priority:
682
- * 1. Agent name known AND has a configured chain → return it directly
683
- * 2. Agent name known but NO chain configured → return [] (no fallback;
684
- * do NOT bleed into other agents' chains which would re-prompt the
685
- * session with a model belonging to a completely different agent)
686
- * 3. Agent name unknown, current model known → search all chains for
687
- * the model to infer which chain to use
688
- * 4. Nothing matches → flatten all chains as a last resort (only
689
- * reached when both agent name and current model are unavailable)
690
- */
691
- resolveChain(agentName, currentModel) {
692
- if (agentName) {
693
- return this.chains[agentName] ?? [];
694
- }
695
- if (currentModel) {
696
- for (const chain of Object.values(this.chains)) {
697
- if (chain.includes(currentModel)) return chain;
698
- }
699
- }
700
- const all = [];
701
- const seen = /* @__PURE__ */ new Set();
702
- for (const chain of Object.values(this.chains)) {
703
- for (const m of chain) {
704
- if (!seen.has(m)) {
705
- seen.add(m);
706
- all.push(m);
707
- }
708
- }
709
- }
710
- return all;
711
- }
712
- };
713
-
714
297
  // src/hooks/json-error-recovery/hook.ts
715
- var JSON_ERROR_TOOL_EXCLUDE_LIST = [
716
- "bash",
717
- "read",
718
- "glob",
719
- "webfetch",
720
- "grep_app_searchgithub",
721
- "exa_web_search_exa"
722
- ];
723
298
  var JSON_ERROR_PATTERNS = [
724
299
  /json parse error/i,
725
300
  /failed to parse json/i,
@@ -731,7 +306,6 @@ var JSON_ERROR_PATTERNS = [
731
306
  /json[^\n]*unexpected eof/i
732
307
  ];
733
308
  var JSON_ERROR_REMINDER_MARKER = "[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]";
734
- var JSON_ERROR_EXCLUDED_TOOLS = new Set(JSON_ERROR_TOOL_EXCLUDE_LIST);
735
309
  var JSON_ERROR_REMINDER = `
736
310
  [JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]
737
311
 
@@ -744,56 +318,74 @@ STOP and do this NOW:
744
318
 
745
319
  DO NOT repeat the exact same invalid call.
746
320
  `;
747
- function createJsonErrorRecoveryHook(_ctx) {
748
- return {
749
- "tool.execute.after": async (input, output) => {
750
- if (JSON_ERROR_EXCLUDED_TOOLS.has(input.tool.toLowerCase())) return;
751
- if (typeof output.output !== "string") return;
752
- if (output.output.includes(JSON_ERROR_REMINDER_MARKER)) return;
753
- const outputText = output.output;
754
- const hasJsonError = JSON_ERROR_PATTERNS.some(
755
- (pattern) => pattern.test(outputText)
756
- );
757
- if (hasJsonError) {
758
- output.output += `
759
- ${JSON_ERROR_REMINDER}`;
760
- }
761
- }
321
+ function createJsonErrorRecoveryHook() {
322
+ return async (event) => {
323
+ if (event.tool !== "subagent" || event.status !== "error") return;
324
+ const text = event.error.message;
325
+ if (text.includes(JSON_ERROR_REMINDER_MARKER)) return;
326
+ if (!JSON_ERROR_PATTERNS.some((pattern) => pattern.test(text))) return;
327
+ appendErrorGuidance(event, JSON_ERROR_REMINDER);
762
328
  };
763
329
  }
764
330
 
765
331
  // src/hooks/oracle-gate-verdict/hook.ts
766
- var ORACLE_AGENT = getAgentRole("oracle").name;
767
332
  function createOracleGateVerdictHook(ctx) {
768
- return {
769
- "tool.execute.after": async (input, output) => {
770
- if (input.tool.toLowerCase() !== "task") return;
771
- if (input.args?.subagent_type !== ORACLE_AGENT) return;
772
- if (typeof output.output !== "string") return;
773
- const text = output.output;
774
- if (!text.includes("```thoth-result")) return;
775
- const opening = "<task_result>";
776
- const closing = "</task_result>";
777
- const start = text.indexOf(opening);
778
- const end = text.lastIndexOf(closing);
779
- const wrapped = start !== -1 && end >= start + opening.length;
780
- const child = wrapped ? text.slice(start + opening.length, end) : text;
781
- const summary = {
782
- ...summarizeGateResult(child, { cwd: ctx.directory }),
783
- agent: ORACLE_AGENT
784
- };
785
- const verdict = formatGateVerdict(summary);
786
- if (wrapped && text.slice(end + closing.length).endsWith(`
333
+ const delivered = /* @__PURE__ */ new WeakSet();
334
+ return async (event) => {
335
+ if (event.tool !== "subagent" || event.status !== "completed") return;
336
+ if (delivered.has(event.result)) return;
337
+ if (!event.input || typeof event.input !== "object" || !("agent" in event.input) || event.input.agent !== "oracle")
338
+ return;
339
+ const output = event.result.output;
340
+ if (!output || typeof output !== "object" || !("status" in output) || output.status !== "completed")
341
+ return;
342
+ const content = event.result.content;
343
+ if (content === void 0) return;
344
+ const text = typeof content === "string" ? content : content.filter((part) => part.type === "text").map((part) => part.text).join("");
345
+ const raw = "output" in output && typeof output.output === "string" && text.includes(output.output) ? output.output : void 0;
346
+ const message = raw ?? text;
347
+ if (!message.trim().split(/\r\n|\r|\n/).includes("```thoth-result"))
348
+ return;
349
+ const end = text.lastIndexOf("</subagent>");
350
+ const tail = end < 0 ? "" : text.slice(end + "</subagent>".length);
351
+ if (tail.split(/\r\n|\r|\n/).some((line) => line.startsWith("thoth-gate-verdict ")))
352
+ return;
353
+ if (Array.isArray(content) && content.some((part) => {
354
+ if (part.type !== "text") return false;
355
+ const lines = part.text.split(/\r\n|\r|\n/);
356
+ return lines.length === 3 && lines[0] === "" && lines[1] === "" && lines[2].startsWith("thoth-gate-verdict ");
357
+ }))
358
+ return;
359
+ const bare = raw === void 0 || !raw.trimStart().startsWith("<subagent ");
360
+ const summary = {
361
+ ...summarizeGateResult(bare ? message : "", { cwd: ctx.directory }),
362
+ agent: "oracle"
363
+ };
364
+ const verdict = formatGateVerdict(summary);
365
+ event.result = {
366
+ ...event.result,
367
+ content: typeof content === "string" ? `${content}
787
368
 
788
- ${verdict}`))
789
- return;
790
- output.output += `
369
+ ${verdict}` : [...content, { type: "text", text: `
791
370
 
792
- ${verdict}`;
793
- }
371
+ ${verdict}` }]
372
+ };
373
+ delivered.add(event.result);
794
374
  };
795
375
  }
796
376
 
377
+ // src/opencode/delegation-hooks.ts
378
+ async function registerDelegationHooks(ctx, deps) {
379
+ const retry = createDelegateTaskRetryHook();
380
+ const recover = createJsonErrorRecoveryHook();
381
+ const verdict = createOracleGateVerdictHook({ directory: deps.directory });
382
+ await ctx.tool.hook("execute.after", async (event) => {
383
+ await retry(event);
384
+ await recover(event);
385
+ await verdict(event);
386
+ });
387
+ }
388
+
797
389
  // src/mcp/index.ts
798
390
  function createAllBuiltinMcps() {
799
391
  return {
@@ -811,271 +403,427 @@ function createBuiltinMcps(disabledMcps = []) {
811
403
  );
812
404
  }
813
405
 
814
- // src/tools/ast-grep/tools.ts
815
- import { tool } from "@opencode-ai/plugin/tool";
816
-
817
- // src/tools/ast-grep/cli.ts
818
- import { existsSync as existsSync5 } from "fs";
406
+ // src/opencode/mcp.ts
407
+ async function registerMcp(ctx, deps) {
408
+ await ctx.mcp.transform((editor) => {
409
+ for (const [name, config] of Object.entries(
410
+ createBuiltinMcps(deps.config.disabled_mcps)
411
+ )) {
412
+ editor.set(name, config);
413
+ }
414
+ });
415
+ }
819
416
 
820
- // src/tools/ast-grep/constants.ts
821
- import { spawnSync as spawnSync2 } from "child_process";
822
- import { existsSync as existsSync4, statSync as statSync2 } from "fs";
823
- import { createRequire as createRequire3 } from "module";
824
- import { dirname as dirname2, join as join5 } from "path";
417
+ // src/opencode/permissions.ts
418
+ async function registerPermissions(ctx, deps) {
419
+ const specialists = new Set(
420
+ deps.agentDefs.map(({ name }) => name).filter((name) => name !== "orchestrator")
421
+ );
422
+ await ctx.permission.hook("evaluate", (event) => {
423
+ if (event.action !== "subagent" || !event.agent || !specialists.has(event.agent))
424
+ return;
425
+ event.effect = "deny";
426
+ event.message = `Thoth role "${event.agent}" cannot delegate: only orchestrator may call subagent.`;
427
+ });
428
+ }
825
429
 
826
- // src/tools/ast-grep/downloader.ts
827
- import { chmodSync, existsSync as existsSync3, mkdirSync, unlinkSync } from "fs";
828
- import { createRequire as createRequire2 } from "module";
829
- import { homedir as homedir2 } from "os";
830
- import { join as join4 } from "path";
430
+ // src/utils/logger.ts
431
+ import { createRequire } from "module";
432
+ var require2 = createRequire(import.meta.url);
433
+ function getLogFile() {
434
+ const os = require2("node:os");
435
+ const path = require2("node:path");
436
+ const configuredPath = process.env.THOTH_AGENTS_LOG_FILE?.trim();
437
+ return configuredPath || path.join(os.tmpdir(), "thoth-agents.log");
438
+ }
439
+ function log(message, data) {
440
+ try {
441
+ const fs = require2("node:fs");
442
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
443
+ const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
444
+ `;
445
+ fs.appendFileSync(getLogFile(), logEntry);
446
+ } catch {
447
+ }
448
+ }
831
449
 
832
- // src/utils/tmux.ts
833
- var tmuxPath = null;
834
- var tmuxChecked = false;
835
- var storedConfig = null;
836
- var serverAvailable = null;
837
- var serverCheckUrl = null;
838
- async function isServerRunning(serverUrl) {
839
- if (serverCheckUrl === serverUrl && serverAvailable === true) {
840
- return true;
450
+ // src/hooks/foreground-fallback/index.ts
451
+ var RETRYABLE = /* @__PURE__ */ new Set([
452
+ "provider.rate-limit",
453
+ "provider.quota",
454
+ "provider.free-limit",
455
+ "provider.chatgpt-limit",
456
+ "provider.go-limit",
457
+ "provider.budget-limit",
458
+ "provider.model-budget-limit",
459
+ "provider.transport",
460
+ "provider.timeout",
461
+ "provider.internal",
462
+ "provider.invalid-output"
463
+ ]);
464
+ function isRateLimitError(error) {
465
+ if (!error || typeof error !== "object") return false;
466
+ const value = error;
467
+ return value.type !== void 0 && RETRYABLE.has(value.type);
468
+ }
469
+ function modelRef(value) {
470
+ const slash = value.indexOf("/");
471
+ if (slash <= 0 || slash === value.length - 1) return;
472
+ const [id, variant] = value.slice(slash + 1).split("#");
473
+ if (!id) return;
474
+ return {
475
+ providerID: value.slice(0, slash),
476
+ id,
477
+ ...variant ? { variant } : {}
478
+ };
479
+ }
480
+ var ForegroundFallbackManager = class {
481
+ constructor(session, chains, models = {}) {
482
+ this.session = session;
483
+ this.chains = chains;
484
+ this.models = models;
841
485
  }
842
- const healthUrl = new URL("/health", serverUrl).toString();
843
- const timeoutMs = 3e3;
844
- const maxAttempts = 2;
845
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
846
- const controller = new AbortController();
847
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
848
- let response = null;
486
+ session;
487
+ chains;
488
+ models;
489
+ tried = /* @__PURE__ */ new Map();
490
+ switching = /* @__PURE__ */ new Set();
491
+ forget(sessionID) {
492
+ this.tried.delete(sessionID);
493
+ }
494
+ async retry(input) {
495
+ if (!Object.hasOwn(this.chains, input.agent)) return;
496
+ const chain = this.chains[input.agent];
497
+ const current = `${input.model.providerID}/${input.model.id}`;
498
+ const inChain = chain.some((entry) => {
499
+ const ref = modelRef(entry);
500
+ return ref && `${ref.providerID}/${ref.id}` === current;
501
+ });
502
+ if (!inChain || !isRateLimitError(input.error)) return;
503
+ const lock = JSON.stringify([input.sessionID, input.agent]);
504
+ if (this.switching.has(lock)) return;
505
+ this.switching.add(lock);
849
506
  try {
850
- response = await fetch(healthUrl, { signal: controller.signal }).catch(
851
- () => null
852
- );
507
+ let agents = this.tried.get(input.sessionID);
508
+ if (!agents) {
509
+ agents = /* @__PURE__ */ new Map();
510
+ this.tried.set(input.sessionID, agents);
511
+ }
512
+ let tried = agents.get(input.agent);
513
+ if (!tried) {
514
+ tried = /* @__PURE__ */ new Set();
515
+ agents.set(input.agent, tried);
516
+ }
517
+ tried.add(current);
518
+ for (const entry of chain) {
519
+ const ref = modelRef(entry);
520
+ if (!ref) continue;
521
+ const id = `${ref.providerID}/${ref.id}`;
522
+ if (tried.has(id)) continue;
523
+ tried.add(id);
524
+ const variant = this.models[input.agent]?.find(
525
+ (model) => model.id === entry
526
+ )?.variant;
527
+ await this.session.switchModel({
528
+ sessionID: input.sessionID,
529
+ model: {
530
+ ...ref,
531
+ ...variant ? { variant } : {}
532
+ }
533
+ });
534
+ input.decision = { retry: true, delay: 0 };
535
+ return;
536
+ }
537
+ } catch (error) {
538
+ log("[foreground-fallback] model switch failed", {
539
+ sessionID: input.sessionID,
540
+ error: String(error)
541
+ });
853
542
  } finally {
854
- clearTimeout(timeout);
855
- }
856
- const available = response?.ok ?? false;
857
- if (available) {
858
- serverCheckUrl = serverUrl;
859
- serverAvailable = true;
860
- log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
861
- return true;
862
- }
863
- if (attempt < maxAttempts) {
864
- await new Promise((r) => setTimeout(r, 250));
543
+ this.switching.delete(lock);
865
544
  }
866
545
  }
867
- log("[tmux] isServerRunning: checked", { serverUrl, available: false });
868
- return false;
869
- }
870
- async function findTmuxPath() {
871
- const isWindows = process.platform === "win32";
872
- const cmd = isWindows ? "where" : "which";
873
- try {
874
- const proc = spawn([cmd, "tmux"], {
875
- stdout: "pipe",
876
- stderr: "pipe"
546
+ };
547
+
548
+ // src/opencode/session-state.ts
549
+ import { readdir, readFile, stat } from "fs/promises";
550
+ import { join as join2 } from "path";
551
+ function missing(error) {
552
+ return error.code === "ENOENT";
553
+ }
554
+ var SessionState = class {
555
+ constructor(directory, instructions) {
556
+ this.directory = directory;
557
+ this.instructions = instructions;
558
+ }
559
+ directory;
560
+ instructions;
561
+ parents = /* @__PURE__ */ new Map();
562
+ running = /* @__PURE__ */ new Set();
563
+ published = /* @__PURE__ */ new Map();
564
+ records = /* @__PURE__ */ new Map();
565
+ queue = Promise.resolve();
566
+ serialize(run) {
567
+ const result = this.queue.then(run);
568
+ this.queue = result.catch(() => {
877
569
  });
878
- const exitCode = await proc.exited;
879
- if (exitCode !== 0) {
880
- log("[tmux] findTmuxPath: 'which tmux' failed", { exitCode });
881
- return null;
882
- }
883
- const stdout = await new Response(proc.stdout).text();
884
- const path3 = stdout.trim().split("\n")[0];
885
- if (!path3) {
886
- log("[tmux] findTmuxPath: no path in output");
887
- return null;
888
- }
889
- const verifyProc = spawn([path3, "-V"], {
890
- stdout: "pipe",
891
- stderr: "pipe"
570
+ return result;
571
+ }
572
+ refresh(sessionID) {
573
+ return this.serialize(() => this.publish(sessionID));
574
+ }
575
+ event(event) {
576
+ return this.serialize(async () => {
577
+ const { sessionID } = event.data;
578
+ if (event.type === "session.created") {
579
+ if (event.data.parentID) {
580
+ this.parents.set(sessionID, event.data.parentID);
581
+ this.running.add(sessionID);
582
+ await this.publish(event.data.parentID);
583
+ }
584
+ await this.publish(sessionID);
585
+ return;
586
+ }
587
+ const parent = this.parents.get(sessionID);
588
+ if (event.type === "session.status") {
589
+ if (event.data.status.type === "idle") this.running.delete(sessionID);
590
+ else if (parent) this.running.add(sessionID);
591
+ if (parent) await this.publish(parent);
592
+ if (this.published.has(sessionID)) await this.publish(sessionID);
593
+ return;
594
+ }
595
+ this.parents.delete(sessionID);
596
+ this.running.delete(sessionID);
597
+ this.published.delete(sessionID);
598
+ for (const [child, owner] of this.parents) {
599
+ if (owner !== sessionID) continue;
600
+ this.parents.delete(child);
601
+ this.running.delete(child);
602
+ }
603
+ if (parent) await this.publish(parent);
892
604
  });
893
- const verifyExit = await verifyProc.exited;
894
- if (verifyExit !== 0) {
895
- log("[tmux] findTmuxPath: tmux -V failed", { path: path3, verifyExit });
896
- return null;
897
- }
898
- log("[tmux] findTmuxPath: found tmux", { path: path3 });
899
- return path3;
900
- } catch (err) {
901
- log("[tmux] findTmuxPath: exception", { error: String(err) });
902
- return null;
903
605
  }
904
- }
905
- async function getTmuxPath() {
906
- if (tmuxChecked) {
907
- return tmuxPath;
606
+ async inventory() {
607
+ const root = join2(this.directory, ".thoth", "changes");
608
+ const entries = await readdir(root, { withFileTypes: true }).catch(
609
+ (error) => {
610
+ if (missing(error)) return [];
611
+ throw error;
612
+ }
613
+ );
614
+ const active = /* @__PURE__ */ new Set();
615
+ for (const entry of entries) {
616
+ if (!entry.isDirectory() || entry.name === "archive") continue;
617
+ const id = entry.name;
618
+ const path = join2(root, id, `${id}.md`);
619
+ try {
620
+ const info = await stat(path);
621
+ if (!info.isFile()) continue;
622
+ active.add(id);
623
+ const fingerprint = `${info.mtimeMs}:${info.ctimeMs}:${info.size}`;
624
+ if (this.records.get(id)?.fingerprint === fingerprint) continue;
625
+ const content = await readFile(path, "utf8");
626
+ const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim() ?? id;
627
+ const status = content.match(/^\*\*(?:Status|Phase)\*\*:\s*.+$/gm) ?? [];
628
+ this.records.set(id, {
629
+ fingerprint,
630
+ label: `${title}${status.length ? ` (${status.join("; ")})` : ""}`
631
+ });
632
+ } catch (error) {
633
+ if (!missing(error)) throw error;
634
+ active.delete(id);
635
+ }
636
+ }
637
+ for (const id of this.records.keys())
638
+ if (!active.has(id)) this.records.delete(id);
639
+ return [...this.records].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(
640
+ ([id, record]) => `- .thoth/changes/${id}/${id}.md \u2014 ${record.label}`
641
+ );
908
642
  }
909
- tmuxPath = await findTmuxPath();
910
- tmuxChecked = true;
911
- log("[tmux] getTmuxPath: initialized", { tmuxPath });
912
- return tmuxPath;
913
- }
914
- function isInsideTmux() {
915
- return !!process.env.TMUX;
916
- }
917
- async function applyLayout(tmux, layout, mainPaneSize) {
918
- try {
919
- const layoutProc = spawn([tmux, "select-layout", layout], {
920
- stdout: "pipe",
921
- stderr: "pipe"
922
- });
923
- await layoutProc.exited;
924
- if (layout === "main-horizontal" || layout === "main-vertical") {
925
- const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
926
- const sizeProc = spawn(
927
- [tmux, "set-window-option", sizeOption, `${mainPaneSize}%`],
928
- {
929
- stdout: "pipe",
930
- stderr: "pipe"
931
- }
932
- );
933
- await sizeProc.exited;
934
- const reapplyProc = spawn([tmux, "select-layout", layout], {
935
- stdout: "pipe",
936
- stderr: "pipe"
643
+ async publish(sessionID) {
644
+ const records = await this.inventory();
645
+ const children = [...this.parents].filter(
646
+ ([child, parent]) => parent === sessionID && this.running.has(child)
647
+ ).map(([child]) => child).sort();
648
+ const value = [
649
+ "Thoth state",
650
+ "Active SDD records:",
651
+ ...records.length ? records : ["- none"],
652
+ "Running delegated sessions:",
653
+ ...children.length ? children.map((id) => `- ${id}`) : ["- none"]
654
+ ].join("\n");
655
+ if (this.published.get(sessionID) !== value) {
656
+ await this.instructions.entry.put({
657
+ sessionID,
658
+ key: "thoth.state",
659
+ value
937
660
  });
938
- await reapplyProc.exited;
661
+ this.published.set(sessionID, value);
939
662
  }
940
- log("[tmux] applyLayout: applied", { layout, mainPaneSize });
941
- } catch (err) {
942
- log("[tmux] applyLayout: exception", { error: String(err) });
663
+ return value;
943
664
  }
944
- }
945
- async function spawnTmuxPane(sessionId, description, config, serverUrl) {
946
- log("[tmux] spawnTmuxPane called", {
947
- sessionId,
948
- description,
949
- config,
950
- serverUrl
665
+ };
666
+
667
+ // src/opencode/session-hooks.ts
668
+ async function registerSessionHooks(ctx, deps) {
669
+ const names = new Set(deps.agentDefs.map((agent) => agent.name));
670
+ const chains = Object.fromEntries(
671
+ Object.entries(deps.runtimeChains).filter(([name]) => names.has(name))
672
+ );
673
+ const fallback = new ForegroundFallbackManager(
674
+ ctx.session,
675
+ chains,
676
+ deps.effectiveModelArrays
677
+ );
678
+ const state = new SessionState(deps.directory, ctx.session.instructions);
679
+ await ctx.session.hook("retry", (input) => fallback.retry(input));
680
+ await ctx.session.hook("prompt", async (input) => {
681
+ await state.refresh(input.sessionID);
951
682
  });
952
- if (!config.enabled) {
953
- log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
954
- return { success: false };
955
- }
956
- if (!isInsideTmux()) {
957
- log("[tmux] spawnTmuxPane: not inside tmux, skipping");
958
- return { success: false };
959
- }
960
- const serverRunning = await isServerRunning(serverUrl);
961
- if (!serverRunning) {
962
- const defaultPort = process.env.OPENCODE_PORT ?? "4096";
963
- log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
964
- serverUrl,
965
- hint: `Start opencode with --port ${defaultPort}`
966
- });
967
- return { success: false };
968
- }
969
- const tmux = await getTmuxPath();
970
- if (!tmux) {
971
- log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
972
- return { success: false };
973
- }
974
- storedConfig = config;
975
- try {
976
- const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
977
- const args = [
978
- "split-window",
979
- "-h",
980
- "-d",
981
- // Don't switch focus to new pane
982
- "-P",
983
- // Print pane info
984
- "-F",
985
- "#{pane_id}",
986
- // Format: just the pane ID
987
- opencodeCmd
988
- ];
989
- log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
990
- const proc = spawn([tmux, ...args], {
991
- stdout: "pipe",
992
- stderr: "pipe"
993
- });
994
- const exitCode = await proc.exited;
995
- const stdout = await new Response(proc.stdout).text();
996
- const stderr = await new Response(proc.stderr).text();
997
- const paneId = stdout.trim();
998
- log("[tmux] spawnTmuxPane: split result", {
999
- exitCode,
1000
- paneId,
1001
- stderr: stderr.trim()
683
+ await ctx.session.hook("compaction", async (input) => {
684
+ const value = await state.refresh(input.sessionID);
685
+ if (input.result) {
686
+ input.result.summary += `
687
+
688
+ ${value}`;
689
+ return;
690
+ }
691
+ input.messages.push({
692
+ role: "user",
693
+ content: [
694
+ {
695
+ type: "text",
696
+ text: `Preserve this current Thoth state in the compaction summary:
697
+ ${value}`
698
+ }
699
+ ]
1002
700
  });
1003
- if (exitCode === 0 && paneId) {
1004
- const renameProc = spawn(
1005
- [tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)],
1006
- { stdout: "ignore", stderr: "ignore" }
701
+ });
702
+ const signal = new AbortController();
703
+ const events = ctx.event.subscribe({
704
+ types: ["session.created", "session.status", "session.deleted"],
705
+ signal: signal.signal
706
+ });
707
+ void (async () => {
708
+ try {
709
+ for await (const event of events) {
710
+ if (event.type === "session.deleted")
711
+ fallback.forget(event.data.sessionID);
712
+ try {
713
+ await state.event(event);
714
+ } catch (error) {
715
+ log("[session-state] update failed", { error: String(error) });
716
+ }
717
+ }
718
+ } catch (error) {
719
+ log("[session-state] subscription ended", { error: String(error) });
720
+ } finally {
721
+ signal.abort();
722
+ }
723
+ })();
724
+ }
725
+
726
+ // src/opencode/skills.ts
727
+ import { readFile as readFile2 } from "fs/promises";
728
+ import { resolve } from "path";
729
+ import { parse } from "yaml";
730
+ var ownedSkills = [
731
+ "thoth-init",
732
+ "thoth-sdd",
733
+ "thoth-constitution",
734
+ "thoth-archive",
735
+ "plan-reviewer"
736
+ ];
737
+ async function registerSkills(ctx, deps) {
738
+ const skills = [];
739
+ for (const id of ownedSkills) {
740
+ const path = resolve(deps.packageRoot, "skills", id, "SKILL.md");
741
+ try {
742
+ const source = await readFile2(path, "utf8");
743
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(
744
+ source
1007
745
  );
1008
- await renameProc.exited;
1009
- const layout = config.layout ?? "main-vertical";
1010
- const mainPaneSize = config.main_pane_size ?? 60;
1011
- await applyLayout(tmux, layout, mainPaneSize);
1012
- log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", {
1013
- paneId,
1014
- layout
746
+ if (!match) throw new Error("Missing YAML frontmatter");
747
+ const metadata = parse(match[1]);
748
+ if (metadata?.name !== id || typeof metadata?.description !== "string" || !metadata.description.trim()) {
749
+ throw new Error(`Expected name ${id} and a non-empty description`);
750
+ }
751
+ skills.push({
752
+ id,
753
+ name: metadata.name,
754
+ description: metadata.description,
755
+ path,
756
+ content: match[2].trim()
1015
757
  });
1016
- return { success: true, paneId };
758
+ } catch (cause) {
759
+ throw new Error(
760
+ `Cannot load bundled skill ${path}: ${cause instanceof Error ? cause.message : String(cause)}`,
761
+ { cause }
762
+ );
1017
763
  }
1018
- return { success: false };
1019
- } catch (err) {
1020
- log("[tmux] spawnTmuxPane: exception", { error: String(err) });
1021
- return { success: false };
1022
764
  }
765
+ await ctx.skill.transform((editor) => {
766
+ for (const skill of skills) {
767
+ if (editor.get(skill.id))
768
+ editor.update(skill.id, (existing) => {
769
+ Object.assign(existing, skill);
770
+ });
771
+ else editor.add(skill);
772
+ }
773
+ });
1023
774
  }
1024
- async function closeTmuxPane(paneId) {
1025
- log("[tmux] closeTmuxPane called", { paneId });
1026
- if (!paneId) {
1027
- log("[tmux] closeTmuxPane: no paneId provided");
1028
- return false;
1029
- }
1030
- const tmux = await getTmuxPath();
1031
- if (!tmux) {
1032
- log("[tmux] closeTmuxPane: tmux binary not found");
1033
- return false;
1034
- }
1035
- try {
1036
- log("[tmux] closeTmuxPane: sending Ctrl+C for graceful shutdown", {
1037
- paneId
1038
- });
1039
- const ctrlCProc = spawn([tmux, "send-keys", "-t", paneId, "C-c"], {
1040
- stdout: "pipe",
1041
- stderr: "pipe"
1042
- });
1043
- await ctrlCProc.exited;
1044
- await new Promise((r) => setTimeout(r, 250));
1045
- log("[tmux] closeTmuxPane: killing pane", { paneId });
1046
- const proc = spawn([tmux, "kill-pane", "-t", paneId], {
1047
- stdout: "pipe",
1048
- stderr: "pipe"
1049
- });
1050
- const exitCode = await proc.exited;
1051
- const stderr = await new Response(proc.stderr).text();
1052
- log("[tmux] closeTmuxPane: result", { exitCode, stderr: stderr.trim() });
1053
- if (exitCode === 0) {
1054
- log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
1055
- if (storedConfig) {
1056
- const layout = storedConfig.layout ?? "main-vertical";
1057
- const mainPaneSize = storedConfig.main_pane_size ?? 60;
1058
- await applyLayout(tmux, layout, mainPaneSize);
1059
- log("[tmux] closeTmuxPane: layout reapplied", { layout });
775
+
776
+ // src/tools/ast-grep/tools.ts
777
+ import { z } from "zod";
778
+
779
+ // src/tools/define-tool.ts
780
+ function defineTool(definition) {
781
+ return {
782
+ name: definition.name,
783
+ description: definition.description,
784
+ input: definition.input,
785
+ async execute(input, context) {
786
+ context.signal.throwIfAborted();
787
+ let onAbort = () => {
788
+ };
789
+ const aborted = new Promise((_, reject) => {
790
+ onAbort = () => reject(context.signal.reason);
791
+ context.signal.addEventListener("abort", onAbort, { once: true });
792
+ });
793
+ try {
794
+ const content = await Promise.race([
795
+ definition.execute(input, context),
796
+ aborted
797
+ ]);
798
+ context.signal.throwIfAborted();
799
+ if (definition.showOutput) {
800
+ await context.progress({ output: content });
801
+ context.signal.throwIfAborted();
802
+ return { content, metadata: { output: content } };
803
+ }
804
+ return { content };
805
+ } finally {
806
+ context.signal.removeEventListener("abort", onAbort);
1060
807
  }
1061
- return true;
1062
808
  }
1063
- log("[tmux] closeTmuxPane: failed (pane may already be closed)", {
1064
- paneId
1065
- });
1066
- return false;
1067
- } catch (err) {
1068
- log("[tmux] closeTmuxPane: exception", { error: String(err) });
1069
- return false;
1070
- }
1071
- }
1072
- function startTmuxCheck() {
1073
- if (!tmuxChecked) {
1074
- getTmuxPath().catch(() => {
1075
- });
1076
- }
809
+ };
1077
810
  }
1078
811
 
812
+ // src/tools/ast-grep/cli.ts
813
+ import { existsSync as existsSync4 } from "fs";
814
+
815
+ // src/tools/ast-grep/constants.ts
816
+ import { spawnSync as spawnSync2 } from "child_process";
817
+ import { existsSync as existsSync3, statSync } from "fs";
818
+ import { createRequire as createRequire3 } from "module";
819
+ import { dirname, join as join4 } from "path";
820
+
821
+ // src/tools/ast-grep/downloader.ts
822
+ import { chmodSync, existsSync as existsSync2, mkdirSync, unlinkSync } from "fs";
823
+ import { createRequire as createRequire2 } from "module";
824
+ import { homedir } from "os";
825
+ import { join as join3 } from "path";
826
+
1079
827
  // src/utils/zip-extractor.ts
1080
828
  import { release } from "os";
1081
829
  var WINDOWS_BUILD_WITH_TAR = 17134;
@@ -1097,8 +845,8 @@ function isPwshAvailable() {
1097
845
  });
1098
846
  return result.exitCode === 0;
1099
847
  }
1100
- function escapePowerShellPath(path3) {
1101
- return path3.replace(/'/g, "''");
848
+ function escapePowerShellPath(path) {
849
+ return path.replace(/'/g, "''");
1102
850
  }
1103
851
  function getWindowsZipExtractor() {
1104
852
  const buildNumber = getWindowsBuildNumber();
@@ -1166,7 +914,7 @@ async function extractZip(archivePath, destDir) {
1166
914
  }
1167
915
 
1168
916
  // src/utils/file-io.ts
1169
- import { readFile, writeFile } from "fs/promises";
917
+ import { readFile as readFile3, writeFile } from "fs/promises";
1170
918
  async function writeTextFile(filePath, contents) {
1171
919
  await writeFile(
1172
920
  filePath,
@@ -1195,22 +943,22 @@ var PLATFORM_MAP = {
1195
943
  "win32-arm64": { arch: "aarch64", os: "pc-windows-msvc" },
1196
944
  "win32-ia32": { arch: "i686", os: "pc-windows-msvc" }
1197
945
  };
1198
- function getCacheDir2() {
946
+ function getCacheDir() {
1199
947
  if (process.platform === "win32") {
1200
948
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
1201
- const base2 = localAppData || join4(homedir2(), "AppData", "Local");
1202
- return join4(base2, "thoth-agents", "bin");
949
+ const base2 = localAppData || join3(homedir(), "AppData", "Local");
950
+ return join3(base2, "thoth-agents", "bin");
1203
951
  }
1204
952
  const xdgCache = process.env.XDG_CACHE_HOME;
1205
- const base = xdgCache || join4(homedir2(), ".cache");
1206
- return join4(base, "thoth-agents", "bin");
953
+ const base = xdgCache || join3(homedir(), ".cache");
954
+ return join3(base, "thoth-agents", "bin");
1207
955
  }
1208
956
  function getBinaryName() {
1209
957
  return process.platform === "win32" ? "sg.exe" : "sg";
1210
958
  }
1211
959
  function getCachedBinaryPath() {
1212
- const binaryPath = join4(getCacheDir2(), getBinaryName());
1213
- return existsSync3(binaryPath) ? binaryPath : null;
960
+ const binaryPath = join3(getCacheDir(), getBinaryName());
961
+ return existsSync2(binaryPath) ? binaryPath : null;
1214
962
  }
1215
963
  async function downloadAstGrep(version = DEFAULT_VERSION) {
1216
964
  const platformKey = `${process.platform}-${process.arch}`;
@@ -1221,32 +969,32 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
1221
969
  );
1222
970
  return null;
1223
971
  }
1224
- const cacheDir = getCacheDir2();
972
+ const cacheDir = getCacheDir();
1225
973
  const binaryName = getBinaryName();
1226
- const binaryPath = join4(cacheDir, binaryName);
1227
- if (existsSync3(binaryPath)) {
974
+ const binaryPath = join3(cacheDir, binaryName);
975
+ if (existsSync2(binaryPath)) {
1228
976
  return binaryPath;
1229
977
  }
1230
- const { arch, os: os2 } = platformInfo;
1231
- const assetName = `app-${arch}-${os2}.zip`;
978
+ const { arch, os } = platformInfo;
979
+ const assetName = `app-${arch}-${os}.zip`;
1232
980
  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
1233
981
  console.log(`[thoth-agents] Downloading ast-grep binary...`);
1234
982
  try {
1235
- if (!existsSync3(cacheDir)) {
983
+ if (!existsSync2(cacheDir)) {
1236
984
  mkdirSync(cacheDir, { recursive: true });
1237
985
  }
1238
986
  const response = await fetch(downloadUrl, { redirect: "follow" });
1239
987
  if (!response.ok) {
1240
988
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1241
989
  }
1242
- const archivePath = join4(cacheDir, assetName);
990
+ const archivePath = join3(cacheDir, assetName);
1243
991
  const arrayBuffer = await response.arrayBuffer();
1244
992
  await writeTextFile(archivePath, arrayBuffer);
1245
993
  await extractZip(archivePath, cacheDir);
1246
- if (existsSync3(archivePath)) {
994
+ if (existsSync2(archivePath)) {
1247
995
  unlinkSync(archivePath);
1248
996
  }
1249
- if (process.platform !== "win32" && existsSync3(binaryPath)) {
997
+ if (process.platform !== "win32" && existsSync2(binaryPath)) {
1250
998
  chmodSync(binaryPath, 493);
1251
999
  }
1252
1000
  console.log(`[thoth-agents] ast-grep binary ready.`);
@@ -1300,7 +1048,7 @@ var CLI_LANGUAGES = [
1300
1048
  var MIN_BINARY_SIZE = 1e4;
1301
1049
  function isValidBinary(filePath) {
1302
1050
  try {
1303
- return statSync2(filePath).size > MIN_BINARY_SIZE;
1051
+ return statSync(filePath).size > MIN_BINARY_SIZE;
1304
1052
  } catch {
1305
1053
  return false;
1306
1054
  }
@@ -1329,9 +1077,9 @@ function findSgCliPathSync() {
1329
1077
  try {
1330
1078
  const require3 = createRequire3(import.meta.url);
1331
1079
  const cliPkgPath = require3.resolve("@ast-grep/cli/package.json");
1332
- const cliDir = dirname2(cliPkgPath);
1333
- const sgPath = join5(cliDir, binaryName);
1334
- if (existsSync4(sgPath) && isValidBinary(sgPath)) {
1080
+ const cliDir = dirname(cliPkgPath);
1081
+ const sgPath = join4(cliDir, binaryName);
1082
+ if (existsSync3(sgPath) && isValidBinary(sgPath)) {
1335
1083
  return sgPath;
1336
1084
  }
1337
1085
  } catch {
@@ -1341,10 +1089,10 @@ function findSgCliPathSync() {
1341
1089
  try {
1342
1090
  const require3 = createRequire3(import.meta.url);
1343
1091
  const pkgPath = require3.resolve(`${platformPkg}/package.json`);
1344
- const pkgDir = dirname2(pkgPath);
1092
+ const pkgDir = dirname(pkgPath);
1345
1093
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
1346
- const binaryPath = join5(pkgDir, astGrepName);
1347
- if (existsSync4(binaryPath) && isValidBinary(binaryPath)) {
1094
+ const binaryPath = join4(pkgDir, astGrepName);
1095
+ if (existsSync3(binaryPath) && isValidBinary(binaryPath)) {
1348
1096
  return binaryPath;
1349
1097
  }
1350
1098
  } catch {
@@ -1352,9 +1100,9 @@ function findSgCliPathSync() {
1352
1100
  }
1353
1101
  if (process.platform === "darwin") {
1354
1102
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
1355
- for (const path3 of homebrewPaths) {
1356
- if (existsSync4(path3) && isValidBinary(path3)) {
1357
- return path3;
1103
+ for (const path of homebrewPaths) {
1104
+ if (existsSync3(path) && isValidBinary(path)) {
1105
+ return path;
1358
1106
  }
1359
1107
  }
1360
1108
  }
@@ -1371,8 +1119,8 @@ function getSgCliPath() {
1371
1119
  }
1372
1120
  return "sg";
1373
1121
  }
1374
- function setSgCliPath(path3) {
1375
- resolvedCliPath = path3;
1122
+ function setSgCliPath(path) {
1123
+ resolvedCliPath = path;
1376
1124
  }
1377
1125
  var DEFAULT_TIMEOUT_MS = 3e5;
1378
1126
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
@@ -1382,7 +1130,7 @@ var DEFAULT_MAX_MATCHES = 500;
1382
1130
  var initPromise = null;
1383
1131
  async function getAstGrepPath() {
1384
1132
  const currentPath = getSgCliPath();
1385
- if (currentPath !== "sg" && existsSync5(currentPath)) {
1133
+ if (currentPath !== "sg" && existsSync4(currentPath)) {
1386
1134
  return currentPath;
1387
1135
  }
1388
1136
  if (initPromise) {
@@ -1390,7 +1138,7 @@ async function getAstGrepPath() {
1390
1138
  }
1391
1139
  initPromise = (async () => {
1392
1140
  const syncPath = findSgCliPathSync();
1393
- if (syncPath && existsSync5(syncPath)) {
1141
+ if (syncPath && existsSync4(syncPath)) {
1394
1142
  setSgCliPath(syncPath);
1395
1143
  return syncPath;
1396
1144
  }
@@ -1404,6 +1152,7 @@ async function getAstGrepPath() {
1404
1152
  return initPromise;
1405
1153
  }
1406
1154
  async function runSg(options) {
1155
+ options.signal?.throwIfAborted();
1407
1156
  const args = [
1408
1157
  "run",
1409
1158
  "-p",
@@ -1429,18 +1178,21 @@ async function runSg(options) {
1429
1178
  const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
1430
1179
  args.push(...paths);
1431
1180
  let cliPath = getSgCliPath();
1432
- if (!existsSync5(cliPath) && cliPath !== "sg") {
1181
+ if (!existsSync4(cliPath) && cliPath !== "sg") {
1433
1182
  const downloadedPath = await getAstGrepPath();
1434
1183
  if (downloadedPath) {
1435
1184
  cliPath = downloadedPath;
1436
1185
  }
1437
1186
  }
1187
+ options.signal?.throwIfAborted();
1438
1188
  const timeout = DEFAULT_TIMEOUT_MS;
1439
1189
  const proc = spawn([cliPath, ...args], {
1440
1190
  windowsHide: true,
1441
1191
  stdout: "pipe",
1442
1192
  stderr: "pipe"
1443
1193
  });
1194
+ const onAbort = () => proc.kill();
1195
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1444
1196
  const timeoutPromise = new Promise((_, reject) => {
1445
1197
  const id = setTimeout(() => {
1446
1198
  proc.kill();
@@ -1459,6 +1211,7 @@ async function runSg(options) {
1459
1211
  stderr = await new Response(proc.stderr).text();
1460
1212
  exitCode = await proc.exited;
1461
1213
  } catch (e) {
1214
+ options.signal?.throwIfAborted();
1462
1215
  const error = e;
1463
1216
  if (error.message?.includes("timeout")) {
1464
1217
  return {
@@ -1495,7 +1248,10 @@ Auto-download failed. Manual install options:
1495
1248
  truncated: false,
1496
1249
  error: `Failed to spawn ast-grep: ${error.message}`
1497
1250
  };
1251
+ } finally {
1252
+ options.signal?.removeEventListener("abort", onAbort);
1498
1253
  }
1254
+ options.signal?.throwIfAborted();
1499
1255
  if (exitCode !== 0 && stdout.trim() === "") {
1500
1256
  if (stderr.includes("No files found")) {
1501
1257
  return { matches: [], totalMatches: 0, truncated: false };
@@ -1649,24 +1405,23 @@ function getEmptyResultHint(pattern, lang) {
1649
1405
  }
1650
1406
 
1651
1407
  // src/tools/ast-grep/tools.ts
1652
- function showOutputToUser(context, output) {
1653
- const ctx = context;
1654
- ctx.metadata?.({ metadata: { output } });
1655
- }
1656
- var ast_grep_search = tool({
1408
+ var ast_grep_search = defineTool({
1409
+ showOutput: true,
1410
+ name: "ast_grep_search",
1657
1411
  description: "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. Use meta-variables: $VAR (single node), $$$ (multiple nodes). IMPORTANT: Patterns must be complete AST nodes (valid code). For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
1658
- args: {
1659
- pattern: tool.schema.string().describe(
1412
+ input: z.object({
1413
+ pattern: z.string().describe(
1660
1414
  "AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."
1661
1415
  ),
1662
- lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
1663
- paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
1664
- globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
1665
- context: tool.schema.number().optional().describe("Context lines around match")
1666
- },
1416
+ lang: z.enum(CLI_LANGUAGES).describe("Target language"),
1417
+ paths: z.array(z.string()).optional().describe("Paths to search (default: ['.'])"),
1418
+ globs: z.array(z.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
1419
+ context: z.number().optional().describe("Context lines around match")
1420
+ }),
1667
1421
  execute: async (args, context) => {
1668
1422
  try {
1669
1423
  const result = await runSg({
1424
+ signal: context.signal,
1670
1425
  pattern: args.pattern,
1671
1426
  lang: args.lang,
1672
1427
  paths: args.paths,
@@ -1682,28 +1437,28 @@ var ast_grep_search = tool({
1682
1437
  ${hint}`;
1683
1438
  }
1684
1439
  }
1685
- showOutputToUser(context, output);
1686
1440
  return output;
1687
1441
  } catch (e) {
1688
- const output = `Error: ${e instanceof Error ? e.message : String(e)}`;
1689
- showOutputToUser(context, output);
1690
- return output;
1442
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
1691
1443
  }
1692
1444
  }
1693
1445
  });
1694
- var ast_grep_replace = tool({
1446
+ var ast_grep_replace = defineTool({
1447
+ showOutput: true,
1448
+ name: "ast_grep_replace",
1695
1449
  description: "Replace code patterns across filesystem with AST-aware rewriting. Dry-run by default. Use meta-variables in rewrite to preserve matched content. Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
1696
- args: {
1697
- pattern: tool.schema.string().describe("AST pattern to match"),
1698
- rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
1699
- lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
1700
- paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
1701
- globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"),
1702
- dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)")
1703
- },
1450
+ input: z.object({
1451
+ pattern: z.string().describe("AST pattern to match"),
1452
+ rewrite: z.string().describe("Replacement pattern (can use $VAR from pattern)"),
1453
+ lang: z.enum(CLI_LANGUAGES).describe("Target language"),
1454
+ paths: z.array(z.string()).optional().describe("Paths to search"),
1455
+ globs: z.array(z.string()).optional().describe("Include/exclude globs"),
1456
+ dryRun: z.boolean().optional().describe("Preview changes without applying (default: true)")
1457
+ }),
1704
1458
  execute: async (args, context) => {
1705
1459
  try {
1706
1460
  const result = await runSg({
1461
+ signal: context.signal,
1707
1462
  pattern: args.pattern,
1708
1463
  rewrite: args.rewrite,
1709
1464
  lang: args.lang,
@@ -1711,20 +1466,16 @@ var ast_grep_replace = tool({
1711
1466
  globs: args.globs,
1712
1467
  updateAll: args.dryRun === false
1713
1468
  });
1714
- const output = formatReplaceResult(result, args.dryRun !== false);
1715
- showOutputToUser(context, output);
1716
- return output;
1469
+ return formatReplaceResult(result, args.dryRun !== false);
1717
1470
  } catch (e) {
1718
- const output = `Error: ${e instanceof Error ? e.message : String(e)}`;
1719
- showOutputToUser(context, output);
1720
- return output;
1471
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
1721
1472
  }
1722
1473
  }
1723
1474
  });
1724
1475
 
1725
1476
  // src/tools/lsp/client.ts
1726
- import { readFileSync as readFileSync3 } from "fs";
1727
- import { extname, resolve as resolve2 } from "path";
1477
+ import { readFileSync as readFileSync2 } from "fs";
1478
+ import { extname, resolve as resolve3 } from "path";
1728
1479
  import { Readable, Writable } from "stream";
1729
1480
  import { pathToFileURL } from "url";
1730
1481
  import {
@@ -1734,31 +1485,13 @@ import {
1734
1485
  } from "vscode-jsonrpc/node.js";
1735
1486
 
1736
1487
  // src/tools/lsp/config.ts
1737
- import { existsSync as existsSync7 } from "fs";
1738
- import { homedir as homedir3 } from "os";
1739
- import { join as join6 } from "path";
1488
+ import { existsSync as existsSync6 } from "fs";
1489
+ import { homedir as homedir2 } from "os";
1490
+ import { join as join5 } from "path";
1740
1491
  import whichSync from "which";
1741
1492
 
1742
1493
  // src/tools/lsp/config-store.ts
1743
1494
  var userConfig = /* @__PURE__ */ new Map();
1744
- function setUserLspConfig(config) {
1745
- userConfig.clear();
1746
- if (config) {
1747
- for (const [id, server] of Object.entries(config)) {
1748
- if (server && typeof server === "object") {
1749
- const s = server;
1750
- userConfig.set(id, {
1751
- id,
1752
- command: s.command,
1753
- extensions: s.extensions,
1754
- disabled: s.disabled,
1755
- env: s.env,
1756
- initialization: s.initialization
1757
- });
1758
- }
1759
- }
1760
- }
1761
- }
1762
1495
  function getAllUserLspConfigs() {
1763
1496
  return new Map(userConfig);
1764
1497
  }
@@ -1767,8 +1500,8 @@ function hasUserLspConfig() {
1767
1500
  }
1768
1501
 
1769
1502
  // src/tools/lsp/constants.ts
1770
- import { existsSync as existsSync6, readdirSync, statSync as statSync3 } from "fs";
1771
- import { dirname as dirname3, resolve } from "path";
1503
+ import { existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
1504
+ import { dirname as dirname2, resolve as resolve2 } from "path";
1772
1505
  var SEVERITY_MAP = {
1773
1506
  1: "error",
1774
1507
  2: "warning",
@@ -1785,20 +1518,20 @@ var LOCK_FILE_PATTERNS = [
1785
1518
  "bun.lock"
1786
1519
  ];
1787
1520
  function* walkUpDirectories(start, stop) {
1788
- let dir = resolve(start);
1521
+ let dir = resolve2(start);
1789
1522
  try {
1790
- if (!statSync3(dir).isDirectory()) {
1791
- dir = dirname3(dir);
1523
+ if (!statSync2(dir).isDirectory()) {
1524
+ dir = dirname2(dir);
1792
1525
  }
1793
1526
  } catch {
1794
- dir = dirname3(dir);
1527
+ dir = dirname2(dir);
1795
1528
  }
1796
1529
  let prevDir = "";
1797
1530
  while (dir !== prevDir && dir !== "/") {
1798
1531
  yield dir;
1799
1532
  prevDir = dir;
1800
1533
  if (dir === stop) break;
1801
- dir = dirname3(dir);
1534
+ dir = dirname2(dir);
1802
1535
  }
1803
1536
  }
1804
1537
  function NearestRoot(includePatterns, excludePatterns) {
@@ -1807,7 +1540,7 @@ function NearestRoot(includePatterns, excludePatterns) {
1807
1540
  if (excludePatterns) {
1808
1541
  for (const dir of walkUpDirectories(file, cwd)) {
1809
1542
  for (const pattern of excludePatterns) {
1810
- if (existsSync6(`${dir}/${pattern}`)) {
1543
+ if (existsSync5(`${dir}/${pattern}`)) {
1811
1544
  return void 0;
1812
1545
  }
1813
1546
  }
@@ -1826,7 +1559,7 @@ function NearestRoot(includePatterns, excludePatterns) {
1826
1559
  }
1827
1560
  } catch {
1828
1561
  }
1829
- } else if (existsSync6(`${dir}/${pattern}`)) {
1562
+ } else if (existsSync5(`${dir}/${pattern}`)) {
1830
1563
  return dir;
1831
1564
  }
1832
1565
  }
@@ -2448,11 +2181,11 @@ function isServerInstalled(command) {
2448
2181
  if (command.length === 0) return false;
2449
2182
  const cmd = command[0];
2450
2183
  if (cmd.includes("/") || cmd.includes("\\")) {
2451
- return existsSync7(cmd);
2184
+ return existsSync6(cmd);
2452
2185
  }
2453
2186
  const isWindows = process.platform === "win32";
2454
2187
  const ext = isWindows ? ".exe" : "";
2455
- const opencodeBin = join6(homedir3(), ".config", "opencode", "bin");
2188
+ const opencodeBin = join5(homedir2(), ".config", "opencode", "bin");
2456
2189
  const searchPath = (process.env.PATH ?? "") + (isWindows ? ";" : ":") + opencodeBin;
2457
2190
  const result = whichSync.sync(cmd, {
2458
2191
  path: searchPath,
@@ -2463,8 +2196,8 @@ function isServerInstalled(command) {
2463
2196
  return true;
2464
2197
  }
2465
2198
  const cwd = process.cwd();
2466
- const localBin = join6(cwd, "node_modules", ".bin", cmd);
2467
- if (existsSync7(localBin) || existsSync7(localBin + ext)) {
2199
+ const localBin = join5(cwd, "node_modules", ".bin", cmd);
2200
+ if (existsSync6(localBin) || existsSync6(localBin + ext)) {
2468
2201
  return true;
2469
2202
  }
2470
2203
  return false;
@@ -2725,7 +2458,7 @@ var LSPClient = class {
2725
2458
  this.processExited = true;
2726
2459
  });
2727
2460
  this.connection.listen();
2728
- await new Promise((resolve4) => setTimeout(resolve4, 100));
2461
+ await new Promise((resolve5) => setTimeout(resolve5, 100));
2729
2462
  if (this.proc.exitCode !== null) {
2730
2463
  const stderr = this.stderrBuffer.join("\n");
2731
2464
  log("[lsp] LSPClient.start: server exited immediately", {
@@ -2800,12 +2533,12 @@ stderr: ${stderr}` : "")
2800
2533
  log("[lsp] LSPClient.initialize: complete", { server: this.server.id });
2801
2534
  }
2802
2535
  async openFile(filePath) {
2803
- const absPath = resolve2(filePath);
2536
+ const absPath = resolve3(filePath);
2804
2537
  if (this.openedFiles.has(absPath)) {
2805
2538
  log("[lsp] openFile: already open, skipping", { filePath: absPath });
2806
2539
  return;
2807
2540
  }
2808
- const text = readFileSync3(absPath, "utf-8");
2541
+ const text = readFileSync2(absPath, "utf-8");
2809
2542
  const ext = extname(absPath);
2810
2543
  const languageId = getLanguageId(ext);
2811
2544
  log("[lsp] openFile: opening document", {
@@ -2825,7 +2558,7 @@ stderr: ${stderr}` : "")
2825
2558
  await new Promise((r) => setTimeout(r, 1e3));
2826
2559
  }
2827
2560
  async definition(filePath, line, character) {
2828
- const absPath = resolve2(filePath);
2561
+ const absPath = resolve3(filePath);
2829
2562
  await this.openFile(absPath);
2830
2563
  return this.connection?.sendRequest("textDocument/definition", {
2831
2564
  textDocument: { uri: pathToFileURL(absPath).href },
@@ -2833,7 +2566,7 @@ stderr: ${stderr}` : "")
2833
2566
  });
2834
2567
  }
2835
2568
  async references(filePath, line, character, includeDeclaration = true) {
2836
- const absPath = resolve2(filePath);
2569
+ const absPath = resolve3(filePath);
2837
2570
  await this.openFile(absPath);
2838
2571
  return this.connection?.sendRequest("textDocument/references", {
2839
2572
  textDocument: { uri: pathToFileURL(absPath).href },
@@ -2842,7 +2575,7 @@ stderr: ${stderr}` : "")
2842
2575
  });
2843
2576
  }
2844
2577
  async diagnostics(filePath) {
2845
- const absPath = resolve2(filePath);
2578
+ const absPath = resolve3(filePath);
2846
2579
  const uri = pathToFileURL(absPath).href;
2847
2580
  await this.openFile(absPath);
2848
2581
  await new Promise((r) => setTimeout(r, 500));
@@ -2861,7 +2594,7 @@ stderr: ${stderr}` : "")
2861
2594
  return { items: this.diagnosticsStore.get(uri) ?? [] };
2862
2595
  }
2863
2596
  async rename(filePath, line, character, newName) {
2864
- const absPath = resolve2(filePath);
2597
+ const absPath = resolve3(filePath);
2865
2598
  await this.openFile(absPath);
2866
2599
  return this.connection?.sendRequest("textDocument/rename", {
2867
2600
  textDocument: { uri: pathToFileURL(absPath).href },
@@ -2892,23 +2625,23 @@ stderr: ${stderr}` : "")
2892
2625
  };
2893
2626
 
2894
2627
  // src/tools/lsp/tools.ts
2895
- import { tool as tool2 } from "@opencode-ai/plugin/tool";
2628
+ import { z as z2 } from "zod";
2896
2629
 
2897
2630
  // src/tools/lsp/utils.ts
2898
2631
  import {
2899
- existsSync as existsSync8,
2900
- readFileSync as readFileSync4,
2901
- statSync as statSync4,
2632
+ existsSync as existsSync7,
2633
+ readFileSync as readFileSync3,
2634
+ statSync as statSync3,
2902
2635
  unlinkSync as unlinkSync2,
2903
2636
  writeFileSync
2904
2637
  } from "fs";
2905
- import { dirname as dirname4, extname as extname2, join as join7, resolve as resolve3 } from "path";
2638
+ import { dirname as dirname3, extname as extname2, join as join6, resolve as resolve4 } from "path";
2906
2639
  import { fileURLToPath as fileURLToPath2 } from "url";
2907
2640
  function findServerProjectRoot(filePath, server) {
2908
2641
  if (server.root) {
2909
- return server.root(filePath) ?? dirname4(resolve3(filePath));
2642
+ return server.root(filePath) ?? dirname3(resolve4(filePath));
2910
2643
  }
2911
- return dirname4(resolve3(filePath));
2644
+ return dirname3(resolve4(filePath));
2912
2645
  }
2913
2646
  function uriToPath(uri) {
2914
2647
  return fileURLToPath2(uri);
@@ -2926,7 +2659,7 @@ function formatServerLookupError(result) {
2926
2659
  return `No LSP server configured for extension: ${result.extension}`;
2927
2660
  }
2928
2661
  async function withLspClient(filePath, fn) {
2929
- const absPath = resolve3(filePath);
2662
+ const absPath = resolve4(filePath);
2930
2663
  const ext = extname2(absPath);
2931
2664
  const result = findServerForExtension(ext);
2932
2665
  if (result.status !== "found") {
@@ -2937,7 +2670,7 @@ async function withLspClient(filePath, fn) {
2937
2670
  throw new Error(formatServerLookupError(result));
2938
2671
  }
2939
2672
  const server = result.server;
2940
- const root = findServerProjectRoot(absPath, server) ?? dirname4(absPath);
2673
+ const root = findServerProjectRoot(absPath, server) ?? dirname3(absPath);
2941
2674
  log("[lsp] withLspClient: acquiring client", {
2942
2675
  filePath: absPath,
2943
2676
  server: server.id,
@@ -3008,7 +2741,7 @@ function filterDiagnosticsBySeverity(diagnostics, severityFilter) {
3008
2741
  }
3009
2742
  function applyTextEditsToFile(filePath, edits) {
3010
2743
  try {
3011
- const content = readFileSync4(filePath, "utf-8");
2744
+ const content = readFileSync3(filePath, "utf-8");
3012
2745
  const lines = content.split("\n");
3013
2746
  const sortedEdits = [...edits].sort((a, b) => {
3014
2747
  if (b.range.start.line !== a.range.start.line) {
@@ -3092,7 +2825,7 @@ function applyWorkspaceEdit(edit) {
3092
2825
  try {
3093
2826
  const oldPath = uriToPath(change.oldUri);
3094
2827
  const newPath = uriToPath(change.newUri);
3095
- const content = readFileSync4(oldPath, "utf-8");
2828
+ const content = readFileSync3(oldPath, "utf-8");
3096
2829
  writeFileSync(newPath, content, "utf-8");
3097
2830
  unlinkSync2(oldPath);
3098
2831
  result.filesModified.push(newPath);
@@ -3154,16 +2887,18 @@ function formatApplyResult(result) {
3154
2887
 
3155
2888
  // src/tools/lsp/tools.ts
3156
2889
  var formatError = (e) => `Error: ${e instanceof Error ? e.message : String(e)}`;
3157
- var lsp_goto_definition = tool2({
2890
+ var lsp_goto_definition = defineTool({
2891
+ name: "lsp_goto_definition",
3158
2892
  description: "Jump to symbol definition. Find WHERE something is defined.",
3159
- args: {
3160
- filePath: tool2.schema.string().describe("Absolute path to the file"),
3161
- line: tool2.schema.number().min(1).describe("1-based line number"),
3162
- character: tool2.schema.number().min(0).describe("0-based character offset")
3163
- },
3164
- execute: async (args) => {
2893
+ input: z2.object({
2894
+ filePath: z2.string().describe("Absolute path to the file"),
2895
+ line: z2.number().min(1).describe("1-based line number"),
2896
+ character: z2.number().min(0).describe("0-based character offset")
2897
+ }),
2898
+ execute: async (args, context) => {
3165
2899
  try {
3166
2900
  const result = await withLspClient(args.filePath, async (client) => {
2901
+ context.signal.throwIfAborted();
3167
2902
  return await client.definition(
3168
2903
  args.filePath,
3169
2904
  args.line,
@@ -3183,17 +2918,19 @@ var lsp_goto_definition = tool2({
3183
2918
  }
3184
2919
  }
3185
2920
  });
3186
- var lsp_find_references = tool2({
2921
+ var lsp_find_references = defineTool({
2922
+ name: "lsp_find_references",
3187
2923
  description: "Find ALL usages/references of a symbol across the entire workspace.",
3188
- args: {
3189
- filePath: tool2.schema.string().describe("Absolute path to the file"),
3190
- line: tool2.schema.number().min(1).describe("1-based line number"),
3191
- character: tool2.schema.number().min(0).describe("0-based character offset"),
3192
- includeDeclaration: tool2.schema.boolean().optional().describe("Include the declaration itself")
3193
- },
3194
- execute: async (args) => {
2924
+ input: z2.object({
2925
+ filePath: z2.string().describe("Absolute path to the file"),
2926
+ line: z2.number().min(1).describe("1-based line number"),
2927
+ character: z2.number().min(0).describe("0-based character offset"),
2928
+ includeDeclaration: z2.boolean().optional().describe("Include the declaration itself")
2929
+ }),
2930
+ execute: async (args, context) => {
3195
2931
  try {
3196
2932
  const result = await withLspClient(args.filePath, async (client) => {
2933
+ context.signal.throwIfAborted();
3197
2934
  return await client.references(
3198
2935
  args.filePath,
3199
2936
  args.line,
@@ -3219,15 +2956,17 @@ var lsp_find_references = tool2({
3219
2956
  }
3220
2957
  }
3221
2958
  });
3222
- var lsp_diagnostics = tool2({
2959
+ var lsp_diagnostics = defineTool({
2960
+ name: "lsp_diagnostics",
3223
2961
  description: "Get errors, warnings, hints from language server BEFORE running build.",
3224
- args: {
3225
- filePath: tool2.schema.string().describe("Absolute path to the file"),
3226
- severity: tool2.schema.enum(["error", "warning", "information", "hint", "all"]).optional().describe("Filter by severity level")
3227
- },
3228
- execute: async (args) => {
2962
+ input: z2.object({
2963
+ filePath: z2.string().describe("Absolute path to the file"),
2964
+ severity: z2.enum(["error", "warning", "information", "hint", "all"]).optional().describe("Filter by severity level")
2965
+ }),
2966
+ execute: async (args, context) => {
3229
2967
  try {
3230
2968
  const result = await withLspClient(args.filePath, async (client) => {
2969
+ context.signal.throwIfAborted();
3231
2970
  return await client.diagnostics(args.filePath);
3232
2971
  });
3233
2972
  let diagnostics = [];
@@ -3257,17 +2996,19 @@ var lsp_diagnostics = tool2({
3257
2996
  }
3258
2997
  }
3259
2998
  });
3260
- var lsp_rename = tool2({
2999
+ var lsp_rename = defineTool({
3000
+ name: "lsp_rename",
3261
3001
  description: "Rename symbol across entire workspace. APPLIES changes to all files.",
3262
- args: {
3263
- filePath: tool2.schema.string().describe("Absolute path to the file"),
3264
- line: tool2.schema.number().min(1).describe("1-based line number"),
3265
- character: tool2.schema.number().min(0).describe("0-based character offset"),
3266
- newName: tool2.schema.string().describe("New symbol name")
3267
- },
3268
- execute: async (args) => {
3002
+ input: z2.object({
3003
+ filePath: z2.string().describe("Absolute path to the file"),
3004
+ line: z2.number().min(1).describe("1-based line number"),
3005
+ character: z2.number().min(0).describe("0-based character offset"),
3006
+ newName: z2.string().describe("New symbol name")
3007
+ }),
3008
+ execute: async (args, context) => {
3269
3009
  try {
3270
3010
  const edit = await withLspClient(args.filePath, async (client) => {
3011
+ context.signal.throwIfAborted();
3271
3012
  return await client.rename(
3272
3013
  args.filePath,
3273
3014
  args.line,
@@ -3275,6 +3016,7 @@ var lsp_rename = tool2({
3275
3016
  args.newName
3276
3017
  );
3277
3018
  });
3019
+ context.signal.throwIfAborted();
3278
3020
  const result = applyWorkspaceEdit(edit);
3279
3021
  return formatApplyResult(result);
3280
3022
  } catch (e) {
@@ -3283,375 +3025,34 @@ var lsp_rename = tool2({
3283
3025
  }
3284
3026
  });
3285
3027
 
3286
- // src/utils/tmux-session-manager.ts
3287
- var SESSION_TIMEOUT_MS = 10 * 60 * 1e3;
3288
- var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
3289
- var TmuxSessionManager = class {
3290
- client;
3291
- tmuxConfig;
3292
- serverUrl;
3293
- sessions = /* @__PURE__ */ new Map();
3294
- pollInterval;
3295
- enabled = false;
3296
- constructor(ctx, tmuxConfig) {
3297
- this.client = ctx.client;
3298
- this.tmuxConfig = tmuxConfig;
3299
- const defaultPort = process.env.OPENCODE_PORT ?? "4096";
3300
- this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
3301
- this.enabled = tmuxConfig.enabled && isInsideTmux();
3302
- log("[tmux-session-manager] initialized", {
3303
- enabled: this.enabled,
3304
- tmuxConfig: this.tmuxConfig,
3305
- serverUrl: this.serverUrl
3306
- });
3307
- }
3308
- /**
3309
- * Handle session.created events.
3310
- * Spawns a tmux pane for child sessions (those with parentID).
3311
- */
3312
- async onSessionCreated(event) {
3313
- if (!this.enabled) return;
3314
- if (event.type !== "session.created") return;
3315
- const info = event.properties?.info;
3316
- if (!info?.id || !info?.parentID) {
3317
- return;
3318
- }
3319
- const sessionId = info.id;
3320
- const parentId = info.parentID;
3321
- const title = info.title ?? "Subagent";
3322
- if (this.sessions.has(sessionId)) {
3323
- log("[tmux-session-manager] session already tracked", { sessionId });
3324
- return;
3325
- }
3326
- log("[tmux-session-manager] child session created, spawning pane", {
3327
- sessionId,
3328
- parentId,
3329
- title
3330
- });
3331
- const paneResult = await spawnTmuxPane(
3332
- sessionId,
3333
- title,
3334
- this.tmuxConfig,
3335
- this.serverUrl
3336
- ).catch((err) => {
3337
- log("[tmux-session-manager] failed to spawn pane", {
3338
- error: String(err)
3339
- });
3340
- return { success: false, paneId: void 0 };
3341
- });
3342
- if (paneResult.success && paneResult.paneId) {
3343
- const now = Date.now();
3344
- this.sessions.set(sessionId, {
3345
- sessionId,
3346
- paneId: paneResult.paneId,
3347
- parentId,
3348
- title,
3349
- createdAt: now,
3350
- lastSeenAt: now
3351
- });
3352
- log("[tmux-session-manager] pane spawned", {
3353
- sessionId,
3354
- paneId: paneResult.paneId
3355
- });
3356
- this.startPolling();
3357
- }
3358
- }
3359
- /**
3360
- * Handle session.status events for completion detection.
3361
- * Uses session.status instead of deprecated session.idle.
3362
- *
3363
- * When a session becomes idle (completed), close its pane.
3364
- */
3365
- async onSessionStatus(event) {
3366
- if (!this.enabled) return;
3367
- if (event.type !== "session.status") return;
3368
- const sessionId = event.properties?.sessionID;
3369
- if (!sessionId) return;
3370
- if (event.properties?.status?.type === "idle") {
3371
- await this.closeSession(sessionId);
3372
- }
3373
- }
3374
- /**
3375
- * Handle session.deleted events.
3376
- * When a session is deleted, close its tmux pane immediately.
3377
- */
3378
- async onSessionDeleted(event) {
3379
- if (!this.enabled) return;
3380
- if (event.type !== "session.deleted") return;
3381
- const sessionId = event.properties?.sessionID;
3382
- if (!sessionId) return;
3383
- log("[tmux-session-manager] session deleted, closing pane", {
3384
- sessionId
3385
- });
3386
- await this.closeSession(sessionId);
3387
- }
3388
- startPolling() {
3389
- if (this.pollInterval) return;
3390
- this.pollInterval = setInterval(
3391
- () => this.pollSessions(),
3392
- POLL_INTERVAL_BACKGROUND_MS
3393
- );
3394
- log("[tmux-session-manager] polling started");
3395
- }
3396
- stopPolling() {
3397
- if (this.pollInterval) {
3398
- clearInterval(this.pollInterval);
3399
- this.pollInterval = void 0;
3400
- log("[tmux-session-manager] polling stopped");
3401
- }
3402
- }
3403
- /**
3404
- * Poll sessions for status updates (fallback for reliability).
3405
- * Also handles timeout and missing session detection.
3406
- */
3407
- async pollSessions() {
3408
- if (this.sessions.size === 0) {
3409
- this.stopPolling();
3410
- return;
3411
- }
3412
- try {
3413
- const statusResult = await this.client.session.status();
3414
- const allStatuses = statusResult.data ?? {};
3415
- const now = Date.now();
3416
- const sessionsToClose = [];
3417
- for (const [sessionId, tracked] of this.sessions.entries()) {
3418
- const status = allStatuses[sessionId];
3419
- const isIdle = status?.type === "idle";
3420
- if (status) {
3421
- tracked.lastSeenAt = now;
3422
- tracked.missingSince = void 0;
3423
- } else if (!tracked.missingSince) {
3424
- tracked.missingSince = now;
3425
- }
3426
- const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
3427
- const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
3428
- if (isIdle || missingTooLong || isTimedOut) {
3429
- sessionsToClose.push(sessionId);
3430
- }
3431
- }
3432
- for (const sessionId of sessionsToClose) {
3433
- await this.closeSession(sessionId);
3434
- }
3435
- } catch (err) {
3436
- log("[tmux-session-manager] poll error", { error: String(err) });
3437
- }
3438
- }
3439
- async closeSession(sessionId) {
3440
- const tracked = this.sessions.get(sessionId);
3441
- if (!tracked) return;
3442
- log("[tmux-session-manager] closing session pane", {
3443
- sessionId,
3444
- paneId: tracked.paneId
3445
- });
3446
- await closeTmuxPane(tracked.paneId);
3447
- this.sessions.delete(sessionId);
3448
- if (this.sessions.size === 0) {
3449
- this.stopPolling();
3450
- }
3451
- }
3452
- /**
3453
- * Clean up all tracked sessions.
3454
- */
3455
- async cleanup() {
3456
- this.stopPolling();
3457
- if (this.sessions.size > 0) {
3458
- log("[tmux-session-manager] closing all panes", {
3459
- count: this.sessions.size
3460
- });
3461
- const closePromises = Array.from(this.sessions.values()).map(
3462
- (s) => closeTmuxPane(s.paneId).catch(
3463
- (err) => log("[tmux-session-manager] cleanup error for pane", {
3464
- paneId: s.paneId,
3465
- error: String(err)
3466
- })
3467
- )
3468
- );
3469
- await Promise.all(closePromises);
3470
- this.sessions.clear();
3471
- }
3472
- log("[tmux-session-manager] cleanup complete");
3473
- }
3474
- };
3028
+ // src/opencode/tools.ts
3029
+ async function registerTools(ctx, _deps) {
3030
+ await ctx.tool.transform((editor) => {
3031
+ editor.add(lsp_goto_definition);
3032
+ editor.add(lsp_find_references);
3033
+ editor.add(lsp_diagnostics);
3034
+ editor.add(lsp_rename);
3035
+ editor.add(ast_grep_search);
3036
+ editor.add(ast_grep_replace);
3037
+ });
3038
+ }
3475
3039
 
3476
3040
  // src/index.ts
3477
- var ThothAgents = async (ctx, _options) => {
3478
- const { client, directory } = ctx;
3479
- const config = loadPluginConfig(directory);
3480
- const agentDefs = createAgents(config);
3481
- const agents = renderOpenCodeAgentConfigs(config);
3482
- const packageRoot = fileURLToPath3(new URL("..", import.meta.url));
3483
- const thothInitCommand = createOpenCodeInitCommand({
3484
- projectRoot: directory,
3485
- packageRoot
3486
- });
3487
- const modelArrayMap = {};
3488
- for (const agentDef of agentDefs) {
3489
- if (agentDef._modelArray && agentDef._modelArray.length > 0) {
3490
- modelArrayMap[agentDef.name] = agentDef._modelArray;
3491
- }
3492
- }
3493
- const runtimeChains = {};
3494
- for (const agentDef of agentDefs) {
3495
- if (agentDef._modelArray?.length) {
3496
- runtimeChains[agentDef.name] = agentDef._modelArray.map((m) => m.id);
3497
- }
3498
- }
3499
- if (config.fallback?.enabled !== false) {
3500
- const chains = config.fallback?.chains ?? {};
3501
- for (const [agentName, chainModels] of Object.entries(chains)) {
3502
- if (!chainModels?.length) continue;
3503
- const existing = runtimeChains[agentName] ?? [];
3504
- const seen = new Set(existing);
3505
- for (const m of chainModels) {
3506
- if (!seen.has(m)) {
3507
- seen.add(m);
3508
- existing.push(m);
3509
- }
3510
- }
3511
- runtimeChains[agentName] = existing;
3512
- }
3041
+ var plugin = {
3042
+ id: "thoth-agents",
3043
+ async setup(ctx) {
3044
+ const deps = loadOpenCodeDependencies(ctx);
3045
+ await registerAgents(ctx, deps);
3046
+ await registerTools(ctx, deps);
3047
+ await registerMcp(ctx, deps);
3048
+ await registerCommand(ctx, deps);
3049
+ await registerSkills(ctx, deps);
3050
+ await registerPermissions(ctx, deps);
3051
+ await registerDelegationHooks(ctx, deps);
3052
+ await registerSessionHooks(ctx, deps);
3513
3053
  }
3514
- const tmuxConfig = {
3515
- enabled: config.tmux?.enabled ?? false,
3516
- layout: config.tmux?.layout ?? "main-vertical",
3517
- main_pane_size: config.tmux?.main_pane_size ?? 60
3518
- };
3519
- log("[plugin] initialized with tmux config", {
3520
- tmuxConfig,
3521
- rawTmuxConfig: config.tmux,
3522
- directory
3523
- });
3524
- if (tmuxConfig.enabled) {
3525
- startTmuxCheck();
3526
- }
3527
- const mcps = createBuiltinMcps(config.disabled_mcps);
3528
- const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
3529
- const autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
3530
- showStartupToast: true
3531
- });
3532
- const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
3533
- const jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
3534
- const oracleGateVerdictHook = createOracleGateVerdictHook(ctx);
3535
- const foregroundFallback = new ForegroundFallbackManager(
3536
- client,
3537
- runtimeChains,
3538
- config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0
3539
- );
3540
- return {
3541
- name: "thoth-agents",
3542
- agent: agents,
3543
- tool: {
3544
- lsp_goto_definition,
3545
- lsp_find_references,
3546
- lsp_diagnostics,
3547
- lsp_rename,
3548
- ast_grep_search,
3549
- ast_grep_replace
3550
- },
3551
- mcp: mcps,
3552
- config: async (opencodeConfig) => {
3553
- const lspConfig = opencodeConfig.lsp;
3554
- setUserLspConfig(lspConfig);
3555
- const commands = opencodeConfig.command ?? {};
3556
- if (!commands["thoth-init"]) {
3557
- commands["thoth-init"] = thothInitCommand;
3558
- }
3559
- opencodeConfig.command = commands;
3560
- if (config.setDefaultAgent !== false && !opencodeConfig.default_agent) {
3561
- opencodeConfig.default_agent = "orchestrator";
3562
- }
3563
- if (!opencodeConfig.agent) {
3564
- opencodeConfig.agent = { ...agents };
3565
- } else {
3566
- for (const [name, pluginAgent] of Object.entries(agents)) {
3567
- const existing = opencodeConfig.agent[name];
3568
- if (existing) {
3569
- opencodeConfig.agent[name] = {
3570
- ...pluginAgent,
3571
- ...existing
3572
- };
3573
- } else {
3574
- opencodeConfig.agent[name] = {
3575
- ...pluginAgent
3576
- };
3577
- }
3578
- }
3579
- }
3580
- const configAgent = opencodeConfig.agent;
3581
- const fallbackChainsEnabled = config.fallback?.enabled !== false;
3582
- const fallbackChains = fallbackChainsEnabled ? config.fallback?.chains ?? {} : {};
3583
- const effectiveArrays = {};
3584
- for (const [agentName, models] of Object.entries(modelArrayMap)) {
3585
- effectiveArrays[agentName] = [...models];
3586
- }
3587
- for (const [agentName, chainModels] of Object.entries(fallbackChains)) {
3588
- if (!chainModels || chainModels.length === 0) continue;
3589
- if (!effectiveArrays[agentName]) {
3590
- const entry = configAgent[agentName];
3591
- const currentModel = typeof entry?.model === "string" ? entry.model : void 0;
3592
- effectiveArrays[agentName] = currentModel ? [{ id: currentModel }] : [];
3593
- }
3594
- const seen = new Set(effectiveArrays[agentName].map((m) => m.id));
3595
- for (const chainModel of chainModels) {
3596
- if (!seen.has(chainModel)) {
3597
- seen.add(chainModel);
3598
- effectiveArrays[agentName].push({ id: chainModel });
3599
- }
3600
- }
3601
- }
3602
- if (Object.keys(effectiveArrays).length > 0) {
3603
- for (const [agentName, modelArray] of Object.entries(effectiveArrays)) {
3604
- if (modelArray.length === 0) continue;
3605
- const chosen = modelArray[0];
3606
- const entry = configAgent[agentName];
3607
- if (entry) {
3608
- entry.model = chosen.id;
3609
- if (chosen.variant) {
3610
- entry.variant = chosen.variant;
3611
- }
3612
- }
3613
- log("[plugin] resolved model from array", {
3614
- agent: agentName,
3615
- model: chosen.id,
3616
- variant: chosen.variant
3617
- });
3618
- }
3619
- }
3620
- const configMcp = opencodeConfig.mcp;
3621
- if (!configMcp) {
3622
- opencodeConfig.mcp = { ...mcps };
3623
- } else {
3624
- Object.assign(configMcp, mcps);
3625
- }
3626
- },
3627
- event: async (input) => {
3628
- await foregroundFallback.handleEvent(input.event);
3629
- await autoUpdateChecker.event(input);
3630
- await tmuxSessionManager.onSessionCreated(
3631
- input.event
3632
- );
3633
- await tmuxSessionManager.onSessionStatus(
3634
- input.event
3635
- );
3636
- await tmuxSessionManager.onSessionDeleted(
3637
- input.event
3638
- );
3639
- },
3640
- // Post-tool hooks: delegation retry, JSON recovery, then Oracle gate verdicts.
3641
- "tool.execute.after": async (input, output) => {
3642
- await delegateTaskRetryHook["tool.execute.after"](
3643
- input,
3644
- output
3645
- );
3646
- await jsonErrorRecoveryHook["tool.execute.after"](
3647
- input,
3648
- output
3649
- );
3650
- await oracleGateVerdictHook["tool.execute.after"](input, output);
3651
- }
3652
- };
3653
3054
  };
3654
- var src_default = ThothAgents;
3055
+ var src_default = plugin;
3655
3056
  export {
3656
3057
  src_default as default
3657
3058
  };