jeo-code 0.7.13 → 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 +11 -0
- package/README.ja.md +13 -1
- package/README.ko.md +13 -1
- package/README.md +13 -1
- package/README.zh.md +13 -1
- package/package.json +1 -1
- package/src/agent/engine.ts +22 -2
- package/src/agent/task-tool.ts +20 -15
- package/src/cli/runner.ts +2 -1
- package/src/commands/launch/flags.ts +4 -1
- package/src/commands/launch.ts +24 -19
- package/src/commands/team.ts +11 -6
- package/src/tui/components/transcript.ts +73 -25
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
package/src/agent/engine.ts
CHANGED
|
@@ -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
|
-
|
|
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")
|
|
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;
|
package/src/agent/task-tool.ts
CHANGED
|
@@ -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):
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
// bash
|
|
214
|
-
// bash CAN mutate
|
|
215
|
-
|
|
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
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
const audit = complete && !role.readOnly &&
|
|
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> => {
|
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") {
|
package/src/commands/launch.ts
CHANGED
|
@@ -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 (
|
|
1280
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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();
|
package/src/commands/team.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.
|