jeo-code 0.8.19 → 0.8.20

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
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.8.20] - 2026-07-10
10
+ _"모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here._
11
+
12
+ ### Fixed
13
+ - **README `[!CAUTION]` block updated for the Antigravity exception** (`README.md`, `README.ko.md`, `README.ja.md`, `README.zh.md`) — the block's `high`/`complex` tier-fallback description ("scan for the strongest live-credentialed model... land on a different model each turn") stopped being accurate for Antigravity-/Gemini-OAuth-credentialed sessions after v0.8.19's `antigravityCompanyPoolPick` (default-on, no opt-in): those sessions instead spread across ONE model per company, session-stably (same session -> same pick every turn, not varying). Added an explicit exception clause in all 4 languages.
14
+ - **CHANGELOG 0.8.18 test-count error** — "69 pass" for `test/route-history.test.ts test/route-slash.test.ts test/launch-prompt-routing.test.ts` corrected to the actual **71 pass** (independently reproduced twice, including in an isolated `git worktree` pinned to commit 50bd0b1).
15
+ - **`RouteHistory`/`route-slash.ts` doc comments corrected** (`src/agent/route-history.ts`, `src/commands/launch/route-slash.ts`) — `turnNumber`'s doc comment now states it's a sequential ADD-counter (increments once per pre-call route AND once per mid-turn fallback retry), not a 1:1 conversational-turn counter as the old wording implied; `RouteHistory`'s hardcoded `maxSize` default (10) is now documented as an unconditional retention cap that `/route history <n>` can only narrow, never widen — `/route history 50` silently returns at most 10 entries. No behavior changed, doc-only.
16
+ - **Tightened an over-loose test assertion** (`test/prompt-router.test.ts`) — the antigravity multi-company reachability test asserted `gemini-3.5-flash-low OR gemini-pro-agent` for Google's "high"-tier company slot, when the code's actual behavior (confirmed by independent manual trace against `MODEL_CATALOG`) is fully deterministic: `gemini-3.5-flash-low` always wins the recency tiebreak, `gemini-pro-agent` never does. Replaced the OR with two precise assertions.
17
+
18
+ ### Verified
19
+ - 4 independent subagent audits, zero trust in prior claims: CodeCorrectnessAuditor (manual pen-and-paper trace of 2 antigravity routing scenarios against live `MODEL_CATALOG` — both confirmed correct), TestIntegrityAuditor (mutation-tested `onModelSwitch` and the `/model`-pin gate — both confirmed to genuinely fail when reverted; zero `.skip`/`.todo`/weakened assertions found across all 5 touched test files), LiveBehaviorVerifier (re-reproduced all 4 behavioral claims with self-generated session ids/mock server/HTTP status code — all PASS), DocsAccuracyAuditor (cross-checked every README/CHANGELOG claim in isolated `git worktree`s pinned to the exact historical commits).
20
+ - Full `bun test` — 2898 pass / 0 fail across 291 files (290 tracked at this commit; the +1 is an unrelated concurrent session's untracked file, not part of this change).
21
+ - `bun run typecheck` — no errors.
22
+ - `bun run changelog:sync` — idempotent, zero diff on a clean run (independently confirmed by DocsAccuracyAuditor before this entry was added).
23
+
9
24
  ## [0.8.19] - 2026-07-09
10
25
  _"프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools._
11
26
 
@@ -36,7 +51,7 @@ _"라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동
36
51
  ### Verified
37
52
  - Live PTY re-verification (`bun src/cli.ts`, isolated `JEO_CONFIG_DIR`, real Anthropic credentials): 5 prompts of varying complexity in one session routed to 5 distinct tier/model pairs (trivial→claude-haiku-4-5, complex→claude-fable-5, trivial→claude-haiku-4-5, high→claude-sonnet-5, trivial→claude-haiku-4-5); `/route why` and `/route status` matched the actual serving model on every turn.
38
53
  - Live post-call-fallback reproduction: a real `Bun.serve` mock returning HTTP 500 on every call forced `launch.ts`'s equivalent-pool fallback to fire mid-turn; `/route why` immediately after correctly reported the POST-fallback model and a `warning: … switched to equivalent …` field, not the original pre-call pick.
39
- - `bun test test/route-history.test.ts test/route-slash.test.ts test/launch-prompt-routing.test.ts` — 69 pass / 0 fail.
54
+ - `bun test test/route-history.test.ts test/route-slash.test.ts test/launch-prompt-routing.test.ts` — 71 pass / 0 fail.
40
55
  - `bun test test/tui-app.test.ts` — 51 pass / 0 fail (50 pre-existing + 1 new, covering the footer live-update fix).
41
56
  - Full `bun test` — 2885 pass / 0 fail across 290 files.
42
57
  - `bun run typecheck` — no errors.
package/README.ja.md CHANGED
@@ -91,7 +91,7 @@ jeo --tmux # 独立した tmux セッションで実行
91
91
  | `jeo autopilot status` | スコア方向、keep/revert 数、次アクションを示す ratchet ステータスフィールド |
92
92
 
93
93
  > [!CAUTION]
94
- > **`/model <name>` で特定モデルを手動指定すると、そのセッション中ルーティングが固定されます。** プロンプトルーティング(`/route`)はモデルが手動固定されていない間のみ毎ターン再評価されます。`/model <name>` で特定モデルを選ぶとその選択が固定され、`/model auto`(ピンを完全に解除)を実行するか、`/route on`(ピンを消さずに優先度だけ上げる — `/route off` した瞬間ピンが復活)を実行するまでルーティングは切り替わりません。`roles.*` 未設定時に `defaultModel` へ確定フォールバックするのは `standard` ティアのみで、`high`/`complex` ティアはフォールバック前にライブでクレデンシャル済みの最強モデルを先に探索するため、未設定でも毎ターン別モデルに着地することがあります。
94
+ > **`/model <name>` で特定モデルを手動指定すると、そのセッション中ルーティングが固定されます。** プロンプトルーティング(`/route`)はモデルが手動固定されていない間のみ毎ターン再評価されます。`/model <name>` で特定モデルを選ぶとその選択が固定され、`/model auto`(ピンを完全に解除)を実行するか、`/route on`(ピンを消さずに優先度だけ上げる — `/route off` した瞬間ピンが復活)を実行するまでルーティングは切り替わりません。`roles.*` 未設定時に `defaultModel` へ確定フォールバックするのは `standard` ティアのみで、`high`/`complex` ティアは通常フォールバック前にライブでクレデンシャル済みの最強モデルを先に探索するため、未設定でも毎ターン別モデルに着地することがあります。**例外:** Antigravity または Gemini OAuth でクレデンシャルされたセッションは一つの資格情報で Anthropic/Google/OpenAI のモデルを再公開しており、その場合 `high`/`complex` は(必ずしも最強ではなく)会社ごとに1モデルへセッション単位で安定的に分散されるため、ターンごとに変わらずそのセッション中は固定されます。
95
95
 
96
96
  ## Spec-first ワークフロー
97
97
 
@@ -265,11 +265,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
265
265
  ## 変更履歴 (Changelog)
266
266
 
267
267
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
268
+ - **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
268
269
  - **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
269
270
  - **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
270
271
  - **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
271
272
  - **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
272
- - **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
273
273
 
274
274
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
275
275
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -92,7 +92,7 @@ jeo --tmux # 독립 tmux 세션에서 실행
92
92
  | `jeo autopilot status` | 점수 방향, keep/revert 횟수, 다음 액션을 보여주는 ratchet 상태 필드 |
93
93
 
94
94
  > [!CAUTION]
95
- > **`/model <name>`으로 특정 모델을 수동 지정하면 그 세션 동안 라우팅이 고정됩니다.** 프롬프트 라우팅(`/route`)은 모델이 수동으로 고정되지 않은 동안만 매 턴 재평가됩니다. `/model <name>`으로 특정 모델을 선택하면 그 선택이 그대로 고정되며, `/model auto`(핀을 완전히 해제)를 실행하거나 `/route on`(핀을 지우지 않고 그보다 우선순위를 높임 — `/route off` 하는 즉시 핀이 다시 살아남)을 실행하기 전까지는 라우팅이 다시 전환되지 않습니다. `roles.*` 항목 미설정 시 `defaultModel`로 확정 폴백되는 건 `standard` 티어뿐이며, `high`/`complex` 티어는 폴백 전에 실시간으로 크레덴셜된 가장 강력한 모델을 먼저 탐색하므로, 설정이 없어도 매 턴 다른 모델로 갈 수 있습니다.
95
+ > **`/model <name>`으로 특정 모델을 수동 지정하면 그 세션 동안 라우팅이 고정됩니다.** 프롬프트 라우팅(`/route`)은 모델이 수동으로 고정되지 않은 동안만 매 턴 재평가됩니다. `/model <name>`으로 특정 모델을 선택하면 그 선택이 그대로 고정되며, `/model auto`(핀을 완전히 해제)를 실행하거나 `/route on`(핀을 지우지 않고 그보다 우선순위를 높임 — `/route off` 하는 즉시 핀이 다시 살아남)을 실행하기 전까지는 라우팅이 다시 전환되지 않습니다. `roles.*` 항목 미설정 시 `defaultModel`로 확정 폴백되는 건 `standard` 티어뿐이며, `high`/`complex` 티어는 보통 폴백 전에 실시간으로 크레덴셜된 가장 강력한 모델을 먼저 탐색하므로, 설정이 없어도 매 턴 다른 모델로 갈 수 있습니다. **예외:** Antigravity 또는 Gemini OAuth로 크레덴셜된 세션은 하나의 크레덴셜로 Anthropic/Google/OpenAI 모델을 함께 재노출하는데, 이 경우 `high`/`complex`는 (항상 최강 모델이 아니라) 회사당 모델 1개로 세션-안정적으로 분산되어, 턴마다 바뀌지 않고 해당 세션 내내 고정됩니다.
96
96
 
97
97
  ## Spec-first 워크플로
98
98
 
@@ -266,11 +266,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
266
266
  ## 변경 이력 (Changelog)
267
267
 
268
268
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
269
+ - **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
269
270
  - **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
270
271
  - **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
271
272
  - **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
272
273
  - **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
273
- - **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
274
274
 
275
275
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
276
276
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -97,7 +97,7 @@ Inside the `jeo` REPL (Tab autocompletes; `/` opens the palette).
97
97
  | `jeo autopilot status` | Ratchet status field with score direction, keep/revert counts, and next action |
98
98
 
99
99
  > [!CAUTION]
100
- > **`/model <name>` locks routing for the rest of the session.** Prompt routing (`/route`) only re-evaluates per turn while no model is manually pinned. Picking a specific model via `/model <name>` freezes that choice — routing will *not* switch away from it again until you run `/model auto` (which clears the pin), or `/route on` (which *outranks* an active pin without clearing it — the pin reasserts itself the moment you run `/route off`). Missing a `roles.*` entry only guarantees a `defaultModel` fallback on the `standard` tier; the `high`/`complex` tiers first scan for the strongest live-credentialed model before falling back, so they can still land on a different model each turn even when unconfigured.
100
+ > **`/model <name>` locks routing for the rest of the session.** Prompt routing (`/route`) only re-evaluates per turn while no model is manually pinned. Picking a specific model via `/model <name>` freezes that choice — routing will *not* switch away from it again until you run `/model auto` (which clears the pin), or `/route on` (which *outranks* an active pin without clearing it — the pin reasserts itself the moment you run `/route off`). Missing a `roles.*` entry only guarantees a `defaultModel` fallback on the `standard` tier; the `high`/`complex` tiers otherwise scan for the strongest live-credentialed model, so they can still land on a different model each turn even when unconfigured. **Exception:** an Antigravity- or Gemini-OAuth-credentialed session re-exports Anthropic/Google/OpenAI models under one credential — `high`/`complex` there instead session-stably spread across one model per company (not necessarily the strongest), so the pick stays fixed for that session rather than varying turn to turn.
101
101
 
102
102
  ## Spec-first workflow
103
103
 
@@ -273,11 +273,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
273
273
  ## Changelog
274
274
 
275
275
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
276
+ - **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
276
277
  - **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
277
278
  - **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
278
279
  - **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
279
280
  - **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
280
- - **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
281
281
 
282
282
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
283
283
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -91,7 +91,7 @@ jeo --tmux # 在独立 tmux 会话中运行
91
91
  | `jeo autopilot status` | 显示分数方向、keep/revert 次数和下一步动作的 ratchet 状态字段 |
92
92
 
93
93
  > [!CAUTION]
94
- > **通过 `/model <name>` 手动指定模型后,路由会在本会话内保持锁定。** Prompt routing(`/route`)只在没有手动锁定模型时才会逐轮重新评估。用 `/model <name>` 选定具体模型后,该选择会被锁定 — 直到你运行 `/model auto`(彻底解除锁定),或运行 `/route on`(不会清除锁定,只是优先级更高 — 一旦运行 `/route off`,锁定会立刻恢复)之前,路由都不会再切换。未配置 `roles.*` 条目时,只有 `standard` 层级会确定性地退回到 `defaultModel`;`high`/`complex` 层级会先实时扫描已配置凭证中最强的可用模型再决定是否回退,所以即使未配置,也可能每轮落到不同的模型上。
94
+ > **通过 `/model <name>` 手动指定模型后,路由会在本会话内保持锁定。** Prompt routing(`/route`)只在没有手动锁定模型时才会逐轮重新评估。用 `/model <name>` 选定具体模型后,该选择会被锁定 — 直到你运行 `/model auto`(彻底解除锁定),或运行 `/route on`(不会清除锁定,只是优先级更高 — 一旦运行 `/route off`,锁定会立刻恢复)之前,路由都不会再切换。未配置 `roles.*` 条目时,只有 `standard` 层级会确定性地退回到 `defaultModel`;`high`/`complex` 层级通常会先实时扫描已配置凭证中最强的可用模型再决定是否回退,所以即使未配置,也可能每轮落到不同的模型上。**例外:** 通过 Antigravity 或 Gemini OAuth 授权的会话会用同一个凭证重新导出 Anthropic/Google/OpenAI 的模型 —此时 `high`/`complex` 会按公司各选一个模型、以会话为单位稳定分布(不一定是最强的那个),因此在同一会话内保持固定,而不是逐轮变化。
95
95
 
96
96
  ## Spec-first 工作流
97
97
 
@@ -265,11 +265,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
265
265
  ## 更新日志 (Changelog)
266
266
 
267
267
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
268
+ - **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
268
269
  - **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
269
270
  - **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
270
271
  - **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
271
272
  - **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
272
- - **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
273
273
 
274
274
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
275
275
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.8.19",
3
+ "version": "0.8.20",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -258,8 +258,17 @@ export interface AgentLoopEvents {
258
258
  /** Consulted when a lone `done` arrives. Return a corrective message to bounce
259
259
  * the done ONCE (e.g. "todo list still shows unfinished items — update it
260
260
  * first"); return null to let the turn finish. The engine guarantees at most
261
- * one bounce per turn, so a stubborn model can never loop here. */
262
- onBeforeDone?(reason: string): Promise<string | null> | string | null;
261
+ * one bounce per turn, so a stubborn model can never loop here. `evidence` is
262
+ * the SAME mutation/verification signal the engine's own done-gate already
263
+ * computed (see `classifyDoneGate` above) — a caller-owned gate (e.g. the goal
264
+ * verifier) can use it to deterministically downgrade an LLM-judged MET verdict
265
+ * when the turn mutated files without a fresh passing verification, instead of
266
+ * trusting the transcript self-report alone. */
267
+ onBeforeDone?(
268
+ reason: string,
269
+ evidence: { sawMutation: boolean; sawVerification: boolean; verificationStale: boolean }
270
+ ): Promise<string | null> | string | null;
271
+
263
272
  /** Fired when a mid-turn steering message (an additional user query typed while
264
273
  * the turn is running) is injected into the live history. `text` is the raw
265
274
  * user line — drives a TUI notice so the user sees their input was picked up. */
@@ -994,7 +1003,12 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
994
1003
  // [DONE] with the Todos checklist still showing 1 in-progress + 4 pending
995
1004
  // because nothing ever forced a status update.
996
1005
  if (!beforeDoneNudgeUsed && ev.onBeforeDone) {
997
- const nudge = await ev.onBeforeDone((toolCalls[0].arguments?.reason as string) ?? "");
1006
+ const nudge = await ev.onBeforeDone((toolCalls[0].arguments?.reason as string) ?? "", {
1007
+ sawMutation,
1008
+ sawVerification,
1009
+ verificationStale: sawMutation && lastMutationSeq > lastVerificationSeq,
1010
+ });
1011
+
998
1012
  if (nudge) {
999
1013
  beforeDoneNudgeUsed = true;
1000
1014
  pushAssistantTurn(history, responseText, reasonBuf, artifactBuf);
@@ -78,8 +78,47 @@ Do not include any other text, markdown formatting, or code blocks. Output raw J
78
78
  verdict: "NOT_MET",
79
79
  reason: `Goal verification failed to parse or execute: ${(err as Error).message}. Please verify the goal manually.`
80
80
  };
81
+
81
82
  }
82
83
  }
84
+
85
+ /** Evidence the engine's own done-gate already computed this turn (see
86
+ * `classifyDoneGate`/`engine.ts`) — NOT re-derived here, just consumed, so this
87
+ * stays a pure function over already-verified signals rather than a second
88
+ * regex/text scan. */
89
+ export interface TurnEvidence {
90
+ sawMutation: boolean;
91
+ sawVerification: boolean;
92
+ verificationStale: boolean;
93
+ }
94
+
95
+ /**
96
+ * Deterministic downgrade gate: an LLM-judged MET verdict is trustworthy only when
97
+ * the turn either made no file changes, or made changes AND a fresh (non-stale)
98
+ * verification signal (test/build/typecheck/lint) was observed. A turn that mutated
99
+ * files with no verification at all, or whose only verification predates the last
100
+ * mutation, cannot self-report MET on the transcript alone — that is exactly the
101
+ * "gate theater" failure mode (an LLM asserting success without re-checked evidence).
102
+ * NOT_MET/IMPOSSIBLE verdicts pass through unchanged: this only ever tightens MET,
103
+ * never loosens a verdict the LLM itself already found lacking.
104
+ */
105
+ export function applyEvidenceGate(verdict: GoalVerdict, evidence: TurnEvidence): GoalVerdict {
106
+ if (verdict.verdict !== "MET") return verdict;
107
+ if (!evidence.sawMutation) return verdict; // nothing changed — the transcript judgment stands
108
+ if (evidence.sawVerification && !evidence.verificationStale) return verdict; // fresh verification backs the claim
109
+
110
+ const gap = !evidence.sawVerification
111
+ ? "the turn modified files but no test/build/typecheck/lint run was observed"
112
+ : "the turn's only verification run predates its last file modification (stale evidence)";
113
+ return {
114
+ verdict: "NOT_MET",
115
+ reason:
116
+ `Goal verifier LLM judged MET, but this was overridden by deterministic evidence: ${gap}. ` +
117
+ `Re-run the relevant verification AFTER the last file change, then call done again. ` +
118
+ `(Original LLM reason: ${verdict.reason})`,
119
+ };
120
+ }
121
+
83
122
 
84
123
 
85
124
  export interface GoalState {
@@ -9,12 +9,21 @@ import type { RouteDecision } from "./prompt-router";
9
9
 
10
10
  export interface RouteHistoryEntry extends RouteDecision {
11
11
  timestamp: number; // Date.now()
12
- turnNumber: number; // 1-indexed turn in this session
12
+ /** 1-indexed sequence number of this RECORDED DECISION (not the REPL turn) —
13
+ * `add()` fires once for the pre-call routing pick AND once more per
14
+ * mid-turn equivalent-pool fallback (launch.ts's retry loop), so a single
15
+ * user turn that hits a fallback can produce several CONSECUTIVE entries
16
+ * here. Do not treat this as a 1:1 conversational-turn counter. */
17
+ turnNumber: number;
13
18
  }
14
19
 
15
20
  /**
16
21
  * Bounded FIFO queue of routing decisions. Oldest entries are dropped when
17
- * the queue exceeds `maxSize`.
22
+ * the queue exceeds `maxSize`. The REPL constructs this with the default
23
+ * `maxSize` (10) — `/route history <n>`'s `n` argument only WINDOWS this
24
+ * already-capped retention (`launch.ts`'s single `new RouteHistory()` call
25
+ * site never passes a wider size), so `/route history 50` silently returns
26
+ * at most the last 10 decisions recorded this session, not 50.
18
27
  */
19
28
  export class RouteHistory {
20
29
  private entries: RouteHistoryEntry[] = [];
@@ -98,7 +98,8 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
98
98
  { canonical: "claude-sonnet-4-6", provider: "anthropic", providerModel: "claude-sonnet-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
99
99
  { canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
100
100
  { canonical: "claude-haiku-4-5", provider: "anthropic", providerModel: "claude-haiku-4-5-20251001", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true, releaseDate: "2025-10" },
101
- // OpenAI (newest first: gpt-5.5 > gpt-5.4 > o4-mini/o3/gpt-4.1 > o3-mini > gpt-4o-mini > gpt-4o)
101
+ // OpenAI (newest first: gpt-5.6 > gpt-5.5 > gpt-5.4 > o4-mini/o3/gpt-4.1 > o3-mini > gpt-4o-mini > gpt-4o)
102
+ { canonical: "gpt-5.6", provider: "openai", providerModel: "gpt-5.6", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
102
103
  { canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
103
104
  { canonical: "gpt-5.4", provider: "openai", providerModel: "gpt-5.4", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-03" },
104
105
  { canonical: "gpt-5.4-mini", provider: "openai", providerModel: "gpt-5.4-mini", contextTokens: 272_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-03" },
@@ -69,7 +69,10 @@ function explainDecision(decision: RouteDecision | { note: string } | null): str
69
69
  /**
70
70
  * Format the last `n` history entries (default 10) as one line each:
71
71
  * `turn N: tier -> model (source, confidence X.XX)`, oldest of the selected
72
- * window first — matches `getAll()`'s chronological ordering.
72
+ * window first — matches `getAll()`'s chronological ordering. `entries` is
73
+ * ALREADY capped to `RouteHistory`'s `maxSize` (default 10) by the caller —
74
+ * `n` only narrows that window further, it can never widen it, so a large
75
+ * `n` (e.g. 50) silently returns at most `maxSize` entries.
73
76
  */
74
77
  function formatHistory(entries: RouteHistoryEntry[], n: number): string[] {
75
78
  if (entries.length === 0) return ["No routing decisions recorded yet this session."];
@@ -110,8 +110,6 @@ import {
110
110
  latestSessionId,
111
111
  exportSession,
112
112
  updateSessionModel,
113
- updateSessionDraft,
114
-
115
113
  appendCompaction,
116
114
  resolveSessionRef,
117
115
  } from "../agent/session";
@@ -754,10 +752,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
754
752
 
755
753
  // pi-style session persistence: resume an existing session or create a new one.
756
754
  let sessionId: string | undefined;
757
- // Unsent input-box draft restored from a resumed session's header (gajae-code
758
- // parity) — folded into the very first prompt once `queuedPromptInput` exists.
759
- let restoredDraft = "";
760
-
761
755
  let compactionSeq = 0;
762
756
  if (!flags.noSession) {
763
757
  if (flags.resumeInteractive) {
@@ -776,8 +770,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
776
770
  for (const m of messages) history.push(m);
777
771
  sessionId = id;
778
772
  if (!initialSessionModel && header.model) sessionModel = header.model;
779
- if (header.draft) restoredDraft = header.draft;
780
-
781
773
  const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
782
774
  console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
783
775
  } catch (err) {
@@ -816,8 +808,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
816
808
  // Restore the model this session was last using unless the CLI explicitly
817
809
  // pinned one (flags.model/role/provider → initialSessionModel wins).
818
810
  if (!initialSessionModel && header.model) sessionModel = header.model;
819
- if (header.draft) restoredDraft = header.draft;
820
-
821
811
  const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
822
812
  console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
823
813
  } catch (err) {
@@ -2193,12 +2183,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2193
2183
  // (the user explicitly invoked them — no second Enter).
2194
2184
  const pendingMidTurnCommands: string[] = [];
2195
2185
  const queuedPromptInput: PromptInputQueue = { pendingLines: pendingStdinLines, partial: "", pastedLines: [], inPaste: false };
2196
- // Fold a restored `/resume` draft (session header `draft`) into the very first
2197
- // prompt the same way a queued mid-turn prefill lands — visible in the input box,
2198
- // Enter to run, Esc/Ctrl+U to discard (gajae-code parity: an in-progress prompt
2199
- // survives a quit + resume instead of being silently lost).
2200
- if (restoredDraft) queuedPromptInput.partial = restoredDraft;
2201
-
2202
2186
  queueBusyInput = (chunk: string) => captureLivePromptInputChunk(queuedPromptInput, chunk);
2203
2187
  queueBusyPasteActive = () => queuedPromptInput.inPaste;
2204
2188
  queueBusySnapshot = () => ({
@@ -2210,19 +2194,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2210
2194
  // default routes to the immediate-dispatch queue served at the top of the loop.
2211
2195
  (jeoEnv("NO_MIDTURN_DISPATCH") === "1" ? pendingStdinLines : pendingMidTurnCommands).push(line);
2212
2196
  };
2213
- // Persist (or clear) the unsent input-box draft into the session header so a later
2214
- // `/resume` restores it (gajae-code parity). Best-effort, mirrors persistSessionModel;
2215
- // reads readline's own line buffer since that is the single source of truth for
2216
- // "what's currently typed but not submitted" (same buffer the box-render/prefill
2217
- // logic above reads from).
2218
- const persistSessionDraft = async (): Promise<void> => {
2219
- if (flags.noSession || !sessionId) return;
2220
- const current = (rl as unknown as { line?: string }).line ?? queuedPromptInput.partial ?? "";
2221
- try {
2222
- await updateSessionDraft(sessionId, current, cwd);
2223
- } catch { /* best-effort */ }
2224
- };
2225
-
2226
2197
  // Bracketed-paste line routing at the PROMPT: readline strips the 2004 markers
2227
2198
  // and replays pasted lines as synthetic keypresses, emitting paste-start /
2228
2199
  // paste-end around them. Lines submitted INSIDE that window are intentional
@@ -3724,8 +3695,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3724
3695
  if (pickResult.sessionModel !== undefined) sessionModel = pickResult.sessionModel;
3725
3696
  if (pickResult.lastUserInput !== undefined) lastUserInput = pickResult.lastUserInput;
3726
3697
  if (pickResult.lastReply !== undefined) lastReply = pickResult.lastReply;
3727
- if (pickResult.draft) queuedPromptInput.partial = pickResult.draft;
3728
-
3729
3698
  }
3730
3699
 
3731
3700
  // Remote (Telegram) free-text reply / config command routing — wired here,
@@ -3927,9 +3896,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3927
3896
  });
3928
3897
  sessionId = result.sessionId;
3929
3898
  if (result.sessionModel !== undefined) sessionModel = result.sessionModel;
3930
- if (result.lastReply !== undefined) lastReply = result.lastReply;
3931
- if (result.draft) queuedPromptInput.partial = result.draft;
3932
-
3899
+ if (result.lastUserInput !== undefined) lastUserInput = result.lastUserInput;
3933
3900
  if (result.lastReply !== undefined) lastReply = result.lastReply;
3934
3901
  continue;
3935
3902
  }
@@ -4631,9 +4598,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4631
4598
  }
4632
4599
  }
4633
4600
  disarmPreview(); // clear footer + restore full-screen scrolling before leaving the REPL
4634
- await persistSessionDraft();
4635
4601
  if (hardExitOnLoopEnd) {
4636
-
4637
4602
  try {
4638
4603
  disarmPreview();
4639
4604
  out.write("\x1b[?25h\n");