oh-my-opencode-slim 0.8.3 → 0.8.5

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 (38) hide show
  1. package/README.md +35 -7
  2. package/dist/agents/council-master.d.ts +2 -0
  3. package/dist/agents/council.d.ts +28 -0
  4. package/dist/agents/councillor.d.ts +2 -0
  5. package/dist/agents/orchestrator.d.ts +6 -0
  6. package/dist/background/background-manager.d.ts +6 -1
  7. package/dist/background/index.d.ts +1 -0
  8. package/dist/background/subagent-depth.d.ts +35 -0
  9. package/dist/cli/config-io.d.ts +1 -1
  10. package/dist/cli/custom-skills.d.ts +2 -2
  11. package/dist/cli/index.js +173 -56
  12. package/dist/cli/paths.d.ts +22 -0
  13. package/dist/cli/providers.d.ts +4 -4
  14. package/dist/cli/types.d.ts +2 -0
  15. package/dist/config/constants.d.ts +7 -2
  16. package/dist/config/council-schema.d.ts +134 -0
  17. package/dist/config/index.d.ts +1 -0
  18. package/dist/config/loader.d.ts +2 -1
  19. package/dist/config/schema.d.ts +27 -0
  20. package/dist/council/council-manager.d.ts +40 -0
  21. package/dist/council/index.d.ts +1 -0
  22. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  23. package/dist/hooks/index.d.ts +2 -1
  24. package/dist/hooks/json-error-recovery/hook.d.ts +1 -1
  25. package/dist/hooks/phase-reminder/index.d.ts +1 -1
  26. package/dist/hooks/post-file-tool-nudge/index.d.ts +18 -0
  27. package/dist/index.js +2273 -879
  28. package/dist/tools/council.d.ts +9 -0
  29. package/dist/tools/index.d.ts +2 -2
  30. package/dist/tools/lsp/config-store.d.ts +29 -0
  31. package/dist/tools/lsp/constants.d.ts +18 -2
  32. package/dist/tools/lsp/index.d.ts +1 -0
  33. package/dist/tools/lsp/types.d.ts +7 -0
  34. package/dist/tools/lsp/utils.d.ts +14 -1
  35. package/dist/utils/index.d.ts +1 -0
  36. package/dist/utils/session.d.ts +59 -0
  37. package/oh-my-opencode-slim.schema.json +78 -0
  38. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -3037,7 +3037,7 @@ var require_main = __commonJS((exports) => {
3037
3037
  var ril_1 = require_ril();
3038
3038
  ril_1.default.install();
3039
3039
  var path6 = __require("path");
3040
- var os4 = __require("os");
3040
+ var os3 = __require("os");
3041
3041
  var crypto_1 = __require("crypto");
3042
3042
  var net_1 = __require("net");
3043
3043
  var api_1 = require_api();
@@ -3180,7 +3180,7 @@ var require_main = __commonJS((exports) => {
3180
3180
  if (XDG_RUNTIME_DIR) {
3181
3181
  result = path6.join(XDG_RUNTIME_DIR, `vscode-ipc-${randomSuffix}.sock`);
3182
3182
  } else {
3183
- result = path6.join(os4.tmpdir(), `vscode-${randomSuffix}.sock`);
3183
+ result = path6.join(os3.tmpdir(), `vscode-${randomSuffix}.sock`);
3184
3184
  }
3185
3185
  const limit = safeIpcPathLengths.get(process.platform);
3186
3186
  if (limit !== undefined && result.length > limit) {
@@ -3277,6 +3277,242 @@ var require_main = __commonJS((exports) => {
3277
3277
  exports.createMessageConnection = createMessageConnection;
3278
3278
  });
3279
3279
 
3280
+ // node_modules/isexe/dist/commonjs/index.min.js
3281
+ var require_index_min = __commonJS((exports) => {
3282
+ var a = (t, e) => () => (e || t((e = { exports: {} }).exports, e), e.exports);
3283
+ var _ = a((i) => {
3284
+ Object.defineProperty(i, "__esModule", { value: true });
3285
+ i.sync = i.isexe = undefined;
3286
+ var M = __require("fs"), x = __require("fs/promises"), q = async (t, e = {}) => {
3287
+ let { ignoreErrors: r = false } = e;
3288
+ try {
3289
+ return d(await (0, x.stat)(t), e);
3290
+ } catch (s) {
3291
+ let n = s;
3292
+ if (r || n.code === "EACCES")
3293
+ return false;
3294
+ throw n;
3295
+ }
3296
+ };
3297
+ i.isexe = q;
3298
+ var m = (t, e = {}) => {
3299
+ let { ignoreErrors: r = false } = e;
3300
+ try {
3301
+ return d((0, M.statSync)(t), e);
3302
+ } catch (s) {
3303
+ let n = s;
3304
+ if (r || n.code === "EACCES")
3305
+ return false;
3306
+ throw n;
3307
+ }
3308
+ };
3309
+ i.sync = m;
3310
+ var d = (t, e) => t.isFile() && A(t, e), A = (t, e) => {
3311
+ let r = e.uid ?? process.getuid?.(), s = e.groups ?? process.getgroups?.() ?? [], n = e.gid ?? process.getgid?.() ?? s[0];
3312
+ if (r === undefined || n === undefined)
3313
+ throw new Error("cannot get uid or gid");
3314
+ let u = new Set([n, ...s]), c = t.mode, S = t.uid, P = t.gid, f = parseInt("100", 8), l = parseInt("010", 8), j = parseInt("001", 8), C = f | l;
3315
+ return !!(c & j || c & l && u.has(P) || c & f && S === r || c & C && r === 0);
3316
+ };
3317
+ });
3318
+ var g = a((o) => {
3319
+ Object.defineProperty(o, "__esModule", { value: true });
3320
+ o.sync = o.isexe = undefined;
3321
+ var T = __require("fs"), I = __require("fs/promises"), D = __require("path"), F = async (t, e = {}) => {
3322
+ let { ignoreErrors: r = false } = e;
3323
+ try {
3324
+ return y(await (0, I.stat)(t), t, e);
3325
+ } catch (s) {
3326
+ let n = s;
3327
+ if (r || n.code === "EACCES")
3328
+ return false;
3329
+ throw n;
3330
+ }
3331
+ };
3332
+ o.isexe = F;
3333
+ var L = (t, e = {}) => {
3334
+ let { ignoreErrors: r = false } = e;
3335
+ try {
3336
+ return y((0, T.statSync)(t), t, e);
3337
+ } catch (s) {
3338
+ let n = s;
3339
+ if (r || n.code === "EACCES")
3340
+ return false;
3341
+ throw n;
3342
+ }
3343
+ };
3344
+ o.sync = L;
3345
+ var B = (t, e) => {
3346
+ let { pathExt: r = process.env.PATHEXT || "" } = e, s = r.split(D.delimiter);
3347
+ if (s.indexOf("") !== -1)
3348
+ return true;
3349
+ for (let n of s) {
3350
+ let u = n.toLowerCase(), c = t.substring(t.length - u.length).toLowerCase();
3351
+ if (u && c === u)
3352
+ return true;
3353
+ }
3354
+ return false;
3355
+ }, y = (t, e, r) => t.isFile() && B(e, r);
3356
+ });
3357
+ var p = a((h) => {
3358
+ Object.defineProperty(h, "__esModule", { value: true });
3359
+ });
3360
+ var v = exports && exports.__createBinding || (Object.create ? function(t, e, r, s) {
3361
+ s === undefined && (s = r);
3362
+ var n = Object.getOwnPropertyDescriptor(e, r);
3363
+ (!n || ("get" in n ? !e.__esModule : n.writable || n.configurable)) && (n = { enumerable: true, get: function() {
3364
+ return e[r];
3365
+ } }), Object.defineProperty(t, s, n);
3366
+ } : function(t, e, r, s) {
3367
+ s === undefined && (s = r), t[s] = e[r];
3368
+ });
3369
+ var G = exports && exports.__setModuleDefault || (Object.create ? function(t, e) {
3370
+ Object.defineProperty(t, "default", { enumerable: true, value: e });
3371
+ } : function(t, e) {
3372
+ t.default = e;
3373
+ });
3374
+ var w = exports && exports.__importStar || function() {
3375
+ var t = function(e) {
3376
+ return t = Object.getOwnPropertyNames || function(r) {
3377
+ var s = [];
3378
+ for (var n in r)
3379
+ Object.prototype.hasOwnProperty.call(r, n) && (s[s.length] = n);
3380
+ return s;
3381
+ }, t(e);
3382
+ };
3383
+ return function(e) {
3384
+ if (e && e.__esModule)
3385
+ return e;
3386
+ var r = {};
3387
+ if (e != null)
3388
+ for (var s = t(e), n = 0;n < s.length; n++)
3389
+ s[n] !== "default" && v(r, e, s[n]);
3390
+ return G(r, e), r;
3391
+ };
3392
+ }();
3393
+ var X = exports && exports.__exportStar || function(t, e) {
3394
+ for (var r in t)
3395
+ r !== "default" && !Object.prototype.hasOwnProperty.call(e, r) && v(e, t, r);
3396
+ };
3397
+ Object.defineProperty(exports, "__esModule", { value: true });
3398
+ exports.sync = exports.isexe = exports.posix = exports.win32 = undefined;
3399
+ var E = w(_());
3400
+ exports.posix = E;
3401
+ var O = w(g());
3402
+ exports.win32 = O;
3403
+ X(p(), exports);
3404
+ var H = process.env._ISEXE_TEST_PLATFORM_ || process.platform;
3405
+ var b = H === "win32" ? O : E;
3406
+ exports.isexe = b.isexe;
3407
+ exports.sync = b.sync;
3408
+ });
3409
+
3410
+ // node_modules/which/lib/index.js
3411
+ var require_lib = __commonJS((exports, module) => {
3412
+ var { isexe, sync: isexeSync } = require_index_min();
3413
+ var { join: join9, delimiter, sep, posix } = __require("path");
3414
+ var isWindows = process.platform === "win32";
3415
+ var rSlash = new RegExp(`[${posix.sep}${sep === posix.sep ? "" : sep}]`.replace(/(\\)/g, "\\$1"));
3416
+ var rRel = new RegExp(`^\\.${rSlash.source}`);
3417
+ var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" });
3418
+ var getPathInfo = (cmd, {
3419
+ path: optPath = process.env.PATH,
3420
+ pathExt: optPathExt = process.env.PATHEXT,
3421
+ delimiter: optDelimiter = delimiter
3422
+ }) => {
3423
+ const pathEnv = cmd.match(rSlash) ? [""] : [
3424
+ ...isWindows ? [process.cwd()] : [],
3425
+ ...(optPath || "").split(optDelimiter)
3426
+ ];
3427
+ if (isWindows) {
3428
+ const pathExtExe = optPathExt || [".EXE", ".CMD", ".BAT", ".COM"].join(optDelimiter);
3429
+ const pathExt = pathExtExe.split(optDelimiter).flatMap((item) => [item, item.toLowerCase()]);
3430
+ if (cmd.includes(".") && pathExt[0] !== "") {
3431
+ pathExt.unshift("");
3432
+ }
3433
+ return { pathEnv, pathExt, pathExtExe };
3434
+ }
3435
+ return { pathEnv, pathExt: [""] };
3436
+ };
3437
+ var getPathPart = (raw, cmd) => {
3438
+ const pathPart = /^".*"$/.test(raw) ? raw.slice(1, -1) : raw;
3439
+ const prefix = !pathPart && rRel.test(cmd) ? cmd.slice(0, 2) : "";
3440
+ return prefix + join9(pathPart, cmd);
3441
+ };
3442
+ var which = async (cmd, opt = {}) => {
3443
+ const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
3444
+ const found = [];
3445
+ for (const envPart of pathEnv) {
3446
+ const p = getPathPart(envPart, cmd);
3447
+ for (const ext of pathExt) {
3448
+ const withExt = p + ext;
3449
+ const is = await isexe(withExt, { pathExt: pathExtExe, ignoreErrors: true });
3450
+ if (is) {
3451
+ if (!opt.all) {
3452
+ return withExt;
3453
+ }
3454
+ found.push(withExt);
3455
+ }
3456
+ }
3457
+ }
3458
+ if (opt.all && found.length) {
3459
+ return found;
3460
+ }
3461
+ if (opt.nothrow) {
3462
+ return null;
3463
+ }
3464
+ throw getNotFoundError(cmd);
3465
+ };
3466
+ var whichSync = (cmd, opt = {}) => {
3467
+ const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
3468
+ const found = [];
3469
+ for (const pathEnvPart of pathEnv) {
3470
+ const p = getPathPart(pathEnvPart, cmd);
3471
+ for (const ext of pathExt) {
3472
+ const withExt = p + ext;
3473
+ const is = isexeSync(withExt, { pathExt: pathExtExe, ignoreErrors: true });
3474
+ if (is) {
3475
+ if (!opt.all) {
3476
+ return withExt;
3477
+ }
3478
+ found.push(withExt);
3479
+ }
3480
+ }
3481
+ }
3482
+ if (opt.all && found.length) {
3483
+ return found;
3484
+ }
3485
+ if (opt.nothrow) {
3486
+ return null;
3487
+ }
3488
+ throw getNotFoundError(cmd);
3489
+ };
3490
+ module.exports = which;
3491
+ which.sync = whichSync;
3492
+ });
3493
+
3494
+ // src/cli/paths.ts
3495
+ import { homedir } from "os";
3496
+ import { dirname, join } from "path";
3497
+ function getDefaultOpenCodeConfigDir() {
3498
+ const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config");
3499
+ return join(userConfigDir, "opencode");
3500
+ }
3501
+ function getCustomOpenCodeConfigDir() {
3502
+ const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
3503
+ return configDir || undefined;
3504
+ }
3505
+ function getConfigSearchDirs() {
3506
+ const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
3507
+ return dirs.filter((dir, index) => {
3508
+ return Boolean(dir) && dirs.indexOf(dir) === index;
3509
+ });
3510
+ }
3511
+ function getOpenCodeConfigPaths() {
3512
+ const configDir = getDefaultOpenCodeConfigDir();
3513
+ return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
3514
+ }
3515
+
3280
3516
  // src/cli/custom-skills.ts
3281
3517
  var CUSTOM_SKILLS = [
3282
3518
  {
@@ -3363,86 +3599,53 @@ var SUBAGENT_NAMES = [
3363
3599
  "librarian",
3364
3600
  "oracle",
3365
3601
  "designer",
3366
- "fixer"
3602
+ "fixer",
3603
+ "council",
3604
+ "councillor",
3605
+ "council-master"
3367
3606
  ];
3368
3607
  var ORCHESTRATOR_NAME = "orchestrator";
3369
3608
  var ALL_AGENT_NAMES = [ORCHESTRATOR_NAME, ...SUBAGENT_NAMES];
3609
+ var ORCHESTRATABLE_AGENTS = [
3610
+ "explorer",
3611
+ "librarian",
3612
+ "oracle",
3613
+ "designer",
3614
+ "fixer",
3615
+ "council"
3616
+ ];
3370
3617
  var SUBAGENT_DELEGATION_RULES = {
3371
- orchestrator: SUBAGENT_NAMES,
3618
+ orchestrator: ORCHESTRATABLE_AGENTS,
3372
3619
  fixer: [],
3373
3620
  designer: [],
3374
3621
  explorer: [],
3375
3622
  librarian: [],
3376
- oracle: []
3623
+ oracle: [],
3624
+ council: [],
3625
+ councillor: [],
3626
+ "council-master": []
3377
3627
  };
3378
3628
  var DEFAULT_MODELS = {
3379
3629
  orchestrator: undefined,
3380
3630
  oracle: "openai/gpt-5.4",
3381
- librarian: "openai/gpt-5-codex",
3382
- explorer: "openai/gpt-5-codex",
3383
- designer: "kimi-for-coding/k2p5",
3384
- fixer: "openai/gpt-5-codex"
3631
+ librarian: "openai/gpt-5.4-mini",
3632
+ explorer: "openai/gpt-5.4-mini",
3633
+ designer: "openai/gpt-5.4-mini",
3634
+ fixer: "openai/gpt-5.4-mini",
3635
+ council: "openai/gpt-5.4-mini",
3636
+ councillor: "openai/gpt-5.4-mini",
3637
+ "council-master": "openai/gpt-5.4-mini"
3385
3638
  };
3386
3639
  var POLL_INTERVAL_BACKGROUND_MS = 2000;
3387
3640
  var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
3388
3641
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
3389
3642
  var FALLBACK_FAILOVER_TIMEOUT_MS = 15000;
3390
- // src/config/loader.ts
3391
- import * as fs from "fs";
3392
- import * as os from "os";
3393
- import * as path from "path";
3394
-
3395
- // src/cli/paths.ts
3396
- import { homedir } from "os";
3397
- import { join } from "path";
3398
- function getConfigDir() {
3399
- const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config");
3400
- return join(userConfigDir, "opencode");
3401
- }
3402
- function getOpenCodeConfigPaths() {
3403
- const configDir = getConfigDir();
3404
- return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
3405
- }
3406
-
3407
- // src/config/agent-mcps.ts
3408
- var DEFAULT_AGENT_MCPS = {
3409
- orchestrator: ["websearch"],
3410
- designer: [],
3411
- oracle: [],
3412
- librarian: ["websearch", "context7", "grep_app"],
3413
- explorer: [],
3414
- fixer: []
3415
- };
3416
- function parseList(items, allAvailable) {
3417
- if (!items || items.length === 0) {
3418
- return [];
3419
- }
3420
- const allow = items.filter((i) => !i.startsWith("!"));
3421
- const deny = items.filter((i) => i.startsWith("!")).map((i) => i.slice(1));
3422
- if (deny.includes("*")) {
3423
- return [];
3424
- }
3425
- if (allow.includes("*")) {
3426
- return allAvailable.filter((item) => !deny.includes(item));
3427
- }
3428
- return allow.filter((item) => !deny.includes(item));
3429
- }
3430
- function getAgentMcpList(agentName, config) {
3431
- const agentConfig = getAgentOverride(config, agentName);
3432
- if (agentConfig?.mcps !== undefined) {
3433
- return agentConfig.mcps;
3434
- }
3435
- const defaultMcps = DEFAULT_AGENT_MCPS[agentName];
3436
- return defaultMcps ?? [];
3437
- }
3438
-
3439
- // src/cli/config-io.ts
3440
- function stripJsonComments(json) {
3441
- const commentPattern = /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g;
3442
- const trailingCommaPattern = /\\"|"(?:\\"|[^"])*"|(,)(\s*[}\]])/g;
3443
- return json.replace(commentPattern, (match, commentGroup) => commentGroup ? "" : match).replace(trailingCommaPattern, (match, comma, closing) => comma ? closing : match);
3444
- }
3445
-
3643
+ var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
3644
+ var PHASE_REMINDER_TEXT = `Recall Workflow Rules:
3645
+ Understand \u2192 build the best path (delegated based on Agent rules, split and parallelized as much as possible) \u2192 execute \u2192 verify.
3646
+ If delegating, launch the specialist in the same turn you mention it.`;
3647
+ var TMUX_SPAWN_DELAY_MS = 500;
3648
+ var COUNCILLOR_STAGGER_MS = 250;
3446
3649
  // node_modules/zod/v4/classic/external.js
3447
3650
  var exports_external = {};
3448
3651
  __export(exports_external, {
@@ -5682,7 +5885,7 @@ class Doc {
5682
5885
  var version = {
5683
5886
  major: 4,
5684
5887
  minor: 3,
5685
- patch: 5
5888
+ patch: 6
5686
5889
  };
5687
5890
 
5688
5891
  // node_modules/zod/v4/core/schemas.js
@@ -6968,7 +7171,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
6968
7171
  if (keyResult instanceof Promise) {
6969
7172
  throw new Error("Async schemas not supported in object keys currently");
6970
7173
  }
6971
- const checkNumericKey = typeof key === "string" && number.test(key) && keyResult.issues.length && keyResult.issues.some((iss) => iss.code === "invalid_type" && iss.expected === "number");
7174
+ const checkNumericKey = typeof key === "string" && number.test(key) && keyResult.issues.length;
6972
7175
  if (checkNumericKey) {
6973
7176
  const retryResult = def.keyType._zod.run({ value: Number(key), issues: [] }, ctx);
6974
7177
  if (retryResult instanceof Promise) {
@@ -14339,7 +14542,7 @@ function finalize(ctx, schema) {
14339
14542
  }
14340
14543
  }
14341
14544
  }
14342
- if (refSchema.$ref) {
14545
+ if (refSchema.$ref && refSeen.def) {
14343
14546
  for (const key in schema2) {
14344
14547
  if (key === "$ref" || key === "allOf")
14345
14548
  continue;
@@ -16975,6 +17178,107 @@ function date4(params) {
16975
17178
 
16976
17179
  // node_modules/zod/v4/classic/external.js
16977
17180
  config(en_default());
17181
+ // src/config/council-schema.ts
17182
+ var ModelIdSchema = exports_external.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.4-mini")');
17183
+ var CouncillorConfigSchema = exports_external.object({
17184
+ model: ModelIdSchema.describe('Model ID in provider/model format (e.g. "openai/gpt-5.4-mini")'),
17185
+ variant: exports_external.string().optional(),
17186
+ prompt: exports_external.string().optional().describe("Optional role/guidance injected into the councillor user prompt")
17187
+ });
17188
+ var PresetMasterOverrideSchema = exports_external.object({
17189
+ model: ModelIdSchema.optional().describe("Override the master model for this preset"),
17190
+ variant: exports_external.string().optional().describe("Override the master variant for this preset"),
17191
+ prompt: exports_external.string().optional().describe("Override the master synthesis guidance for this preset")
17192
+ });
17193
+ var CouncilPresetSchema = exports_external.record(exports_external.string(), exports_external.record(exports_external.string(), exports_external.unknown())).transform((entries, ctx) => {
17194
+ const councillors = {};
17195
+ let masterOverride;
17196
+ for (const [key, raw] of Object.entries(entries)) {
17197
+ if (key === "master") {
17198
+ const parsed = PresetMasterOverrideSchema.safeParse(raw);
17199
+ if (!parsed.success) {
17200
+ ctx.addIssue(`Invalid master override in preset: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
17201
+ return exports_external.NEVER;
17202
+ }
17203
+ masterOverride = parsed.data;
17204
+ } else {
17205
+ const parsed = CouncillorConfigSchema.safeParse(raw);
17206
+ if (!parsed.success) {
17207
+ ctx.addIssue(`Invalid councillor "${key}": ${parsed.error.issues.map((i) => i.message).join(", ")}`);
17208
+ return exports_external.NEVER;
17209
+ }
17210
+ councillors[key] = parsed.data;
17211
+ }
17212
+ }
17213
+ return { councillors, master: masterOverride };
17214
+ });
17215
+ var CouncilMasterConfigSchema = exports_external.object({
17216
+ model: ModelIdSchema.describe('Model ID for the council master (e.g. "anthropic/claude-opus-4-6")'),
17217
+ variant: exports_external.string().optional(),
17218
+ prompt: exports_external.string().optional().describe("Optional role/guidance injected into the master synthesis prompt")
17219
+ });
17220
+ var CouncilConfigSchema = exports_external.object({
17221
+ master: CouncilMasterConfigSchema,
17222
+ presets: exports_external.record(exports_external.string(), CouncilPresetSchema),
17223
+ master_timeout: exports_external.number().min(0).default(300000),
17224
+ councillors_timeout: exports_external.number().min(0).default(180000),
17225
+ default_preset: exports_external.string().default("default"),
17226
+ master_fallback: exports_external.array(ModelIdSchema).optional().transform((val) => {
17227
+ if (!val)
17228
+ return val;
17229
+ const unique = [...new Set(val)];
17230
+ if (unique.length !== val.length) {
17231
+ return unique;
17232
+ }
17233
+ return val;
17234
+ }).describe("Fallback models for the council master. Tried in order if the primary model fails. " + 'Example: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4"]')
17235
+ });
17236
+ // src/config/loader.ts
17237
+ import * as fs from "fs";
17238
+ import * as path from "path";
17239
+
17240
+ // src/config/agent-mcps.ts
17241
+ var DEFAULT_AGENT_MCPS = {
17242
+ orchestrator: ["websearch"],
17243
+ designer: [],
17244
+ oracle: [],
17245
+ librarian: ["websearch", "context7", "grep_app"],
17246
+ explorer: [],
17247
+ fixer: [],
17248
+ council: [],
17249
+ councillor: [],
17250
+ "council-master": []
17251
+ };
17252
+ function parseList(items, allAvailable) {
17253
+ if (!items || items.length === 0) {
17254
+ return [];
17255
+ }
17256
+ const allow = items.filter((i) => !i.startsWith("!"));
17257
+ const deny = items.filter((i) => i.startsWith("!")).map((i) => i.slice(1));
17258
+ if (deny.includes("*")) {
17259
+ return [];
17260
+ }
17261
+ if (allow.includes("*")) {
17262
+ return allAvailable.filter((item) => !deny.includes(item));
17263
+ }
17264
+ return allow.filter((item) => !deny.includes(item));
17265
+ }
17266
+ function getAgentMcpList(agentName, config2) {
17267
+ const agentConfig = getAgentOverride(config2, agentName);
17268
+ if (agentConfig?.mcps !== undefined) {
17269
+ return agentConfig.mcps;
17270
+ }
17271
+ const defaultMcps = DEFAULT_AGENT_MCPS[agentName];
17272
+ return defaultMcps ?? [];
17273
+ }
17274
+
17275
+ // src/cli/config-io.ts
17276
+ function stripJsonComments(json2) {
17277
+ const commentPattern = /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g;
17278
+ const trailingCommaPattern = /\\"|"(?:\\"|[^"])*"|(,)(\s*[}\]])/g;
17279
+ return json2.replace(commentPattern, (match, commentGroup) => commentGroup ? "" : match).replace(trailingCommaPattern, (match, comma, closing) => comma ? closing : match);
17280
+ }
17281
+
16978
17282
  // src/config/schema.ts
16979
17283
  var ProviderModelIdSchema = exports_external.string().regex(/^[^/\s]+\/[^\s]+$/, "Expected provider/model format (provider/.../model)");
16980
17284
  var ManualAgentPlanSchema = exports_external.object({
@@ -17049,10 +17353,12 @@ var BackgroundTaskConfigSchema = exports_external.object({
17049
17353
  var FailoverConfigSchema = exports_external.object({
17050
17354
  enabled: exports_external.boolean().default(true),
17051
17355
  timeoutMs: exports_external.number().min(0).default(15000),
17356
+ retryDelayMs: exports_external.number().min(0).default(500),
17052
17357
  chains: FallbackChainsSchema.default({})
17053
17358
  });
17054
17359
  var PluginConfigSchema = exports_external.object({
17055
17360
  preset: exports_external.string().optional(),
17361
+ setDefaultAgent: exports_external.boolean().optional(),
17056
17362
  scoringEngineVersion: exports_external.enum(["v1", "v2-shadow", "v2"]).optional(),
17057
17363
  balanceProviderUsage: exports_external.boolean().optional(),
17058
17364
  manualPlan: ManualPlanSchema.optional(),
@@ -17061,14 +17367,12 @@ var PluginConfigSchema = exports_external.object({
17061
17367
  disabled_mcps: exports_external.array(exports_external.string()).optional(),
17062
17368
  tmux: TmuxConfigSchema.optional(),
17063
17369
  background: BackgroundTaskConfigSchema.optional(),
17064
- fallback: FailoverConfigSchema.optional()
17370
+ fallback: FailoverConfigSchema.optional(),
17371
+ council: CouncilConfigSchema.optional()
17065
17372
  });
17066
17373
 
17067
17374
  // src/config/loader.ts
17068
17375
  var PROMPTS_DIR_NAME = "oh-my-opencode-slim";
17069
- function getUserConfigDir() {
17070
- return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
17071
- }
17072
17376
  function loadConfigFromPath(configPath) {
17073
17377
  try {
17074
17378
  const content = fs.readFileSync(configPath, "utf-8");
@@ -17098,6 +17402,15 @@ function findConfigPath(basePath) {
17098
17402
  }
17099
17403
  return null;
17100
17404
  }
17405
+ function findConfigPathInDirs(configDirs, baseName) {
17406
+ for (const configDir of configDirs) {
17407
+ const configPath = findConfigPath(path.join(configDir, baseName));
17408
+ if (configPath) {
17409
+ return configPath;
17410
+ }
17411
+ }
17412
+ return null;
17413
+ }
17101
17414
  function deepMerge(base, override) {
17102
17415
  if (!base)
17103
17416
  return override;
@@ -17116,9 +17429,8 @@ function deepMerge(base, override) {
17116
17429
  return result;
17117
17430
  }
17118
17431
  function loadPluginConfig(directory) {
17119
- const userConfigBasePath = path.join(getUserConfigDir(), "opencode", "oh-my-opencode-slim");
17432
+ const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
17120
17433
  const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
17121
- const userConfigPath = findConfigPath(userConfigBasePath);
17122
17434
  const projectConfigPath = findConfigPath(projectConfigBasePath);
17123
17435
  let config2 = userConfigPath ? loadConfigFromPath(userConfigPath) ?? {} : {};
17124
17436
  const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath) : null;
@@ -17149,8 +17461,10 @@ function loadPluginConfig(directory) {
17149
17461
  }
17150
17462
  function loadAgentPrompt(agentName, preset) {
17151
17463
  const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
17152
- const promptsDir = path.join(getUserConfigDir(), "opencode", PROMPTS_DIR_NAME);
17153
- const promptSearchDirs = presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
17464
+ const promptSearchDirs = getConfigSearchDirs().flatMap((configDir) => {
17465
+ const promptsDir = path.join(configDir, PROMPTS_DIR_NAME);
17466
+ return presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
17467
+ });
17154
17468
  const result = {};
17155
17469
  const readFirstPrompt = (fileName, errorPrefix) => {
17156
17470
  for (const dir of promptSearchDirs) {
@@ -17175,49 +17489,461 @@ function getAgentOverride(config2, name) {
17175
17489
  const overrides = config2?.agents ?? {};
17176
17490
  return overrides[name] ?? overrides[Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === name) ?? ""];
17177
17491
  }
17178
- // src/agents/designer.ts
17179
- var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who creates intentional, polished experiences.
17180
-
17181
- **Role**: Craft cohesive UI/UX that balances visual impact with usability.
17492
+ // src/utils/session.ts
17493
+ function shortModelLabel(model) {
17494
+ return model.split("/").pop() ?? model;
17495
+ }
17496
+ function parseModelReference(model) {
17497
+ const slashIndex = model.indexOf("/");
17498
+ if (slashIndex <= 0 || slashIndex >= model.length - 1) {
17499
+ return null;
17500
+ }
17501
+ return {
17502
+ providerID: model.slice(0, slashIndex),
17503
+ modelID: model.slice(slashIndex + 1)
17504
+ };
17505
+ }
17506
+ async function promptWithTimeout(client, args, timeoutMs) {
17507
+ if (timeoutMs <= 0) {
17508
+ await client.session.prompt(args);
17509
+ return;
17510
+ }
17511
+ const sessionId = args.path.id;
17512
+ let timer;
17513
+ try {
17514
+ const promptPromise = client.session.prompt(args);
17515
+ promptPromise.catch(() => {});
17516
+ await Promise.race([
17517
+ promptPromise,
17518
+ new Promise((_, reject) => {
17519
+ timer = setTimeout(() => {
17520
+ client.session.abort({ path: { id: sessionId } }).catch(() => {});
17521
+ reject(new Error(`Prompt timed out after ${timeoutMs}ms`));
17522
+ }, timeoutMs);
17523
+ })
17524
+ ]);
17525
+ } finally {
17526
+ clearTimeout(timer);
17527
+ }
17528
+ }
17529
+ async function extractSessionResult(client, sessionId, options) {
17530
+ const includeReasoning = options?.includeReasoning ?? true;
17531
+ const messagesResult = await client.session.messages({
17532
+ path: { id: sessionId }
17533
+ });
17534
+ const messages = messagesResult.data ?? [];
17535
+ const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
17536
+ const extractedContent = [];
17537
+ for (const message of assistantMessages) {
17538
+ for (const part of message.parts ?? []) {
17539
+ const allowed = includeReasoning ? part.type === "text" || part.type === "reasoning" : part.type === "text";
17540
+ if (allowed && part.text) {
17541
+ extractedContent.push(part.text);
17542
+ }
17543
+ }
17544
+ }
17545
+ return extractedContent.filter((t) => t.length > 0).join(`
17182
17546
 
17183
- ## Design Principles
17547
+ `);
17548
+ }
17184
17549
 
17185
- **Typography**
17186
- - Choose distinctive, characterful fonts that elevate aesthetics
17187
- - Avoid generic defaults (Arial, Inter)\u2014opt for unexpected, beautiful choices
17188
- - Pair display fonts with refined body fonts for hierarchy
17550
+ // src/agents/orchestrator.ts
17551
+ function resolvePrompt(base, customPrompt, customAppendPrompt) {
17552
+ if (customPrompt)
17553
+ return customPrompt;
17554
+ if (customAppendPrompt)
17555
+ return `${base}
17189
17556
 
17190
- **Color & Theme**
17191
- - Commit to a cohesive aesthetic with clear color variables
17192
- - Dominant colors with sharp accents > timid, evenly-distributed palettes
17193
- - Create atmosphere through intentional color relationships
17557
+ ${customAppendPrompt}`;
17558
+ return base;
17559
+ }
17560
+ var ORCHESTRATOR_PROMPT = `<Role>
17561
+ You are an AI coding orchestrator that optimizes for quality, speed, cost, and reliability by delegating to specialists when it provides net efficiency gains.
17562
+ </Role>
17194
17563
 
17195
- **Motion & Interaction**
17196
- - Leverage framework animation utilities when available (Tailwind's transition/animation classes)
17197
- - Focus on high-impact moments: orchestrated page loads with staggered reveals
17198
- - Use scroll-triggers and hover states that surprise and delight
17199
- - One well-timed animation > scattered micro-interactions
17200
- - Drop to custom CSS/JS only when utilities can't achieve the vision
17564
+ <Agents>
17201
17565
 
17202
- **Spatial Composition**
17203
- - Break conventions: asymmetry, overlap, diagonal flow, grid-breaking
17204
- - Generous negative space OR controlled density\u2014commit to the choice
17205
- - Unexpected layouts that guide the eye
17566
+ @explorer
17567
+ - Role: Parallel search specialist for discovering unknowns across the codebase
17568
+ - Stats: 3x faster codebase search than orchestrator, 1/2 cost of orchestrator
17569
+ - Capabilities: Glob, grep, AST queries to locate files, symbols, patterns
17570
+ - **Delegate when:** Need to discover what exists before planning \u2022 Parallel searches speed discovery \u2022 Need summarized map vs full contents \u2022 Broad/uncertain scope
17571
+ - **Don't delegate when:** Know the path and need actual content \u2022 Need full file anyway \u2022 Single specific lookup \u2022 About to edit the file
17206
17572
 
17207
- **Visual Depth**
17208
- - Create atmosphere beyond solid colors: gradient meshes, noise textures, geometric patterns
17209
- - Layer transparencies, dramatic shadows, decorative borders
17210
- - Contextual effects that match the aesthetic (grain overlays, custom cursors)
17573
+ @librarian
17574
+ - Role: Authoritative source for current library docs and API references
17575
+ - Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
17576
+ - Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
17577
+ - **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) \u2022 Complex APIs needing official examples (ORMs, auth) \u2022 Version-specific behavior matters \u2022 Unfamiliar library \u2022 Edge cases or advanced features \u2022 Nuanced best practices
17578
+ - **Don't delegate when:** Standard usage you're confident about (\`Array.map()\`, \`fetch()\`) \u2022 Simple stable APIs \u2022 General programming knowledge \u2022 Info already in conversation \u2022 Built-in language features
17579
+ - **Rule of thumb:** "How does this library work?" \u2192 @librarian. "How does programming work?" \u2192 yourself.
17211
17580
 
17212
- **Styling Approach**
17213
- - Default to Tailwind CSS utility classes when available\u2014fast, maintainable, consistent
17214
- - Use custom CSS when the vision requires it: complex animations, unique effects, advanced compositions
17215
- - Balance utility-first speed with creative freedom where it matters
17581
+ @oracle
17582
+ - Role: Strategic advisor for high-stakes decisions and persistent problems
17583
+ - Stats: 10x better decisions maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
17584
+ - Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging
17585
+ - Tools/Constraints: Slow, expensive, high-quality\u2014use sparingly when thoroughness beats speed
17586
+ - **Delegate when:** Major architectural decisions with long-term impact \u2022 Problems persisting after 2+ fix attempts \u2022 High-risk multi-system refactors \u2022 Costly trade-offs (performance vs maintainability) \u2022 Complex debugging with unclear root cause \u2022 Security/scalability/data integrity decisions \u2022 Genuinely uncertain and cost of wrong choice is high
17587
+ - **Don't delegate when:** Routine decisions you're confident about \u2022 First bug fix attempt \u2022 Straightforward trade-offs \u2022 Tactical "how" vs strategic "should" \u2022 Time-sensitive good-enough decisions \u2022 Quick research/testing can answer
17588
+ - **Rule of thumb:** Need senior architect review? \u2192 @oracle. Just do it and PR? \u2192 yourself.
17216
17589
 
17217
- **Match Vision to Execution**
17218
- - Maximalist designs \u2192 elaborate implementation, extensive animations, rich effects
17219
- - Minimalist designs \u2192 restraint, precision, careful spacing and typography
17220
- - Elegance comes from executing the chosen vision fully, not halfway
17590
+ @designer
17591
+ - Role: UI/UX specialist for intentional, polished experiences
17592
+ - Stats: 10x better UI/UX than orchestrator
17593
+ - Capabilities: Visual direction, interactions, responsive layouts, design systems with aesthetic intent
17594
+ - **Delegate when:** User-facing interfaces needing polish \u2022 Responsive layouts \u2022 UX-critical components (forms, nav, dashboards) \u2022 Visual consistency systems \u2022 Animations/micro-interactions \u2022 Landing/marketing pages \u2022 Refining functional\u2192delightful
17595
+ - **Don't delegate when:** Backend/logic with no visual \u2022 Quick prototypes where design doesn't matter yet
17596
+ - **Rule of thumb:** Users see it and polish matters? \u2192 @designer. Headless/functional? \u2192 yourself.
17597
+
17598
+ @fixer
17599
+ - Role: Fast execution specialist for well-defined tasks, which empowers orchestrator with parallel, speedy executions
17600
+ - Stats: 2x faster code edits, 1/2 cost of orchestrator, 0.8x quality of orchestrator
17601
+ - Tools/Constraints: Execution-focused\u2014no research, no architectural decisions
17602
+ - **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer
17603
+ - **Don't delegate when:** Needs discovery/research/decisions \u2022 Single small change (<20 lines, one file) \u2022 Unclear requirements needing iteration \u2022 Explaining to fixer > doing \u2022 Tight integration with your current work \u2022 Sequential dependencies
17604
+ - **Rule of thumb:** Explaining > doing? \u2192 yourself. Orchestrator paths selection is vastly improved by Fixer. eg it can reduce overall speed if Orchestrator splits what's usually a single task into multiple subtasks and parallelize it with fixer.
17605
+
17606
+ @council
17607
+ - Role: Multi-LLM consensus engine for high-confidence answers
17608
+ - Stats: 3x slower than orchestrator, 3x or more cost of orchestrator
17609
+ - Capabilities: Runs multiple models in parallel, synthesizes their responses via a council master
17610
+ - **Delegate when:** Critical decisions needing diverse model perspectives \u2022 High-stakes architectural choices where consensus reduces risk \u2022 Ambiguous problems where multi-model disagreement is informative \u2022 Security-sensitive design reviews
17611
+ - **Don't delegate when:** Straightforward tasks you're confident about \u2022 Speed matters more than confidence \u2022 Single-model answer is sufficient \u2022 Routine implementation work
17612
+ - **Result handling:** Present the council's synthesized response verbatim. Do not re-summarize \u2014 the council master has already produced the final answer.
17613
+ - **Rule of thumb:** Need second/third opinions from different models? \u2192 @council. One good answer enough? \u2192 yourself.
17614
+
17615
+ </Agents>
17616
+
17617
+ <Workflow>
17618
+
17619
+ ## 1. Understand
17620
+ Parse request: explicit requirements + implicit needs.
17621
+
17622
+ ## 2. Path Selection
17623
+ Evaluate approach by: quality, speed, cost, reliability.
17624
+ Choose the path that optimizes all four.
17625
+
17626
+ ## 3. Delegation Check
17627
+ **STOP. Review specialists before acting.**
17628
+
17629
+ !!! Review available agents and delegation rules. Decide whether to delegate or do it yourself. !!!
17630
+
17631
+ **Delegation efficiency:**
17632
+ - Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)
17633
+ - Provide context summaries, let specialists read what they need
17634
+ - Brief user on delegation goal before each call
17635
+ - Skip delegation if overhead \u2265 doing it yourself
17636
+
17637
+ ## 4. Split and Parallelize
17638
+ Can tasks be split into subtasks and run in parallel?
17639
+ - Multiple @explorer searches across different domains?
17640
+ - @explorer + @librarian research in parallel?
17641
+ - Multiple @fixer instances for faster, scoped implementation?
17642
+
17643
+ Balance: respect dependencies, avoid parallelizing what must be sequential.
17644
+
17645
+ ## 5. Execute
17646
+ 1. Break complex tasks into todos
17647
+ 2. Fire parallel research/implementation
17648
+ 3. Delegate to specialists or do it yourself based on step 3
17649
+ 4. Integrate results
17650
+ 5. Adjust if needed
17651
+
17652
+ ## 6. Verify
17653
+ - Run \`lsp_diagnostics\` for errors
17654
+ - Suggest \`simplify\` skill when applicable
17655
+ - Confirm specialists completed successfully
17656
+ - Verify solution meets requirements
17657
+
17658
+ </Workflow>
17659
+
17660
+ <Communication>
17661
+
17662
+ ## Clarity Over Assumptions
17663
+ - If request is vague or has multiple valid interpretations, ask a targeted question before proceeding
17664
+ - Don't guess at critical details (file paths, API choices, architectural decisions)
17665
+ - Do make reasonable assumptions for minor details and state them briefly
17666
+
17667
+ ## Concise Execution
17668
+ - Answer directly, no preamble
17669
+ - Don't summarize what you did unless asked
17670
+ - Don't explain code unless asked
17671
+ - One-word answers are fine when appropriate
17672
+ - Brief delegation notices: "Checking docs via @librarian..." not "I'm going to delegate to @librarian because..."
17673
+
17674
+ ## No Flattery
17675
+ Never: "Great question!" "Excellent idea!" "Smart choice!" or any praise of user input.
17676
+
17677
+ ## Honest Pushback
17678
+ When user's approach seems problematic:
17679
+ - State concern + alternative concisely
17680
+ - Ask if they want to proceed anyway
17681
+ - Don't lecture, don't blindly implement
17682
+
17683
+ ## Example
17684
+ **Bad:** "Great question! Let me think about the best approach here. I'm going to delegate to @librarian to check the latest Next.js documentation for the App Router, and then I'll implement the solution for you."
17685
+
17686
+ **Good:** "Checking Next.js App Router docs via @librarian..."
17687
+ [proceeds with implementation]
17688
+
17689
+ </Communication>
17690
+ `;
17691
+ function createOrchestratorAgent(model, customPrompt, customAppendPrompt) {
17692
+ const prompt = resolvePrompt(ORCHESTRATOR_PROMPT, customPrompt, customAppendPrompt);
17693
+ const definition = {
17694
+ name: "orchestrator",
17695
+ description: "AI coding orchestrator that delegates tasks to specialist agents for optimal quality, speed, and cost",
17696
+ config: {
17697
+ temperature: 0.1,
17698
+ prompt
17699
+ }
17700
+ };
17701
+ if (Array.isArray(model)) {
17702
+ definition._modelArray = model.map((m) => typeof m === "string" ? { id: m } : m);
17703
+ } else if (typeof model === "string" && model) {
17704
+ definition.config.model = model;
17705
+ }
17706
+ return definition;
17707
+ }
17708
+
17709
+ // src/agents/council.ts
17710
+ var COUNCIL_AGENT_PROMPT = `You are the Council agent \u2014 a multi-LLM orchestration system that runs consensus across multiple models.
17711
+
17712
+ **Tool**: You have access to the \`council_session\` tool.
17713
+
17714
+ **When to use**:
17715
+ - When invoked by a user with a request
17716
+ - When you want multiple expert opinions on a complex problem
17717
+ - When higher confidence is needed through model consensus
17718
+
17719
+ **Usage**:
17720
+ 1. Call the \`council_session\` tool with the user's prompt
17721
+ 2. Optionally specify a preset (default: "default")
17722
+ 3. Receive the synthesized response from the council master
17723
+ 4. Present the result to the user
17724
+
17725
+ **Behavior**:
17726
+ - Delegate requests directly to council_session
17727
+ - Don't pre-analyze or filter the prompt
17728
+ - Present the synthesized result verbatim \u2014 do not re-summarize or condense
17729
+ - Briefly explain the consensus if requested`;
17730
+ function createCouncilAgent(model, customPrompt, customAppendPrompt) {
17731
+ const prompt = resolvePrompt(COUNCIL_AGENT_PROMPT, customPrompt, customAppendPrompt);
17732
+ const definition = {
17733
+ name: "council",
17734
+ description: "Multi-LLM council agent that synthesizes responses from multiple models for higher-quality outputs",
17735
+ config: {
17736
+ temperature: 0.1,
17737
+ prompt
17738
+ }
17739
+ };
17740
+ if (model) {
17741
+ definition.config.model = model;
17742
+ }
17743
+ return definition;
17744
+ }
17745
+ function formatCouncillorPrompt(userPrompt, councillorPrompt) {
17746
+ if (!councillorPrompt)
17747
+ return userPrompt;
17748
+ return `${councillorPrompt}
17749
+
17750
+ ---
17751
+
17752
+ ${userPrompt}`;
17753
+ }
17754
+ function formatMasterSynthesisPrompt(originalPrompt, councillorResults, masterPrompt) {
17755
+ const completedWithResults = councillorResults.filter((cr) => cr.status === "completed" && cr.result);
17756
+ const councillorSection = completedWithResults.map((cr) => {
17757
+ const shortModel = shortModelLabel(cr.model);
17758
+ return `**${cr.name}** (${shortModel}):
17759
+ ${cr.result}`;
17760
+ }).join(`
17761
+
17762
+ `);
17763
+ const failedSection = councillorResults.filter((cr) => cr.status !== "completed").map((cr) => `**${cr.name}**: ${cr.status} \u2014 ${cr.error ?? "Unknown"}`).join(`
17764
+ `);
17765
+ if (completedWithResults.length === 0) {
17766
+ return `---
17767
+
17768
+ **Original Prompt**:
17769
+ ${originalPrompt}
17770
+
17771
+ ---
17772
+
17773
+ **Councillor Responses**:
17774
+ All councillors failed to produce output. Please generate a response based on the original prompt alone.`;
17775
+ }
17776
+ let prompt = `---
17777
+
17778
+ **Original Prompt**:
17779
+ ${originalPrompt}
17780
+
17781
+ ---
17782
+
17783
+ **Councillor Responses**:
17784
+ ${councillorSection}`;
17785
+ if (failedSection) {
17786
+ prompt += `
17787
+
17788
+ ---
17789
+
17790
+ **Failed/Timed-out Councillors**:
17791
+ ${failedSection}`;
17792
+ }
17793
+ prompt += `
17794
+
17795
+ ---
17796
+
17797
+ Synthesize the optimal response based on the above.`;
17798
+ if (masterPrompt) {
17799
+ prompt += `
17800
+
17801
+ ---
17802
+
17803
+ **Master Guidance**:
17804
+ ${masterPrompt}`;
17805
+ }
17806
+ return prompt;
17807
+ }
17808
+
17809
+ // src/agents/council-master.ts
17810
+ var COUNCIL_MASTER_PROMPT = `You are the council master responsible for synthesizing responses from multiple AI models.
17811
+
17812
+ **Role**: Review all councillor responses and create the optimal final answer.
17813
+
17814
+ **Process**:
17815
+ 1. Read the original user prompt
17816
+ 2. Review each councillor's response carefully
17817
+ 3. Identify the best elements from each response
17818
+ 4. Resolve contradictions between councillors
17819
+ 5. Synthesize a final, optimal response
17820
+
17821
+ **Behavior**:
17822
+ - Each councillor had read-only access to the codebase \u2014 their responses may reference specific files, functions, and line numbers
17823
+ - Clearly explain your reasoning for the chosen approach
17824
+ - Be transparent about trade-offs
17825
+ - Credit specific insights from individual councillors by name
17826
+ - If councillors disagree, explain your resolution
17827
+ - Don't just average responses \u2014 choose and improve
17828
+
17829
+ **Output**:
17830
+ - Present the synthesized solution
17831
+ - Review, retain, and include relevant code examples, diagrams, and concrete details from councillor responses
17832
+ - Explain your synthesis reasoning
17833
+ - Note any remaining uncertainties
17834
+ - Acknowledge if consensus was impossible`;
17835
+ function createCouncilMasterAgent(model, customPrompt, customAppendPrompt) {
17836
+ const prompt = resolvePrompt(COUNCIL_MASTER_PROMPT, customPrompt, customAppendPrompt);
17837
+ return {
17838
+ name: "council-master",
17839
+ description: "Council synthesis engine. Receives councillor responses and produces the final answer. No tools, pure text synthesis.",
17840
+ config: {
17841
+ model,
17842
+ temperature: 0.1,
17843
+ prompt,
17844
+ permission: {
17845
+ "*": "deny",
17846
+ question: "deny"
17847
+ }
17848
+ }
17849
+ };
17850
+ }
17851
+
17852
+ // src/agents/councillor.ts
17853
+ var COUNCILLOR_PROMPT = `You are a councillor in a multi-model council.
17854
+
17855
+ **Role**: Provide your best independent analysis and solution to the given problem.
17856
+
17857
+ **Capabilities**: You have read-only access to the codebase. You can:
17858
+ - Read files (read)
17859
+ - Search by name patterns (glob)
17860
+ - Search by content (grep)
17861
+ - Query language server (lsp_diagnostics, lsp_goto_definition, lsp_find_references)
17862
+ - Search code patterns (ast_grep_search)
17863
+ - Search external docs (if MCPs are configured for this agent)
17864
+
17865
+ You CANNOT edit files, write files, run shell commands, or delegate to other agents. You are an advisor, not an implementer.
17866
+
17867
+ **Behavior**:
17868
+ - **Examine the codebase** before answering \u2014 your read access is what makes council valuable. Don't guess at code you can see.
17869
+ - Analyze the problem thoroughly
17870
+ - Provide a complete, well-reasoned response
17871
+ - Focus on the quality and correctness of your solution
17872
+ - Be direct and concise
17873
+ - Don't be influenced by what other councillors might say \u2014 you won't see their responses
17874
+
17875
+ **Output**:
17876
+ - Give your honest assessment
17877
+ - Reference specific files and line numbers when relevant
17878
+ - Include relevant reasoning
17879
+ - State any assumptions clearly
17880
+ - Note any uncertainties`;
17881
+ function createCouncillorAgent(model, customPrompt, customAppendPrompt) {
17882
+ const prompt = resolvePrompt(COUNCILLOR_PROMPT, customPrompt, customAppendPrompt);
17883
+ return {
17884
+ name: "councillor",
17885
+ description: "Read-only council advisor. Examines codebase and provides independent analysis. Spawned internally by the council system.",
17886
+ config: {
17887
+ model,
17888
+ temperature: 0.2,
17889
+ prompt,
17890
+ permission: {
17891
+ "*": "deny",
17892
+ question: "deny",
17893
+ read: "allow",
17894
+ glob: "allow",
17895
+ grep: "allow",
17896
+ lsp: "allow",
17897
+ list: "allow",
17898
+ codesearch: "allow"
17899
+ }
17900
+ }
17901
+ };
17902
+ }
17903
+
17904
+ // src/agents/designer.ts
17905
+ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who creates intentional, polished experiences.
17906
+
17907
+ **Role**: Craft cohesive UI/UX that balances visual impact with usability.
17908
+
17909
+ ## Design Principles
17910
+
17911
+ **Typography**
17912
+ - Choose distinctive, characterful fonts that elevate aesthetics
17913
+ - Avoid generic defaults (Arial, Inter)\u2014opt for unexpected, beautiful choices
17914
+ - Pair display fonts with refined body fonts for hierarchy
17915
+
17916
+ **Color & Theme**
17917
+ - Commit to a cohesive aesthetic with clear color variables
17918
+ - Dominant colors with sharp accents > timid, evenly-distributed palettes
17919
+ - Create atmosphere through intentional color relationships
17920
+
17921
+ **Motion & Interaction**
17922
+ - Leverage framework animation utilities when available (Tailwind's transition/animation classes)
17923
+ - Focus on high-impact moments: orchestrated page loads with staggered reveals
17924
+ - Use scroll-triggers and hover states that surprise and delight
17925
+ - One well-timed animation > scattered micro-interactions
17926
+ - Drop to custom CSS/JS only when utilities can't achieve the vision
17927
+
17928
+ **Spatial Composition**
17929
+ - Break conventions: asymmetry, overlap, diagonal flow, grid-breaking
17930
+ - Generous negative space OR controlled density\u2014commit to the choice
17931
+ - Unexpected layouts that guide the eye
17932
+
17933
+ **Visual Depth**
17934
+ - Create atmosphere beyond solid colors: gradient meshes, noise textures, geometric patterns
17935
+ - Layer transparencies, dramatic shadows, decorative borders
17936
+ - Contextual effects that match the aesthetic (grain overlays, custom cursors)
17937
+
17938
+ **Styling Approach**
17939
+ - Default to Tailwind CSS utility classes when available\u2014fast, maintainable, consistent
17940
+ - Use custom CSS when the vision requires it: complex animations, unique effects, advanced compositions
17941
+ - Balance utility-first speed with creative freedom where it matters
17942
+
17943
+ **Match Vision to Execution**
17944
+ - Maximalist designs \u2192 elaborate implementation, extensive animations, rich effects
17945
+ - Minimalist designs \u2192 restraint, precision, careful spacing and typography
17946
+ - Elegance comes from executing the chosen vision fully, not halfway
17221
17947
 
17222
17948
  ## Constraints
17223
17949
  - Respect existing design systems when present
@@ -17251,19 +17977,9 @@ var EXPLORER_PROMPT = `You are Explorer - a fast codebase navigation specialist.
17251
17977
 
17252
17978
  **Role**: Quick contextual grep for codebases. Answer "Where is X?", "Find Y", "Which file has Z".
17253
17979
 
17254
- **Tools Available**:
17255
- - **grep**: Fast regex content search (powered by ripgrep). Use for text patterns, function names, strings.
17256
- Example: grep(pattern="function handleClick", include="*.ts")
17257
- - **glob**: File pattern matching. Use to find files by name/extension.
17258
- - **ast_grep_search**: AST-aware structural search (25 languages). Use for code patterns.
17259
- - Meta-variables: $VAR (single node), $$$ (multiple nodes)
17260
- - Patterns must be complete AST nodes
17261
- - Example: ast_grep_search(pattern="console.log($MSG)", lang="typescript")
17262
- - Example: ast_grep_search(pattern="async function $NAME($$$) { $$$ }", lang="javascript")
17263
-
17264
- **When to use which**:
17980
+ **When to use which tools**:
17265
17981
  - **Text/regex patterns** (strings, comments, variable names): grep
17266
- - **Structural patterns** (function shapes, class structures): ast_grep_search
17982
+ - **Structural patterns** (function shapes, class structures): ast_grep_search
17267
17983
  - **File discovery** (find by name/extension): glob
17268
17984
 
17269
17985
  **Behavior**:
@@ -17448,165 +18164,6 @@ ${customAppendPrompt}`;
17448
18164
  };
17449
18165
  }
17450
18166
 
17451
- // src/agents/orchestrator.ts
17452
- var ORCHESTRATOR_PROMPT = `<Role>
17453
- You are an AI coding orchestrator that optimizes for quality, speed, cost, and reliability by delegating to specialists when it provides net efficiency gains.
17454
- </Role>
17455
-
17456
- <Agents>
17457
-
17458
- @explorer
17459
- - Role: Parallel search specialist for discovering unknowns across the codebase
17460
- - Capabilities: Glob, grep, AST queries to locate files, symbols, patterns
17461
- - **Delegate when:** Need to discover what exists before planning \u2022 Parallel searches speed discovery \u2022 Need summarized map vs full contents \u2022 Broad/uncertain scope
17462
- - **Don't delegate when:** Know the path and need actual content \u2022 Need full file anyway \u2022 Single specific lookup \u2022 About to edit the file
17463
-
17464
- @librarian
17465
- - Role: Authoritative source for current library docs and API references
17466
- - Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
17467
- - **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) \u2022 Complex APIs needing official examples (ORMs, auth) \u2022 Version-specific behavior matters \u2022 Unfamiliar library \u2022 Edge cases or advanced features \u2022 Nuanced best practices
17468
- - **Don't delegate when:** Standard usage you're confident about (\`Array.map()\`, \`fetch()\`) \u2022 Simple stable APIs \u2022 General programming knowledge \u2022 Info already in conversation \u2022 Built-in language features
17469
- - **Rule of thumb:** "How does this library work?" \u2192 @librarian. "How does programming work?" \u2192 yourself.
17470
-
17471
- @oracle
17472
- - Role: Strategic advisor for high-stakes decisions and persistent problems
17473
- - Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging
17474
- - Tools/Constraints: Slow, expensive, high-quality\u2014use sparingly when thoroughness beats speed
17475
- - **Delegate when:** Major architectural decisions with long-term impact \u2022 Problems persisting after 2+ fix attempts \u2022 High-risk multi-system refactors \u2022 Costly trade-offs (performance vs maintainability) \u2022 Complex debugging with unclear root cause \u2022 Security/scalability/data integrity decisions \u2022 Genuinely uncertain and cost of wrong choice is high
17476
- - **Don't delegate when:** Routine decisions you're confident about \u2022 First bug fix attempt \u2022 Straightforward trade-offs \u2022 Tactical "how" vs strategic "should" \u2022 Time-sensitive good-enough decisions \u2022 Quick research/testing can answer
17477
- - **Rule of thumb:** Need senior architect review? \u2192 @oracle. Just do it and PR? \u2192 yourself.
17478
-
17479
- @designer
17480
- - Role: UI/UX specialist for intentional, polished experiences
17481
- - Capabilities: Visual direction, interactions, responsive layouts, design systems with aesthetic intent
17482
- - **Delegate when:** User-facing interfaces needing polish \u2022 Responsive layouts \u2022 UX-critical components (forms, nav, dashboards) \u2022 Visual consistency systems \u2022 Animations/micro-interactions \u2022 Landing/marketing pages \u2022 Refining functional\u2192delightful
17483
- - **Don't delegate when:** Backend/logic with no visual \u2022 Quick prototypes where design doesn't matter yet
17484
- - **Rule of thumb:** Users see it and polish matters? \u2192 @designer. Headless/functional? \u2192 yourself.
17485
-
17486
- @fixer
17487
- - Role: Fast, parallel execution specialist for well-defined tasks
17488
- - Capabilities: Efficient implementation when spec and context are clear
17489
- - Tools/Constraints: Execution-focused\u2014no research, no architectural decisions
17490
- - **Delegate when:** Clearly specified with known approach \u2022 3+ independent parallel tasks \u2022 Straightforward but time-consuming \u2022 Solid plan needing execution \u2022 Repetitive multi-location changes \u2022 Overhead < time saved by parallelization
17491
- - **Don't delegate when:** Needs discovery/research/decisions \u2022 Single small change (<20 lines, one file) \u2022 Unclear requirements needing iteration \u2022 Explaining > doing \u2022 Tight integration with your current work \u2022 Sequential dependencies
17492
- - **Parallelization:** 3+ independent tasks \u2192 spawn multiple @fixers. 1-2 simple tasks \u2192 do yourself.
17493
- - **Rule of thumb:** Explaining > doing? \u2192 yourself. Can split to parallel streams? \u2192 multiple @fixers.
17494
-
17495
- </Agents>
17496
-
17497
- <Workflow>
17498
-
17499
- ## 1. Understand
17500
- Parse request: explicit requirements + implicit needs.
17501
-
17502
- ## 2. Path Analysis
17503
- Evaluate approach by: quality, speed, cost, reliability.
17504
- Choose the path that optimizes all four.
17505
-
17506
- ## 3. Delegation Check
17507
- **STOP. Review specialists before acting.**
17508
-
17509
- Each specialist delivers 10x results in their domain:
17510
- - @explorer \u2192 Parallel discovery when you need to find unknowns, not read knowns
17511
- - @librarian \u2192 Complex/evolving APIs where docs prevent errors, not basic usage
17512
- - @oracle \u2192 High-stakes decisions where wrong choice is costly, not routine calls
17513
- - @designer \u2192 User-facing experiences where polish matters, not internal logic
17514
- - @fixer \u2192 Parallel execution of clear specs, not explaining trivial changes
17515
-
17516
- **Delegation efficiency:**
17517
- - Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)
17518
- - Provide context summaries, let specialists read what they need
17519
- - Brief user on delegation goal before each call
17520
- - Skip delegation if overhead \u2265 doing it yourself
17521
-
17522
- **Fixer parallelization:**
17523
- - 3+ independent tasks? Spawn multiple @fixers simultaneously
17524
- - 1-2 simple tasks? Do it yourself
17525
- - Sequential dependencies? Handle serially or do yourself
17526
-
17527
- ## 4. Parallelize
17528
- Can tasks run simultaneously?
17529
- - Multiple @explorer searches across different domains?
17530
- - @explorer + @librarian research in parallel?
17531
- - Multiple @fixer instances for independent changes?
17532
-
17533
- Balance: respect dependencies, avoid parallelizing what must be sequential.
17534
-
17535
- ## 5. Execute
17536
- 1. Break complex tasks into todos if needed
17537
- 2. Fire parallel research/implementation
17538
- 3. Delegate to specialists or do it yourself based on step 3
17539
- 4. Integrate results
17540
- 5. Adjust if needed
17541
-
17542
- ## 6. Verify
17543
- - Run \`lsp_diagnostics\` for errors
17544
- - Suggest \`simplify\` skill when applicable
17545
- - Confirm specialists completed successfully
17546
- - Verify solution meets requirements
17547
-
17548
- ## Agent Role Mapping
17549
- When a workflow calls for an **implementer** subagent: dispatch \`@fixer\`. Fixer has enforced constraints (no research, no delegation, structured output) that match the implementer role exactly.
17550
- When a workflow calls for a **reviewer** subagent: dispatch \`@oracle\`. Oracle has the depth for architectural review and access to code review skills.
17551
-
17552
- </Workflow>
17553
-
17554
- <Communication>
17555
-
17556
- ## Clarity Over Assumptions
17557
- - If request is vague or has multiple valid interpretations, ask a targeted question before proceeding
17558
- - Don't guess at critical details (file paths, API choices, architectural decisions)
17559
- - Do make reasonable assumptions for minor details and state them briefly
17560
-
17561
- ## Concise Execution
17562
- - Answer directly, no preamble
17563
- - Don't summarize what you did unless asked
17564
- - Don't explain code unless asked
17565
- - One-word answers are fine when appropriate
17566
- - Brief delegation notices: "Checking docs via @librarian..." not "I'm going to delegate to @librarian because..."
17567
-
17568
- ## No Flattery
17569
- Never: "Great question!" "Excellent idea!" "Smart choice!" or any praise of user input.
17570
-
17571
- ## Honest Pushback
17572
- When user's approach seems problematic:
17573
- - State concern + alternative concisely
17574
- - Ask if they want to proceed anyway
17575
- - Don't lecture, don't blindly implement
17576
-
17577
- ## Example
17578
- **Bad:** "Great question! Let me think about the best approach here. I'm going to delegate to @librarian to check the latest Next.js documentation for the App Router, and then I'll implement the solution for you."
17579
-
17580
- **Good:** "Checking Next.js App Router docs via @librarian..."
17581
- [proceeds with implementation]
17582
-
17583
- </Communication>
17584
- `;
17585
- function createOrchestratorAgent(model, customPrompt, customAppendPrompt) {
17586
- let prompt = ORCHESTRATOR_PROMPT;
17587
- if (customPrompt) {
17588
- prompt = customPrompt;
17589
- } else if (customAppendPrompt) {
17590
- prompt = `${ORCHESTRATOR_PROMPT}
17591
-
17592
- ${customAppendPrompt}`;
17593
- }
17594
- const definition = {
17595
- name: "orchestrator",
17596
- description: "AI coding orchestrator that delegates tasks to specialist agents for optimal quality, speed, and cost",
17597
- config: {
17598
- temperature: 0.1,
17599
- prompt
17600
- }
17601
- };
17602
- if (Array.isArray(model)) {
17603
- definition._modelArray = model.map((m) => typeof m === "string" ? { id: m } : m);
17604
- } else if (typeof model === "string" && model) {
17605
- definition.config.model = model;
17606
- }
17607
- return definition;
17608
- }
17609
-
17610
18167
  // src/agents/index.ts
17611
18168
  function applyOverrides(agent, override) {
17612
18169
  if (override.model) {
@@ -17625,9 +18182,10 @@ function applyOverrides(agent, override) {
17625
18182
  function applyDefaultPermissions(agent, configuredSkills) {
17626
18183
  const existing = agent.config.permission ?? {};
17627
18184
  const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills);
18185
+ const questionPerm = existing.question === "deny" ? "deny" : "allow";
17628
18186
  agent.config.permission = {
17629
18187
  ...existing,
17630
- question: "allow",
18188
+ question: questionPerm,
17631
18189
  skill: {
17632
18190
  ...typeof existing.skill === "object" ? existing.skill : {},
17633
18191
  ...skillPermissions
@@ -17642,7 +18200,10 @@ var SUBAGENT_FACTORIES = {
17642
18200
  librarian: createLibrarianAgent,
17643
18201
  oracle: createOracleAgent,
17644
18202
  designer: createDesignerAgent,
17645
- fixer: createFixerAgent
18203
+ fixer: createFixerAgent,
18204
+ council: createCouncilAgent,
18205
+ councillor: createCouncillorAgent,
18206
+ "council-master": createCouncilMasterAgent
17646
18207
  };
17647
18208
  function createAgents(config2) {
17648
18209
  const getModelForAgent = (name) => {
@@ -17689,7 +18250,12 @@ function getAgentConfigs(config2) {
17689
18250
  description: a.description,
17690
18251
  mcps: getAgentMcpList(a.name, config2)
17691
18252
  };
17692
- if (isSubagent(a.name)) {
18253
+ if (a.name === "council") {
18254
+ sdkConfig.mode = "all";
18255
+ } else if (a.name === "councillor" || a.name === "council-master") {
18256
+ sdkConfig.mode = "subagent";
18257
+ sdkConfig.hidden = true;
18258
+ } else if (isSubagent(a.name)) {
17693
18259
  sdkConfig.mode = "subagent";
17694
18260
  } else if (a.name === "orchestrator") {
17695
18261
  sdkConfig.mode = "primary";
@@ -17700,9 +18266,9 @@ function getAgentConfigs(config2) {
17700
18266
 
17701
18267
  // src/utils/logger.ts
17702
18268
  import * as fs2 from "fs";
17703
- import * as os2 from "os";
18269
+ import * as os from "os";
17704
18270
  import * as path2 from "path";
17705
- var logFile = path2.join(os2.tmpdir(), "oh-my-opencode-slim.log");
18271
+ var logFile = path2.join(os.tmpdir(), "oh-my-opencode-slim.log");
17706
18272
  function log(message, data) {
17707
18273
  try {
17708
18274
  const timestamp = new Date().toISOString();
@@ -18077,17 +18643,48 @@ async function extractZip(archivePath, destDir) {
18077
18643
  throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
18078
18644
  }
18079
18645
  }
18080
- // src/background/background-manager.ts
18081
- function parseModelReference(model) {
18082
- const slashIndex = model.indexOf("/");
18083
- if (slashIndex <= 0 || slashIndex >= model.length - 1) {
18084
- return null;
18646
+ // src/background/subagent-depth.ts
18647
+ class SubagentDepthTracker {
18648
+ depthBySession = new Map;
18649
+ _maxDepth;
18650
+ constructor(maxDepth = DEFAULT_MAX_SUBAGENT_DEPTH) {
18651
+ this._maxDepth = maxDepth;
18652
+ }
18653
+ get maxDepth() {
18654
+ return this._maxDepth;
18655
+ }
18656
+ getDepth(sessionId) {
18657
+ return this.depthBySession.get(sessionId) ?? 0;
18658
+ }
18659
+ registerChild(parentSessionId, childSessionId) {
18660
+ const parentDepth = this.getDepth(parentSessionId);
18661
+ const childDepth = parentDepth + 1;
18662
+ if (childDepth > this.maxDepth) {
18663
+ log("[subagent-depth] spawn blocked: max depth exceeded", {
18664
+ parentSessionId,
18665
+ parentDepth,
18666
+ childDepth,
18667
+ maxDepth: this.maxDepth
18668
+ });
18669
+ return false;
18670
+ }
18671
+ this.depthBySession.set(childSessionId, childDepth);
18672
+ log("[subagent-depth] child registered", {
18673
+ parentSessionId,
18674
+ childSessionId,
18675
+ childDepth
18676
+ });
18677
+ return true;
18678
+ }
18679
+ cleanup(sessionId) {
18680
+ this.depthBySession.delete(sessionId);
18681
+ }
18682
+ cleanupAll() {
18683
+ this.depthBySession.clear();
18085
18684
  }
18086
- return {
18087
- providerID: model.slice(0, slashIndex),
18088
- modelID: model.slice(slashIndex + 1)
18089
- };
18090
18685
  }
18686
+
18687
+ // src/background/background-manager.ts
18091
18688
  function generateTaskId() {
18092
18689
  return `bg_${Math.random().toString(36).substring(2, 10)}`;
18093
18690
  }
@@ -18096,6 +18693,7 @@ class BackgroundTaskManager {
18096
18693
  tasks = new Map;
18097
18694
  tasksBySessionId = new Map;
18098
18695
  agentBySessionId = new Map;
18696
+ depthTracker;
18099
18697
  client;
18100
18698
  directory;
18101
18699
  tmuxEnabled;
@@ -18114,6 +18712,7 @@ class BackgroundTaskManager {
18114
18712
  maxConcurrentStarts: 10
18115
18713
  };
18116
18714
  this.maxConcurrentStarts = this.backgroundConfig.maxConcurrentStarts;
18715
+ this.depthTracker = new SubagentDepthTracker;
18117
18716
  }
18118
18717
  getSubagentRules(agentName) {
18119
18718
  return SUBAGENT_DELEGATION_RULES[agentName] ?? ["explorer"];
@@ -18186,20 +18785,6 @@ class BackgroundTaskManager {
18186
18785
  }
18187
18786
  return chain;
18188
18787
  }
18189
- async promptWithTimeout(args, timeoutMs) {
18190
- if (timeoutMs <= 0) {
18191
- await this.client.session.prompt(args);
18192
- return;
18193
- }
18194
- await Promise.race([
18195
- this.client.session.prompt(args),
18196
- new Promise((_, reject) => {
18197
- setTimeout(() => {
18198
- reject(new Error(`Prompt timed out after ${timeoutMs}ms`));
18199
- }, timeoutMs);
18200
- })
18201
- ]);
18202
- }
18203
18788
  calculateToolPermissions(agentName) {
18204
18789
  const allowedSubagents = this.getSubagentRules(agentName);
18205
18790
  if (allowedSubagents.length === 0) {
@@ -18215,20 +18800,31 @@ class BackgroundTaskManager {
18215
18800
  return;
18216
18801
  }
18217
18802
  try {
18218
- const session = await this.client.session.create({
18803
+ const parentDepth = this.depthTracker.getDepth(task.parentSessionId);
18804
+ if (parentDepth + 1 > this.depthTracker.maxDepth) {
18805
+ log("[background-manager] spawn blocked: max depth exceeded", {
18806
+ parentSessionId: task.parentSessionId,
18807
+ parentDepth,
18808
+ maxDepth: this.depthTracker.maxDepth
18809
+ });
18810
+ this.completeTask(task, "failed", "Subagent depth exceeded");
18811
+ return;
18812
+ }
18813
+ const session2 = await this.client.session.create({
18219
18814
  body: {
18220
18815
  parentID: task.parentSessionId,
18221
18816
  title: `Background: ${task.description}`
18222
18817
  },
18223
18818
  query: { directory: this.directory }
18224
18819
  });
18225
- if (!session.data?.id) {
18820
+ if (!session2.data?.id) {
18226
18821
  throw new Error("Failed to create background session");
18227
18822
  }
18228
- task.sessionId = session.data.id;
18229
- this.tasksBySessionId.set(session.data.id, task.id);
18230
- this.agentBySessionId.set(session.data.id, task.agent);
18823
+ task.sessionId = session2.data.id;
18824
+ this.tasksBySessionId.set(session2.data.id, task.id);
18825
+ this.agentBySessionId.set(session2.data.id, task.agent);
18231
18826
  task.status = "running";
18827
+ this.depthTracker.registerChild(task.parentSessionId, session2.data.id);
18232
18828
  if (this.tmuxEnabled) {
18233
18829
  await new Promise((r) => setTimeout(r, 500));
18234
18830
  }
@@ -18242,11 +18838,15 @@ class BackgroundTaskManager {
18242
18838
  });
18243
18839
  const fallbackEnabled = this.config?.fallback?.enabled ?? true;
18244
18840
  const timeoutMs = fallbackEnabled ? this.config?.fallback?.timeoutMs ?? FALLBACK_FAILOVER_TIMEOUT_MS : 0;
18841
+ const retryDelayMs = this.config?.fallback?.retryDelayMs ?? 500;
18245
18842
  const chain = fallbackEnabled ? this.resolveFallbackChain(task.agent) : [];
18246
18843
  const attemptModels = chain.length > 0 ? chain : [undefined];
18247
18844
  const errors3 = [];
18248
18845
  let succeeded = false;
18249
- for (const model of attemptModels) {
18846
+ const sessionId = session2.data.id;
18847
+ for (let i = 0;i < attemptModels.length; i++) {
18848
+ const model = attemptModels[i];
18849
+ const modelLabel = model ?? "default-model";
18250
18850
  try {
18251
18851
  const body = {
18252
18852
  ...basePromptBody,
@@ -18259,8 +18859,11 @@ class BackgroundTaskManager {
18259
18859
  }
18260
18860
  body.model = ref;
18261
18861
  }
18262
- await this.promptWithTimeout({
18263
- path: { id: session.data.id },
18862
+ if (i > 0) {
18863
+ log(`[background-manager] fallback attempt ${i + 1}/${attemptModels.length}: ${modelLabel}`, { taskId: task.id });
18864
+ }
18865
+ await promptWithTimeout(this.client, {
18866
+ path: { id: sessionId },
18264
18867
  body,
18265
18868
  query: promptQuery
18266
18869
  }, timeoutMs);
@@ -18268,10 +18871,17 @@ class BackgroundTaskManager {
18268
18871
  break;
18269
18872
  } catch (error48) {
18270
18873
  const msg = error48 instanceof Error ? error48.message : String(error48);
18271
- if (model) {
18272
- errors3.push(`${model}: ${msg}`);
18273
- } else {
18274
- errors3.push(`default-model: ${msg}`);
18874
+ errors3.push(`${modelLabel}: ${msg}`);
18875
+ log(`[background-manager] model failed: ${modelLabel} \u2014 ${msg}`, {
18876
+ taskId: task.id
18877
+ });
18878
+ if (i < attemptModels.length - 1) {
18879
+ try {
18880
+ await this.client.session.abort({
18881
+ path: { id: sessionId }
18882
+ });
18883
+ await new Promise((r) => setTimeout(r, retryDelayMs));
18884
+ } catch {}
18275
18885
  }
18276
18886
  }
18277
18887
  }
@@ -18279,7 +18889,7 @@ class BackgroundTaskManager {
18279
18889
  throw new Error(`All fallback models failed. ${errors3.join(" | ")}`);
18280
18890
  }
18281
18891
  log(`[background-manager] task started: ${task.id}`, {
18282
- sessionId: session.data.id
18892
+ sessionId: session2.data.id
18283
18893
  });
18284
18894
  } catch (error48) {
18285
18895
  const errorMessage = error48 instanceof Error ? error48.message : String(error48);
@@ -18324,6 +18934,7 @@ class BackgroundTaskManager {
18324
18934
  task.error = "Session deleted";
18325
18935
  this.tasksBySessionId.delete(sessionId);
18326
18936
  this.agentBySessionId.delete(sessionId);
18937
+ this.depthTracker.cleanup(sessionId);
18327
18938
  const resolver = this.completionResolvers.get(taskId);
18328
18939
  if (resolver) {
18329
18940
  resolver(task);
@@ -18336,22 +18947,7 @@ class BackgroundTaskManager {
18336
18947
  if (!task.sessionId)
18337
18948
  return;
18338
18949
  try {
18339
- const messagesResult = await this.client.session.messages({
18340
- path: { id: task.sessionId }
18341
- });
18342
- const messages = messagesResult.data ?? [];
18343
- const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
18344
- const extractedContent = [];
18345
- for (const message of assistantMessages) {
18346
- for (const part of message.parts ?? []) {
18347
- if ((part.type === "text" || part.type === "reasoning") && part.text) {
18348
- extractedContent.push(part.text);
18349
- }
18350
- }
18351
- }
18352
- const responseText = extractedContent.filter((t) => t.length > 0).join(`
18353
-
18354
- `);
18950
+ const responseText = await extractSessionResult(this.client, task.sessionId);
18355
18951
  if (responseText) {
18356
18952
  this.completeTask(task, "completed", responseText);
18357
18953
  } else {
@@ -18467,6 +19063,10 @@ class BackgroundTaskManager {
18467
19063
  this.tasks.clear();
18468
19064
  this.tasksBySessionId.clear();
18469
19065
  this.agentBySessionId.clear();
19066
+ this.depthTracker.cleanupAll();
19067
+ }
19068
+ getDepthTracker() {
19069
+ return this.depthTracker;
18470
19070
  }
18471
19071
  }
18472
19072
  // src/background/tmux-session-manager.ts
@@ -18602,54 +19202,315 @@ class TmuxSessionManager {
18602
19202
  for (const sessionId of sessionsToClose) {
18603
19203
  await this.closeSession(sessionId);
18604
19204
  }
18605
- } catch (err) {
18606
- log("[tmux-session-manager] poll error", { error: String(err) });
18607
- }
18608
- }
18609
- async closeSession(sessionId) {
18610
- const tracked = this.sessions.get(sessionId);
18611
- if (!tracked)
18612
- return;
18613
- log("[tmux-session-manager] closing session pane", {
18614
- sessionId,
18615
- paneId: tracked.paneId
19205
+ } catch (err) {
19206
+ log("[tmux-session-manager] poll error", { error: String(err) });
19207
+ }
19208
+ }
19209
+ async closeSession(sessionId) {
19210
+ const tracked = this.sessions.get(sessionId);
19211
+ if (!tracked)
19212
+ return;
19213
+ log("[tmux-session-manager] closing session pane", {
19214
+ sessionId,
19215
+ paneId: tracked.paneId
19216
+ });
19217
+ await closeTmuxPane(tracked.paneId);
19218
+ this.sessions.delete(sessionId);
19219
+ if (this.sessions.size === 0) {
19220
+ this.stopPolling();
19221
+ }
19222
+ }
19223
+ async cleanup() {
19224
+ this.stopPolling();
19225
+ if (this.sessions.size > 0) {
19226
+ log("[tmux-session-manager] closing all panes", {
19227
+ count: this.sessions.size
19228
+ });
19229
+ const closePromises = Array.from(this.sessions.values()).map((s) => closeTmuxPane(s.paneId).catch((err) => log("[tmux-session-manager] cleanup error for pane", {
19230
+ paneId: s.paneId,
19231
+ error: String(err)
19232
+ })));
19233
+ await Promise.all(closePromises);
19234
+ this.sessions.clear();
19235
+ }
19236
+ log("[tmux-session-manager] cleanup complete");
19237
+ }
19238
+ }
19239
+ // src/council/council-manager.ts
19240
+ class CouncilManager {
19241
+ client;
19242
+ directory;
19243
+ config;
19244
+ depthTracker;
19245
+ tmuxEnabled;
19246
+ constructor(ctx, config2, depthTracker, tmuxEnabled = false) {
19247
+ this.client = ctx.client;
19248
+ this.directory = ctx.directory;
19249
+ this.config = config2;
19250
+ this.depthTracker = depthTracker;
19251
+ this.tmuxEnabled = tmuxEnabled;
19252
+ }
19253
+ async runCouncil(prompt, presetName, parentSessionId) {
19254
+ if (this.depthTracker) {
19255
+ const parentDepth = this.depthTracker.getDepth(parentSessionId);
19256
+ if (parentDepth + 1 > this.depthTracker.maxDepth) {
19257
+ log("[council-manager] spawn blocked: max depth exceeded", {
19258
+ parentSessionId,
19259
+ parentDepth,
19260
+ maxDepth: this.depthTracker.maxDepth
19261
+ });
19262
+ return {
19263
+ success: false,
19264
+ error: "Subagent depth exceeded",
19265
+ councillorResults: []
19266
+ };
19267
+ }
19268
+ }
19269
+ const councilConfig = this.config?.council;
19270
+ if (!councilConfig) {
19271
+ log("[council-manager] Council configuration not found");
19272
+ return {
19273
+ success: false,
19274
+ error: "Council not configured",
19275
+ councillorResults: []
19276
+ };
19277
+ }
19278
+ const resolvedPreset = presetName ?? councilConfig.default_preset ?? "default";
19279
+ const preset = councilConfig.presets[resolvedPreset];
19280
+ if (!preset) {
19281
+ log(`[council-manager] Preset "${resolvedPreset}" not found`);
19282
+ return {
19283
+ success: false,
19284
+ error: `Preset "${resolvedPreset}" not found`,
19285
+ councillorResults: []
19286
+ };
19287
+ }
19288
+ if (Object.keys(preset.councillors).length === 0) {
19289
+ log(`[council-manager] Preset "${resolvedPreset}" has no councillors`);
19290
+ return {
19291
+ success: false,
19292
+ error: `Preset "${resolvedPreset}" has no councillors configured`,
19293
+ councillorResults: []
19294
+ };
19295
+ }
19296
+ const councillorsTimeout = councilConfig.councillors_timeout ?? 180000;
19297
+ const masterTimeout = councilConfig.master_timeout ?? 300000;
19298
+ const councillorCount = Object.keys(preset.councillors).length;
19299
+ log(`[council-manager] Starting council with preset "${resolvedPreset}"`, {
19300
+ councillors: Object.keys(preset.councillors)
19301
+ });
19302
+ this.sendStartNotification(parentSessionId, councillorCount).catch((err) => {
19303
+ log("[council-manager] Failed to send start notification", {
19304
+ error: err instanceof Error ? err.message : String(err)
19305
+ });
19306
+ });
19307
+ const councillorResults = await this.runCouncillors(prompt, preset.councillors, parentSessionId, councillorsTimeout);
19308
+ const completedCount = councillorResults.filter((r) => r.status === "completed").length;
19309
+ log(`[council-manager] Councillors completed: ${completedCount}/${councillorResults.length}`);
19310
+ if (completedCount === 0) {
19311
+ return {
19312
+ success: false,
19313
+ error: "All councillors failed or timed out",
19314
+ councillorResults
19315
+ };
19316
+ }
19317
+ const masterResult = await this.runMaster(prompt, councillorResults, councilConfig, parentSessionId, masterTimeout, preset.master);
19318
+ if (!masterResult.success) {
19319
+ log("[council-manager] Master failed", {
19320
+ error: masterResult.error
19321
+ });
19322
+ const bestResult = councillorResults.find((r) => r.status === "completed" && r.result);
19323
+ return {
19324
+ success: false,
19325
+ error: masterResult.error ?? "Council master failed",
19326
+ result: bestResult?.result ? `(Degraded \u2014 master failed, using ${bestResult.name}'s response)
19327
+
19328
+ ${bestResult.result}` : undefined,
19329
+ councillorResults
19330
+ };
19331
+ }
19332
+ log("[council-manager] Council completed successfully");
19333
+ return {
19334
+ success: true,
19335
+ result: masterResult.result,
19336
+ councillorResults
19337
+ };
19338
+ }
19339
+ async sendStartNotification(parentSessionId, councillorCount) {
19340
+ const message = [
19341
+ `\u2394 Council starting \u2014 ${councillorCount} councillors launching \u2014 ctrl+x \u2193 to watch`,
19342
+ "",
19343
+ "[system status: continue without acknowledging this notification]"
19344
+ ].join(`
19345
+ `);
19346
+ await this.client.session.prompt({
19347
+ path: { id: parentSessionId },
19348
+ body: {
19349
+ noReply: true,
19350
+ parts: [{ type: "text", text: message }]
19351
+ }
19352
+ });
19353
+ }
19354
+ async runAgentSession(options) {
19355
+ const modelRef = parseModelReference(options.model);
19356
+ if (!modelRef) {
19357
+ throw new Error(`Invalid model format: ${options.model}`);
19358
+ }
19359
+ let sessionId;
19360
+ try {
19361
+ const session2 = await this.client.session.create({
19362
+ body: {
19363
+ parentID: options.parentSessionId,
19364
+ title: options.title
19365
+ },
19366
+ query: { directory: this.directory }
19367
+ });
19368
+ if (!session2.data?.id) {
19369
+ throw new Error("Failed to create session");
19370
+ }
19371
+ sessionId = session2.data.id;
19372
+ if (this.depthTracker) {
19373
+ const registered = this.depthTracker.registerChild(options.parentSessionId, sessionId);
19374
+ if (!registered) {
19375
+ throw new Error("Subagent depth exceeded");
19376
+ }
19377
+ }
19378
+ if (this.tmuxEnabled) {
19379
+ await new Promise((r) => setTimeout(r, TMUX_SPAWN_DELAY_MS));
19380
+ }
19381
+ const body = {
19382
+ agent: options.agent,
19383
+ model: modelRef,
19384
+ tools: { background_task: false, task: false },
19385
+ parts: [{ type: "text", text: options.promptText }]
19386
+ };
19387
+ if (options.variant) {
19388
+ body.variant = options.variant;
19389
+ }
19390
+ await promptWithTimeout(this.client, {
19391
+ path: { id: sessionId },
19392
+ body,
19393
+ query: { directory: this.directory }
19394
+ }, options.timeout);
19395
+ const result = await extractSessionResult(this.client, sessionId, {
19396
+ includeReasoning: options.includeReasoning
19397
+ });
19398
+ return result || "(No output)";
19399
+ } finally {
19400
+ if (sessionId) {
19401
+ this.client.session.abort({ path: { id: sessionId } }).catch(() => {});
19402
+ if (this.depthTracker) {
19403
+ this.depthTracker.cleanup(sessionId);
19404
+ }
19405
+ }
19406
+ }
19407
+ }
19408
+ async runCouncillors(prompt, councillors, parentSessionId, timeout) {
19409
+ const entries = Object.entries(councillors);
19410
+ const promises = entries.map(([name, config2], index) => (async () => {
19411
+ if (index > 0) {
19412
+ await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
19413
+ }
19414
+ const modelLabel = shortModelLabel(config2.model);
19415
+ try {
19416
+ const result = await this.runAgentSession({
19417
+ parentSessionId,
19418
+ title: `Council ${name} (${modelLabel})`,
19419
+ agent: "councillor",
19420
+ model: config2.model,
19421
+ promptText: formatCouncillorPrompt(prompt, config2.prompt),
19422
+ variant: config2.variant,
19423
+ timeout,
19424
+ includeReasoning: false
19425
+ });
19426
+ return {
19427
+ name,
19428
+ model: config2.model,
19429
+ status: "completed",
19430
+ result
19431
+ };
19432
+ } catch (error48) {
19433
+ const msg = error48 instanceof Error ? error48.message : String(error48);
19434
+ return {
19435
+ name,
19436
+ model: config2.model,
19437
+ status: msg.includes("timed out") ? "timed_out" : "failed",
19438
+ error: `Councillor "${name}": ${msg}`
19439
+ };
19440
+ }
19441
+ })());
19442
+ const settled = await Promise.allSettled(promises);
19443
+ return settled.map((result, index) => {
19444
+ const [name, cfg] = entries[index];
19445
+ if (result.status === "fulfilled") {
19446
+ return {
19447
+ name,
19448
+ model: cfg.model,
19449
+ status: result.value.status,
19450
+ result: result.value.result,
19451
+ error: result.value.error
19452
+ };
19453
+ }
19454
+ return {
19455
+ name,
19456
+ model: cfg.model,
19457
+ status: "failed",
19458
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
19459
+ };
18616
19460
  });
18617
- await closeTmuxPane(tracked.paneId);
18618
- this.sessions.delete(sessionId);
18619
- if (this.sessions.size === 0) {
18620
- this.stopPolling();
18621
- }
18622
19461
  }
18623
- async cleanup() {
18624
- this.stopPolling();
18625
- if (this.sessions.size > 0) {
18626
- log("[tmux-session-manager] closing all panes", {
18627
- count: this.sessions.size
18628
- });
18629
- const closePromises = Array.from(this.sessions.values()).map((s) => closeTmuxPane(s.paneId).catch((err) => log("[tmux-session-manager] cleanup error for pane", {
18630
- paneId: s.paneId,
18631
- error: String(err)
18632
- })));
18633
- await Promise.all(closePromises);
18634
- this.sessions.clear();
19462
+ async runMaster(prompt, councillorResults, councilConfig, parentSessionId, timeout, presetMasterOverride) {
19463
+ const masterConfig = councilConfig.master;
19464
+ const fallbackModels = councilConfig.master_fallback ?? [];
19465
+ const effectiveModel = presetMasterOverride?.model ?? masterConfig.model;
19466
+ const effectiveVariant = presetMasterOverride?.variant ?? masterConfig.variant;
19467
+ const effectivePrompt = presetMasterOverride?.prompt ?? masterConfig.prompt;
19468
+ const attemptModels = [effectiveModel, ...fallbackModels];
19469
+ const synthesisPrompt = formatMasterSynthesisPrompt(prompt, councillorResults, effectivePrompt);
19470
+ const errors3 = [];
19471
+ for (let i = 0;i < attemptModels.length; i++) {
19472
+ const model = attemptModels[i];
19473
+ const currentLabel = shortModelLabel(model);
19474
+ try {
19475
+ if (i > 0) {
19476
+ log(`[council-manager] master fallback ${i}/${attemptModels.length - 1}: ${currentLabel}`);
19477
+ }
19478
+ const result = await this.runAgentSession({
19479
+ parentSessionId,
19480
+ title: `Council Master (${currentLabel})`,
19481
+ agent: "council-master",
19482
+ model,
19483
+ promptText: synthesisPrompt,
19484
+ variant: effectiveVariant,
19485
+ timeout
19486
+ });
19487
+ return { success: true, result };
19488
+ } catch (error48) {
19489
+ const msg = error48 instanceof Error ? error48.message : String(error48);
19490
+ errors3.push(`${currentLabel}: ${msg}`);
19491
+ log(`[council-manager] master model failed: ${currentLabel} \u2014 ${msg}`);
19492
+ }
18635
19493
  }
18636
- log("[tmux-session-manager] cleanup complete");
19494
+ return {
19495
+ success: false,
19496
+ error: `All master models failed. ${errors3.join(" | ")}`
19497
+ };
18637
19498
  }
18638
19499
  }
18639
19500
  // src/hooks/auto-update-checker/cache.ts
18640
19501
  import * as fs3 from "fs";
18641
19502
  import * as path4 from "path";
18642
19503
  // src/hooks/auto-update-checker/constants.ts
18643
- import * as os3 from "os";
19504
+ import * as os2 from "os";
18644
19505
  import * as path3 from "path";
18645
19506
  var PACKAGE_NAME = "oh-my-opencode-slim";
18646
19507
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
18647
19508
  var NPM_FETCH_TIMEOUT = 5000;
18648
19509
  function getCacheDir() {
18649
19510
  if (process.platform === "win32") {
18650
- return path3.join(process.env.LOCALAPPDATA ?? os3.homedir(), "opencode");
19511
+ return path3.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
18651
19512
  }
18652
- return path3.join(os3.homedir(), ".cache", "opencode");
19513
+ return path3.join(os2.homedir(), ".cache", "opencode");
18653
19514
  }
18654
19515
  var CACHE_DIR = getCacheDir();
18655
19516
  var INSTALLED_PACKAGE_JSON = path3.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
@@ -19186,12 +20047,224 @@ ${buildRetryGuidance(detected)}`;
19186
20047
  }
19187
20048
  };
19188
20049
  }
20050
+ // src/hooks/foreground-fallback/index.ts
20051
+ var RATE_LIMIT_PATTERNS = [
20052
+ /\b429\b/,
20053
+ /rate.?limit/i,
20054
+ /too many requests/i,
20055
+ /quota.?exceeded/i,
20056
+ /usage.?exceeded/i,
20057
+ /usage limit/i,
20058
+ /overloaded/i,
20059
+ /resource.?exhausted/i,
20060
+ /insufficient.?quota/i,
20061
+ /high concurrency/i,
20062
+ /reduce concurrency/i
20063
+ ];
20064
+ function isRateLimitError(error48) {
20065
+ if (!error48 || typeof error48 !== "object")
20066
+ return false;
20067
+ const err = error48;
20068
+ const text = [
20069
+ err.message ?? "",
20070
+ String(err.data?.statusCode ?? ""),
20071
+ err.data?.message ?? "",
20072
+ err.data?.responseBody ?? ""
20073
+ ].join(" ");
20074
+ return RATE_LIMIT_PATTERNS.some((p) => p.test(text));
20075
+ }
20076
+ function parseModel(model) {
20077
+ const slash = model.indexOf("/");
20078
+ if (slash <= 0 || slash >= model.length - 1)
20079
+ return null;
20080
+ return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
20081
+ }
20082
+ var DEDUP_WINDOW_MS = 5000;
20083
+
20084
+ class ForegroundFallbackManager {
20085
+ client;
20086
+ chains;
20087
+ enabled;
20088
+ sessionModel = new Map;
20089
+ sessionAgent = new Map;
20090
+ sessionTried = new Map;
20091
+ inProgress = new Set;
20092
+ lastTrigger = new Map;
20093
+ constructor(client, chains, enabled) {
20094
+ this.client = client;
20095
+ this.chains = chains;
20096
+ this.enabled = enabled;
20097
+ }
20098
+ async handleEvent(rawEvent) {
20099
+ if (!this.enabled)
20100
+ return;
20101
+ const event = rawEvent;
20102
+ if (!event?.type)
20103
+ return;
20104
+ switch (event.type) {
20105
+ case "message.updated": {
20106
+ const info = event.properties?.info;
20107
+ if (!info)
20108
+ break;
20109
+ const sessionID = info.sessionID;
20110
+ if (!sessionID)
20111
+ break;
20112
+ if (typeof info.agent === "string") {
20113
+ this.sessionAgent.set(sessionID, info.agent);
20114
+ }
20115
+ if (typeof info.providerID === "string" && typeof info.modelID === "string") {
20116
+ this.sessionModel.set(sessionID, `${info.providerID}/${info.modelID}`);
20117
+ }
20118
+ if (info.error && isRateLimitError(info.error)) {
20119
+ await this.tryFallback(sessionID);
20120
+ }
20121
+ break;
20122
+ }
20123
+ case "session.error": {
20124
+ const props = event.properties;
20125
+ if (props?.sessionID && props.error && isRateLimitError(props.error)) {
20126
+ await this.tryFallback(props.sessionID);
20127
+ }
20128
+ break;
20129
+ }
20130
+ case "session.status": {
20131
+ const props = event.properties;
20132
+ if (!props?.sessionID || props.status?.type !== "retry")
20133
+ break;
20134
+ const msg = props.status.message?.toLowerCase() ?? "";
20135
+ 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")) {
20136
+ await this.tryFallback(props.sessionID);
20137
+ }
20138
+ break;
20139
+ }
20140
+ case "subagent.session.created": {
20141
+ const props = event.properties;
20142
+ if (props?.sessionID && typeof props.agentName === "string") {
20143
+ this.sessionAgent.set(props.sessionID, props.agentName);
20144
+ }
20145
+ break;
20146
+ }
20147
+ case "session.deleted": {
20148
+ const props = event.properties;
20149
+ const id = props?.info?.id ?? props?.sessionID;
20150
+ if (id) {
20151
+ this.sessionModel.delete(id);
20152
+ this.sessionAgent.delete(id);
20153
+ this.sessionTried.delete(id);
20154
+ this.inProgress.delete(id);
20155
+ this.lastTrigger.delete(id);
20156
+ }
20157
+ break;
20158
+ }
20159
+ }
20160
+ }
20161
+ async tryFallback(sessionID) {
20162
+ if (!sessionID)
20163
+ return;
20164
+ if (this.inProgress.has(sessionID))
20165
+ return;
20166
+ const now = Date.now();
20167
+ if (now - (this.lastTrigger.get(sessionID) ?? 0) < DEDUP_WINDOW_MS)
20168
+ return;
20169
+ this.lastTrigger.set(sessionID, now);
20170
+ this.inProgress.add(sessionID);
20171
+ try {
20172
+ const currentModel = this.sessionModel.get(sessionID);
20173
+ const agentName = this.sessionAgent.get(sessionID);
20174
+ const chain = this.resolveChain(agentName, currentModel);
20175
+ if (!chain.length) {
20176
+ log("[foreground-fallback] no chain configured", {
20177
+ sessionID,
20178
+ agentName
20179
+ });
20180
+ return;
20181
+ }
20182
+ if (!this.sessionTried.has(sessionID)) {
20183
+ this.sessionTried.set(sessionID, new Set);
20184
+ }
20185
+ const tried = this.sessionTried.get(sessionID);
20186
+ if (currentModel)
20187
+ tried.add(currentModel);
20188
+ const nextModel = chain.find((m) => !tried.has(m));
20189
+ if (!nextModel) {
20190
+ log("[foreground-fallback] fallback chain exhausted", {
20191
+ sessionID,
20192
+ agentName,
20193
+ tried: [...tried]
20194
+ });
20195
+ return;
20196
+ }
20197
+ tried.add(nextModel);
20198
+ const ref = parseModel(nextModel);
20199
+ if (!ref) {
20200
+ log("[foreground-fallback] invalid model format", {
20201
+ sessionID,
20202
+ nextModel
20203
+ });
20204
+ return;
20205
+ }
20206
+ const result = await this.client.session.messages({
20207
+ path: { id: sessionID }
20208
+ });
20209
+ const messages = result.data ?? [];
20210
+ const lastUser = [...messages].reverse().find((m) => m.info.role === "user");
20211
+ if (!lastUser) {
20212
+ log("[foreground-fallback] no user message found", { sessionID });
20213
+ return;
20214
+ }
20215
+ try {
20216
+ await this.client.session.abort({ path: { id: sessionID } });
20217
+ } catch {}
20218
+ await new Promise((r) => setTimeout(r, 500));
20219
+ const sessionClient = this.client.session;
20220
+ await sessionClient.promptAsync({
20221
+ path: { id: sessionID },
20222
+ body: { parts: lastUser.parts, model: ref }
20223
+ });
20224
+ this.sessionModel.set(sessionID, nextModel);
20225
+ log("[foreground-fallback] switched to fallback model", {
20226
+ sessionID,
20227
+ agentName,
20228
+ from: currentModel,
20229
+ to: nextModel
20230
+ });
20231
+ } catch (err) {
20232
+ log("[foreground-fallback] fallback attempt failed", {
20233
+ sessionID,
20234
+ error: err instanceof Error ? err.message : String(err)
20235
+ });
20236
+ } finally {
20237
+ this.inProgress.delete(sessionID);
20238
+ }
20239
+ }
20240
+ resolveChain(agentName, currentModel) {
20241
+ if (agentName) {
20242
+ return this.chains[agentName] ?? [];
20243
+ }
20244
+ if (currentModel) {
20245
+ for (const chain of Object.values(this.chains)) {
20246
+ if (chain.includes(currentModel))
20247
+ return chain;
20248
+ }
20249
+ }
20250
+ const all = [];
20251
+ const seen = new Set;
20252
+ for (const chain of Object.values(this.chains)) {
20253
+ for (const m of chain) {
20254
+ if (!seen.has(m)) {
20255
+ seen.add(m);
20256
+ all.push(m);
20257
+ }
20258
+ }
20259
+ }
20260
+ return all;
20261
+ }
20262
+ }
19189
20263
  // src/hooks/json-error-recovery/hook.ts
19190
20264
  var JSON_ERROR_TOOL_EXCLUDE_LIST = [
19191
20265
  "bash",
19192
20266
  "read",
19193
20267
  "glob",
19194
- "grep",
19195
20268
  "webfetch",
19196
20269
  "grep_app_searchgithub",
19197
20270
  "websearch_web_search_exa"
@@ -19239,9 +20312,7 @@ ${JSON_ERROR_REMINDER}`;
19239
20312
  };
19240
20313
  }
19241
20314
  // src/hooks/phase-reminder/index.ts
19242
- var PHASE_REMINDER = `<reminder>Recall Workflow Rules:
19243
- Understand \u2192 find the best path (delegate based on rules and parallelize independent work) \u2192 execute \u2192 verify.
19244
- If delegating, launch the specialist in the same turn you mention it.</reminder>`;
20315
+ var PHASE_REMINDER = `<reminder>${PHASE_REMINDER_TEXT}</reminder>`;
19245
20316
  function createPhaseReminderHook() {
19246
20317
  return {
19247
20318
  "experimental.chat.messages.transform": async (_input, output) => {
@@ -19280,15 +20351,15 @@ ${originalText}`;
19280
20351
  }
19281
20352
  };
19282
20353
  }
19283
- // src/hooks/post-read-nudge/index.ts
20354
+ // src/hooks/post-file-tool-nudge/index.ts
19284
20355
  var NUDGE = `
19285
20356
 
19286
20357
  ---
19287
- Workflow Reminder: delegate based on rules; If mentioning a specialist, launch it in this same turn.`;
19288
- function createPostReadNudgeHook() {
20358
+ ${PHASE_REMINDER_TEXT}`;
20359
+ function createPostFileToolNudgeHook() {
19289
20360
  return {
19290
20361
  "tool.execute.after": async (input, output) => {
19291
- if (input.tool !== "Read" && input.tool !== "read") {
20362
+ if (input.tool !== "Read" && input.tool !== "read" && input.tool !== "Write" && input.tool !== "write") {
19292
20363
  return;
19293
20364
  }
19294
20365
  output.output = output.output + NUDGE;
@@ -31656,12 +32727,12 @@ var {spawn: spawn3 } = globalThis.Bun;
31656
32727
  // src/tools/ast-grep/constants.ts
31657
32728
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
31658
32729
  import { createRequire as createRequire2 } from "module";
31659
- import { dirname as dirname2, join as join8 } from "path";
32730
+ import { dirname as dirname3, join as join8 } from "path";
31660
32731
 
31661
32732
  // src/tools/ast-grep/downloader.ts
31662
32733
  import { chmodSync, existsSync as existsSync4, mkdirSync, unlinkSync } from "fs";
31663
32734
  import { createRequire } from "module";
31664
- import { homedir as homedir4 } from "os";
32735
+ import { homedir as homedir3 } from "os";
31665
32736
  import { join as join7 } from "path";
31666
32737
  var REPO = "ast-grep/ast-grep";
31667
32738
  var DEFAULT_VERSION = "0.40.0";
@@ -31686,11 +32757,11 @@ var PLATFORM_MAP = {
31686
32757
  function getCacheDir2() {
31687
32758
  if (process.platform === "win32") {
31688
32759
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
31689
- const base2 = localAppData || join7(homedir4(), "AppData", "Local");
32760
+ const base2 = localAppData || join7(homedir3(), "AppData", "Local");
31690
32761
  return join7(base2, "oh-my-opencode-slim", "bin");
31691
32762
  }
31692
32763
  const xdgCache = process.env.XDG_CACHE_HOME;
31693
- const base = xdgCache || join7(homedir4(), ".cache");
32764
+ const base = xdgCache || join7(homedir3(), ".cache");
31694
32765
  return join7(base, "oh-my-opencode-slim", "bin");
31695
32766
  }
31696
32767
  function getBinaryName() {
@@ -31713,8 +32784,8 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
31713
32784
  if (existsSync4(binaryPath)) {
31714
32785
  return binaryPath;
31715
32786
  }
31716
- const { arch, os: os4 } = platformInfo;
31717
- const assetName = `app-${arch}-${os4}.zip`;
32787
+ const { arch, os: os3 } = platformInfo;
32788
+ const assetName = `app-${arch}-${os3}.zip`;
31718
32789
  const downloadUrl = `https://github.com/${REPO}/releases/download/${version3}/${assetName}`;
31719
32790
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
31720
32791
  try {
@@ -31813,7 +32884,7 @@ function findSgCliPathSync() {
31813
32884
  try {
31814
32885
  const require2 = createRequire2(import.meta.url);
31815
32886
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
31816
- const cliDir = dirname2(cliPkgPath);
32887
+ const cliDir = dirname3(cliPkgPath);
31817
32888
  const sgPath = join8(cliDir, binaryName);
31818
32889
  if (existsSync5(sgPath) && isValidBinary(sgPath)) {
31819
32890
  return sgPath;
@@ -31824,7 +32895,7 @@ function findSgCliPathSync() {
31824
32895
  try {
31825
32896
  const require2 = createRequire2(import.meta.url);
31826
32897
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
31827
- const pkgDir = dirname2(pkgPath);
32898
+ const pkgDir = dirname3(pkgPath);
31828
32899
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
31829
32900
  const binaryPath = join8(pkgDir, astGrepName);
31830
32901
  if (existsSync5(binaryPath) && isValidBinary(binaryPath)) {
@@ -32269,380 +33340,146 @@ Returns: results if completed, error if failed, status if running.`,
32269
33340
  Duration: ${duration5}
32270
33341
 
32271
33342
  ---
32272
-
32273
- `;
32274
- if (task.status === "completed" && task.result != null) {
32275
- output += task.result;
32276
- } else if (task.status === "failed") {
32277
- output += `Error: ${task.error}`;
32278
- } else if (task.status === "cancelled") {
32279
- output += "(Task cancelled)";
32280
- } else {
32281
- output += "(Task still running)";
32282
- }
32283
- return output;
32284
- }
32285
- });
32286
- const background_cancel = tool({
32287
- description: `Cancel background task(s).
32288
-
32289
- task_id: cancel specific task
32290
- all=true: cancel all running tasks
32291
-
32292
- Only cancels pending/starting/running tasks.`,
32293
- args: {
32294
- task_id: z2.string().optional().describe("Specific task to cancel"),
32295
- all: z2.boolean().optional().describe("Cancel all running tasks")
32296
- },
32297
- async execute(args) {
32298
- if (args.all === true) {
32299
- const count = manager.cancel();
32300
- return `Cancelled ${count} task(s).`;
32301
- }
32302
- if (typeof args.task_id === "string") {
32303
- const count = manager.cancel(args.task_id);
32304
- return count > 0 ? `Cancelled task ${args.task_id}.` : `Task ${args.task_id} not found or not running.`;
32305
- }
32306
- return "Specify task_id or use all=true.";
32307
- }
32308
- });
32309
- return { background_task, background_output, background_cancel };
32310
- }
32311
- // src/tools/grep/cli.ts
32312
- var {spawn: spawn4 } = globalThis.Bun;
32313
-
32314
- // src/tools/grep/constants.ts
32315
- import { spawnSync as spawnSync2 } from "child_process";
32316
- import { existsSync as existsSync8 } from "fs";
32317
- import { dirname as dirname3, join as join10 } from "path";
32318
-
32319
- // src/tools/grep/downloader.ts
32320
- import {
32321
- chmodSync as chmodSync2,
32322
- existsSync as existsSync7,
32323
- mkdirSync as mkdirSync2,
32324
- readdirSync,
32325
- unlinkSync as unlinkSync2
32326
- } from "fs";
32327
- import { join as join9 } from "path";
32328
- function getInstallDir() {
32329
- const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
32330
- return join9(homeDir, ".cache", "oh-my-opencode-slim", "bin");
32331
- }
32332
- function getRgPath() {
32333
- const isWindows = process.platform === "win32";
32334
- return join9(getInstallDir(), isWindows ? "rg.exe" : "rg");
32335
- }
32336
- function getInstalledRipgrepPath() {
32337
- const rgPath = getRgPath();
32338
- return existsSync7(rgPath) ? rgPath : null;
32339
- }
32340
-
32341
- // src/tools/grep/constants.ts
32342
- var cachedCli = null;
32343
- function findExecutable(name) {
32344
- const isWindows = process.platform === "win32";
32345
- const cmd = isWindows ? "where" : "which";
32346
- try {
32347
- const result = spawnSync2(cmd, [name], { encoding: "utf-8", timeout: 5000 });
32348
- if (result.status === 0 && result.stdout.trim()) {
32349
- return result.stdout.trim().split(/\r?\n/)[0];
32350
- }
32351
- } catch {}
32352
- return null;
32353
- }
32354
- function getDataDir() {
32355
- if (process.platform === "win32") {
32356
- return process.env.LOCALAPPDATA || process.env.APPDATA || join10(process.env.USERPROFILE || ".", "AppData", "Local");
32357
- }
32358
- return process.env.XDG_DATA_HOME || join10(process.env.HOME || ".", ".local", "share");
32359
- }
32360
- function getOpenCodeBundledRg() {
32361
- const execPath = process.execPath;
32362
- const execDir = dirname3(execPath);
32363
- const isWindows = process.platform === "win32";
32364
- const rgName = isWindows ? "rg.exe" : "rg";
32365
- const candidates = [
32366
- join10(getDataDir(), "opencode", "bin", rgName),
32367
- join10(execDir, rgName),
32368
- join10(execDir, "bin", rgName),
32369
- join10(execDir, "..", "bin", rgName),
32370
- join10(execDir, "..", "libexec", rgName)
32371
- ];
32372
- for (const candidate of candidates) {
32373
- if (existsSync8(candidate)) {
32374
- return candidate;
32375
- }
32376
- }
32377
- return null;
32378
- }
32379
- function resolveGrepCli() {
32380
- if (cachedCli)
32381
- return cachedCli;
32382
- const bundledRg = getOpenCodeBundledRg();
32383
- if (bundledRg) {
32384
- cachedCli = { path: bundledRg, backend: "rg" };
32385
- return cachedCli;
32386
- }
32387
- const systemRg = findExecutable("rg");
32388
- if (systemRg) {
32389
- cachedCli = { path: systemRg, backend: "rg" };
32390
- return cachedCli;
32391
- }
32392
- const installedRg = getInstalledRipgrepPath();
32393
- if (installedRg) {
32394
- cachedCli = { path: installedRg, backend: "rg" };
32395
- return cachedCli;
32396
- }
32397
- const grep = findExecutable("grep");
32398
- if (grep) {
32399
- cachedCli = { path: grep, backend: "grep" };
32400
- return cachedCli;
32401
- }
32402
- cachedCli = { path: "rg", backend: "rg" };
32403
- return cachedCli;
32404
- }
32405
- var DEFAULT_MAX_DEPTH = 20;
32406
- var DEFAULT_MAX_FILESIZE = "10M";
32407
- var DEFAULT_MAX_COUNT = 500;
32408
- var DEFAULT_MAX_COLUMNS = 1000;
32409
- var DEFAULT_TIMEOUT_MS3 = 300000;
32410
- var DEFAULT_MAX_OUTPUT_BYTES2 = 10 * 1024 * 1024;
32411
- var RG_SAFETY_FLAGS = [
32412
- "--no-follow",
32413
- "--color=never",
32414
- "--no-heading",
32415
- "--line-number",
32416
- "--with-filename"
32417
- ];
32418
- var GREP_SAFETY_FLAGS = ["-n", "-H", "--color=never"];
32419
-
32420
- // src/tools/grep/cli.ts
32421
- function buildRgArgs(options) {
32422
- const args = [
32423
- ...RG_SAFETY_FLAGS,
32424
- `--max-depth=${Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)}`,
32425
- `--max-filesize=${options.maxFilesize ?? DEFAULT_MAX_FILESIZE}`,
32426
- `--max-count=${Math.min(options.maxCount ?? DEFAULT_MAX_COUNT, DEFAULT_MAX_COUNT)}`,
32427
- `--max-columns=${Math.min(options.maxColumns ?? DEFAULT_MAX_COLUMNS, DEFAULT_MAX_COLUMNS)}`
32428
- ];
32429
- if (options.context !== undefined && options.context > 0) {
32430
- args.push(`-C${Math.min(options.context, 10)}`);
32431
- }
32432
- if (options.caseSensitive) {
32433
- args.push("--case-sensitive");
32434
- } else {
32435
- args.push("-i");
32436
- }
32437
- if (options.wholeWord)
32438
- args.push("-w");
32439
- if (options.fixedStrings)
32440
- args.push("-F");
32441
- if (options.multiline)
32442
- args.push("-U");
32443
- if (options.hidden)
32444
- args.push("--hidden");
32445
- if (options.noIgnore)
32446
- args.push("--no-ignore");
32447
- if (options.fileType?.length) {
32448
- for (const type of options.fileType) {
32449
- args.push(`--type=${type}`);
32450
- }
32451
- }
32452
- if (options.globs) {
32453
- for (const glob of options.globs) {
32454
- args.push(`--glob=${glob}`);
32455
- }
32456
- }
32457
- if (options.excludeGlobs) {
32458
- for (const glob of options.excludeGlobs) {
32459
- args.push(`--glob=!${glob}`);
32460
- }
32461
- }
32462
- return args;
32463
- }
32464
- function buildGrepArgs(options) {
32465
- const args = [...GREP_SAFETY_FLAGS, "-r"];
32466
- if (options.context !== undefined && options.context > 0) {
32467
- args.push(`-C${Math.min(options.context, 10)}`);
32468
- }
32469
- if (!options.caseSensitive)
32470
- args.push("-i");
32471
- if (options.wholeWord)
32472
- args.push("-w");
32473
- if (options.fixedStrings)
32474
- args.push("-F");
32475
- if (options.globs?.length) {
32476
- for (const glob of options.globs) {
32477
- args.push(`--include=${glob}`);
32478
- }
32479
- }
32480
- if (options.excludeGlobs?.length) {
32481
- for (const glob of options.excludeGlobs) {
32482
- args.push(`--exclude=${glob}`);
32483
- }
32484
- }
32485
- args.push("--exclude-dir=.git", "--exclude-dir=node_modules");
32486
- return args;
32487
- }
32488
- function buildArgs(options, backend) {
32489
- return backend === "rg" ? buildRgArgs(options) : buildGrepArgs(options);
32490
- }
32491
- function parseOutput(output) {
32492
- if (!output.trim())
32493
- return [];
32494
- const matches = [];
32495
- const lines = output.trim().split(/\r?\n/);
32496
- for (const line of lines) {
32497
- if (!line.trim())
32498
- continue;
32499
- const match = line.match(/^(.+?):(\d+):(.*)$/);
32500
- if (match) {
32501
- matches.push({
32502
- file: match[1],
32503
- line: parseInt(match[2], 10),
32504
- text: match[3]
32505
- });
32506
- }
32507
- }
32508
- return matches;
32509
- }
32510
- async function runRg(options) {
32511
- const cli = resolveGrepCli();
32512
- const args = buildArgs(options, cli.backend);
32513
- const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3);
32514
- if (cli.backend === "rg") {
32515
- args.push("--", options.pattern);
32516
- } else {
32517
- args.push("-e", options.pattern);
32518
- }
32519
- const paths2 = options.paths?.length ? options.paths : ["."];
32520
- args.push(...paths2);
32521
- const proc = spawn4([cli.path, ...args], {
32522
- stdout: "pipe",
32523
- stderr: "pipe"
32524
- });
32525
- const timeoutPromise = new Promise((_, reject) => {
32526
- const id = setTimeout(() => {
32527
- proc.kill();
32528
- reject(new Error(`Search timeout after ${timeout}ms`));
32529
- }, timeout);
32530
- proc.exited.then(() => clearTimeout(id));
32531
- });
32532
- try {
32533
- const stdout = await Promise.race([
32534
- new Response(proc.stdout).text(),
32535
- timeoutPromise
32536
- ]);
32537
- const stderr = await new Response(proc.stderr).text();
32538
- const exitCode = await proc.exited;
32539
- const truncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES2;
32540
- const outputToProcess = truncated ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES2) : stdout;
32541
- if (exitCode > 1 && stderr.trim()) {
32542
- return {
32543
- matches: [],
32544
- totalMatches: 0,
32545
- filesSearched: 0,
32546
- truncated: false,
32547
- error: stderr.trim()
32548
- };
32549
- }
32550
- const matches = parseOutput(outputToProcess);
32551
- const filesSearched = new Set(matches.map((m) => m.file)).size;
32552
- return {
32553
- matches,
32554
- totalMatches: matches.length,
32555
- filesSearched,
32556
- truncated
32557
- };
32558
- } catch (e) {
32559
- return {
32560
- matches: [],
32561
- totalMatches: 0,
32562
- filesSearched: 0,
32563
- truncated: false,
32564
- error: e instanceof Error ? e.message : String(e)
32565
- };
32566
- }
32567
- }
32568
- // src/tools/grep/utils.ts
32569
- function formatGrepResult(result) {
32570
- if (result.error) {
32571
- return `Error: ${result.error}`;
32572
- }
32573
- if (result.matches.length === 0) {
32574
- return "No matches found.";
32575
- }
32576
- const lines = [];
32577
- const byFile = new Map;
32578
- for (const match of result.matches) {
32579
- const existing = byFile.get(match.file) || [];
32580
- existing.push({ line: match.line, text: match.text });
32581
- byFile.set(match.file, existing);
32582
- }
32583
- for (const [file3, matches] of byFile) {
32584
- lines.push(`
32585
- ${file3}:`);
32586
- for (const match of matches) {
32587
- lines.push(` ${match.line}: ${match.text}`);
33343
+
33344
+ `;
33345
+ if (task.status === "completed" && task.result != null) {
33346
+ output += task.result;
33347
+ } else if (task.status === "failed") {
33348
+ output += `Error: ${task.error}`;
33349
+ } else if (task.status === "cancelled") {
33350
+ output += "(Task cancelled)";
33351
+ } else {
33352
+ output += "(Task still running)";
33353
+ }
33354
+ return output;
32588
33355
  }
32589
- }
32590
- const summary = `Found ${result.totalMatches} matches in ${result.filesSearched} files`;
32591
- if (result.truncated) {
32592
- lines.push(`
32593
- ${summary} (output truncated)`);
32594
- } else {
32595
- lines.push(`
32596
- ${summary}`);
32597
- }
32598
- return lines.join(`
32599
- `);
33356
+ });
33357
+ const background_cancel = tool({
33358
+ description: `Cancel background task(s).
33359
+
33360
+ task_id: cancel specific task
33361
+ all=true: cancel all running tasks
33362
+
33363
+ Only cancels pending/starting/running tasks.`,
33364
+ args: {
33365
+ task_id: z2.string().optional().describe("Specific task to cancel"),
33366
+ all: z2.boolean().optional().describe("Cancel all running tasks")
33367
+ },
33368
+ async execute(args) {
33369
+ if (args.all === true) {
33370
+ const count = manager.cancel();
33371
+ return `Cancelled ${count} task(s).`;
33372
+ }
33373
+ if (typeof args.task_id === "string") {
33374
+ const count = manager.cancel(args.task_id);
33375
+ return count > 0 ? `Cancelled task ${args.task_id}.` : `Task ${args.task_id} not found or not running.`;
33376
+ }
33377
+ return "Specify task_id or use all=true.";
33378
+ }
33379
+ });
33380
+ return { background_task, background_output, background_cancel };
32600
33381
  }
33382
+ // src/tools/council.ts
33383
+ var z3 = tool.schema;
33384
+ function formatModelComposition(councillorResults) {
33385
+ return councillorResults.map((cr) => {
33386
+ const shortModel = shortModelLabel(cr.model ?? "");
33387
+ return `${cr.name}: ${shortModel}`;
33388
+ }).join(", ");
33389
+ }
33390
+ function createCouncilTool(_ctx, councilManager) {
33391
+ const council_session = tool({
33392
+ description: `Launch a multi-LLM council session for consensus-based analysis.
32601
33393
 
32602
- // src/tools/grep/tools.ts
32603
- var grep = tool({
32604
- description: "Fast content search tool with safety limits (60s timeout, 10MB output). " + "Searches file contents using regular expressions. " + 'Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.). ' + 'Filter files by pattern with the include parameter (e.g. "*.js", "*.{ts,tsx}"). ' + "Returns file paths with matches sorted by modification time.",
32605
- args: {
32606
- pattern: tool.schema.string().describe("The regex pattern to search for in file contents"),
32607
- include: tool.schema.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
32608
- path: tool.schema.string().optional().describe("The directory to search in. Defaults to the current working directory."),
32609
- caseSensitive: tool.schema.boolean().optional().default(false).describe("Perform case-sensitive search (default: false)"),
32610
- wholeWord: tool.schema.boolean().optional().default(false).describe("Match whole words only (default: false)"),
32611
- fixedStrings: tool.schema.boolean().optional().default(false).describe("Treat pattern as literal string (default: false)")
32612
- },
32613
- execute: async (args) => {
32614
- try {
32615
- const globs = args.include ? [args.include] : undefined;
32616
- const paths2 = args.path ? [args.path] : undefined;
32617
- const result = await runRg({
32618
- pattern: args.pattern,
32619
- paths: paths2,
32620
- globs,
32621
- context: 0,
32622
- caseSensitive: args.caseSensitive ?? false,
32623
- wholeWord: args.wholeWord ?? false,
32624
- fixedStrings: args.fixedStrings ?? false
32625
- });
32626
- return formatGrepResult(result);
32627
- } catch (e) {
32628
- return `Error: ${e instanceof Error ? e.message : String(e)}`;
33394
+ Sends the prompt to multiple models (councillors) in parallel, then a council master synthesizes the best response.
33395
+
33396
+ Returns the synthesized result with councillor summary.`,
33397
+ args: {
33398
+ prompt: z3.string().describe("The prompt to send to all councillors"),
33399
+ preset: z3.string().optional().describe('Council preset to use (default: "default"). Must match a preset in the council config.')
33400
+ },
33401
+ async execute(args, toolContext) {
33402
+ if (!toolContext || typeof toolContext !== "object" || !("sessionID" in toolContext)) {
33403
+ throw new Error("Invalid toolContext: missing sessionID");
33404
+ }
33405
+ const allowedAgents = ["council", "orchestrator"];
33406
+ const callingAgent = toolContext.agent;
33407
+ if (callingAgent && !allowedAgents.includes(callingAgent)) {
33408
+ throw new Error(`Council sessions can only be invoked by council or orchestrator agents. Current agent: ${callingAgent}`);
33409
+ }
33410
+ const prompt = String(args.prompt);
33411
+ const preset = typeof args.preset === "string" ? args.preset : undefined;
33412
+ const parentSessionId = toolContext.sessionID;
33413
+ const result = await councilManager.runCouncil(prompt, preset, parentSessionId);
33414
+ if (!result.success) {
33415
+ if (result.result) {
33416
+ const completed2 = result.councillorResults.filter((cr) => cr.status === "completed").length;
33417
+ const total2 = result.councillorResults.length;
33418
+ const composition2 = formatModelComposition(result.councillorResults);
33419
+ return `${result.result}
33420
+
33421
+ ---
33422
+ *Council: ${completed2}/${total2} councillors responded (${composition2}) \u2014 degraded*`;
33423
+ }
33424
+ return `Council session failed: ${result.error}`;
33425
+ }
33426
+ let output = result.result ?? "(No output)";
33427
+ const completed = result.councillorResults.filter((cr) => cr.status === "completed").length;
33428
+ const total = result.councillorResults.length;
33429
+ const composition = formatModelComposition(result.councillorResults);
33430
+ output += `
33431
+
33432
+ ---
33433
+ *Council: ${completed}/${total} councillors responded (${composition})*`;
33434
+ return output;
32629
33435
  }
32630
- }
32631
- });
33436
+ });
33437
+ return { council_session };
33438
+ }
32632
33439
  // src/tools/lsp/client.ts
32633
33440
  var import_node = __toESM(require_main(), 1);
32634
33441
  import { readFileSync as readFileSync4 } from "fs";
32635
- import { extname, resolve } from "path";
33442
+ import { extname, resolve as resolve2 } from "path";
32636
33443
  import { Readable, Writable } from "stream";
32637
33444
  import { pathToFileURL } from "url";
32638
- var {spawn: spawn5 } = globalThis.Bun;
33445
+ var {spawn: spawn4 } = globalThis.Bun;
32639
33446
 
32640
33447
  // src/tools/lsp/config.ts
32641
- import { existsSync as existsSync9 } from "fs";
32642
- import { homedir as homedir5 } from "os";
32643
- import { join as join11 } from "path";
33448
+ var import_which = __toESM(require_lib(), 1);
33449
+ import { existsSync as existsSync8 } from "fs";
33450
+ import { homedir as homedir4 } from "os";
33451
+ import { join as join9 } from "path";
33452
+
33453
+ // src/tools/lsp/config-store.ts
33454
+ var userConfig = new Map;
33455
+ function setUserLspConfig(config3) {
33456
+ userConfig.clear();
33457
+ if (config3) {
33458
+ for (const [id, server] of Object.entries(config3)) {
33459
+ if (server && typeof server === "object") {
33460
+ const s = server;
33461
+ userConfig.set(id, {
33462
+ id,
33463
+ command: s.command,
33464
+ extensions: s.extensions,
33465
+ disabled: s.disabled,
33466
+ env: s.env,
33467
+ initialization: s.initialization
33468
+ });
33469
+ }
33470
+ }
33471
+ }
33472
+ }
33473
+ function getAllUserLspConfigs() {
33474
+ return new Map(userConfig);
33475
+ }
33476
+ function hasUserLspConfig() {
33477
+ return userConfig.size > 0;
33478
+ }
32644
33479
 
32645
33480
  // src/tools/lsp/constants.ts
33481
+ import { existsSync as existsSync7, readdirSync, statSync as statSync3 } from "fs";
33482
+ import { dirname as dirname4, resolve } from "path";
32646
33483
  var SEVERITY_MAP = {
32647
33484
  1: "error",
32648
33485
  2: "warning",
@@ -32651,22 +33488,76 @@ var SEVERITY_MAP = {
32651
33488
  };
32652
33489
  var DEFAULT_MAX_REFERENCES = 200;
32653
33490
  var DEFAULT_MAX_DIAGNOSTICS = 200;
33491
+ var LOCK_FILE_PATTERNS = [
33492
+ "package-lock.json",
33493
+ "bun.lockb",
33494
+ "bun.lock",
33495
+ "pnpm-lock.yaml",
33496
+ "yarn.lock"
33497
+ ];
33498
+ function* walkUpDirectories(start, stop) {
33499
+ let dir = resolve(start);
33500
+ try {
33501
+ if (!statSync3(dir).isDirectory()) {
33502
+ dir = dirname4(dir);
33503
+ }
33504
+ } catch {
33505
+ dir = dirname4(dir);
33506
+ }
33507
+ let prevDir = "";
33508
+ while (dir !== prevDir && dir !== "/") {
33509
+ yield dir;
33510
+ prevDir = dir;
33511
+ if (dir === stop)
33512
+ break;
33513
+ dir = dirname4(dir);
33514
+ }
33515
+ }
33516
+ function NearestRoot(includePatterns, excludePatterns) {
33517
+ return (file3) => {
33518
+ const cwd = process.cwd();
33519
+ if (excludePatterns) {
33520
+ for (const dir of walkUpDirectories(file3, cwd)) {
33521
+ for (const pattern of excludePatterns) {
33522
+ if (existsSync7(`${dir}/${pattern}`)) {
33523
+ return;
33524
+ }
33525
+ }
33526
+ }
33527
+ }
33528
+ for (const dir of walkUpDirectories(file3, cwd)) {
33529
+ for (const pattern of includePatterns) {
33530
+ if (pattern.includes("*")) {
33531
+ try {
33532
+ const entries = readdirSync(dir);
33533
+ const regex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\*/g, ".*")}$`);
33534
+ if (entries.some((entry) => regex.test(entry))) {
33535
+ return dir;
33536
+ }
33537
+ } catch {}
33538
+ } else if (existsSync7(`${dir}/${pattern}`)) {
33539
+ return dir;
33540
+ }
33541
+ }
33542
+ }
33543
+ return;
33544
+ };
33545
+ }
32654
33546
  var BUILTIN_SERVERS = {
33547
+ deno: {
33548
+ command: ["deno", "lsp"],
33549
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
33550
+ root: NearestRoot(["deno.json", "deno.jsonc"])
33551
+ },
32655
33552
  typescript: {
32656
33553
  command: ["typescript-language-server", "--stdio"],
32657
- extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]
33554
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
33555
+ root: NearestRoot(LOCK_FILE_PATTERNS, ["deno.json", "deno.jsonc"])
32658
33556
  },
32659
33557
  vue: {
32660
33558
  command: ["vue-language-server", "--stdio"],
32661
- extensions: [".vue"]
32662
- },
32663
- svelte: {
32664
- command: ["svelteserver", "--stdio"],
32665
- extensions: [".svelte"]
32666
- },
32667
- astro: {
32668
- command: ["astro-ls", "--stdio"],
32669
- extensions: [".astro"]
33559
+ extensions: [".vue"],
33560
+ root: NearestRoot(LOCK_FILE_PATTERNS)
32670
33561
  },
32671
33562
  eslint: {
32672
33563
  command: ["vscode-eslint-language-server", "--stdio"],
@@ -32677,54 +33568,316 @@ var BUILTIN_SERVERS = {
32677
33568
  ".jsx",
32678
33569
  ".mjs",
32679
33570
  ".cjs",
33571
+ ".mts",
33572
+ ".cts",
33573
+ ".vue"
33574
+ ],
33575
+ root: NearestRoot(LOCK_FILE_PATTERNS)
33576
+ },
33577
+ oxlint: {
33578
+ command: ["oxlint", "--lsp"],
33579
+ extensions: [
33580
+ ".ts",
33581
+ ".tsx",
33582
+ ".js",
33583
+ ".jsx",
33584
+ ".mjs",
33585
+ ".cjs",
33586
+ ".mts",
33587
+ ".cts",
32680
33588
  ".vue",
33589
+ ".astro",
32681
33590
  ".svelte"
32682
- ]
33591
+ ],
33592
+ root: NearestRoot([
33593
+ ".oxlintrc.json",
33594
+ ...LOCK_FILE_PATTERNS,
33595
+ "package.json"
33596
+ ])
32683
33597
  },
32684
- tailwindcss: {
32685
- command: ["tailwindcss-language-server", "--stdio"],
32686
- extensions: [".html", ".jsx", ".tsx", ".vue", ".svelte", ".astro"]
33598
+ biome: {
33599
+ command: ["biome", "lsp-proxy", "--stdio"],
33600
+ extensions: [
33601
+ ".ts",
33602
+ ".tsx",
33603
+ ".js",
33604
+ ".jsx",
33605
+ ".mjs",
33606
+ ".cjs",
33607
+ ".mts",
33608
+ ".cts",
33609
+ ".json",
33610
+ ".jsonc",
33611
+ ".vue",
33612
+ ".astro",
33613
+ ".svelte",
33614
+ ".css",
33615
+ ".graphql",
33616
+ ".gql",
33617
+ ".html"
33618
+ ],
33619
+ root: NearestRoot(["biome.json", "biome.jsonc", ...LOCK_FILE_PATTERNS])
32687
33620
  },
32688
33621
  gopls: {
32689
33622
  command: ["gopls"],
32690
- extensions: [".go"]
33623
+ extensions: [".go"],
33624
+ root: NearestRoot(["go.work", "go.mod", "go.sum"])
32691
33625
  },
32692
- rust: {
32693
- command: ["rust-analyzer"],
32694
- extensions: [".rs"]
33626
+ ruby_lsp: {
33627
+ command: ["rubocop", "--lsp"],
33628
+ extensions: [".rb", ".rake", ".gemspec", ".ru"],
33629
+ root: NearestRoot(["Gemfile"])
32695
33630
  },
32696
- basedpyright: {
32697
- command: ["basedpyright-langserver", "--stdio"],
32698
- extensions: [".py", ".pyi"]
33631
+ ty: {
33632
+ command: ["ty", "server"],
33633
+ extensions: [".py", ".pyi"],
33634
+ root: NearestRoot([
33635
+ "pyproject.toml",
33636
+ "ty.toml",
33637
+ "setup.py",
33638
+ "setup.cfg",
33639
+ "requirements.txt",
33640
+ "Pipfile",
33641
+ "pyrightconfig.json"
33642
+ ])
32699
33643
  },
32700
33644
  pyright: {
32701
33645
  command: ["pyright-langserver", "--stdio"],
32702
- extensions: [".py", ".pyi"]
33646
+ extensions: [".py", ".pyi"],
33647
+ root: NearestRoot([
33648
+ "pyproject.toml",
33649
+ "setup.py",
33650
+ "setup.cfg",
33651
+ "requirements.txt",
33652
+ "Pipfile",
33653
+ "pyrightconfig.json"
33654
+ ])
32703
33655
  },
32704
- clangd: {
32705
- command: ["clangd", "--background-index"],
32706
- extensions: [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp"]
33656
+ elixir_ls: {
33657
+ command: ["elixir-ls"],
33658
+ extensions: [".ex", ".exs"],
33659
+ root: NearestRoot(["mix.exs", "mix.lock"])
32707
33660
  },
32708
33661
  zls: {
32709
33662
  command: ["zls"],
32710
- extensions: [".zig"]
33663
+ extensions: [".zig", ".zon"],
33664
+ root: NearestRoot(["build.zig"])
33665
+ },
33666
+ csharp: {
33667
+ command: ["csharp-ls"],
33668
+ extensions: [".cs"],
33669
+ root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"])
33670
+ },
33671
+ fsharp: {
33672
+ command: ["fsautocomplete"],
33673
+ extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
33674
+ root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"])
33675
+ },
33676
+ sourcekit_lsp: {
33677
+ command: ["sourcekit-lsp"],
33678
+ extensions: [".swift", ".objc", ".objcpp"],
33679
+ root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"])
33680
+ },
33681
+ rust: {
33682
+ command: ["rust-analyzer"],
33683
+ extensions: [".rs"],
33684
+ root: NearestRoot(["Cargo.toml", "Cargo.lock"])
33685
+ },
33686
+ clangd: {
33687
+ command: ["clangd", "--background-index", "--clang-tidy"],
33688
+ extensions: [
33689
+ ".c",
33690
+ ".cpp",
33691
+ ".cc",
33692
+ ".cxx",
33693
+ ".c++",
33694
+ ".h",
33695
+ ".hpp",
33696
+ ".hh",
33697
+ ".hxx",
33698
+ ".h++"
33699
+ ],
33700
+ root: NearestRoot([
33701
+ "compile_commands.json",
33702
+ "compile_flags.txt",
33703
+ ".clangd",
33704
+ "CMakeLists.txt",
33705
+ "Makefile"
33706
+ ])
33707
+ },
33708
+ svelte: {
33709
+ command: ["svelteserver", "--stdio"],
33710
+ extensions: [".svelte"],
33711
+ root: NearestRoot(LOCK_FILE_PATTERNS)
33712
+ },
33713
+ astro: {
33714
+ command: ["astro-ls", "--stdio"],
33715
+ extensions: [".astro"],
33716
+ root: NearestRoot(LOCK_FILE_PATTERNS)
33717
+ },
33718
+ jdtls: {
33719
+ command: ["jdtls"],
33720
+ extensions: [".java"],
33721
+ root: NearestRoot([
33722
+ "pom.xml",
33723
+ "build.gradle",
33724
+ "build.gradle.kts",
33725
+ ".project",
33726
+ ".classpath"
33727
+ ])
33728
+ },
33729
+ kotlin_ls: {
33730
+ command: ["kotlin-lsp", "--stdio"],
33731
+ extensions: [".kt", ".kts"],
33732
+ root: NearestRoot([
33733
+ "settings.gradle.kts",
33734
+ "settings.gradle",
33735
+ "gradlew",
33736
+ "build.gradle.kts",
33737
+ "build.gradle",
33738
+ "pom.xml"
33739
+ ])
33740
+ },
33741
+ yaml_ls: {
33742
+ command: ["yaml-language-server", "--stdio"],
33743
+ extensions: [".yaml", ".yml"],
33744
+ root: NearestRoot(LOCK_FILE_PATTERNS)
33745
+ },
33746
+ lua_ls: {
33747
+ command: ["lua-language-server"],
33748
+ extensions: [".lua"],
33749
+ root: NearestRoot([
33750
+ ".luarc.json",
33751
+ ".luarc.jsonc",
33752
+ ".luacheckrc",
33753
+ "stylua.toml",
33754
+ "selene.toml",
33755
+ "selene.yml"
33756
+ ])
33757
+ },
33758
+ php_intelephense: {
33759
+ command: ["intelephense", "--stdio"],
33760
+ extensions: [".php"],
33761
+ root: NearestRoot(["composer.json", "composer.lock", ".php-version"])
33762
+ },
33763
+ prisma: {
33764
+ command: ["prisma", "language-server"],
33765
+ extensions: [".prisma"],
33766
+ root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"])
33767
+ },
33768
+ dart: {
33769
+ command: ["dart", "language-server", "--lsp"],
33770
+ extensions: [".dart"],
33771
+ root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"])
33772
+ },
33773
+ ocaml_lsp: {
33774
+ command: ["ocamllsp"],
33775
+ extensions: [".ml", ".mli"],
33776
+ root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"])
33777
+ },
33778
+ bash: {
33779
+ command: ["bash-language-server", "start"],
33780
+ extensions: [".sh", ".bash", ".zsh", ".ksh"],
33781
+ root: undefined
33782
+ },
33783
+ terraform_ls: {
33784
+ command: ["terraform-ls", "serve"],
33785
+ extensions: [".tf", ".tfvars"],
33786
+ root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"])
33787
+ },
33788
+ texlab: {
33789
+ command: ["texlab"],
33790
+ extensions: [".tex", ".bib"],
33791
+ root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"])
33792
+ },
33793
+ dockerfile: {
33794
+ command: ["docker-langserver", "--stdio"],
33795
+ extensions: [".dockerfile", "Dockerfile"],
33796
+ root: undefined
33797
+ },
33798
+ gleam: {
33799
+ command: ["gleam", "lsp"],
33800
+ extensions: [".gleam"],
33801
+ root: NearestRoot(["gleam.toml"])
33802
+ },
33803
+ clojure_lsp: {
33804
+ command: ["clojure-lsp", "listen"],
33805
+ extensions: [".clj", ".cljs", ".cljc", ".edn"],
33806
+ root: NearestRoot([
33807
+ "deps.edn",
33808
+ "project.clj",
33809
+ "shadow-cljs.edn",
33810
+ "bb.edn",
33811
+ "build.boot"
33812
+ ])
33813
+ },
33814
+ nixd: {
33815
+ command: ["nixd"],
33816
+ extensions: [".nix"],
33817
+ root: NearestRoot(["flake.nix"])
33818
+ },
33819
+ tinymist: {
33820
+ command: ["tinymist"],
33821
+ extensions: [".typ", ".typc"],
33822
+ root: NearestRoot(["typst.toml"])
33823
+ },
33824
+ haskell_language_server: {
33825
+ command: ["haskell-language-server-wrapper", "--lsp"],
33826
+ extensions: [".hs", ".lhs"],
33827
+ root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"])
33828
+ },
33829
+ julials: {
33830
+ command: [
33831
+ "julia",
33832
+ "--startup-file=no",
33833
+ "--history-file=no",
33834
+ "-e",
33835
+ "using LanguageServer; runserver()"
33836
+ ],
33837
+ extensions: [".jl"],
33838
+ root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"])
32711
33839
  }
32712
33840
  };
32713
33841
  var LSP_INSTALL_HINTS = {
33842
+ deno: "Install Deno: https://deno.land/#installation",
32714
33843
  typescript: "npm install -g typescript-language-server typescript",
32715
33844
  vue: "npm install -g @vue/language-server",
32716
- svelte: "npm install -g svelte-language-server",
32717
- astro: "npm install -g @astrojs/language-server",
32718
33845
  eslint: "npm install -g vscode-langservers-extracted",
32719
- tailwindcss: "npm install -g @tailwindcss/language-server",
33846
+ oxlint: "npm install -g oxlint or install via package manager",
33847
+ biome: "npm install -g @biomejs/biome",
32720
33848
  gopls: "go install golang.org/x/tools/gopls@latest",
32721
- rust: "rustup component add rust-analyzer",
32722
- basedpyright: "pip install basedpyright",
33849
+ ruby_lsp: "gem install rubocop (Ruby LSP runs via rubocop --lsp)",
33850
+ ty: "pip install ty or see https://github.com/astral-sh/ty",
32723
33851
  pyright: "pip install pyright",
32724
- clangd: "See https://clangd.llvm.org/installation",
32725
- zls: "See https://github.com/zigtools/zls"
33852
+ elixir_ls: "Download from https://github.com/elixir-lsp/elixir-ls/releases or build from source",
33853
+ zls: "Install via your package manager or build from source: https://github.com/zigtools/zls",
33854
+ csharp: "dotnet tool install --global csharp-ls",
33855
+ fsharp: "dotnet tool install --global fsautocomplete",
33856
+ sourcekit_lsp: "Install via Xcode or Swift toolchain (included with Xcode)",
33857
+ rust: "rustup component add rust-analyzer",
33858
+ clangd: "Install clangd via your system package manager or LLVM",
33859
+ svelte: "npm install -g svelte-language-server",
33860
+ astro: "npm install -g @astrojs/language-server",
33861
+ jdtls: "See https://github.com/eclipse-jdtls/eclipse.jdt.ls for installation",
33862
+ kotlin_ls: "Download from https://github.com/Kotlin/kotlin-lsp/releases",
33863
+ yaml_ls: "npm install -g yaml-language-server",
33864
+ lua_ls: "Download from https://github.com/LuaLS/lua-language-server/releases",
33865
+ php_intelephense: "npm install -g intelephense",
33866
+ prisma: "npm install -g @prisma/language-server or use npx",
33867
+ dart: "dart pub global activate language_server",
33868
+ ocaml_lsp: "opam install ocaml-lsp-server",
33869
+ bash: "npm install -g bash-language-server",
33870
+ terraform_ls: "Download from https://github.com/hashicorp/terraform-ls/releases or install via tfenv",
33871
+ texlab: "Download from https://github.com/latex-lsp/texlab/releases",
33872
+ dockerfile: "npm install -g dockerfile-language-server-nodejs",
33873
+ gleam: "Install Gleam: https://gleam.run/getting-started/",
33874
+ clojure_lsp: "Install via deps.edn, project.clj, or: clj -M -m clojure-lsp.main",
33875
+ nixd: "Install via nix-env or your system package manager",
33876
+ tinymist: "cargo install tinymist or download from releases",
33877
+ haskell_language_server: "Install Haskell Tool Stack or Cabal, then language-server",
33878
+ julials: "Install Julia: https://julialang.org/downloads/"
32726
33879
  };
32727
- var EXT_TO_LANG = {
33880
+ var LANGUAGE_EXTENSIONS = {
32728
33881
  ".ts": "typescript",
32729
33882
  ".tsx": "typescriptreact",
32730
33883
  ".mts": "typescript",
@@ -32733,35 +33886,180 @@ var EXT_TO_LANG = {
32733
33886
  ".jsx": "javascriptreact",
32734
33887
  ".mjs": "javascript",
32735
33888
  ".cjs": "javascript",
33889
+ ".ets": "typescript",
32736
33890
  ".vue": "vue",
32737
33891
  ".svelte": "svelte",
32738
33892
  ".astro": "astro",
32739
33893
  ".html": "html",
33894
+ ".htm": "html",
33895
+ ".xml": "xml",
33896
+ ".xsl": "xsl",
32740
33897
  ".css": "css",
32741
33898
  ".scss": "scss",
33899
+ ".sass": "sass",
32742
33900
  ".less": "less",
32743
33901
  ".json": "json",
33902
+ ".jsonc": "json",
33903
+ ".graphql": "graphql",
33904
+ ".gql": "graphql",
33905
+ ".dockerfile": "dockerfile",
33906
+ ".sh": "shellscript",
33907
+ ".bash": "shellscript",
33908
+ ".zsh": "shellscript",
33909
+ ".ksh": "shellscript",
32744
33910
  ".go": "go",
32745
33911
  ".rs": "rust",
32746
33912
  ".py": "python",
32747
33913
  ".pyi": "python",
33914
+ ".rb": "ruby",
33915
+ ".rake": "ruby",
33916
+ ".gemspec": "ruby",
33917
+ ".ru": "ruby",
32748
33918
  ".c": "c",
32749
33919
  ".cpp": "cpp",
32750
33920
  ".cc": "cpp",
32751
33921
  ".cxx": "cpp",
33922
+ ".c++": "cpp",
32752
33923
  ".h": "c",
32753
33924
  ".hpp": "cpp",
32754
- ".zig": "zig"
33925
+ ".hh": "cpp",
33926
+ ".hxx": "cpp",
33927
+ ".h++": "cpp",
33928
+ ".java": "java",
33929
+ ".kt": "kotlin",
33930
+ ".kts": "kotlin",
33931
+ ".cs": "csharp",
33932
+ ".fs": "fsharp",
33933
+ ".fsi": "fsharp",
33934
+ ".fsx": "fsharp",
33935
+ ".fsscript": "fsharp",
33936
+ ".swift": "swift",
33937
+ ".m": "objective-c",
33938
+ ".mm": "objective-cpp",
33939
+ ".zig": "zig",
33940
+ ".zon": "zig",
33941
+ ".ex": "elixir",
33942
+ ".exs": "elixir",
33943
+ ".clj": "clojure",
33944
+ ".cljs": "clojure",
33945
+ ".cljc": "clojure",
33946
+ ".edn": "clojure",
33947
+ ".hs": "haskell",
33948
+ ".lhs": "haskell",
33949
+ ".ml": "ocaml",
33950
+ ".mli": "ocaml",
33951
+ ".scala": "scala",
33952
+ ".php": "php",
33953
+ ".lua": "lua",
33954
+ ".dart": "dart",
33955
+ ".yaml": "yaml",
33956
+ ".yml": "yaml",
33957
+ ".tf": "terraform",
33958
+ ".tfvars": "terraform-vars",
33959
+ ".hcl": "hcl",
33960
+ ".nix": "nix",
33961
+ ".typ": "typst",
33962
+ ".typc": "typst",
33963
+ ".tex": "latex",
33964
+ ".latex": "latex",
33965
+ ".bib": "bibtex",
33966
+ ".bibtex": "bibtex",
33967
+ ".prisma": "prisma",
33968
+ ".jl": "julia",
33969
+ ".gleam": "gleam",
33970
+ ".md": "markdown",
33971
+ ".markdown": "markdown",
33972
+ ".d": "d",
33973
+ ".pas": "pascal",
33974
+ ".pascal": "pascal",
33975
+ ".diff": "diff",
33976
+ ".patch": "diff",
33977
+ ".erl": "erlang",
33978
+ ".hrl": "erlang",
33979
+ ".groovy": "groovy",
33980
+ ".handlebars": "handlebars",
33981
+ ".hbs": "handlebars",
33982
+ ".ini": "ini",
33983
+ ".makefile": "makefile",
33984
+ makefile: "makefile",
33985
+ ".pug": "jade",
33986
+ ".jade": "jade",
33987
+ ".r": "r",
33988
+ ".cshtml": "razor",
33989
+ ".razor": "razor",
33990
+ ".erb": "erb",
33991
+ ".html.erb": "erb",
33992
+ ".js.erb": "erb",
33993
+ ".css.erb": "erb",
33994
+ ".json.erb": "erb",
33995
+ ".shader": "shaderlab",
33996
+ ".sql": "sql",
33997
+ ".perl": "perl",
33998
+ ".pl": "perl",
33999
+ ".pm": "perl",
34000
+ ".pm6": "perl6",
34001
+ ".ps1": "powershell",
34002
+ ".psm1": "powershell",
34003
+ ".coffee": "coffeescript",
34004
+ ".bat": "bat",
34005
+ ".abap": "abap",
34006
+ ".gitcommit": "git-commit",
34007
+ ".gitrebase": "git-rebase"
32755
34008
  };
32756
34009
 
32757
34010
  // src/tools/lsp/config.ts
32758
- function findServerForExtension(ext) {
34011
+ function buildMergedServers() {
34012
+ const servers = new Map;
32759
34013
  for (const [id, config3] of Object.entries(BUILTIN_SERVERS)) {
34014
+ servers.set(id, {
34015
+ id,
34016
+ command: config3.command,
34017
+ extensions: config3.extensions,
34018
+ root: config3.root,
34019
+ env: config3.env,
34020
+ initialization: config3.initialization
34021
+ });
34022
+ }
34023
+ if (hasUserLspConfig()) {
34024
+ for (const [id, userConfig2] of getAllUserLspConfigs()) {
34025
+ if (userConfig2.disabled === true) {
34026
+ servers.delete(id);
34027
+ continue;
34028
+ }
34029
+ const existing = servers.get(id);
34030
+ if (existing) {
34031
+ servers.set(id, {
34032
+ ...existing,
34033
+ id,
34034
+ command: userConfig2.command ?? existing.command,
34035
+ extensions: userConfig2.extensions ?? existing.extensions,
34036
+ root: existing.root,
34037
+ env: userConfig2.env ?? existing.env,
34038
+ initialization: userConfig2.initialization ?? existing.initialization
34039
+ });
34040
+ } else {
34041
+ servers.set(id, {
34042
+ id,
34043
+ command: userConfig2.command ?? [],
34044
+ extensions: userConfig2.extensions ?? [],
34045
+ root: undefined,
34046
+ env: userConfig2.env,
34047
+ initialization: userConfig2.initialization
34048
+ });
34049
+ }
34050
+ }
34051
+ }
34052
+ return servers;
34053
+ }
34054
+ function findServerForExtension(ext) {
34055
+ const servers = buildMergedServers();
34056
+ for (const [, config3] of servers) {
32760
34057
  if (config3.extensions.includes(ext)) {
32761
34058
  const server = {
32762
- id,
34059
+ id: config3.id,
32763
34060
  command: config3.command,
32764
34061
  extensions: config3.extensions,
34062
+ root: config3.root,
32765
34063
  env: config3.env,
32766
34064
  initialization: config3.initialization
32767
34065
  };
@@ -32771,39 +34069,37 @@ function findServerForExtension(ext) {
32771
34069
  return {
32772
34070
  status: "not_installed",
32773
34071
  server,
32774
- installHint: LSP_INSTALL_HINTS[id] || `Install '${config3.command[0]}' and add to PATH`
34072
+ installHint: LSP_INSTALL_HINTS[config3.id] || `Install '${config3.command[0]}' and add to PATH`
32775
34073
  };
32776
34074
  }
32777
34075
  }
32778
34076
  return { status: "not_configured", extension: ext };
32779
34077
  }
32780
34078
  function getLanguageId(ext) {
32781
- return EXT_TO_LANG[ext] || "plaintext";
34079
+ return LANGUAGE_EXTENSIONS[ext] || "plaintext";
32782
34080
  }
32783
34081
  function isServerInstalled(command) {
32784
34082
  if (command.length === 0)
32785
34083
  return false;
32786
34084
  const cmd = command[0];
32787
34085
  if (cmd.includes("/") || cmd.includes("\\")) {
32788
- return existsSync9(cmd);
34086
+ return existsSync8(cmd);
32789
34087
  }
32790
34088
  const isWindows = process.platform === "win32";
32791
34089
  const ext = isWindows ? ".exe" : "";
32792
- const pathEnv = process.env.PATH || "";
32793
- const pathSeparator = isWindows ? ";" : ":";
32794
- const paths2 = pathEnv.split(pathSeparator);
32795
- for (const p of paths2) {
32796
- if (existsSync9(join11(p, cmd)) || existsSync9(join11(p, cmd + ext))) {
32797
- return true;
32798
- }
32799
- }
32800
- const cwd = process.cwd();
32801
- const localBin = join11(cwd, "node_modules", ".bin", cmd);
32802
- if (existsSync9(localBin) || existsSync9(localBin + ext)) {
34090
+ const opencodeBin = join9(homedir4(), ".config", "opencode", "bin");
34091
+ const searchPath = (process.env.PATH ?? "") + (isWindows ? ";" : ":") + opencodeBin;
34092
+ const result = import_which.default.sync(cmd, {
34093
+ path: searchPath,
34094
+ pathExt: isWindows ? process.env.PATHEXT : undefined,
34095
+ nothrow: true
34096
+ });
34097
+ if (result !== null) {
32803
34098
  return true;
32804
34099
  }
32805
- const globalBin = join11(homedir5(), ".config", "opencode", "bin", cmd);
32806
- if (existsSync9(globalBin) || existsSync9(globalBin + ext)) {
34100
+ const cwd = process.cwd();
34101
+ const localBin = join9(cwd, "node_modules", ".bin", cmd);
34102
+ if (existsSync8(localBin) || existsSync8(localBin + ext)) {
32807
34103
  return true;
32808
34104
  }
32809
34105
  return false;
@@ -32816,6 +34112,7 @@ class LSPServerManager {
32816
34112
  cleanupInterval = null;
32817
34113
  IDLE_TIMEOUT = 5 * 60 * 1000;
32818
34114
  constructor() {
34115
+ log("[lsp] manager initialized");
32819
34116
  this.startCleanupTimer();
32820
34117
  this.registerProcessCleanup();
32821
34118
  }
@@ -32872,16 +34169,31 @@ class LSPServerManager {
32872
34169
  const managed = this.clients.get(key);
32873
34170
  if (managed) {
32874
34171
  if (managed.initPromise) {
34172
+ log("[lsp] getClient: waiting for init", { key, server: server.id });
32875
34173
  await managed.initPromise;
32876
34174
  }
32877
34175
  if (managed.client.isAlive()) {
32878
34176
  managed.refCount++;
32879
34177
  managed.lastUsedAt = Date.now();
34178
+ log("[lsp] getClient: reuse pooled client", {
34179
+ key,
34180
+ server: server.id,
34181
+ refCount: managed.refCount
34182
+ });
32880
34183
  return managed.client;
32881
34184
  }
34185
+ log("[lsp] getClient: client dead, recreating", {
34186
+ key,
34187
+ server: server.id
34188
+ });
32882
34189
  await managed.client.stop();
32883
34190
  this.clients.delete(key);
32884
34191
  }
34192
+ log("[lsp] getClient: creating new client", {
34193
+ key,
34194
+ server: server.id,
34195
+ root
34196
+ });
32885
34197
  const client = new LSPClient(root, server);
32886
34198
  const initPromise2 = (async () => {
32887
34199
  await client.start();
@@ -32901,7 +34213,13 @@ class LSPServerManager {
32901
34213
  m.initPromise = undefined;
32902
34214
  m.isInitializing = false;
32903
34215
  }
34216
+ log("[lsp] getClient: client ready", { key, server: server.id });
32904
34217
  } catch (err) {
34218
+ log("[lsp] getClient: init failed", {
34219
+ key,
34220
+ server: server.id,
34221
+ error: String(err)
34222
+ });
32905
34223
  this.clients.delete(key);
32906
34224
  throw err;
32907
34225
  }
@@ -32913,6 +34231,11 @@ class LSPServerManager {
32913
34231
  if (managed && managed.refCount > 0) {
32914
34232
  managed.refCount--;
32915
34233
  managed.lastUsedAt = Date.now();
34234
+ log("[lsp] releaseClient", {
34235
+ key,
34236
+ server: serverId,
34237
+ refCount: managed.refCount
34238
+ });
32916
34239
  }
32917
34240
  }
32918
34241
  isServerInitializing(root, serverId) {
@@ -32921,14 +34244,19 @@ class LSPServerManager {
32921
34244
  return managed?.isInitializing ?? false;
32922
34245
  }
32923
34246
  async stopAll() {
32924
- for (const [, managed] of this.clients) {
34247
+ log("[lsp] stopAll: shutting down all clients", {
34248
+ count: this.clients.size
34249
+ });
34250
+ for (const [key, managed] of this.clients) {
32925
34251
  await managed.client.stop();
34252
+ log("[lsp] stopAll: client stopped", { key });
32926
34253
  }
32927
34254
  this.clients.clear();
32928
34255
  if (this.cleanupInterval) {
32929
34256
  clearInterval(this.cleanupInterval);
32930
34257
  this.cleanupInterval = null;
32931
34258
  }
34259
+ log("[lsp] stopAll: complete");
32932
34260
  }
32933
34261
  }
32934
34262
  var lspManager = LSPServerManager.getInstance();
@@ -32947,7 +34275,12 @@ class LSPClient {
32947
34275
  this.server = server;
32948
34276
  }
32949
34277
  async start() {
32950
- this.proc = spawn5(this.server.command, {
34278
+ log("[lsp] LSPClient.start: spawning server", {
34279
+ server: this.server.id,
34280
+ command: this.server.command.join(" "),
34281
+ root: this.root
34282
+ });
34283
+ this.proc = spawn4(this.server.command, {
32951
34284
  stdin: "pipe",
32952
34285
  stdout: "pipe",
32953
34286
  stderr: "pipe",
@@ -33016,13 +34349,19 @@ class LSPClient {
33016
34349
  this.processExited = true;
33017
34350
  });
33018
34351
  this.connection.listen();
33019
- await new Promise((resolve2) => setTimeout(resolve2, 100));
34352
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
33020
34353
  if (this.proc.exitCode !== null) {
33021
34354
  const stderr = this.stderrBuffer.join(`
33022
34355
  `);
34356
+ log("[lsp] LSPClient.start: server exited immediately", {
34357
+ server: this.server.id,
34358
+ exitCode: this.proc.exitCode,
34359
+ stderr: stderr.slice(0, 500)
34360
+ });
33023
34361
  throw new Error(`LSP server exited immediately with code ${this.proc.exitCode}` + (stderr ? `
33024
34362
  stderr: ${stderr}` : ""));
33025
34363
  }
34364
+ log("[lsp] LSPClient.start: server spawned", { server: this.server.id });
33026
34365
  }
33027
34366
  startStderrReading() {
33028
34367
  if (!this.proc)
@@ -33048,6 +34387,10 @@ stderr: ${stderr}` : ""));
33048
34387
  async initialize() {
33049
34388
  if (!this.connection)
33050
34389
  throw new Error("LSP connection not established");
34390
+ log("[lsp] LSPClient.initialize: sending initialize request", {
34391
+ server: this.server.id,
34392
+ root: this.root
34393
+ });
33051
34394
  const rootUri = pathToFileURL(this.root).href;
33052
34395
  await this.connection.sendRequest("initialize", {
33053
34396
  processId: process.pid,
@@ -33079,14 +34422,22 @@ stderr: ${stderr}` : ""));
33079
34422
  });
33080
34423
  this.connection.sendNotification("initialized");
33081
34424
  await new Promise((r) => setTimeout(r, 300));
34425
+ log("[lsp] LSPClient.initialize: complete", { server: this.server.id });
33082
34426
  }
33083
34427
  async openFile(filePath) {
33084
- const absPath = resolve(filePath);
33085
- if (this.openedFiles.has(absPath))
34428
+ const absPath = resolve2(filePath);
34429
+ if (this.openedFiles.has(absPath)) {
34430
+ log("[lsp] openFile: already open, skipping", { filePath: absPath });
33086
34431
  return;
34432
+ }
33087
34433
  const text = readFileSync4(absPath, "utf-8");
33088
34434
  const ext = extname(absPath);
33089
34435
  const languageId = getLanguageId(ext);
34436
+ log("[lsp] openFile: opening document", {
34437
+ filePath: absPath,
34438
+ languageId,
34439
+ size: text.length
34440
+ });
33090
34441
  this.connection?.sendNotification("textDocument/didOpen", {
33091
34442
  textDocument: {
33092
34443
  uri: pathToFileURL(absPath).href,
@@ -33099,7 +34450,7 @@ stderr: ${stderr}` : ""));
33099
34450
  await new Promise((r) => setTimeout(r, 1000));
33100
34451
  }
33101
34452
  async definition(filePath, line, character) {
33102
- const absPath = resolve(filePath);
34453
+ const absPath = resolve2(filePath);
33103
34454
  await this.openFile(absPath);
33104
34455
  return this.connection?.sendRequest("textDocument/definition", {
33105
34456
  textDocument: { uri: pathToFileURL(absPath).href },
@@ -33107,7 +34458,7 @@ stderr: ${stderr}` : ""));
33107
34458
  });
33108
34459
  }
33109
34460
  async references(filePath, line, character, includeDeclaration = true) {
33110
- const absPath = resolve(filePath);
34461
+ const absPath = resolve2(filePath);
33111
34462
  await this.openFile(absPath);
33112
34463
  return this.connection?.sendRequest("textDocument/references", {
33113
34464
  textDocument: { uri: pathToFileURL(absPath).href },
@@ -33116,7 +34467,7 @@ stderr: ${stderr}` : ""));
33116
34467
  });
33117
34468
  }
33118
34469
  async diagnostics(filePath) {
33119
- const absPath = resolve(filePath);
34470
+ const absPath = resolve2(filePath);
33120
34471
  const uri = pathToFileURL(absPath).href;
33121
34472
  await this.openFile(absPath);
33122
34473
  await new Promise((r) => setTimeout(r, 500));
@@ -33131,7 +34482,7 @@ stderr: ${stderr}` : ""));
33131
34482
  return { items: this.diagnosticsStore.get(uri) ?? [] };
33132
34483
  }
33133
34484
  async rename(filePath, line, character, newName) {
33134
- const absPath = resolve(filePath);
34485
+ const absPath = resolve2(filePath);
33135
34486
  await this.openFile(absPath);
33136
34487
  return this.connection?.sendRequest("textDocument/rename", {
33137
34488
  textDocument: { uri: pathToFileURL(absPath).href },
@@ -33143,6 +34494,7 @@ stderr: ${stderr}` : ""));
33143
34494
  return this.proc !== null && !this.processExited && this.proc.exitCode === null;
33144
34495
  }
33145
34496
  async stop() {
34497
+ log("[lsp] LSPClient.stop: stopping", { server: this.server.id });
33146
34498
  try {
33147
34499
  if (this.connection) {
33148
34500
  await this.connection.sendRequest("shutdown");
@@ -33155,45 +34507,24 @@ stderr: ${stderr}` : ""));
33155
34507
  this.connection = null;
33156
34508
  this.processExited = true;
33157
34509
  this.diagnosticsStore.clear();
34510
+ log("[lsp] LSPClient.stop: complete", { server: this.server.id });
33158
34511
  }
33159
34512
  }
33160
34513
  // src/tools/lsp/utils.ts
33161
34514
  import {
33162
- existsSync as existsSync10,
34515
+ existsSync as existsSync9,
33163
34516
  readFileSync as readFileSync5,
33164
- statSync as statSync3,
33165
- unlinkSync as unlinkSync3,
34517
+ statSync as statSync4,
34518
+ unlinkSync as unlinkSync2,
33166
34519
  writeFileSync as writeFileSync3
33167
34520
  } from "fs";
33168
- import { dirname as dirname4, extname as extname2, join as join12, resolve as resolve2 } from "path";
34521
+ import { dirname as dirname5, extname as extname2, join as join10, resolve as resolve3 } from "path";
33169
34522
  import { fileURLToPath as fileURLToPath2 } from "url";
33170
- function findWorkspaceRoot(filePath) {
33171
- let dir = resolve2(filePath);
33172
- try {
33173
- if (!statSync3(dir).isDirectory()) {
33174
- dir = dirname4(dir);
33175
- }
33176
- } catch {
33177
- dir = dirname4(dir);
33178
- }
33179
- const markers = [
33180
- ".git",
33181
- "package.json",
33182
- "pyproject.toml",
33183
- "Cargo.toml",
33184
- "go.mod"
33185
- ];
33186
- let prevDir = "";
33187
- while (dir !== prevDir) {
33188
- for (const marker of markers) {
33189
- if (existsSync10(join12(dir, marker))) {
33190
- return dir;
33191
- }
33192
- }
33193
- prevDir = dir;
33194
- dir = dirname4(dir);
34523
+ function findServerProjectRoot(filePath, server) {
34524
+ if (server.root) {
34525
+ return server.root(filePath) ?? dirname5(resolve3(filePath));
33195
34526
  }
33196
- return dirname4(resolve2(filePath));
34527
+ return dirname5(resolve3(filePath));
33197
34528
  }
33198
34529
  function uriToPath(uri) {
33199
34530
  return fileURLToPath2(uri);
@@ -33212,24 +34543,42 @@ function formatServerLookupError(result) {
33212
34543
  return `No LSP server configured for extension: ${result.extension}`;
33213
34544
  }
33214
34545
  async function withLspClient(filePath, fn) {
33215
- const absPath = resolve2(filePath);
34546
+ const absPath = resolve3(filePath);
33216
34547
  const ext = extname2(absPath);
33217
34548
  const result = findServerForExtension(ext);
33218
34549
  if (result.status !== "found") {
34550
+ log("[lsp] withLspClient: server not found", {
34551
+ filePath: absPath,
34552
+ extension: ext
34553
+ });
33219
34554
  throw new Error(formatServerLookupError(result));
33220
34555
  }
33221
34556
  const server = result.server;
33222
- const root = findWorkspaceRoot(absPath);
34557
+ const root = findServerProjectRoot(absPath, server) ?? dirname5(absPath);
34558
+ log("[lsp] withLspClient: acquiring client", {
34559
+ filePath: absPath,
34560
+ server: server.id,
34561
+ root
34562
+ });
33223
34563
  const client = await lspManager.getClient(root, server);
33224
34564
  try {
33225
- return await fn(client);
34565
+ const result2 = await fn(client);
34566
+ log("[lsp] withLspClient: operation complete", { server: server.id });
34567
+ return result2;
33226
34568
  } catch (e) {
33227
34569
  if (e instanceof Error && e.message.includes("timeout")) {
33228
34570
  const isInitializing = lspManager.isServerInitializing(root, server.id);
33229
34571
  if (isInitializing) {
34572
+ log("[lsp] withLspClient: timeout during init", {
34573
+ server: server.id
34574
+ });
33230
34575
  throw new Error(`LSP server is still initializing. Please retry in a few seconds.`);
33231
34576
  }
33232
34577
  }
34578
+ log("[lsp] withLspClient: operation failed", {
34579
+ server: server.id,
34580
+ error: String(e)
34581
+ });
33233
34582
  throw e;
33234
34583
  } finally {
33235
34584
  lspManager.releaseClient(root, server.id);
@@ -33313,6 +34662,7 @@ function applyTextEditsToFile(filePath, edits) {
33313
34662
  }
33314
34663
  function applyWorkspaceEdit(edit) {
33315
34664
  if (!edit) {
34665
+ log("[lsp] applyWorkspaceEdit: no edit provided");
33316
34666
  return {
33317
34667
  success: false,
33318
34668
  filesModified: [],
@@ -33320,6 +34670,8 @@ function applyWorkspaceEdit(edit) {
33320
34670
  errors: ["No edit provided"]
33321
34671
  };
33322
34672
  }
34673
+ const changeCount = (edit.changes ? Object.keys(edit.changes).length : 0) + (edit.documentChanges ? edit.documentChanges.length : 0);
34674
+ log("[lsp] applyWorkspaceEdit: applying", { changeCount });
33323
34675
  const result = {
33324
34676
  success: true,
33325
34677
  filesModified: [],
@@ -33357,7 +34709,7 @@ function applyWorkspaceEdit(edit) {
33357
34709
  const newPath = uriToPath(change.newUri);
33358
34710
  const content = readFileSync5(oldPath, "utf-8");
33359
34711
  writeFileSync3(newPath, content, "utf-8");
33360
- unlinkSync3(oldPath);
34712
+ unlinkSync2(oldPath);
33361
34713
  result.filesModified.push(newPath);
33362
34714
  } catch (err) {
33363
34715
  result.success = false;
@@ -33366,7 +34718,7 @@ function applyWorkspaceEdit(edit) {
33366
34718
  } else if (change.kind === "delete") {
33367
34719
  try {
33368
34720
  const filePath = uriToPath(change.uri);
33369
- unlinkSync3(filePath);
34721
+ unlinkSync2(filePath);
33370
34722
  result.filesModified.push(filePath);
33371
34723
  } catch (err) {
33372
34724
  result.success = false;
@@ -33386,6 +34738,12 @@ function applyWorkspaceEdit(edit) {
33386
34738
  }
33387
34739
  }
33388
34740
  }
34741
+ log("[lsp] applyWorkspaceEdit: complete", {
34742
+ success: result.success,
34743
+ filesModified: result.filesModified.length,
34744
+ totalEdits: result.totalEdits,
34745
+ errors: result.errors.length
34746
+ });
33389
34747
  return result;
33390
34748
  }
33391
34749
  function formatApplyResult(result) {
@@ -33534,6 +34892,28 @@ var OhMyOpenCodeLite = async (ctx) => {
33534
34892
  modelArrayMap[agentDef.name] = agentDef._modelArray;
33535
34893
  }
33536
34894
  }
34895
+ const runtimeChains = {};
34896
+ for (const agentDef of agentDefs) {
34897
+ if (agentDef._modelArray?.length) {
34898
+ runtimeChains[agentDef.name] = agentDef._modelArray.map((m) => m.id);
34899
+ }
34900
+ }
34901
+ if (config3.fallback?.enabled !== false) {
34902
+ const chains = config3.fallback?.chains ?? {};
34903
+ for (const [agentName, chainModels] of Object.entries(chains)) {
34904
+ if (!chainModels?.length)
34905
+ continue;
34906
+ const existing = runtimeChains[agentName] ?? [];
34907
+ const seen = new Set(existing);
34908
+ for (const m of chainModels) {
34909
+ if (!seen.has(m)) {
34910
+ seen.add(m);
34911
+ existing.push(m);
34912
+ }
34913
+ }
34914
+ runtimeChains[agentName] = existing;
34915
+ }
34916
+ }
33537
34917
  const tmuxConfig = {
33538
34918
  enabled: config3.tmux?.enabled ?? false,
33539
34919
  layout: config3.tmux?.layout ?? "main-vertical",
@@ -33549,6 +34929,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33549
34929
  }
33550
34930
  const backgroundManager = new BackgroundTaskManager(ctx, tmuxConfig, config3);
33551
34931
  const backgroundTools = createBackgroundTools(ctx, backgroundManager, tmuxConfig, config3);
34932
+ const councilTools = config3.council ? createCouncilTool(ctx, new CouncilManager(ctx, config3, backgroundManager.getDepthTracker(), tmuxConfig.enabled)) : {};
33552
34933
  const mcps = createBuiltinMcps(config3.disabled_mcps);
33553
34934
  const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
33554
34935
  const autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
@@ -33556,26 +34937,31 @@ var OhMyOpenCodeLite = async (ctx) => {
33556
34937
  autoUpdate: true
33557
34938
  });
33558
34939
  const phaseReminderHook = createPhaseReminderHook();
33559
- const postReadNudgeHook = createPostReadNudgeHook();
34940
+ const postFileToolNudgeHook = createPostFileToolNudgeHook();
33560
34941
  const chatHeadersHook = createChatHeadersHook(ctx);
33561
34942
  const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
33562
34943
  const jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
34944
+ const foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config3.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
33563
34945
  return {
33564
34946
  name: "oh-my-opencode-slim",
33565
34947
  agent: agents,
33566
34948
  tool: {
33567
34949
  ...backgroundTools,
34950
+ ...councilTools,
33568
34951
  lsp_goto_definition,
33569
34952
  lsp_find_references,
33570
34953
  lsp_diagnostics,
33571
34954
  lsp_rename,
33572
- grep,
33573
34955
  ast_grep_search,
33574
34956
  ast_grep_replace
33575
34957
  },
33576
34958
  mcp: mcps,
33577
34959
  config: async (opencodeConfig) => {
33578
- opencodeConfig.default_agent = "orchestrator";
34960
+ const lspConfig = opencodeConfig.lsp;
34961
+ setUserLspConfig(lspConfig);
34962
+ if (config3.setDefaultAgent !== false && !opencodeConfig.default_agent) {
34963
+ opencodeConfig.default_agent = "orchestrator";
34964
+ }
33579
34965
  if (!opencodeConfig.agent) {
33580
34966
  opencodeConfig.agent = { ...agents };
33581
34967
  } else {
@@ -33594,38 +34980,45 @@ var OhMyOpenCodeLite = async (ctx) => {
33594
34980
  }
33595
34981
  }
33596
34982
  const configAgent = opencodeConfig.agent;
33597
- if (Object.keys(modelArrayMap).length > 0) {
33598
- const providerConfig = opencodeConfig.provider ?? {};
33599
- const configuredProviders = Object.keys(providerConfig);
33600
- for (const [agentName, modelArray] of Object.entries(modelArrayMap)) {
33601
- let resolved = false;
33602
- for (const modelEntry of modelArray) {
33603
- const slashIdx = modelEntry.id.indexOf("/");
33604
- if (slashIdx === -1)
33605
- continue;
33606
- const providerID = modelEntry.id.slice(0, slashIdx);
33607
- if (configuredProviders.includes(providerID)) {
33608
- const entry = configAgent[agentName];
33609
- if (entry) {
33610
- entry.model = modelEntry.id;
33611
- if (modelEntry.variant) {
33612
- entry.variant = modelEntry.variant;
33613
- }
33614
- }
33615
- log("[plugin] resolved model fallback", {
33616
- agent: agentName,
33617
- model: modelEntry.id,
33618
- variant: modelEntry.variant
33619
- });
33620
- resolved = true;
33621
- break;
33622
- }
34983
+ const fallbackChainsEnabled = config3.fallback?.enabled !== false;
34984
+ const fallbackChains = fallbackChainsEnabled ? config3.fallback?.chains ?? {} : {};
34985
+ const effectiveArrays = {};
34986
+ for (const [agentName, models] of Object.entries(modelArrayMap)) {
34987
+ effectiveArrays[agentName] = [...models];
34988
+ }
34989
+ for (const [agentName, chainModels] of Object.entries(fallbackChains)) {
34990
+ if (!chainModels || chainModels.length === 0)
34991
+ continue;
34992
+ if (!effectiveArrays[agentName]) {
34993
+ const entry = configAgent[agentName];
34994
+ const currentModel = typeof entry?.model === "string" ? entry.model : undefined;
34995
+ effectiveArrays[agentName] = currentModel ? [{ id: currentModel }] : [];
34996
+ }
34997
+ const seen = new Set(effectiveArrays[agentName].map((m) => m.id));
34998
+ for (const chainModel of chainModels) {
34999
+ if (!seen.has(chainModel)) {
35000
+ seen.add(chainModel);
35001
+ effectiveArrays[agentName].push({ id: chainModel });
33623
35002
  }
33624
- if (!resolved) {
33625
- log("[plugin] no provider match for model array", {
33626
- agent: agentName
33627
- });
35003
+ }
35004
+ }
35005
+ if (Object.keys(effectiveArrays).length > 0) {
35006
+ for (const [agentName, modelArray] of Object.entries(effectiveArrays)) {
35007
+ if (modelArray.length === 0)
35008
+ continue;
35009
+ const chosen = modelArray[0];
35010
+ const entry = configAgent[agentName];
35011
+ if (entry) {
35012
+ entry.model = chosen.id;
35013
+ if (chosen.variant) {
35014
+ entry.variant = chosen.variant;
35015
+ }
33628
35016
  }
35017
+ log("[plugin] resolved model from array", {
35018
+ agent: agentName,
35019
+ model: chosen.id,
35020
+ variant: chosen.variant
35021
+ });
33629
35022
  }
33630
35023
  }
33631
35024
  const configMcp = opencodeConfig.mcp;
@@ -33657,6 +35050,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33657
35050
  }
33658
35051
  },
33659
35052
  event: async (input) => {
35053
+ await foregroundFallback.handleEvent(input.event);
33660
35054
  await autoUpdateChecker.event(input);
33661
35055
  await tmuxSessionManager.onSessionCreated(input.event);
33662
35056
  await backgroundManager.handleSessionStatus(input.event);
@@ -33669,7 +35063,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33669
35063
  "tool.execute.after": async (input, output) => {
33670
35064
  await delegateTaskRetryHook["tool.execute.after"](input, output);
33671
35065
  await jsonErrorRecoveryHook["tool.execute.after"](input, output);
33672
- await postReadNudgeHook["tool.execute.after"](input, output);
35066
+ await postFileToolNudgeHook["tool.execute.after"](input, output);
33673
35067
  }
33674
35068
  };
33675
35069
  };