jeo-code 0.11.4 → 0.11.6

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
@@ -9,6 +9,38 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.11.6] - 2026-10-16
13
+ _Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability._
14
+
15
+ ### Added
16
+ - **Live Antigravity model discovery scoped to account** (`src/ai/model-discovery.ts`, `src/commands/launch.ts`) — OAuth account's real `agentModelSorts` models from the Cloud Code Assist endpoint are cached and rehydrated at launch, scoped to the selected account. Picker `/model provider antigravity` and `/agents <role> provider antigravity` offer only the account's live model list (no static fallback catalog, no Argon guarantee for every account). Empty or failed discovery is reported honestly.
17
+
18
+ ### Changed
19
+ - **Model picker account scoping** — OpenAI and Antigravity pickers honor the selected OAuth account; prior-account stale models are suppressed. Cache keys include provider, account ID, and source (oauth/api_key) for isolation.
20
+
21
+ ### Fixed
22
+ - **Picker empty results** — an authoritative empty Antigravity account list no longer falls back to fabricated static rows. Project lookup is best-effort; mismatches between discovered and stored project IDs are handled safely.
23
+
24
+ ### Verified
25
+ - Live Antigravity OAuth account discovery: account scope is honored on picker refresh and cache refresh. Model picker respects discovered list with no static fallback. `bun run typecheck` clean.
26
+
27
+ ## [0.11.5] - 2026-10-04
28
+ _Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified._
29
+
30
+ ### Added
31
+ - **Anthropic 5.5 model selection** — `opus5.5` / `opus-5.5` select `claude-opus-5-5`; `sonnet5.5` / `sonnet-5.5` select `claude-sonnet-5-5`. Catalog metadata reflects the official 1M context, 128K output and adaptive-thinking support; Opus 5.5 pricing is $4 input / $20 output per million tokens.
32
+
33
+ ### Fixed
34
+ - **Opus 5.5 rejected after OAuth login** — update the shared request User-Agent and billing-header client version from 2.1.63 to 2.1.289, satisfying Anthropic's observed 2.1.280 minimum. The companion Anthropic web-search OAuth path uses the same supported version.
35
+ - **Fast-mode 5.5 requests rejected on temperature** — omit custom temperature for Opus/Sonnet 5.5 and newer before the first request; retain the selected wire model and existing older-model recovery behavior.
36
+
37
+ ### Changed
38
+ - **Anthropic setup recommendation and automatic routing** — Sonnet 5.5 is the recommended setup model and high-tier automatic pick; the complex-tier automatic pick moves from Fable 5 to Opus 5.5 under the existing catalog ranking. Explicit model pins and the legacy bare `opus` / `sonnet` aliases remain unchanged.
39
+
40
+ ### Verified
41
+ - Real Anthropic OAuth requests returned HTTP 200 with exact `claude-opus-5-5` and `claude-sonnet-5-5` response identities in both low-effort and fast mode. Both models also completed real streaming requests with matching `message_start.model` and `message_stop`.
42
+ - Modified behavioral regressions: 292 pass, 0 fail across 11 files; `bun run typecheck` passed. Regression coverage enforces OAuth client version eligibility, first-request sampling compatibility, exact model preservation, setup defaults, pricing and web-search headers.
43
+
12
44
  ## [0.11.4] - 2026-10-01
13
45
  _The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire._
14
46
 
package/README.ja.md CHANGED
@@ -49,7 +49,7 @@
49
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
50
  - **低コスト層のグレーダールーティング** — `/goal` の検証器、`critic` サブエージェントロール、固定されていない `task` ファンアウトバッチは、採点・実行対象の作業と同じフルプライスモデルに黙って乗る代わりに、デフォルトで低コストのクレデンシャル済みモデルを使用します(`resolveVerifierModel`、ブラウザの `verify` アクションについてはビジョン対応能力でフィルタされ、テキストのみの低コストモデルが添付スクリーンショットを黙って取りこぼすことがありません)。
51
51
  - **`jeo routine init`** — スケジュール/issue/PR トリガーで jeo をヘッドレス実行する(`jeo "<prompt>" -p`) GitHub Actions ワークフローを、GitHub 自身のランナー上に生成します — ラップトップは不要で、jeo 自体の内部に新しい攻撃対象領域も一切増えません(インプロセスのスケジューラや Webhook リスナーはありません)。`--dry-run` でプレビュー、`--no-pr` でデフォルトの実行ごとの PR の代わりに直接コミット。
52
- - **リモート監視・制御(Telegram, Discord, Slack)** — ボットを一度ペアリング(`jeo notify setup --provider telegram|discord|slack`)すれば、共有の `jeo daemon start` 1 つが、稼働中のすべてのセッションにわたってサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、人間の許可リスト(allowlist)からの `/subagents`、`/steer`、`/cancel`(Discord/Slack ではさらに `/sessions` と `/send`)を受け付けます。Telegram はフォーラムトピック・インラインキーボード・画像添付に対応し、Discord は REST + Gateway WebSocket、Slack は Web API + Socket Mode を使います。すべてのリモート制御はローカルセッションが確認応答(acknowledge)するものであり、実行済みと決めつけることはありません。
52
+ - **リモート監視・制御(Telegram, Discord, Slack)** — ボットを一度ペアリング(`jeo notify setup --provider telegram|discord|slack`)すれば、共有の `jeo daemon start` 1 つが、稼働中のすべてのセッションにわたってサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、人間の許可リスト(allowlist)からの `/subagents`、`/steer`、`/cancel`(Discord/Slack ではさらに `/sessions` と `/send`)を受け付けます。Telegram はフォーラムトピック・インラインキーボード・画像添付に対応し、Discord は REST + Gateway WebSocket、Slack は Web API + Socket Mode を使います。プラットフォーム別詳細ガイドおよびインストール可能なエージェントスキル(`channel-notify`)を提供します。
53
53
  - **ごまかしの効かないゲート** — `approve` は承認対象そのもののプランダイジェストを再検証し、`team` はレビュー済みダイジェストが変わったプランを拒否します。`done` 呼び出しは最新の検証証拠と照らして再チェックされ(後から失敗したチェックは先の合格を無効化し、修正の差し戻し 3 回の後は強い拒否)、`autopilot` ラチェットはロールバック失敗を `rollback_failed` として記録し、ステップを戻したと主張する代わりに非ゼロで停止します。
54
54
  - **ターンをブロックしないセッション単位の非同期実行** — `task` ツールの実際の `tasks` 配列で独立した作業をファンアウトし、親ターンを止めません。detached サブエージェント、バックグラウンド job、行単位 monitor は後続ターンからも `subagent`/`job`/`monitor` の `list`、`inspect`、`await`、`cancel`、`tail` で制御できます。インライン TUI は各ワーカーのライブ activity を専用スロットに表示し、セッション終了または Ctrl-C ですべてのレジストリを破棄します。
55
55
  - **独立した検証者、実際に強制** — プランはもう architect/critic ステップをスキップできません: `PlanSchema` は未検証の変更で終わるプランを拒否し(検証対象の変更より前に置かれた verifier も無効)、`ralplan` のドラフト時点と `team`/`approve` の実行時点の両方で適用されます。すべての architect/critic 評決も実際の証拠を示す必要があり、観測された `read`/`search`/`find`/`ast_grep`/`lsp` 呼び出しがゼロなら、テキストが何を主張していても評決はブロックされます。
@@ -107,6 +107,11 @@ jeo --tmux # 独立した tmux セッションで実行
107
107
  > [!CAUTION]
108
108
  > **`/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モデルへセッション単位で安定的に分散されるため、ターンごとに変わらずそのセッション中は固定されます。
109
109
 
110
+ ### Antigravity アカウント モデル ライブディスカバリー
111
+
112
+ **ライブ モデル ディスカバリー** — `/model provider antigravity` と `/agents <ロール> provider antigravity` は OAuth アカウントが Cloud Code Assist で実際に保有するモデルを表示します。ピッカーはアカウントが持つモデルのみを認め、すべてのアカウントが Argon を備えていることは保証しません。空のアカウント一覧は正直に報告され、静的なカタログでの代用はしません。アカウントスコープはセッションのリフレッシュ全体を通じて保持されます。
113
+
114
+ **例:** `jeo auth login antigravity` はアカウントの実際に利用可能なモデルを発見しキャッシュします。`/model antigravity/<モデル-id>` または `/agents executor provider antigravity` で選択し、セッション中に利用するか `/model save antigravity/<モデル-id>` で既定値として設定します。
110
115
  ## CLI コマンド
111
116
 
112
117
  `jeo --help` が正式な一覧です。よく使うものは次のとおり:
@@ -205,6 +210,13 @@ jeo ultragoal
205
210
 
206
211
  オプトイン: 共有デーモン 1 つがマシン上の稼働中の `jeo` セッションをすべて監視し、**サブエージェントの状態遷移**(開始 → 完了/失敗/キャンセル)ごとにメッセージを送ります。加えてセッション識別ヘッダー、ターンの開始/終了サマリー、確定したターンのテキストも送りますが、「まだ実行中」という繰り返しの ping は決して送りません。allowlist に登録された人間はチャットから一覧・送信・ステア・キャンセルができ、すべての制御はローカルセッションへ中継されて **確認応答済み(受理であって完了ではない)** または **未確認応答** として報告されます — デーモンがコマンドの実行完了を主張することはありません。
207
212
 
213
+ プラットフォーム別詳細ガイド:
214
+ - 📖 [チャネル連携概要 & アーキテクチャ](docs/integrations/README.md) — 機能マトリクスと全体構成
215
+ - 📱 [Telegram 連携詳細ガイド](docs/integrations/telegram.md) — 内蔵バックグラウンドデーモン、BotFather ペアリング、フォーラムトピック、`/steer` コマンド
216
+ - 🎮 [Discord 連携詳細ガイド](docs/integrations/discord.md) — 即時チャネル通知用 Webhook、双方向対話ボット、Embed カード形式
217
+ - 💬 [Slack 連携詳細ガイド](docs/integrations/slack.md) — ワークスペース通知用 Incoming Webhook、チーム共同作業用 Slack App / Socket Mode、Block Kit カード
218
+ - 🧩 [エージェントスキルインストールガイド](docs/integrations/agent-skills.md) — `jeo`、Claude Code、Cursor、Codex などへチャネル通知スキルを導入
219
+
208
220
  ```bash
209
221
  jeo notify setup [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID | --channel-id ID] [--allowed-user-ids ID,ID,...]
210
222
  jeo notify status [--provider …] # マスク済みトークン、宛先、allowlist、デーモン状態(stopped / stale / initializing / initialized / pairing)
@@ -240,6 +252,41 @@ jeo daemon start|stop|status|reload # 共有デーモンにプロバイダ選
240
252
  └─────────────────────┘ session└──────────────────┘ └───────────────────────────┘
241
253
  ```
242
254
 
255
+ ### Discord & Slack (Webhook & ボットアプリ)
256
+
257
+ チームのワークスペース通知用に環境変数を設定します:
258
+
259
+ ```bash
260
+ # Discord Webhook
261
+ export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."
262
+
263
+ # Slack Incoming Webhook
264
+ export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
265
+ ```
266
+
267
+ タスク実行状況、所要時間、コミットおよび PR リンクを含むリッチな Embed(Discord) や Block Kit カード(Slack) を送信できます。ペイロード形式や CI 自動化スニペットは [Discord ガイド](docs/integrations/discord.md) および [Slack ガイド](docs/integrations/slack.md) をご覧ください。
268
+
269
+ ### エージェントスキルとしてのインストール
270
+
271
+ 標準の [Agent Skills](https://agentskills.io) CLI を使用して、`jeo` やその他の AI コーディングエージェントに通知スキルを追加できます:
272
+
273
+ ```bash
274
+ # 現在のリポジトリに channel-notify スキルをインストール:
275
+ npx skills add akillness/jeo-code --skill channel-notify
276
+
277
+ # または全プロジェクト共通で使えるようグローバル(-g)にインストール:
278
+ npx skills add akillness/jeo-code --skill channel-notify -g
279
+ ```
280
+
281
+ インストール後はエージェントがタスク実行中に自動で進捗を通知します:
282
+ ```bash
283
+ # jeo でのワンショット実行:
284
+ jeo $channel-notify "全47件のテストにパスしました。レビュー準備完了。"
285
+
286
+ # Claude Code での実行:
287
+ /channel-notify "ビルド成功。PR: https://github.com/..."
288
+ ```
289
+
243
290
  ## ルーティン(GitHub Actions)
244
291
 
245
292
  ```bash
@@ -348,10 +395,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
348
395
 
349
396
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
350
397
  - **[Unreleased]**
398
+ - **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
399
+ - **[0.11.5]** (2026-10-04) — Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified.
351
400
  - **[0.11.4]** (2026-10-01) — The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire.
352
401
  - **[0.11.3]** (2026-10-01) — Remote control grew from one Telegram bot into one shared daemon serving Telegram, Discord, and Slack — and the workflow gates that were meant to block unverified work (`approve`/`team` plan identity, `done` re-checks, the autopilot ratchet) now actually block it.
353
- - **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
354
- - **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
355
402
 
356
403
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
357
404
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -49,7 +49,7 @@
49
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
50
  - **저비용 등급 채점 라우팅** — `/goal` 검증기, `critic` 서브에이전트 역할, 그리고 고정되지 않은 `task` 팬아웃 배치는 채점·실행 대상인 작업과 같은 풀프라이스 모델에 조용히 편승하는 대신 기본적으로 저비용 크레덴셜 모델을 사용합니다(`resolveVerifierModel`, 브라우저 `verify` 액션에 대해서는 비전 능력으로 필터링되어 텍스트 전용 저비용 모델이 첨부된 스크린샷을 조용히 누락시키지 않도록 합니다).
51
51
  - **`jeo routine init`** — 스케줄/이슈/PR 트리거에서 jeo를 헤드리스로 실행하는(`jeo "<prompt>" -p`) GitHub Actions 워크플로를 생성합니다, GitHub 자체 러너 위에서 — 노트북이 필요 없고, jeo 내부에 새로운 공격 표면도 전혀 추가되지 않습니다(인프로세스 스케줄러도 웹훅 리스너도 없음). `--dry-run`으로 미리보기, `--no-pr`로 기본값인 실행당 PR 대신 직접 커밋.
52
- - **원격 모니터링 & 제어(Telegram, Discord, Slack)** — 봇을 한 번 페어링(`jeo notify setup --provider telegram|discord|slack`)하면, 공유 `jeo daemon start` 하나가 모든 활성 세션에 걸쳐 서브에이전트 상태 전환(시작 → 완료/실패/취소)마다 메시지를 보내고, 사람 allowlist로부터 `/subagents`, `/steer`, `/cancel`(Discord/Slack에서는 `/sessions`, `/send` 추가)을 되받습니다. Telegram은 포럼 토픽·인라인 키보드·이미지 첨부를 더하고, Discord는 REST + Gateway WebSocket, Slack은 Web API + Socket Mode를 사용합니다. 모든 원격 제어는 로컬 세션이 확인 응답(acknowledge)하며, 실행된 것으로 가정하지 않습니다.
52
+ - **원격 모니터링 & 제어(Telegram, Discord, Slack)** — 봇을 한 번 페어링(`jeo notify setup --provider telegram|discord|slack`)하면, 공유 `jeo daemon start` 하나가 모든 활성 세션에 걸쳐 서브에이전트 상태 전환(시작 → 완료/실패/취소)마다 메시지를 보내고, 사람 allowlist로부터 `/subagents`, `/steer`, `/cancel`(Discord/Slack에서는 `/sessions`, `/send` 추가)을 되받습니다. Telegram은 포럼 토픽·인라인 키보드·이미지 첨부를 더하고, Discord는 REST + Gateway WebSocket, Slack은 Web API + Socket Mode를 사용합니다. 플랫폼별 상세 가이드 및 설치 가능한 에이전트 스킬(`channel-notify`)을 제공합니다.
53
53
  - **말로 넘길 수 없는 게이트** — `approve`는 승인 대상인 바로 그 플랜 다이제스트를 재검증하고, `team`은 리뷰된 다이제스트가 바뀐 플랜을 거부합니다. `done` 호출은 최신 검증 증거와 대조해 재확인되며(나중에 실패한 검사가 앞선 통과를 무효화하고, 교정 반송 3회 뒤에는 하드 거부), `autopilot` ratchet은 실패한 롤백을 `rollback_failed`로 기록하고 되돌린 스텝이라고 주장하는 대신 0이 아닌 코드로 중단합니다.
54
54
  - **턴을 막지 않는 세션 범위 비동기 실행** — `task` 도구의 실제 `tasks` 배열로 독립 작업을 팬아웃해 부모 턴을 막지 않습니다. detached 서브에이전트, 백그라운드 job, 줄 단위 monitor는 이후 턴에서도 `subagent`/`job`/`monitor`의 `list`, `inspect`, `await`, `cancel`, `tail`로 계속 제어할 수 있습니다. 인라인 TUI는 각 워커의 실시간 activity를 별도 슬롯에 표시하고 세션 종료 또는 Ctrl-C에서 모든 레지스트리를 정리합니다.
55
55
  - **실제로 강제되는 독립 검증자** — 플랜은 이제 architect/critic 단계를 건너뛸 수 없습니다: `PlanSchema`는 미검증 변이로 끝나는 모든 플랜을 거부하며(검증 대상 변이보다 앞에 배치된 검증자도 인정하지 않음), `ralplan` 드래프트 시점과 `team`/`approve` 실행 시점 양쪽 모두에 적용됩니다. 모든 architect/critic 평결도 실제 증거를 제시해야 하며, 관찰된 `read`/`search`/`find`/`ast_grep`/`lsp` 호출이 0건이면 텍스트가 무엇을 주장하든 평결이 차단됩니다.
@@ -107,6 +107,11 @@ jeo --tmux # 독립 tmux 세션에서 실행
107
107
 
108
108
  > [!CAUTION]
109
109
  > **`/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개로 세션-안정적으로 분산되어, 턴마다 바뀌지 않고 해당 세션 내내 고정됩니다.
110
+ ### Antigravity 계정 모델 라이브 디스커버리
111
+
112
+ **실시간 모델 디스커버리** — `/model provider antigravity`와 `/agents <역할> provider antigravity`는 OAuth 계정이 Cloud Code Assist에서 실제로 보유한 모델을 보여줍니다. 피커는 계정이 가진 모델만 인정하며, 모든 계정이 Argon을 갖추었다고 보장하지 않습니다. 빈 계정 목록은 정직하게 보고되며, 정적 카탈로그로 대체하지 않습니다. 계정 스코프는 세션 리프레시 전체에서 보존됩니다.
113
+
114
+ **예시:** `jeo auth login antigravity`는 계정의 실제 이용 가능한 모델을 발견하고 캐시합니다. `/model antigravity/<모델-id>` 또는 `/agents executor provider antigravity`로 선택한 뒤 세션 동안 사용하거나 `/model save antigravity/<모델-id>`로 기본값 설정.
110
115
 
111
116
  ## CLI 명령
112
117
 
@@ -206,6 +211,13 @@ jeo ultragoal
206
211
 
207
212
  옵트인: 공유 데몬 하나가 머신의 모든 활성 `jeo` 세션을 지켜보다가 **서브에이전트 상태 전환**(시작 → 완료/실패/취소)마다 메시지를 보내고, 세션 식별 헤더, 턴 시작/종료 요약, 확정된 턴 텍스트도 함께 전송합니다 — "여전히 실행 중" 반복 핑은 절대 보내지 않습니다. allowlist에 등록된 사람은 채팅에서 목록 조회·전송·조종·취소를 할 수 있고, 모든 제어는 로컬 세션으로 중계되어 **확인 응답됨(수락됨, 완료 아님)** 또는 **확인 응답 안 됨**으로 보고됩니다 — 데몬은 명령이 실행됐다고 절대 주장하지 않습니다.
208
213
 
214
+ 플랫폼별 상세 단독 가이드:
215
+ - 📖 [채널 연동 개요 & 아키텍처](docs/integrations/README.md) — 기능 매트릭스 및 구조 개요
216
+ - 📱 [Telegram 연동 상세 가이드](docs/integrations/telegram.md) — 내장 백그라운드 데몬, BotFather 대화형 페어링, 포럼 토픽, `/steer` 원격 명령
217
+ - 🎮 [Discord 연동 상세 가이드](docs/integrations/discord.md) — 즉각적인 채널 알림을 위한 웹훅, 양방향 상호작용을 위한 봇 및 임베드 카드 포맷
218
+ - 💬 [Slack 연동 상세 가이드](docs/integrations/slack.md) — 워크스페이스 알림용 Incoming Webhook, 팀 협업용 Slack App / Socket Mode, Block Kit 카드
219
+ - 🧩 [에이전트 스킬 설치 가이드](docs/integrations/agent-skills.md) — `jeo`, Claude Code, Cursor, Codex 등 다양한 에이전트에 채널 알림 스킬 설치하기
220
+
209
221
  ```bash
210
222
  jeo notify setup [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID | --channel-id ID] [--allowed-user-ids ID,ID,...]
211
223
  jeo notify status [--provider …] # 마스킹된 토큰, 목적지, allowlist, 데몬 상태 (stopped / stale / initializing / initialized / pairing)
@@ -242,6 +254,41 @@ jeo daemon start|stop|status|reload # 공유 데몬에는 프로바이더 선
242
254
  ```
243
255
 
244
256
 
257
+ ### Discord & Slack (웹훅 & 봇 앱)
258
+
259
+ 팀 워크스페이스를 위해 표준 웹훅 또는 봇 자격증명을 환경 변수로 설정합니다:
260
+
261
+ ```bash
262
+ # Discord Webhook
263
+ export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."
264
+
265
+ # Slack Incoming Webhook
266
+ export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
267
+ ```
268
+
269
+ 작업 상태, 소요 시간, 커밋 및 PR 링크가 포함된 풍부한 임베드(Discord) 및 Block Kit 카드(Slack)를 전송할 수 있습니다. 페이로드 스키마와 CI 자동화 스니펫은 [Discord 가이드](docs/integrations/discord.md) 및 [Slack 가이드](docs/integrations/slack.md)를 참고하세요.
270
+
271
+ ### 에이전트 스킬로 설치
272
+
273
+ 표준 [Agent Skills](https://agentskills.io) CLI를 사용하여 `jeo` 및 다른 AI 코딩 에이전트에 채널 알림 스킬을 즉시 설치할 수 있습니다:
274
+
275
+ ```bash
276
+ # 현재 저장소에 channel-notify 스킬 설치:
277
+ npx skills add akillness/jeo-code --skill channel-notify
278
+
279
+ # 또는 모든 프로젝트에서 사용하도록 전역(-g) 설치:
280
+ npx skills add akillness/jeo-code --skill channel-notify -g
281
+ ```
282
+
283
+ 설치 후 에이전트는 작업 수행 중 자동으로 상태를 브로드캐스트할 수 있습니다:
284
+ ```bash
285
+ # jeo에서 원샷 호출:
286
+ jeo $channel-notify "테스트 47개 전수 통과 완료. 리뷰 준비됨."
287
+
288
+ # Claude Code에서 호출:
289
+ /channel-notify "빌드 성공. PR: https://github.com/..."
290
+ ```
291
+
245
292
  ## 루틴 (GitHub Actions)
246
293
 
247
294
  ```bash
@@ -350,10 +397,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
350
397
 
351
398
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
352
399
  - **[Unreleased]**
400
+ - **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
401
+ - **[0.11.5]** (2026-10-04) — Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified.
353
402
  - **[0.11.4]** (2026-10-01) — The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire.
354
403
  - **[0.11.3]** (2026-10-01) — Remote control grew from one Telegram bot into one shared daemon serving Telegram, Discord, and Slack — and the workflow gates that were meant to block unverified work (`approve`/`team` plan identity, `done` re-checks, the autopilot ratchet) now actually block it.
355
- - **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
356
- - **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
357
404
 
358
405
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
359
406
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -54,7 +54,7 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
54
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
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
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.
57
- - **Remote monitoring & control (Telegram, Discord, Slack)** — pair a bot once (`jeo notify setup --provider telegram|discord|slack`), then one shared `jeo daemon start` pushes a message on every subagent state edge (started → done/failed/cancelled) across every live session and accepts `/subagents`, `/steer`, `/cancel` (plus `/sessions` and `/send` on Discord/Slack) back from a human allowlist. Telegram adds forum topics, inline keyboards, and image attachments; Discord uses REST + Gateway WebSocket; Slack uses Web API + Socket Mode. Every remote control is acknowledged by the local session, never assumed executed.
57
+ - **Remote monitoring & control (Telegram, Discord, Slack)** — pair a bot once (`jeo notify setup --provider telegram|discord|slack`), then one shared `jeo daemon start` pushes a message on every subagent state edge (started → done/failed/cancelled) across every live session and accepts `/subagents`, `/steer`, `/cancel` (plus `/sessions` and `/send` on Discord/Slack) back from a human allowlist. Telegram adds forum topics, inline keyboards, and image attachments; Discord uses REST + Gateway WebSocket; Slack uses Web API + Socket Mode. Dedicated standalone setup guides and an installable Agent Skill (`channel-notify`) are included.
58
58
  - **Gates that cannot be talked around** — `approve` re-validates the exact plan digest it is approving and `team` refuses a plan whose reviewed digest changed; a `done` call is re-checked against the latest verification evidence (a later failed check invalidates an earlier pass, three correction bounces then a hard rejection); the `autopilot` ratchet records a failed rollback as `rollback_failed` and halts non-zero instead of claiming a reverted step.
59
59
  - **Session-scoped async execution** — fan out independent work through the `task` tool's real `tasks` array without blocking the parent turn; detached subagents, background jobs, and line monitors remain controllable from later turns with `subagent`/`job`/`monitor` actions (`list`, `inspect`, `await`, `cancel`, `tail`). The inline TUI keeps each worker's live activity in its own slot and tears down every registry on session exit or Ctrl-C.
60
60
  - **Independent verifier, actually enforced** — a plan can no longer skip its architect/critic step: `PlanSchema` rejects any plan that ends with an unverified mutation (a verifier placed BEFORE the mutation it should check doesn't count either), at both `ralplan` draft time and `team`/`approve` execution time. Every architect/critic verdict must also show real evidence — zero observed `read`/`search`/`find`/`ast_grep`/`lsp` calls blocks the verdict regardless of what the text claims.
@@ -112,6 +112,11 @@ Inside the `jeo` REPL (Tab autocompletes; `/` opens the palette).
112
112
 
113
113
  > [!CAUTION]
114
114
  > **`/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.
115
+ ### Antigravity account model discovery
116
+
117
+ **Live model discovery** — `/model provider antigravity` and `/agents <role> provider antigravity` show your OAuth account's actual available models from Cloud Code Assist. The picker honors only models your account holds; no Argon is guaranteed for every account. Empty account lists are reported honestly instead of falling back to static catalogs. Account scope is preserved across session refreshes.
118
+
119
+ **Example:** `jeo auth login antigravity` discovers and caches your account's available models. Pick one with `/model antigravity/<model-id>` or `/agents executor provider antigravity`, then use it for the session or persist it as default via `/model save antigravity/<model-id>`.
115
120
 
116
121
  ## CLI commands
117
122
 
@@ -212,6 +217,13 @@ Non-zero hook output is appended to the tool result the model reads (deduped per
212
217
 
213
218
  Opt-in: one shared daemon watches every live `jeo` session on the machine and pushes a message on each **subagent state edge** (started → completed/failed/cancelled), plus the session identity header, turn start/finish summaries, and the finalized turn text — never repeated "still running" pings. Allowlisted humans can list, send, steer, and cancel from the chat; every control is relayed to the local session and reported as **acknowledged (accepted, not completed)** or **not acknowledged** — the daemon never claims a command ran.
214
219
 
220
+ Detailed standalone guides:
221
+ - 📖 [Channel Integrations Overview](docs/integrations/README.md) — Feature matrix and architecture overview
222
+ - 📱 [Telegram Integration Guide](docs/integrations/telegram.md) — Built-in background daemon, interactive BotFather pairing, forum topics, and `/steer` commands
223
+ - 🎮 [Discord Integration Guide](docs/integrations/discord.md) — Webhooks for instant channel updates, Discord Bot for bidirectional interactions, and embed formats
224
+ - 💬 [Slack Integration Guide](docs/integrations/slack.md) — Incoming Webhooks for workspace notifications, Slack App / Socket Mode for team collaboration, and Block Kit cards
225
+ - 🧩 [Agent Skills Installation Guide](docs/integrations/agent-skills.md) — Install channel notification capabilities into `jeo`, Claude Code, Cursor, Codex, and other agents
226
+
215
227
  ```bash
216
228
  jeo notify setup [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID | --channel-id ID] [--allowed-user-ids ID,ID,...]
217
229
  jeo notify status [--provider …] # masked token, destination, allowlist, daemon state (stopped / stale / initializing / initialized / pairing)
@@ -247,6 +259,27 @@ What the words mean, because they are not interchangeable:
247
259
  └─────────────────────┘ session└──────────────────┘ └───────────────────────────┘
248
260
  ```
249
261
 
262
+ ### Install as an Agent Skill
263
+
264
+ Install channel notification capabilities into `jeo` or any other AI coding agent using the standard [Agent Skills](https://agentskills.io) CLI:
265
+
266
+ ```bash
267
+ # Install channel-notify skill into the current repository:
268
+ npx skills add akillness/jeo-code --skill channel-notify
269
+
270
+ # Or install globally across all your projects:
271
+ npx skills add akillness/jeo-code --skill channel-notify -g
272
+ ```
273
+
274
+ Once installed, agents can automatically broadcast status updates during tasks:
275
+ ```bash
276
+ # One-shot invocation in jeo:
277
+ jeo $channel-notify "All 47 tests passed. Ready for review."
278
+
279
+ # In Claude Code:
280
+ /channel-notify "Build #108 succeeded. PR: https://github.com/..."
281
+ ```
282
+
250
283
  ## Routines (GitHub Actions)
251
284
 
252
285
  ```bash
@@ -356,10 +389,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
356
389
 
357
390
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
358
391
  - **[Unreleased]**
392
+ - **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
393
+ - **[0.11.5]** (2026-10-04) — Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified.
359
394
  - **[0.11.4]** (2026-10-01) — The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire.
360
395
  - **[0.11.3]** (2026-10-01) — Remote control grew from one Telegram bot into one shared daemon serving Telegram, Discord, and Slack — and the workflow gates that were meant to block unverified work (`approve`/`team` plan identity, `done` re-checks, the autopilot ratchet) now actually block it.
361
- - **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
362
- - **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
363
396
 
364
397
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
365
398
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -49,7 +49,7 @@
49
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
50
  - **低价位评分模型路由** — `/goal` 验证器、`critic` 子代理角色,以及未锁定的 `task` 扇出批次,现在默认使用低价位、已配置凭证的模型,而不是悄悄搭乘与它们所评分/执行的工作相同的全价模型(`resolveVerifierModel`,针对浏览器 `verify` 动作按视觉能力过滤,确保纯文本的低价模型不会悄悄丢弃附带的截图)。
51
51
  - **`jeo routine init`** — 生成一个 GitHub Actions 工作流,在 schedule/issue/PR 触发器上以无头模式运行 jeo(`jeo "<prompt>" -p`),运行在 GitHub 自己的 runner 上 — 不需要笔记本电脑,并且不会给 jeo 自身增加任何新的攻击面(没有进程内调度器或 webhook 监听器)。`--dry-run` 可预览,`--no-pr` 则改为直接提交而非默认的每次运行一个 PR。
52
- - **远程监控与控制(Telegram、Discord、Slack)** — 配对一次机器人(`jeo notify setup --provider telegram|discord|slack`)后,一个共享的 `jeo daemon start` 会在所有活跃会话中、子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受来自人类 allowlist 的 `/subagents`、`/steer`、`/cancel`(Discord/Slack 上还有 `/sessions` 和 `/send`)回传。Telegram 额外提供论坛主题、内联键盘和图片附件;Discord 使用 REST + Gateway WebSocket;Slack 使用 Web API + Socket Mode。每一条远程控制都由本地会话确认收到,绝不假定已执行。
52
+ - **远程监控与控制(Telegram、Discord、Slack)** — 配对一次机器人(`jeo notify setup --provider telegram|discord|slack`)后,一个共享的 `jeo daemon start` 会在所有活跃会话中、子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受来自人类 allowlist 的 `/subagents`、`/steer`、`/cancel`(Discord/Slack 上还有 `/sessions` 和 `/send`)回传。Telegram 额外提供论坛主题、内联键盘和图片附件;Discord 使用 REST + Gateway WebSocket;Slack 使用 Web API + Socket Mode。附带各平台详细配置指南及可安装的 Agent Skill(`channel-notify`)。
53
53
  - **无法绕过的门禁** — `approve` 会重新校验它正在批准的那份计划的精确摘要,`team` 会拒绝已审阅摘要发生变化的计划;`done` 调用会对照最新的验证证据重新检查(后来的失败检查会使先前的通过失效,三次纠正往返后硬性拒绝);`autopilot` ratchet 会把失败的回滚记录为 `rollback_failed` 并以非零状态停止,而不是声称该步骤已回退。
54
54
  - **不阻塞回合的会话级异步执行** — 通过 `task` 工具真实的 `tasks` 数组扇出独立工作,不阻塞父回合。detached 子代理、后台 job 和逐行 monitor 可在后续回合中继续使用 `subagent`/`job`/`monitor` 的 `list`、`inspect`、`await`、`cancel`、`tail` 控制。内联 TUI 为每个 worker 保留独立的实时 activity 槽位,并在会话退出或 Ctrl-C 时清理全部 registry。
55
55
  - **独立验证器,真正强制执行** — 计划再也无法跳过 architect/critic 步骤: `PlanSchema` 会拒绝任何以未验证变更结尾的计划(把验证器放在它该检查的变更之前也不算数),在 `ralplan` 起草阶段和 `team`/`approve` 执行阶段都强制生效。每一次 architect/critic 裁决也都必须展示真实证据 — 观测到的 `read`/`search`/`find`/`ast_grep`/`lsp` 调用为零,无论文本怎么声称,裁决都会被拦下。
@@ -107,6 +107,11 @@ jeo --tmux # 在独立 tmux 会话中运行
107
107
  > [!CAUTION]
108
108
  > **通过 `/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` 会按公司各选一个模型、以会话为单位稳定分布(不一定是最强的那个),因此在同一会话内保持固定,而不是逐轮变化。
109
109
 
110
+ ### Antigravity 账户 模型 实时发现
111
+
112
+ **实时模型发现** — `/model provider antigravity` 与 `/agents <角色> provider antigravity` 会显示你的 OAuth 账户在 Cloud Code Assist 实际拥有的模型。选择器仅认可账户持有的模型;并不保证每个账户都备有 Argon。空的账户列表会被诚实报告,不会被静态目录代用。账户范围在会话刷新全程得以保留。
113
+
114
+ **示例:** `jeo auth login antigravity` 会发现并缓存你账户的实际可用模型。选择: `/model antigravity/<模型-id>` 或 `/agents executor provider antigravity`,之后在本会话内使用,或通过 `/model save antigravity/<模型-id>` 设为默认值。
110
115
  ## CLI 命令
111
116
 
112
117
  `jeo --help` 是权威列表;下面是你最常用到的:
@@ -205,6 +210,13 @@ jeo ultragoal
205
210
 
206
211
  可选功能: 一个共享守护进程监视本机上所有活跃的 `jeo` 会话,并在每次**子代理状态变化**(启动 → 完成/失败/取消)时推送消息,另附会话身份头、回合开始/结束摘要以及最终的回合文本 — 绝不会重复"仍在运行"之类的通知。allowlist 中的人类可以在聊天里列出、发送、引导和取消;每一条控制都会转发给本地会话,并报告为**已确认(已接受,未必已完成)**或**未确认** — 守护进程绝不声称某条命令已执行。
207
212
 
213
+ 各平台详细独立指南:
214
+ - 📖 [渠道集成概览与架构](docs/integrations/README.md) — 功能对比矩阵与架构图
215
+ - 📱 [Telegram 集成指南](docs/integrations/telegram.md) — 内置后台守护进程、BotFather 交互配对、论坛主题与 `/steer` 命令
216
+ - 🎮 [Discord 集成指南](docs/integrations/discord.md) — 即时频道通知 Webhook、双向交互机器人与 Embed 卡片规范
217
+ - 💬 [Slack 集成指南](docs/integrations/slack.md) — 工作区通知 Incoming Webhook、团队协作 Slack App / Socket Mode 与 Block Kit 卡片
218
+ - 🧩 [Agent Skill 安装指南](docs/integrations/agent-skills.md) — 将渠道通知技能一键安装至 `jeo`、Claude Code、Cursor、Codex 等环境
219
+
208
220
  ```bash
209
221
  jeo notify setup [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID | --channel-id ID] [--allowed-user-ids ID,ID,...]
210
222
  jeo notify status [--provider …] # masked token, destination, allowlist, daemon state (stopped / stale / initializing / initialized / pairing)
@@ -240,6 +252,41 @@ jeo daemon start|stop|status|reload # the shared daemon has no provider select
240
252
  └─────────────────────┘ session└──────────────────┘ └───────────────────────────┘
241
253
  ```
242
254
 
255
+ ### Discord & Slack (Webhook 与 Bot App)
256
+
257
+ 为团队工作区配置标准 Webhook 或 Bot 凭证环境变量:
258
+
259
+ ```bash
260
+ # Discord Webhook
261
+ export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."
262
+
263
+ # Slack Incoming Webhook
264
+ export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
265
+ ```
266
+
267
+ 支持发送包含任务状态、耗时、Git 提交与 PR 链接的富文本 Embed(Discord) 及 Block Kit 卡片(Slack)。详见 [Discord 指南](docs/integrations/discord.md) 与 [Slack 指南](docs/integrations/slack.md)。
268
+
269
+ ### 安装为 Agent Skill
270
+
271
+ 使用标准的 [Agent Skills](https://agentskills.io) CLI,可将渠道通知技能快速安装到 `jeo` 或其它 AI 编码代理中:
272
+
273
+ ```bash
274
+ # 安装 channel-notify 技能到当前仓库:
275
+ npx skills add akillness/jeo-code --skill channel-notify
276
+
277
+ # 或全局(-g)安装,供所有项目使用:
278
+ npx skills add akillness/jeo-code --skill channel-notify -g
279
+ ```
280
+
281
+ 安装后,代理可在执行任务时自主发送进度通知:
282
+ ```bash
283
+ # 在 jeo 中单次调用:
284
+ jeo $channel-notify "所有 47 个测试均已通过,等待代码审查。"
285
+
286
+ # 在 Claude Code 中调用:
287
+ /channel-notify "构建成功。PR 链接: https://github.com/..."
288
+ ```
289
+
243
290
  ## 例行任务 (GitHub Actions)
244
291
 
245
292
  ```bash
@@ -346,10 +393,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
346
393
 
347
394
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
348
395
  - **[Unreleased]**
396
+ - **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
397
+ - **[0.11.5]** (2026-10-04) — Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified.
349
398
  - **[0.11.4]** (2026-10-01) — The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire.
350
399
  - **[0.11.3]** (2026-10-01) — Remote control grew from one Telegram bot into one shared daemon serving Telegram, Discord, and Slack — and the workflow gates that were meant to block unverified work (`approve`/`team` plan identity, `done` re-checks, the autopilot ratchet) now actually block it.
351
- - **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
352
- - **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
353
400
 
354
401
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
355
402
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -76,7 +76,7 @@ const SEARCH_SYSTEM_PROMPT =
76
76
 
77
77
  // Claude Code OAuth request shape (mirrors src/ai/providers/anthropic.ts —
78
78
  // Anthropic OAuth tokens are scoped to Claude Code-shaped requests).
79
- const CLAUDE_CODE_VERSION = "2.1.63";
79
+ const CLAUDE_CODE_VERSION = "2.1.289";
80
80
  const CLAUDE_CODE_SYSTEM_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
81
81
  const ANTHROPIC_OAUTH_BETA = [
82
82
  "claude-code-20250219",
@@ -14,12 +14,14 @@
14
14
  * corrupt, or stale file only means live discovery must refresh the catalog,
15
15
  * never an error.
16
16
  */
17
+ import { createHash } from "node:crypto";
17
18
  import { mkdir, readFile, writeFile } from "node:fs/promises";
18
19
  import * as os from "node:os";
19
20
  import * as path from "node:path";
20
21
  import { jeoEnv } from "../util/env";
21
- import { recordLiveCodexModels, recordLiveProviderModels, setOpenAIOauthAccountScope } from "./model-catalog";
22
- import { getStoredOAuth } from "../auth/storage";
22
+ import { recordLiveCodexModels, recordLiveProviderModels, setAntigravityAccountScope, setOpenAIOauthAccountScope } from "./model-catalog";
23
+ import type { Credential } from "../auth";
24
+ import { getStoredOAuth, snapshotProvider } from "../auth/storage";
23
25
  import { extractChatgptAccountId } from "./providers/openai-responses";
24
26
  import type { ProviderName } from "./types";
25
27
 
@@ -32,7 +34,7 @@ export interface CachedProviderModels {
32
34
  source: ModelCacheSource;
33
35
  /** Base URL the list came from, so an OpenAI-compatible endpoint's ids stay scoped to it. */
34
36
  baseUrl?: string;
35
- /** Stable OpenAI OAuth account scope for this row; required for account-specific rehydration. */
37
+ /** Stable account scope for OAuth rows; required for OpenAI and Antigravity rows. */
36
38
  accountId?: string;
37
39
  }
38
40
 
@@ -46,7 +48,7 @@ export interface ModelCacheFile {
46
48
  const CACHE_VERSION = 3;
47
49
  /** Refresh in the background once the cache is older than this (6h). */
48
50
  export const MODEL_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
49
- /** Bound other providers' cache rows; preserve complete authoritative OpenAI lists. */
51
+ /** Bound non-authoritative provider rows; preserve complete OpenAI and Antigravity lists. */
50
52
  const MAX_MODELS_PER_PROVIDER = 500;
51
53
 
52
54
  function cacheDir(): string {
@@ -57,6 +59,40 @@ function cachePath(): string {
57
59
  return path.join(cacheDir(), "model-catalog-cache.json");
58
60
  }
59
61
 
62
+ /** Peek at the selected OAuth account without importing or refreshing credentials. */
63
+ export async function antigravityAccountScope(credential?: Credential): Promise<string | undefined> {
64
+ try {
65
+ let selected = credential;
66
+ if (!selected) {
67
+ const own = await snapshotProvider("antigravity");
68
+ const provider = own.oauth ? "antigravity" : "gemini";
69
+ const snapshot = own.oauth ? own : await snapshotProvider("gemini");
70
+ if (!snapshot.oauth) return undefined;
71
+ selected = { kind: "oauth", provider, token: snapshot.oauth };
72
+ }
73
+ if (selected.kind !== "oauth" || (selected.provider !== "antigravity" && selected.provider !== "gemini")) return undefined;
74
+ const providerSlot = selected.provider;
75
+ const stored = await getStoredOAuth(providerSlot).catch(() => undefined);
76
+ const accessMatches = stored?.access === selected.token;
77
+ const email = accessMatches ? stored?.email?.trim().toLowerCase() : "";
78
+ const identity = accessMatches
79
+ ? email
80
+ ? `email:${email}`
81
+ : stored?.refresh
82
+ ? `refresh:${stored.refresh}`
83
+ : `access:${selected.token}`
84
+ : `access:${selected.token}`;
85
+ const projectId = selected.projectId?.trim()
86
+ || (accessMatches ? stored?.projectId?.trim() : undefined)
87
+ || process.env.GOOGLE_CLOUD_PROJECT?.trim()
88
+ || process.env.GOOGLE_CLOUD_PROJECT_ID?.trim()
89
+ || "";
90
+ return createHash("sha256").update(`${providerSlot}\u0000${identity}\u0000${projectId}`, "utf8").digest("hex");
91
+ } catch {
92
+ return undefined;
93
+ }
94
+ }
95
+
60
96
  export function normalizeCacheEntries(raw: unknown): CachedProviderModels[] {
61
97
  if (!Array.isArray(raw)) return [];
62
98
  const out: CachedProviderModels[] = [];
@@ -64,19 +100,21 @@ export function normalizeCacheEntries(raw: unknown): CachedProviderModels[] {
64
100
  const row = entry as Partial<CachedProviderModels>;
65
101
  if (!row || typeof row.provider !== "string" || !Array.isArray(row.models)) continue;
66
102
  const provider = row.provider as ProviderName;
67
- const models = row.models.filter((m): m is string => typeof m === "string" && m.trim().length > 0).slice(0, provider === "openai" ? undefined : MAX_MODELS_PER_PROVIDER);
68
- if (models.length === 0 && (provider !== "openai" || row.models.length !== 0)) continue;
69
103
  const source: ModelCacheSource =
70
104
  row.source === "oauth" || row.source === "api_key" || row.source === "keyless" || row.source === "none" ? row.source : "none";
71
105
  const baseUrl = typeof row.baseUrl === "string" ? row.baseUrl.replace(/\/+$/, "") : undefined;
72
106
  const accountId = typeof row.accountId === "string" ? row.accountId.trim() : "";
73
- if (provider === "openai" && source === "oauth" && !accountId) continue;
107
+ const isScopedOAuth = (provider === "openai" || provider === "antigravity") && source === "oauth";
108
+ if ((isScopedOAuth && !accountId) || (provider === "antigravity" && source !== "oauth")) continue;
109
+ const models = row.models.filter((m): m is string => typeof m === "string" && m.trim().length > 0)
110
+ .slice(0, provider === "openai" || provider === "antigravity" ? undefined : MAX_MODELS_PER_PROVIDER);
111
+ if (models.length === 0 && provider !== "openai" && provider !== "antigravity") continue;
74
112
  out.push({
75
113
  provider,
76
114
  models,
77
115
  source,
78
116
  ...(baseUrl ? { baseUrl } : {}),
79
- ...(provider === "openai" && source === "oauth" ? { accountId } : {}),
117
+ ...(isScopedOAuth ? { accountId } : {}),
80
118
  });
81
119
  }
82
120
  return out;
@@ -115,26 +153,29 @@ export function mergeCacheEntries(
115
153
  results: readonly { provider: ProviderName; models: readonly string[]; ok: boolean; source?: ModelCacheSource; baseUrl?: string; accountId?: string; fallback?: boolean }[],
116
154
  ): CachedProviderModels[] {
117
155
  const byKey = new Map<string, CachedProviderModels>();
118
- const keyOf = (provider: string, baseUrl?: string, accountId?: string, source?: ModelCacheSource) =>
119
- `${provider}\u0000${baseUrl?.replace(/\/+$/, "") ?? ""}\u0000${accountId ?? ""}\u0000${provider === "openai" ? source ?? "none" : ""}`;
156
+ const isScopedOAuth = (provider: ProviderName, source: ModelCacheSource): boolean =>
157
+ (provider === "openai" || provider === "antigravity") && source === "oauth";
158
+ const keyOf = (provider: ProviderName, baseUrl?: string, accountId?: string, source?: ModelCacheSource) =>
159
+ `${provider}\u0000${baseUrl?.replace(/\/+$/, "") ?? ""}\u0000${isScopedOAuth(provider, source ?? "none") ? accountId ?? "" : ""}\u0000${provider === "openai" || provider === "antigravity" ? source ?? "none" : ""}`;
120
160
  for (const entry of previous) {
121
- const isOpenAIOAuth = entry.provider === "openai" && entry.source === "oauth";
122
- if (isOpenAIOAuth && !entry.accountId) continue;
123
- byKey.set(keyOf(entry.provider, entry.baseUrl, isOpenAIOAuth ? entry.accountId : undefined, entry.source), entry);
161
+ const scoped = isScopedOAuth(entry.provider, entry.source);
162
+ if ((scoped && !entry.accountId) || (entry.provider === "antigravity" && entry.source !== "oauth")) continue;
163
+ byKey.set(keyOf(entry.provider, entry.baseUrl, entry.accountId, entry.source), entry);
124
164
  }
125
165
  for (const result of results) {
126
- if (!result.ok || result.fallback || (result.models.length === 0 && result.provider !== "openai")) continue;
127
166
  const source: ModelCacheSource = result.source ?? "none";
128
- const isOpenAIOAuth = result.provider === "openai" && source === "oauth";
167
+ const scoped = isScopedOAuth(result.provider, source);
168
+ const isAntigravity = result.provider === "antigravity";
129
169
  const baseUrl = typeof result.baseUrl === "string" ? result.baseUrl.replace(/\/+$/, "") : undefined;
130
170
  const accountId = typeof result.accountId === "string" ? result.accountId.trim() : "";
131
- if (isOpenAIOAuth && !accountId) continue;
132
- byKey.set(keyOf(result.provider, baseUrl, isOpenAIOAuth ? accountId : undefined, source), {
171
+ if ((scoped && !accountId) || (isAntigravity && source !== "oauth")) continue;
172
+ if (!result.ok || result.fallback || (result.models.length === 0 && result.provider !== "openai" && !isAntigravity)) continue;
173
+ byKey.set(keyOf(result.provider, baseUrl, accountId, source), {
133
174
  provider: result.provider,
134
- models: [...result.models].slice(0, result.provider === "openai" ? undefined : MAX_MODELS_PER_PROVIDER),
175
+ models: [...result.models].slice(0, result.provider === "openai" || isAntigravity ? undefined : MAX_MODELS_PER_PROVIDER),
135
176
  source,
136
177
  ...(baseUrl ? { baseUrl } : {}),
137
- ...(isOpenAIOAuth ? { accountId } : {}),
178
+ ...(scoped ? { accountId } : {}),
138
179
  });
139
180
  }
140
181
  return [...byKey.values()];
@@ -148,9 +189,12 @@ export async function writeModelCache(
148
189
  // explicitly sandboxed JEO_CONFIG_DIR, never the developer's real ~/.jeo.
149
190
  if (process.env.NODE_ENV === "test" && !jeoEnv("CONFIG_DIR")) return;
150
191
  // A failed/fallback-only refresh must not make an old snapshot appear fresh.
151
- const hasFreshResult = results.some(result => result.ok && !result.fallback
152
- && (result.models.length > 0 || result.provider === "openai")
153
- && !(result.provider === "openai" && result.source === "oauth" && !result.accountId?.trim()));
192
+ const hasFreshResult = results.some(result => {
193
+ if (!result.ok || result.fallback) return false;
194
+ if (result.provider === "antigravity") return result.source === "oauth" && !!result.accountId?.trim();
195
+ if (result.provider === "openai") return result.models.length > 0 || !(result.source === "oauth" && !result.accountId?.trim());
196
+ return result.models.length > 0;
197
+ });
154
198
  if (!hasFreshResult) return;
155
199
  try {
156
200
  const existing = await readModelCache();
@@ -169,18 +213,23 @@ export async function writeModelCache(
169
213
  * routing, and — critically — the OAuth Codex gate know the account's real models
170
214
  * BEFORE the first network call of this launch. Returns the number of ids applied.
171
215
  */
172
- export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccountId?: string): number {
216
+ export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccountId?: string, antigravityAccountId?: string): number {
217
+ const openAIScope = typeof openAIOauthAccountId === "string" ? openAIOauthAccountId.trim() : "";
218
+ const antigravityScope = typeof antigravityAccountId === "string" ? antigravityAccountId.trim() : "";
219
+ setAntigravityAccountScope(antigravityScope);
173
220
  if (!cache) return 0;
174
- const accountId = typeof openAIOauthAccountId === "string" ? openAIOauthAccountId.trim() : "";
175
221
  let applied = 0;
176
222
  for (const entry of cache.providers) {
177
223
  const isOpenAIOAuth = entry.provider === "openai" && entry.source === "oauth";
178
- if (isOpenAIOAuth && (!accountId || entry.accountId !== accountId)) continue;
224
+ const isAntigravityOAuth = entry.provider === "antigravity" && entry.source === "oauth";
225
+ if (isOpenAIOAuth && (!openAIScope || entry.accountId !== openAIScope)) continue;
226
+ if (isAntigravityOAuth && (!antigravityScope || entry.accountId !== antigravityScope)) continue;
227
+ if (entry.provider === "antigravity" && !isAntigravityOAuth) continue;
179
228
  recordLiveProviderModels(entry.provider, entry.models, {
180
229
  source: entry.source,
181
230
  baseUrl: entry.baseUrl,
182
- replace: entry.provider === "openai",
183
- ...(isOpenAIOAuth && entry.accountId ? { accountId: entry.accountId } : {}),
231
+ replace: entry.provider === "openai" || entry.provider === "antigravity",
232
+ ...(isOpenAIOAuth || isAntigravityOAuth ? { accountId: entry.accountId } : {}),
184
233
  });
185
234
  if (isOpenAIOAuth && entry.accountId && !entry.baseUrl) {
186
235
  recordLiveCodexModels(entry.models, entry.accountId);
@@ -201,7 +250,9 @@ export async function rehydrateLiveModels(): Promise<ModelCacheFile | null> {
201
250
  } catch {
202
251
  accountId = undefined;
203
252
  }
253
+ const antigravityAccountId = await antigravityAccountScope();
204
254
  setOpenAIOauthAccountScope(accountId);
205
- applyCachedModels(cache, accountId);
255
+ setAntigravityAccountScope(antigravityAccountId);
256
+ applyCachedModels(cache, accountId, antigravityAccountId);
206
257
  return cache;
207
258
  }
@@ -27,10 +27,10 @@ export interface ModelCatalogEntry {
27
27
  /** Canonical ids surfaced as a provider's recommended default — the current
28
28
  * (2026-07 deep-research verified) general-availability flagship per provider,
29
29
  * NOT necessarily the single most expensive/most-capable model (e.g. Anthropic's
30
- * actual top tier, claude-fable-5, is pricier than the "recommended" sonnet-5 —
30
+ * actual top tier, claude-fable-5, is pricier than the "recommended" sonnet-5-5 —
31
31
  * recommended means "solid everyday default", not "strongest"). Re-verify and
32
32
  * update whenever a provider ships a new default-tier release. */
33
- const RECOMMENDED: Record<string, true> = { "claude-sonnet-5": true, "gpt-5.5": true, "gemini-3-flash": true, "antigravity/gemini-pro-agent": true, "qwen2.5": true };
33
+ const RECOMMENDED: Record<string, true> = { "claude-sonnet-5-5": true, "gpt-5.5": true, "gemini-3-flash": true, "antigravity/gemini-pro-agent": true, "qwen2.5": true };
34
34
 
35
35
  export function normalizeModelId(id: string): string {
36
36
  return (id ?? "").trim().toLowerCase();
@@ -80,7 +80,13 @@ const STD: ThinkLevel[] = ["low", "medium", "high"];
80
80
  * (`cheapestCredentialed`/`strongestCredentialed`/`compareStrengthAscending`)
81
81
  * now tiebreak on `releaseDate` directly rather than relying on position. */
82
82
  export const MODEL_CATALOG: readonly CatalogModel[] = [
83
- // Anthropic (newest first: sonnet-5 > fable-5 > opus-4-8 > opus-4-7 > sonnet-4-6 > opus-4-6). 4.5-and-below dropped from jeo's exposed model list per user direction.
83
+ // Anthropic (newest first: sonnet-5-5 > opus-5-5 > sonnet-5 > fable-5 > opus-4-8 > opus-4-7 > sonnet-4-6 > opus-4-6). 4.5-and-below dropped from jeo's exposed model list per user direction.
84
+ // 5.5 generation (platform.claude.com/docs/en/models/{opus-5-5,sonnet-5-5}/overview): dateless pinned wire ids
85
+ // `claude-opus-5-5` (released 2026-09-22) / `claude-sonnet-5-5` (2026-09-28), 1M ctx / 128k sync max output, adaptive
86
+ // thinking (Opus 5.5 always on; Sonnet 5.5 on by default), text+image input. Canonical == wire id; anthropic.ts already
87
+ // classifies major >= 5 as adaptive + display:"summarized".
88
+ { canonical: "claude-sonnet-5-5", provider: "anthropic", providerModel: "claude-sonnet-5-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-09" },
89
+ { canonical: "claude-opus-5-5", provider: "anthropic", providerModel: "claude-opus-5-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-09" },
84
90
  { canonical: "claude-sonnet-5", provider: "anthropic", providerModel: "claude-sonnet-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
85
91
  // Fable 5 = Anthropic's most capable widely-released model (adaptive thinking always on);
86
92
  // Mythos 5 is limited-availability (Project Glasswing) but callable by id for approved accounts.
@@ -196,6 +202,7 @@ export const CODEX_MODELS: readonly string[] = ["gpt-5.5", "gpt-5.4", "gpt-5.4-m
196
202
  const EPHEMERAL_OPENAI_OAUTH_SCOPE = "__jeo-openai-oauth-ephemeral__";
197
203
  let activeOpenAIOauthScope = EPHEMERAL_OPENAI_OAUTH_SCOPE;
198
204
  const liveCodexModels = new Map<string, Set<string>>();
205
+ let activeAntigravityAccountScope: string | undefined;
199
206
 
200
207
  function normalizeOpenAIOauthScope(scope: string | undefined): string {
201
208
  return typeof scope === "string" ? scope.trim() : "";
@@ -230,6 +237,18 @@ export function setOpenAIOauthAccountScope(accountId: string | undefined): void
230
237
  }
231
238
  }
232
239
 
240
+ /** Select the current Antigravity OAuth account and evict every other account's live rows. */
241
+ export function setAntigravityAccountScope(accountId: string | undefined): void {
242
+ const nextScope = normalizeOpenAIOauthScope(accountId) || undefined;
243
+ activeAntigravityAccountScope = nextScope;
244
+ for (const [recordKey, record] of liveProviderModels.entries()) {
245
+ if (record.row.provider === "antigravity" && record.source === "oauth"
246
+ && normalizeOpenAIOauthScope(record.accountId) !== nextScope) {
247
+ removeLiveProviderRecord(recordKey, record);
248
+ }
249
+ }
250
+ }
251
+
233
252
  /** Record ids confirmed by the current account's Codex Responses endpoint. */
234
253
  export function recordLiveCodexModels(ids: readonly string[], accountId?: string): void {
235
254
  const explicitScope = normalizeOpenAIOauthScope(accountId);
@@ -332,8 +351,8 @@ function liveModelRecordKey(
332
351
  accountId?: string,
333
352
  source?: LiveProviderModelSource,
334
353
  ): string {
335
- const normalizedAccountId = provider === "openai" ? normalizeOpenAIOauthScope(accountId) : "";
336
- return `${provider}\u0000${normalizeBaseUrl(baseUrl) ?? ""}\u0000${normalizedAccountId}\u0000${provider === "openai" ? source ?? "none" : ""}\u0000${canonical}`;
354
+ const normalizedAccountId = (provider === "openai" || provider === "antigravity") ? normalizeOpenAIOauthScope(accountId) : "";
355
+ return `${provider}\u0000${normalizeBaseUrl(baseUrl) ?? ""}\u0000${normalizedAccountId}\u0000${provider === "openai" || provider === "antigravity" ? source ?? "none" : ""}\u0000${canonical}`;
337
356
  }
338
357
 
339
358
  function liveModelFallbackRow(provider: ProviderName, canonical: string, providerModel: string): CatalogModel {
@@ -352,8 +371,8 @@ function liveModelFallbackRow(provider: ProviderName, canonical: string, provide
352
371
  };
353
372
  }
354
373
 
355
- /** Record live ids. Complete OpenAI refreshes may replace their matching scope;
356
- * partial observations and other providers remain additive. */
374
+ /** Record live ids. Complete OpenAI and Antigravity refreshes replace their matching scope;
375
+ * partial observations and other providers remain additive. */
357
376
  export function recordLiveProviderModels(
358
377
  provider: ProviderName,
359
378
  ids: readonly string[],
@@ -361,16 +380,26 @@ export function recordLiveProviderModels(
361
380
  ): void {
362
381
  const source = opts.source ?? "none";
363
382
  const baseUrl = normalizeBaseUrl(opts.baseUrl);
364
- const scopedAccountId = provider === "openai" && source === "oauth"
383
+ const scopedAccountId = (provider === "openai" || provider === "antigravity") && source === "oauth"
365
384
  ? normalizeOpenAIOauthScope(opts.accountId) || undefined
366
385
  : undefined;
386
+ // A complete discovery refresh must not reactivate an account that was
387
+ // superseded while its request was in flight. Non-refresh observations may
388
+ // remain hidden until that account is explicitly selected.
389
+ if (provider === "antigravity" && (!scopedAccountId || source !== "oauth")) return;
390
+ if (provider === "antigravity" && opts.replace && scopedAccountId !== activeAntigravityAccountScope) return;
367
391
  if (provider === "openai" && source === "oauth" && scopedAccountId) {
368
392
  setOpenAIOauthAccountScope(scopedAccountId);
369
393
  }
370
- if (provider === "openai" && opts.replace) {
394
+ const shouldReplace = provider === "antigravity" || (provider === "openai" && !!opts.replace);
395
+ if (shouldReplace) {
371
396
  for (const [key, record] of liveProviderModels) {
372
- if (record.row.provider === provider && record.source === source && record.baseUrl === baseUrl
373
- && oauthScope(record.accountId) === oauthScope(scopedAccountId)) {
397
+ const sameScope = provider === "openai"
398
+ ? oauthScope(record.accountId) === oauthScope(scopedAccountId)
399
+ : provider === "antigravity"
400
+ ? normalizeOpenAIOauthScope(record.accountId) === normalizeOpenAIOauthScope(scopedAccountId)
401
+ : true;
402
+ if (record.row.provider === provider && record.source === source && record.baseUrl === baseUrl && sameScope) {
374
403
  removeLiveProviderRecord(key, record);
375
404
  }
376
405
  }
@@ -399,6 +428,10 @@ function liveRecordMatchesConfig(
399
428
  record: LiveProviderModelRecord,
400
429
  config?: { openaiBaseUrl?: string; openaiOauthScope?: string },
401
430
  ): boolean {
431
+ if (record.row.provider === "antigravity") {
432
+ return record.source === "oauth" && activeAntigravityAccountScope !== undefined
433
+ && normalizeOpenAIOauthScope(record.accountId) === activeAntigravityAccountScope;
434
+ }
402
435
  if (record.row.provider !== "openai") return true;
403
436
  const wanted = normalizeBaseUrl(config?.openaiBaseUrl);
404
437
  if (wanted || record.baseUrl) {
@@ -428,7 +461,8 @@ export function liveProviderCatalogModels(config?: { openaiBaseUrl?: string; ope
428
461
  export function findLiveCatalogModel(canonical: string): CatalogModel | undefined {
429
462
  for (const record of liveProviderModels.values()) {
430
463
  if (record.row.canonical !== canonical) continue;
431
- if (record.row.provider === "openai" && record.source === "oauth" && !liveRecordMatchesConfig(record)) continue;
464
+ if ((record.row.provider === "openai" && record.source === "oauth" && !liveRecordMatchesConfig(record))
465
+ || (record.row.provider === "antigravity" && !liveRecordMatchesConfig(record))) continue;
432
466
  return record.row;
433
467
  }
434
468
  return undefined;
@@ -455,6 +489,7 @@ export function isLiveProviderModel(provider: ProviderName, model: string, confi
455
489
  export function resetLiveProviderModels(): void {
456
490
  liveProviderModels.clear();
457
491
  liveModelIdIndex.clear();
492
+ activeAntigravityAccountScope = undefined;
458
493
  }
459
494
 
460
495
 
@@ -17,10 +17,14 @@ import {
17
17
  recordLiveCodexModels,
18
18
  recordLiveProviderModels,
19
19
  setOpenAIOauthAccountScope,
20
+ setAntigravityAccountScope,
20
21
  } from "./model-catalog";
21
22
  import { extractChatgptAccountId } from "./providers/openai-responses";
22
23
  import { getStoredOAuth } from "../auth/storage";
23
24
  import { openaiCompatDef } from "./providers/openai-compatible-catalog";
25
+ import { antigravityAccountScope } from "./model-cache";
26
+ import { getAntigravityUserAgent, resolveAntigravityProjectId } from "./providers/antigravity";
27
+ import { discoverGoogleProjectId, ANTIGRAVITY_DISCOVERY_METADATA } from "../auth/flows/google-project";
24
28
 
25
29
  export interface ProviderModelsResult {
26
30
  provider: ProviderName;
@@ -29,7 +33,7 @@ export interface ProviderModelsResult {
29
33
  ok: boolean;
30
34
  /** How the request authenticated (for display). */
31
35
  source: "oauth" | "api_key" | "keyless" | "none";
32
- /** Stable OpenAI OAuth account scope for account-specific cache rows. */
36
+ /** Opaque OAuth account scope for account-specific cache rows. */
33
37
  accountId?: string;
34
38
  /** Actual custom endpoint queried, for endpoint-scoped persistent caches. */
35
39
  baseUrl?: string;
@@ -46,7 +50,7 @@ export interface DiscoveryOptions {
46
50
  /** Per-request timeout; default 5000ms. */
47
51
  timeoutMs?: number;
48
52
  signal?: AbortSignal;
49
- /** Cap returned ids; OpenAI defaults to the full list, other providers to 100. */
53
+ /** Cap returned ids; OpenAI and Antigravity default to the full account list. */
50
54
  limit?: number;
51
55
  /** Config snapshot used for provider base URLs. */
52
56
  config?: Config;
@@ -147,9 +151,9 @@ export function discoveryRequest(
147
151
  const token = cred?.kind === "oauth" ? cred.token : "";
148
152
  return {
149
153
  url: ANTIGRAVITY_MODELS_URL,
150
- headers: token ? { authorization: `Bearer ${token}`, "content-type": "application/json", "User-Agent": "antigravity/1.104.0" } : {},
154
+ headers: token ? { authorization: `Bearer ${token}`, "content-type": "application/json", "User-Agent": getAntigravityUserAgent() } : {},
151
155
  method: "POST",
152
- body: "{}",
156
+ body: JSON.stringify(cred?.kind === "oauth" && cred.projectId ? { project: cred.projectId } : {}),
153
157
  };
154
158
  }
155
159
  case "ollama": {
@@ -267,7 +271,7 @@ export function parseModelsBody(provider: ProviderName, body: unknown, opts: { o
267
271
  .filter(([id, model]) =>
268
272
  id.length > 0 &&
269
273
  model?.isInternal !== true &&
270
- (agentIds.size > 0 ? agentIds.has(id) : !nonChat.has(id)))
274
+ (Array.isArray(payload.agentModelSorts) ? agentIds.has(id) : !nonChat.has(id)))
271
275
  .map(([id]) => `antigravity/${id}`);
272
276
  }
273
277
  if (provider === "gemini") {
@@ -342,7 +346,7 @@ export async function listProviderModels(
342
346
  opts: DiscoveryOptions = {},
343
347
  ): Promise<ProviderModelsResult> {
344
348
  const fetchImpl = opts.fetchImpl ?? fetch;
345
- const limit = opts.limit ?? (provider === "openai" ? Number.POSITIVE_INFINITY : DEFAULT_LIMIT);
349
+ const limit = opts.limit ?? (provider === "openai" || provider === "antigravity" ? Number.POSITIVE_INFINITY : DEFAULT_LIMIT);
346
350
  let baseUrl = opts.baseUrl;
347
351
 
348
352
  let cred: Credential | undefined;
@@ -376,6 +380,7 @@ export async function listProviderModels(
376
380
  }
377
381
  }
378
382
  if (cred.kind !== "oauth") {
383
+ setAntigravityAccountScope(undefined);
379
384
  return { provider, models: [], ok: false, source, error: "not logged in with Antigravity OAuth" };
380
385
  }
381
386
  }
@@ -417,11 +422,41 @@ export async function listProviderModels(
417
422
  }
418
423
  }
419
424
 
420
- const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred, baseUrl, oauthAccountId);
421
425
  const scope = { ...(oauthAccountId ? { accountId: oauthAccountId } : {}), ...(baseUrl ? { baseUrl } : {}) };
422
426
  const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
423
- const signal = opts.signal ?? AbortSignal.timeout(timeout);
427
+ let signal = opts.signal ? AbortSignal.any([opts.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
428
+ let initialAntigravityScope: string | undefined;
424
429
  try {
430
+ if (provider === "antigravity" && cred?.kind === "oauth") {
431
+ initialAntigravityScope = await antigravityAccountScope(cred);
432
+ scope.accountId = initialAntigravityScope;
433
+ try {
434
+ const projectId = await resolveAntigravityProjectId(cred, {
435
+ signal,
436
+ discover: token => discoverGoogleProjectId(token, {
437
+ fetchImpl,
438
+ metadata: { ...ANTIGRAVITY_DISCOVERY_METADATA },
439
+ extraHeaders: { "User-Agent": getAntigravityUserAgent() },
440
+ alwaysOnboard: true,
441
+ signal,
442
+ requestTimeoutMs: timeout,
443
+ }),
444
+ });
445
+ cred = { ...cred, projectId };
446
+ } catch (error) {
447
+ // The OAuth models endpoint also accepts {}. Project lookup is optional
448
+ // for listing, so an onboarding failure must not hide account models.
449
+ if (opts.signal?.aborted) throw error;
450
+ }
451
+ signal = opts.signal ? AbortSignal.any([opts.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
452
+ scope.accountId = await antigravityAccountScope(cred);
453
+ const currentScope = await antigravityAccountScope();
454
+ if (currentScope !== scope.accountId && currentScope !== initialAntigravityScope) {
455
+ return { provider, models: [], ok: false, source, error: "account changed during model discovery", ...scope };
456
+ }
457
+ setAntigravityAccountScope(scope.accountId);
458
+ }
459
+ const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred, baseUrl, oauthAccountId);
425
460
  const res = await fetchImpl(url, { method: method ?? "GET", headers, body: requestBody, signal });
426
461
  if (!res.ok) {
427
462
  const reason = res.status === 401 || res.status === 403 ? "auth rejected" : `HTTP ${res.status}`;
@@ -436,6 +471,30 @@ export async function listProviderModels(
436
471
  return { provider, models: [], ok: false, source, error: "invalid models response", ...scope };
437
472
  }
438
473
  }
474
+ if (provider === "antigravity") {
475
+ const payload = body as ({ models?: unknown; agentModelSorts?: unknown } & Record<string, unknown>) | null;
476
+ const models = payload?.models;
477
+ const rows = models && typeof models === "object" ? Object.entries(models) : undefined;
478
+ const sorts = payload?.agentModelSorts;
479
+ const invalidSorts = sorts !== undefined && (!Array.isArray(sorts) || sorts.some(sort =>
480
+ !sort || typeof sort !== "object" || !Array.isArray(sort.groups) || sort.groups.some((group: { modelIds?: unknown } | null) =>
481
+ !group || !Array.isArray(group.modelIds) || group.modelIds.some(id => typeof id !== "string" || !id.trim()))));
482
+ const invalidRoles = ["tabModelIds", "imageGenerationModelIds", "audioTranscriptionModelIds", "commitMessageModelIds", "mqueryModelIds"].some(field => {
483
+ const ids = payload?.[field];
484
+ return ids !== undefined && (!Array.isArray(ids) || ids.some(id => typeof id !== "string" || !id.trim()));
485
+ });
486
+ const deprecated = payload?.deprecatedModelIds;
487
+ const invalidDeprecated = deprecated !== undefined && (Array.isArray(deprecated)
488
+ ? deprecated.some(id => typeof id !== "string" || !id.trim())
489
+ : !deprecated || typeof deprecated !== "object");
490
+ if (!rows || rows.some(([id, row]) => !row || typeof row !== "object" || Array.isArray(row)
491
+ || (Array.isArray(models)
492
+ ? ![row.slug, row.id, row.name].some(value => typeof value === "string" && value.trim().length > 0)
493
+ : !id.trim()))
494
+ || invalidSorts || invalidRoles || invalidDeprecated) {
495
+ return { provider, models: [], ok: false, source, error: "invalid models response", ...scope };
496
+ }
497
+ }
439
498
  let ids = parseModelsBody(provider, body, { openaiCompatible: provider === "openai" && !!baseUrl });
440
499
  // Gemini paginates: follow nextPageToken (bounded) so the available list is
441
500
  // COMPLETE — page 1 alone silently dropped the newest models (round-15).
@@ -452,14 +511,21 @@ export async function listProviderModels(
452
511
  }
453
512
  const allModels = [...new Set(ids)].sort();
454
513
  const models = allModels.slice(0, limit);
514
+ if (provider === "antigravity") {
515
+ const currentScope = await antigravityAccountScope();
516
+ if (currentScope !== scope.accountId && currentScope !== initialAntigravityScope) {
517
+ return { provider, models: [], ok: false, source, error: "account changed during model discovery", ...scope };
518
+ }
519
+ setAntigravityAccountScope(scope.accountId);
520
+ }
455
521
  // Record live ids for routing supplements. For OpenAI OAuth, keep the
456
522
  // existing Codex allow-list widening separate: API-key/custom-base discovery
457
523
  // must not make OAuth Codex calls accept unrelated API models.
458
524
  recordLiveProviderModels(provider, allModels, {
459
525
  source,
460
526
  baseUrl,
461
- replace: provider === "openai",
462
- ...(provider === "openai" && source === "oauth" && oauthAccountId ? { accountId: oauthAccountId } : {}),
527
+ replace: provider === "openai" || provider === "antigravity",
528
+ ...(source === "oauth" && scope.accountId ? { accountId: scope.accountId } : {}),
463
529
  });
464
530
  if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthAccountId);
465
531
  return {
@@ -470,7 +536,7 @@ export async function listProviderModels(
470
536
  ...scope,
471
537
  };
472
538
  } catch (err) {
473
- const msg = (err as Error)?.name === "TimeoutError" || (err as Error)?.name === "AbortError" ? "timeout" : "unreachable";
539
+ const msg = signal.aborted || (err as Error)?.name === "TimeoutError" || (err as Error)?.name === "AbortError" ? "timeout" : "unreachable";
474
540
  return { provider, models: [], ok: false, source, error: msg, ...scope };
475
541
  }
476
542
  }
@@ -15,6 +15,12 @@ export const BUILTIN_ALIASES: ModelAliases = {
15
15
  flash: "gemini-2.5-flash",
16
16
  grok: "grok-4.3",
17
17
  kimi: "kimi-k2-0711-preview",
18
+ // Anthropic 5.5 generation: spellings that name the version. Bare `opus`/`sonnet` above stay on the
19
+ // 4.6 ids (pinned backward-compat); users opt into 5.5 by naming the version.
20
+ "opus-5.5": "claude-opus-5-5",
21
+ "sonnet-5.5": "claude-sonnet-5-5",
22
+ "opus5.5": "claude-opus-5-5",
23
+ "sonnet5.5": "claude-sonnet-5-5",
18
24
  // Antigravity's backend DEPRECATED this wire id (deprecatedModelIds:
19
25
  // { "gemini-3.1-pro-high": { newModelId: "gemini-pro-agent" } }) — keep
20
26
  // existing configs/roles pinned to the old id working on the successor.
package/src/ai/pricing.ts CHANGED
@@ -30,6 +30,10 @@ const PRICE_TABLE: ReadonlyArray<readonly [pattern: string, price: ModelPrice]>
30
30
  ["claude-opus-4-8", { inPerM: 5, outPerM: 25 }],
31
31
  ["claude-fable", { inPerM: 10, outPerM: 50 }],
32
32
  ["claude-mythos", { inPerM: 10, outPerM: 50 }],
33
+ // Opus 5.5 = $4/$20 (Anthropic pricing page). MUST precede the generic `claude-opus` row or the
34
+ // substring match prices it at the legacy $15/$75 and skews cost-ranked routing. Sonnet 5.5 shares
35
+ // Sonnet 5's $2/$10 and is already covered by the `claude-sonnet-5` row below.
36
+ ["claude-opus-5-5", { inPerM: 4, outPerM: 20 }],
33
37
  ["claude-opus", { inPerM: 15, outPerM: 75 }],
34
38
  ["claude-sonnet-5", { inPerM: 2, outPerM: 10 }],
35
39
  ["claude-sonnet", { inPerM: 3, outPerM: 15 }],
@@ -10,7 +10,7 @@ import { jeoEnv } from "../../util/env";
10
10
  const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
11
11
 
12
12
  const DEPRECATED_TEMPERATURE = "`temperature` is deprecated for this model.";
13
- const CLAUDE_CODE_VERSION = "2.1.63";
13
+ const CLAUDE_CODE_VERSION = "2.1.289";
14
14
  const CLAUDE_CODE_SYSTEM_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
15
15
  const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
16
16
  /** Betas needed for API-key requests: interleaved-thinking enables thinking+tools,
@@ -324,7 +324,11 @@ export function anthropicPayload(
324
324
  if (thinkingMode === "budget-effort" && !disableEffort) payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
325
325
  }
326
326
  } else if (includeTemperature && options.temperature !== undefined) {
327
- payload.temperature = options.temperature;
327
+ // Opus/Sonnet 5.5 reject custom sampling even without explicit thinking.
328
+ const version = parseAnthropicVersion(model);
329
+ const fixedSampling = version && (version.kind === "opus" || version.kind === "sonnet")
330
+ && (version.major > 5 || (version.major === 5 && version.minor >= 5));
331
+ if (!fixedSampling) payload.temperature = options.temperature;
328
332
  }
329
333
  if (options.tools?.length) {
330
334
  // NATIVE tool-calling: declare jeo's tools as Anthropic functions. tool_choice
@@ -6,6 +6,7 @@ import { providerHttpError, ProviderStreamError } from "./errors";
6
6
  import { serializeToolCalls } from "../../agent/tool-schemas";
7
7
  import { sanitizeJsonStrings } from "../../util/sanitize-json";
8
8
  import { geminiThinkingConfig, type GeminiThinkingConfig } from "./gemini";
9
+ import { saveConfigPatch } from "../../agent/state";
9
10
 
10
11
  const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
11
12
  const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
@@ -133,10 +134,12 @@ export async function resolveAntigravityProjectId(
133
134
  discoveredProjects.set(credential.token, projectId);
134
135
 
135
136
  const persist = opts.persist ?? (async (id: string) => {
136
- const { getStoredOAuth, setOauthCredential } = await import("../../auth/storage");
137
137
  const owner = credential.provider === "antigravity" ? "antigravity" : "gemini";
138
- const stored = await getStoredOAuth(owner);
139
- if (stored && !stored.projectId) await setOauthCredential(owner, { ...stored, projectId: id });
138
+ await saveConfigPatch(raw => {
139
+ const stored = raw.oauth?.[owner];
140
+ if (!stored || typeof stored === "string" || stored.access !== credential.token || stored.projectId) return {};
141
+ return { oauth: { ...raw.oauth, [owner]: { ...stored, projectId: id } } };
142
+ });
140
143
  });
141
144
  try {
142
145
  await persist(projectId);