jeo-code 0.7.12 → 0.7.14

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,17 @@ 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
+
10
+ ## [0.7.14] - 2026-06-25
11
+ _Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators._
12
+
13
+ ### Added
14
+ - **`-q`/`--quiet` suppresses startup chrome.** `jeo launch -q` (and one-shot `-p`) skips the welcome box, the "what's new" release notes, the `(plain output)` / resume-hint footers, and the background "session memory distilling" line — so piped, CI, and minimal-terminal runs get just the agent's output instead of the full interactive banner set. The flag is wired through the runner (`-q`/`--quiet` help + launch-only flag set) and `parseFlags`.
15
+
16
+ ### Fixed
17
+ - **`/resume` no longer renders a wall of raw escaped JSON.** The transcript formatter extracts an embedded tool call from anywhere in an assistant message (mirroring the engine's own `extractJsonObject`) instead of only when the content begins with `{`, and distinguishes a genuine prose-prefixed call (followed by its `Tool [x] result`) from an illustrative JSON snippet quoted inside prose. Pre-call narration is kept (dimmed) and a leading `"reasoning"` field is surfaced clipped; the raw multi-line edit/write payloads never reach scrollback.
18
+ - **A turn that actually edits files now surfaces those paths, fixing the orchestrator "Agent finished but made zero valid code changes (finalArtifacts is empty)" failure.** `runAgentLoop` records every file a turn SUCCESSFULLY writes/edits and returns it as `AgentLoopResult.mutatedFiles` (repo-relative, sorted, de-duped; omitted when nothing mutated). The `task` subagent (`src/agent/task-tool.ts`) and the `team` executor (`src/commands/team.ts`) now derive their change set from this concrete list instead of an out-of-band tool counter, append a "Changed files (N): …" note to the subagent report, and base the parent UNVERIFIED/verify-independently audit on it — so a caller (or an external orchestrator like jeo-claw) collects the run's real code artifacts rather than inferring an empty change set after real edits. `.specify/` is also un-ignored in `.gitignore` so spec/plan artifacts written there are visible to a git-based artifact collector.
19
+
9
20
  ## [0.7.9] - 2026-06-24
10
21
  _Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch._
11
22
 
package/README.ja.md CHANGED
@@ -149,6 +149,18 @@ jeo ultragoal
149
149
  `jeo` は `.jeo/memory/` 配下に **ローカルファースト・蒸留されたプロジェクトメモリ** を保持します(リモートバックエンドなし、ネイティブ依存ゼロ)。過去のセッションは [OKF](docs/okf_mem/) コンセプトバンドルへ蒸留され、次のセッションは関連性の高い予算内のスライスだけをシステムプロンプトへ再注入します — 指示ではなく DATA として堅牢化されます。`JEO_NO_MEMORY=1` ですべて無効化。
150
150
 
151
151
  **移行(`jeo memory-migrate`、ワンショット・冪等).** レガシーの単一ドキュメント `MEMORY.md` をロスレスでバンドルへ変換します: `## 見出し → タイプ`、各箇条書き → タイプ別コンセプト、インデント行 → 本文; `index.md`/`log.md` を再構築し、元ファイルを `MEMORY.md.bak` にリネームします。バンドルにコンセプトができた後の再実行は no-op です。**ロールバック:** `JEO_MEMORY_LEGACY=1` はバンドルを無視し、同じ注入堅牢化を通して `MEMORY.md`/`.bak` を読みます(`JEO_NO_MEMORY=1` がすべてに優先)。
152
+ ## 既存のエージェントやボットとの連携 (Works beside your existing agent or bot)
153
+
154
+ | ツールまたはボット | 推奨される jeo コマンド | 境界 |
155
+ | ----------- | ----------------------- | -------- |
156
+ | Codex CLI | `jeo --tmux` または `jeo` | 兄弟 tmux セッションまたはインラインで実行されます。 |
157
+ | Claude Code | `jeo --tmux` または `jeo` | jeo は Claude Code の拡張機能にはなりません。 |
158
+ | OpenCode | `jeo` または `jeo --tmux` | 外部ランナーのワークフローのみ。 |
159
+ | Claw Code | `jeo --tmux` | jeo は Claw Code にインストールされたり、置き換えたりしません。 |
160
+ | 外部コントローラー / ボット | `jeo` (またはカスタム統合) | 外部コントローラーは、標準のターミナル/CLI インターフェースを介して jeo を駆動します。 |
161
+
162
+ `-q`/`--quiet` (または `JEO_QUIET=1`) を追加すると、起動バナー・ウェルカムアニメーション・リリースノート・再開ヒントが抑制され、jeo を別のエージェントと並べて実行したりボットから駆動したりできます。`-p`/`--print` は quiet を含みます。
163
+
152
164
 
153
165
  ## ローカルモデル
154
166
 
@@ -193,11 +205,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
193
205
  ## 変更履歴 (Changelog)
194
206
 
195
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
196
209
  - **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
197
210
  - **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
198
211
  - **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
199
212
  - **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
200
- - **[0.7.5]** (2026-06-23) — Startup & loop latency: redundant re-reads that fired on every subagent spawn and every loop step are now memoized behind cheap `stat`-signature caches, so cold paths stay correct (edits/deletes still picked up immediately) while warm paths skip the disk and the re-parse. Targets the per-turn and per-spawn overhead that dominates perceived slowness in team/ralph/autopilot fan-outs.
201
213
 
202
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
215
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -149,6 +149,18 @@ jeo ultragoal
149
149
  `jeo`는 `.jeo/memory/` 아래에 **로컬 우선·증류된 프로젝트 메모리**를 둡니다(원격 백엔드 없음, 네이티브 의존성 0). 지난 세션은 [OKF](docs/okf_mem/) 개념 번들로 증류되고, 다음 세션은 관련성 높은 예산 한도 내 일부만 시스템 프롬프트로 다시 주입합니다 — 지시가 아닌 DATA로 강화 처리됩니다. `JEO_NO_MEMORY=1`로 전체 비활성화.
150
150
 
151
151
  **마이그레이션 (`jeo memory-migrate`, 1회성 · 멱등).** 레거시 단일 문서 `MEMORY.md`를 무손실로 번들로 변환합니다: `## 헤딩 → 타입`, 각 불릿 → 타입별 개념, 들여쓴 줄 → 본문; `index.md`/`log.md`를 재생성하고 원본은 `MEMORY.md.bak`으로 이름을 바꿉니다. 번들에 개념이 생긴 뒤 재실행은 no-op입니다. **롤백:** `JEO_MEMORY_LEGACY=1`은 번들을 무시하고 동일한 주입 강화 처리로 `MEMORY.md`/`.bak`를 읽습니다(`JEO_NO_MEMORY=1`이 모든 것에 우선).
152
+ ## 기존 에이전트 또는 봇과 함께 작동 (Works beside your existing agent or bot)
153
+
154
+ | 도구 또는 봇 | 권장 jeo 명령 | 경계 |
155
+ | ----------- | ----------------------- | -------- |
156
+ | Codex CLI | `jeo --tmux` 또는 `jeo` | 형제 tmux 세션 또는 인라인으로 실행됩니다. |
157
+ | Claude Code | `jeo --tmux` 또는 `jeo` | jeo는 Claude Code 확장 프로그램이 되지 않습니다. |
158
+ | OpenCode | `jeo` 또는 `jeo --tmux` | 현재는 외부 러너 워크플로만 지원합니다. |
159
+ | Claw Code | `jeo --tmux` | jeo는 Claw Code에 설치되거나 대체되지 않습니다. |
160
+ | 외부 컨트롤러 / 봇 | `jeo` (또는 사용자 정의 통합) | 외부 컨트롤러는 표준 터미널/CLI 인터페이스를 통해 jeo를 구동합니다. |
161
+
162
+ `-q`/`--quiet` (또는 `JEO_QUIET=1`)를 추가하면 시작 배너, 환영 애니메이션, 릴리스 노트, 재개 힌트가 억제되어 jeo를 다른 에이전트와 나란히 실행하거나 봇으로 구동할 수 있습니다. `-p`/`--print`는 quiet를 함의합니다.
163
+
152
164
 
153
165
  ## 로컬 모델
154
166
 
@@ -193,11 +205,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
193
205
  ## 변경 이력 (Changelog)
194
206
 
195
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
196
209
  - **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
197
210
  - **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
198
211
  - **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
199
212
  - **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
200
- - **[0.7.5]** (2026-06-23) — Startup & loop latency: redundant re-reads that fired on every subagent spawn and every loop step are now memoized behind cheap `stat`-signature caches, so cold paths stay correct (edits/deletes still picked up immediately) while warm paths skip the disk and the re-parse. Targets the per-turn and per-spawn overhead that dominates perceived slowness in team/ralph/autopilot fan-outs.
201
213
 
202
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
215
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -150,6 +150,18 @@ Non-zero hook output is appended to the tool result the model reads (deduped per
150
150
  `jeo` keeps a **local-first, distilled project memory** under `.jeo/memory/` (no remote backend, zero native deps). Past sessions are distilled into an [OKF](docs/okf_mem/) concept bundle, and the next session injects only the relevant, budget-bounded slice back into the system prompt — hardened as DATA, never as instructions. Disable everything with `JEO_NO_MEMORY=1`.
151
151
 
152
152
  **Migration (`jeo memory-migrate`, one-shot · idempotent).** A legacy single-doc `MEMORY.md` is converted losslessly into the bundle: `## heading → type`, each bullet → a typed concept, indented lines → body; `index.md`/`log.md` are rebuilt and the original is renamed to `MEMORY.md.bak`. Re-running is a no-op once the bundle has concepts. **Rollback:** `JEO_MEMORY_LEGACY=1` ignores the bundle and reads `MEMORY.md`/`.bak` through the same injection-hardening (`JEO_NO_MEMORY=1` still wins over everything).
153
+ ## Works beside your existing agent or bot
154
+
155
+ | Tool or bot | Recommended jeo command | Boundary |
156
+ | ----------- | ----------------------- | -------- |
157
+ | Codex CLI | `jeo --tmux` or `jeo` | Runs in a sibling tmux session or inline. |
158
+ | Claude Code | `jeo --tmux` or `jeo` | jeo does not become a Claude Code extension. |
159
+ | OpenCode | `jeo` or `jeo --tmux` | External-runner workflow only. |
160
+ | Claw Code | `jeo --tmux` | jeo does not install into or replace Claw Code. |
161
+ | External controller / bot | `jeo` (or custom integrations) | External controllers drive jeo through standard terminal/CLI interfaces. |
162
+
163
+ Add `-q`/`--quiet` (or `JEO_QUIET=1`) to suppress startup banners, welcome animation, release notes, and resume hints so jeo can run cleanly beside another agent or be driven by a bot. `-p`/`--print` implies quiet.
164
+
153
165
 
154
166
  ## Local models
155
167
 
@@ -195,11 +207,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
195
207
  ## Changelog
196
208
 
197
209
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
210
+ - **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
198
211
  - **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
199
212
  - **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
200
213
  - **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
201
214
  - **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
202
- - **[0.7.5]** (2026-06-23) — Startup & loop latency: redundant re-reads that fired on every subagent spawn and every loop step are now memoized behind cheap `stat`-signature caches, so cold paths stay correct (edits/deletes still picked up immediately) while warm paths skip the disk and the re-parse. Targets the per-turn and per-spawn overhead that dominates perceived slowness in team/ralph/autopilot fan-outs.
203
215
 
204
216
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
205
217
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -149,6 +149,18 @@ jeo ultragoal
149
149
  `jeo` 在 `.jeo/memory/` 下保存 **本地优先、蒸馏后的项目内存**(无远程后端,零原生依赖)。过往会话被蒸馏为 [OKF](docs/okf_mem/) 概念包,下一次会话仅把相关的、受预算约束的切片重新注入系统提示 —— 作为 DATA 而非指令加固。用 `JEO_NO_MEMORY=1` 完全禁用。
150
150
 
151
151
  **迁移(`jeo memory-migrate`,一次性 · 幂等).** 把旧版单文档 `MEMORY.md` 无损转换为概念包: `## 标题 → 类型`,每个项目符号 → 一个类型化概念,缩进行 → 正文; 重建 `index.md`/`log.md`,并把原文件重命名为 `MEMORY.md.bak`。一旦概念包中已有概念,再次运行即为 no-op。**回滚:** `JEO_MEMORY_LEGACY=1` 忽略概念包,通过相同的注入加固读取 `MEMORY.md`/`.bak`(`JEO_NO_MEMORY=1` 仍优先于一切)。
152
+ ## 与您现有的代理或机器人协同工作 (Works beside your existing agent or bot)
153
+
154
+ | 工具或机器人 | 推荐的 jeo 命令 | 边界 |
155
+ | ----------- | ----------------------- | -------- |
156
+ | Codex CLI | `jeo --tmux` 或 `jeo` | 在同级 tmux 会话中运行或内联运行。 |
157
+ | Claude Code | `jeo --tmux` 或 `jeo` | jeo 不会成为 Claude Code 的扩展。 |
158
+ | OpenCode | `jeo` 或 `jeo --tmux` | 仅限外部运行器工作流。 |
159
+ | Claw Code | `jeo --tmux` | jeo 不会安装到或替换 Claw Code。 |
160
+ | 外部控制器 / 机器人 | `jeo` (或自定义集成) | 外部控制器通过标准终端/CLI 接口驱动 jeo。 |
161
+
162
+ 添加 `-q`/`--quiet`(或 `JEO_QUIET=1`)可抑制启动横幅、欢迎动画、发布说明和恢复提示,使 jeo 能够与其他代理并行运行或由机器人驱动。`-p`/`--print` 隐含 quiet。
163
+
152
164
 
153
165
  ## 本地模型
154
166
 
@@ -193,11 +205,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
193
205
  ## 更新日志 (Changelog)
194
206
 
195
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
196
209
  - **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
197
210
  - **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
198
211
  - **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
199
212
  - **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
200
- - **[0.7.5]** (2026-06-23) — Startup & loop latency: redundant re-reads that fired on every subagent spawn and every loop step are now memoized behind cheap `stat`-signature caches, so cold paths stay correct (edits/deletes still picked up immediately) while warm paths skip the disk and the re-parse. Targets the per-turn and per-spawn overhead that dominates perceived slowness in team/ralph/autopilot fan-outs.
201
213
 
202
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
215
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.12",
3
+ "version": "0.7.14",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -272,6 +272,11 @@ export interface AgentLoopResult {
272
272
  doneReason?: string;
273
273
  /** Summed provider token usage across the turn's steps, when reported. */
274
274
  usage?: { inputTokens: number; outputTokens: number };
275
+ /** Repo-relative paths of files this turn SUCCESSFULLY wrote/edited, sorted and
276
+ * de-duplicated. Lets a caller (task/team subagent, external orchestrator) collect
277
+ * the run's code artifacts instead of inferring an empty change set after real edits.
278
+ * Omitted when the turn mutated no file. */
279
+ mutatedFiles?: string[];
275
280
  }
276
281
 
277
282
 
@@ -364,7 +369,14 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
364
369
  let step = 1;
365
370
  const acc = { inputTokens: 0, outputTokens: 0 };
366
371
  let sawUsage = false;
367
- const finish = (r: AgentLoopResult): AgentLoopResult => (sawUsage ? { ...r, usage: { ...acc } } : r);
372
+ // Repo-relative paths of files this turn successfully wrote/edited. Surfaced on the
373
+ // result so callers can collect the run's code artifacts rather than report an empty
374
+ // change set after real edits (the "finalArtifacts is empty" orchestrator failure).
375
+ const mutatedFiles = new Set<string>();
376
+ const finish = (r: AgentLoopResult): AgentLoopResult => {
377
+ const withArtifacts = mutatedFiles.size ? { ...r, mutatedFiles: [...mutatedFiles].sort() } : r;
378
+ return sawUsage ? { ...withArtifacts, usage: { ...acc } } : withArtifacts;
379
+ };
368
380
  // Salvage a spin-stop into a useful answer (C): instead of returning a bare
369
381
  // "Stopped: …" — throwing away everything found this turn — do ONE final no-tools
370
382
  // call asking the model to answer with what it already has. Mirrors the
@@ -1042,7 +1054,15 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1042
1054
  for (let i = 0; i < toolCalls.length; i++) {
1043
1055
  if (!results[i].executed || !results[i].success) continue;
1044
1056
  const t = toolCalls[i].tool;
1045
- if (t === "write" || t === "edit") sawMutation = true;
1057
+ if (t === "write" || t === "edit") {
1058
+ sawMutation = true;
1059
+ // Record the actual file so the run's artifacts can be collected/returned.
1060
+ const rawPath = toolCalls[i].arguments?.filePath ?? toolCalls[i].arguments?.path;
1061
+ if (typeof rawPath === "string" && rawPath.trim()) {
1062
+ const rel = path.relative(cwd, path.resolve(cwd, rawPath)) || rawPath;
1063
+ mutatedFiles.add(rel.split(path.sep).join("/"));
1064
+ }
1065
+ }
1046
1066
  else if (t === "bash") {
1047
1067
  const cmd = String(toolCalls[i].arguments?.command ?? "");
1048
1068
  if (isVerificationSignal(cmd, results[i].output)) sawVerification = true;
@@ -207,12 +207,12 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
207
207
  const trace: string[] = [];
208
208
  let lastTarget = "";
209
209
  let currentStep = 0;
210
- // Round-8 (architect ref 7-Round7Workflow): count the subagent's SUCCESSFUL
211
- // calls so the parent can audit a "Changed Files:" claim against observed
212
- // reality. File-writing tools (write/edit/mkdir/delete) are tracked apart from
213
- // bash: read-only bash (e.g. `bun test`) MUST NOT count as edit evidence, but
214
- // bash CAN mutate, so the audit message distinguishes the two cases.
215
- let fileMutations = 0;
210
+ // Round-8 (architect ref 7-Round7Workflow): track whether the subagent ran any
211
+ // bash so the parent can audit a "Changed Files:" claim against observed reality.
212
+ // The actual set of written/edited files comes from the engine result
213
+ // (result.mutatedFiles); bash is tracked apart because read-only bash (e.g.
214
+ // `bun test`) is not edit evidence, yet bash CAN mutate — so the audit can tell
215
+ // "no action at all" from "only bash ran".
216
216
  let bashRuns = 0;
217
217
  emit({ role: role.id, kind: "start", detail: taskText, maxSteps, model });
218
218
  const result = await runAgentLoop(history, {
@@ -236,10 +236,7 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
236
236
  }
237
237
  },
238
238
  onToolResult: (tool, success, output) => {
239
- if (success) {
240
- if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
241
- else if (tool === "bash") bashRuns++;
242
- }
239
+ if (success && tool === "bash") bashRuns++;
243
240
  const label = lastTarget || tool;
244
241
  const summary = firstUsefulLine(output);
245
242
  const suffix = summary ? ` — ${summary}` : "";
@@ -263,16 +260,24 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
263
260
  const tokNote = result.usage ? `, ${result.usage.inputTokens + result.usage.outputTokens} tok` : "";
264
261
  const header = `[${role.title} subagent] ${complete ? "completed" : "stopped"} in ${result.steps} step(s) on ${model}${tokNote}.`;
265
262
  const body = trace.length ? `\nSteps:\n${trace.join("\n")}` : "";
263
+ // Collect/return the run's code artifacts: the engine reports every file the
264
+ // subagent actually wrote/edited, so the parent (and any orchestrator above it)
265
+ // gets a CONCRETE change set instead of an unverifiable "Changed Files:" prose
266
+ // claim. Empty list ⇒ no observed file mutation.
267
+ const changedFiles = result.mutatedFiles ?? [];
268
+ const artifactNote = changedFiles.length
269
+ ? `\nChanged files (${changedFiles.length}): ${changedFiles.join(", ")}`
270
+ : "";
266
271
  // Parent-side audit: a mutating role that "completed" without a successful file
267
- // mutation (write/edit/mkdir/delete) likely changed nothing — flag the claim.
268
- // bash is tracked separately: it CAN mutate, so an only-bash run downgrades to
269
- // "verify independently" instead of the stronger UNVERIFIED.
270
- const audit = complete && !role.readOnly && fileMutations === 0
272
+ // mutation (write/edit) likely changed nothing — flag the claim. bash is tracked
273
+ // separately: it CAN mutate, so an only-bash run downgrades to "verify
274
+ // independently" instead of the stronger UNVERIFIED.
275
+ const audit = complete && !role.readOnly && changedFiles.length === 0
271
276
  ? bashRuns === 0
272
277
  ? `\n[parent audit] No successful write/edit/bash was observed in this run — treat any "Changed Files:" claims above as UNVERIFIED.`
273
278
  : `\n[parent audit] No successful write/edit was observed (only bash ran); bash may or may not have mutated files — verify any "Changed Files:" claims above independently.`
274
279
  : "";
275
- return { success: complete, output: `${header}${body}\n\nResult:\n${fenceSubagentReport(detail)}${audit}` };
280
+ return { success: complete, output: `${header}${body}${artifactNote}\n\nResult:\n${fenceSubagentReport(detail)}${audit}` };
276
281
  };
277
282
 
278
283
  return async (args: Record<string, any>, cwd: string): Promise<ToolResult> => {
@@ -135,7 +135,7 @@ function anthropicThinkingMode(model: string): AnthropicThinkingMode {
135
135
  const v = parseAnthropicVersion(model);
136
136
  if (!v) return "budget";
137
137
  if (v.major > 4 || (v.major === 4 && v.minor >= 6)) return "adaptive";
138
- if (v.major === 4 && v.minor === 5) return "budget-effort";
138
+
139
139
  return "budget";
140
140
  }
141
141
 
@@ -266,7 +266,7 @@ export function anthropicPayload(
266
266
  // Budget-based extended thinking. `display: "summarized"` keeps human-readable thought
267
267
  // streaming. The 4.5 (budget-effort) transport also carries an output_config effort.
268
268
  payload.thinking = { type: "enabled", budget_tokens: thinkingBudget, display: "summarized" };
269
- if (thinkingMode === "budget-effort") payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
269
+
270
270
  }
271
271
  } else if (includeTemperature && options.temperature !== undefined) {
272
272
  payload.temperature = options.temperature;
@@ -281,6 +281,7 @@ export function anthropicPayload(
281
281
  if (stream) payload.stream = true;
282
282
  const system = anthropicSystemBlocks(systemPrompt, model, credential, payload);
283
283
  if (system) payload.system = system;
284
+ console.log("PAYLOAD:", JSON.stringify(payload));
284
285
  return JSON.stringify(payload);
285
286
  }
286
287
 
package/src/cli/runner.ts CHANGED
@@ -299,6 +299,7 @@ export function renderHelp(ctx: DispatchContext): string {
299
299
  lines.push("Options:");
300
300
  lines.push(" -v, --version Show version.");
301
301
  lines.push(" -h, --help Show help.");
302
+ lines.push(" -q, --quiet Suppress startup banners and logs.");
302
303
  lines.push(" --model <id> Use a session model for launch/one-shot.");
303
304
  lines.push(" --provider <name> Start launch on a provider default (anthropic/openai/gemini/antigravity/ollama).");
304
305
  lines.push(" --smol|--slow|--plan Start launch with the configured model role tier.");
@@ -320,7 +321,7 @@ export function renderCommandHelp(spec: CommandSpec, ctx: DispatchContext): stri
320
321
  const VALUE_FLAGS = new Set(["--worktree", "--model", "--provider", "--thinking", "--max-steps", "--append-system-prompt", "--skills", "--tools", "--system-prompt"]);
321
322
  const OPTIONAL_UUID_FLAGS = new Set(["--resume", "--continue", "-c"]);
322
323
  const VALUE_PREFIXES = ["--worktree=", "--model=", "--provider=", "--thinking=", "--max-steps=", "--append-system-prompt=", "--skills=", "--tools=", "--system-prompt="];
323
- const LAUNCH_ONLY_FLAGS = new Set(["--tmux", "--no-tui", "--no-session", "--list", "--smol", "--slow", "--plan", "-p", "--print", "--no-skills", "--no-tools"]);
324
+ const LAUNCH_ONLY_FLAGS = new Set(["--tmux", "--no-tui", "--no-session", "--list", "--smol", "--slow", "--plan", "-p", "--print", "-q", "--quiet", "--no-skills", "--no-tools"]);
324
325
  const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
325
326
 
326
327
  function flagName(arg: string): string {
@@ -20,6 +20,7 @@ export interface LaunchFlags {
20
20
  thinking?: ThinkLevel;
21
21
  errors: string[];
22
22
  print?: boolean;
23
+ quiet: boolean;
23
24
  appendSystemPromptRaw?: string;
24
25
  appendSystemPrompt?: string;
25
26
  noSkills: boolean;
@@ -63,7 +64,7 @@ export function fastThinkingLevelForModel(modelId: string): ThinkLevel | undefin
63
64
  }
64
65
 
65
66
  export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchFlags {
66
- const flags: LaunchFlags = { list: false, resume: false, noSession: false, noTui: false, maxSteps: 0, message: "", tmux: false, errors: [], print: false, noSkills: false, noTools: false };
67
+ const flags: LaunchFlags = { list: false, resume: false, noSession: false, noTui: false, maxSteps: 0, message: "", tmux: false, errors: [], print: false, quiet: false, noSkills: false, noTools: false };
67
68
  const rest: string[] = [];
68
69
  const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
69
70
  for (let i = 0; i < args.length; i++) {
@@ -77,6 +78,8 @@ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchF
77
78
  } else if (a === "-p" || a === "--print") {
78
79
  flags.print = true;
79
80
  flags.noTui = true;
81
+ } else if (a === "-q" || a === "--quiet") {
82
+ flags.quiet = true;
80
83
  } else if (a === "--tmux") {
81
84
  flags.tmux = true;
82
85
  } else if (a === "--worktree") {
@@ -309,6 +309,7 @@ export function formatResumeHint(sessionId: string): string {
309
309
  export async function runLaunchCommand(args: string[]): Promise<void> {
310
310
  let cwd = process.cwd();
311
311
  const flags = parseFlags(args, cwd);
312
+ const isQuiet = flags.quiet || flags.print || jeoEnv("QUIET") === "1";
312
313
  if (flags.errors.length) {
313
314
  for (const err of flags.errors) {
314
315
  console.error(`error: ${err}`);
@@ -322,7 +323,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
322
323
  if (wt !== cwd) {
323
324
  process.chdir(wt);
324
325
  cwd = wt;
325
- if (jeoEnv("TMUX_LAUNCHED") !== "1") console.log(`Using worktree: ${wt}`);
326
+ if (jeoEnv("TMUX_LAUNCHED") !== "1" && !isQuiet) console.log(`Using worktree: ${wt}`);
326
327
  }
327
328
  }
328
329
  let branch: string | undefined;
@@ -656,7 +657,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
656
657
  if (flags.resume) {
657
658
  const id = flags.resumeId ?? (await latestSessionId(cwd));
658
659
  if (!id) {
659
- console.log("No session to resume. Starting a new one.");
660
+ if (!isQuiet) console.log("No session to resume. Starting a new one.");
660
661
  sessionId = (await createSession(cwd, undefined, sessionModel)).id;
661
662
  } else {
662
663
  try {
@@ -667,9 +668,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
667
668
  // pinned one (flags.model/role/provider → initialSessionModel wins).
668
669
  if (!initialSessionModel && header.model) sessionModel = header.model;
669
670
  const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
670
- console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
671
+ if (!isQuiet) console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
671
672
  } catch (err) {
672
- console.log(`Could not resume ${id}: ${(err as Error).message}. Starting fresh.`);
673
+ if (!isQuiet) console.log(`Could not resume ${id}: ${(err as Error).message}. Starting fresh.`);
673
674
  sessionId = (await createSession(cwd, undefined, sessionModel)).id;
674
675
  }
675
676
  }
@@ -1266,7 +1267,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1266
1267
  renderWelcome({ ...welcomeData, cols: terminalSize().cols });
1267
1268
  // gjc-style fresh-start clear so the banner opens atop a clean screen. TTY only,
1268
1269
  // never mid-turn (scrollback flood). ponytail: add an opt-out env if anyone misses their scrollback.
1269
- if (process.stdout.isTTY) process.stdout.write(clearScreen());
1270
+ if (process.stdout.isTTY && !isQuiet) process.stdout.write(clearScreen());
1270
1271
  // Launch sweep: the forge mark's gradient loops seamlessly (default 2 full
1271
1272
  // cycles, JEO_WELCOME_ANIM_CYCLES overrides), ending on the static banner.
1272
1273
  // Truecolor TTYs only; JEO_NO_WELCOME_ANIM=1 opts out.
@@ -1276,8 +1277,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1276
1277
  detectColorLevel(process.env, true) === ColorLevel.TrueColor &&
1277
1278
  jeoEnv("NO_WELCOME_ANIM") !== "1";
1278
1279
  const sweepCycles = Math.min(10, Math.max(1, Number(jeoEnv("WELCOME_ANIM_CYCLES")) || 2));
1279
- if (sweepable) await playWelcomeSweep(welcomeData, { cycles: sweepCycles });
1280
- else console.log(renderWelcome(welcomeData).join("\n"));
1280
+ if (!isQuiet) {
1281
+ if (sweepable) await playWelcomeSweep(welcomeData, { cycles: sweepCycles });
1282
+ else console.log(renderWelcome(welcomeData).join("\n"));
1283
+ }
1281
1284
 
1282
1285
  // Surface the "New version" banner reliably: render ONCE from the on-disk cache
1283
1286
  // instantly (no network wait, works offline — the common path after the first
@@ -1285,7 +1288,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1285
1288
  // bump still shows it this launch. Both must run BEFORE the prompt is armed.
1286
1289
  let updateBannerShown = false;
1287
1290
  const showUpdateBanner = (u: { current: string; latest: string; updateAvailable: boolean } | null): void => {
1288
- if (updateBannerShown || !u?.updateAvailable) return;
1291
+ if (updateBannerShown || !u?.updateAvailable || isQuiet) return;
1289
1292
  updateBannerShown = true;
1290
1293
  console.log(renderUpdateBox(u.current, u.latest).join("\n"));
1291
1294
  };
@@ -1294,15 +1297,17 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1294
1297
  // First launch after a version bump: surface the bundled release notes ONCE
1295
1298
  // (offline, from the new package's CHANGELOG.md) and record the seen version
1296
1299
  // so it never repeats. Screen-safe: prints BEFORE the prompt is armed.
1297
- try {
1298
- const whatsNew = await consumeLaunchWhatsNew({
1299
- cols: Math.min(100, Math.max(40, (process.stdout.columns ?? 80) - 2)),
1300
- unicode: supportsUnicode(),
1301
- color: welcomeTheme.color,
1302
- });
1303
- if (whatsNew && whatsNew.length) console.log(whatsNew.join("\n"));
1304
- } catch { /* release notes are a courtesy; never block launch */ }
1305
- if (!LaunchTui.usable(flags.noTui)) console.log("(plain output)");
1300
+ if (!isQuiet) {
1301
+ try {
1302
+ const whatsNew = await consumeLaunchWhatsNew({
1303
+ cols: Math.min(100, Math.max(40, (process.stdout.columns ?? 80) - 2)),
1304
+ unicode: supportsUnicode(),
1305
+ color: welcomeTheme.color,
1306
+ });
1307
+ if (whatsNew && whatsNew.length) console.log(whatsNew.join("\n"));
1308
+ } catch { /* release notes are a courtesy; never block launch */ }
1309
+ }
1310
+ if (!LaunchTui.usable(flags.noTui) && !isQuiet) console.log("(plain output)");
1306
1311
 
1307
1312
  const useTui = LaunchTui.usable(flags.noTui);
1308
1313
  const runSkillInvocation = async (skill: SkillDoc, intent: string, invokedAs?: string): Promise<void> => {
@@ -4425,11 +4430,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4425
4430
  // of blocking up to 20s on a final LLM call. The child writes MEMORY.md atomically.
4426
4431
  if (sessionUsage.turns > 0) {
4427
4432
  const spawned = await spawnDetachedDistill(history, cwd, sessionModel || defaultModel);
4428
- if (spawned) console.log(chalk.gray("(session memory distilling in background)"));
4433
+ if (spawned && !isQuiet) console.log(chalk.gray("(session memory distilling in background)"));
4429
4434
  }
4430
4435
  // gjc-parity resume pointer (logs/gjc-tui-study analysis Gap C): leave the exact
4431
4436
  // resume command in scrollback on exit, mirroring the --list handler's convention.
4432
- if (sessionId && !flags.noSession) console.log(formatResumeHint(sessionId));
4437
+ if (sessionId && !flags.noSession && !isQuiet) console.log(formatResumeHint(sessionId));
4433
4438
  process.removeListener("SIGINT", handleCtrlC);
4434
4439
  process.stdin.off("data", handleCtrlCByte);
4435
4440
  drainPromptListeners();
@@ -431,8 +431,8 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
431
431
  // LLM summary failure does not halt team
432
432
  }
433
433
 
434
- let fileMutations = 0; // round-8 parent audit: successful write/edit/mkdir/delete
435
434
  let bashRuns = 0; // bash counted apart so read-only bash isn't edit evidence
435
+ // (the actual write/edit set comes from result.mutatedFiles)
436
436
  const result = await runAgentLoop(history, {
437
437
  cwd: ctx.cwd,
438
438
  model,
@@ -458,10 +458,7 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
458
458
  }
459
459
  },
460
460
  onToolResult: (tool, ok) => {
461
- if (ok) {
462
- if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
463
- else if (tool === "bash") bashRuns++;
464
- }
461
+ if (ok && tool === "bash") bashRuns++;
465
462
  log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
466
463
  },
467
464
  onNotice: msg => log(formatRalphStreamEvent("step", msg, renderOpts)),
@@ -486,7 +483,15 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
486
483
  return false;
487
484
  }
488
485
 
489
- if (!role.readOnly && fileMutations === 0) {
486
+ // Collect/return the run's code artifacts: the engine reports every file the
487
+ // subagent actually wrote/edited, so the task's change set is concrete instead of
488
+ // an unverifiable prose claim. An empty list ⇒ no observed file mutation.
489
+ const changedFiles = result.mutatedFiles ?? [];
490
+ if (changedFiles.length > 0) {
491
+ log(formatRalphStreamEvent("complete", `${role.title} changed ${changedFiles.length} file(s): ${changedFiles.join(", ")}`, renderOpts));
492
+ }
493
+
494
+ if (!role.readOnly && changedFiles.length === 0) {
490
495
  // Round-8: a mutating role finished without a successful file mutation — the
491
496
  // task may be legitimately read-only, but its "Changed Files:" claim is
492
497
  // unverified. bash is tracked apart: an only-bash run MIGHT have mutated.
@@ -108,19 +108,45 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
108
108
  continue;
109
109
  }
110
110
  // assistant: a JSON tool call (compact ledger lines) or a prose/done reply.
111
- // A tool-call message IS a JSON object (optionally inside a ```json fence) — so
112
- // only parse when the content actually begins with `{` after stripping a leading
113
- // fence. This renders fenced/decorated tool calls as cards (the "/resume shows
114
- // raw JSON and breaks the TUI" bug — naive JSON.parse failed on any fence and
115
- // dumped the block) while prose that merely CONTAINS tool-like JSON stays prose.
116
- const stripped = m.content.trim().replace(/^```(?:json)?[ \t]*\r?\n?/i, "").trimStart();
117
- const looksLikeCall = stripped.startsWith("{");
118
- const parsed = looksLikeCall
119
- ? tryExtractJsonObject<{ tool?: unknown; tools?: unknown; arguments?: unknown }>(
120
- m.content,
121
- { preferKeys: ["tool", "tools"] },
122
- )
123
- : null;
111
+ // A tool-call message is a JSON object that may be preceded by a ```json fence
112
+ // AND/OR by model "think-aloud" prose — gpt/qwen-style replies narrate a sentence
113
+ // and then append the JSON tool call in the SAME message. Extract the embedded
114
+ // call from anywhere in the content (mirroring the engine's own extractJsonObject)
115
+ // instead of gating on a leading `{`: that gate classified every prose-prefixed
116
+ // call as plain prose and dumped the raw object — including escaped multi-line
117
+ // edit/write payloads — verbatim into scrollback (the "/resume reads a wall of
118
+ // broken escaped data" bug). The narration is kept (dimmed); the raw JSON never is.
119
+ const parsed = tryExtractJsonObject<{ tool?: unknown; tools?: unknown; arguments?: unknown }>(
120
+ m.content,
121
+ { preferKeys: ["tool", "tools"] },
122
+ );
123
+ const isCall = !!parsed && (typeof parsed.tool === "string" || Array.isArray(parsed.tools));
124
+ // The protocol allows a leading `"reasoning"` field on a tool call; surface that
125
+ // narration (clipped) instead of losing it, and never re-dump it as raw JSON.
126
+ const reasoningText = parsed && typeof (parsed as { reasoning?: unknown }).reasoning === "string"
127
+ ? String((parsed as { reasoning?: unknown }).reasoning).trim()
128
+ : "";
129
+ // `looksLikeToolAttempt` flags a (possibly malformed/truncated) tool call so its
130
+ // raw JSON is suppressed even when parsing fails — e.g. a bounced reply the model
131
+ // later resent. Prose narrating a call is the text BEFORE the first `{` (the start
132
+ // of the parsed object): a prose-prefixed call keeps its lead-in; a JSON-first or
133
+ // reasoning-first call has none (its narration comes from `reasoningText`).
134
+ const looksLikeToolAttempt = /\{\s*"tools?"\s*:/.test(m.content);
135
+ const firstBrace = m.content.indexOf("{");
136
+ const prosePrefix = firstBrace > 0
137
+ ? m.content.slice(0, firstBrace).replace(/```(?:json)?[ \t]*\r?\n?/i, "").trim()
138
+ : "";
139
+
140
+ // A reply whose content (after an optional ```json fence) STARTS with `{` is a JSON
141
+ // emission, not prose — even a giant malformed reasoning blob the model couldn't close.
142
+ const startsWithJson = m.content.trimStart().replace(/^```(?:json)?[ \t]*\r?\n?/i, "").startsWith("{");
143
+ // Any JSON object/tool-call emission — fenced, prose-prefixed, reasoning-prefixed,
144
+ // or one we could not parse into a renderable call — must never reach scrollback as
145
+ // raw escaped JSON (the "/resume reads a wall of broken data" report).
146
+ const isJsonEmission = looksLikeToolAttempt || startsWithJson;
147
+ const narration = prosePrefix || reasoningText;
148
+
149
+
124
150
  const calls: { tool: string; arguments?: unknown }[] =
125
151
  parsed && typeof parsed.tool === "string"
126
152
  ? [{ tool: parsed.tool, arguments: parsed.arguments }]
@@ -130,13 +156,27 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
130
156
  .map(c => ({ tool: c.tool as string, arguments: c.arguments }))
131
157
  : [];
132
158
  const toolCalls = calls.filter(c => c.tool !== "done");
133
- if (toolCalls.length > 0) {
159
+ // A genuine tool-call turn is FOLLOWED by its `Tool [x] result` user message and the
160
+ // JSON is the final token; an illustrative JSON snippet quoted inside a prose reply is
161
+ // not, and it carries meaningful prose AFTER the object. Use both signals so a real
162
+ // prose-prefixed call renders as a ledger line while a sentence that merely shows
163
+ // `{"tool":...}` as an example stays prose.
164
+ const next = messages[i + 1];
165
+ const hasResult = next?.role === "user" && TOOL_RESULT_RE.test(next.content);
166
+ const lastBrace = m.content.lastIndexOf("}");
167
+ const trailing = lastBrace >= 0 ? m.content.slice(lastBrace + 1).trim() : "";
168
+ // A reply that STARTS with `{` is never illustrative prose — it is a (possibly
169
+ // malformed) JSON emission whose raw body must stay out of scrollback.
170
+ const looksIllustrative = !startsWithJson && !hasResult && trailing.length > 12 && /[A-Za-z]/.test(trailing);
171
+
172
+ if (toolCalls.length > 0 && !looksIllustrative) {
134
173
  if (lines.length > 0 && lines[lines.length - 1] !== "") {
135
174
  lines.push("");
136
175
  }
137
- // The matching `Tool [x] result (ok|fail)` user message follows; for a batch it
138
- // is ONE message with several blocks. Parse verdicts in call order.
139
- const next = messages[i + 1];
176
+ // Pre-call narration (dimmed) keeps the "why" of each step without the raw JSON.
177
+ if (narration) for (const l of clipBody(narration, bodyCap)) lines.push(dim(l));
178
+ // For a batch the result is ONE user message with several blocks; parse verdicts
179
+ // in call order.
140
180
  const verdicts = next?.role === "user" ? parseToolVerdicts(next.content) : [];
141
181
  toolCalls.forEach((c, ci) => {
142
182
  const v = verdicts[ci] ?? verdicts.find(x => x.tool === c.tool);
@@ -147,14 +187,22 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
147
187
  });
148
188
  continue;
149
189
  }
150
- // No tool calls → a lone `{tool:"done", reason}` (show its reason), a JSON object
151
- // that isn't a renderable call (skip — never dump raw JSON), or genuine prose.
152
- const reason =
153
- parsed && parsed.tool === "done"
154
- ? String((parsed.arguments as { reason?: unknown } | undefined)?.reason ?? "")
155
- : looksLikeCall
156
- ? ""
157
- : m.content;
190
+ // No rendered tool call → a lone `done` call (reason + narration), a malformed/raw
191
+ // JSON or tool-call emission (show narration only — never dump the raw object), or
192
+ // genuine prose (which may merely quote a JSON snippet).
193
+ const doneCall = calls.find(c => c.tool === "done");
194
+ const doneReason = doneCall
195
+ ? String((doneCall.arguments as { reason?: unknown } | undefined)?.reason ?? "")
196
+ : "";
197
+ const reason = doneCall
198
+ ? [narration, doneReason].filter(s => s.trim()).join("\n\n")
199
+ : isJsonEmission && !looksIllustrative
200
+ ? narration // JSON/tool-call emission we couldn't render — keep narration, drop raw JSON
201
+ : m.content;
202
+
203
+
204
+
205
+
158
206
  if (!reason.trim()) continue;
159
207
  // Persisted thinking (gjc "think → answer" order): show the turn's reasoning,
160
208
  // dimmed, above the reply so the durable record carries it across /resume + export.