opencode-resolve 0.1.8 → 0.1.10

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/README.ko.md CHANGED
@@ -156,11 +156,11 @@ npm install -g opencode-resolve
156
156
 
157
157
  1. `opencode-resolve`를 `~/.config/opencode/opencode.json`의 `plugin` 배열에 추가 (이미 없는 경우).
158
158
  2. 파일이 존재하지 않는 경우, 현재 모델 프로바이더에 맞게 적응된 `~/.config/opencode/resolve.json`을 생성:
159
- - **GLM/ZAI 모델 감지** GLM-only 별칭 프리셋(GPT 의존 없음) 비밀값 없는 로컬 ZAI MCP 부트스트랩.
160
- - **OpenAI/GPT 모델 감지** 모든 역할에 현재 모델을 사용하는 단일 프로바이더 GPT 프리셋.
161
- - **다른 프로바이더 또는 모델 없음**모델 중립적 `models: {}` (모든 역할이 OpenCode 기본 모델 상속).
159
+ - **인터랙티브 터미널** → 항상 `mix` / `gpt` / `glm`을 묻고, `bronze` / `silver` / `gold` 3티어 모델을 고르게 합니다. `mix`에서는 전용 `codex`, `glm` primary 에이전트를 켤지도 묻습니다.
160
+ - **비대화형 설치** → 모델 pinning을 추측하지 않습니다. `profile: "mix"`, `models: {}`를 쓰고 primary 경로(`resolver`, `codex`, `glm`)를 활성화하므로 이후 명시적으로 모델을 고르면 됩니다.
161
+ - **GLM/ZAI 모델 감지**비밀값 없는 로컬 ZAI MCP 부트스트랩은 계속 추가합니다.
162
162
 
163
- 기존 `resolve.json` 파일은 **절대 덮어쓰지 않습니다** 적응형 프리셋은 최초 생성 시에만 적용됩니다. 다시 생성하려면 `resolve.json`을 삭제하고 재설치하세요.
163
+ 기존 `resolve.json` 파일은 **동의 없이 덮어쓰지 않습니다**. 재설치 인터랙티브 환경이면 기존 파일을 업데이트할지, 백업 후 fresh setup을 다시 돌릴지 묻습니다. 비대화형 자동화에서는 `OPENCODE_RESOLVE_REINSTALL=fresh` 또는 `OPENCODE_RESOLVE_REINSTALL=update`를 설정하세요.
164
164
 
165
165
  자동 등록을 건너뛰려면:
166
166
 
@@ -552,7 +552,7 @@ opencode-resolve는 전체 저장소를 프롬프트에 밀어 넣지 않고도
552
552
  | `autoApprove` | `boolean` | `true` | 하위 호환/가독성 플래그. 현재 동작은 내장 기본 권한과 `permission.ask` bash 분류기가 제어하며, 이 플래그가 권한을 재작성하지 않음. |
553
553
  | `autoUpdate` | `boolean` | `true` | npm 버전 확인과 OpenCode 플러그인 캐시 갱신 알림을 best-effort로 수행. 비활성화하려면 false. |
554
554
  | `maxParallelSubagents` | `positive integer` | _미설정_ | 동시 coder 수에 대한 선택적 프롬프트 수준 상한. 미설정 시 resolver는 soft fan-out 가이드를 사용하고 rate-limit 에러에 backoff. GLM profile도 사용자가 설정하지 않는 한 hard cap을 걸지 않음. |
555
- | `models` | `object` | `{}` | 별칭 맵. 키는 에이전트 이름 또는 `fast`/`strong`/`mini`/`codex`/`quick`/`deep`/`glm`/`gpt`. 값은 모델 id 또는 다른 별칭. 기본적으로 비어있음 — 모든 역할이 OpenCode 기본 모델을 상속. |
555
+ | `models` | `object` | `{}` | 별칭 맵. 키는 에이전트 이름 또는 `bronze`/`silver`/`gold`, `gpt-*`, `glm-*`, `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt` 같은 별칭. 값은 모델 id 또는 다른 별칭. |
556
556
  | `agents` | `object` | `{}` | 에이전트별 재정의 (아래 참조). |
557
557
  | `config` | `string` | _없음_ | 설정 파일의 사용자 정의 경로 (프로젝트 상대경로 또는 절대경로). |
558
558
 
@@ -647,6 +647,7 @@ opencode-resolve는 전체 저장소를 프롬프트에 밀어 넣지 않고도
647
647
  - 이미 설정한 키는 **절대** 수정하지 않음.
648
648
  - `enabled` 목록, `models` 맵, 또는 `agents` 재정의를 **절대** 다시 쓰지 않음.
649
649
  - `enabled`가 설정되어 있고 `"resolver"`를 포함하지 않으면, 추가를 제안하는 한 줄 팁을 출력. 파일은 그대로 유지.
650
+ - 인터랙티브 재설치에서는 기존 파일을 업데이트할지, 백업 후 fresh setup을 다시 돌릴지 물음. 비대화형 자동화에서는 fresh 재설치에 `OPENCODE_RESOLVE_REINSTALL=fresh`, 보존 마이그레이션에 `OPENCODE_RESOLVE_REINSTALL=update` 사용.
650
651
 
651
652
  ### 적응형 최초 설치 프리셋
652
653
 
@@ -654,12 +655,12 @@ opencode-resolve는 전체 저장소를 프롬프트에 밀어 넣지 않고도
654
655
 
655
656
  | 감지된 프로바이더 | 프리셋 |
656
657
  |---|---|
657
- | GLM/ZAI + OpenAI/GPT | Mixed: `profile: "mix"`, scout/coder 별칭은 GLM, resolver/reviewer/planner 별칭은 GPT |
658
- | GLM / ZAI | GLM-only: 모든 resolve 에이전트가 GLM 별칭 사용, GPT 의존 없음 |
659
- | OpenAI / GPT | 단일 프로바이더: 모든 역할이 현재 OpenAI 모델 사용 |
660
- | 다른 프로바이더 또는 없음 | `profile: "mix"`와 모델 중립적 `models: {}` (모든 역할이 OpenCode 기본 모델 상속) |
658
+ | 인터랙티브 터미널 | 항상 `mix` / `gpt` / `glm`을 묻고 3티어 모델을 고르게 하며, `mix`에서는 `codex`와 `glm` primary 에이전트 활성화도 묻습니다 |
659
+ | 비대화형 설치 | 모델 pinning을 추측하지 않음; `profile: "mix"`, `models: {}`를 쓰고 `resolver`, `codex`, `glm` primary 경로를 활성화 |
660
+ | 레거시 opt-in | `OPENCODE_RESOLVE_AUTO_PRESET=1`을 설정하면 비대화형 provider-adapted 프리셋 허용 |
661
+ | GLM/ZAI 감지 | API 키를 복사하지 않고 ZAI MCP 부트스트랩 추가 |
661
662
 
662
- 언제든지 프리셋을 변경하려면 `resolve.json`의 `models`를 직접 편집하거나, 파일을 삭제하고 재설치하세요.
663
+ 언제든지 프리셋을 변경하려면 `resolve.json`의 `models`를 직접 편집하거나, `OPENCODE_RESOLVE_REINSTALL=fresh`로 재설치하세요.
663
664
 
664
665
  마이그레이션을 완전히 건너뛰려면:
665
666
 
@@ -748,6 +749,9 @@ OPENCODE_RESOLVE_SKIP_POSTINSTALL=1 npm install -g opencode-resolve
748
749
  | `strong` | 강력한/비싼 모델의 프로바이더 중립적 별칭 |
749
750
  | `mini` | 미니/효율적 모델의 프로바이더 중립적 별칭 |
750
751
  | `codex` | 코덱스 스타일 코딩 모델의 프로바이더 중립적 별칭 |
752
+ | `bronze` / `silver` / `gold` | scout / coder / reasoner 3티어 별칭 |
753
+ | `gpt-bronze` / `gpt-silver` / `gpt-gold` | mixed 설정의 GPT/Codex 전용 3티어 별칭 |
754
+ | `glm-bronze` / `glm-silver` / `glm-gold` | mixed 설정의 GLM 전용 3티어 별칭 |
751
755
  | `quick` | 레거시 별칭 (`fast`와 동등) |
752
756
  | `deep` | 레거시 별칭 (`strong`과 동등) |
753
757
  | `glm` | 레거시 별칭 (하위 호환성) |
@@ -762,6 +766,8 @@ OPENCODE_RESOLVE_SKIP_POSTINSTALL=1 npm install -g opencode-resolve
762
766
  | 에이전트 | 기본 | 모드 | Edit | Bash | WebFetch | 용도 |
763
767
  |---|:---:|---|---|---|---|---|
764
768
  | `resolver` | 예 (핵심) | `all` | allow | ask (분류기 라우팅) | allow | 컨텍스트 효율적 오케스트레이터. 작업을 검증된 체크포인트로 분해, coder 디스패치, 각각 검증, 반복 회복 실패 시 차단 사항 보고. |
769
+ | `codex` | 아니오 | `all` | allow | ask (분류기 라우팅) | allow | `resolver`와 같은 검증 resolve-loop 스타일의 Codex 최적화 primary. 최초 설치 GPT/mix 프리셋 또는 명시 설정으로 활성화. |
770
+ | `glm` | 아니오 | `all` | allow | ask (분류기 라우팅) | allow | `resolver`와 같은 검증 resolve-loop 스타일의 GLM 최적화 primary. 최초 설치 GLM/mix 프리셋 또는 명시 설정으로 활성화. |
765
771
  | `coder` | 예 (핵심) | `subagent` | allow | ask (분류기 라우팅) | allow | 집중된 구현자. 가장 작은 올바른 패치. 필요한 파일만 읽음. |
766
772
  | `explorer` | 예 (서브에이전트) | `subagent` | **deny** | **deny** | allow | 내부 빠른 코드베이스 스카우트. Resolver가 범위가 불명확할 때만 디스패치; 좁은 범위는 로컬 read/grep/glob 선호. |
767
773
  | `reviewer` | 예 (서브에이전트) | `subagent` | **deny** | **deny** | allow | 내부 검증 갭 감사자. 비사소한 변경에 검증 갭이 있을 때 디스패치. |
@@ -784,7 +790,7 @@ OPENCODE_RESOLVE_SKIP_POSTINSTALL=1 npm install -g opencode-resolve
784
790
 
785
791
  지원되는 권한 값: `ask`, `allow`, `deny`.
786
792
 
787
- 지원되는 모델 별칭 키: `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`, 그리고 지원되는 모든 에이전트 이름. 별칭은 `models`에 정의된 경우에만 해석됩니다.
793
+ 지원되는 모델 별칭 키: `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`, `bronze`, `silver`, `gold`, `gpt-bronze`, `gpt-silver`, `gpt-gold`, `glm-bronze`, `glm-silver`, `glm-gold`, 그리고 지원되는 모든 에이전트 이름. 별칭은 `models`에 정의된 경우에만 해석됩니다.
788
794
 
789
795
  `preserveNative`은 가독성을 위해 허용되지만, 네이티브 `plan`과 `build`는 항상 보존됩니다. 플러그인은 빌트인 OpenCode 에이전트를 절대 다시 쓰지 않습니다.
790
796
 
@@ -875,12 +881,20 @@ opencode plugin opencode-resolve --global --force
875
881
 
876
882
  ```sh
877
883
  npm install
884
+ npm run hooks:install
878
885
  npm test
879
886
  npm run install:local
880
887
  ```
881
888
 
882
889
  `install:local`은 플러그인을 빌드하고, OpenCode 전역 플러그인 디렉토리에 링크하며, `~/.config/opencode/resolve.json`이 없으면 생성합니다.
883
890
 
891
+ `hooks:install`은 이 checkout의 `core.hooksPath`를 `.githooks`로 설정합니다. 추적되는 훅은 의도적으로 빡세게 동작합니다:
892
+
893
+ | 훅 | 게이트 |
894
+ |---|---|
895
+ | `pre-commit` | `npm run typecheck`, `npm test`, `npm run coverage` |
896
+ | `pre-push` | 전체 `pre-commit` 게이트와 `npm pack --dry-run`을 통과해야 원격에 반영 |
897
+
884
898
  수동 로컬 설치:
885
899
 
886
900
  ```sh
@@ -900,6 +914,7 @@ ln -sf "$PWD/dist/index.js" ~/.config/opencode/plugins/opencode-resolve.js
900
914
  ```sh
901
915
  npm run typecheck
902
916
  npm test
917
+ npm run coverage
903
918
  npm run build
904
919
  ```
905
920
 
@@ -910,6 +925,7 @@ npm run build
910
925
  ```sh
911
926
  npm run typecheck
912
927
  npm test
928
+ npm run coverage
913
929
  npm audit --audit-level=moderate
914
930
  npm publish --dry-run
915
931
  ```
package/README.md CHANGED
@@ -156,11 +156,11 @@ The `postinstall` script automatically:
156
156
 
157
157
  1. Adds `opencode-resolve` to `~/.config/opencode/opencode.json` `plugin` array (if not already present).
158
158
  2. Creates `~/.config/opencode/resolve.json` adapted to your current model provider when the file does not exist:
159
- - **GLM/ZAI model detected** → GLM-only alias preset (no GPT dependency) and a non-secret local ZAI MCP bootstrap.
160
- - **OpenAI/GPT model detected** → single-provider GPT preset using your current model for all roles.
161
- - **Other or no model** → model-neutral `models: {}` (all roles inherit OpenCode default).
159
+ - **Interactive terminal** → always asks for `mix` / `gpt` / `glm`, then lets you pick three-tier `bronze` / `silver` / `gold` models. In `mix`, it also asks whether to enable dedicated `codex` and `glm` primary agents.
160
+ - **Non-interactive install** → does not guess model pinning. It writes `profile: "mix"`, leaves `models: {}`, and enables the three primary routes (`resolver`, `codex`, `glm`) so you can pin models explicitly afterward.
161
+ - **GLM/ZAI model detected** → still adds the non-secret local ZAI MCP bootstrap.
162
162
 
163
- Existing `resolve.json` files are **never overwritten** the adaptive preset only applies to first-time creation. To regenerate, delete `resolve.json` and reinstall.
163
+ Existing `resolve.json` files are **never overwritten without consent**. On reinstall, interactive setup asks whether to update the existing file or back it up and run fresh setup. For non-interactive automation, set `OPENCODE_RESOLVE_REINSTALL=fresh` or `OPENCODE_RESOLVE_REINSTALL=update`.
164
164
 
165
165
  To skip automatic registration:
166
166
 
@@ -552,7 +552,7 @@ Every accepted top-level option:
552
552
  | `autoApprove` | `boolean` | `true` | Compatibility/readability flag. Current behavior is controlled by built-in base permissions and the `permission.ask` bash classifier; the flag does not rewrite permissions. |
553
553
  | `autoUpdate` | `boolean` | `true` | Best-effort npm version check and OpenCode plugin cache refresh notice. Set false to disable. |
554
554
  | `maxParallelSubagents` | `positive integer` | _unset_ | Optional prompt-level cap on simultaneous coders. When unset, the resolver uses soft fan-out guidance and backs off on rate-limit errors. GLM profile does not impose a hard cap unless you set one. |
555
- | `models` | `object` | `{}` | Alias map. Keys are agent names or `fast`/`strong`/`mini`/`codex`/`quick`/`deep`/`glm`/`gpt`. Values are model ids or other aliases. Empty by default — all roles inherit the OpenCode default model. |
555
+ | `models` | `object` | `{}` | Alias map. Keys are agent names or provider-neutral aliases including `bronze`/`silver`/`gold`, `gpt-*`, `glm-*`, `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`. Values are model ids or other aliases. |
556
556
  | `agents` | `object` | `{}` | Per-agent overrides (see below). |
557
557
  | `config` | `string` | _none_ | Custom path to a config file (relative to the project or absolute). |
558
558
 
@@ -647,6 +647,7 @@ When you upgrade to a newer version of `opencode-resolve`, the `postinstall` scr
647
647
  - **Never** modifies keys you have already set.
648
648
  - **Never** rewrites your `enabled` list, `models` map, or `agents` overrides.
649
649
  - If `enabled` is set and does not include `"resolver"`, prints a one-line tip suggesting you add it. Your file is left untouched.
650
+ - In an interactive reinstall, asks whether to update the existing file or back it up and run fresh setup. In non-interactive automation, use `OPENCODE_RESOLVE_REINSTALL=fresh` for a fresh reinstall or `OPENCODE_RESOLVE_REINSTALL=update` to preserve and migrate.
650
651
 
651
652
  ### Adaptive first-install preset
652
653
 
@@ -654,12 +655,12 @@ When `resolve.json` does **not** exist, postinstall inspects your OpenCode model
654
655
 
655
656
  | Detected provider | Preset |
656
657
  |---|---|
657
- | GLM/ZAI + OpenAI/GPT | Mixed: `profile: "mix"`, GLM for scout/coder aliases, GPT for resolver/reviewer/planner aliases |
658
- | GLM / ZAI | GLM-only: all resolve agents use GLM aliases, avoiding GPT dependency |
659
- | OpenAI / GPT | Single-provider: all roles use your current OpenAI model |
660
- | Other or none | `profile: "mix"` with model-neutral `models: {}` (all roles inherit OpenCode default) |
658
+ | Interactive terminal | Always prompts for `mix` / `gpt` / `glm`, asks for three-tier model picks, and in `mix` asks whether to enable `codex` and `glm` primary agents |
659
+ | Non-interactive install | Does not guess model pinning; writes `profile: "mix"`, `models: {}`, and enables `resolver`, `codex`, and `glm` as primary routes |
660
+ | Legacy opt-in | Set `OPENCODE_RESOLVE_AUTO_PRESET=1` to allow non-interactive provider-adapted presets |
661
+ | GLM/ZAI detected | Adds the ZAI MCP bootstrap without copying API keys |
661
662
 
662
- To change presets at any time, edit `models` in `resolve.json` directly or delete the file and reinstall.
663
+ To change presets at any time, edit `models` in `resolve.json` directly or run reinstall with `OPENCODE_RESOLVE_REINSTALL=fresh`.
663
664
 
664
665
  Skip the migration entirely with:
665
666
 
@@ -748,6 +749,9 @@ In this setup, `plan` uses `openai/gpt-5.3-codex`; native `build`, resolve `code
748
749
  | `strong` | Provider-neutral alias for a strong/expensive model |
749
750
  | `mini` | Provider-neutral alias for a mini/efficient model |
750
751
  | `codex` | Provider-neutral alias for a codex-style coding model |
752
+ | `bronze` / `silver` / `gold` | Three-tier scout / coder / reasoner aliases |
753
+ | `gpt-bronze` / `gpt-silver` / `gpt-gold` | GPT/Codex-specific three-tier aliases for mixed setups |
754
+ | `glm-bronze` / `glm-silver` / `glm-gold` | GLM-specific three-tier aliases for mixed setups |
751
755
  | `quick` | Legacy alias (equivalent to `fast`) |
752
756
  | `deep` | Legacy alias (equivalent to `strong`) |
753
757
  | `glm` | Legacy alias (backward compatibility) |
@@ -762,6 +766,8 @@ Aliases only resolve when defined in `models`. Agent names (`coder`, `resolver`,
762
766
  | Agent | Default | Mode | Edit | Bash | WebFetch | Purpose |
763
767
  |---|:---:|---|---|---|---|---|
764
768
  | `resolver` | Yes (core) | `all` | allow | ask (classifier-routed) | allow | Context-efficient orchestrator. Decomposes work into verified checkpoints, dispatches coder, verifies each, and reports blockers when repeated recovery fails. |
769
+ | `codex` | No | `all` | allow | ask (classifier-routed) | allow | Codex-optimized primary resolver with the same verified resolve-loop style as `resolver`. Enabled by first-install GPT/mix presets or explicitly. |
770
+ | `glm` | No | `all` | allow | ask (classifier-routed) | allow | GLM-optimized primary resolver with the same verified resolve-loop style as `resolver`. Enabled by first-install GLM/mix presets or explicitly. |
765
771
  | `coder` | Yes (core) | `subagent` | allow | ask (classifier-routed) | allow | Focused implementer. Smallest correct patch. Reads only needed files. |
766
772
  | `explorer` | Yes (subagent) | `subagent` | **deny** | **deny** | allow | Internal fast codebase scout. Resolver dispatches when scope is genuinely unknown; prefers local read/grep/glob for narrow scope. |
767
773
  | `reviewer` | Yes (subagent) | `subagent` | **deny** | **deny** | allow | Internal verification-gap auditor. Resolver dispatches for post-change verification gaps on non-trivial changes. |
@@ -784,7 +790,7 @@ Supported modes:
784
790
 
785
791
  Supported permission values: `ask`, `allow`, `deny`.
786
792
 
787
- Supported model alias keys: `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`, and every supported agent name. Aliases only resolve when defined in `models`.
793
+ Supported model alias keys: `fast`, `strong`, `mini`, `codex`, `quick`, `deep`, `glm`, `gpt`, `bronze`, `silver`, `gold`, `gpt-bronze`, `gpt-silver`, `gpt-gold`, `glm-bronze`, `glm-silver`, `glm-gold`, and every supported agent name. Aliases only resolve when defined in `models`.
788
794
 
789
795
  `preserveNative` is accepted for readability, but native `plan` and `build` are always preserved. The plugin never rewrites built-in OpenCode agents.
790
796
 
@@ -875,12 +881,20 @@ From this repository:
875
881
 
876
882
  ```sh
877
883
  npm install
884
+ npm run hooks:install
878
885
  npm test
879
886
  npm run install:local
880
887
  ```
881
888
 
882
889
  `install:local` builds the plugin, links it into the OpenCode global plugin directory, and creates `~/.config/opencode/resolve.json` if it does not exist.
883
890
 
891
+ `hooks:install` sets this checkout's `core.hooksPath` to `.githooks`. The tracked hooks are intentionally strict:
892
+
893
+ | Hook | Gate |
894
+ |---|---|
895
+ | `pre-commit` | `npm run typecheck`, `npm test`, `npm run coverage` |
896
+ | `pre-push` | full `pre-commit` gate plus `npm pack --dry-run` before anything reaches the remote |
897
+
884
898
  Manual local install:
885
899
 
886
900
  ```sh
@@ -900,6 +914,7 @@ Run the normal checks:
900
914
  ```sh
901
915
  npm run typecheck
902
916
  npm test
917
+ npm run coverage
903
918
  npm run build
904
919
  ```
905
920
 
@@ -910,6 +925,7 @@ Before publishing:
910
925
  ```sh
911
926
  npm run typecheck
912
927
  npm test
928
+ npm run coverage
913
929
  npm audit --audit-level=moderate
914
930
  npm publish --dry-run
915
931
  ```
package/dist/agents.d.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  import { ResolveAgentName, ResolveAgentConfig, ModelAlias, TierName } from "./types.js";
2
2
  export declare const DEFAULT_MODELS: Partial<Record<ModelAlias, string>>;
3
3
  export declare const DEFAULT_ENABLED: ResolveAgentName[];
4
- export declare const VALID_AGENT_NAMES: readonly ["coder", "reviewer", "resolver", "glm", "architect", "gpt-coder", "debugger", "researcher", "explorer", "deep-reviewer", "planner"];
4
+ export declare const VALID_AGENT_NAMES: readonly ["coder", "reviewer", "resolver", "codex", "glm", "architect", "gpt-coder", "debugger", "researcher", "explorer", "deep-reviewer", "planner"];
5
5
  export declare const VALID_AGENT_NAME_SET: Set<string>;
6
6
  export declare const DEFAULT_AGENT_CONFIG: Record<ResolveAgentName, Required<Pick<ResolveAgentConfig, "mode" | "description" | "prompt" | "color">> & ResolveAgentConfig>;
7
7
  export declare const GLM_CODER_PROMPT: string;
8
8
  export declare const GPT_CODER_PROMPT: string;
9
9
  export declare function buildGLMResolverPrompt(maxParallelSubagents: number | undefined): string;
10
10
  export declare function buildGPTResolverPrompt(): string;
11
+ export declare function buildCodexResolverPrompt(): string;
11
12
  export declare function buildResolverPrompt(maxParallelSubagents: number | undefined): string;
12
- export declare const VALID_MODEL_ALIASES: readonly ["coder", "reviewer", "resolver", "glm", "architect", "gpt-coder", "debugger", "researcher", "explorer", "deep-reviewer", "planner", "glm", "gpt", "quick", "deep", "fast", "strong", "mini", "codex", "bronze", "silver", "gold"];
13
+ export declare const VALID_MODEL_ALIASES: readonly ["coder", "reviewer", "resolver", "codex", "glm", "architect", "gpt-coder", "debugger", "researcher", "explorer", "deep-reviewer", "planner", "glm", "gpt", "quick", "deep", "fast", "strong", "mini", "codex", "bronze", "silver", "gold", "gpt-bronze", "gpt-silver", "gpt-gold", "glm-bronze", "glm-silver", "glm-gold"];
13
14
  export declare const VALID_MODEL_ALIAS_SET: Set<string>;
14
15
  export declare const VALID_PROFILES: Set<string>;
15
16
  export declare const VALID_TIERS: Set<string>;
package/dist/agents.js CHANGED
@@ -4,6 +4,7 @@ export const VALID_AGENT_NAMES = [
4
4
  "coder",
5
5
  "reviewer",
6
6
  "resolver",
7
+ "codex",
7
8
  "glm",
8
9
  "architect",
9
10
  "gpt-coder",
@@ -72,6 +73,18 @@ export const DEFAULT_AGENT_CONFIG = {
72
73
  webfetch: "allow",
73
74
  },
74
75
  },
76
+ codex: {
77
+ mode: "all",
78
+ color: "#FFB347",
79
+ maxSteps: 35,
80
+ description: "Codex-optimized primary resolver for agentic coding work. Use when a Codex-style OpenAI coding model is pinned and you want a dedicated user-facing route separate from the neutral resolver.",
81
+ prompt: buildCodexResolverPrompt(),
82
+ permission: {
83
+ edit: "allow",
84
+ bash: "ask",
85
+ webfetch: "allow",
86
+ },
87
+ },
75
88
  architect: {
76
89
  mode: "subagent",
77
90
  color: "#00BFFF",
@@ -282,6 +295,23 @@ export function buildGPTResolverPrompt() {
282
295
  "Specialists: explorer (scope unknown), reviewer (verification gap), deep-reviewer (risky/security/architectural), debugger (verify failure diagnosis), planner (user asks for plan).",
283
296
  ].join("\n");
284
297
  }
298
+ export function buildCodexResolverPrompt() {
299
+ return [
300
+ "You are Codex Resolver, the OpenAI Codex-optimized primary agent for OpenCode Resolve.",
301
+ "Use Codex-style software engineering judgment: inspect narrowly, patch directly when trivial, dispatch focused coders when parallel work is useful, and verify before reporting.",
302
+ "",
303
+ "Prefer coder for implementation slices, explorer for quick scope discovery, reviewer for verification gaps, deep-reviewer for risky/security/architecture changes, debugger for verify failure diagnosis, planner only when the user asks for a plan.",
304
+ "Dispatch coder with: TASK (atomic goal), OUTCOME (success criteria), MUST DO, MUST NOT DO, CONTEXT (files/patterns).",
305
+ "After EVERY coder return: verify it works + follows codebase patterns. If not → re-dispatch with precise fix.",
306
+ "INTELLIGENT RECOVERY: On verify failure, dispatch debugger FIRST to diagnose root cause, THEN re-dispatch coder with precise fix. Do NOT blindly retry.",
307
+ "Trivial fixes → apply yourself. No subagent needed.",
308
+ "3 consecutive failures → STOP, REVERT, REPORT, ASK user.",
309
+ "",
310
+ "Verify: type check or lint MUST pass on changed files. Check LSP diagnostics when available. NO EVIDENCE = NOT COMPLETE.",
311
+ "",
312
+ "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request.",
313
+ ].join("\n");
314
+ }
285
315
  export function buildResolverPrompt(maxParallelSubagents) {
286
316
  const explicitLimit = typeof maxParallelSubagents === "number" && Number.isFinite(maxParallelSubagents)
287
317
  ? Math.max(1, Math.trunc(maxParallelSubagents))
@@ -327,16 +357,22 @@ export const VALID_MODEL_ALIASES = [
327
357
  "bronze",
328
358
  "silver",
329
359
  "gold",
360
+ "gpt-bronze",
361
+ "gpt-silver",
362
+ "gpt-gold",
363
+ "glm-bronze",
364
+ "glm-silver",
365
+ "glm-gold",
330
366
  ];
331
367
  export const VALID_MODEL_ALIAS_SET = new Set(VALID_MODEL_ALIASES);
332
368
  export const VALID_PROFILES = new Set(["mix", "glm", "gpt"]);
333
369
  export const VALID_TIERS = new Set(["bronze", "silver", "gold"]);
334
- export const GLM_ENABLED = ["coder", "resolver", "explorer", "reviewer", "planner"];
335
- export const GPT_ENABLED = ["coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner"];
370
+ export const GLM_ENABLED = ["coder", "resolver", "glm", "explorer", "reviewer", "planner"];
371
+ export const GPT_ENABLED = ["coder", "resolver", "codex", "explorer", "reviewer", "deep-reviewer", "planner"];
336
372
  export const TIER_ENABLED = {
337
373
  bronze: ["coder", "resolver"],
338
374
  silver: ["coder", "resolver", "explorer", "reviewer", "planner"],
339
- gold: ["coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner", "debugger", "researcher"],
375
+ gold: ["coder", "resolver", "codex", "glm", "explorer", "reviewer", "deep-reviewer", "planner", "debugger", "researcher"],
340
376
  };
341
377
  export const GLM_AGENT_OVERRIDES = {
342
378
  coder: { maxSteps: 15 },
@@ -348,6 +384,7 @@ export const GLM_AGENT_OVERRIDES = {
348
384
  export const GPT_AGENT_OVERRIDES = {
349
385
  coder: { maxSteps: 25 },
350
386
  resolver: { maxSteps: 40 },
387
+ codex: { maxSteps: 40 },
351
388
  explorer: { maxSteps: 8 },
352
389
  reviewer: { maxSteps: 10 },
353
390
  "deep-reviewer": { maxSteps: 15 },
package/dist/config.js CHANGED
@@ -47,7 +47,7 @@ export function applyResolveConfig(config, resolveConfig, projectContext) {
47
47
  agentConfig.prompt = buildResolverPrompt(maxParallelSubagents);
48
48
  }
49
49
  // Inject project context into all resolver-type agents
50
- if ((name === "resolver" || name === "glm") && contextInjection) {
50
+ if ((name === "resolver" || name === "codex" || name === "glm") && contextInjection) {
51
51
  agentConfig.prompt = agentConfig.prompt + "\n\n" + contextInjection;
52
52
  }
53
53
  // Inject verify commands into coder prompts
@@ -153,7 +153,13 @@ export function mergeAgents(left, right) {
153
153
  export function resolveModel(model, models) {
154
154
  if (!model)
155
155
  return undefined;
156
- return models[model] ?? model;
156
+ let current = model;
157
+ const seen = new Set();
158
+ while (models[current] !== undefined && !seen.has(current)) {
159
+ seen.add(current);
160
+ current = models[current] ?? current;
161
+ }
162
+ return current;
157
163
  }
158
164
  export function buildPermission(basePermission, userPermission) {
159
165
  const merged = {
@@ -192,7 +192,7 @@ export function getHooks(directory, options, sessionState) {
192
192
  output.temperature = capTemperature(output.temperature, 0.3);
193
193
  }
194
194
  // Write agents: slightly higher temperature for creative problem-solving
195
- const writeAgents = new Set(["coder", "resolver", "glm", "gpt-coder"]);
195
+ const writeAgents = new Set(["coder", "resolver", "codex", "glm", "gpt-coder"]);
196
196
  if (writeAgents.has(input.agent) && output.temperature === undefined) {
197
197
  output.temperature = 0.5;
198
198
  }
@@ -5,7 +5,7 @@ import { writeFileSync, mkdirSync } from "node:fs";
5
5
  import { classifyBashCommand, runCommand, sanitizeShellArg, truncateOutput } from "../utils.js";
6
6
  import { DIAGNOSTICS_TTL_MS } from "../state.js";
7
7
  import { VALID_PROFILES, VALID_TIERS, VALID_AGENT_NAME_SET, VALID_AGENT_NAMES } from "../agents.js";
8
- const WRITE_CAPABLE_AGENTS = new Set(["resolver", "coder", "glm", "gpt-coder", "debugger"]);
8
+ const WRITE_CAPABLE_AGENTS = new Set(["resolver", "codex", "coder", "glm", "gpt-coder", "debugger"]);
9
9
  function canWriteFromTool(ctx) {
10
10
  return typeof ctx.agent !== "string" || WRITE_CAPABLE_AGENTS.has(ctx.agent);
11
11
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type PermissionValue = "ask" | "allow" | "deny";
2
- export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "glm" | "architect" | "gpt-coder" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
3
- export type ModelAlias = ResolveAgentName | "glm" | "gpt" | "quick" | "deep" | "fast" | "strong" | "mini" | "codex" | "bronze" | "silver" | "gold";
2
+ export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "codex" | "glm" | "architect" | "gpt-coder" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
3
+ export type ModelAlias = ResolveAgentName | "glm" | "gpt" | "quick" | "deep" | "fast" | "strong" | "mini" | "codex" | "bronze" | "silver" | "gold" | "gpt-bronze" | "gpt-silver" | "gpt-gold" | "glm-bronze" | "glm-silver" | "glm-gold";
4
4
  export type AgentMode = "subagent" | "primary" | "all";
5
5
  export type ResolveAgentConfig = {
6
6
  enabled?: boolean;
@@ -14,6 +14,9 @@
14
14
  "resolver": {
15
15
  "enabled": true
16
16
  },
17
+ "codex": {
18
+ "enabled": false
19
+ },
17
20
  "explorer": {
18
21
  "enabled": true,
19
22
  "mode": "subagent"
@@ -119,9 +119,12 @@
119
119
  // -----------------------------------------------------------------------
120
120
  // models (object)
121
121
  // Alias map. Keys are either an agent name (coder, reviewer, resolver,
122
- // architect, gpt-coder, debugger, researcher, explorer, deep-reviewer)
122
+ // codex, glm, architect, gpt-coder, debugger, researcher, explorer,
123
+ // deep-reviewer, planner)
123
124
  // OR one of the well-known aliases (`glm`, `gpt`, `quick`, `deep`,
124
- // `fast`, `strong`, `mini`, `codex`).
125
+ // `fast`, `strong`, `mini`, `codex`, `bronze`, `silver`, `gold`,
126
+ // `gpt-bronze`, `gpt-silver`, `gpt-gold`, `glm-bronze`, `glm-silver`,
127
+ // `glm-gold`).
125
128
  // Values are model identifiers OR another alias.
126
129
  //
127
130
  // Resolution order for a given agent:
@@ -137,11 +140,17 @@
137
140
  // When resolve.json is created for the first time by postinstall, the
138
141
  // plugin inspects your OpenCode model config and writes an adaptive
139
142
  // preset:
140
- // - GLM/ZAI detectedGLM-only aliases (no GPT dependency)
141
- // - OpenAI/GPT detected single-provider GPT aliases using your model
142
- // - Other or no model empty (model-neutral inheritance)
143
- // Existing resolve.json files are never overwritten. Delete resolve.json
144
- // and reinstall to regenerate the preset.
143
+ // - interactive terminalalways prompts for mix/gpt/glm, three-tier models,
144
+ // and optional dedicated codex/glm primary agents
145
+ // - non-interactive install no guessed model pinning; profile mix,
146
+ // models {}, and resolver/codex/glm primary routes enabled
147
+ // - OPENCODE_RESOLVE_AUTO_PRESET=1 legacy non-interactive
148
+ // provider-adapted presets
149
+ // Existing resolve.json files are never overwritten without consent.
150
+ // Interactive reinstall asks whether to update or back up and run fresh
151
+ // setup. Non-interactive automation can set:
152
+ // OPENCODE_RESOLVE_REINSTALL=fresh
153
+ // OPENCODE_RESOLVE_REINSTALL=update
145
154
  //
146
155
  // Example — one efficient model for everything (recommended default):
147
156
  // "models": {}
@@ -154,19 +163,22 @@
154
163
  // "resolver": "strong"
155
164
  // }
156
165
  //
157
- // Example — GLM-only preset (auto-generated for GLM/ZAI users):
158
- // All agents use GLM — no GPT dependency, so token exhaustion on
166
+ // Example — GLM-only three-tier preset (auto-generated for GLM/ZAI users):
167
+ // GLM fills all tiers — no GPT dependency, so token exhaustion on
159
168
  // third-party providers never blocks agent execution.
160
169
  // "models": {
161
170
  // "glm": "zai-coding-plan/glm-5.1",
162
- // "fast": "glm",
163
- // "strong": "glm",
164
- // "coder": "glm",
165
- // "resolver": "glm",
166
- // "reviewer": "glm",
167
- // "deep-reviewer": "glm",
168
- // "explorer": "fast",
169
- // "planner": "glm"
171
+ // "bronze": "zai-coding-plan/glm-5.1",
172
+ // "silver": "zai-coding-plan/glm-5.1",
173
+ // "gold": "zai-coding-plan/glm-5.1",
174
+ // "fast": "bronze",
175
+ // "strong": "gold",
176
+ // "coder": "silver",
177
+ // "resolver": "gold",
178
+ // "reviewer": "gold",
179
+ // "deep-reviewer": "gold",
180
+ // "explorer": "bronze",
181
+ // "planner": "gold"
170
182
  // }
171
183
  //
172
184
  // Legacy aliases (glm, gpt, quick, deep) remain supported for backward
@@ -209,6 +221,15 @@
209
221
  }
210
222
  },
211
223
 
224
+ // codex — Codex-optimized primary resolver (selectable in agent picker)
225
+ // Same verified resolve-loop style as resolver, with a Codex-specific
226
+ // system prompt. Enabled by GPT/mix first-install presets when selected.
227
+ "codex": {
228
+ "enabled": false,
229
+ "mode": "all",
230
+ "maxSteps": 35
231
+ },
232
+
212
233
  // Internal specialist subagents — enabled by default as subagents only.
213
234
  // Core path is resolver→coder. These are dispatched only when justified.
214
235
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-resolve",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "OpenCode plugin that adds a lightweight resolver/coder harness for continuous agentic coding.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/jshsakura/opencode-resolve#readme",
@@ -30,9 +30,13 @@
30
30
  ],
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.json",
33
+ "coverage": "npm run build && node --experimental-test-coverage --test-coverage-lines=79 --test-coverage-branches=69 --test-coverage-functions=85 --test test/*.mjs",
34
+ "hooks:install": "node scripts/install-git-hooks.mjs",
33
35
  "install:local": "npm run build && node scripts/install-local.mjs",
34
36
  "postinstall": "node scripts/postinstall.mjs",
35
37
  "prepack": "npm test",
38
+ "precommit": "npm run typecheck && npm test && npm run coverage",
39
+ "prepush": "npm run precommit && npm pack --dry-run",
36
40
  "test": "npm run build && node --test test/*.mjs",
37
41
  "typecheck": "tsc -p tsconfig.json --noEmit"
38
42
  },
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process"
3
+ import { chmodSync, existsSync } from "node:fs"
4
+ import { dirname, join } from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+
7
+ const root = dirname(dirname(fileURLToPath(import.meta.url)))
8
+ const hooksDir = join(root, ".githooks")
9
+
10
+ if (!existsSync(join(root, ".git"))) {
11
+ console.error("[opencode-resolve] not a git checkout; cannot install hooks")
12
+ process.exit(1)
13
+ }
14
+
15
+ for (const hook of ["pre-commit", "pre-push"]) {
16
+ chmodSync(join(hooksDir, hook), 0o755)
17
+ }
18
+
19
+ const result = spawnSync("git", ["config", "core.hooksPath", ".githooks"], {
20
+ cwd: root,
21
+ stdio: "inherit",
22
+ })
23
+
24
+ if (result.status !== 0) {
25
+ process.exit(result.status ?? 1)
26
+ }
27
+
28
+ console.log("[opencode-resolve] git hooks installed: core.hooksPath=.githooks")
@@ -2,6 +2,7 @@ import { constants } from "node:fs"
2
2
  import { access, mkdir, readFile, symlink, unlink, writeFile } from "node:fs/promises"
3
3
  import { homedir } from "node:os"
4
4
  import { dirname, join, resolve } from "node:path"
5
+ import { createInterface } from "node:readline/promises"
5
6
  import { fileURLToPath } from "node:url"
6
7
 
7
8
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..")
@@ -25,11 +26,58 @@ await symlink(pluginTarget, pluginLink)
25
26
 
26
27
  if (!(await exists(resolveConfigPath))) {
27
28
  await createAdaptiveResolveConfig()
29
+ } else {
30
+ await handleExistingResolveConfig()
28
31
  }
29
32
 
30
33
  console.log(`Linked plugin: ${pluginLink} -> ${pluginTarget}`)
31
34
  console.log(`Resolve config: ${resolveConfigPath}`)
32
35
 
36
+ async function handleExistingResolveConfig() {
37
+ const action = await chooseExistingResolveConfigAction()
38
+ if (action !== "fresh") {
39
+ console.log(`Existing resolve config preserved: ${resolveConfigPath}`)
40
+ return
41
+ }
42
+
43
+ await backupResolveConfig()
44
+ await createAdaptiveResolveConfig()
45
+ }
46
+
47
+ async function chooseExistingResolveConfigAction() {
48
+ const requested = (process.env.OPENCODE_RESOLVE_REINSTALL ?? "").trim().toLowerCase()
49
+ if (["fresh", "reset", "recreate", "new"].includes(requested)) return "fresh"
50
+ if (["update", "keep", "migrate", "preserve"].includes(requested)) return "update"
51
+ if (requested) {
52
+ console.warn(`Ignoring unknown OPENCODE_RESOLVE_REINSTALL=${JSON.stringify(requested)}; use "fresh" or "update".`)
53
+ }
54
+
55
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
56
+ console.log(`Existing resolve config found; preserving it. Set OPENCODE_RESOLVE_REINSTALL=fresh for a fresh reinstall.`)
57
+ return "update"
58
+ }
59
+
60
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
61
+ try {
62
+ console.log("")
63
+ console.log(`Existing resolve config found: ${resolveConfigPath}`)
64
+ console.log(" 1. update existing config — preserve your settings")
65
+ console.log(" 2. fresh reinstall — back up resolve.json and create a new config")
66
+ const raw = await rl.question("Existing config [1=update, 2=fresh reinstall, default 1]: ")
67
+ return raw.trim() === "2" ? "fresh" : "update"
68
+ } finally {
69
+ rl.close()
70
+ }
71
+ }
72
+
73
+ async function backupResolveConfig() {
74
+ const raw = await readFile(resolveConfigPath, "utf8")
75
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
76
+ const backupPath = `${resolveConfigPath}.bak.${stamp}`
77
+ await writeFile(backupPath, raw)
78
+ console.log(`Backed up existing resolve config to ${backupPath}`)
79
+ }
80
+
33
81
  async function createAdaptiveResolveConfig() {
34
82
  const raw = await readFile(exampleConfig, "utf8")
35
83
  const example = JSON.parse(raw)
@@ -42,10 +90,20 @@ async function createAdaptiveResolveConfig() {
42
90
  // opencode.json not found or unreadable — use empty config
43
91
  }
44
92
 
45
- const currentModel = detectOpenCodeModel(opencodeConfig)
46
- const preset = buildModelPreset(currentModel)
47
-
48
93
  const resolveConfig = { ...example }
94
+ let preset = {}
95
+ if (process.env.OPENCODE_RESOLVE_AUTO_PRESET === "1") {
96
+ const currentModel = detectOpenCodeModel(opencodeConfig)
97
+ preset = buildModelPreset(currentModel)
98
+ } else {
99
+ resolveConfig.profile = "mix"
100
+ resolveConfig.models = {}
101
+ resolveConfig.agents = {
102
+ ...resolveConfig.agents,
103
+ codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
104
+ glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
105
+ }
106
+ }
49
107
  if (preset && Object.keys(preset).length > 0) {
50
108
  resolveConfig.models = preset
51
109
  }
@@ -53,7 +111,9 @@ async function createAdaptiveResolveConfig() {
53
111
  await mkdir(configDir, { recursive: true })
54
112
  await writeFile(resolveConfigPath, `${JSON.stringify(resolveConfig, null, 2)}\n`)
55
113
 
56
- const label = getPresetLabel(currentModel)
114
+ const label = process.env.OPENCODE_RESOLVE_AUTO_PRESET === "1"
115
+ ? getPresetLabel(detectOpenCodeModel(opencodeConfig))
116
+ : "prompt-required"
57
117
  console.log(`Created ${resolveConfigPath} (preset: ${label})`)
58
118
  }
59
119
 
@@ -41,6 +41,26 @@ const COMPANION_PLUGINS = [
41
41
  },
42
42
  ]
43
43
 
44
+ const OPENAI_MODEL_HINTS = [
45
+ "openai/gpt-5.5",
46
+ "openai/gpt-5.4",
47
+ "openai/gpt-5.3-codex",
48
+ "openai/gpt-5.3-codex-spark",
49
+ "openai/gpt-5.2",
50
+ "openai/gpt-5-mini",
51
+ "openai/gpt-4o-mini",
52
+ ]
53
+
54
+ const GLM_MODEL_HINTS = [
55
+ "zai-coding-plan/glm-5.1",
56
+ "zai/glm-5.1",
57
+ "zai/glm-5",
58
+ "zai/glm-4.7-flash",
59
+ "zai/glm-4.5-flash",
60
+ "zai/glm-4.5",
61
+ "zai/glm-4.5-air",
62
+ ]
63
+
44
64
  if (process.env.OPENCODE_RESOLVE_SKIP_POSTINSTALL === "1") {
45
65
  process.exit(0)
46
66
  }
@@ -69,6 +89,7 @@ async function readOwnVersion() {
69
89
 
70
90
  async function registerPlugin() {
71
91
  await mkdir(configDir, { recursive: true })
92
+ const scriptedAnswers = await readScriptedAnswersIfNeeded()
72
93
 
73
94
  const config = await readOpenCodeConfig()
74
95
  let configChanged = addPlugin(config)
@@ -89,14 +110,62 @@ async function registerPlugin() {
89
110
  }
90
111
 
91
112
  if (!(await exists(resolveConfigPath))) {
92
- await createAdaptiveResolveConfig(config)
113
+ await createAdaptiveResolveConfig(config, scriptedAnswers)
114
+ return
115
+ }
116
+
117
+ await handleExistingResolveConfig(config, scriptedAnswers)
118
+ }
119
+
120
+ async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
121
+ const action = await chooseExistingResolveConfigAction(scriptedAnswers)
122
+ if (action === "fresh") {
123
+ await backupResolveConfig()
124
+ await createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers)
93
125
  return
94
126
  }
95
127
 
96
128
  await migrateResolveConfig()
97
129
  }
98
130
 
99
- async function createAdaptiveResolveConfig(opencodeConfig) {
131
+ async function chooseExistingResolveConfigAction(scriptedAnswers) {
132
+ const requested = (process.env.OPENCODE_RESOLVE_REINSTALL ?? "").trim().toLowerCase()
133
+ if (["fresh", "reset", "recreate", "new"].includes(requested)) return "fresh"
134
+ if (["update", "keep", "migrate", "preserve"].includes(requested)) return "update"
135
+ if (requested) {
136
+ console.warn(`[${packageName}] ignoring unknown OPENCODE_RESOLVE_REINSTALL=${JSON.stringify(requested)}; use "fresh" or "update".`)
137
+ }
138
+
139
+ const forcePrompt = process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1"
140
+ const canPrompt = Boolean((process.stdin.isTTY && process.stdout.isTTY) || forcePrompt)
141
+ if (!canPrompt) {
142
+ console.log(`[${packageName}] existing ${resolveConfigPath} found; preserving it and applying additive updates.`)
143
+ console.log(`[${packageName}] for a fresh reinstall, rerun in a TTY or set OPENCODE_RESOLVE_REINSTALL=fresh.`)
144
+ return "update"
145
+ }
146
+
147
+ const rl = createPromptInterface(scriptedAnswers)
148
+ try {
149
+ console.log("")
150
+ console.log(`[${packageName}] Existing resolve config found: ${resolveConfigPath}`)
151
+ console.log(" 1. update existing config — preserve your settings and add missing defaults")
152
+ console.log(" 2. fresh reinstall — back up resolve.json and run setup again")
153
+ const answer = await askChoice(rl, "Existing config [1=update, 2=fresh reinstall, default 1]: ", ["1", "2"], "1")
154
+ return answer === "2" ? "fresh" : "update"
155
+ } finally {
156
+ rl.close()
157
+ }
158
+ }
159
+
160
+ async function backupResolveConfig() {
161
+ const raw = await readFile(resolveConfigPath, "utf8")
162
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
163
+ const backupPath = `${resolveConfigPath}.bak.${stamp}`
164
+ await writeFile(backupPath, raw)
165
+ console.log(`[${packageName}] backed up existing resolve config to ${backupPath}`)
166
+ }
167
+
168
+ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers) {
100
169
  await assertReadable(exampleConfigPath)
101
170
  const raw = await readFile(exampleConfigPath, "utf8")
102
171
  const example = JSON.parse(raw)
@@ -104,6 +173,44 @@ async function createAdaptiveResolveConfig(opencodeConfig) {
104
173
  const currentModel = detectOpenCodeModel(opencodeConfig)
105
174
  const allModels = detectAllModels(opencodeConfig)
106
175
  const resolveConfig = { ...example }
176
+ const forcePrompt = process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1"
177
+ const canPrompt = Boolean(
178
+ (process.stdin.isTTY && process.stdout.isTTY) || forcePrompt,
179
+ )
180
+ const interactivePreset = canPrompt
181
+ ? await buildInteractivePreset(currentModel, allModels, scriptedAnswers)
182
+ : undefined
183
+
184
+ if (interactivePreset) {
185
+ resolveConfig.profile = interactivePreset.profile
186
+ if (interactivePreset.tier) resolveConfig.tier = interactivePreset.tier
187
+ else delete resolveConfig.tier
188
+ if (interactivePreset.enabled) resolveConfig.enabled = interactivePreset.enabled
189
+ resolveConfig.models = interactivePreset.models
190
+ resolveConfig.agents = {
191
+ ...resolveConfig.agents,
192
+ ...(interactivePreset.agents ?? {}),
193
+ }
194
+ await writeFile(resolveConfigPath, `${JSON.stringify(resolveConfig, null, 2)}\n`)
195
+ console.log(`[${packageName}] created ${resolveConfigPath} (preset: ${interactivePreset.label})`)
196
+ return
197
+ }
198
+
199
+ if (process.env.OPENCODE_RESOLVE_AUTO_PRESET !== "1") {
200
+ resolveConfig.profile = "mix"
201
+ delete resolveConfig.tier
202
+ resolveConfig.models = {}
203
+ resolveConfig.agents = {
204
+ ...resolveConfig.agents,
205
+ codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
206
+ glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
207
+ }
208
+ await writeFile(resolveConfigPath, `${JSON.stringify(resolveConfig, null, 2)}\n`)
209
+ console.log(`[${packageName}] created ${resolveConfigPath} (preset: prompt-required)`)
210
+ console.log(`[${packageName}] model pinning was not guessed because no interactive prompt was available.`)
211
+ console.log(`[${packageName}] rerun setup in a TTY or edit ${resolveConfigPath} to choose GPT/GLM three-tier models.`)
212
+ return
213
+ }
107
214
 
108
215
  // Profile selection based on detected providers
109
216
  const hasGLM = allModels.some((m) => isGLMModel(m))
@@ -114,13 +221,28 @@ async function createAdaptiveResolveConfig(opencodeConfig) {
114
221
  // GLM only → GLM profile, silver tier (token-efficient, no deep-reviewer)
115
222
  resolveConfig.profile = "glm"
116
223
  resolveConfig.tier = "silver"
224
+ resolveConfig.agents = {
225
+ ...resolveConfig.agents,
226
+ glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
227
+ }
117
228
  } else if (hasGPT && !hasGLM) {
118
229
  // GPT only → GPT profile, gold tier (full power)
119
230
  resolveConfig.profile = "gpt"
120
231
  resolveConfig.tier = "gold"
232
+ resolveConfig.agents = {
233
+ ...resolveConfig.agents,
234
+ codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
235
+ }
121
236
  } else {
122
237
  // Mixed or unknown provider → explicit mix profile. No tier: use DEFAULT_ENABLED.
123
238
  resolveConfig.profile = "mix"
239
+ if (hasGLM && hasGPT) {
240
+ resolveConfig.agents = {
241
+ ...resolveConfig.agents,
242
+ codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
243
+ glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
244
+ }
245
+ }
124
246
  }
125
247
 
126
248
  if (preset && Object.keys(preset).length > 0) {
@@ -161,21 +283,33 @@ function detectOpenCodeModel(config) {
161
283
  }
162
284
 
163
285
  function buildModelPreset(currentModel, allModels = []) {
164
- const glmModel = allModels.find((m) => isGLMModel(m))
165
- const gptModel = allModels.find((m) => isGPTModel(m))
286
+ const detectedGLMModels = allModels.filter(isGLMModel)
287
+ const detectedGPTModels = allModels.filter(isGPTModel)
288
+ const glmModels = detectedGLMModels.length > 0 ? collectModelChoices(detectedGLMModels, isGLMModel, GLM_MODEL_HINTS, false) : []
289
+ const gptModels = detectedGPTModels.length > 0 ? collectModelChoices(detectedGPTModels, isGPTModel, OPENAI_MODEL_HINTS, false) : []
290
+ const glmModel = glmModels[0]
291
+ const gptModel = gptModels[0]
166
292
 
167
293
  if (glmModel && gptModel) {
294
+ const glmTiers = chooseThreeTier(glmModels, "glm", false)
295
+ const gptTiers = chooseThreeTier(gptModels, "gpt", false)
168
296
  return {
169
297
  mix: "gpt",
170
- glm: glmModel,
171
- gpt: gptModel,
172
- bronze: "glm",
173
- silver: "glm",
174
- gold: "gpt",
175
- fast: "glm",
176
- strong: "gpt",
177
- mini: "glm",
178
- codex: "gpt",
298
+ gpt: gptTiers.gold,
299
+ bronze: glmTiers.bronze,
300
+ silver: glmTiers.silver,
301
+ gold: gptTiers.gold,
302
+ "glm-bronze": glmTiers.bronze,
303
+ "glm-silver": glmTiers.silver,
304
+ "glm-gold": glmTiers.gold,
305
+ "gpt-bronze": gptTiers.bronze,
306
+ "gpt-silver": gptTiers.silver,
307
+ "gpt-gold": gptTiers.gold,
308
+ fast: "bronze",
309
+ strong: "gold",
310
+ mini: "bronze",
311
+ codex: "gpt-gold",
312
+ glm: glmTiers.gold,
179
313
  explorer: "bronze",
180
314
  coder: "silver",
181
315
  resolver: "gold",
@@ -186,8 +320,6 @@ function buildModelPreset(currentModel, allModels = []) {
186
320
  }
187
321
 
188
322
  if (!currentModel) {
189
- if (glmModel) return buildGLMOnlyPreset(glmModel)
190
- if (gptModel) return buildGPTOnlyPreset(gptModel)
191
323
  return {}
192
324
  }
193
325
 
@@ -208,32 +340,256 @@ function buildModelPreset(currentModel, allModels = []) {
208
340
  }
209
341
 
210
342
  function buildGLMOnlyPreset(model) {
343
+ const tiers = chooseThreeTier(collectModelChoices([model], isGLMModel, GLM_MODEL_HINTS, false), "glm", false)
211
344
  return {
212
- glm: model,
213
- fast: "glm",
214
- strong: "glm",
215
- coder: "glm",
216
- resolver: "glm",
217
- reviewer: "glm",
218
- "deep-reviewer": "glm",
219
- explorer: "fast",
220
- planner: "glm",
345
+ glm: tiers.gold,
346
+ bronze: tiers.bronze,
347
+ silver: tiers.silver,
348
+ gold: tiers.gold,
349
+ fast: "bronze",
350
+ strong: "gold",
351
+ mini: "bronze",
352
+ coder: "silver",
353
+ resolver: "gold",
354
+ reviewer: "gold",
355
+ "deep-reviewer": "gold",
356
+ explorer: "bronze",
357
+ planner: "gold",
221
358
  }
222
359
  }
223
360
 
224
361
  function buildGPTOnlyPreset(model) {
362
+ const tiers = chooseThreeTier(collectModelChoices([model], isGPTModel, OPENAI_MODEL_HINTS, false), "gpt", false)
363
+ return {
364
+ gpt: tiers.gold,
365
+ bronze: tiers.bronze,
366
+ silver: tiers.silver,
367
+ gold: tiers.gold,
368
+ fast: "bronze",
369
+ strong: "gold",
370
+ mini: "bronze",
371
+ codex: "gold",
372
+ coder: "silver",
373
+ resolver: "gold",
374
+ explorer: "bronze",
375
+ reviewer: "gold",
376
+ "deep-reviewer": "gold",
377
+ }
378
+ }
379
+
380
+ async function buildInteractivePreset(currentModel, allModels, scriptedAnswers) {
381
+ const choices = {
382
+ gpt: collectModelChoices(allModels, isGPTModel, OPENAI_MODEL_HINTS),
383
+ glm: collectModelChoices(allModels, isGLMModel, GLM_MODEL_HINTS),
384
+ }
385
+ if (currentModel) {
386
+ if (isGPTModel(currentModel)) choices.gpt = unique([currentModel, ...choices.gpt])
387
+ if (isGLMModel(currentModel)) choices.glm = unique([currentModel, ...choices.glm])
388
+ }
389
+
390
+ const rl = createPromptInterface(scriptedAnswers)
391
+ try {
392
+ console.log("")
393
+ console.log(`[${packageName}] Choose resolve profile:`)
394
+ console.log(" 1. mix — neutral resolver plus optional Codex and GLM primary agents")
395
+ console.log(" 2. gpt — GPT/Codex-only, three-tier")
396
+ console.log(" 3. glm — GLM-only, three-tier")
397
+ const profileAnswer = await askChoice(rl, "Profile [1=mix, 2=gpt, 3=glm, default 1]: ", ["1", "2", "3"], "1")
398
+ const profile = profileAnswer === "2" ? "gpt" : profileAnswer === "3" ? "glm" : "mix"
399
+
400
+ if (profile === "gpt") {
401
+ const tiers = await askThreeTier(rl, "GPT/Codex", choices.gpt)
402
+ return {
403
+ label: "gpt-three-tier",
404
+ profile: "gpt",
405
+ tier: "gold",
406
+ models: buildGPTThreeTierModels(tiers),
407
+ agents: { codex: { enabled: true } },
408
+ }
409
+ }
410
+
411
+ if (profile === "glm") {
412
+ const tiers = await askThreeTier(rl, "GLM", choices.glm)
413
+ return {
414
+ label: "glm-three-tier",
415
+ profile: "glm",
416
+ tier: "gold",
417
+ models: buildGLMThreeTierModels(tiers),
418
+ agents: { glm: { enabled: true } },
419
+ }
420
+ }
421
+
422
+ const useCodex = await askYesNo(rl, "Enable dedicated Codex primary agent too? [Y/n]: ", true)
423
+ const useGLM = await askYesNo(rl, "Enable dedicated GLM primary agent too? [Y/n]: ", true)
424
+ const gptTiers = await askThreeTier(rl, "Codex/GPT", choices.gpt)
425
+ const glmTiers = await askThreeTier(rl, "GLM", choices.glm)
426
+ return {
427
+ label: "mix-three-tier",
428
+ profile: "mix",
429
+ models: buildMixedThreeTierModels(gptTiers, glmTiers),
430
+ agents: {
431
+ codex: { enabled: useCodex },
432
+ glm: { enabled: useGLM },
433
+ },
434
+ }
435
+ } finally {
436
+ rl.close()
437
+ }
438
+ }
439
+
440
+ function createPromptInterface(scriptedAnswers) {
441
+ return scriptedAnswers
442
+ ? {
443
+ async question(prompt) {
444
+ const answer = scriptedAnswers.length > 0 ? scriptedAnswers.shift() ?? "" : ""
445
+ process.stdout.write(prompt)
446
+ process.stdout.write(`${answer}\n`)
447
+ return answer
448
+ },
449
+ close() {},
450
+ }
451
+ : createInterface({ input: process.stdin, output: process.stdout })
452
+ }
453
+
454
+ async function askThreeTier(rl, label, models) {
455
+ const choices = models.length > 0 ? models : (label.toLowerCase().includes("glm") ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS)
456
+ console.log("")
457
+ console.log(`[${packageName}] ${label} model choices:`)
458
+ choices.forEach((model, index) => console.log(` ${index + 1}. ${model}`))
459
+ const defaults = chooseThreeTier(choices, label.toLowerCase().includes("glm") ? "glm" : "gpt")
460
+ const bronze = await askModel(rl, choices, `Pick ${label} bronze/scout [default ${defaults.bronze}]: `, defaults.bronze)
461
+ const silver = await askModel(rl, choices, `Pick ${label} silver/coder [default ${defaults.silver}]: `, defaults.silver)
462
+ const gold = await askModel(rl, choices, `Pick ${label} gold/reasoner [default ${defaults.gold}]: `, defaults.gold)
463
+ return { bronze, silver, gold }
464
+ }
465
+
466
+ async function askModel(rl, choices, question, defaultValue) {
467
+ const answer = (await rl.question(question)).trim()
468
+ if (!answer) return defaultValue
469
+ const index = Number.parseInt(answer, 10)
470
+ if (Number.isInteger(index) && index >= 1 && index <= choices.length) return choices[index - 1]
471
+ return answer
472
+ }
473
+
474
+ async function askChoice(rl, question, valid, defaultValue) {
475
+ const answer = (await rl.question(question)).trim().toLowerCase()
476
+ if (!answer) return defaultValue
477
+ return valid.includes(answer) ? answer : defaultValue
478
+ }
479
+
480
+ async function askYesNo(rl, question, defaultValue) {
481
+ const answer = (await rl.question(question)).trim().toLowerCase()
482
+ if (!answer) return defaultValue
483
+ return answer === "y" || answer === "yes"
484
+ }
485
+
486
+ function buildMixedThreeTierModels(gptTiers, glmTiers) {
487
+ return {
488
+ mix: "gpt-gold",
489
+ gpt: "gpt-gold",
490
+ glm: "glm-gold",
491
+ bronze: "glm-bronze",
492
+ silver: "glm-silver",
493
+ gold: "gpt-gold",
494
+ "gpt-bronze": gptTiers.bronze,
495
+ "gpt-silver": gptTiers.silver,
496
+ "gpt-gold": gptTiers.gold,
497
+ "glm-bronze": glmTiers.bronze,
498
+ "glm-silver": glmTiers.silver,
499
+ "glm-gold": glmTiers.gold,
500
+ fast: "bronze",
501
+ strong: "gold",
502
+ mini: "bronze",
503
+ codex: "gpt-gold",
504
+ explorer: "bronze",
505
+ coder: "silver",
506
+ resolver: "gold",
507
+ reviewer: "gold",
508
+ "deep-reviewer": "gold",
509
+ planner: "gold",
510
+ }
511
+ }
512
+
513
+ function buildGPTThreeTierModels(tiers) {
514
+ return {
515
+ gpt: "gold",
516
+ bronze: tiers.bronze,
517
+ silver: tiers.silver,
518
+ gold: tiers.gold,
519
+ fast: "bronze",
520
+ strong: "gold",
521
+ mini: "bronze",
522
+ codex: "gold",
523
+ explorer: "bronze",
524
+ coder: "silver",
525
+ resolver: "gold",
526
+ reviewer: "gold",
527
+ "deep-reviewer": "gold",
528
+ planner: "gold",
529
+ }
530
+ }
531
+
532
+ function buildGLMThreeTierModels(tiers) {
225
533
  return {
226
- gpt: model,
227
- fast: "gpt",
228
- strong: "gpt",
229
- mini: "gpt",
230
- codex: "gpt",
231
- coder: "gpt",
232
- resolver: "gpt",
233
- explorer: "gpt",
234
- reviewer: "gpt",
235
- "deep-reviewer": "gpt",
534
+ glm: "gold",
535
+ bronze: tiers.bronze,
536
+ silver: tiers.silver,
537
+ gold: tiers.gold,
538
+ fast: "bronze",
539
+ strong: "gold",
540
+ mini: "bronze",
541
+ explorer: "bronze",
542
+ coder: "silver",
543
+ resolver: "gold",
544
+ reviewer: "gold",
545
+ "deep-reviewer": "gold",
546
+ planner: "gold",
547
+ }
548
+ }
549
+
550
+ function collectModelChoices(allModels, predicate, hints, includeFallbackHints = true) {
551
+ const detected = allModels.filter(predicate)
552
+ const providerIds = new Set(detected.map((model) => model.split("/")[0]).filter(Boolean))
553
+ const matchingHints = includeFallbackHints
554
+ ? hints.filter((model) => providerIds.size === 0 || providerIds.has(model.split("/")[0]) || detected.length < 3)
555
+ : []
556
+ return unique([...detected, ...matchingHints])
557
+ }
558
+
559
+ function chooseThreeTier(models, family, includeFallbackHints = true) {
560
+ const fallback = family === "glm" ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS
561
+ const choices = unique(includeFallbackHints ? [...models, ...fallback] : models)
562
+ return {
563
+ bronze: preferModel(choices, family === "glm" ? ["flash", "air", "mini"] : ["spark", "mini", "4o-mini"], choices[0]),
564
+ silver: preferModel(choices, family === "glm" ? ["5.1", "4.5"] : ["codex", "5.3", "5.2"], choices[1] ?? choices[0]),
565
+ gold: preferModel(choices, family === "glm" ? ["5.1", "5", "4.5"] : ["5.5", "5.4", "gpt-5.3-codex"], choices[2] ?? choices[1] ?? choices[0]),
566
+ }
567
+ }
568
+
569
+ function preferModel(models, needles, fallback) {
570
+ return models.find((model) => {
571
+ const lower = model.toLowerCase()
572
+ return needles.some((needle) => lower.includes(needle))
573
+ }) ?? fallback
574
+ }
575
+
576
+ function unique(values) {
577
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))]
578
+ }
579
+
580
+ async function readAllStdin() {
581
+ let raw = ""
582
+ for await (const chunk of process.stdin) {
583
+ raw += chunk
584
+ }
585
+ return raw
586
+ }
587
+
588
+ async function readScriptedAnswersIfNeeded() {
589
+ if (process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1" && !process.stdin.isTTY) {
590
+ return (await readAllStdin()).split(/\r?\n/)
236
591
  }
592
+ return undefined
237
593
  }
238
594
 
239
595
  function getPresetLabel(currentModel) {