@litfamily/litopencode 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ are tracked separately from this product history.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 1.0.21 - 2026-10-12
9
+
10
+ - The README demo on GitHub is a new film of one real OpenCode turn. It plays without music, and a numbered label names each part LitOpenCode adds as it appears: the `lit` word, the status card switching to WORK, the light mode for a small fix, the reply that reads before editing, the fix with its test, the real test run and the closing summary.
11
+ - The README now shows the LIT status card as it looks in OpenCode: quiet before the first lit prompt, bright skill chips while a request runs, and dimmed recent chips after the turn. It also walks through automatic handoff with pictures of the `lit-handoff auto` messages, the message and toast when the handoff runs, and the doctor block, in English and Korean.
12
+ - The quick start has a short prompt to paste into a coding agent so it installs LitOpenCode for you. The agent checks the requirements, shows a dry run and asks before installing, runs `litopencode doctor`, and reminds you to restart OpenCode.
13
+ - The LIT card along the bottom of the screen no longer stays on WORK after the agent has finished. When the session's turn ends it settles to `○ IDLE`, no step stays lit and the skill chips dim to the recent list; a subagent finishing does not change the main card, and your next prompt brings WORK back.
14
+ - Install now asks for a reasoning effort after each model, for the lead and the helpers, and lists every effort OpenCode offers for that model with the recommended one marked as the default. GPT-6 Astra and GPT-6.1 Sol offer low to max; GPT-6 Luna, GPT-6 Sol and the GPT-5.6 models offer none to max, so GPT-5.6 Luna can now use xhigh. OpenCode offers `ultra` for no model, so it is no longer offered either. A route you saved with `ultra` keeps working as `max`, `litopencode doctor` warns about it, and the next install changes it to `max` with a one-line notice after backing up `litopencode.json`.
15
+ - `litopencode doctor` now reads the automatic handoff setting from your project, the same place LitOpenCode itself uses, so a `lit-handoff auto on 70` sent in chat shows up in doctor. Run it from the project folder.
16
+ - lit-pptx now installs sharp 0.35.5, and the readme-studio motion templates use sharp 0.35.5 and source-map-js 1.2.2.
17
+
8
18
  ## 1.0.20 - 2026-10-12
9
19
 
10
20
  - The band along the bottom of OpenCode's main screen is now a small card that stays on screen for the whole session. The top edge shows LIT, the current workflow and its phase. The first row shows PLAN ━ WORK ━ REVIEW with the goal and the next task. The second row shows a chip for each LitOpenCode skill that ran: skills used by the current request are bright and get a short shine as they appear, then dim into a recent list when the answer is done, and extra chips fold into `+N`. The bottom edge shows context use, elapsed time and the git branch. Before you use lit in a session, the card shows the project's last goal. `band: false` still hides it and `band: { card: true }` still adds the detail card.
package/README-Ko-KR.md CHANGED
@@ -1,13 +1,13 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitOpenCode 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitOpenCode 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
2
2
 
3
3
  # LitOpenCode
4
4
 
5
5
  **LitOpenCode는 OpenCode용 워크플로 플러그인입니다. 프롬프트에 `lit`만 붙이면 계획하고, 만들고, 확인하고, 기록까지 남겨서 다음 세션이 어디서 멈췄는지 바로 압니다.**
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/readme/badge-version.svg" alt="1.0.20" /></a>
8
+ <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/readme/badge-version.svg" alt="1.0.21" /></a>
9
9
  <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://img.shields.io/npm/dm/@litfamily/litopencode?label=downloads&color=D7F75B" alt="월간 npm 다운로드 수" /></a>
10
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
11
11
  <a href="https://github.com/wjgoarxiv/litopencode"><img src="https://img.shields.io/github/stars/wjgoarxiv/litopencode?style=social" alt="GitHub 스타" /></a>
12
12
  </p>
13
13
 
@@ -57,9 +57,9 @@ npm uninstall -g @litfamily/litopencode
57
57
  ## 더 보기
58
58
 
59
59
  - [GitHub의 사용 안내](https://github.com/wjgoarxiv/litopencode/blob/main/docs/guide-Ko-KR.md): 설치 옵션, 자동 인수인계, 문제 해결, 제거
60
- - [워크플로 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/reference-Ko-KR.md) · [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/reference.md)
61
- - [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/CHANGELOG.md) · [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/privacy.md)
62
- - [LitFamily 허브](https://github.com/wjgoarxiv/litfamily) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/LICENSE) · [ASCII 글꼴 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/readme/JetBrainsMono-OFL.txt)
60
+ - [워크플로 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/reference-Ko-KR.md) · [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/reference.md)
61
+ - [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/CHANGELOG.md) · [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/privacy.md)
62
+ - [LitFamily 허브](https://github.com/wjgoarxiv/litfamily) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/LICENSE) · [ASCII 글꼴 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/readme/JetBrainsMono-OFL.txt)
63
63
 
64
64
  <details>
65
65
  <summary>ASCII 로고 복사</summary>
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitOpenCode robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitOpenCode robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
2
2
 
3
3
  # LitOpenCode
4
4
 
5
5
  **LitOpenCode is a workflow plugin for OpenCode. Add `lit` to a prompt and the work gets planned, built, checked and written down, so the next session knows where you stopped.**
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/readme/badge-version.svg" alt="1.0.20" /></a>
8
+ <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/readme/badge-version.svg" alt="1.0.21" /></a>
9
9
  <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://img.shields.io/npm/dm/@litfamily/litopencode?label=downloads&color=D7F75B" alt="npm downloads per month" /></a>
10
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
11
11
  <a href="https://github.com/wjgoarxiv/litopencode"><img src="https://img.shields.io/github/stars/wjgoarxiv/litopencode?style=social" alt="GitHub stars" /></a>
12
12
  </p>
13
13
 
@@ -57,9 +57,9 @@ npm uninstall -g @litfamily/litopencode
57
57
  ## More
58
58
 
59
59
  - [Guide on GitHub](https://github.com/wjgoarxiv/litopencode/blob/main/docs/guide.md): install options, automatic handoff, troubleshooting and removal.
60
- - [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/reference.md)
61
- - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/CHANGELOG.md) · [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/privacy.md)
62
- - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/LICENSE) · [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.20/docs/assets/readme/JetBrainsMono-OFL.txt)
60
+ - [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/reference.md)
61
+ - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/CHANGELOG.md) · [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/privacy.md)
62
+ - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/LICENSE) · [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.21/docs/assets/readme/JetBrainsMono-OFL.txt)
63
63
 
64
64
  <details>
65
65
  <summary>Copy ASCII logo</summary>
@@ -1,7 +1,7 @@
1
1
  import { litOpenCodeDefaultAgents } from "./defaults.js";
2
2
  import { litOpenCodeSpecialistAgents } from "./specialists.js";
3
3
  import { withReaderFacingCommunicationContract } from "../reader-facing-output.js";
4
- import { defaultGlobalConfigFile, configSectionWasAuthored, effectiveAgentConfig } from "../config.js";
4
+ import { defaultGlobalConfigFile, configSectionWasAuthored, effectiveAgentConfig, withRetiredEffortsReplaced } from "../config.js";
5
5
  import { defaultOpenAiRoutes, newGenerationOpenAiModels, openAiModelEfforts, openaiProviderModels } from "../cli/model-catalog.js";
6
6
  import { assertSafeEffectiveAgentRoutes } from "../model-route-policy.js";
7
7
  export const litOpenCodeAgents = Object.freeze([
@@ -190,7 +190,7 @@ function routeLeadAgent(config, authoredOverride) {
190
190
  if (hasAuthoredRouteFields(config.agents["lit-loop"]))
191
191
  return authoredOverride;
192
192
  const lead = leadCategoryRoute(config);
193
- return lead === undefined ? authoredOverride : { ...authoredOverride, ...lead };
193
+ return lead === undefined ? authoredOverride : withRetiredEffortsReplaced({ ...authoredOverride, ...lead });
194
194
  }
195
195
  function applySpecialLeadRoute(agentTarget, agentId) {
196
196
  const existing = agentTarget[agentId];
@@ -1,2 +1,7 @@
1
1
  import type { CliResult } from "./types.ts";
2
+ /**
3
+ * The project the plugin would use from here: OpenCode's worktree is the git root above the working
4
+ * directory, and without one the directory itself.
5
+ */
6
+ export declare function doctorProjectRoot(cwd?: string): string;
2
7
  export declare function doctor(root: string): Promise<CliResult>;
@@ -1,9 +1,10 @@
1
+ import fsSync from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { litOpenCodeAgents, registerLitOpenCodeAgents } from "../agents.js";
4
5
  import { defaultGlobalConfigFile, loadConfig, mergeConfigs, readLitOpenCodeConfigFile } from "../config.js";
5
- import { diagnoseModelRoutes, effectiveAuthoredAgentRoutes, projectModelRoutesForDiagnostics } from "../model-route-policy.js";
6
- import { litOpenCodeConfigFileForOpenCodeRoot } from "../state.js";
6
+ import { diagnoseModelRoutes, effectiveAuthoredAgentRoutes, retiredEffortRoutes, retiredEffortWarning, projectModelRoutesForDiagnostics } from "../model-route-policy.js";
7
+ import { createRuntimePaths, litOpenCodeConfigFileForOpenCodeRoot } from "../state.js";
7
8
  import { isRecord, readJsonObjectIfPresent, readPackageMetadata } from "./json.js";
8
9
  import { parseJsonc } from "./jsonc.js";
9
10
  import { inspectCommandAliases } from "./command-aliases.js";
@@ -83,6 +84,19 @@ async function inspectPlugin(root, metadata) {
83
84
  ...(openCodeConfig.error === undefined ? {} : { error: openCodeConfig.error })
84
85
  };
85
86
  }
87
+ /**
88
+ * The project the plugin would use from here: OpenCode's worktree is the git root above the working
89
+ * directory, and without one the directory itself.
90
+ */
91
+ export function doctorProjectRoot(cwd = process.cwd()) {
92
+ const start = path.resolve(cwd);
93
+ for (let dir = start;; dir = path.dirname(dir)) {
94
+ if (fsSync.existsSync(path.join(dir, ".git")))
95
+ return dir;
96
+ if (path.dirname(dir) === dir)
97
+ return start;
98
+ }
99
+ }
86
100
  export async function doctor(root) {
87
101
  const metadata = await readPackageMetadata();
88
102
  const loaded = await loadConfig(root);
@@ -117,9 +131,14 @@ export async function doctor(root) {
117
131
  const tui = await inspectTuiPlugin(root, installPlugin.present && installPlugin.configured !== null ? installPlugin.configured : installPlugin.expected);
118
132
  const hostConfig = await readOpenCodeConfig(root);
119
133
  const motion = probeMotionRuntime(metadata.packageRoot);
134
+ // The plugin keeps the auto-handoff setting per project, so doctor reads it from the project too.
135
+ const project = doctorProjectRoot();
136
+ const projectPaths = createRuntimePaths(project);
137
+ const projectLitConfig = await readLitOpenCodeConfigFile(projectPaths.configFile);
138
+ const autoHandoffConfig = projectLitConfig === undefined ? effectiveOpenCodeConfig : mergeConfigs(effectiveOpenCodeConfig, projectLitConfig);
120
139
  const autoHandoff = await createAutoHandoffSettings({
121
- stateFile: loaded.paths.autoHandoffFile,
122
- config: loaded.config.autoHandoff
140
+ stateFile: projectPaths.autoHandoffFile,
141
+ config: autoHandoffConfig.autoHandoff
123
142
  }).resolve();
124
143
  const hostCompaction = inspectHostCompactionPoint(hostConfig.config);
125
144
  const autoHandoffWarnings = [...autoHandoff.warnings];
@@ -137,7 +156,8 @@ export async function doctor(root) {
137
156
  enabled: autoHandoff.enabled,
138
157
  percent: autoHandoff.percent,
139
158
  sources: autoHandoff.sources,
140
- stateFile: loaded.paths.autoHandoffFile,
159
+ project,
160
+ stateFile: projectPaths.autoHandoffFile,
141
161
  env: { enabled: autoHandoffEnvEnabled, percent: autoHandoffEnvPercent },
142
162
  host: { autoCompaction: hostCompaction.autoCompaction, compactionPercent: hostCompaction.percent },
143
163
  compaction: "The plugin starts compaction itself after the handoff is saved; the user does not need to run /compact.",
@@ -160,7 +180,8 @@ export async function doctor(root) {
160
180
  agents: Object.keys(effectiveOpenCodeConfig.agents),
161
181
  categories: Object.keys(effectiveOpenCodeConfig.categories),
162
182
  effective: projectModelRoutesForDiagnostics(reportedRoutes, routeDiagnostics),
163
- routeDiagnostics
183
+ routeDiagnostics,
184
+ retiredEfforts: retiredEffortRoutes(await readJsonObjectIfPresent(openCodeLitConfigPath)).map(retiredEffortWarning)
164
185
  },
165
186
  state: {
166
187
  runtimeDir: loaded.paths.runtimeDir,
@@ -9,6 +9,12 @@ export declare function renderOpenCodeConfig(filePath: string, currentText: stri
9
9
  export declare function writeOpenCodeConfig(filePath: string, next: Record<string, unknown>): Promise<void>;
10
10
  export declare function readInstallModelClassification(root: string): Promise<ModelConfigClassification>;
11
11
  export declare function readInstallModelSelection(root: string): Promise<InstallModelSelection | undefined>;
12
+ /**
13
+ * Rewrites saved routes that name an effort OpenCode no longer offers (ultra) to its replacement
14
+ * (max), after copying the file to `<file>.litopencode-backup`. Returns the one-line notice, or
15
+ * undefined when nothing changed. A dry run writes nothing and says nothing.
16
+ */
17
+ export declare function migrateRetiredEfforts(root: string, dryRun: boolean): Promise<string | undefined>;
12
18
  export declare function assertExistingModelRoutesSafe(root: string): Promise<void>;
13
19
  export declare function describeInstallRoutes(root: string, hostConfig: Record<string, unknown> | null, selection: InstallModelSelection | undefined, permissionMode: InstallPermissionMode, permissionModeExplicit: boolean, outputStyle: string): Promise<NonNullable<InstallReport["routes"]>>;
14
20
  export declare function readExistingOutputStyle(root: string): Promise<string>;
@@ -3,11 +3,11 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { litOpenCodeAgents, registerLitOpenCodeAgents } from "../agents.js";
5
5
  import { defaultGlobalConfigFile, defaultGlobalConfigJson, mergeConfigs, readLitOpenCodeConfigFile } from "../config.js";
6
- import { diagnoseModelRoutes, effectiveAuthoredAgentRoutes, UnsafeModelRouteError } from "../model-route-policy.js";
6
+ import { diagnoseModelRoutes, effectiveAuthoredAgentRoutes, retiredEffortRoutes, UnsafeModelRouteError } from "../model-route-policy.js";
7
7
  import { litOpenCodeConfigFileForOpenCodeRoot } from "../state.js";
8
8
  import { isRecord, readJsonObjectIfPresent, readTextIfPresent } from "./json.js";
9
9
  import { editJsoncObject, formatJsoncMember, hasJsoncComments, lineEnding } from "./jsonc.js";
10
- import { installModelEfforts, isModelProvider, modelMenuRows, stripFastSuffix } from "./model-catalog.js";
10
+ import { installModelEfforts, isModelProvider, modelMenuRows, replacedRetiredEffort, stripFastSuffix } from "./model-catalog.js";
11
11
  import { classifyModelConfig, selectedLitConfig } from "./model-policy.js";
12
12
  export async function writeTextAtomically(filePath, text, overrides = {}) {
13
13
  const tempPath = path.join(path.dirname(filePath), "." + path.basename(filePath) + ".tmp-" + randomUUID());
@@ -101,6 +101,36 @@ export async function readInstallModelSelection(root) {
101
101
  return undefined;
102
102
  return { ...planning, helper: execution };
103
103
  }
104
+ // The same suffix the TUI config backups use.
105
+ const litConfigBackupSuffix = ".litopencode-backup";
106
+ /**
107
+ * Rewrites saved routes that name an effort OpenCode no longer offers (ultra) to its replacement
108
+ * (max), after copying the file to `<file>.litopencode-backup`. Returns the one-line notice, or
109
+ * undefined when nothing changed. A dry run writes nothing and says nothing.
110
+ */
111
+ export async function migrateRetiredEfforts(root, dryRun) {
112
+ const filePath = litOpenCodeConfigFileForOpenCodeRoot(root);
113
+ const existing = await readValidatedConfig(root);
114
+ const retired = retiredEffortRoutes(existing);
115
+ if (existing === null || retired.length === 0 || dryRun)
116
+ return undefined;
117
+ const next = structuredClone(existing);
118
+ for (const { section, id } of retired) {
119
+ const route = next[section][id];
120
+ const provider = typeof route.provider === "string" ? route.provider : undefined;
121
+ const model = route.model;
122
+ for (const field of ["variant", "reasoningEffort"]) {
123
+ const replacement = replacedRetiredEffort(provider, model, typeof route[field] === "string" ? route[field] : undefined);
124
+ if (replacement !== undefined)
125
+ route[field] = replacement;
126
+ }
127
+ }
128
+ const backup = filePath + litConfigBackupSuffix;
129
+ await writeTextAtomically(backup, await fs.readFile(filePath, "utf8"));
130
+ await writeTextAtomically(filePath, JSON.stringify(next, null, 2) + "\n");
131
+ const count = retired.length === 1 ? "1 route" : `${retired.length} routes`;
132
+ return `Moved ${count} from ultra to max, which OpenCode no longer offers; the old file is at ${backup}.`;
133
+ }
104
134
  export async function assertExistingModelRoutesSafe(root) {
105
135
  const existing = await readValidatedConfig(root);
106
136
  if (existing === null)
@@ -7,7 +7,7 @@ import { createRuntimePaths } from "../state.js";
7
7
  import { describeCommandAliasMutation, ensureCommandAliases, inspectCommandAliases } from "./command-aliases.js";
8
8
  import { openCodeHostCapabilities } from "./host-capabilities.js";
9
9
  import { applyHostLimits, describeHostLimitMutation, ensureHostLimits } from "./host-limits.js";
10
- import { assertExistingModelRoutesSafe, describeInstallRoutes, describeLitConfigMutation, ensureLitConfig, OpenCodeConfigCommentError, readExistingOutputStyle, readInstallModelClassification, readInstallModelSelection, renderOpenCodeConfig, writeOpenCodeConfig } from "./install-config.js";
10
+ import { assertExistingModelRoutesSafe, migrateRetiredEfforts, describeInstallRoutes, describeLitConfigMutation, ensureLitConfig, OpenCodeConfigCommentError, readExistingOutputStyle, readInstallModelClassification, readInstallModelSelection, renderOpenCodeConfig, writeOpenCodeConfig } from "./install-config.js";
11
11
  import { readJsonObjectIfPresent, readPackageMetadata, readTextIfPresent } from "./json.js";
12
12
  import { renderInstallReport, wrapReceiptLine } from "./install-report.js";
13
13
  import { renderLitOpenCodeWordmark } from "./install-tui.js";
@@ -215,6 +215,9 @@ async function verifyInstalledSurfaces(root, configFile, target, metadata, selec
215
215
  throw new Error("Install verification failed: " + failures.join("; "));
216
216
  }
217
217
  export async function install(root, dryRun, modelSelection = undefined, modelPrompt = "auto", permissionMode = "safe", permissionModeExplicit = false, permissionPrompt = "auto", progress, outputStylePrompt = "auto") {
218
+ const retiredNotice = await migrateRetiredEfforts(root, dryRun);
219
+ if (retiredNotice !== undefined)
220
+ process.stdout.write(retiredNotice + "\n");
218
221
  await assertExistingModelRoutesSafe(root);
219
222
  const classification = await readInstallModelClassification(root);
220
223
  const currentModel = await readInstallModelSelection(root);
@@ -13,7 +13,7 @@ export type ProviderMenuRow = {
13
13
  readonly credentialEnv: string;
14
14
  };
15
15
  export declare const providerMenu: readonly ProviderMenuRow[];
16
- export declare const installModelEfforts: readonly ["low", "medium", "high", "xhigh", "max", "ultra"];
16
+ export declare const installModelEfforts: readonly ["none", "low", "medium", "high", "xhigh", "max"];
17
17
  export declare const nativeReasoningEfforts: readonly ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
18
18
  export declare const defaultOpenAiRoutes: Readonly<{
19
19
  lead: Readonly<{
@@ -73,6 +73,11 @@ export declare const openaiProviderModels: Readonly<{
73
73
  }>;
74
74
  export declare function openAiModelRow(model: string): ModelMenuRow | undefined;
75
75
  export declare function openAiModelEfforts(model: string): readonly InstallModelEffort[];
76
+ /**
77
+ * OpenCode no longer offers `ultra` for any OpenAI row. A saved route that names it on a catalog
78
+ * model runs as `max`, and the next install rewrites it; this returns that replacement, or undefined.
79
+ */
80
+ export declare function replacedRetiredEffort(provider: string | undefined, model: string | undefined, effort: string | undefined): "max" | undefined;
76
81
  export declare function isInstallModelEffort(value: string): value is InstallModelEffort;
77
82
  export declare function openaiModelHelpLine(): string;
78
83
  export declare function shippedDefaultsSummary(): string;
@@ -6,8 +6,13 @@ export const providerMenu = [
6
6
  { provider: "openai", label: "OpenAI GPT-6 family / GPT-5.6 previous generation", credentialEnv: "OPENAI_API_KEY" },
7
7
  { provider: "xai", label: "xAI Grok", credentialEnv: "XAI_API_KEY" }
8
8
  ];
9
- export const installModelEfforts = Object.freeze(["low", "medium", "high", "xhigh", "max", "ultra"]);
10
- export const nativeReasoningEfforts = Object.freeze(["none", "minimal", ...installModelEfforts]);
9
+ export const installModelEfforts = Object.freeze(["none", "low", "medium", "high", "xhigh", "max"]);
10
+ // What a config file may name. Custom models are user-owned, so this stays wider than any catalog row.
11
+ export const nativeReasoningEfforts = Object.freeze(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
12
+ // The efforts OpenCode itself offers for each OpenAI row: the model's `variants` in
13
+ // `opencode models openai --verbose`, checked on OpenCode 1.18.35.
14
+ const hostEffortsWithNone = installModelEfforts;
15
+ const hostEffortsFromLow = installModelEfforts.slice(1);
11
16
  export const defaultOpenAiRoutes = Object.freeze({
12
17
  lead: Object.freeze({ provider: "openai", model: "gpt-6-astra", effort: "xhigh" }),
13
18
  helper: Object.freeze({ provider: "openai", model: "gpt-6-luna", effort: "max" })
@@ -39,18 +44,18 @@ export const hostLimitModelIds = Object.freeze([
39
44
  defaultOpenAiRoutes.helper.model,
40
45
  previousGenerationOpenAiModels.luna
41
46
  ]);
42
- // The OpenAI rows are the product policy, not the host catalog. Keep every
43
- // supported effort here so the picker, flags, config reader, and route guard
44
- // all consume one catalog rather than maintaining independent literal lists.
47
+ // The OpenAI rows choose the models and their recommended efforts; each row's
48
+ // efforts are the host list above. The picker, flags, config reader, and route
49
+ // guard all consume this one catalog rather than independent literal lists.
45
50
  export const openaiRows = [
46
- { provider: "openai", model: defaultOpenAiRoutes.lead.model, effort: defaultOpenAiRoutes.lead.effort, efforts: installModelEfforts, hint: "frontier reasoning (recommended lead)" },
47
- { provider: "openai", model: newGenerationOpenAiModels.sol, effort: "xhigh", efforts: installModelEfforts, hint: "coding lead (recommended alternative)" },
48
- { provider: "openai", model: defaultOpenAiRoutes.helper.model, effort: defaultOpenAiRoutes.helper.effort, efforts: installModelEfforts.slice(0, 5), hint: "balanced (recommended helper)" },
49
- { provider: "openai", model: previousGpt6OpenAiModels.sol, effort: "xhigh", efforts: installModelEfforts, hint: "coding lead, previous generation" },
50
- { provider: "openai", model: previousGenerationOpenAiModels.sol, effort: "xhigh", efforts: ["high", "xhigh", "max"], hint: "deepest reasoning, previous generation" },
51
- { provider: "openai", model: previousGenerationOpenAiModels.luna, effort: "max", efforts: ["high", "max"], hint: "balanced, previous generation" },
52
- { provider: "openai", model: previousGenerationOpenAiModels.family, effort: "high", efforts: ["high", "xhigh", "max"], hint: "previous generation" },
53
- { provider: "openai", model: previousGenerationOpenAiModels.terra, effort: "xhigh", efforts: ["high", "xhigh"], hint: "previous generation" }
51
+ { provider: "openai", model: defaultOpenAiRoutes.lead.model, effort: defaultOpenAiRoutes.lead.effort, efforts: hostEffortsFromLow, hint: "frontier reasoning (recommended lead)" },
52
+ { provider: "openai", model: newGenerationOpenAiModels.sol, effort: "xhigh", efforts: hostEffortsFromLow, hint: "coding lead (recommended alternative)" },
53
+ { provider: "openai", model: defaultOpenAiRoutes.helper.model, effort: defaultOpenAiRoutes.helper.effort, efforts: hostEffortsWithNone, hint: "balanced (recommended helper)" },
54
+ { provider: "openai", model: previousGpt6OpenAiModels.sol, effort: "xhigh", efforts: hostEffortsWithNone, hint: "coding lead, previous generation" },
55
+ { provider: "openai", model: previousGenerationOpenAiModels.sol, effort: "xhigh", efforts: hostEffortsWithNone, hint: "deepest reasoning, previous generation" },
56
+ { provider: "openai", model: previousGenerationOpenAiModels.luna, effort: "max", efforts: hostEffortsWithNone, hint: "balanced, previous generation" },
57
+ { provider: "openai", model: previousGenerationOpenAiModels.family, effort: "high", efforts: hostEffortsWithNone, hint: "previous generation" },
58
+ { provider: "openai", model: previousGenerationOpenAiModels.terra, effort: "xhigh", efforts: hostEffortsWithNone, hint: "previous generation" }
54
59
  ];
55
60
  export const openaiProviderModels = Object.freeze({
56
61
  [newGenerationOpenAiModels.astra]: Object.freeze({ name: "GPT-6 Astra", reasoning: true, temperature: false, tool_call: true }),
@@ -64,6 +69,18 @@ export function openAiModelRow(model) {
64
69
  export function openAiModelEfforts(model) {
65
70
  return openAiModelRow(model)?.efforts ?? [];
66
71
  }
72
+ /**
73
+ * OpenCode no longer offers `ultra` for any OpenAI row. A saved route that names it on a catalog
74
+ * model runs as `max`, and the next install rewrites it; this returns that replacement, or undefined.
75
+ */
76
+ export function replacedRetiredEffort(provider, model, effort) {
77
+ if (effort !== "ultra" || model === undefined)
78
+ return undefined;
79
+ const slash = model.indexOf("/");
80
+ const owner = slash > 0 ? model.slice(0, slash) : provider;
81
+ const id = slash > 0 ? model.slice(slash + 1) : model;
82
+ return owner === "openai" && openAiModelRow(stripFastSuffix(id)) !== undefined ? "max" : undefined;
83
+ }
67
84
  export function isInstallModelEffort(value) {
68
85
  return installModelEfforts.includes(value);
69
86
  }
@@ -79,13 +96,13 @@ export function legacyFastAliasHelp() {
79
96
  return "The previous-generation GPT-5.6 rows retain their <id>-fast aliases; GPT-6 rows have no inferred -fast alias.";
80
97
  }
81
98
  export function modelEffortHelp() {
82
- return `GPT-6 Astra and Sol accept ${installModelEfforts.join(", ")}; GPT-6 Luna accepts ${openAiModelEfforts(newGenerationOpenAiModels.luna).join(", ")} and rejects ultra.`;
99
+ return `GPT-6 Astra and GPT-6.1 Sol accept ${openAiModelEfforts(newGenerationOpenAiModels.astra).join(", ")}; GPT-6 Luna, GPT-6 Sol and the GPT-5.6 rows accept ${openAiModelEfforts(newGenerationOpenAiModels.luna).join(", ")}.`;
83
100
  }
84
101
  // Used only when the host catalog (~/.cache/opencode/models.json) is absent or unreadable.
85
102
  const fallbackXaiRows = [
86
103
  { provider: "xai", model: "grok-4.6", effort: "xhigh", efforts: ["low", "medium", "high", "xhigh"], hint: "frontier reasoning (catalog unavailable)" },
87
104
  { provider: "xai", model: "grok-4.5", effort: "high", efforts: ["low", "medium", "high"], hint: "catalog unavailable" },
88
- { provider: "xai", model: "grok-4.3", effort: "high", efforts: ["low", "medium", "high"], hint: "catalog unavailable" }
105
+ { provider: "xai", model: "grok-4.3", effort: "high", efforts: ["none", "low", "medium", "high"], hint: "catalog unavailable" }
89
106
  ];
90
107
  const xaiRowLimit = 3;
91
108
  export function hostCatalogPath() {
@@ -120,19 +120,24 @@ async function chooseProvider(reader, title, defaultProvider) {
120
120
  const index = await chooseIndex(reader, title, providerMenu.map((row) => ({ label: row.label.padEnd(35, " ") + " (" + row.provider + ")", hint: "" })), defaultIndex);
121
121
  return providerMenu[index].provider;
122
122
  }
123
- async function chooseRoute(reader, title, provider, defaultIndex, preferred) {
123
+ // After a model, the role's effort: every effort the host offers for that model, the
124
+ // recommended one marked, and the current route's effort as the default when it is still offered.
125
+ async function chooseEffort(reader, role, row, current) {
126
+ const preferred = current !== undefined && row.efforts.includes(current) ? current : row.effort;
127
+ const index = await chooseIndex(reader, "Choose the " + role + " reasoning effort for " + row.model + ".", row.efforts.map((effort) => ({ label: effort, hint: effort === row.effort ? "recommended" : "" })), Math.max(0, row.efforts.indexOf(preferred)));
128
+ return row.efforts[index];
129
+ }
130
+ async function chooseRoute(reader, title, role, provider, defaultIndex, preferred) {
124
131
  const rows = modelMenuRows(provider);
125
132
  const preferredModel = preferred === undefined ? undefined : provider === "openai" ? stripFastSuffix(preferred.model) : preferred.model;
126
133
  const preferredIndex = preferredModel === undefined ? -1 : rows.findIndex((row) => row.model === preferredModel);
127
134
  const selectedDefault = preferredIndex >= 0 ? preferredIndex : Math.min(defaultIndex, rows.length - 1);
128
135
  const index = await chooseIndex(reader, title, rows.map((row) => ({ label: menuLabel(row), hint: row.hint })), selectedDefault);
129
136
  const row = rows[index];
130
- if (preferred !== undefined &&
131
- row.provider === preferred.provider &&
132
- row.model === preferredModel) {
133
- return preferred;
134
- }
135
- return { provider: row.provider, model: row.model, effort: row.effort };
137
+ const same = preferred !== undefined && row.provider === preferred.provider && row.model === preferredModel;
138
+ const effort = await chooseEffort(reader, role, row, same ? preferred.effort : undefined);
139
+ // An existing route keeps its exact model id (a -fast alias included).
140
+ return same ? { ...preferred, effort } : { provider: row.provider, model: row.model, effort };
136
141
  }
137
142
  function recommendedHelperIndex(provider) {
138
143
  return Math.max(0, modelMenuRows(provider).findIndex((row) => row.hint.includes("recommended helper")));
@@ -193,10 +198,10 @@ export async function promptModelSelection(selection, mode, dryRun, classificati
193
198
  process.stdout.write("\nExisting route (" + classification.class + ") will be rewritten in managed keys only.\n");
194
199
  }
195
200
  const leadProvider = await chooseProvider(reader, "Choose the provider for LitOpenCode agents.", preferredSelection !== undefined && isModelProvider(preferredSelection.provider) ? preferredSelection.provider : "openai");
196
- const lead = await chooseRoute(reader, "Choose the LEAD model (plans and reviews).", leadProvider, 0, preferredSelection);
201
+ const lead = await chooseRoute(reader, "Choose the LEAD model (plans and reviews).", "LEAD", leadProvider, 0, preferredSelection);
197
202
  const preferredHelper = preferredSelection?.helper;
198
203
  const helperProvider = await chooseProvider(reader, "Choose the provider for HELPER agents (workers, explorers, librarians).", preferredHelper !== undefined && isModelProvider(preferredHelper.provider) ? preferredHelper.provider : leadProvider);
199
- const helper = await chooseRoute(reader, "Choose the HELPER model (spawned and delegated agents).", helperProvider, recommendedHelperIndex(helperProvider), preferredHelper);
204
+ const helper = await chooseRoute(reader, "Choose the HELPER model (spawned and delegated agents).", "HELPER", helperProvider, recommendedHelperIndex(helperProvider), preferredHelper);
200
205
  const chosen = { ...lead, helper };
201
206
  process.stdout.write(renderModelRouteCard(chosen, configPath));
202
207
  await askLine(reader, "");
@@ -54,7 +54,7 @@ export type InstallModelRoute = {
54
54
  export type InstallModelSelection = InstallModelRoute & {
55
55
  readonly helper?: InstallModelRoute;
56
56
  };
57
- export type InstallModelEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
57
+ export type InstallModelEffort = "none" | "low" | "medium" | "high" | "xhigh" | "max";
58
58
  export type ModelConfigClass = "fresh" | "managed_legacy" | "gpt56_managed" | "astra_managed" | "gpt56_other" | "custom" | "mixed";
59
59
  export type ModelConfigClassification = {
60
60
  readonly class: ModelConfigClass;
package/dist/config.d.ts CHANGED
@@ -74,4 +74,6 @@ export declare function mergeConfigs(base: LitOpenCodeConfig, override: Partial<
74
74
  export declare function defaultGlobalConfigFile(): LitOpenCodeConfig;
75
75
  export declare function defaultGlobalConfigJson(): Record<string, unknown>;
76
76
  export declare function effectiveAgentConfig(config: LitOpenCodeConfig, agentId: string): LitOpenCodeAgentModelConfig;
77
+ /** A saved route on an effort OpenCode no longer offers runs as its replacement until install rewrites it. */
78
+ export declare function withRetiredEffortsReplaced(route: LitOpenCodeAgentModelConfig): LitOpenCodeAgentModelConfig;
77
79
  export declare function loadConfig(projectRoot: string): Promise<LoadedConfig>;
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRuntimePaths } from "./state.js";
2
2
  import { readLitOpenCodeConfigFile } from "./config-parser.js";
3
- import { defaultOpenAiRoutes } from "./cli/model-catalog.js";
3
+ import { defaultOpenAiRoutes, replacedRetiredEffort } from "./cli/model-catalog.js";
4
4
  export { LitOpenCodeConfigError, readLitOpenCodeConfigFile } from "./config-parser.js";
5
5
  // The runtime receives a merged config, so keep source ownership alongside
6
6
  // (rather than inside) the JSON-shaped config. This never serializes into a
@@ -140,7 +140,15 @@ export function defaultGlobalConfigJson() {
140
140
  export function effectiveAgentConfig(config, agentId) {
141
141
  const agentConfig = config.agents[agentId] ?? {};
142
142
  const categoryConfig = agentConfig.category ? config.categories[agentConfig.category] : undefined;
143
- return mergeAgentModelConfig(categoryConfig, agentConfig);
143
+ return withRetiredEffortsReplaced(mergeAgentModelConfig(categoryConfig, agentConfig));
144
+ }
145
+ /** A saved route on an effort OpenCode no longer offers runs as its replacement until install rewrites it. */
146
+ export function withRetiredEffortsReplaced(route) {
147
+ const variant = replacedRetiredEffort(route.provider, route.model, route.variant);
148
+ const reasoningEffort = replacedRetiredEffort(route.provider, route.model, route.reasoningEffort);
149
+ if (variant === undefined && reasoningEffort === undefined)
150
+ return route;
151
+ return { ...route, ...(variant === undefined ? {} : { variant }), ...(reasoningEffort === undefined ? {} : { reasoningEffort }) };
144
152
  }
145
153
  export async function loadConfig(projectRoot) {
146
154
  const paths = createRuntimePaths(projectRoot);
@@ -1,7 +1,7 @@
1
1
  import { type LitOpenCodeAgentModelConfig, type LitOpenCodeConfig } from "./config.ts";
2
2
  export type ModelRouteDiagnostic = {
3
3
  readonly agentId: string;
4
- readonly code: "luna_effort_below_high" | "luna_xhigh_forbidden" | "luna_effort_conflict" | "terra_effort_below_high" | "terra_effort_conflict" | "astra_effort_unsupported" | "astra_effort_conflict" | "model_effort_unsupported" | "model_effort_conflict";
4
+ readonly code: "astra_effort_unsupported" | "astra_effort_conflict" | "model_effort_unsupported" | "model_effort_conflict";
5
5
  readonly model: string;
6
6
  readonly effort: string;
7
7
  };
@@ -23,3 +23,11 @@ export declare class UnsafeModelRouteError extends Error {
23
23
  constructor(diagnostics: readonly ModelRouteDiagnostic[]);
24
24
  }
25
25
  export declare function assertSafeEffectiveAgentRoutes(config: LitOpenCodeConfig, agentIds: readonly string[]): void;
26
+ export type RetiredEffortRoute = {
27
+ readonly section: "categories" | "agents";
28
+ readonly id: string;
29
+ readonly model: string;
30
+ };
31
+ /** The saved routes in a litopencode.json object that still name an effort OpenCode no longer offers. */
32
+ export declare function retiredEffortRoutes(config: unknown): readonly RetiredEffortRoute[];
33
+ export declare function retiredEffortWarning(route: RetiredEffortRoute): string;