jeo-code 0.7.40 → 0.7.41
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 +14 -0
- package/README.ja.md +54 -2
- package/README.ko.md +54 -2
- package/README.md +29 -2
- package/README.zh.md +54 -2
- package/package.json +1 -1
- package/src/agent/engine.ts +22 -2
- package/src/ai/providers/anthropic.ts +18 -8
- package/src/ai/providers/antigravity.ts +18 -3
- package/src/util/provider-error.ts +1 -1
- package/src/util/retry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ 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.7.41] - 2026-07-06
|
|
10
|
+
_Refusal-handling workflow audit (follow-up to 0.7.39): fixes a HIGH-severity, long-masked bug where Gemini's real refusal error shape never matched jeo's refusal detector, plus Antigravity's opaque empty-response error, plus a category-aware fail-fast for deterministic ToS-severity refusals that previously spun the full 30-minute stall budget with zero chance of success._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Gemini `SAFETY`/`PROHIBITED_CONTENT`/`BLOCKLIST` refusals were never detected — masked by drifted tests since ~0.7.27.** `isRefusalError` (`src/util/retry.ts`) matched a bare `(SAFETY)` shape, but `gemini.ts`'s `blockedReason()` always emits a prefixed `blockReason=SAFETY`/`finishReason=SAFETY` (API-key call, API-key stream, and the Cloud Code Assist/OAuth path all affected). The regex now matches `(?:block|finish)Reason=(?:SAFETY|PROHIBITED_CONTENT|BLOCKLIST|RECITATION|SPII)` — the last two (`RECITATION`, `SPII`) were entirely uncovered before. Previously: a Gemini safety refusal was blindly transport-retried with the identical (deterministically re-refusing) payload, billed each time, then reached the engine with no refusal ladder and no friendly message — just the raw error. Test fixtures in `test/refusal-recovery.test.ts`/`test/retry.test.ts` that asserted against hand-written literals `blockedReason()` never actually produces (giving false-green coverage) are corrected to the real production shape.
|
|
14
|
+
- **Antigravity's empty-completion error carried no reason and wasn't even transport-retried.** `Antigravity Cloud Code Assist returned an empty response.` (both `call` and `stream`) is now `... returned no content (finishReason=<X>).` when the response carries a `finishReason` — aligning the "returned no content" wording with `defaultRetryable`'s existing transient-empty-200 branch (the old wording didn't match it at all, so a genuinely transient empty reply wasn't even retried once) and making Antigravity refusals detectable by the same fixed regex above.
|
|
15
|
+
- **The refusal ladder's final rung could spin for the full ~30-minute stall budget on a refusal that can never succeed.** A `Refusal (<category>)`-shaped refusal (e.g. Anthropic's `reasoning_extraction`, 0.7.39) is a classification of the request content itself — by the time rung 4 is reached, context has already been minimized (rungs 1-3), so every further identical resend re-refuses identically. Rung 4 now fails fast with the category-aware `friendlyProviderError` message for this shape instead of entering unbounded backoff, while every other (genuinely time-sensitive, rolling-classifier) refusal shape keeps the existing "never terminal" backoff behavior unchanged.
|
|
16
|
+
- **The rung-4 auto-retry notice's `(Esc to cancel)` hint is now only shown when a TUI is actually attached** (keyed off `ev.onModelStream`, which only interactive consumers set) — a one-shot/piped invocation no longer shows a cancel hint for a key it has no live handler for.
|
|
17
|
+
- **Anthropic's `stop_details.category` is now captured defensively in the 200-body and stream response paths, not just the HTTP-error path.** `emptyCompletionError` folds a present category into `(stop_reason=refusal, category=<X>)`, and `friendlyProviderError`'s category extraction now recognizes both the `Refusal (<X>)` (HTTP-error) and `category=<X>` (body/stream) shapes — so the `reasoning_extraction` clarifying note keeps applying regardless of which form Anthropic uses to deliver a given refusal.
|
|
18
|
+
|
|
19
|
+
### Verified
|
|
20
|
+
- `bun run typecheck` clean; full suite green.
|
|
21
|
+
- New/updated coverage: `test/refusal-recovery.test.ts`, `test/retry.test.ts` (real Gemini production-shape fixtures + `RECITATION`/`SPII`/`blockReason=` cases, category-aware rung-4 fail-fast vs. regression guard that non-category refusals still back off), `test/antigravity.test.ts` (SAFETY/RECITATION/no-reason cases), `test/anthropic-stream.test.ts` (`stop_details.category` capture in both `call()` and `stream()`).
|
|
22
|
+
|
|
9
23
|
## [0.7.40] - 2026-07-06
|
|
10
24
|
_Fixes the release CI's standalone-binary build, broken since before v0.7.31 (last release where that job actually ran and passed) — the `release-binaries` job on v0.7.38 and v0.7.39 both failed silently on every platform target, so neither release has downloadable binaries attached._
|
|
11
25
|
|
package/README.ja.md
CHANGED
|
@@ -38,13 +38,15 @@
|
|
|
38
38
|
|
|
39
39
|
## ハイライト
|
|
40
40
|
|
|
41
|
-
- **マルチプロバイダ・単一ループ** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama を均一な JSON ツールループで。入力欄から OAuth ログイン(`/provider login`)、モデル選択は即座にデフォルトとして永続化。
|
|
41
|
+
- **マルチプロバイダ・単一ループ** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio に加え、OpenAI・Anthropic 互換クラウド20以上(Groq、DeepSeek、Mistral、OpenRouter、xAI、Kimi、z.ai など)を均一な JSON ツールループで。入力欄から OAuth ログイン(`/provider login`)、モデル選択は即座にデフォルトとして永続化。
|
|
42
42
|
- **編集の完全性** — read 出力にコンテンツアンカー(`42ab|`)が付き、アンカー付き編集は現在のファイルと照合・行移動時は自動再マッピング・不一致時は最新内容と共に拒否されます。
|
|
43
43
|
- **自己修正の検証ループ** — post-edit フック(tsc / eslint / テスト)を設定すると、エージェントが診断を*自ら読み*ループ内で修正します。フックが赤のままだと `done` はブロックされます。
|
|
44
44
|
- **芝居なしの本物のゲート** — `ralplan` の合議はリポジトリを実際に読む critic サブエージェントで、`[OKAY]` 評決が永続化され `jeo approve` がそれを*要求*します。`ultragoal` は誠実に報告します(スイート1回実行はグローバル信号であり、基準ごとの合格を捏造しません)。
|
|
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 側の別ダウンロードです)。
|
|
49
|
+
- **リモートサブエージェント可視化(Telegram)** — ボットを一度ペアリング(`jeo notify setup`)すれば、`jeo daemon start` がサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、`/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` を受け付けます — コマンドはペアリングされたチャットのみ許可されます。
|
|
48
50
|
|
|
49
51
|
## インストール
|
|
50
52
|
|
|
@@ -163,6 +165,31 @@ jeo ultragoal
|
|
|
163
165
|
|
|
164
166
|
`--worktree <name>` は隔離された兄弟 git worktree で jeo を実行するため(パスがあれば再利用、なければ basename ブランチで作成)、リスクのある作業やレビュー対象の作業がメインのチェックアウトに触れることはありません。`jeo mcp serve` は stdio を介して MCP 対応のあらゆるコントローラーに jeo のツールを公開します(`jeo mcp tools` で一覧表示)。`-q`/`--quiet` (または `JEO_QUIET=1`) を追加すると、起動バナー・ウェルカムアニメーション・リリースノート・再開ヒントが抑制され、jeo を別のエージェントと並べて実行したりボットから駆動したりできます。`-p`/`--print` は quiet を含みます。
|
|
165
167
|
|
|
168
|
+
## リモート監視・制御(Telegram)
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
jeo notify setup # BotFather ボットを一度ペアリング(getMe 検証 + chat-id ペアリング)
|
|
172
|
+
jeo notify status # マスクされたトークン、ペアリング済み chat id、デーモン状態
|
|
173
|
+
jeo daemon start # シングルトンのバックグラウンドデーモンを起動
|
|
174
|
+
jeo daemon status # 実行中かどうかを確認
|
|
175
|
+
jeo daemon stop # SIGTERM で停止
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|
180
|
+
│ interactive turn │◄──ws──►│ notify daemon │◄─poll──►│ Telegram bot │
|
|
181
|
+
│ SubagentRegistry │ │ (singleton) │ │ (paired chat) │
|
|
182
|
+
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
オプトインかつ遅延バインド: `notifications.enabled` が設定され、かつ detached サブエージェント(`task {detached:true}`)が実際に実行されるまで何もバインドされません。デーモンは生存中のセッションディスカバリファイルをスキャンし、セッションごとにループバック WebSocket を接続、サブエージェントの状態 *遷移*(開始 → 完了/失敗/キャンセル)時にのみメッセージを送信します — 「実行中のまま」の繰り返し通知はありません。受信した Telegram コマンドはペアリング済みチャットのみ許可され、それ以外は黙って破棄されます。
|
|
186
|
+
|
|
187
|
+
| コマンド | 効果 |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `/subagents` | 接続中の全セッションの実行中/最近のサブエージェント一覧 |
|
|
190
|
+
| `/steer <sessionId> <subagentId> <message>` | 実行中のサブエージェントへライブメッセージを送信 |
|
|
191
|
+
| `/cancel <sessionId> <subagentId>` | 実行中のサブエージェントをキャンセル |
|
|
192
|
+
| `/help` | コマンドリファレンスを表示 |
|
|
166
193
|
|
|
167
194
|
## ローカルモデル
|
|
168
195
|
|
|
@@ -186,11 +213,36 @@ JEO_TUI_ALT_SCREEN=1 # レガシー alt-screen ターン(デフォル
|
|
|
186
213
|
JEO_STEP_BASE=24 # 動的ステップ予算のローリングベース
|
|
187
214
|
JEO_STEP_HARD_CAP=600 # 絶対終了保証
|
|
188
215
|
JEO_STREAM_MAX_MS=300000 # オプトインの全体ストリーム期限(デフォルト off; スロードリップ遮断)
|
|
216
|
+
JEO_STREAM_IDLE_MS=300000 # チャンク単位のアイドル上限(デフォルト300秒); 最初のトークン前が長い低速/ローカルバックエンドでは引き上げてください
|
|
189
217
|
JEO_TOOL_OUTPUT_MAX=4000 # モデル可視のツール出力上限(全文はアーティファクトへ)
|
|
190
218
|
```
|
|
191
219
|
|
|
192
220
|
リトライ動作は `~/.jeo/config.json` の `retry` で調整します(`requestMaxRetries`、`streamMaxRetries`、`rateLimitRetries`、`failFastStatuses` など)。ステップ予算はデフォルトで動的 — 新規進捗が見える間は延長され、停滞時は要約に収束します。`--max-steps N` で有限フローに戻ります。
|
|
193
221
|
|
|
222
|
+
## スキル移行とバンドルスキルの確認
|
|
223
|
+
|
|
224
|
+
ワークフローを jeo に移す際は、何かをインストール・上書きする前にバンドルされたデフォルトを確認してください:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
jeo skills list # バンドル + ユーザー + プロジェクトのスキル、発見ディレクトリ付き
|
|
228
|
+
jeo skills read ralplan # 1つのスキルの完全な SKILL.md を出力
|
|
229
|
+
jeo skills sync --check # ~/.jeo/skills との差分をレポート(差分があれば非ゼロ終了)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`jeo skills sync` はバンドルされたワークフロースキル(deep-interview、deep-dive、ralplan、team、ultragoal)を `~/.jeo/skills` にインストールし、**デフォルトで既存のローカルファイルを保持します** — 異なるローカルコピーは上書きされず `preserved` として報告されます。`--check` が欠落または差分のあるファイルを検出したら、まず `jeo skills read <name>` で比較してください。ローカルのデフォルトワークフロースキルファイルを意図的に置き換えたい場合のみ `jeo skills sync --force` を使用してください。末尾のパス引数(または `JEO_CONFIG_DIR`)で別のディレクトリを指定でき、`--json` で構造化された `SkillSyncResult` を取得できます。
|
|
233
|
+
|
|
234
|
+
## 開発
|
|
235
|
+
|
|
236
|
+
jeo は Bun 上の純粋な TypeScript で **ネイティブ依存ゼロ** なので、グローバルの `jeo` コマンドはこのチェックアウトのソースを直接実行できます — ビルド不要、あらゆる編集に即座に反映されます。
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
bun install
|
|
240
|
+
bun run dev:link # `jeo` を <repo>/src/cli.ts へシンボリックリンク -> ~/.local/bin
|
|
241
|
+
bun run dev:doctor # グローバル `jeo` がこのソースを実行しているか報告(linked/drift/missing)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`dev:link` は `PATH` 上で管理対象リンクより先に別の `jeo` が存在する場合は進行を拒否し(宛先は `JEO_DEV_LINK_DIR` で上書き可能)、`--version` のスモークテストを実行します。`dev:doctor` は解決された `jeo` がこのソースではなくコンパイル済みバイナリやインストール済みコピーの場合に非ゼロで終了します。リンクせずソースから直接実行するには `bun src/cli.ts --help`。バンドルされたワークフロースキルはソース内の `src/prompts/skills/<name>/SKILL.md` にあります; `bun run typecheck` と `bun test` で検証してください。
|
|
245
|
+
|
|
194
246
|
## 公開 (Publishing)
|
|
195
247
|
|
|
196
248
|
CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリース公開時に自動、または `workflow_dispatch` の手動実行(ドライラン可)。ワークフローは型チェック・テスト・トークン検証(`npm whoami`)の後、`npm publish --provenance` を実行します。
|
|
@@ -207,11 +259,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
207
259
|
## 変更履歴 (Changelog)
|
|
208
260
|
|
|
209
261
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
262
|
+
- **[0.7.41]** (2026-07-06) — Refusal-handling workflow audit (follow-up to 0.7.39): fixes a HIGH-severity, long-masked bug where Gemini's real refusal error shape never matched jeo's refusal detector, plus Antigravity's opaque empty-response error, plus a category-aware fail-fast for deterministic ToS-severity refusals that previously spun the full 30-minute stall budget with zero chance of success.
|
|
210
263
|
- **[0.7.40]** (2026-07-06) — Fixes the release CI's standalone-binary build, broken since before v0.7.31 (last release where that job actually ran and passed) — the `release-binaries` job on v0.7.38 and v0.7.39 both failed silently on every platform target, so neither release has downloadable binaries attached.
|
|
211
264
|
- **[0.7.39]** (2026-07-05) — Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder.
|
|
212
265
|
- **[0.7.38]** (2026-07-05) — Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes.
|
|
213
266
|
- **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
214
|
-
- **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
|
|
215
267
|
|
|
216
268
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
217
269
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -38,13 +38,15 @@
|
|
|
38
38
|
|
|
39
39
|
## 하이라이트
|
|
40
40
|
|
|
41
|
-
- **멀티 프로바이더, 단일 루프** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama
|
|
41
|
+
- **멀티 프로바이더, 단일 루프** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio, 그리고 OpenAI·Anthropic 호환 클라우드 20종 이상(Groq, DeepSeek, Mistral, OpenRouter, xAI, Kimi, z.ai 등)까지 균일한 JSON 도구 루프로. 입력창에서 바로 OAuth 로그인(`/provider login`), 모델 선택은 즉시 기본값으로 영속.
|
|
42
42
|
- **편집 무결성** — read 출력에 콘텐츠 앵커(`42ab|`)가 붙고, 앵커 편집은 현재 파일과 대조 검증·줄 이동 시 자동 재매핑·불일치 시 최신 내용과 함께 거부 — 파일을 오염시키지 않습니다.
|
|
43
43
|
- **자기수정 검증 루프** — post-edit 훅(tsc / eslint / 테스트)을 설정하면 에이전트가 진단을 *직접 읽고* 루프 안에서 수정합니다. 훅이 빨간 상태면 `done`이 차단됩니다.
|
|
44
44
|
- **연극 없는 진짜 게이트** — `ralplan` 합의는 실제 저장소를 읽는 critic 서브에이전트이며 `[OKAY]` 평결이 영속되고 `jeo approve`가 이를 *요구*합니다. `ultragoal`은 정직하게 보고합니다(스위트 1회 실행은 전역 신호일 뿐, 기준별 통과를 조작하지 않음).
|
|
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의 별도 다운로드).
|
|
49
|
+
- **원격 서브에이전트 가시성(Telegram)** — 봇을 한 번 페어링(`jeo notify setup`)하면, `jeo daemon start`가 서브에이전트 상태 전환(시작 → 완료/실패/취소)마다 메시지를 보내고 `/subagents`, `/steer <id> <subagentId> <msg>`, `/cancel <id> <subagentId>`를 되받습니다 — 명령은 페어링된 채팅에서만 허용됩니다.
|
|
48
50
|
|
|
49
51
|
## 설치
|
|
50
52
|
|
|
@@ -163,6 +165,31 @@ jeo ultragoal
|
|
|
163
165
|
|
|
164
166
|
`--worktree <name>`는 격리된 형제 git worktree에서 jeo를 실행하므로(경로가 있으면 재사용, 없으면 basename 브랜치로 생성) 위험하거나 검토가 필요한 작업이 메인 체크아웃을 건드리지 않습니다. `jeo mcp serve`는 stdio를 통해 MCP를 지원하는 모든 컨트롤러에 jeo의 도구를 노출합니다(`jeo mcp tools`로 목록 확인). `-q`/`--quiet` (또는 `JEO_QUIET=1`)를 추가하면 시작 배너, 환영 애니메이션, 릴리스 노트, 재개 힌트가 억제되어 jeo를 다른 에이전트와 나란히 실행하거나 봇으로 구동할 수 있습니다. `-p`/`--print`는 quiet를 함의합니다.
|
|
165
167
|
|
|
168
|
+
## 원격 모니터링 & 제어 (Telegram)
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
jeo notify setup # BotFather 봇 한 번 페어링 (getMe 검증 + chat-id 페어링)
|
|
172
|
+
jeo notify status # 마스킹된 토큰, 페어링된 chat id, 데몬 상태
|
|
173
|
+
jeo daemon start # 싱글턴 백그라운드 데몬 실행
|
|
174
|
+
jeo daemon status # 실행 여부 확인
|
|
175
|
+
jeo daemon stop # SIGTERM으로 종료
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|
180
|
+
│ interactive turn │◄──ws──►│ notify daemon │◄─poll──►│ Telegram bot │
|
|
181
|
+
│ SubagentRegistry │ │ (singleton) │ │ (paired chat) │
|
|
182
|
+
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
옵트인이며 지연 바인딩됩니다: `notifications.enabled`가 설정되고 detached 서브에이전트(`task {detached:true}`)가 실제로 실행되어야만 동작합니다. 데몬은 살아있는 세션 디스커버리 파일을 스캔해 세션별로 루프백 WebSocket을 연결하고, 서브에이전트 상태 *전환* 시점(시작 → 완료/실패/취소)에만 메시지를 보냅니다 — "여전히 실행 중" 같은 반복 알림은 없습니다. 수신되는 Telegram 명령은 페어링된 채팅에서만 허용되며, 그 외는 조용히 무시됩니다.
|
|
186
|
+
|
|
187
|
+
| 명령 | 동작 |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `/subagents` | 연결된 모든 세션의 실행 중/최근 서브에이전트 목록 |
|
|
190
|
+
| `/steer <sessionId> <subagentId> <message>` | 실행 중인 서브에이전트에 실시간 메시지 전송 |
|
|
191
|
+
| `/cancel <sessionId> <subagentId>` | 실행 중인 서브에이전트 취소 |
|
|
192
|
+
| `/help` | 명령 안내 표시 |
|
|
166
193
|
|
|
167
194
|
## 로컬 모델
|
|
168
195
|
|
|
@@ -186,11 +213,36 @@ JEO_TUI_ALT_SCREEN=1 # 레거시 alt-screen 턴(기본: 인라인 스
|
|
|
186
213
|
JEO_STEP_BASE=24 # 동적 스텝 버짓의 롤링 베이스
|
|
187
214
|
JEO_STEP_HARD_CAP=600 # 절대 종료 보증
|
|
188
215
|
JEO_STREAM_MAX_MS=300000 # 옵트인 전체 스트림 데드라인(기본 off; 슬로우드립 차단)
|
|
216
|
+
JEO_STREAM_IDLE_MS=300000 # 청크당 유휴 타임아웃(기본 300초); 첫 토큰 전 침묵이 긴 느린/로컬 백엔드는 값을 높이세요
|
|
189
217
|
JEO_TOOL_OUTPUT_MAX=4000 # 모델 가시 도구 출력 캡(전체는 아티팩트로 스필)
|
|
190
218
|
```
|
|
191
219
|
|
|
192
220
|
재시도 동작은 `~/.jeo/config.json`의 `retry`로 조정합니다(`requestMaxRetries`, `streamMaxRetries`, `rateLimitRetries`, `failFastStatuses`, …). 스텝 버짓은 기본 동적 — 새로운 진전이 보이는 동안 연장되고 정체 시 요약으로 수렴하며, `--max-steps N`이면 유한 플로로 복귀합니다.
|
|
193
221
|
|
|
222
|
+
## 스킬 마이그레이션 및 번들 스킬 확인
|
|
223
|
+
|
|
224
|
+
워크플로를 jeo로 옮기기 전에, 무언가를 설치하거나 덮어쓰기 전에 먼저 번들 기본값을 확인하세요:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
jeo skills list # 번들 + 사용자 + 프로젝트 스킬, 디스커버리 디렉터리 포함
|
|
228
|
+
jeo skills read ralplan # 스킬 하나의 전체 SKILL.md 출력
|
|
229
|
+
jeo skills sync --check # ~/.jeo/skills 대비 drift 리포트 (drift 시 non-zero exit)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`jeo skills sync`는 번들 워크플로 스킬(deep-interview, deep-dive, ralplan, team, ultragoal)을 `~/.jeo/skills`에 설치하며 **기본적으로 기존 로컬 파일을 보존**합니다 — 다른 로컬 사본은 덮어쓰지 않고 `preserved`로 보고됩니다. `--check`가 누락되거나 다른 파일을 플래그하면 먼저 `jeo skills read <name>`으로 비교하세요; 로컬 기본 워크플로 스킬 파일을 의도적으로 교체하고 싶을 때만 `jeo skills sync --force`를 사용하세요. 후행 경로 인자(또는 `JEO_CONFIG_DIR`)로 다른 디렉터리를 지정할 수 있고, `--json`으로 구조화된 `SkillSyncResult`를 받을 수 있습니다.
|
|
233
|
+
|
|
234
|
+
## 개발
|
|
235
|
+
|
|
236
|
+
jeo는 Bun 위의 순수 TypeScript이며 **네이티브 의존성이 0**이라, 전역 `jeo` 명령이 이 체크아웃의 소스를 직접 실행할 수 있습니다 — 빌드 단계 없이, 모든 수정이 즉시 반영됩니다.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
bun install
|
|
240
|
+
bun run dev:link # `jeo`를 <repo>/src/cli.ts에 심볼릭 링크 -> ~/.local/bin
|
|
241
|
+
bun run dev:doctor # 전역 `jeo`가 이 소스를 실행하는지 보고(linked/drift/missing)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`dev:link`는 `PATH`에서 관리되는 링크보다 먼저 오는 다른 `jeo`가 있으면 진행을 거부하고(대상은 `JEO_DEV_LINK_DIR`로 재정의), `--version` 스모크 테스트를 실행합니다. `dev:doctor`는 해석된 `jeo`가 이 소스가 아니라 컴파일된 바이너리나 설치된 사본이면 non-zero로 종료합니다. 링크 없이 소스에서 바로 실행하려면 `bun src/cli.ts --help`. 번들 워크플로 스킬은 소스의 `src/prompts/skills/<name>/SKILL.md`에 있습니다; `bun run typecheck`와 `bun test`로 검증하세요.
|
|
245
|
+
|
|
194
246
|
## 배포 (Publishing)
|
|
195
247
|
|
|
196
248
|
CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리즈 게시 시 자동, 또는 `workflow_dispatch` 수동 실행(드라이런 옵션). 워크플로는 타입체크·테스트·토큰 검증(`npm whoami`) 후 `npm publish --provenance`를 실행합니다.
|
|
@@ -207,11 +259,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
207
259
|
## 변경 이력 (Changelog)
|
|
208
260
|
|
|
209
261
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
262
|
+
- **[0.7.41]** (2026-07-06) — Refusal-handling workflow audit (follow-up to 0.7.39): fixes a HIGH-severity, long-masked bug where Gemini's real refusal error shape never matched jeo's refusal detector, plus Antigravity's opaque empty-response error, plus a category-aware fail-fast for deterministic ToS-severity refusals that previously spun the full 30-minute stall budget with zero chance of success.
|
|
210
263
|
- **[0.7.40]** (2026-07-06) — Fixes the release CI's standalone-binary build, broken since before v0.7.31 (last release where that job actually ran and passed) — the `release-binaries` job on v0.7.38 and v0.7.39 both failed silently on every platform target, so neither release has downloadable binaries attached.
|
|
211
264
|
- **[0.7.39]** (2026-07-05) — Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder.
|
|
212
265
|
- **[0.7.38]** (2026-07-05) — Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes.
|
|
213
266
|
- **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
214
|
-
- **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
|
|
215
267
|
|
|
216
268
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
217
269
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -38,13 +38,15 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
|
|
|
38
38
|
|
|
39
39
|
## Highlights
|
|
40
40
|
|
|
41
|
-
- **Multi-provider, one loop** — Anthropic / OpenAI (+Codex) / Gemini / Antigravity / Ollama behind
|
|
41
|
+
- **Multi-provider, one loop** — Anthropic / OpenAI (+Codex) / Gemini / Antigravity / Ollama / LM Studio, plus 20+ OpenAI- and Anthropic-compatible clouds (Groq, DeepSeek, Mistral, OpenRouter, xAI, Kimi, z.ai, …), all behind one uniform JSON tool loop. OAuth login from the input box (`/provider login`), every model pick persists as the new default.
|
|
42
42
|
- **Edit integrity** — read output carries content anchors (`42ab|`); anchored edits are verified against the current file, re-mapped when lines shifted, and rejected with fresh content instead of corrupting.
|
|
43
43
|
- **Self-correcting verification loop** — configure a post-edit hook (tsc / eslint / tests) and the agent *sees* the diagnostics and fixes them in-loop; a red hook blocks `done` until resolved.
|
|
44
44
|
- **Real gates, no theater** — `ralplan` consensus is a repo-grounded critic subagent whose `[OKAY]` verdict is persisted and *required* by `jeo approve`; `ultragoal` reports honestly (a suite run is a global signal, never fabricated per-criterion passes).
|
|
45
45
|
- **Crash-durable, local-first** — all state under `.jeo/` with atomic writes, cross-process run locks, failed-task markers with partial-edit warnings on resume.
|
|
46
46
|
- **Dynamic step budget** — turns extend while the tool window shows novel progress and consolidate gracefully when stalled; subagents keep exact step contracts.
|
|
47
47
|
- **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.
|
|
48
|
+
- **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).
|
|
49
|
+
- **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 — commands are authorized to the paired chat only.
|
|
48
50
|
|
|
49
51
|
## Install
|
|
50
52
|
|
|
@@ -164,6 +166,31 @@ Non-zero hook output is appended to the tool result the model reads (deduped per
|
|
|
164
166
|
|
|
165
167
|
`--worktree <name>` runs jeo in an isolated sibling git worktree (reused if the path exists, else created on a branch named after the basename) so risky or reviewable work never touches your main checkout. `jeo mcp serve` exposes jeo's tools to any MCP-capable controller over stdio (`jeo mcp tools` lists them). Add `-q`/`--quiet` (or `JEO_QUIET=1`) to suppress startup banners, the welcome animation, release notes, and resume hints so jeo runs cleanly beside another agent or is driven by a bot — `-p`/`--print` implies quiet.
|
|
166
168
|
|
|
169
|
+
## Remote monitoring & control (Telegram)
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
jeo notify setup # pair a BotFather bot once (getMe verification + chat-id pairing)
|
|
173
|
+
jeo notify status # masked token, paired chat id, daemon state
|
|
174
|
+
jeo daemon start # spawn the singleton background daemon
|
|
175
|
+
jeo daemon status # check whether it's running
|
|
176
|
+
jeo daemon stop # SIGTERM it
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|
181
|
+
│ interactive turn │◄──ws──►│ notify daemon │◄─poll──►│ Telegram bot │
|
|
182
|
+
│ SubagentRegistry │ │ (singleton) │ │ (paired chat) │
|
|
183
|
+
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Opt-in and lazy: nothing binds until `notifications.enabled` is set AND a detached subagent (`task {detached:true}`) actually runs. The daemon scans live session discovery files, connects a loopback WebSocket per session, and pushes a message only on a subagent state *edge* (started → completed/failed/cancelled) — never a repeated "still running" ping. Inbound Telegram commands are authorized to the paired chat only; anything else is dropped silently.
|
|
187
|
+
|
|
188
|
+
| Command | Effect |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `/subagents` | List running/recent subagents across every connected session |
|
|
191
|
+
| `/steer <sessionId> <subagentId> <message>` | Send a live message into a running subagent |
|
|
192
|
+
| `/cancel <sessionId> <subagentId>` | Cancel a running subagent |
|
|
193
|
+
| `/help` | Show the command reference |
|
|
167
194
|
|
|
168
195
|
## Local models
|
|
169
196
|
|
|
@@ -234,11 +261,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
234
261
|
## Changelog
|
|
235
262
|
|
|
236
263
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
264
|
+
- **[0.7.41]** (2026-07-06) — Refusal-handling workflow audit (follow-up to 0.7.39): fixes a HIGH-severity, long-masked bug where Gemini's real refusal error shape never matched jeo's refusal detector, plus Antigravity's opaque empty-response error, plus a category-aware fail-fast for deterministic ToS-severity refusals that previously spun the full 30-minute stall budget with zero chance of success.
|
|
237
265
|
- **[0.7.40]** (2026-07-06) — Fixes the release CI's standalone-binary build, broken since before v0.7.31 (last release where that job actually ran and passed) — the `release-binaries` job on v0.7.38 and v0.7.39 both failed silently on every platform target, so neither release has downloadable binaries attached.
|
|
238
266
|
- **[0.7.39]** (2026-07-05) — Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder.
|
|
239
267
|
- **[0.7.38]** (2026-07-05) — Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes.
|
|
240
268
|
- **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
241
|
-
- **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
|
|
242
269
|
|
|
243
270
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
244
271
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -38,13 +38,15 @@
|
|
|
38
38
|
|
|
39
39
|
## 亮点
|
|
40
40
|
|
|
41
|
-
- **多提供商、单一循环** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama
|
|
41
|
+
- **多提供商、单一循环** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio,以及 20+ 个 OpenAI、Anthropic 兼容云服务(Groq、DeepSeek、Mistral、OpenRouter、xAI、Kimi、z.ai 等),统一在一个 JSON 工具循环中。输入框内直接 OAuth 登录(`/provider login`),模型选择即刻持久化为默认值。
|
|
42
42
|
- **编辑完整性** — read 输出携带内容锚点(`42ab|`);带锚点的编辑会与当前文件校验、行移动时自动重映射、不匹配时连同最新内容一起拒绝 — 绝不污染文件。
|
|
43
43
|
- **自我修正的验证循环** — 配置 post-edit 钩子(tsc / eslint / 测试),代理会*亲自读取*诊断并在循环内修复;钩子未通过时 `done` 会被阻断。
|
|
44
44
|
- **没有表演的真实门禁** — `ralplan` 共识由真正读取仓库的 critic 子代理执行,`[OKAY]` 裁决被持久化且 `jeo approve` *强制要求*它;`ultragoal` 诚实报告(套件运行只是全局信号,绝不伪造逐条通过)。
|
|
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 的独立下载项)。
|
|
49
|
+
- **远程子代理可见性(Telegram)** — 配对一次机器人(`jeo notify setup`)后,`jeo daemon start` 会在子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受 `/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` 回传 — 命令仅对配对的聊天授权。
|
|
48
50
|
|
|
49
51
|
## 安装
|
|
50
52
|
|
|
@@ -163,6 +165,31 @@ jeo ultragoal
|
|
|
163
165
|
|
|
164
166
|
`--worktree <name>` 在隔离的同级 git worktree 中运行 jeo(路径存在则复用,否则以 basename 分支创建),因此有风险或需审查的工作绝不会触及您的主检出。`jeo mcp serve` 通过 stdio 向任何支持 MCP 的控制器公开 jeo 的工具(用 `jeo mcp tools` 列出)。添加 `-q`/`--quiet`(或 `JEO_QUIET=1`)可抑制启动横幅、欢迎动画、发布说明和恢复提示,使 jeo 能够与其他代理并行运行或由机器人驱动。`-p`/`--print` 隐含 quiet。
|
|
165
167
|
|
|
168
|
+
## 远程监控与控制 (Telegram)
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
jeo notify setup # 配对一次 BotFather 机器人(getMe 校验 + chat-id 配对)
|
|
172
|
+
jeo notify status # 已遮蔽的令牌、已配对 chat id、守护进程状态
|
|
173
|
+
jeo daemon start # 启动单例后台守护进程
|
|
174
|
+
jeo daemon status # 检查是否正在运行
|
|
175
|
+
jeo daemon stop # 发送 SIGTERM 停止
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|
180
|
+
│ interactive turn │◄──ws──►│ notify daemon │◄─poll──►│ Telegram bot │
|
|
181
|
+
│ SubagentRegistry │ │ (singleton) │ │ (paired chat) │
|
|
182
|
+
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
默认关闭、延迟绑定:只有设置了 `notifications.enabled` 且真正运行了一个 detached 子代理(`task {detached:true}`)才会绑定。守护进程扫描存活的会话发现文件,为每个会话建立一条回环 WebSocket,并且只在子代理状态发生*变化*时(启动 → 完成/失败/取消)推送消息 — 绝不会重复推送"仍在运行"这类状态。收到的 Telegram 命令仅对已配对的聊天授权,其余一律静默丢弃。
|
|
186
|
+
|
|
187
|
+
| 命令 | 效果 |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `/subagents` | 列出所有已连接会话中正在运行/最近的子代理 |
|
|
190
|
+
| `/steer <sessionId> <subagentId> <message>` | 向正在运行的子代理发送实时消息 |
|
|
191
|
+
| `/cancel <sessionId> <subagentId>` | 取消正在运行的子代理 |
|
|
192
|
+
| `/help` | 显示命令参考 |
|
|
166
193
|
|
|
167
194
|
## 本地模型
|
|
168
195
|
|
|
@@ -186,11 +213,36 @@ JEO_TUI_ALT_SCREEN=1 # 旧版 alt-screen 回合(默认: 内联滚动
|
|
|
186
213
|
JEO_STEP_BASE=24 # 动态步数预算的滚动基数
|
|
187
214
|
JEO_STEP_HARD_CAP=600 # 绝对终止保证
|
|
188
215
|
JEO_STREAM_MAX_MS=300000 # 可选的整体流截止(默认关闭; 约束慢滴流)
|
|
216
|
+
JEO_STREAM_IDLE_MS=300000 # 单次分块空闲上限(默认300秒); 首个 token 前静默较久的慢速/本地后端可调高
|
|
189
217
|
JEO_TOOL_OUTPUT_MAX=4000 # 模型可见的工具输出上限(全文溢出到 artifacts)
|
|
190
218
|
```
|
|
191
219
|
|
|
192
220
|
重试行为通过 `~/.jeo/config.json` 的 `retry` 调整(`requestMaxRetries`、`streamMaxRetries`、`rateLimitRetries`、`failFastStatuses` 等)。步数预算默认动态 — 只要看到新的进展就延长,停滞时收敛为总结;`--max-steps N` 恢复有界流程。
|
|
193
221
|
|
|
222
|
+
## 技能迁移与内置技能检查
|
|
223
|
+
|
|
224
|
+
把某个工作流迁移到 jeo 之前,先检查内置默认值,再决定是否安装或覆盖任何内容:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
jeo skills list # 内置 + 用户 + 项目技能,附带发现目录
|
|
228
|
+
jeo skills read ralplan # 打印某个技能的完整 SKILL.md
|
|
229
|
+
jeo skills sync --check # 报告与 ~/.jeo/skills 的差异(有差异时非零退出)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`jeo skills sync` 会把内置的工作流技能(deep-interview、deep-dive、ralplan、team、ultragoal)安装到 `~/.jeo/skills`,并且**默认保留已有的本地文件** —— 不同的本地副本会被报告为 `preserved`,绝不会被覆盖。如果 `--check` 标记出缺失或不同的文件,先用 `jeo skills read <name>` 对比;只有在确实想替换本地默认工作流技能文件时才使用 `jeo skills sync --force`。可通过末尾路径参数(或 `JEO_CONFIG_DIR`)指定不同目录,并加上 `--json` 获取结构化的 `SkillSyncResult`。
|
|
233
|
+
|
|
234
|
+
## 开发
|
|
235
|
+
|
|
236
|
+
jeo 是运行在 Bun 上的纯 TypeScript,**零原生依赖**,因此全局 `jeo` 命令可以直接运行本仓库的源码 —— 无需构建步骤,每次编辑立即生效。
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
bun install
|
|
240
|
+
bun run dev:link # 将 `jeo` 软链接到 <repo>/src/cli.ts -> ~/.local/bin
|
|
241
|
+
bun run dev:doctor # 报告全局 `jeo` 是否运行的是本源码(linked/drift/missing)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
如果 `PATH` 中在受管链接之前还有另一个 `jeo` 遮蔽了它,`dev:link` 会拒绝继续(可用 `JEO_DEV_LINK_DIR` 覆盖目标位置),并运行一次 `--version` 冒烟测试。当解析出的 `jeo` 是编译后的二进制文件或已安装副本而非本源码时,`dev:doctor` 以非零状态退出。无需链接直接从源码运行: `bun src/cli.ts --help`。内置工作流技能位于源码 `src/prompts/skills/<name>/SKILL.md`;用 `bun run typecheck` 和 `bun test` 验证。
|
|
245
|
+
|
|
194
246
|
## 发布 (Publishing)
|
|
195
247
|
|
|
196
248
|
CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release 时自动触发,或手动 `workflow_dispatch`(可选 dry-run)。工作流执行类型检查、测试、令牌校验(`npm whoami`)后运行 `npm publish --provenance`。
|
|
@@ -207,11 +259,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
207
259
|
## 更新日志 (Changelog)
|
|
208
260
|
|
|
209
261
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
262
|
+
- **[0.7.41]** (2026-07-06) — Refusal-handling workflow audit (follow-up to 0.7.39): fixes a HIGH-severity, long-masked bug where Gemini's real refusal error shape never matched jeo's refusal detector, plus Antigravity's opaque empty-response error, plus a category-aware fail-fast for deterministic ToS-severity refusals that previously spun the full 30-minute stall budget with zero chance of success.
|
|
210
263
|
- **[0.7.40]** (2026-07-06) — Fixes the release CI's standalone-binary build, broken since before v0.7.31 (last release where that job actually ran and passed) — the `release-binaries` job on v0.7.38 and v0.7.39 both failed silently on every platform target, so neither release has downloadable binaries attached.
|
|
211
264
|
- **[0.7.39]** (2026-07-05) — Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder.
|
|
212
265
|
- **[0.7.38]** (2026-07-05) — Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes.
|
|
213
266
|
- **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
214
|
-
- **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
|
|
215
267
|
|
|
216
268
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
217
269
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/engine.ts
CHANGED
|
@@ -735,12 +735,32 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
735
735
|
}
|
|
736
736
|
// Nothing left to strip — fall through to the backoff rung below.
|
|
737
737
|
}
|
|
738
|
-
// Rung 4: every context mutation is spent.
|
|
738
|
+
// Rung 4: every context mutation is spent. A `Refusal (<category>)`-shaped
|
|
739
|
+
// error (the structural shape Anthropic uses for a classification of the
|
|
740
|
+
// REQUEST CONTENT ITSELF, e.g. reasoning_extraction) is deterministic — by
|
|
741
|
+
// this rung context is already minimized, so every further resend is
|
|
742
|
+
// identical and will re-refuse identically. Fail fast with the category-aware
|
|
743
|
+
// message instead of spinning the backoff for the full stall budget. Every
|
|
744
|
+
// OTHER refusal shape (bare stop_reason=refusal, finish_reason=content_filter,
|
|
745
|
+
// Gemini finishReason=SAFETY/etc.) genuinely can clear as a rolling classifier
|
|
746
|
+
// resets, so those still get the unbounded capped backoff below.
|
|
747
|
+
const category = /Refusal \(\w+\)/i.test((err as Error)?.message ?? String(err));
|
|
748
|
+
if (category) {
|
|
749
|
+
return finish({ done: false, steps: step, doneReason: `Error: ${friendlyProviderError(err)}` });
|
|
750
|
+
}
|
|
751
|
+
// Backoff-resend forever (abortable).
|
|
739
752
|
const attempt = Math.max(1, refusalRetries - MAX_REFUSAL_RETRIES);
|
|
740
753
|
const baseRaw = Number(jeoEnv("REFUSAL_BACKOFF_BASE_MS") ?? NaN);
|
|
741
754
|
const baseMs = Number.isFinite(baseRaw) && baseRaw >= 0 ? baseRaw : GUARD_LIMITS.REFUSAL_BACKOFF_BASE_MS;
|
|
742
755
|
const delayMs = Math.min(baseMs * 2 ** (attempt - 1), GUARD_LIMITS.REFUSAL_BACKOFF_MAX_MS);
|
|
743
|
-
|
|
756
|
+
// "(Esc to cancel)" only makes sense where a live interactive Esc handler
|
|
757
|
+
// exists — that's the TUI, which is exactly the consumer that attaches
|
|
758
|
+
// onModelStream (see the streaming comment above: "the engine streams
|
|
759
|
+
// solely for the TUI"). One-shot/non-interactive callers (e.g. `jeo team`)
|
|
760
|
+
// leave onModelStream unset and have no live Esc handler, only the
|
|
761
|
+
// JEO_TURN_MAX_MS stall budget — showing the hint there is misleading.
|
|
762
|
+
const escHint = ev.onModelStream ? " (Esc to cancel)" : "";
|
|
763
|
+
ev.onNotice?.(`provider refused again — auto-retry #${attempt} in ${Math.max(1, Math.round(delayMs / 1000))}s${escHint}`);
|
|
744
764
|
await waitAbortable(delayMs, opts.signal);
|
|
745
765
|
continue; // loop top surfaces "Cancelled." when the wait was aborted
|
|
746
766
|
}
|
|
@@ -407,12 +407,19 @@ export function totalInputTokens(u: AnthropicUsage): number {
|
|
|
407
407
|
|
|
408
408
|
/** Round-5 #1: HTTP-200-with-no-text must surface its CAUSE (stop_reason) instead
|
|
409
409
|
* of returning "" — an empty reply just bounces in the JSON loop, burning billed
|
|
410
|
-
* calls until the step budget dies. Mirrors gemini's blockedReason contract.
|
|
411
|
-
|
|
410
|
+
* calls until the step budget dies. Mirrors gemini's blockedReason contract.
|
|
411
|
+
* `category` (from `stop_details.category`, e.g. "reasoning_extraction") is
|
|
412
|
+
* defensive: today's live refusal-category delivery is an HTTP-error message
|
|
413
|
+
* (see providerHttpError), but if Anthropic ever ships a category via a 200
|
|
414
|
+
* body/stream event instead, folding it in here keeps friendlyProviderError's
|
|
415
|
+
* category-aware clarifying note working instead of silently degrading to the
|
|
416
|
+
* generic refusal copy. Absent category keeps the existing plain shape unchanged. */
|
|
417
|
+
function emptyCompletionError(stopReason: string | undefined, category?: string): Error {
|
|
412
418
|
const hint = stopReason === "max_tokens"
|
|
413
419
|
? " — output budget exhausted before any text; raise maxTokens or lower the thinking level"
|
|
414
420
|
: "";
|
|
415
|
-
|
|
421
|
+
const reason = stopReason ? (category ? `stop_reason=${stopReason}, category=${category}` : `stop_reason=${stopReason}`) : undefined;
|
|
422
|
+
return new Error(`Anthropic returned no content${reason ? ` (${reason})` : ""}${hint}.`);
|
|
416
423
|
}
|
|
417
424
|
|
|
418
425
|
export const anthropicAdapter: ProviderAdapter = {
|
|
@@ -420,7 +427,7 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
420
427
|
supportsNativeTools: true,
|
|
421
428
|
async call(messages, options, credential) {
|
|
422
429
|
const response = await postAnthropic(messages, options, credential, false);
|
|
423
|
-
const result = (await response.json()) as { content: { type: string; text?: string; name?: string; input?: unknown; thinking?: string; signature?: string; data?: string }[]; stop_reason?: string; usage?: AnthropicUsage };
|
|
430
|
+
const result = (await response.json()) as { content: { type: string; text?: string; name?: string; input?: unknown; thinking?: string; signature?: string; data?: string }[]; stop_reason?: string; stop_details?: { category?: string }; usage?: AnthropicUsage };
|
|
424
431
|
if (result.usage) options.onUsage?.({ inputTokens: totalInputTokens(result.usage), outputTokens: result.usage.output_tokens });
|
|
425
432
|
// Capture thinking/redacted blocks as replay artifacts (parity with the stream path).
|
|
426
433
|
for (const c of result.content) {
|
|
@@ -438,7 +445,7 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
438
445
|
);
|
|
439
446
|
if (toolCall) return toolCall;
|
|
440
447
|
const text = result.content.find(c => c.type === "text")?.text ?? "";
|
|
441
|
-
if (!text) throw emptyCompletionError(result.stop_reason);
|
|
448
|
+
if (!text) throw emptyCompletionError(result.stop_reason, result.stop_details?.category);
|
|
442
449
|
return text;
|
|
443
450
|
},
|
|
444
451
|
async *stream(messages, options, credential) {
|
|
@@ -447,6 +454,7 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
447
454
|
let cachedInput: number | undefined;
|
|
448
455
|
let yieldedAny = false;
|
|
449
456
|
let stopReason: string | undefined;
|
|
457
|
+
let stopCategory: string | undefined;
|
|
450
458
|
// Native tool_use streams as content_block_start (name) + input_json_delta fragments,
|
|
451
459
|
// never as text_delta — accumulate per block index, then re-serialize to canonical
|
|
452
460
|
// JSON and yield it once at the end (concatenation still equals call()).
|
|
@@ -460,8 +468,8 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
460
468
|
type?: string;
|
|
461
469
|
index?: number;
|
|
462
470
|
content_block?: { type?: string; name?: string; data?: string };
|
|
463
|
-
delta?: { type?: string; text?: string; partial_json?: string; thinking?: string; signature?: string; stop_reason?: string };
|
|
464
|
-
message?: { usage?: AnthropicUsage; stop_reason?: string };
|
|
471
|
+
delta?: { type?: string; text?: string; partial_json?: string; thinking?: string; signature?: string; stop_reason?: string; stop_details?: { category?: string } };
|
|
472
|
+
message?: { usage?: AnthropicUsage; stop_reason?: string; stop_details?: { category?: string } };
|
|
465
473
|
usage?: { output_tokens?: number };
|
|
466
474
|
};
|
|
467
475
|
try {
|
|
@@ -499,6 +507,7 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
499
507
|
thinkBlocks.set(evt.index, tb);
|
|
500
508
|
} else if (evt.type === "message_start" && evt.message) {
|
|
501
509
|
if (evt.message.stop_reason) stopReason = evt.message.stop_reason;
|
|
510
|
+
if (evt.message.stop_details?.category) stopCategory = evt.message.stop_details.category;
|
|
502
511
|
if (evt.message.usage) {
|
|
503
512
|
// Cache only — usage is reported ONCE at message_delta so an accumulating
|
|
504
513
|
// sink can't double-count input (and a pre-first-chunk retry that replays
|
|
@@ -507,6 +516,7 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
507
516
|
}
|
|
508
517
|
} else if (evt.type === "message_delta") {
|
|
509
518
|
if (evt.delta?.stop_reason) stopReason = evt.delta.stop_reason;
|
|
519
|
+
if (evt.delta?.stop_details?.category) stopCategory = evt.delta.stop_details.category;
|
|
510
520
|
if (evt.usage) options.onUsage?.({ inputTokens: cachedInput, outputTokens: evt.usage.output_tokens });
|
|
511
521
|
}
|
|
512
522
|
}
|
|
@@ -519,7 +529,7 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
519
529
|
}
|
|
520
530
|
const envelope = serializeAccumulatedToolCalls(toolBlocks);
|
|
521
531
|
if (envelope) { yieldedAny = true; yield envelope; }
|
|
522
|
-
if (!yieldedAny) throw emptyCompletionError(stopReason);
|
|
532
|
+
if (!yieldedAny) throw emptyCompletionError(stopReason, stopCategory);
|
|
523
533
|
},
|
|
524
534
|
};
|
|
525
535
|
function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}` {
|
|
@@ -234,6 +234,16 @@ interface CcaChunk {
|
|
|
234
234
|
usageMetadata?: CcaUsage;
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
|
+
/** When Antigravity (Cloud Code Assist) returns HTTP 200 with no text, surface the
|
|
238
|
+
* real cause (safety block / RECITATION / etc.) instead of a silent, reason-free
|
|
239
|
+
* empty response — mirrors gemini.ts's `blockedReason` (same message wording, so
|
|
240
|
+
* `defaultRetryable`'s "returned no content" transient-empty-200 branch and
|
|
241
|
+
* `isRefusalError`'s `finishReason=` matcher both apply consistently). */
|
|
242
|
+
function finishReasonOf(chunk: CcaChunk): string | undefined {
|
|
243
|
+
const finish = chunk.response?.candidates?.[0]?.finishReason;
|
|
244
|
+
if (finish && finish !== "STOP") return `finishReason=${finish}`;
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
237
247
|
|
|
238
248
|
function textOf(chunk: CcaChunk): string {
|
|
239
249
|
return chunk.response?.candidates?.[0]?.content?.parts?.filter(p => !p.thought).map(p => p.text ?? "").join("") ?? "";
|
|
@@ -279,13 +289,16 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
279
289
|
if (!response.body) return "";
|
|
280
290
|
let out = "";
|
|
281
291
|
let usage: CcaUsage | undefined;
|
|
292
|
+
let lastEmptyReason: string | undefined;
|
|
282
293
|
const fnCalls: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
283
294
|
for await (const data of readSse(response.body)) {
|
|
284
295
|
let chunk: CcaChunk;
|
|
285
296
|
try { chunk = JSON.parse(data); } catch { continue; }
|
|
286
297
|
const thought = thoughtOf(chunk);
|
|
287
298
|
if (thought) options.onReasoning?.(thought);
|
|
288
|
-
|
|
299
|
+
const delta = textOf(chunk);
|
|
300
|
+
if (delta) out += delta;
|
|
301
|
+
else lastEmptyReason = finishReasonOf(chunk) ?? lastEmptyReason;
|
|
289
302
|
fnCalls.push(...functionCallsOf(chunk));
|
|
290
303
|
if (chunk.response?.usageMetadata) usage = chunk.response.usageMetadata;
|
|
291
304
|
}
|
|
@@ -293,7 +306,7 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
293
306
|
// Prefer a native tool call (re-serialized to canonical JSON) over any stray text.
|
|
294
307
|
const envelope = serializeToolCalls(fnCalls);
|
|
295
308
|
if (envelope) return envelope;
|
|
296
|
-
if (!out) throw new Error(
|
|
309
|
+
if (!out) throw new Error(`Antigravity Cloud Code Assist returned no content${lastEmptyReason ? ` (${lastEmptyReason})` : ""}.`);
|
|
297
310
|
return out;
|
|
298
311
|
},
|
|
299
312
|
async *stream(messages, options, credential) {
|
|
@@ -304,6 +317,7 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
304
317
|
if (antigravityThinkingActive(options)) options.onReasoningStart?.();
|
|
305
318
|
let yielded = false;
|
|
306
319
|
let usage: CcaUsage | undefined;
|
|
320
|
+
let lastEmptyReason: string | undefined;
|
|
307
321
|
const fnCalls: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
308
322
|
for await (const data of readSse(response.body, options.onStreamActivity)) {
|
|
309
323
|
let chunk: CcaChunk;
|
|
@@ -312,6 +326,7 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
312
326
|
if (thought) options.onReasoning?.(thought);
|
|
313
327
|
const delta = textOf(chunk);
|
|
314
328
|
if (delta) { yielded = true; yield delta; }
|
|
329
|
+
else lastEmptyReason = finishReasonOf(chunk) ?? lastEmptyReason;
|
|
315
330
|
fnCalls.push(...functionCallsOf(chunk));
|
|
316
331
|
if (chunk.response?.usageMetadata) usage = chunk.response.usageMetadata;
|
|
317
332
|
}
|
|
@@ -319,6 +334,6 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
319
334
|
// Native tool calls have no text deltas — yield the re-serialized envelope once at end.
|
|
320
335
|
const envelope = serializeToolCalls(fnCalls);
|
|
321
336
|
if (envelope) { yielded = true; yield envelope; }
|
|
322
|
-
if (!yielded) throw new Error(
|
|
337
|
+
if (!yielded) throw new Error(`Antigravity Cloud Code Assist returned no content${lastEmptyReason ? ` (${lastEmptyReason})` : ""}.`);
|
|
323
338
|
},
|
|
324
339
|
};
|
|
@@ -62,7 +62,7 @@ export function friendlyProviderError(err: unknown): string {
|
|
|
62
62
|
}
|
|
63
63
|
if (isRefusalError(err)) {
|
|
64
64
|
const base = `${provider} declined to answer (safety refusal — no content returned). Usually a content classifier tripped on recently read file/search content: /retry, /compact or /new to drop the triggering context, or switch model with /model. If this persists on a Claude subscription (OAuth) login, Anthropic restricts third-party OAuth clients — set ANTHROPIC_API_KEY or use another provider.`;
|
|
65
|
-
const category = /Refusal \((\w+)\)/i.exec(msg)?.[1];
|
|
65
|
+
const category = /Refusal \((\w+)\)/i.exec(msg)?.[1] ?? /category=(\w+)/i.exec(msg)?.[1];
|
|
66
66
|
if (category === "reasoning_extraction") {
|
|
67
67
|
return `${base} (Category: reasoning_extraction — this classifier flags requests that look like probing the model's internal reasoning; jeo's own thinking-block replay across multi-step tool use is a common benign trigger, not an actual violation. /retry after a /compact usually clears it.)`;
|
|
68
68
|
}
|
package/src/util/retry.ts
CHANGED
|
@@ -282,5 +282,5 @@ export function isUsageLimitError(err: unknown): boolean {
|
|
|
282
282
|
* capped backoff — gjc parity: a refusal is never terminal) is the correct recovery
|
|
283
283
|
* layer because it MUTATES the context between attempts. */
|
|
284
284
|
export function isRefusalError(err: unknown): boolean {
|
|
285
|
-
return /stop_reason=refusal|finish_reason=content_filter|\(content_filter\)
|
|
285
|
+
return /stop_reason=refusal|finish_reason=content_filter|\(content_filter\)|(?:block|finish)Reason=(?:SAFETY|PROHIBITED_CONTENT|BLOCKLIST|RECITATION|SPII)|Refusal \(\w+\)/i.test(errorMessageOf(err));
|
|
286
286
|
}
|