jeo-code 0.8.40 → 0.8.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.8.42] - 2026-07-21
10
+ _Request (paraphrased): "adopt gjc's subagent execution model and TUI parallel-processing monitoring — all the async/parallel parts — into jeo-code, with real operational verification." Investigated live before assuming a gap: jeo already has a mature, gjc-parity concurrent fan-out worker pool (`MAX_FANOUT`-bounded `Promise.all`, broadcast steering hub, detached/subagent/job/irc tools, a live TUI panel for concurrent subagents). Booted real `jeo --tmux` sessions with real credentials and repeatedly drove a genuine 3-file concurrent executor fan-out to verify this stack end to end — not to re-implement what already works, but to find what's actually broken in it. Found and fixed three real, live-reproduced bugs across the fan-out call path, none of them "missing gjc parity" — all pre-existing defects in jeo's own already-built implementation._
11
+
12
+ ### Fixed
13
+ - **Anthropic rejects the `effort` parameter on a live-discovered model snapshot the version-based transport classifier did not expect to reject it** (`src/ai/providers/anthropic.ts`). Live-reproduced: a rate-limit fallback chain (subagent's own model rate-limited → "equivalent" fallback) landed on `claude-sonnet-4-5-20250929`, which classifies onto the `budget-effort` thinking transport by name/version (same as its `claude-sonnet-4-5` alias) but 400'd with "This model does not support the effort parameter" — the exact wording already known for Haiku 4.5, but on a model this codebase's classifier did not special-case. New `isEffortUnsupportedError` fail-safe (mirrors the existing deprecated-temperature/reasoning-artifact/fallback-unsupported retries in the same request loop) retries once with `output_config` dropped — `thinking`/`budget_tokens`, if any, survive; only the effort field itself is unsupported. Retry budget raised 4→5 attempts to fit the new flip alongside the three existing ones.
14
+ - **A `tasks` fan-out call sent as a JSON-STRINGIFIED array (correct content, wrong JSON type) was hard-rejected instead of recovered** (`src/agent/task-tool.ts`). Live-reproduced across two different models/providers (including a native-tool-calling-capable one): models repeatedly self-diagnosed "I'm passing tasks as a string instead of an actual array" turn after turn, never recovering, until the 5-consecutive-failure loop guard stopped them — a real, observed reliability gap in the flagship "fan out N concurrent subagents in one call" workflow. `createTaskTool`'s handler now transparently `JSON.parse`s a string `tasks` value when it parses to a real array, before falling through to the ordinary single-task path.
15
+ - **The `task`-tool-call preview box showed a bare "&lt;missing task&gt;" placeholder for EVERY fan-out/batch call**, well-formed or not (`src/tui/components/forge.ts`) — the preview only ever read a single `task`/`prompt`/`assignment` string, never the `tasks` array the concurrent path actually uses, so a user watching a real, successful 3-task fan-out saw zero useful preview of what was about to run. Now renders a numbered per-task summary (`1. …`, `2. …`, …) for both a real array and the JSON-stringified-array mistake above, title becomes `Task: <role> ×N`.
16
+ - **`taskToolProtocolLine`'s fan-out shape was compressed into an ambiguous one-line shorthand** (`{role, task|tasks[], context?}`) that plausibly contributed to the stringified-array mistake above — rewritten with two separate, concrete-example shapes (single vs fan-out) mirroring the `calc` tool's already-clear `{calculations:[{expression, prefix?, suffix?}]}` style, plus an explicit "MUST be a real JSON array... NEVER a JSON-stringified array, NEVER a single task string" warning.
17
+
18
+ ### Verified
19
+ - Live (unmocked), multiple real `jeo --tmux` sessions with real Anthropic/Antigravity OAuth credentials: reproduced the original `<missing task>` + repeated-failure bug end to end; after the fixes, the SAME fan-out request correctly dispatched 3 concurrent executor subagents with a live "parallel · 3 running" TUI panel showing real-time per-task spinners and labels — confirming jeo's existing concurrent-monitoring TUI panel itself works correctly once the dispatch path is fixed.
20
+ - `test/anthropic-fallback.test.ts` +1 (effort-unsupported retry, exactly 2 fetch calls, `output_config` dropped while `thinking.budget_tokens` survives); `test/task-tool.test.ts` +3 (JSON-stringified `tasks` coercion, protocol-line wording, not-JSON/non-array fallthrough); `test/forge-status.test.ts` +4 (single-task unchanged, real array, JSON-stringified array, malformed fallthrough) — 82 pass / 0 fail across the 5 targeted files.
21
+ - `bun run typecheck` clean.
22
+ - Full `bun test`: this host ran under sustained, unrelated multi-tenant load throughout this pass (load average 21-35, 29 concurrent users) — full-suite runs surfaced 7-10 timing-shaped failures per run in areas completely unrelated to this change (model-pin persistence, route-fallback timing, browser automation, LSP), none reproducing in isolation; every file actually touched this pass stayed green and fast in repeated isolated runs regardless of host load. Recorded honestly rather than claiming a clean full-suite number this host could not currently produce.
23
+
24
+ ## [0.8.41] - 2026-07-21
25
+ _Ponytail-mode review requested directly ("review ponytail") over the two files this session's most recent pass (v0.8.40) touched: `src/agent/notify/daemon-control.ts` and `src/commands/launch/route-slash.ts`. Self-review found no correctness bugs (the v0.8.40 logic held up), but did find real, concrete style-bloat and one genuine test-coverage gap — both fixed with evidence, per the established convention._
26
+
27
+ ### Fixed
28
+ - **Formatting bloat from the v0.8.40 edit passes** (`route-slash.ts`): two spots left double/triple blank lines between declarations (an artifact of iterative insert/delete edits) instead of the file's established single-blank-line convention; `daemon-control.ts` was missing the separating blank line between `isPidAlive` and the newly added `parseEtimeToMs`. Purely whitespace — zero behavior change, confirmed by an unchanged `bun test` pass count before/after.
29
+ - **`parseEtimeToMs` had zero direct unit tests** despite being a new, non-trivial 3-branch parser (`mm:ss` / `hh:mm:ss` / `dd-hh:mm:ss`) introduced in v0.8.40 — it was previously exercised only indirectly (through `isLockOwnerAlive`/`processStartTimeMs` against real short-lived test processes, which never reach the multi-day branch nor explicitly probe malformed input). Added direct tests for all three time formats plus empty/malformed/wrong-segment-count input, and direct tests for `processStartTimeMs` against both a dead pid (undefined) and a real live one (a plausible, non-garbage timestamp).
30
+
31
+ ### Verified
32
+ - `bun run typecheck` clean.
33
+ - Targeted files (`test/route-slash.test.ts`, `test/notify-daemon-control.test.ts`, `test/daemon-command.test.ts`) — 61 pass / 0 fail, stable across 3 repeated isolated runs.
34
+ - Full `bun test`: the host this session runs on was under extreme, unrelated multi-tenant load during this pass (load average 18-35, 29 concurrent users, 700+ processes) — a full-suite run surfaced ~39 timing-shaped failures (5s+ timeouts) spread across completely unrelated areas (OAuth, LSP rename, model routing/rate-limit fallback), none touching the files changed this pass; the three targeted files stayed green and fast (under 3s) in every isolated run regardless of host load. Recorded here rather than claiming an unreliable full-suite number.
35
+
9
36
  ## [0.8.40] - 2026-07-21
10
37
  _User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."_
11
38
 
package/README.ja.md CHANGED
@@ -283,11 +283,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
283
283
  ## 変更履歴 (Changelog)
284
284
 
285
285
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
286
+ - **[0.8.42]** (2026-07-21) — Request (paraphrased): "adopt gjc's subagent execution model and TUI parallel-processing monitoring — all the async/parallel parts — into jeo-code, with real operational verification." Investigated live before assuming a gap: jeo already has a mature, gjc-parity concurrent fan-out worker pool (`MAX_FANOUT`-bounded `Promise.all`, broadcast steering hub, detached/subagent/job/irc tools, a live TUI panel for concurrent subagents). Booted real `jeo --tmux` sessions with real credentials and repeatedly drove a genuine 3-file concurrent executor fan-out to verify this stack end to end — not to re-implement what already works, but to find what's actually broken in it. Found and fixed three real, live-reproduced bugs across the fan-out call path, none of them "missing gjc parity" — all pre-existing defects in jeo's own already-built implementation.
287
+ - **[0.8.41]** (2026-07-21) — Ponytail-mode review requested directly ("review ponytail") over the two files this session's most recent pass (v0.8.40) touched: `src/agent/notify/daemon-control.ts` and `src/commands/launch/route-slash.ts`. Self-review found no correctness bugs (the v0.8.40 logic held up), but did find real, concrete style-bloat and one genuine test-coverage gap — both fixed with evidence, per the established convention.
286
288
  - **[0.8.40]** (2026-07-21) — User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."
287
289
  - **[0.8.39]** (2026-07-21) — User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)
288
290
  - **[0.8.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
289
- - **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
290
- - **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
291
291
 
292
292
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
293
293
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -284,11 +284,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
284
284
  ## 변경 이력 (Changelog)
285
285
 
286
286
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
287
+ - **[0.8.42]** (2026-07-21) — Request (paraphrased): "adopt gjc's subagent execution model and TUI parallel-processing monitoring — all the async/parallel parts — into jeo-code, with real operational verification." Investigated live before assuming a gap: jeo already has a mature, gjc-parity concurrent fan-out worker pool (`MAX_FANOUT`-bounded `Promise.all`, broadcast steering hub, detached/subagent/job/irc tools, a live TUI panel for concurrent subagents). Booted real `jeo --tmux` sessions with real credentials and repeatedly drove a genuine 3-file concurrent executor fan-out to verify this stack end to end — not to re-implement what already works, but to find what's actually broken in it. Found and fixed three real, live-reproduced bugs across the fan-out call path, none of them "missing gjc parity" — all pre-existing defects in jeo's own already-built implementation.
288
+ - **[0.8.41]** (2026-07-21) — Ponytail-mode review requested directly ("review ponytail") over the two files this session's most recent pass (v0.8.40) touched: `src/agent/notify/daemon-control.ts` and `src/commands/launch/route-slash.ts`. Self-review found no correctness bugs (the v0.8.40 logic held up), but did find real, concrete style-bloat and one genuine test-coverage gap — both fixed with evidence, per the established convention.
287
289
  - **[0.8.40]** (2026-07-21) — User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."
288
290
  - **[0.8.39]** (2026-07-21) — User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)
289
291
  - **[0.8.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
290
- - **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
291
- - **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
292
292
 
293
293
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
294
294
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -291,11 +291,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
291
291
  ## Changelog
292
292
 
293
293
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
294
+ - **[0.8.42]** (2026-07-21) — Request (paraphrased): "adopt gjc's subagent execution model and TUI parallel-processing monitoring — all the async/parallel parts — into jeo-code, with real operational verification." Investigated live before assuming a gap: jeo already has a mature, gjc-parity concurrent fan-out worker pool (`MAX_FANOUT`-bounded `Promise.all`, broadcast steering hub, detached/subagent/job/irc tools, a live TUI panel for concurrent subagents). Booted real `jeo --tmux` sessions with real credentials and repeatedly drove a genuine 3-file concurrent executor fan-out to verify this stack end to end — not to re-implement what already works, but to find what's actually broken in it. Found and fixed three real, live-reproduced bugs across the fan-out call path, none of them "missing gjc parity" — all pre-existing defects in jeo's own already-built implementation.
295
+ - **[0.8.41]** (2026-07-21) — Ponytail-mode review requested directly ("review ponytail") over the two files this session's most recent pass (v0.8.40) touched: `src/agent/notify/daemon-control.ts` and `src/commands/launch/route-slash.ts`. Self-review found no correctness bugs (the v0.8.40 logic held up), but did find real, concrete style-bloat and one genuine test-coverage gap — both fixed with evidence, per the established convention.
294
296
  - **[0.8.40]** (2026-07-21) — User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."
295
297
  - **[0.8.39]** (2026-07-21) — User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)
296
298
  - **[0.8.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
297
- - **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
298
- - **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
299
299
 
300
300
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
301
301
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -283,11 +283,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
283
283
  ## 更新日志 (Changelog)
284
284
 
285
285
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
286
+ - **[0.8.42]** (2026-07-21) — Request (paraphrased): "adopt gjc's subagent execution model and TUI parallel-processing monitoring — all the async/parallel parts — into jeo-code, with real operational verification." Investigated live before assuming a gap: jeo already has a mature, gjc-parity concurrent fan-out worker pool (`MAX_FANOUT`-bounded `Promise.all`, broadcast steering hub, detached/subagent/job/irc tools, a live TUI panel for concurrent subagents). Booted real `jeo --tmux` sessions with real credentials and repeatedly drove a genuine 3-file concurrent executor fan-out to verify this stack end to end — not to re-implement what already works, but to find what's actually broken in it. Found and fixed three real, live-reproduced bugs across the fan-out call path, none of them "missing gjc parity" — all pre-existing defects in jeo's own already-built implementation.
287
+ - **[0.8.41]** (2026-07-21) — Ponytail-mode review requested directly ("review ponytail") over the two files this session's most recent pass (v0.8.40) touched: `src/agent/notify/daemon-control.ts` and `src/commands/launch/route-slash.ts`. Self-review found no correctness bugs (the v0.8.40 logic held up), but did find real, concrete style-bloat and one genuine test-coverage gap — both fixed with evidence, per the established convention.
286
288
  - **[0.8.40]** (2026-07-21) — User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."
287
289
  - **[0.8.39]** (2026-07-21) — User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)
288
290
  - **[0.8.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
289
- - **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
290
- - **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
291
291
 
292
292
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
293
293
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.8.40",
3
+ "version": "0.8.42",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -25,6 +25,7 @@ export function isPidAlive(pid: number): boolean {
25
25
  return false;
26
26
  }
27
27
  }
28
+
28
29
  export function parseEtimeToMs(raw: string): number | undefined {
29
30
  const trimmed = raw.trim();
30
31
  if (!trimmed) return undefined;
@@ -167,12 +167,25 @@ function createSteerHub(drain?: () => string[]) {
167
167
  }
168
168
 
169
169
  /** One-line protocol description appended to the launch system prompt. Pass a
170
- * config so CONFIG-DECLARED custom roles are advertised to the model too. */
170
+ * config so CONFIG-DECLARED custom roles are advertised to the model too.
171
+ *
172
+ * The single-vs-fan-out shapes are spelled out as two SEPARATE literal
173
+ * examples (not the terse `task|tasks[]` shorthand this replaced) — live
174
+ * reproduction (multiple real models, including a native-tool-calling-capable
175
+ * one) showed models repeatedly sending `tasks` as a JSON-STRINGIFIED array
176
+ * or a single task string instead of a real JSON array, self-diagnosing
177
+ * "I'm passing tasks as a string instead of as an actual array" turn after
178
+ * turn until the loop guard stopped them. `task` has no native function-
179
+ * calling schema (its shape is too variable for one static schema) — EVERY
180
+ * provider, including native-schema-capable ones, learns this tool's shape
181
+ * from this exact prose line, so ambiguity here directly costs reliability
182
+ * across the board, not just for the JSON-in-prose fallback providers. */
171
183
  export function taskToolProtocolLine(config?: Pick<Config, "subagents">): string {
172
184
  return (
173
- `task {role, task|tasks[], context?} — delegate to a subagent ` +
174
- `(role: ${subagentRoleIds(config).join("|")}; executor can edit, planner/architect/critic are read-only). ` +
175
- `Pass 'tasks' (array) to fan out — ALL roles run concurrently (bounded); scope each executor task to disjoint files (no shared-file coordination channel between concurrent tasks — use sequential task calls if scopes overlap). Integrate the findings yourself.`
185
+ `task single: {role?, task, context?} — fan-out: {role?, tasks:["task 1", "task 2", ...]} ` +
186
+ `— delegate to a subagent (role: ${subagentRoleIds(config).join("|")}; executor can edit, planner/architect/critic are read-only). ` +
187
+ `'tasks' MUST be a real JSON array (of strings, or {task, context?} objects) — NEVER a JSON-stringified array, NEVER a single task string. ` +
188
+ `All items in 'tasks' run concurrently (bounded); scope each executor task to disjoint files (no shared-file coordination channel between concurrent tasks — use sequential task calls if scopes overlap). Integrate the findings yourself.`
176
189
  );
177
190
  }
178
191
 
@@ -555,8 +568,17 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
555
568
  const ctx = (c: unknown) => (typeof c === "string" && c.trim() ? `\n\nContext:\n${c.trim()}` : "");
556
569
 
557
570
  // Fan-out form: `tasks: [ "assignment" | {task|assignment|prompt, context?} ]`.
558
- if (Array.isArray(args.tasks)) {
559
- const items = (args.tasks as unknown[])
571
+ // Live-reproduced (v0.8.42, multiple real models across two different providers,
572
+ // including a native-tool-calling-capable one): even with an explicit, example-driven
573
+ // protocol line, models repeatedly send `tasks` as a JSON-STRINGIFIED array (correct
574
+ // content, wrong JSON type) rather than a real array — a classifiable, unambiguous
575
+ // mistake, not a genuinely different intent. Accept it rather than hard-reject: any
576
+ // string that itself parses to an array is exactly this mistake.
577
+ const tasksArg = typeof args.tasks === "string"
578
+ ? (() => { try { const p = JSON.parse(args.tasks as string); return Array.isArray(p) ? p : args.tasks; } catch { return args.tasks; } })()
579
+ : args.tasks;
580
+ if (Array.isArray(tasksArg)) {
581
+ const items = (tasksArg as unknown[])
560
582
  .map(entry => {
561
583
  if (typeof entry === "string") return { task: entry.trim(), context: "" };
562
584
  if (entry && typeof entry === "object") {
@@ -255,6 +255,7 @@ export function anthropicPayload(
255
255
  credential: Credential = { kind: "none", provider: "anthropic" },
256
256
  stripArtifacts = false,
257
257
  disableFallback = false,
258
+ disableEffort = false,
258
259
  ): string {
259
260
  const model = stripAnthropicPrefix(options.model);
260
261
  const systemPrompt = options.systemPrompt ?? messages.find(m => m.role === "system")?.content;
@@ -306,16 +307,21 @@ export function anthropicPayload(
306
307
  // Opus/Sonnet 4.6+: the model decides how much to think. `display: "summarized"` is
307
308
  // REQUIRED on Opus 4.7+ or thinking content is omitted from the response (the empty-thought
308
309
  // bug); older adaptive models (4.6) reject the field, so it is gated. Effort rides
309
- // output_config — there is no budget_tokens on this transport.
310
+ // output_config — there is no budget_tokens on this transport. `disableEffort` (fail-safe
311
+ // retry after a live "does not support the effort parameter" 400 — some model variants
312
+ // classify as adaptive-transport by name/version but still reject this field) drops ONLY
313
+ // output_config; `thinking` alone is still valid, the model just uses its own default depth.
310
314
  payload.thinking = supportsAdaptiveThinkingDisplay(model)
311
315
  ? { type: "adaptive", display: "summarized" }
312
316
  : { type: "adaptive" };
313
- payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
317
+ if (!disableEffort) payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
314
318
  } else {
315
319
  // Budget-based extended thinking. `display: "summarized"` keeps human-readable thought
316
- // streaming. The 4.5 (budget-effort) transport also carries an output_config effort.
320
+ // streaming. The 4.5 (budget-effort) transport also carries an output_config effort —
321
+ // unless `disableEffort` is set (see the adaptive branch above for the same rationale;
322
+ // this mirrors Haiku 4.5's already-proven-working budget_tokens-without-effort combination).
317
323
  payload.thinking = { type: "enabled", budget_tokens: thinkingBudget, display: "summarized" };
318
- if (thinkingMode === "budget-effort") payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
324
+ if (thinkingMode === "budget-effort" && !disableEffort) payload.output_config = { effort: anthropicAdaptiveEffort(effort) };
319
325
  }
320
326
  } else if (includeTemperature && options.temperature !== undefined) {
321
327
  payload.temperature = options.temperature;
@@ -345,13 +351,14 @@ export function anthropicRequest(
345
351
  includeTemperature: boolean,
346
352
  stripArtifacts = false,
347
353
  disableFallback = false,
354
+ disableEffort = false,
348
355
  ): { url: string; headers: Record<string, string>; body: string } {
349
356
  return {
350
357
  // Anthropic-compatible providers (z.ai, MiniMax, …) accept the Messages wire
351
358
  // format at their own host; an explicit baseUrl pins `${base}/v1/messages`.
352
359
  url: options.baseUrl ? `${options.baseUrl.replace(/\/$/, "")}/v1/messages` : ANTHROPIC_URL,
353
360
  headers: { ...headersFor(credential, stream, stripAnthropicPrefix(options.model), options.baseUrl, disableFallback), ...options.extraHeaders },
354
- body: anthropicPayload(messages, options, stream, includeTemperature, credential, stripArtifacts, disableFallback),
361
+ body: anthropicPayload(messages, options, stream, includeTemperature, credential, stripArtifacts, disableFallback, disableEffort),
355
362
  };
356
363
  }
357
364
 
@@ -376,6 +383,18 @@ function isFallbackUnsupportedError(status: number, detail: string): boolean {
376
383
  return status === 400 && /fallback/i.test(detail);
377
384
  }
378
385
 
386
+ /** A 400 naming "effort"/"output_config" means this specific model variant rejects the
387
+ * effort field even though its name/version classifies it onto a transport that normally
388
+ * carries one (live-reproduced: a rate-limit-fallback-selected dated snapshot,
389
+ * `claude-sonnet-4-5-20250929`, 400'd with "This model does not support the effort
390
+ * parameter" — the same wording already known for Haiku 4.5, but on a model this
391
+ * codebase's version-based classification did not expect to reject it). Retry once with
392
+ * `output_config` dropped (thinking/budget_tokens, if any, stay — only the effort field
393
+ * itself is unsupported) so the turn survives instead of dying on an avoidable 400. */
394
+ function isEffortUnsupportedError(status: number, detail: string): boolean {
395
+ return status === 400 && /effort/i.test(detail) && /output_config|not support/i.test(detail);
396
+ }
397
+
379
398
  /** gjc parity (getSafeAnthropicHeaderEvidence): fold Anthropic's rate-limit response
380
399
  * headers into a 429's error detail so the retry classifier can see them. The decisive
381
400
  * usage-exhaustion signal (`anthropic-ratelimit-unified-overage-disabled-reason:
@@ -401,8 +420,9 @@ async function postAnthropic(
401
420
  let includeTemperature = true;
402
421
  let stripArtifacts = false;
403
422
  let disableFallback = false;
423
+ let disableEffort = false;
404
424
 
405
- for (let attempt = 1; attempt <= 4; attempt++) {
425
+ for (let attempt = 1; attempt <= 5; attempt++) {
406
426
  const { url, headers, body } = anthropicRequest(
407
427
  messages,
408
428
  options,
@@ -411,6 +431,7 @@ async function postAnthropic(
411
431
  includeTemperature,
412
432
  stripArtifacts,
413
433
  disableFallback,
434
+ disableEffort,
414
435
  );
415
436
  const response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
416
437
  if (response.ok) return response;
@@ -428,6 +449,10 @@ async function postAnthropic(
428
449
  disableFallback = true;
429
450
  continue;
430
451
  }
452
+ if (isEffortUnsupportedError(response.status, detail) && !disableEffort) {
453
+ disableEffort = true;
454
+ continue;
455
+ }
431
456
 
432
457
  // Unrecoverable error, or all retries for the active error type spent
433
458
  const enrichedDetail = response.status === 429 ? `${detail}${anthropicRateLimitEvidence(response.headers)}` : detail;
@@ -37,8 +37,6 @@ export interface RouteSlashCtx {
37
37
  routeHistory: RouteHistoryEntry[];
38
38
  }
39
39
 
40
-
41
-
42
40
  export interface RouteSlashResult {
43
41
  /** Present only when changed by "on"/"off". */
44
42
  sessionRouteOverride?: boolean | undefined;
@@ -111,8 +109,6 @@ export async function runRouteSlash(input: string, ctx: RouteSlashCtx): Promise<
111
109
  return { lines };
112
110
  }
113
111
 
114
-
115
-
116
112
  if (sub === "on" || sub === "off") {
117
113
  const enabled = sub === "on";
118
114
  if (arg === "save") {
@@ -293,6 +293,37 @@ export function summarizeForgeInvocation(tool: string, rawArgs: unknown, opts: {
293
293
 
294
294
  if (normalized === "task") {
295
295
  const role = stringArg(args, "role") ?? "executor";
296
+ // Fan-out/batch form (`tasks: [...]`, task-tool.ts's concurrent MAX_FANOUT path) previously
297
+ // fell through to the single-task branch below, which only reads `task`/`prompt`/`assignment`
298
+ // — every batch call rendered as a bare "<missing task>" placeholder with zero visibility into
299
+ // what was actually about to run concurrently, even on a well-formed call (live-reproduced: a
300
+ // real 3-item fan-out showed nothing but "<missing task>" in the tool-call preview box).
301
+ // A JSON-STRINGIFIED `tasks` (live-reproduced model mistake task-tool.ts's handler itself now
302
+ // tolerates — see createTaskTool's `tasksArg` coercion) is accepted here too, for the same
303
+ // reason: the preview must reflect what will actually run, not just the well-formed case.
304
+ const tasksValue = typeof args.tasks === "string"
305
+ ? (() => { try { const p = JSON.parse(args.tasks as string); return Array.isArray(p) ? p : args.tasks; } catch { return args.tasks; } })()
306
+ : args.tasks;
307
+ if (Array.isArray(tasksValue)) {
308
+ const items = (tasksValue as unknown[])
309
+ .map(entry => {
310
+ if (typeof entry === "string") return entry.trim();
311
+ if (entry && typeof entry === "object") {
312
+ const e = entry as Record<string, unknown>;
313
+ return String(e.task ?? e.assignment ?? e.prompt ?? "").trim();
314
+ }
315
+ return "";
316
+ })
317
+ .filter(Boolean);
318
+ const lines = items.length > 0
319
+ ? items.map((t, i) => {
320
+ const firstLine = t.split("\n")[0]!;
321
+ const truncated = firstLine.length > 120 || t.includes("\n");
322
+ return `${i + 1}. ${firstLine.slice(0, 120)}${truncated ? "…" : ""}`;
323
+ })
324
+ : ["<missing tasks>"];
325
+ return { title: `Task: ${role} ×${items.length}`, language: "text", lines };
326
+ }
296
327
  const task = stringArg(args, "task", "prompt", "assignment") ?? "<missing task>";
297
328
  const context = stringArg(args, "context");
298
329
  return {