jeo-code 0.8.19 → 0.8.21
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 +35 -1
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/package.json +1 -1
- package/src/agent/engine.ts +37 -5
- package/src/agent/goal-verifier.ts +39 -0
- package/src/agent/route-history.ts +11 -2
- package/src/agent/step-budget.ts +44 -4
- package/src/agent/task-tool.ts +2 -25
- package/src/ai/model-catalog.ts +2 -1
- package/src/commands/launch/input.ts +13 -0
- package/src/commands/launch/route-slash.ts +4 -1
- package/src/commands/launch.ts +52 -37
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ 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.21] - 2026-07-10
|
|
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._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Session draft-restore** (`src/agent/session.ts`, `src/commands/launch/session-slash.ts`, `src/commands/launch.ts`, `src/commands/launch/input.ts`) — an unsent, half-typed prompt now survives a hard exit (Ctrl+D/EOF/disconnected terminal) and is restored into the prompt box on `/resume` (gajae-code parity). The read-side plumbing (`updateSessionDraft`, `header.draft`, `runSessionSlash` surfacing it) already existed; `launch.ts` never consumed it. Added `persistSessionDraft` (mirrors the existing `persistSessionModel` pattern) and a new pure predicate `draftFromUnsentLine` (`launch/input.ts`), wired into all 3 resume paths — the interactive picker, the mid-loop `/session resume` command, and `--resume <id>`/`--continue`'s non-interactive startup path (this third path had NO draft restoration at all before this fix). Live-verified: seeded a real session's draft, launched the real `bun src/cli.ts --resume <id>` under a genuine PTY, confirmed the exact unsent text appeared in the live prompt box.
|
|
14
|
+
- **Evidence-gated goal verification** (`src/agent/goal-verifier.ts`, `src/agent/engine.ts`, `src/commands/launch.ts`) — an LLM-judged `MET` goal verdict is now deterministically downgraded to `NOT_MET` when the turn mutated files with no fresh verification evidence, closing the "gate theater" failure mode (an LLM asserting success without re-checked evidence). `applyEvidenceGate` (pure, already unit-tested with 5 tests) existed but was never called anywhere — the same disconnected-scaffolding pattern `RouteHistory` shipped with in 0.8.16/0.8.17. `onBeforeDone`'s signature now carries the SAME `sawMutation`/`sawVerification`/`verificationStale` signals the engine's own done-gate already computes; `launch.ts`'s goal-verifier closure now gates `verifyGoal`'s raw verdict through it before persisting or acting on it.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **`toolTarget` dedup** (`src/agent/step-budget.ts`, `src/agent/task-tool.ts`) — `task-tool.ts` had a private copy of the same coarse-call-target labeler `step-budget.ts` needed for a new edit-thrashing novelty rule (a model rewriting the same file with different content every attempt no longer earns endless step-budget extensions — only the first attempt at a given target counts as novel). Moved to `step-budget.ts` (exported), `task-tool.ts` now imports it — confirmed byte-identical, zero behavior change.
|
|
18
|
+
|
|
19
|
+
### Added (catalog)
|
|
20
|
+
- **`gpt-5.6`** (`src/ai/model-catalog.ts`) — OpenAI's newest catalogued model, consistent with existing entries; correctly excluded from the OAuth-only `CODEX_MODELS` allow-list (live discovery handles that drift per its own documented contract, not a static edit).
|
|
21
|
+
|
|
22
|
+
### Verified
|
|
23
|
+
- `bun test test/launch-flags.test.ts test/session-draft-restore.test.ts test/step-budget.test.ts test/goal-verifier.test.ts test/task-tool.test.ts test/doctor.test.ts test/prompt-router-tiers.test.ts test/model-catalog.test.ts` — 128 pass / 0 fail.
|
|
24
|
+
- Full `bun test` — 2906 pass / 0 fail across 291 files.
|
|
25
|
+
- `bun run typecheck` — no errors.
|
|
26
|
+
- Live PTY end-to-end: real session seeded with `updateSessionDraft`, real `bun src/cli.ts --resume <id>` subprocess, confirmed the exact restored text in the live prompt box.
|
|
27
|
+
|
|
28
|
+
## [0.8.20] - 2026-07-10
|
|
29
|
+
_"모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 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._
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- **README `[!CAUTION]` block updated for the Antigravity exception** (`README.md`, `README.ko.md`, `README.ja.md`, `README.zh.md`) — the block's `high`/`complex` tier-fallback description ("scan for the strongest live-credentialed model... land on a different model each turn") stopped being accurate for Antigravity-/Gemini-OAuth-credentialed sessions after v0.8.19's `antigravityCompanyPoolPick` (default-on, no opt-in): those sessions instead spread across ONE model per company, session-stably (same session -> same pick every turn, not varying). Added an explicit exception clause in all 4 languages.
|
|
33
|
+
- **CHANGELOG 0.8.18 test-count error** — "69 pass" for `test/route-history.test.ts test/route-slash.test.ts test/launch-prompt-routing.test.ts` corrected to the actual **71 pass** (independently reproduced twice, including in an isolated `git worktree` pinned to commit 50bd0b1).
|
|
34
|
+
- **`RouteHistory`/`route-slash.ts` doc comments corrected** (`src/agent/route-history.ts`, `src/commands/launch/route-slash.ts`) — `turnNumber`'s doc comment now states it's a sequential ADD-counter (increments once per pre-call route AND once per mid-turn fallback retry), not a 1:1 conversational-turn counter as the old wording implied; `RouteHistory`'s hardcoded `maxSize` default (10) is now documented as an unconditional retention cap that `/route history <n>` can only narrow, never widen — `/route history 50` silently returns at most 10 entries. No behavior changed, doc-only.
|
|
35
|
+
- **Tightened an over-loose test assertion** (`test/prompt-router.test.ts`) — the antigravity multi-company reachability test asserted `gemini-3.5-flash-low OR gemini-pro-agent` for Google's "high"-tier company slot, when the code's actual behavior (confirmed by independent manual trace against `MODEL_CATALOG`) is fully deterministic: `gemini-3.5-flash-low` always wins the recency tiebreak, `gemini-pro-agent` never does. Replaced the OR with two precise assertions.
|
|
36
|
+
|
|
37
|
+
### Verified
|
|
38
|
+
- 4 independent subagent audits, zero trust in prior claims: CodeCorrectnessAuditor (manual pen-and-paper trace of 2 antigravity routing scenarios against live `MODEL_CATALOG` — both confirmed correct), TestIntegrityAuditor (mutation-tested `onModelSwitch` and the `/model`-pin gate — both confirmed to genuinely fail when reverted; zero `.skip`/`.todo`/weakened assertions found across all 5 touched test files), LiveBehaviorVerifier (re-reproduced all 4 behavioral claims with self-generated session ids/mock server/HTTP status code — all PASS), DocsAccuracyAuditor (cross-checked every README/CHANGELOG claim in isolated `git worktree`s pinned to the exact historical commits).
|
|
39
|
+
- Full `bun test` — 2898 pass / 0 fail across 291 files (290 tracked at this commit; the +1 is an unrelated concurrent session's untracked file, not part of this change).
|
|
40
|
+
- `bun run typecheck` — no errors.
|
|
41
|
+
- `bun run changelog:sync` — idempotent, zero diff on a clean run (independently confirmed by DocsAccuracyAuditor before this entry was added).
|
|
42
|
+
|
|
9
43
|
## [0.8.19] - 2026-07-09
|
|
10
44
|
_"프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools._
|
|
11
45
|
|
|
@@ -36,7 +70,7 @@ _"라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동
|
|
|
36
70
|
### Verified
|
|
37
71
|
- Live PTY re-verification (`bun src/cli.ts`, isolated `JEO_CONFIG_DIR`, real Anthropic credentials): 5 prompts of varying complexity in one session routed to 5 distinct tier/model pairs (trivial→claude-haiku-4-5, complex→claude-fable-5, trivial→claude-haiku-4-5, high→claude-sonnet-5, trivial→claude-haiku-4-5); `/route why` and `/route status` matched the actual serving model on every turn.
|
|
38
72
|
- Live post-call-fallback reproduction: a real `Bun.serve` mock returning HTTP 500 on every call forced `launch.ts`'s equivalent-pool fallback to fire mid-turn; `/route why` immediately after correctly reported the POST-fallback model and a `warning: … switched to equivalent …` field, not the original pre-call pick.
|
|
39
|
-
- `bun test test/route-history.test.ts test/route-slash.test.ts test/launch-prompt-routing.test.ts` —
|
|
73
|
+
- `bun test test/route-history.test.ts test/route-slash.test.ts test/launch-prompt-routing.test.ts` — 71 pass / 0 fail.
|
|
40
74
|
- `bun test test/tui-app.test.ts` — 51 pass / 0 fail (50 pre-existing + 1 new, covering the footer live-update fix).
|
|
41
75
|
- Full `bun test` — 2885 pass / 0 fail across 290 files.
|
|
42
76
|
- `bun run typecheck` — no errors.
|
package/README.ja.md
CHANGED
|
@@ -91,7 +91,7 @@ jeo --tmux # 独立した tmux セッションで実行
|
|
|
91
91
|
| `jeo autopilot status` | スコア方向、keep/revert 数、次アクションを示す ratchet ステータスフィールド |
|
|
92
92
|
|
|
93
93
|
> [!CAUTION]
|
|
94
|
-
> **`/model <name>` で特定モデルを手動指定すると、そのセッション中ルーティングが固定されます。** プロンプトルーティング(`/route`)はモデルが手動固定されていない間のみ毎ターン再評価されます。`/model <name>` で特定モデルを選ぶとその選択が固定され、`/model auto`(ピンを完全に解除)を実行するか、`/route on`(ピンを消さずに優先度だけ上げる — `/route off` した瞬間ピンが復活)を実行するまでルーティングは切り替わりません。`roles.*` 未設定時に `defaultModel` へ確定フォールバックするのは `standard` ティアのみで、`high`/`complex`
|
|
94
|
+
> **`/model <name>` で特定モデルを手動指定すると、そのセッション中ルーティングが固定されます。** プロンプトルーティング(`/route`)はモデルが手動固定されていない間のみ毎ターン再評価されます。`/model <name>` で特定モデルを選ぶとその選択が固定され、`/model auto`(ピンを完全に解除)を実行するか、`/route on`(ピンを消さずに優先度だけ上げる — `/route off` した瞬間ピンが復活)を実行するまでルーティングは切り替わりません。`roles.*` 未設定時に `defaultModel` へ確定フォールバックするのは `standard` ティアのみで、`high`/`complex` ティアは通常フォールバック前にライブでクレデンシャル済みの最強モデルを先に探索するため、未設定でも毎ターン別モデルに着地することがあります。**例外:** Antigravity または Gemini OAuth でクレデンシャルされたセッションは一つの資格情報で Anthropic/Google/OpenAI のモデルを再公開しており、その場合 `high`/`complex` は(必ずしも最強ではなく)会社ごとに1モデルへセッション単位で安定的に分散されるため、ターンごとに変わらずそのセッション中は固定されます。
|
|
95
95
|
|
|
96
96
|
## Spec-first ワークフロー
|
|
97
97
|
|
|
@@ -265,11 +265,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
265
265
|
## 変更履歴 (Changelog)
|
|
266
266
|
|
|
267
267
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
268
|
+
- **[0.8.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.
|
|
269
|
+
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
268
270
|
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
269
271
|
- **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
|
|
270
272
|
- **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
|
|
271
|
-
- **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
|
|
272
|
-
- **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
|
|
273
273
|
|
|
274
274
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
275
275
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -92,7 +92,7 @@ jeo --tmux # 독립 tmux 세션에서 실행
|
|
|
92
92
|
| `jeo autopilot status` | 점수 방향, keep/revert 횟수, 다음 액션을 보여주는 ratchet 상태 필드 |
|
|
93
93
|
|
|
94
94
|
> [!CAUTION]
|
|
95
|
-
> **`/model <name>`으로 특정 모델을 수동 지정하면 그 세션 동안 라우팅이 고정됩니다.** 프롬프트 라우팅(`/route`)은 모델이 수동으로 고정되지 않은 동안만 매 턴 재평가됩니다. `/model <name>`으로 특정 모델을 선택하면 그 선택이 그대로 고정되며, `/model auto`(핀을 완전히 해제)를 실행하거나 `/route on`(핀을 지우지 않고 그보다 우선순위를 높임 — `/route off` 하는 즉시 핀이 다시 살아남)을 실행하기 전까지는 라우팅이 다시 전환되지 않습니다. `roles.*` 항목 미설정 시 `defaultModel`로 확정 폴백되는 건 `standard` 티어뿐이며, `high`/`complex` 티어는 폴백 전에 실시간으로 크레덴셜된 가장 강력한 모델을 먼저 탐색하므로, 설정이 없어도 매 턴 다른 모델로 갈 수 있습니다.
|
|
95
|
+
> **`/model <name>`으로 특정 모델을 수동 지정하면 그 세션 동안 라우팅이 고정됩니다.** 프롬프트 라우팅(`/route`)은 모델이 수동으로 고정되지 않은 동안만 매 턴 재평가됩니다. `/model <name>`으로 특정 모델을 선택하면 그 선택이 그대로 고정되며, `/model auto`(핀을 완전히 해제)를 실행하거나 `/route on`(핀을 지우지 않고 그보다 우선순위를 높임 — `/route off` 하는 즉시 핀이 다시 살아남)을 실행하기 전까지는 라우팅이 다시 전환되지 않습니다. `roles.*` 항목 미설정 시 `defaultModel`로 확정 폴백되는 건 `standard` 티어뿐이며, `high`/`complex` 티어는 보통 폴백 전에 실시간으로 크레덴셜된 가장 강력한 모델을 먼저 탐색하므로, 설정이 없어도 매 턴 다른 모델로 갈 수 있습니다. **예외:** Antigravity 또는 Gemini OAuth로 크레덴셜된 세션은 하나의 크레덴셜로 Anthropic/Google/OpenAI 모델을 함께 재노출하는데, 이 경우 `high`/`complex`는 (항상 최강 모델이 아니라) 회사당 모델 1개로 세션-안정적으로 분산되어, 턴마다 바뀌지 않고 해당 세션 내내 고정됩니다.
|
|
96
96
|
|
|
97
97
|
## Spec-first 워크플로
|
|
98
98
|
|
|
@@ -266,11 +266,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
266
266
|
## 변경 이력 (Changelog)
|
|
267
267
|
|
|
268
268
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
269
|
+
- **[0.8.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
|
+
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
269
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.
|
|
270
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.
|
|
271
273
|
- **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
|
|
272
|
-
- **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
|
|
273
|
-
- **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
|
|
274
274
|
|
|
275
275
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
276
276
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Inside the `jeo` REPL (Tab autocompletes; `/` opens the palette).
|
|
|
97
97
|
| `jeo autopilot status` | Ratchet status field with score direction, keep/revert counts, and next action |
|
|
98
98
|
|
|
99
99
|
> [!CAUTION]
|
|
100
|
-
> **`/model <name>` locks routing for the rest of the session.** Prompt routing (`/route`) only re-evaluates per turn while no model is manually pinned. Picking a specific model via `/model <name>` freezes that choice — routing will *not* switch away from it again until you run `/model auto` (which clears the pin), or `/route on` (which *outranks* an active pin without clearing it — the pin reasserts itself the moment you run `/route off`). Missing a `roles.*` entry only guarantees a `defaultModel` fallback on the `standard` tier; the `high`/`complex` tiers
|
|
100
|
+
> **`/model <name>` locks routing for the rest of the session.** Prompt routing (`/route`) only re-evaluates per turn while no model is manually pinned. Picking a specific model via `/model <name>` freezes that choice — routing will *not* switch away from it again until you run `/model auto` (which clears the pin), or `/route on` (which *outranks* an active pin without clearing it — the pin reasserts itself the moment you run `/route off`). Missing a `roles.*` entry only guarantees a `defaultModel` fallback on the `standard` tier; the `high`/`complex` tiers otherwise scan for the strongest live-credentialed model, so they can still land on a different model each turn even when unconfigured. **Exception:** an Antigravity- or Gemini-OAuth-credentialed session re-exports Anthropic/Google/OpenAI models under one credential — `high`/`complex` there instead session-stably spread across one model per company (not necessarily the strongest), so the pick stays fixed for that session rather than varying turn to turn.
|
|
101
101
|
|
|
102
102
|
## Spec-first workflow
|
|
103
103
|
|
|
@@ -273,11 +273,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
273
273
|
## Changelog
|
|
274
274
|
|
|
275
275
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
276
|
+
- **[0.8.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.
|
|
277
|
+
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
276
278
|
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
277
279
|
- **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
|
|
278
280
|
- **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
|
|
279
|
-
- **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
|
|
280
|
-
- **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
|
|
281
281
|
|
|
282
282
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
283
283
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -91,7 +91,7 @@ jeo --tmux # 在独立 tmux 会话中运行
|
|
|
91
91
|
| `jeo autopilot status` | 显示分数方向、keep/revert 次数和下一步动作的 ratchet 状态字段 |
|
|
92
92
|
|
|
93
93
|
> [!CAUTION]
|
|
94
|
-
> **通过 `/model <name>` 手动指定模型后,路由会在本会话内保持锁定。** Prompt routing(`/route`)只在没有手动锁定模型时才会逐轮重新评估。用 `/model <name>` 选定具体模型后,该选择会被锁定 — 直到你运行 `/model auto`(彻底解除锁定),或运行 `/route on`(不会清除锁定,只是优先级更高 — 一旦运行 `/route off`,锁定会立刻恢复)之前,路由都不会再切换。未配置 `roles.*` 条目时,只有 `standard` 层级会确定性地退回到 `defaultModel`;`high`/`complex`
|
|
94
|
+
> **通过 `/model <name>` 手动指定模型后,路由会在本会话内保持锁定。** Prompt routing(`/route`)只在没有手动锁定模型时才会逐轮重新评估。用 `/model <name>` 选定具体模型后,该选择会被锁定 — 直到你运行 `/model auto`(彻底解除锁定),或运行 `/route on`(不会清除锁定,只是优先级更高 — 一旦运行 `/route off`,锁定会立刻恢复)之前,路由都不会再切换。未配置 `roles.*` 条目时,只有 `standard` 层级会确定性地退回到 `defaultModel`;`high`/`complex` 层级通常会先实时扫描已配置凭证中最强的可用模型再决定是否回退,所以即使未配置,也可能每轮落到不同的模型上。**例外:** 通过 Antigravity 或 Gemini OAuth 授权的会话会用同一个凭证重新导出 Anthropic/Google/OpenAI 的模型 —此时 `high`/`complex` 会按公司各选一个模型、以会话为单位稳定分布(不一定是最强的那个),因此在同一会话内保持固定,而不是逐轮变化。
|
|
95
95
|
|
|
96
96
|
## Spec-first 工作流
|
|
97
97
|
|
|
@@ -265,11 +265,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
265
265
|
## 更新日志 (Changelog)
|
|
266
266
|
|
|
267
267
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
268
|
+
- **[0.8.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.
|
|
269
|
+
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
268
270
|
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
269
271
|
- **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
|
|
270
272
|
- **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
|
|
271
|
-
- **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
|
|
272
|
-
- **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
|
|
273
273
|
|
|
274
274
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
275
275
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/engine.ts
CHANGED
|
@@ -27,7 +27,8 @@ import { isContextOverflowError, isRefusalError, friendlyProviderError } from ".
|
|
|
27
27
|
import { runPreToolHooks, runPostTurnHooksForBatch } from "./hooks";
|
|
28
28
|
import { truncateToolOutput, formatToolResultBody } from "./tool-output";
|
|
29
29
|
export { TOOL_OUTPUT_MAX, READ_OUTPUT_MAX, TOOL_SPILL_THRESHOLD, MAX_TOOL_ARTIFACTS, truncateToolOutput, spillToolResult } from "./tool-output";
|
|
30
|
-
import { StepBudget, dynamicStepBudgetConfig, resolveStepBudgetConfig, hashSignature, type StepBudgetConfig } from "./step-budget";
|
|
30
|
+
import { StepBudget, dynamicStepBudgetConfig, resolveStepBudgetConfig, hashSignature, toolTarget, type StepBudgetConfig } from "./step-budget";
|
|
31
|
+
|
|
31
32
|
import { historyTokens, trimToolResultsInPlace, stripReasoningArtifactsInPlace } from "./compaction";
|
|
32
33
|
import { jeoEnv } from "../util/env";
|
|
33
34
|
import { GUARD_LIMITS, isVerificationSignal, repeatHint, classifyDoneGate } from "./loop-guards";
|
|
@@ -258,8 +259,17 @@ export interface AgentLoopEvents {
|
|
|
258
259
|
/** Consulted when a lone `done` arrives. Return a corrective message to bounce
|
|
259
260
|
* the done ONCE (e.g. "todo list still shows unfinished items — update it
|
|
260
261
|
* first"); return null to let the turn finish. The engine guarantees at most
|
|
261
|
-
* one bounce per turn, so a stubborn model can never loop here.
|
|
262
|
-
|
|
262
|
+
* one bounce per turn, so a stubborn model can never loop here. `evidence` is
|
|
263
|
+
* the SAME mutation/verification signal the engine's own done-gate already
|
|
264
|
+
* computed (see `classifyDoneGate` above) — a caller-owned gate (e.g. the goal
|
|
265
|
+
* verifier) can use it to deterministically downgrade an LLM-judged MET verdict
|
|
266
|
+
* when the turn mutated files without a fresh passing verification, instead of
|
|
267
|
+
* trusting the transcript self-report alone. */
|
|
268
|
+
onBeforeDone?(
|
|
269
|
+
reason: string,
|
|
270
|
+
evidence: { sawMutation: boolean; sawVerification: boolean; verificationStale: boolean }
|
|
271
|
+
): Promise<string | null> | string | null;
|
|
272
|
+
|
|
263
273
|
/** Fired when a mid-turn steering message (an additional user query typed while
|
|
264
274
|
* the turn is running) is injected into the live history. `text` is the raw
|
|
265
275
|
* user line — drives a TUI notice so the user sees their input was picked up. */
|
|
@@ -994,7 +1004,12 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
994
1004
|
// [DONE] with the Todos checklist still showing 1 in-progress + 4 pending
|
|
995
1005
|
// because nothing ever forced a status update.
|
|
996
1006
|
if (!beforeDoneNudgeUsed && ev.onBeforeDone) {
|
|
997
|
-
const nudge = await ev.onBeforeDone((toolCalls[0].arguments?.reason as string) ?? ""
|
|
1007
|
+
const nudge = await ev.onBeforeDone((toolCalls[0].arguments?.reason as string) ?? "", {
|
|
1008
|
+
sawMutation,
|
|
1009
|
+
sawVerification,
|
|
1010
|
+
verificationStale: sawMutation && lastMutationSeq > lastVerificationSeq,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
998
1013
|
if (nudge) {
|
|
999
1014
|
beforeDoneNudgeUsed = true;
|
|
1000
1015
|
pushAssistantTurn(history, responseText, reasonBuf, artifactBuf);
|
|
@@ -1044,6 +1059,22 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1044
1059
|
// once per call here (instead of joining full sigs, hashing, then re-hashing
|
|
1045
1060
|
// inside budget.record) avoids re-scanning a large write body 3× per step.
|
|
1046
1061
|
const callSigs = toolCalls.map(c => hashSignature(`${c.tool}:${JSON.stringify(c.arguments ?? {})}`));
|
|
1062
|
+
// Coarse per-call TARGET (e.g. `edit src/x.ts`) — feeds the budget's novelty rule
|
|
1063
|
+
// separately from the exact-args digest above. A model that keeps rewriting the
|
|
1064
|
+
// SAME file/command with slightly different content each attempt produces a fresh
|
|
1065
|
+
// `callSigs` entry every time (never "seen" before) but the SAME `callTargets`
|
|
1066
|
+
// entry, so that thrashing stops earning step-budget extensions after its first try.
|
|
1067
|
+
// `toolTarget` falls back to the bare tool name when it can't find an identifying
|
|
1068
|
+
// field (e.g. a custom/dynamic tool with no filePath/command/pattern argument) — that
|
|
1069
|
+
// generic case carries NO real identity ("work" for every call), so treat it as "no
|
|
1070
|
+
// coarse target" and let `budget.record` fall back to the exact-args signature
|
|
1071
|
+
// instead (unchanged legacy novelty for tools this helper doesn't specifically know).
|
|
1072
|
+
const callTargets = toolCalls.map(c => {
|
|
1073
|
+
const t = toolTarget(c.tool, c.arguments);
|
|
1074
|
+
return t.toLowerCase() === (c.tool || "").toLowerCase() ? undefined : t;
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
|
|
1047
1078
|
const sig = callSigs.length === 1 ? callSigs[0] : hashSignature(callSigs.join(" | "));
|
|
1048
1079
|
if (sig === lastSig) repeatCount++;
|
|
1049
1080
|
else {
|
|
@@ -1299,7 +1330,8 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1299
1330
|
// edits plus one trivial successful read must not look like a progressing
|
|
1300
1331
|
// step to the extension heuristic (that loophole earned endless extensions).
|
|
1301
1332
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
1302
|
-
if (results[i].executed) budget.record(callSigs[i], results[i].success);
|
|
1333
|
+
if (results[i].executed) budget.record(callSigs[i], results[i].success, callTargets[i]);
|
|
1334
|
+
|
|
1303
1335
|
}
|
|
1304
1336
|
// done-verification guard bookkeeping: write/edit successes mark the turn as
|
|
1305
1337
|
// mutating; a successful bash whose command/output looks like a test/build run
|
|
@@ -78,8 +78,47 @@ Do not include any other text, markdown formatting, or code blocks. Output raw J
|
|
|
78
78
|
verdict: "NOT_MET",
|
|
79
79
|
reason: `Goal verification failed to parse or execute: ${(err as Error).message}. Please verify the goal manually.`
|
|
80
80
|
};
|
|
81
|
+
|
|
81
82
|
}
|
|
82
83
|
}
|
|
84
|
+
|
|
85
|
+
/** Evidence the engine's own done-gate already computed this turn (see
|
|
86
|
+
* `classifyDoneGate`/`engine.ts`) — NOT re-derived here, just consumed, so this
|
|
87
|
+
* stays a pure function over already-verified signals rather than a second
|
|
88
|
+
* regex/text scan. */
|
|
89
|
+
export interface TurnEvidence {
|
|
90
|
+
sawMutation: boolean;
|
|
91
|
+
sawVerification: boolean;
|
|
92
|
+
verificationStale: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Deterministic downgrade gate: an LLM-judged MET verdict is trustworthy only when
|
|
97
|
+
* the turn either made no file changes, or made changes AND a fresh (non-stale)
|
|
98
|
+
* verification signal (test/build/typecheck/lint) was observed. A turn that mutated
|
|
99
|
+
* files with no verification at all, or whose only verification predates the last
|
|
100
|
+
* mutation, cannot self-report MET on the transcript alone — that is exactly the
|
|
101
|
+
* "gate theater" failure mode (an LLM asserting success without re-checked evidence).
|
|
102
|
+
* NOT_MET/IMPOSSIBLE verdicts pass through unchanged: this only ever tightens MET,
|
|
103
|
+
* never loosens a verdict the LLM itself already found lacking.
|
|
104
|
+
*/
|
|
105
|
+
export function applyEvidenceGate(verdict: GoalVerdict, evidence: TurnEvidence): GoalVerdict {
|
|
106
|
+
if (verdict.verdict !== "MET") return verdict;
|
|
107
|
+
if (!evidence.sawMutation) return verdict; // nothing changed — the transcript judgment stands
|
|
108
|
+
if (evidence.sawVerification && !evidence.verificationStale) return verdict; // fresh verification backs the claim
|
|
109
|
+
|
|
110
|
+
const gap = !evidence.sawVerification
|
|
111
|
+
? "the turn modified files but no test/build/typecheck/lint run was observed"
|
|
112
|
+
: "the turn's only verification run predates its last file modification (stale evidence)";
|
|
113
|
+
return {
|
|
114
|
+
verdict: "NOT_MET",
|
|
115
|
+
reason:
|
|
116
|
+
`Goal verifier LLM judged MET, but this was overridden by deterministic evidence: ${gap}. ` +
|
|
117
|
+
`Re-run the relevant verification AFTER the last file change, then call done again. ` +
|
|
118
|
+
`(Original LLM reason: ${verdict.reason})`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
83
122
|
|
|
84
123
|
|
|
85
124
|
export interface GoalState {
|
|
@@ -9,12 +9,21 @@ import type { RouteDecision } from "./prompt-router";
|
|
|
9
9
|
|
|
10
10
|
export interface RouteHistoryEntry extends RouteDecision {
|
|
11
11
|
timestamp: number; // Date.now()
|
|
12
|
-
|
|
12
|
+
/** 1-indexed sequence number of this RECORDED DECISION (not the REPL turn) —
|
|
13
|
+
* `add()` fires once for the pre-call routing pick AND once more per
|
|
14
|
+
* mid-turn equivalent-pool fallback (launch.ts's retry loop), so a single
|
|
15
|
+
* user turn that hits a fallback can produce several CONSECUTIVE entries
|
|
16
|
+
* here. Do not treat this as a 1:1 conversational-turn counter. */
|
|
17
|
+
turnNumber: number;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* Bounded FIFO queue of routing decisions. Oldest entries are dropped when
|
|
17
|
-
* the queue exceeds `maxSize`.
|
|
22
|
+
* the queue exceeds `maxSize`. The REPL constructs this with the default
|
|
23
|
+
* `maxSize` (10) — `/route history <n>`'s `n` argument only WINDOWS this
|
|
24
|
+
* already-capped retention (`launch.ts`'s single `new RouteHistory()` call
|
|
25
|
+
* site never passes a wider size), so `/route history 50` silently returns
|
|
26
|
+
* at most the last 10 decisions recorded this session, not 50.
|
|
18
27
|
*/
|
|
19
28
|
export class RouteHistory {
|
|
20
29
|
private entries: RouteHistoryEntry[] = [];
|
package/src/agent/step-budget.ts
CHANGED
|
@@ -168,17 +168,29 @@ export class StepBudget {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/** Record an executed tool call (ring-buffered to the scoring window).
|
|
171
|
-
* Stored as a fixed-size digest — see `hashSignature` (memory bound).
|
|
172
|
-
|
|
171
|
+
* Stored as a fixed-size digest — see `hashSignature` (memory bound).
|
|
172
|
+
*
|
|
173
|
+
* `targetKey` (optional) is the COARSE target (e.g. `edit src/x.ts`) the novelty rule
|
|
174
|
+
* scores against, instead of `signature`'s full-args hash. Without it, retrying an
|
|
175
|
+
* edit on the SAME file with slightly different content each attempt looks "novel"
|
|
176
|
+
* forever (the exact args hash never repeats) and the extension heuristic reads that
|
|
177
|
+
* edit-thrashing loop as endless progress. With it, only the FIRST attempt at a given
|
|
178
|
+
* target counts as novel; grinding on the same file/command afterwards does not, so a
|
|
179
|
+
* stuck rewrite loop stops earning extensions even though every individual call
|
|
180
|
+
* "succeeds" and every exact signature is technically unique. Falls back to `signature`
|
|
181
|
+
* when omitted (unchanged behavior for existing callers/tests). */
|
|
182
|
+
record(signature: string, success: boolean, targetKey?: string): void {
|
|
173
183
|
const sig = hashSignature(signature);
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
const noveltyKey = hashSignature(targetKey ?? signature);
|
|
185
|
+
if (!this.seen.has(noveltyKey)) {
|
|
186
|
+
this.seen.add(noveltyKey);
|
|
176
187
|
this.novelSinceExtension++;
|
|
177
188
|
}
|
|
178
189
|
this.window.push({ signature: sig, success });
|
|
179
190
|
if (this.window.length > this.cfg.windowSize) this.window.shift();
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
|
|
182
194
|
/** A mid-turn steering message arrived — fresh, user-driven work. Grant headroom
|
|
183
195
|
* (capped at the hard cap, without consuming the extension budget) and clear the
|
|
184
196
|
* scoring window so the new instruction is never declined by the previous
|
|
@@ -239,3 +251,31 @@ export class StepBudget {
|
|
|
239
251
|
};
|
|
240
252
|
}
|
|
241
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* A concise, gjc-style label for a tool call's actual TARGET (file / command / glob) —
|
|
256
|
+
* not just the bare tool name plus its full argument blob. Two calls that touch the SAME
|
|
257
|
+
* target (e.g. `edit src/x.ts` twice with different bodies) collapse to the same target
|
|
258
|
+
* key here even though `hashSignature` of their full arguments differs. Shared by the
|
|
259
|
+
* budget's novelty rule (below) and `task-tool.ts`'s live subagent monitor label — kept
|
|
260
|
+
* pure/local (no TUI dependency in the agent layer).
|
|
261
|
+
*/
|
|
262
|
+
export function toolTarget(tool: string, rawArgs: unknown): string {
|
|
263
|
+
const a = (rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs) ? rawArgs : {}) as Record<string, unknown>;
|
|
264
|
+
const t = (tool || "").toLowerCase();
|
|
265
|
+
const str = (...keys: string[]): string => {
|
|
266
|
+
for (const k of keys) { const v = a[k]; if (typeof v === "string" && v.length > 0) return v; }
|
|
267
|
+
return "";
|
|
268
|
+
};
|
|
269
|
+
if (t === "bash") {
|
|
270
|
+
const cmd = str("command", "cmd").split("\n")[0]!.trim();
|
|
271
|
+
return cmd ? `bash: ${cmd.length > 80 ? cmd.slice(0, 79) + "…" : cmd}` : "bash";
|
|
272
|
+
}
|
|
273
|
+
if (t === "read" || t === "write" || t === "edit") {
|
|
274
|
+
const f = str("filePath", "path");
|
|
275
|
+
return f ? `${t} ${f}` : t;
|
|
276
|
+
}
|
|
277
|
+
if (t === "find") { const g = str("globPattern", "pattern"); return g ? `find ${g}` : "find"; }
|
|
278
|
+
if (t === "search") { const p = str("pattern"); return p ? `search ${p}` : "search"; }
|
|
279
|
+
if (t === "task") { const r = str("role"); return r ? `task ${r}` : "task"; }
|
|
280
|
+
return tool || "tool";
|
|
281
|
+
}
|
package/src/agent/task-tool.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* `task` itself, so delegation cannot recurse infinitely.
|
|
11
11
|
*/
|
|
12
12
|
import { runAgentLoop, type ToolHandler } from "./engine";
|
|
13
|
+
import { toolTarget } from "./step-budget";
|
|
14
|
+
|
|
13
15
|
import type { ToolResult } from "./tools";
|
|
14
16
|
import type { Message } from "./loop";
|
|
15
17
|
import { loadProjectContext, withProjectContext } from "./context-files";
|
|
@@ -146,31 +148,6 @@ export function taskToolProtocolLine(config?: Pick<Config, "subagents">): string
|
|
|
146
148
|
);
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
/**
|
|
150
|
-
* A concise, gjc-style label for a subagent's tool call — the actual TARGET (file / command /
|
|
151
|
-
* glob), not just the bare tool name — so the parent's live monitor shows "read src/x.ts" or
|
|
152
|
-
* "bash: bun test" instead of "read"/"bash". Kept local (no TUI dependency in the agent layer).
|
|
153
|
-
*/
|
|
154
|
-
function toolTarget(tool: string, rawArgs: unknown): string {
|
|
155
|
-
const a = (rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs) ? rawArgs : {}) as Record<string, unknown>;
|
|
156
|
-
const t = (tool || "").toLowerCase();
|
|
157
|
-
const str = (...keys: string[]): string => {
|
|
158
|
-
for (const k of keys) { const v = a[k]; if (typeof v === "string" && v.length > 0) return v; }
|
|
159
|
-
return "";
|
|
160
|
-
};
|
|
161
|
-
if (t === "bash") {
|
|
162
|
-
const cmd = str("command", "cmd").split("\n")[0]!.trim();
|
|
163
|
-
return cmd ? `bash: ${cmd.length > 80 ? cmd.slice(0, 79) + "…" : cmd}` : "bash";
|
|
164
|
-
}
|
|
165
|
-
if (t === "read" || t === "write" || t === "edit") {
|
|
166
|
-
const f = str("filePath", "path");
|
|
167
|
-
return f ? `${t} ${f}` : t;
|
|
168
|
-
}
|
|
169
|
-
if (t === "find") { const g = str("globPattern", "pattern"); return g ? `find ${g}` : "find"; }
|
|
170
|
-
if (t === "search") { const p = str("pattern"); return p ? `search ${p}` : "search"; }
|
|
171
|
-
if (t === "task") { const r = str("role"); return r ? `task ${r}` : "task"; }
|
|
172
|
-
return tool || "tool";
|
|
173
|
-
}
|
|
174
151
|
|
|
175
152
|
function firstUsefulLine(output: string | undefined): string {
|
|
176
153
|
if (!output) return "";
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -98,7 +98,8 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
|
|
|
98
98
|
{ canonical: "claude-sonnet-4-6", provider: "anthropic", providerModel: "claude-sonnet-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
|
|
99
99
|
{ canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
|
|
100
100
|
{ canonical: "claude-haiku-4-5", provider: "anthropic", providerModel: "claude-haiku-4-5-20251001", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true, releaseDate: "2025-10" },
|
|
101
|
-
// OpenAI (newest first: gpt-5.5 > gpt-5.4 > o4-mini/o3/gpt-4.1 > o3-mini > gpt-4o-mini > gpt-4o)
|
|
101
|
+
// OpenAI (newest first: gpt-5.6 > gpt-5.5 > gpt-5.4 > o4-mini/o3/gpt-4.1 > o3-mini > gpt-4o-mini > gpt-4o)
|
|
102
|
+
{ canonical: "gpt-5.6", provider: "openai", providerModel: "gpt-5.6", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
|
|
102
103
|
{ canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
|
|
103
104
|
{ canonical: "gpt-5.4", provider: "openai", providerModel: "gpt-5.4", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-03" },
|
|
104
105
|
{ canonical: "gpt-5.4-mini", provider: "openai", providerModel: "gpt-5.4-mini", contextTokens: 272_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-03" },
|
|
@@ -415,6 +415,19 @@ export function captureLivePromptInputChunk(state: PromptInputQueue, chunk: stri
|
|
|
415
415
|
return accepted;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Whether an in-flight readline line buffer (captured right before a hard
|
|
420
|
+
* exit — Ctrl+D/EOF/disconnected terminal) should be persisted as an unsent
|
|
421
|
+
* draft (gajae-code "unsent prompt survives /resume" parity). Whitespace-only
|
|
422
|
+
* or empty input is never a real draft. Pure predicate so the exit-path
|
|
423
|
+
* wiring in launch.ts (which owns the actual persistence side effect) stays
|
|
424
|
+
* thin and this decision is independently testable.
|
|
425
|
+
*/
|
|
426
|
+
export function draftFromUnsentLine(line: string | undefined): string | undefined {
|
|
427
|
+
const trimmed = (line ?? "").trim();
|
|
428
|
+
return trimmed ? trimmed : undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
418
431
|
/**
|
|
419
432
|
* TTY "new input first" contract: fold any queued FULL lines (stray
|
|
420
433
|
* Enter-terminated buffer noise, or older persisted queues) into the editable
|
|
@@ -69,7 +69,10 @@ function explainDecision(decision: RouteDecision | { note: string } | null): str
|
|
|
69
69
|
/**
|
|
70
70
|
* Format the last `n` history entries (default 10) as one line each:
|
|
71
71
|
* `turn N: tier -> model (source, confidence X.XX)`, oldest of the selected
|
|
72
|
-
* window first — matches `getAll()`'s chronological ordering.
|
|
72
|
+
* window first — matches `getAll()`'s chronological ordering. `entries` is
|
|
73
|
+
* ALREADY capped to `RouteHistory`'s `maxSize` (default 10) by the caller —
|
|
74
|
+
* `n` only narrows that window further, it can never widen it, so a large
|
|
75
|
+
* `n` (e.g. 50) silently returns at most `maxSize` entries.
|
|
73
76
|
*/
|
|
74
77
|
function formatHistory(entries: RouteHistoryEntry[], n: number): string[] {
|
|
75
78
|
if (entries.length === 0) return ["No routing decisions recorded yet this session."];
|
package/src/commands/launch.ts
CHANGED
|
@@ -50,7 +50,7 @@ import { readGlobalConfig, saveConfigPatch, resolveWikiRoot } from "../agent/sta
|
|
|
50
50
|
import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recency";
|
|
51
51
|
import { describeModel, describeAllProviders, describeProvider, resolveProvider, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, resolveRoleModel, CODEX_MODELS, qualifyModelId, modelServableWithConfig, isLocalProviderReachable } from "../ai";
|
|
52
52
|
import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
|
|
53
|
-
import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../agent/goal-verifier";
|
|
53
|
+
import { readGoalState, writeGoalState, clearGoalState, verifyGoal, applyEvidenceGate } from "../agent/goal-verifier";
|
|
54
54
|
import { routePrompt, deriveCacheSessionKey, warnOnce, tierModelPool, selectFromPool, PROMPT_TIERS, withRoutingTierSetting, inferTierForModel, type PromptTier, type RouteDecision } from "../agent/prompt-router";
|
|
55
55
|
import { RouteHistory } from "../agent/route-history";
|
|
56
56
|
|
|
@@ -111,7 +111,6 @@ import {
|
|
|
111
111
|
exportSession,
|
|
112
112
|
updateSessionModel,
|
|
113
113
|
updateSessionDraft,
|
|
114
|
-
|
|
115
114
|
appendCompaction,
|
|
116
115
|
resolveSessionRef,
|
|
117
116
|
} from "../agent/session";
|
|
@@ -165,6 +164,7 @@ import {
|
|
|
165
164
|
boxVerticalNavAction,
|
|
166
165
|
queuePromptInputChunk,
|
|
167
166
|
captureLivePromptInputChunk,
|
|
167
|
+
draftFromUnsentLine,
|
|
168
168
|
restoreQueuedLinesToPrefill,
|
|
169
169
|
createInFlightAbortHarness,
|
|
170
170
|
classifyMidTurnLine,
|
|
@@ -264,6 +264,7 @@ export {
|
|
|
264
264
|
boxVerticalNavAction,
|
|
265
265
|
queuePromptInputChunk,
|
|
266
266
|
captureLivePromptInputChunk,
|
|
267
|
+
draftFromUnsentLine,
|
|
267
268
|
restoreQueuedLinesToPrefill,
|
|
268
269
|
createInFlightAbortHarness,
|
|
269
270
|
classifyMidTurnLine,
|
|
@@ -689,6 +690,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
689
690
|
let lastUserInput = "";
|
|
690
691
|
// Full untruncated text of the last assistant reply — surfaced in detail by Ctrl+O.
|
|
691
692
|
let lastReply = "";
|
|
693
|
+
// Unsent draft restored from a NON-interactive resume path (`--resume <id>`,
|
|
694
|
+
// `--continue`/`-c`) — the interactive-picker and mid-loop `/session resume`
|
|
695
|
+
// paths feed `queuedPromptInput.partial` directly (declared much later); this
|
|
696
|
+
// one exists because `queuedPromptInput` isn't in scope yet at startup. Read
|
|
697
|
+
// once by `queuedPromptInput`'s own initializer below, then not touched again.
|
|
698
|
+
let startupDraft: string | undefined;
|
|
692
699
|
// Full untruncated output of the most recent tool call — the clipped forge
|
|
693
700
|
// card's `⟦Ctrl+O for more⟧` hint resolves here.
|
|
694
701
|
let lastToolDetail: { tool: string; output: string } | null = null;
|
|
@@ -754,10 +761,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
754
761
|
|
|
755
762
|
// pi-style session persistence: resume an existing session or create a new one.
|
|
756
763
|
let sessionId: string | undefined;
|
|
757
|
-
// Unsent input-box draft restored from a resumed session's header (gajae-code
|
|
758
|
-
// parity) — folded into the very first prompt once `queuedPromptInput` exists.
|
|
759
|
-
let restoredDraft = "";
|
|
760
|
-
|
|
761
764
|
let compactionSeq = 0;
|
|
762
765
|
if (!flags.noSession) {
|
|
763
766
|
if (flags.resumeInteractive) {
|
|
@@ -776,8 +779,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
776
779
|
for (const m of messages) history.push(m);
|
|
777
780
|
sessionId = id;
|
|
778
781
|
if (!initialSessionModel && header.model) sessionModel = header.model;
|
|
779
|
-
if (header.draft) restoredDraft = header.draft;
|
|
780
|
-
|
|
781
782
|
const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
|
|
782
783
|
console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
|
|
783
784
|
} catch (err) {
|
|
@@ -816,8 +817,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
816
817
|
// Restore the model this session was last using unless the CLI explicitly
|
|
817
818
|
// pinned one (flags.model/role/provider → initialSessionModel wins).
|
|
818
819
|
if (!initialSessionModel && header.model) sessionModel = header.model;
|
|
819
|
-
|
|
820
|
-
|
|
820
|
+
// Restore the unsent input-box draft (gajae-code parity) — see
|
|
821
|
+
// startupDraft's own doc comment for why this path can't feed
|
|
822
|
+
// queuedPromptInput.partial directly yet.
|
|
823
|
+
if (header.draft) startupDraft = header.draft;
|
|
821
824
|
const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
|
|
822
825
|
console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
|
|
823
826
|
} catch (err) {
|
|
@@ -851,6 +854,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
851
854
|
} catch { /* best-effort */ }
|
|
852
855
|
};
|
|
853
856
|
|
|
857
|
+
// Persist (or clear) the unsent input-box draft into the session header so a
|
|
858
|
+
// later `/resume` restores it into the prompt (gajae-code parity: an
|
|
859
|
+
// in-progress prompt survives a quit + resume instead of being silently
|
|
860
|
+
// lost). Best-effort, same contract as persistSessionModel above — a
|
|
861
|
+
// header-rewrite failure must never abort the exit path that calls this.
|
|
862
|
+
const persistSessionDraft = async (draft: string): Promise<void> => {
|
|
863
|
+
if (flags.noSession || !sessionId) return;
|
|
864
|
+
try {
|
|
865
|
+
await updateSessionDraft(sessionId, draft, cwd);
|
|
866
|
+
} catch { /* best-effort */ }
|
|
867
|
+
};
|
|
868
|
+
|
|
854
869
|
// `step N/M` display seed: the explicit --max-steps cap, else the dynamic budget's
|
|
855
870
|
// rolling base — the engine's onBudget event keeps the denominator honest as it grows.
|
|
856
871
|
const initialStepLimit = flags.maxSteps > 0 ? flags.maxSteps : initialDynamicStepLimit();
|
|
@@ -1292,7 +1307,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1292
1307
|
// Todos checklist used to end a finished turn stuck at "✓0 ◐1 ·4 / 5"
|
|
1293
1308
|
// because nothing ever forced the model to update item statuses).
|
|
1294
1309
|
let turnTodos: { title: string; status: string }[] = [];
|
|
1295
|
-
const onBeforeDone = async (
|
|
1310
|
+
const onBeforeDone = async (
|
|
1311
|
+
reason: string,
|
|
1312
|
+
evidence: { sawMutation: boolean; sawVerification: boolean; verificationStale: boolean },
|
|
1313
|
+
): Promise<string | null> => {
|
|
1296
1314
|
const unfinished = turnTodos.filter(t => t.status !== "done");
|
|
1297
1315
|
if (turnTodos.length > 0 && unfinished.length > 0) {
|
|
1298
1316
|
return (
|
|
@@ -1314,7 +1332,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1314
1332
|
}
|
|
1315
1333
|
|
|
1316
1334
|
if (tui) tui.events().onNotice?.("[Goal Verifier] Running goal verification...");
|
|
1317
|
-
const
|
|
1335
|
+
const llmVerdict = await verifyGoal(goalState.condition, history, activeModel);
|
|
1336
|
+
// Deterministic downgrade: a fresh MET from the transcript-only LLM
|
|
1337
|
+
// judge still needs mutation/verification evidence to back it up —
|
|
1338
|
+
// see applyEvidenceGate's own docs (goal-verifier.ts). NOT_MET/
|
|
1339
|
+
// IMPOSSIBLE pass through unchanged; this only ever tightens MET.
|
|
1340
|
+
const verdict = applyEvidenceGate(llmVerdict, evidence);
|
|
1318
1341
|
|
|
1319
1342
|
goalState.verdicts.push({
|
|
1320
1343
|
at: Date.now(),
|
|
@@ -2192,13 +2215,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2192
2215
|
// IMMEDIATELY on its next iteration, bypassing the "new input first" prefill contract
|
|
2193
2216
|
// (the user explicitly invoked them — no second Enter).
|
|
2194
2217
|
const pendingMidTurnCommands: string[] = [];
|
|
2195
|
-
const queuedPromptInput: PromptInputQueue = { pendingLines: pendingStdinLines, partial: "", pastedLines: [], inPaste: false };
|
|
2196
|
-
// Fold a restored `/resume` draft (session header `draft`) into the very first
|
|
2197
|
-
// prompt the same way a queued mid-turn prefill lands — visible in the input box,
|
|
2198
|
-
// Enter to run, Esc/Ctrl+U to discard (gajae-code parity: an in-progress prompt
|
|
2199
|
-
// survives a quit + resume instead of being silently lost).
|
|
2200
|
-
if (restoredDraft) queuedPromptInput.partial = restoredDraft;
|
|
2201
|
-
|
|
2218
|
+
const queuedPromptInput: PromptInputQueue = { pendingLines: pendingStdinLines, partial: startupDraft ?? "", pastedLines: [], inPaste: false };
|
|
2202
2219
|
queueBusyInput = (chunk: string) => captureLivePromptInputChunk(queuedPromptInput, chunk);
|
|
2203
2220
|
queueBusyPasteActive = () => queuedPromptInput.inPaste;
|
|
2204
2221
|
queueBusySnapshot = () => ({
|
|
@@ -2210,19 +2227,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2210
2227
|
// default routes to the immediate-dispatch queue served at the top of the loop.
|
|
2211
2228
|
(jeoEnv("NO_MIDTURN_DISPATCH") === "1" ? pendingStdinLines : pendingMidTurnCommands).push(line);
|
|
2212
2229
|
};
|
|
2213
|
-
// Persist (or clear) the unsent input-box draft into the session header so a later
|
|
2214
|
-
// `/resume` restores it (gajae-code parity). Best-effort, mirrors persistSessionModel;
|
|
2215
|
-
// reads readline's own line buffer since that is the single source of truth for
|
|
2216
|
-
// "what's currently typed but not submitted" (same buffer the box-render/prefill
|
|
2217
|
-
// logic above reads from).
|
|
2218
|
-
const persistSessionDraft = async (): Promise<void> => {
|
|
2219
|
-
if (flags.noSession || !sessionId) return;
|
|
2220
|
-
const current = (rl as unknown as { line?: string }).line ?? queuedPromptInput.partial ?? "";
|
|
2221
|
-
try {
|
|
2222
|
-
await updateSessionDraft(sessionId, current, cwd);
|
|
2223
|
-
} catch { /* best-effort */ }
|
|
2224
|
-
};
|
|
2225
|
-
|
|
2226
2230
|
// Bracketed-paste line routing at the PROMPT: readline strips the 2004 markers
|
|
2227
2231
|
// and replays pasted lines as synthetic keypresses, emitting paste-start /
|
|
2228
2232
|
// paste-end around them. Lines submitted INSIDE that window are intentional
|
|
@@ -2280,6 +2284,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2280
2284
|
// hard break, not a graceful `/exit`. Some Bun/tmux paths leave stdin.isTTY
|
|
2281
2285
|
// false while stdout is still a TTY, so accept either side as interactive.
|
|
2282
2286
|
if ((process.stdin.isTTY || process.stdout.isTTY || previewEnabled) && !gracefulReadlineClose) {
|
|
2287
|
+
// Best-effort, fire-and-forget: persist whatever's still typed but unsent
|
|
2288
|
+
// (Ctrl+D / EOF / disconnected terminal — no explicit discard gesture, unlike
|
|
2289
|
+
// Ctrl+C's own "clear the box" contract, which stays untouched here) so
|
|
2290
|
+
// `/resume` can restore it (see persistSessionDraft above). Must not block
|
|
2291
|
+
// process.exit() below.
|
|
2292
|
+
const draft = draftFromUnsentLine((rl as unknown as { line?: string }).line);
|
|
2293
|
+
if (draft) void persistSessionDraft(draft);
|
|
2283
2294
|
hardExitOnLoopEnd = true;
|
|
2284
2295
|
forceExitFromCtrlC();
|
|
2285
2296
|
}
|
|
@@ -3724,8 +3735,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3724
3735
|
if (pickResult.sessionModel !== undefined) sessionModel = pickResult.sessionModel;
|
|
3725
3736
|
if (pickResult.lastUserInput !== undefined) lastUserInput = pickResult.lastUserInput;
|
|
3726
3737
|
if (pickResult.lastReply !== undefined) lastReply = pickResult.lastReply;
|
|
3738
|
+
// Restore the unsent input-box draft left over at last save (gajae-code
|
|
3739
|
+
// parity) — feeds the SAME "new input first" prefill path queued mid-turn
|
|
3740
|
+
// lines already use, so it's visible in the box, Enter to run, Esc/Ctrl+U
|
|
3741
|
+
// to discard, on the very first prompt of this resumed session.
|
|
3727
3742
|
if (pickResult.draft) queuedPromptInput.partial = pickResult.draft;
|
|
3728
|
-
|
|
3729
3743
|
}
|
|
3730
3744
|
|
|
3731
3745
|
// Remote (Telegram) free-text reply / config command routing — wired here,
|
|
@@ -3927,10 +3941,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3927
3941
|
});
|
|
3928
3942
|
sessionId = result.sessionId;
|
|
3929
3943
|
if (result.sessionModel !== undefined) sessionModel = result.sessionModel;
|
|
3944
|
+
if (result.lastUserInput !== undefined) lastUserInput = result.lastUserInput;
|
|
3930
3945
|
if (result.lastReply !== undefined) lastReply = result.lastReply;
|
|
3946
|
+
// Restore the unsent input-box draft (see the resumeInteractive call
|
|
3947
|
+
// site above for the full rationale) — `continue` below immediately
|
|
3948
|
+
// re-enters the loop top, which consumes `queuedPromptInput.partial`
|
|
3949
|
+
// as this next prompt's prefill.
|
|
3931
3950
|
if (result.draft) queuedPromptInput.partial = result.draft;
|
|
3932
|
-
|
|
3933
|
-
if (result.lastReply !== undefined) lastReply = result.lastReply;
|
|
3934
3951
|
continue;
|
|
3935
3952
|
}
|
|
3936
3953
|
if (input === "/retry") {
|
|
@@ -4631,9 +4648,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4631
4648
|
}
|
|
4632
4649
|
}
|
|
4633
4650
|
disarmPreview(); // clear footer + restore full-screen scrolling before leaving the REPL
|
|
4634
|
-
await persistSessionDraft();
|
|
4635
4651
|
if (hardExitOnLoopEnd) {
|
|
4636
|
-
|
|
4637
4652
|
try {
|
|
4638
4653
|
disarmPreview();
|
|
4639
4654
|
out.write("\x1b[?25h\n");
|