jeo-code 0.7.14 → 0.7.15

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
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
9
 
10
+ ## [0.7.15] - 2026-06-25
11
+ _Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9._
12
+
13
+ ### Changed
14
+ - **`src/` and `test/` reverted to the v0.7.9 tree.** Every runtime and test file is byte-identical to the `v0.7.9` tag; the post-0.7.9 test `test/mutated-files-artifacts.test.ts` is removed along with the feature it covered.
15
+
16
+ ### Notes
17
+ - Release tooling (`scripts/ci-release-build-binaries.ts`, keeping the `bun-darwin-x64-baseline` target for older-CPU compatibility) and documentation history (this CHANGELOG, READMEs) are intentionally **not** rolled back, since they are not part of the user-facing runtime and reverting them would regress binary compatibility or erase the record of what was shipped.
18
+
10
19
  ## [0.7.14] - 2026-06-25
11
20
  _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
21
 
package/README.ja.md CHANGED
@@ -205,11 +205,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
205
205
  ## 変更履歴 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
208
209
  - **[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.
209
210
  - **[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.
210
211
  - **[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.
211
212
  - **[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.
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.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -205,11 +205,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
205
205
  ## 변경 이력 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
208
209
  - **[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.
209
210
  - **[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.
210
211
  - **[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.
211
212
  - **[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.
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.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -207,11 +207,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
207
207
  ## Changelog
208
208
 
209
209
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
210
+ - **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
210
211
  - **[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.
211
212
  - **[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.
212
213
  - **[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.
213
214
  - **[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.
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.
215
215
 
216
216
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
217
217
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -205,11 +205,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
205
205
  ## 更新日志 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
208
209
  - **[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.
209
210
  - **[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.
210
211
  - **[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.
211
212
  - **[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.
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.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.14",
3
+ "version": "0.7.15",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -272,11 +272,6 @@ 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[];
280
275
  }
281
276
 
282
277
 
@@ -369,14 +364,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
369
364
  let step = 1;
370
365
  const acc = { inputTokens: 0, outputTokens: 0 };
371
366
  let sawUsage = false;
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
- };
367
+ const finish = (r: AgentLoopResult): AgentLoopResult => (sawUsage ? { ...r, usage: { ...acc } } : r);
380
368
  // Salvage a spin-stop into a useful answer (C): instead of returning a bare
381
369
  // "Stopped: …" — throwing away everything found this turn — do ONE final no-tools
382
370
  // call asking the model to answer with what it already has. Mirrors the
@@ -1054,15 +1042,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1054
1042
  for (let i = 0; i < toolCalls.length; i++) {
1055
1043
  if (!results[i].executed || !results[i].success) continue;
1056
1044
  const t = toolCalls[i].tool;
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
- }
1045
+ if (t === "write" || t === "edit") sawMutation = true;
1066
1046
  else if (t === "bash") {
1067
1047
  const cmd = String(toolCalls[i].arguments?.command ?? "");
1068
1048
  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): 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".
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;
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,7 +236,10 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
236
236
  }
237
237
  },
238
238
  onToolResult: (tool, success, output) => {
239
- if (success && tool === "bash") bashRuns++;
239
+ if (success) {
240
+ if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
241
+ else if (tool === "bash") bashRuns++;
242
+ }
240
243
  const label = lastTarget || tool;
241
244
  const summary = firstUsefulLine(output);
242
245
  const suffix = summary ? ` — ${summary}` : "";
@@ -260,24 +263,16 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
260
263
  const tokNote = result.usage ? `, ${result.usage.inputTokens + result.usage.outputTokens} tok` : "";
261
264
  const header = `[${role.title} subagent] ${complete ? "completed" : "stopped"} in ${result.steps} step(s) on ${model}${tokNote}.`;
262
265
  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
- : "";
271
266
  // Parent-side audit: a mutating role that "completed" without a successful file
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
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
276
271
  ? bashRuns === 0
277
272
  ? `\n[parent audit] No successful write/edit/bash was observed in this run — treat any "Changed Files:" claims above as UNVERIFIED.`
278
273
  : `\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.`
279
274
  : "";
280
- return { success: complete, output: `${header}${body}${artifactNote}\n\nResult:\n${fenceSubagentReport(detail)}${audit}` };
275
+ return { success: complete, output: `${header}${body}\n\nResult:\n${fenceSubagentReport(detail)}${audit}` };
281
276
  };
282
277
 
283
278
  return async (args: Record<string, any>, cwd: string): Promise<ToolResult> => {
@@ -83,7 +83,7 @@ export async function spillToolResult(tool: string, output: string, cwd: string)
83
83
  await fs.mkdir(dir, { recursive: true });
84
84
  const safeTool = tool.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32) || "tool";
85
85
  const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
86
- const rel = `.jeo/artifacts/tool-results/${stamp}-${safeTool}.txt`;
86
+ const rel = path.join(".jeo", "artifacts", "tool-results", `${stamp}-${safeTool}.txt`);
87
87
  await fs.writeFile(path.join(cwd, rel), output, "utf-8");
88
88
  // Retention so a long session can't grow the artifact dir without bound.
89
89
  await pruneToolArtifacts(dir);
@@ -533,8 +533,7 @@ export function streamIdleMs(env?: Record<string, string | undefined>): number {
533
533
  * built-in default. Mirrors streamIdleMs so a slow non-interactive turn (callLlm without
534
534
  * onToken) can relax the cap via JEO_CALL_TIMEOUT_MS without a code change. */
535
535
  export function callTimeoutMs(env?: Record<string, string | undefined>): number {
536
- const e = env ?? process.env;
537
- const raw = jeoEnv("CALL_TIMEOUT_MS", e) ?? e["ANTHROPIC_TIMEOUT"];
536
+ const raw = jeoEnv("CALL_TIMEOUT_MS", env);
538
537
  const n = raw !== undefined ? parseInt(raw, 10) : NaN;
539
538
  return Number.isFinite(n) && n > 0 ? n : DEFAULT_CALL_TIMEOUT_MS;
540
539
  }
@@ -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
-
138
+ if (v.major === 4 && v.minor === 5) return "budget-effort";
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
-
269
+ if (thinkingMode === "budget-effort") payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
270
270
  }
271
271
  } else if (includeTemperature && options.temperature !== undefined) {
272
272
  payload.temperature = options.temperature;
@@ -281,7 +281,6 @@ 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));
285
284
  return JSON.stringify(payload);
286
285
  }
287
286
 
package/src/cli/runner.ts CHANGED
@@ -299,7 +299,6 @@ 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.");
303
302
  lines.push(" --model <id> Use a session model for launch/one-shot.");
304
303
  lines.push(" --provider <name> Start launch on a provider default (anthropic/openai/gemini/antigravity/ollama).");
305
304
  lines.push(" --smol|--slow|--plan Start launch with the configured model role tier.");
@@ -321,7 +320,7 @@ export function renderCommandHelp(spec: CommandSpec, ctx: DispatchContext): stri
321
320
  const VALUE_FLAGS = new Set(["--worktree", "--model", "--provider", "--thinking", "--max-steps", "--append-system-prompt", "--skills", "--tools", "--system-prompt"]);
322
321
  const OPTIONAL_UUID_FLAGS = new Set(["--resume", "--continue", "-c"]);
323
322
  const VALUE_PREFIXES = ["--worktree=", "--model=", "--provider=", "--thinking=", "--max-steps=", "--append-system-prompt=", "--skills=", "--tools=", "--system-prompt="];
324
- const LAUNCH_ONLY_FLAGS = new Set(["--tmux", "--no-tui", "--no-session", "--list", "--smol", "--slow", "--plan", "-p", "--print", "-q", "--quiet", "--no-skills", "--no-tools"]);
323
+ const LAUNCH_ONLY_FLAGS = new Set(["--tmux", "--no-tui", "--no-session", "--list", "--smol", "--slow", "--plan", "-p", "--print", "--no-skills", "--no-tools"]);
325
324
  const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
326
325
 
327
326
  function flagName(arg: string): string {
@@ -20,7 +20,6 @@ export interface LaunchFlags {
20
20
  thinking?: ThinkLevel;
21
21
  errors: string[];
22
22
  print?: boolean;
23
- quiet: boolean;
24
23
  appendSystemPromptRaw?: string;
25
24
  appendSystemPrompt?: string;
26
25
  noSkills: boolean;
@@ -64,7 +63,7 @@ export function fastThinkingLevelForModel(modelId: string): ThinkLevel | undefin
64
63
  }
65
64
 
66
65
  export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchFlags {
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 };
66
+ const flags: LaunchFlags = { list: false, resume: false, noSession: false, noTui: false, maxSteps: 0, message: "", tmux: false, errors: [], print: false, noSkills: false, noTools: false };
68
67
  const rest: string[] = [];
69
68
  const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
70
69
  for (let i = 0; i < args.length; i++) {
@@ -78,8 +77,6 @@ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchF
78
77
  } else if (a === "-p" || a === "--print") {
79
78
  flags.print = true;
80
79
  flags.noTui = true;
81
- } else if (a === "-q" || a === "--quiet") {
82
- flags.quiet = true;
83
80
  } else if (a === "--tmux") {
84
81
  flags.tmux = true;
85
82
  } else if (a === "--worktree") {
@@ -309,7 +309,6 @@ 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";
313
312
  if (flags.errors.length) {
314
313
  for (const err of flags.errors) {
315
314
  console.error(`error: ${err}`);
@@ -323,7 +322,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
323
322
  if (wt !== cwd) {
324
323
  process.chdir(wt);
325
324
  cwd = wt;
326
- if (jeoEnv("TMUX_LAUNCHED") !== "1" && !isQuiet) console.log(`Using worktree: ${wt}`);
325
+ if (jeoEnv("TMUX_LAUNCHED") !== "1") console.log(`Using worktree: ${wt}`);
327
326
  }
328
327
  }
329
328
  let branch: string | undefined;
@@ -657,7 +656,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
657
656
  if (flags.resume) {
658
657
  const id = flags.resumeId ?? (await latestSessionId(cwd));
659
658
  if (!id) {
660
- if (!isQuiet) console.log("No session to resume. Starting a new one.");
659
+ console.log("No session to resume. Starting a new one.");
661
660
  sessionId = (await createSession(cwd, undefined, sessionModel)).id;
662
661
  } else {
663
662
  try {
@@ -668,9 +667,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
668
667
  // pinned one (flags.model/role/provider → initialSessionModel wins).
669
668
  if (!initialSessionModel && header.model) sessionModel = header.model;
670
669
  const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
671
- if (!isQuiet) console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
670
+ console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
672
671
  } catch (err) {
673
- if (!isQuiet) console.log(`Could not resume ${id}: ${(err as Error).message}. Starting fresh.`);
672
+ console.log(`Could not resume ${id}: ${(err as Error).message}. Starting fresh.`);
674
673
  sessionId = (await createSession(cwd, undefined, sessionModel)).id;
675
674
  }
676
675
  }
@@ -1267,7 +1266,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1267
1266
  renderWelcome({ ...welcomeData, cols: terminalSize().cols });
1268
1267
  // gjc-style fresh-start clear so the banner opens atop a clean screen. TTY only,
1269
1268
  // never mid-turn (scrollback flood). ponytail: add an opt-out env if anyone misses their scrollback.
1270
- if (process.stdout.isTTY && !isQuiet) process.stdout.write(clearScreen());
1269
+ if (process.stdout.isTTY) process.stdout.write(clearScreen());
1271
1270
  // Launch sweep: the forge mark's gradient loops seamlessly (default 2 full
1272
1271
  // cycles, JEO_WELCOME_ANIM_CYCLES overrides), ending on the static banner.
1273
1272
  // Truecolor TTYs only; JEO_NO_WELCOME_ANIM=1 opts out.
@@ -1277,10 +1276,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1277
1276
  detectColorLevel(process.env, true) === ColorLevel.TrueColor &&
1278
1277
  jeoEnv("NO_WELCOME_ANIM") !== "1";
1279
1278
  const sweepCycles = Math.min(10, Math.max(1, Number(jeoEnv("WELCOME_ANIM_CYCLES")) || 2));
1280
- if (!isQuiet) {
1281
- if (sweepable) await playWelcomeSweep(welcomeData, { cycles: sweepCycles });
1282
- else console.log(renderWelcome(welcomeData).join("\n"));
1283
- }
1279
+ if (sweepable) await playWelcomeSweep(welcomeData, { cycles: sweepCycles });
1280
+ else console.log(renderWelcome(welcomeData).join("\n"));
1284
1281
 
1285
1282
  // Surface the "New version" banner reliably: render ONCE from the on-disk cache
1286
1283
  // instantly (no network wait, works offline — the common path after the first
@@ -1288,7 +1285,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1288
1285
  // bump still shows it this launch. Both must run BEFORE the prompt is armed.
1289
1286
  let updateBannerShown = false;
1290
1287
  const showUpdateBanner = (u: { current: string; latest: string; updateAvailable: boolean } | null): void => {
1291
- if (updateBannerShown || !u?.updateAvailable || isQuiet) return;
1288
+ if (updateBannerShown || !u?.updateAvailable) return;
1292
1289
  updateBannerShown = true;
1293
1290
  console.log(renderUpdateBox(u.current, u.latest).join("\n"));
1294
1291
  };
@@ -1297,17 +1294,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1297
1294
  // First launch after a version bump: surface the bundled release notes ONCE
1298
1295
  // (offline, from the new package's CHANGELOG.md) and record the seen version
1299
1296
  // so it never repeats. Screen-safe: prints BEFORE the prompt is armed.
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)");
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)");
1311
1306
 
1312
1307
  const useTui = LaunchTui.usable(flags.noTui);
1313
1308
  const runSkillInvocation = async (skill: SkillDoc, intent: string, invokedAs?: string): Promise<void> => {
@@ -4430,11 +4425,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4430
4425
  // of blocking up to 20s on a final LLM call. The child writes MEMORY.md atomically.
4431
4426
  if (sessionUsage.turns > 0) {
4432
4427
  const spawned = await spawnDetachedDistill(history, cwd, sessionModel || defaultModel);
4433
- if (spawned && !isQuiet) console.log(chalk.gray("(session memory distilling in background)"));
4428
+ if (spawned) console.log(chalk.gray("(session memory distilling in background)"));
4434
4429
  }
4435
4430
  // gjc-parity resume pointer (logs/gjc-tui-study analysis Gap C): leave the exact
4436
4431
  // resume command in scrollback on exit, mirroring the --list handler's convention.
4437
- if (sessionId && !flags.noSession && !isQuiet) console.log(formatResumeHint(sessionId));
4432
+ if (sessionId && !flags.noSession) console.log(formatResumeHint(sessionId));
4438
4433
  process.removeListener("SIGINT", handleCtrlC);
4439
4434
  process.stdin.off("data", handleCtrlCByte);
4440
4435
  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
434
435
  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,7 +458,10 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
458
458
  }
459
459
  },
460
460
  onToolResult: (tool, ok) => {
461
- if (ok && tool === "bash") bashRuns++;
461
+ if (ok) {
462
+ if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
463
+ else if (tool === "bash") bashRuns++;
464
+ }
462
465
  log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
463
466
  },
464
467
  onNotice: msg => log(formatRalphStreamEvent("step", msg, renderOpts)),
@@ -483,15 +486,7 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
483
486
  return false;
484
487
  }
485
488
 
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) {
489
+ if (!role.readOnly && fileMutations === 0) {
495
490
  // Round-8: a mutating role finished without a successful file mutation — the
496
491
  // task may be legitimately read-only, but its "Changed Files:" claim is
497
492
  // unverified. bash is tracked apart: an only-bash run MIGHT have mutated.
@@ -108,45 +108,19 @@ 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 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
-
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;
150
124
  const calls: { tool: string; arguments?: unknown }[] =
151
125
  parsed && typeof parsed.tool === "string"
152
126
  ? [{ tool: parsed.tool, arguments: parsed.arguments }]
@@ -156,27 +130,13 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
156
130
  .map(c => ({ tool: c.tool as string, arguments: c.arguments }))
157
131
  : [];
158
132
  const toolCalls = calls.filter(c => c.tool !== "done");
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) {
133
+ if (toolCalls.length > 0) {
173
134
  if (lines.length > 0 && lines[lines.length - 1] !== "") {
174
135
  lines.push("");
175
136
  }
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.
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];
180
140
  const verdicts = next?.role === "user" ? parseToolVerdicts(next.content) : [];
181
141
  toolCalls.forEach((c, ci) => {
182
142
  const v = verdicts[ci] ?? verdicts.find(x => x.tool === c.tool);
@@ -187,22 +147,14 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
187
147
  });
188
148
  continue;
189
149
  }
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
-
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;
206
158
  if (!reason.trim()) continue;
207
159
  // Persisted thinking (gjc "think → answer" order): show the turn's reasoning,
208
160
  // dimmed, above the reply so the durable record carries it across /resume + export.