oh-my-opencode-slim 2.0.5 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.ja-JP.md +64 -38
  2. package/README.ko-KR.md +62 -36
  3. package/README.md +82 -47
  4. package/README.zh-CN.md +63 -39
  5. package/dist/agents/council.d.ts +1 -1
  6. package/dist/agents/index.d.ts +7 -2
  7. package/dist/agents/orchestrator.d.ts +1 -1
  8. package/dist/cli/custom-skills-registry.d.ts +18 -0
  9. package/dist/cli/custom-skills.d.ts +3 -19
  10. package/dist/cli/index.js +1130 -195
  11. package/dist/cli/providers.d.ts +12 -16
  12. package/dist/cli/skills.d.ts +2 -2
  13. package/dist/companion/manager.d.ts +10 -0
  14. package/dist/config/constants.d.ts +2 -1
  15. package/dist/config/council-schema.d.ts +37 -12
  16. package/dist/config/loader.d.ts +5 -2
  17. package/dist/config/schema.d.ts +17 -6
  18. package/dist/council/council-manager.d.ts +7 -3
  19. package/dist/hooks/auto-update-checker/skill-sync.d.ts +59 -1
  20. package/dist/hooks/filter-available-skills/index.d.ts +1 -2
  21. package/dist/hooks/foreground-fallback/index.d.ts +40 -2
  22. package/dist/hooks/image-hook.d.ts +1 -1
  23. package/dist/hooks/index.d.ts +2 -0
  24. package/dist/hooks/loop-command/index.d.ts +13 -0
  25. package/dist/hooks/phase-reminder/index.d.ts +4 -3
  26. package/dist/hooks/post-file-tool-nudge/index.d.ts +15 -9
  27. package/dist/hooks/session-lifecycle.d.ts +11 -0
  28. package/dist/hooks/task-session-manager/index.d.ts +15 -4
  29. package/dist/hooks/task-session-manager/pending-call-tracker.d.ts +13 -0
  30. package/dist/hooks/task-session-manager/task-context-tracker.d.ts +14 -0
  31. package/dist/hooks/types.d.ts +3 -1
  32. package/dist/index.js +2890 -986
  33. package/dist/interview/dashboard-manager.d.ts +21 -0
  34. package/dist/interview/manager.d.ts +0 -14
  35. package/dist/interview/service.d.ts +9 -0
  36. package/dist/interview/session-server.d.ts +21 -0
  37. package/dist/interview/types.d.ts +3 -1
  38. package/dist/loop/loop-session.d.ts +64 -0
  39. package/dist/multiplexer/factory.d.ts +5 -5
  40. package/dist/multiplexer/herdr/index.d.ts +33 -0
  41. package/dist/multiplexer/index.d.ts +1 -0
  42. package/dist/multiplexer/session-manager.d.ts +6 -5
  43. package/dist/multiplexer/shared.d.ts +24 -0
  44. package/dist/multiplexer/tmux/index.d.ts +0 -1
  45. package/dist/multiplexer/types.d.ts +4 -4
  46. package/dist/multiplexer/zellij/index.d.ts +0 -1
  47. package/dist/tools/cancel-task.d.ts +2 -2
  48. package/dist/tui.d.ts +1 -0
  49. package/dist/tui.js +123 -45
  50. package/dist/utils/background-job-board.d.ts +22 -2
  51. package/dist/utils/background-job-coordinator.d.ts +72 -0
  52. package/dist/utils/background-job-store.d.ts +46 -0
  53. package/dist/utils/councillor-models.d.ts +20 -0
  54. package/dist/utils/index.d.ts +2 -0
  55. package/dist/utils/internal-initiator.d.ts +6 -1
  56. package/dist/utils/session.d.ts +2 -2
  57. package/oh-my-opencode-slim.schema.json +18 -1
  58. package/package.json +1 -1
  59. package/src/skills/clonedeps/SKILL.md +2 -2
  60. package/src/skills/clonedeps/codemap.md +23 -32
  61. package/src/skills/codemap/SKILL.md +2 -2
  62. package/src/skills/codemap.md +63 -36
  63. package/src/skills/loop-engineering/SKILL.md +30 -0
  64. package/src/skills/oh-my-opencode-slim/SKILL.md +3 -3
  65. package/src/skills/reflect/SKILL.md +133 -0
  66. package/src/skills/release-smoke-test/SKILL.md +159 -0
  67. package/src/skills/simplify/SKILL.md +6 -6
package/dist/tui.js CHANGED
@@ -131,7 +131,12 @@ var POLL_INTERVAL_BACKGROUND_MS = 2000;
131
131
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
132
132
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
133
133
  var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
134
- var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
134
+ function formatSystemReminder(text) {
135
+ return `<system-reminder>
136
+ ${text}
137
+ </system-reminder>`;
138
+ }
139
+ var PHASE_REMINDER = formatSystemReminder(PHASE_REMINDER_TEXT);
135
140
  var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
136
141
  - Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.
137
142
  - Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
@@ -188,11 +193,35 @@ function getOpenCodeConfigPaths() {
188
193
  }
189
194
  // src/config/council-schema.ts
190
195
  import { z } from "zod";
191
- var ModelIdSchema = z.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.4-mini")');
196
+
197
+ // src/utils/councillor-models.ts
198
+ function normalizeCouncillorModels(model, fallbackVariant) {
199
+ const raw = Array.isArray(model) ? model : [model];
200
+ return raw.map((entry) => typeof entry === "string" ? { id: entry, variant: fallbackVariant } : { id: entry.id, variant: entry.variant ?? fallbackVariant });
201
+ }
202
+
203
+ // src/config/council-schema.ts
204
+ var ModelIdSchema = z.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.6-luna")');
205
+ var CouncillorModelEntrySchema = z.object({
206
+ id: ModelIdSchema,
207
+ variant: z.string().optional()
208
+ });
209
+ var CouncillorModelSchema = z.union([
210
+ ModelIdSchema,
211
+ z.array(z.union([ModelIdSchema, CouncillorModelEntrySchema])).min(1)
212
+ ]).describe('Model ID in provider/model format (e.g. "openai/gpt-5.6-luna"), or an ' + "ordered fallback chain (array of model IDs or { id, variant } entries) " + "tried in order until one responds.");
192
213
  var CouncillorConfigSchema = z.object({
193
- model: ModelIdSchema.describe('Model ID in provider/model format (e.g. "openai/gpt-5.4-mini")'),
214
+ model: CouncillorModelSchema,
194
215
  variant: z.string().optional(),
195
216
  prompt: z.string().optional().describe("Optional role/guidance injected into the councillor user prompt")
217
+ }).transform((c) => {
218
+ const models = normalizeCouncillorModels(c.model, c.variant);
219
+ return {
220
+ model: models[0].id,
221
+ variant: c.variant,
222
+ prompt: c.prompt,
223
+ models
224
+ };
196
225
  });
197
226
  var CouncilPresetSchema = z.record(z.string(), z.record(z.string(), z.unknown())).transform((entries, ctx) => {
198
227
  const councillors = {};
@@ -232,7 +261,7 @@ var CouncilConfigSchema = z.object({
232
261
  default_preset: z.string().default("default"),
233
262
  councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
234
263
  councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
235
- master: z.unknown().optional().describe("DEPRECATED ignored. Council agent synthesizes directly.")
264
+ master: z.unknown().optional().describe("DEPRECATED - ignored. Council agent synthesizes directly.")
236
265
  }).transform((data) => {
237
266
  const deprecated = [];
238
267
  if (data.master !== undefined)
@@ -298,7 +327,13 @@ var AgentOverrideConfigSchema = z2.object({
298
327
  options: z2.record(z2.string(), z2.unknown()).optional(),
299
328
  displayName: z2.string().min(1).optional()
300
329
  }).strict();
301
- var MultiplexerTypeSchema = z2.enum(["auto", "tmux", "zellij", "none"]);
330
+ var MultiplexerTypeSchema = z2.enum([
331
+ "auto",
332
+ "tmux",
333
+ "zellij",
334
+ "herdr",
335
+ "none"
336
+ ]);
302
337
  var MultiplexerLayoutSchema = z2.enum([
303
338
  "main-horizontal",
304
339
  "main-vertical",
@@ -340,7 +375,9 @@ var FailoverConfigSchema = z2.object({
340
375
  enabled: z2.boolean().default(true),
341
376
  timeoutMs: z2.number().min(0).default(15000),
342
377
  retryDelayMs: z2.number().min(0).default(500),
343
- retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes.")
378
+ maxRetries: z2.number().int().min(0).default(3).describe("Number of consecutive 429/rate-limit responses tolerated on the " + "same model before aborting (or swapping to the next fallback " + "model when a chain is configured)."),
379
+ retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes."),
380
+ runtimeOverride: z2.boolean().default(true).describe("When true (default), a runtime model selected via /model that is " + "outside the configured fallback chain will still trigger the chain " + "on rate-limit errors. When false, out-of-chain runtime picks are " + "respected and the error surfaces instead of silently falling back " + "to the chain. Models that are members of the chain always fall back " + "regardless of this setting.")
344
381
  }).strict();
345
382
  var CompanionConfigSchema = z2.object({
346
383
  enabled: z2.boolean().optional(),
@@ -367,24 +404,13 @@ var AcpAgentConfigSchema = z2.object({
367
404
  permissionMode: AcpAgentPermissionModeSchema.default("ask")
368
405
  }).strict();
369
406
  var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
370
- function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
407
+ function rejectOrchestratorPromptOnOrchestrator(overrides, ctx, pathPrefix) {
371
408
  for (const [name, override] of Object.entries(overrides)) {
372
- const isBuiltInOrAlias = ALL_AGENT_NAMES.includes(name) || AGENT_ALIASES[name] !== undefined;
373
- if (!isBuiltInOrAlias) {
374
- continue;
375
- }
376
- if (override.prompt !== undefined) {
377
- ctx.addIssue({
378
- code: z2.ZodIssueCode.custom,
379
- path: [...pathPrefix, name, "prompt"],
380
- message: "prompt is only supported for custom agents"
381
- });
382
- }
383
- if (override.orchestratorPrompt !== undefined) {
409
+ if (name === "orchestrator" && override.orchestratorPrompt !== undefined) {
384
410
  ctx.addIssue({
385
411
  code: z2.ZodIssueCode.custom,
386
412
  path: [...pathPrefix, name, "orchestratorPrompt"],
387
- message: "orchestratorPrompt is only supported for custom agents"
413
+ message: "orchestratorPrompt is not supported for the orchestrator agent"
388
414
  });
389
415
  }
390
416
  }
@@ -392,6 +418,7 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
392
418
  var PluginConfigSchema = z2.object({
393
419
  preset: z2.string().optional(),
394
420
  setDefaultAgent: z2.boolean().optional(),
421
+ compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout."),
395
422
  autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
396
423
  presets: z2.record(z2.string(), PresetSchema).optional(),
397
424
  agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
@@ -410,11 +437,14 @@ var PluginConfigSchema = z2.object({
410
437
  acpAgents: AcpAgentsConfigSchema.optional()
411
438
  }).superRefine((value, ctx) => {
412
439
  if (value.agents) {
413
- validateCustomOnlyPromptFields(value.agents, ctx, ["agents"]);
440
+ rejectOrchestratorPromptOnOrchestrator(value.agents, ctx, ["agents"]);
414
441
  }
415
442
  if (value.presets) {
416
443
  for (const [presetName, preset] of Object.entries(value.presets)) {
417
- validateCustomOnlyPromptFields(preset, ctx, ["presets", presetName]);
444
+ rejectOrchestratorPromptOnOrchestrator(preset, ctx, [
445
+ "presets",
446
+ presetName
447
+ ]);
418
448
  }
419
449
  }
420
450
  });
@@ -470,7 +500,7 @@ function getAgentMcpList(agentName, config) {
470
500
  return defaultMcps ?? [];
471
501
  }
472
502
 
473
- // src/cli/custom-skills.ts
503
+ // src/cli/custom-skills-registry.ts
474
504
  var CUSTOM_SKILLS = [
475
505
  {
476
506
  name: "simplify",
@@ -508,6 +538,12 @@ var CUSTOM_SKILLS = [
508
538
  allowedAgents: ["orchestrator"],
509
539
  sourcePath: "src/skills/oh-my-opencode-slim"
510
540
  },
541
+ {
542
+ name: "release-smoke-test",
543
+ description: "Validate packed release candidates and bugfixes before public publish",
544
+ allowedAgents: ["orchestrator"],
545
+ sourcePath: "src/skills/release-smoke-test"
546
+ },
511
547
  {
512
548
  name: "worktrees",
513
549
  description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
@@ -605,6 +641,7 @@ function mergePluginConfigs(base, override) {
605
641
  ...base,
606
642
  ...override,
607
643
  agents: deepMerge(base.agents, override.agents),
644
+ presets: deepMerge(base.presets, override.presets),
608
645
  tmux: deepMerge(base.tmux, override.tmux),
609
646
  multiplexer: deepMerge(base.multiplexer, override.multiplexer),
610
647
  interview: deepMerge(base.interview, override.interview),
@@ -676,15 +713,33 @@ function loadPluginConfig(directory, options) {
676
713
  }
677
714
  return config;
678
715
  }
679
- function loadAgentPrompt(agentName, preset) {
716
+ function loadAgentPrompt(agentName, optionsOrPreset) {
717
+ let preset;
718
+ let projectDirectory;
719
+ if (typeof optionsOrPreset === "string") {
720
+ preset = optionsOrPreset;
721
+ } else if (optionsOrPreset && typeof optionsOrPreset === "object") {
722
+ preset = optionsOrPreset.preset;
723
+ projectDirectory = optionsOrPreset.projectDirectory;
724
+ }
680
725
  const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
681
- const promptSearchDirs = getConfigSearchDirs().flatMap((configDir) => {
682
- const promptsDir = path.join(configDir, PROMPTS_DIR_NAME);
683
- return presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
684
- });
685
- const result = {};
726
+ const searchDirs = [];
727
+ if (projectDirectory && presetDirName) {
728
+ searchDirs.push(path.join(projectDirectory, ".opencode", PROMPTS_DIR_NAME, presetDirName));
729
+ }
730
+ if (projectDirectory) {
731
+ searchDirs.push(path.join(projectDirectory, ".opencode", PROMPTS_DIR_NAME));
732
+ }
733
+ if (presetDirName) {
734
+ for (const userDir of getConfigSearchDirs()) {
735
+ searchDirs.push(path.join(userDir, PROMPTS_DIR_NAME, presetDirName));
736
+ }
737
+ }
738
+ for (const userDir of getConfigSearchDirs()) {
739
+ searchDirs.push(path.join(userDir, PROMPTS_DIR_NAME));
740
+ }
686
741
  const readFirstPrompt = (fileName, errorPrefix) => {
687
- for (const dir of promptSearchDirs) {
742
+ for (const dir of searchDirs) {
688
743
  const promptPath = path.join(dir, fileName);
689
744
  if (!fs.existsSync(promptPath)) {
690
745
  continue;
@@ -697,6 +752,7 @@ function loadAgentPrompt(agentName, preset) {
697
752
  }
698
753
  return;
699
754
  };
755
+ const result = {};
700
756
  result.prompt = readFirstPrompt(`${agentName}.md`, "Error reading prompt file");
701
757
  result.appendPrompt = readFirstPrompt(`${agentName}_append.md`, "Error reading append prompt file");
702
758
  return result;
@@ -859,25 +915,36 @@ function getSidebarAgentNames(snapshot) {
859
915
  function agentRow(label, model, variant, theme) {
860
916
  const modelParts = splitSidebarModelId(model);
861
917
  const detailRows = [];
918
+ function detailRow(fieldLabel, value) {
919
+ return box({ width: "100%", flexDirection: "row", paddingLeft: 2 }, [
920
+ text({ fg: theme.textMuted, width: 9 }, [fieldLabel]),
921
+ text({ fg: theme.textMuted }, [value])
922
+ ]);
923
+ }
862
924
  if (modelParts.provider) {
863
- detailRows.push(agentDetailRow("provider", modelParts.provider, theme));
925
+ detailRows.push(detailRow("provider", modelParts.provider));
864
926
  }
865
- detailRows.push(agentDetailRow("model", modelParts.model, theme));
927
+ detailRows.push(detailRow("model", modelParts.model));
866
928
  if (variant) {
867
- detailRows.push(agentDetailRow("variant", variant, theme));
929
+ detailRows.push(detailRow("variant", variant));
868
930
  }
869
931
  return box({ width: "100%", flexDirection: "column", marginBottom: 1 }, [
870
932
  text({ fg: theme.textMuted }, [label]),
871
933
  ...detailRows
872
934
  ]);
873
935
  }
874
- function agentDetailRow(label, value, theme) {
875
- return box({ width: "100%", flexDirection: "row", paddingLeft: 2 }, [
876
- text({ fg: theme.textMuted, width: 9 }, [label]),
877
- text({ fg: theme.textMuted }, [value])
936
+ function compactAgentRow(label, model, _variant, theme) {
937
+ const modelName = splitSidebarModelId(model).model;
938
+ return box({
939
+ width: "100%",
940
+ flexDirection: "row",
941
+ justifyContent: "space-between"
942
+ }, [
943
+ text({ fg: theme.textMuted, width: 14 }, [label]),
944
+ text({ fg: theme.textMuted }, [modelName])
878
945
  ]);
879
946
  }
880
- function renderSidebar(snapshot, version, theme, configInvalid) {
947
+ function renderSidebar(snapshot, version, theme, configInvalid, compactSidebar) {
881
948
  const configStatusRow = buildConfigStatusRow(configInvalid, theme);
882
949
  return box({
883
950
  width: "100%",
@@ -895,7 +962,7 @@ function renderSidebar(snapshot, version, theme, configInvalid) {
895
962
  justifyContent: "space-between",
896
963
  alignItems: "center"
897
964
  }, [
898
- box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.background }, ["OMO-Slim"])]),
965
+ box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.text }, ["OMO-Slim"])]),
899
966
  text({ fg: theme.textMuted }, [`v${version}`])
900
967
  ]),
901
968
  configStatusRow,
@@ -905,6 +972,9 @@ function renderSidebar(snapshot, version, theme, configInvalid) {
905
972
  ...getSidebarAgentNames(snapshot).map((agentName) => {
906
973
  const model = snapshot.agentModels[agentName] ?? "pending";
907
974
  const variant = snapshot.agentVariants[agentName];
975
+ if (compactSidebar) {
976
+ return compactAgentRow(agentName, model, variant, theme);
977
+ }
908
978
  return agentRow(agentName, model, variant, theme);
909
979
  })
910
980
  ]);
@@ -922,15 +992,22 @@ function buildConfigStatusRow(configInvalid, theme) {
922
992
  text({ fg: theme.textMuted }, ["Run doctor for details"])
923
993
  ]);
924
994
  }
925
- function readConfigInvalid(directory) {
995
+ function readConfigState(directory) {
926
996
  let configInvalid = false;
927
- loadPluginConfig(directory, {
997
+ const config = loadPluginConfig(directory, {
928
998
  silent: true,
929
999
  onWarning: () => {
930
1000
  configInvalid = true;
931
1001
  }
932
1002
  });
933
- return configInvalid;
1003
+ const compactSidebar = config.compactSidebar ?? true;
1004
+ return { configInvalid, compactSidebar };
1005
+ }
1006
+ function readConfigInvalid(directory) {
1007
+ return readConfigState(directory).configInvalid;
1008
+ }
1009
+ function readCompactSidebar(directory) {
1010
+ return readConfigState(directory).compactSidebar;
934
1011
  }
935
1012
  var plugin = {
936
1013
  id: `${PLUGIN_NAME}:tui`,
@@ -939,7 +1016,7 @@ var plugin = {
939
1016
  return;
940
1017
  const version = meta.version ?? await readPackageVersion() ?? "dev";
941
1018
  let configDirectory = getTuiDirectory(api);
942
- let configInvalid = readConfigInvalid(configDirectory);
1019
+ let { configInvalid, compactSidebar } = readConfigState(configDirectory);
943
1020
  let snapshot = readTuiSnapshot();
944
1021
  const renderTimer = setInterval(async () => {
945
1022
  try {
@@ -947,7 +1024,7 @@ var plugin = {
947
1024
  const currentDirectory = getTuiDirectory(api);
948
1025
  if (currentDirectory !== configDirectory) {
949
1026
  configDirectory = currentDirectory;
950
- configInvalid = readConfigInvalid(configDirectory);
1027
+ ({ configInvalid, compactSidebar } = readConfigState(configDirectory));
951
1028
  }
952
1029
  api.renderer.requestRender();
953
1030
  } catch {}
@@ -959,7 +1036,7 @@ var plugin = {
959
1036
  order: 900,
960
1037
  slots: {
961
1038
  sidebar_content() {
962
- return renderSidebar(snapshot, version, api.theme.current, configInvalid);
1039
+ return renderSidebar(snapshot, version, api.theme.current, configInvalid, compactSidebar);
963
1040
  }
964
1041
  }
965
1042
  });
@@ -969,6 +1046,7 @@ var tui_default = plugin;
969
1046
  export {
970
1047
  splitSidebarModelId,
971
1048
  readConfigInvalid,
1049
+ readCompactSidebar,
972
1050
  getSidebarAgentNames,
973
1051
  tui_default as default
974
1052
  };
@@ -1,3 +1,4 @@
1
+ import type { BackgroundJobStore } from './background-job-store';
1
2
  import { type TaskOutputState } from './task';
2
3
  export interface ContextFile {
3
4
  path: string;
@@ -14,6 +15,7 @@ export interface BackgroundJobRecord {
14
15
  objective?: string;
15
16
  state: BackgroundJobState;
16
17
  timedOut: boolean;
18
+ recoverableAfterLiveBusy: boolean;
17
19
  statusUncertain: boolean;
18
20
  cancellationRequested: boolean;
19
21
  terminalUnreconciled: boolean;
@@ -28,6 +30,9 @@ export interface BackgroundJobRecord {
28
30
  lastUsedAt: number;
29
31
  terminalState?: TaskOutputState;
30
32
  contextFiles: ContextFile[];
33
+ totalErrors?: number;
34
+ timeoutCount?: number;
35
+ lastErrorAt?: number;
31
36
  }
32
37
  export interface BackgroundJobBoardOptions {
33
38
  maxReusablePerAgent?: number;
@@ -52,15 +57,18 @@ export interface BackgroundJobStatusInput {
52
57
  now?: number;
53
58
  }
54
59
  type TerminalStateListener = (taskID: string) => void;
55
- export declare class BackgroundJobBoard {
60
+ export declare class BackgroundJobBoard implements BackgroundJobStore {
56
61
  private readonly jobs;
57
62
  private readonly counters;
58
- private terminalStateListener?;
63
+ private terminalStateListeners;
59
64
  private readonly maxReusablePerAgent;
60
65
  private readonly readContextMinLines;
61
66
  private readonly readContextMaxFiles;
62
67
  constructor(options?: BackgroundJobBoardOptions);
68
+ addTerminalStateListener(listener: TerminalStateListener): void;
69
+ removeTerminalStateListener(listener: TerminalStateListener): void;
63
70
  setTerminalStateListener(listener?: TerminalStateListener): void;
71
+ private notifyTerminalStateListeners;
64
72
  registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
65
73
  updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
66
74
  updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
@@ -70,17 +78,29 @@ export declare class BackgroundJobBoard {
70
78
  force?: boolean;
71
79
  }): BackgroundJobRecord | undefined;
72
80
  get(taskID: string): BackgroundJobRecord | undefined;
81
+ field<K extends keyof BackgroundJobRecord>(taskID: string, key: K): BackgroundJobRecord[K] | undefined;
82
+ isRunning(taskID: string): boolean;
83
+ isTerminalUnreconciled(taskID: string): boolean;
84
+ getResultSummary(taskID: string): string | undefined;
85
+ getLastLiveBusyAt(taskID: string): number | undefined;
86
+ getParentSessionID(taskID: string): string | undefined;
87
+ getState(taskID: string): BackgroundJobState | undefined;
73
88
  resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
74
89
  resolveReusable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
90
+ resolveRecoverable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
75
91
  markUsed(parentSessionID: string, key: string, now?: number): void;
76
92
  taskIDs(): Set<string>;
77
93
  addContext(taskID: string, files: ContextFile[]): void;
78
94
  list(parentSessionID?: string): BackgroundJobRecord[];
79
95
  hasRunning(parentSessionID: string): boolean;
80
96
  hasTerminalUnreconciled(parentSessionID: string): boolean;
97
+ hasConvergenceSignals(taskID: string, threshold?: number): boolean;
81
98
  formatForPrompt(parentSessionID: string, now?: number): string | undefined;
82
99
  clearParent(parentSessionID: string): void;
83
100
  drop(taskID: string): void;
101
+ deferIfRunning(_sessionId: string): boolean;
102
+ retryDeferredClose(_sessionId: string): boolean;
103
+ clearDeferredClose(_sessionId: string): void;
84
104
  private trimReusable;
85
105
  private formatReusableJob;
86
106
  private nextAlias;
@@ -0,0 +1,72 @@
1
+ import type { BackgroundJobBoard, BackgroundJobLaunchInput, BackgroundJobRecord, BackgroundJobStatusInput, ContextFile } from './background-job-board';
2
+ import type { BackgroundJobStore } from './background-job-store';
3
+ import type { TaskOutputState } from './task';
4
+ type TerminalStateListener = (taskID: string) => void;
5
+ /**
6
+ * BackgroundJobCoordinator owns the lifecycle policy for background jobs.
7
+ * It sits between the board and its consumers, providing:
8
+ * - Subscription interface for terminal state notifications (replaces fire-and-forget)
9
+ * - Lifecycle policy: determines when jobs are terminal, when closes should be deferred
10
+ * - Single-writer contract: coordinator is the sole writer to the board
11
+ *
12
+ * The board's guards prevent silent overwrites. The coordinator adds:
13
+ * - Centralized notification with guaranteed delivery
14
+ * - Re-checks board state before notifying (handles races)
15
+ */
16
+ export declare class BackgroundJobCoordinator implements BackgroundJobStore {
17
+ private readonly board;
18
+ private terminalStateListeners;
19
+ private readonly deferredIdleCloses;
20
+ constructor(board: BackgroundJobBoard);
21
+ addTerminalStateListener(listener: TerminalStateListener): void;
22
+ removeTerminalStateListener(listener: TerminalStateListener): void;
23
+ /**
24
+ * Handle terminal state from board. Re-checks board state to handle races.
25
+ * This is the centralized lifecycle policy.
26
+ */
27
+ private handleTerminalState;
28
+ /**
29
+ * Evaluate close policy. Returns true if session should close now.
30
+ * Mutates deferred state: adds to deferred set if running, removes if not.
31
+ */
32
+ deferIfRunning(sessionId: string): boolean;
33
+ /**
34
+ * Retry closing a deferred session. Called when a background job completes.
35
+ * Returns true if the session should now close.
36
+ */
37
+ retryDeferredClose(sessionId: string): boolean;
38
+ /**
39
+ * Clear deferred close state for a session being deleted.
40
+ */
41
+ clearDeferredClose(sessionId: string): void;
42
+ registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
43
+ updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
44
+ updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
45
+ markRunningFromLiveSession(taskID: string, now?: number): BackgroundJobRecord | undefined;
46
+ markReconciled(taskID: string, now?: number): BackgroundJobRecord | undefined;
47
+ markCancelled(taskID: string, reason?: string, now?: number, options?: {
48
+ force?: boolean;
49
+ }): BackgroundJobRecord | undefined;
50
+ get(taskID: string): BackgroundJobRecord | undefined;
51
+ field<K extends keyof BackgroundJobRecord>(taskID: string, key: K): BackgroundJobRecord[K] | undefined;
52
+ isRunning(taskID: string): boolean;
53
+ isTerminalUnreconciled(taskID: string): boolean;
54
+ getResultSummary(taskID: string): string | undefined;
55
+ getLastLiveBusyAt(taskID: string): number | undefined;
56
+ getParentSessionID(taskID: string): string | undefined;
57
+ getState(taskID: string): TaskOutputState | 'reconciled' | undefined;
58
+ resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
59
+ resolveReusable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
60
+ resolveRecoverable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
61
+ markUsed(parentSessionID: string, key: string, now?: number): void;
62
+ taskIDs(): Set<string>;
63
+ addContext(taskID: string, files: ContextFile[]): void;
64
+ list(parentSessionID?: string): BackgroundJobRecord[];
65
+ hasRunning(parentSessionID: string): boolean;
66
+ hasTerminalUnreconciled(parentSessionID: string): boolean;
67
+ hasConvergenceSignals(taskID: string, threshold?: number): boolean;
68
+ formatForPrompt(parentSessionID: string, now?: number): string | undefined;
69
+ clearParent(parentSessionID: string): void;
70
+ drop(taskID: string): void;
71
+ }
72
+ export {};
@@ -0,0 +1,46 @@
1
+ import type { BackgroundJobLaunchInput, BackgroundJobRecord, BackgroundJobStatusInput, ContextFile } from './background-job-board';
2
+ import type { TaskOutputState } from './task';
3
+ /**
4
+ * Unified interface for background job operations.
5
+ * Both BackgroundJobBoard and BackgroundJobCoordinator satisfy this.
6
+ *
7
+ * ponytail: single interface, both board and coordinator implement it.
8
+ */
9
+ export interface BackgroundJobStore {
10
+ registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
11
+ updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
12
+ updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
13
+ markRunningFromLiveSession(taskID: string, now?: number): BackgroundJobRecord | undefined;
14
+ markReconciled(taskID: string, now?: number): BackgroundJobRecord | undefined;
15
+ markCancelled(taskID: string, reason?: string, now?: number, options?: {
16
+ force?: boolean;
17
+ }): BackgroundJobRecord | undefined;
18
+ clearParent(parentSessionID: string): void;
19
+ drop(taskID: string): void;
20
+ addContext(taskID: string, files: ContextFile[]): void;
21
+ markUsed(parentSessionID: string, key: string, now?: number): void;
22
+ get(taskID: string): BackgroundJobRecord | undefined;
23
+ field<K extends keyof BackgroundJobRecord>(taskID: string, key: K): BackgroundJobRecord[K] | undefined;
24
+ isRunning(taskID: string): boolean;
25
+ isTerminalUnreconciled(taskID: string): boolean;
26
+ getResultSummary(taskID: string): string | undefined;
27
+ getLastLiveBusyAt(taskID: string): number | undefined;
28
+ getParentSessionID(taskID: string): string | undefined;
29
+ getState(taskID: string): TaskOutputState | 'reconciled' | undefined;
30
+ resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
31
+ resolveReusable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
32
+ resolveRecoverable(parentSessionID: string, taskIDOrAlias: string, agent?: string): BackgroundJobRecord | undefined;
33
+ taskIDs(): Set<string>;
34
+ list(parentSessionID?: string): BackgroundJobRecord[];
35
+ hasRunning(parentSessionID: string): boolean;
36
+ hasTerminalUnreconciled(parentSessionID: string): boolean;
37
+ hasConvergenceSignals(taskID: string, threshold?: number): boolean;
38
+ formatForPrompt(parentSessionID: string, now?: number): string | undefined;
39
+ /** Evaluate close policy. Returns true if session should close now.
40
+ * Mutates deferred state: adds to deferred set if running, removes if not. */
41
+ deferIfRunning(sessionId: string): boolean;
42
+ /** Retry closing a deferred session. Returns true if session should now close. */
43
+ retryDeferredClose(sessionId: string): boolean;
44
+ /** Clear deferred close state for a session being deleted. */
45
+ clearDeferredClose(sessionId: string): void;
46
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pure helpers for councillor model fallback chains.
3
+ *
4
+ * Kept free of any schema/validation library import so that runtime consumers
5
+ * (e.g. `CouncilManager`) can resolve a councillor's ordered model chain
6
+ * without pulling in zod or the config schema module.
7
+ */
8
+ /** A single model in a councillor fallback chain, with optional variant. */
9
+ export type CouncillorModelEntry = {
10
+ id: string;
11
+ variant?: string;
12
+ };
13
+ /**
14
+ * Flatten a councillor model config into an ordered list of model entries.
15
+ *
16
+ * Accepts either a single "provider/model" string or an ordered fallback
17
+ * chain (array of strings and/or `{ id, variant }` entries). Entries that
18
+ * don't carry their own variant fall back to the shared `fallbackVariant`.
19
+ */
20
+ export declare function normalizeCouncillorModels(model: string | Array<string | CouncillorModelEntry>, fallbackVariant?: string): CouncillorModelEntry[];
@@ -1,5 +1,7 @@
1
1
  export * from './agent-variant';
2
2
  export * from './background-job-board';
3
+ export * from './background-job-coordinator';
4
+ export * from './background-job-store';
3
5
  export * from './internal-initiator';
4
6
  export { getLogDir, initLogger, log } from './logger';
5
7
  export * from './polling';
@@ -1,6 +1,11 @@
1
1
  export declare const SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
2
+ export declare const INTERNAL_INITIATOR_METADATA_KEY = "oh-my-opencode-slim.internalInitiator";
2
3
  export declare function createInternalAgentTextPart(text: string): {
3
4
  type: 'text';
4
5
  text: string;
6
+ synthetic: true;
7
+ metadata: {
8
+ 'oh-my-opencode-slim.internalInitiator': true;
9
+ };
5
10
  };
6
- export declare function hasInternalInitiatorMarker(part: unknown): boolean;
11
+ export declare function isInternalInitiatorPart(part: unknown): boolean;
@@ -11,7 +11,7 @@ export declare function withTimeout<T>(operation: Promise<T>, timeoutMs: number,
11
11
  export declare function abortSessionWithTimeout(client: OpencodeClient, sessionId: string, timeoutMs?: number): Promise<void>;
12
12
  /**
13
13
  * Extract the short model label from a "provider/model" string.
14
- * E.g. "openai/gpt-5.4-mini" → "gpt-5.4-mini"
14
+ * E.g. "openai/gpt-5.6-luna" → "gpt-5.6-luna"
15
15
  */
16
16
  export declare function shortModelLabel(model: string): string;
17
17
  export type PromptBody = {
@@ -52,7 +52,7 @@ export declare function parseModelReference(model: string): {
52
52
  export declare function promptWithTimeout(client: OpencodeClient, args: Parameters<OpencodeClient['session']['prompt']>[0], timeoutMs: number, signal?: AbortSignal): Promise<void>;
53
53
  /**
54
54
  * Result of extracting session content.
55
- * `empty` is true when the assistant produced zero text content
55
+ * `empty` is true when the assistant produced zero text content -
56
56
  * the provider returned an empty response (e.g. rate-limited silently).
57
57
  */
58
58
  export interface SessionExtractionResult {
@@ -8,6 +8,10 @@
8
8
  "setDefaultAgent": {
9
9
  "type": "boolean"
10
10
  },
11
+ "compactSidebar": {
12
+ "description": "Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout.",
13
+ "type": "boolean"
14
+ },
11
15
  "autoUpdate": {
12
16
  "description": "Disable automatic installation of plugin updates when false. Defaults to true.",
13
17
  "type": "boolean"
@@ -221,6 +225,7 @@
221
225
  "auto",
222
226
  "tmux",
223
227
  "zellij",
228
+ "herdr",
224
229
  "none"
225
230
  ]
226
231
  },
@@ -361,10 +366,22 @@
361
366
  "type": "number",
362
367
  "minimum": 0
363
368
  },
369
+ "maxRetries": {
370
+ "default": 3,
371
+ "description": "Number of consecutive 429/rate-limit responses tolerated on the same model before aborting (or swapping to the next fallback model when a chain is configured).",
372
+ "type": "integer",
373
+ "minimum": 0,
374
+ "maximum": 9007199254740991
375
+ },
364
376
  "retry_on_empty": {
365
377
  "default": true,
366
378
  "description": "When true (default), empty provider responses are treated as failures, triggering fallback/retry. Set to false to treat them as successes.",
367
379
  "type": "boolean"
380
+ },
381
+ "runtimeOverride": {
382
+ "default": true,
383
+ "description": "When true (default), a runtime model selected via /model that is outside the configured fallback chain will still trigger the chain on rate-limit errors. When false, out-of-chain runtime picks are respected and the error surfaces instead of silently falling back to the chain. Models that are members of the chain always fall back regardless of this setting.",
384
+ "type": "boolean"
368
385
  }
369
386
  },
370
387
  "additionalProperties": false
@@ -417,7 +434,7 @@
417
434
  "maximum": 5
418
435
  },
419
436
  "master": {
420
- "description": "DEPRECATED ignored. Council agent synthesizes directly."
437
+ "description": "DEPRECATED - ignored. Council agent synthesizes directly."
421
438
  }
422
439
  },
423
440
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode-slim",
3
- "version": "2.0.5",
3
+ "version": "2.1.1",
4
4
  "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",