jeo-code 0.7.4 → 0.7.6

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,34 @@ 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.7.6] - 2026-06-23
10
+ _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._
11
+
12
+ ### Added
13
+ - **Cross-target standalone-binary release matrix (incl. Windows `.exe`).** New `scripts/ci-release-build-binaries.ts` compiles `src/cli.ts` for every release target via Bun's `--compile --target bun-<triple>`; since jeo has no native addons/workers, all targets (`darwin-arm64`/`-x64`, `linux-x64`/`-arm64`, `win32-x64`) cross-compile from a single Linux runner. Driven by `bun run build:binaries` / `build:win` (new `package.json` scripts) with `--targets <ids|all>`, `RELEASE_TARGETS` env, and `--dry-run` support. A new `release-binaries` job in `.github/workflows/npm-publish.yml` builds the full matrix, uploads them as a workflow artifact, and on a `release` event runs `gh release upload <tag> dist/jeo-* --clobber` to attach them.
14
+
15
+ ### Changed
16
+ - **The running version's CHANGELOG is embedded into the compiled binary.** `loadBundledChangelog` (in `src/util/whats-new.ts`) now imports `CHANGELOG.md` with `{ type: "text" }`, so the release notes ship inside the `bun build --compile` single binary — where `import.meta.dir` resolves to the bunfs virtual root and the old two-levels-up on-disk path does not exist. The on-disk read remains as a dev fallback only when the embed is empty.
17
+
18
+ ### Fixed
19
+ - **The in-TUI markdown table renderer no longer breaks a cell on an escaped `\|`.** `markdown-table.ts` now distinguishes a real cell separator from a literal pipe: `hasUnescapedPipe` drives row detection, `splitCells` walks the row character-by-character (turning `\|` into a literal `|` inside the cell instead of a column break and never stripping an escaped trailing pipe), and `isDelimiterRow` reuses that same escape-aware split. A table whose cells contain `\|` now renders with the correct column count.
20
+
21
+ ### Verified
22
+ - `bun run typecheck` clean; the touched suites pass — `test/markdown-table.test.ts` and `test/release-binaries.test.ts` (14 pass / 0 fail) cover escaped-pipe cell splitting/delimiter detection, the Windows `.exe` target shape, `--targets`/env/`all` resolution, the per-target compile invocation, and the workflow's build+upload of every target.
23
+
24
+
25
+ ## [0.7.5] - 2026-06-23
26
+ _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._
27
+
28
+ ### Changed
29
+ - **Project-context guidance files are cached by `mtimeMs:size` signature instead of re-read every spawn.** `readContextFile` (in `src/agent/context-files.ts`) now keeps a 256-entry LRU of raw file text keyed by a stat signature. The cheap `fs.stat` still runs every call, so an edit (new mtime/size) or deletion is caught immediately; truncation is reapplied per call from the raw text, so a differing budget never pollutes the cache. `loadProjectContext` runs once per subagent spawn (team/ralph/autopilot), so this removes the bulk of repeated AGENTS.md/guidance reads.
30
+ - **OKF memory concepts are parse-cached per file under the same stat-signature scheme.** `loadConcepts` (in `src/agent/memory.ts`) walks `.jeo/memory/` and parses every concept file; the new 256-entry cache skips the read+parse for unchanged files. The directory walk and per-file `stat` still run (new files appear, deleted files drop out), and every write path (`migrateLegacyMemory`, both `distillSessionMemory` branches) calls `invalidateConceptCache()` so a freshly written concept is never served stale.
31
+ - **Native tool schemas are derived once per turn, not rebuilt every step.** `runAgentLoop` (in `src/agent/engine.ts`) hoists `nativeToolSchemasFor(...)` out of the step loop since the active toolset is constant for the whole turn, eliminating a per-step list rebuild/reallocation.
32
+ - **Autopilot keeps the running `best` score in memory across iterations.** `cmdLoop` (in `src/autopilot.ts`) seeds `best` from `currentBest(s)` once and folds each kept step forward via the new exported `foldBest(goal, best, score)` helper, instead of re-reading and re-parsing the entire append-only log on every step. `foldBest` mirrors `currentBest`'s reduction exactly (NaN never becomes best; `min`/`max`/last-value semantics by goal) so the in-memory value can never diverge from a fresh log re-scan.
33
+
34
+ ### Verified
35
+ - `bun run typecheck` clean; the touched suites pass — `test/context-files.test.ts`, `test/memory-distill-okf.test.ts`, and `test/autopilot-engine.test.ts` (43 pass / 0 fail) exercise cache hits, post-edit/post-delete invalidation, and `foldBest`/`currentBest` parity.
36
+
9
37
  ## [0.7.4] - 2026-06-22
10
38
  _Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop._
11
39
 
package/README.ja.md CHANGED
@@ -193,11 +193,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
193
193
  ## 変更履歴 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[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.
197
+ - **[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.
196
198
  - **[0.7.4]** (2026-06-22) — Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop.
197
199
  - **[0.7.3]** (2026-06-22) — Provider catalog: the Tencent Cloud MaaS (international) `knownModels` list is broadened from a 4-id DeepSeek/MiniMax set to the full live-verified line-up across five families — DeepSeek, MiniMax, Zhipu GLM, Moonshot Kimi, and Hunyuan. Because the host exposes no `/v1/models` route, this hand-maintained list is the model picker's source of truth for the offline fallback.
198
200
  - **[0.7.2]** (2026-06-22) — Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
199
- - **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
200
- - **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -193,11 +193,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
193
193
  ## 변경 이력 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[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.
197
+ - **[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.
196
198
  - **[0.7.4]** (2026-06-22) — Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop.
197
199
  - **[0.7.3]** (2026-06-22) — Provider catalog: the Tencent Cloud MaaS (international) `knownModels` list is broadened from a 4-id DeepSeek/MiniMax set to the full live-verified line-up across five families — DeepSeek, MiniMax, Zhipu GLM, Moonshot Kimi, and Hunyuan. Because the host exposes no `/v1/models` route, this hand-maintained list is the model picker's source of truth for the offline fallback.
198
200
  - **[0.7.2]** (2026-06-22) — Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
199
- - **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
200
- - **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -195,11 +195,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
195
195
  ## Changelog
196
196
 
197
197
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
198
+ - **[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.
199
+ - **[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.
198
200
  - **[0.7.4]** (2026-06-22) — Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop.
199
201
  - **[0.7.3]** (2026-06-22) — Provider catalog: the Tencent Cloud MaaS (international) `knownModels` list is broadened from a 4-id DeepSeek/MiniMax set to the full live-verified line-up across five families — DeepSeek, MiniMax, Zhipu GLM, Moonshot Kimi, and Hunyuan. Because the host exposes no `/v1/models` route, this hand-maintained list is the model picker's source of truth for the offline fallback.
200
202
  - **[0.7.2]** (2026-06-22) — Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
201
- - **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
202
- - **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
203
203
 
204
204
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
205
205
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -193,11 +193,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
193
193
  ## 更新日志 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[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.
197
+ - **[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.
196
198
  - **[0.7.4]** (2026-06-22) — Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop.
197
199
  - **[0.7.3]** (2026-06-22) — Provider catalog: the Tencent Cloud MaaS (international) `knownModels` list is broadened from a 4-id DeepSeek/MiniMax set to the full live-verified line-up across five families — DeepSeek, MiniMax, Zhipu GLM, Moonshot Kimi, and Hunyuan. Because the host exposes no `/v1/models` route, this hand-maintained list is the model picker's source of truth for the offline fallback.
198
200
  - **[0.7.2]** (2026-06-22) — Workflow honesty + prompt-input testability: `ultragoal` acceptance criteria can now carry a trailing `{verify: <command>}` directive that makes a criterion **individually** provable (real PASS/FAIL instead of a blanket UNVERIFIED on a green suite), with a SUCCESS / PARTIAL / SUITE_GREEN / FAILED status taxonomy; the boxed prompt's full stdin→readline keystroke rewriter is extracted into a pure, PTY-free function so the "↓ cuts the lower text" fix and the rest of the wiring are unit-tested directly. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
199
- - **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
200
- - **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -46,6 +46,8 @@
46
46
  "start": "bun src/cli.ts",
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
48
  "build": "bun build src/cli.ts --compile --outfile dist/jeo",
49
+ "build:binaries": "bun scripts/ci-release-build-binaries.ts",
50
+ "build:win": "bun scripts/ci-release-build-binaries.ts --targets win32-x64",
49
51
  "pack:check": "npm pack --dry-run",
50
52
  "publish:npm": "npm publish --access public --registry https://registry.npmjs.org/",
51
53
  "changelog:sync": "bun scripts/sync-changelog.ts",
@@ -26,12 +26,36 @@ const GUIDANCE_CONTEXT_CHARS = TOTAL_CONTEXT_CHARS - BASE_CONTEXT_CHARS;
26
26
  const MAX_AGENT_GUIDANCE_FILES = 20;
27
27
  const AGENT_GUIDANCE_EXTENSIONS = new Set([".md", ".json", ".jsonc", ".yaml", ".yml", ".toml"]);
28
28
 
29
+ // Per-file RAW content cache: absolute path → mtimeMs:size signature + raw text.
30
+ // loadProjectContext runs once per subagent spawn (team/ralph/autopilot); this skips
31
+ // re-reading every unchanged AGENTS.md / guidance file. The cheap fs.stat still runs
32
+ // each call, so an edit (new mtime/size) or deletion is caught immediately. Truncation
33
+ // is applied per call from the raw text, so a differing budget never pollutes the cache.
34
+ const fileContentCache = new Map<string, { sig: string; content: string }>();
35
+ const FILE_CONTENT_CACHE_CAP = 256;
36
+
29
37
  async function readContextFile(filePath: string, displayPath: string, remainingChars: number): Promise<ProjectContextFile | null> {
30
38
  if (remainingChars <= 0) return null;
31
39
  try {
32
40
  const stat = await fs.stat(filePath);
33
41
  if (!stat.isFile() || stat.size <= 0) return null;
34
- const content = await fs.readFile(filePath, "utf-8");
42
+ const sig = `${stat.mtimeMs}:${stat.size}`;
43
+ let content: string;
44
+ const cached = fileContentCache.get(filePath);
45
+ if (cached && cached.sig === sig) {
46
+ // LRU refresh so a hot file is evicted last.
47
+ fileContentCache.delete(filePath);
48
+ fileContentCache.set(filePath, cached);
49
+ content = cached.content;
50
+ } else {
51
+ content = await fs.readFile(filePath, "utf-8");
52
+ // Evict the oldest entry (Map preserves insertion order) once at capacity.
53
+ if (fileContentCache.size >= FILE_CONTENT_CACHE_CAP) {
54
+ const oldest = fileContentCache.keys().next().value;
55
+ if (oldest !== undefined) fileContentCache.delete(oldest);
56
+ }
57
+ fileContentCache.set(filePath, { sig, content });
58
+ }
35
59
  if (content.length <= 0) return null;
36
60
  const cap = Math.min(PER_CONTEXT_FILE_CHARS, remainingChars);
37
61
  const finalContent = content.length > cap ? content.slice(0, cap) + "\n…(truncated)" : content;
@@ -83,6 +107,7 @@ const EXPLICIT_GUIDANCE_FILES = [".agents/oma-config.yaml", ".agents/oma-config.
83
107
  const workspaceScanCache = new Map<string, WorkspaceScan>();
84
108
  const WORKSPACE_SCAN_CACHE_CAP = 32;
85
109
 
110
+
86
111
  function segsEqual(a: readonly string[], b: readonly string[]): boolean {
87
112
  if (a.length !== b.length) return false;
88
113
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
@@ -188,8 +213,12 @@ async function getWorkspaceScan(cwd: string): Promise<WorkspaceScan> {
188
213
  export function invalidateWorkspaceScan(cwd?: string): void {
189
214
  if (cwd === undefined) {
190
215
  workspaceScanCache.clear();
216
+ fileContentCache.clear();
191
217
  return;
192
218
  }
219
+ // Per-cwd: drop just the downward scan. The per-file content cache is mtime/size
220
+ // self-invalidating, so it needs no per-cwd eviction (a path's stale entry is
221
+ // replaced on the next read once its mtime/size changes).
193
222
  workspaceScanCache.delete(path.resolve(cwd));
194
223
  }
195
224
 
@@ -216,6 +245,10 @@ export async function discoverAgentGuidanceFiles(cwd = process.cwd()): Promise<A
216
245
  }
217
246
 
218
247
 
248
+ // No assembled-result cache: the parent walk + downward scan (cached by workspaceScanCache)
249
+ // are cheap, and readContextFile skips re-reading unchanged file CONTENTS via its mtime/size
250
+ // cache. This keeps the result disk-truthful — a guidance/AGENTS.md edit is reflected on the
251
+ // next spawn — while still avoiding the dominant per-spawn cost (re-reading every file).
219
252
  export async function loadProjectContext(cwd = process.cwd()): Promise<ProjectContextFile[]> {
220
253
  const result: ProjectContextFile[] = [];
221
254
  let baseChars = 0;
@@ -450,6 +450,9 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
450
450
  // model's text as the final answer instead of burning the whole step budget.
451
451
  const MAX_PARSE_BOUNCES = GUARD_LIMITS.MAX_PARSE_BOUNCES;
452
452
  let parseFailures = 0;
453
+ // The active toolset is constant for the whole turn, so derive the native tool
454
+ // schemas ONCE instead of rebuilding/reallocating the list on every step.
455
+ const turnToolSchemas = nativeToolSchemasFor(Object.keys(tools));
453
456
  while (true) {
454
457
  if (turnBudgetMs > 0 && Date.now() - turnStartedAt > turnBudgetMs) {
455
458
  stopKind = "time";
@@ -536,7 +539,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
536
539
  // use these and re-serialize the structured call to canonical JSON; the
537
540
  // antigravity/ollama fallback ignores them. Only on the main step — never
538
541
  // the prose wrap-up call below.
539
- tools: nativeToolSchemasFor(Object.keys(tools)),
542
+ tools: turnToolSchemas,
540
543
  model: opts.model,
541
544
  maxTokens: opts.maxTokens,
542
545
  reasoningEffort: opts.reasoningEffort,
@@ -176,6 +176,7 @@ export async function migrateLegacyMemory(cwd: string): Promise<MigrationResult>
176
176
  // Preserve the legacy doc as a rollback backup, off the active read path.
177
177
  const backupPath = `${memoryFilePath(cwd)}.bak`;
178
178
  await fs.rename(memoryFilePath(cwd), backupPath).catch(() => {});
179
+ invalidateConceptCache();
179
180
  return { migrated: true, conceptCount: written.length, backupPath };
180
181
  }
181
182
 
@@ -205,6 +206,25 @@ export interface Concept {
205
206
  relPath: string;
206
207
  }
207
208
 
209
+ /** Per-file parse cache keyed by absolute path → (mtimeMs+size signature, parsed
210
+ * Concept or null for a frontmatter-less/unparseable file). loadConcepts walks
211
+ * `.jeo/memory/` and read+parses every concept file; team/ralph/autopilot call
212
+ * memoryPromptSection (→ loadConcepts) once per subagent spawn, re-paying that
213
+ * read+parse each time. The cheap directory walk + per-file stat still run (so new
214
+ * files appear and deleted files drop out immediately — disk is the source of
215
+ * truth), but an UNCHANGED file (same mtime+size) skips the read+parse. A changed
216
+ * file's signature differs, so edits are picked up without explicit invalidation. */
217
+ interface ParsedConceptEntry { sig: string; concept: Concept | null }
218
+ const parsedConceptCache = new Map<string, ParsedConceptEntry>();
219
+ const PARSED_CONCEPT_CACHE_CAP = 512;
220
+
221
+ /** Drop the per-file concept parse cache so the next loadConcepts re-reads disk.
222
+ * The cache is already mtime/size self-invalidating; this is a hard reset for
223
+ * tests and callers that mutate files without changing their stat signature. */
224
+ export function invalidateConceptCache(): void {
225
+ parsedConceptCache.clear();
226
+ }
227
+
208
228
  /** Read every concept document in the bundle into structured `Concept`s. Reserved
209
229
  * files (index.md/log.md) and raw/ payloads are skipped; unparseable or
210
230
  * frontmatter-less files are ignored (lenient consumption). */
@@ -226,15 +246,38 @@ async function loadConceptsFromBundle(bundleDir: string): Promise<Concept[]> {
226
246
  for (const file of files) {
227
247
  const relPath = path.relative(bundleDir, file).replace(/\\/g, "/");
228
248
  if (isReservedFile(relPath)) continue;
249
+
250
+ // Skip the read+parse when the file is byte-for-byte unchanged since we last
251
+ // saw it (same mtime+size). A missing stat falls through to a fresh read.
252
+ let sig: string | null = null;
253
+ try {
254
+ const st = await fs.stat(file);
255
+ sig = `${st.mtimeMs}:${st.size}`;
256
+ } catch {
257
+ continue;
258
+ }
259
+ const cached = parsedConceptCache.get(file);
260
+ if (cached && cached.sig === sig) {
261
+ // LRU refresh so a hot file is evicted last.
262
+ parsedConceptCache.delete(file);
263
+ parsedConceptCache.set(file, cached);
264
+ if (cached.concept) concepts.push(cached.concept);
265
+ continue;
266
+ }
267
+
229
268
  let parsed;
230
269
  try {
231
270
  parsed = parseConcept(await fs.readFile(file, "utf-8"));
232
271
  } catch {
272
+ cacheParsedConcept(file, sig, null);
273
+ continue;
274
+ }
275
+ if (!parsed.hasFrontmatter) {
276
+ cacheParsedConcept(file, sig, null);
233
277
  continue;
234
278
  }
235
- if (!parsed.hasFrontmatter) continue;
236
279
  const fm = parsed.frontmatter;
237
- concepts.push({
280
+ const concept: Concept = {
238
281
  type: (fm.type as string) || "RepoFact",
239
282
  title: (fm.title as string) || path.basename(file, ".md"),
240
283
  description: (fm.description as string) || "",
@@ -242,11 +285,24 @@ async function loadConceptsFromBundle(bundleDir: string): Promise<Concept[]> {
242
285
  tags: Array.isArray(fm.tags) ? fm.tags.filter((t): t is string => typeof t === "string") : [],
243
286
  confidence: typeof fm.confidence === "string" ? fm.confidence : "high",
244
287
  relPath,
245
- });
288
+ };
289
+ cacheParsedConcept(file, sig, concept);
290
+ concepts.push(concept);
246
291
  }
247
292
  return concepts;
248
293
  }
249
294
 
295
+ /** Store a per-file parse result under its stat signature, evicting the oldest
296
+ * entry once at capacity (Map preserves insertion order → FIFO/LRU). */
297
+ function cacheParsedConcept(file: string, sig: string, concept: Concept | null): void {
298
+ parsedConceptCache.delete(file);
299
+ if (parsedConceptCache.size >= PARSED_CONCEPT_CACHE_CAP) {
300
+ const oldest = parsedConceptCache.keys().next().value;
301
+ if (oldest !== undefined) parsedConceptCache.delete(oldest);
302
+ }
303
+ parsedConceptCache.set(file, { sig, concept });
304
+ }
305
+
250
306
  /** Tokenize a free-text query into distinct lowercased keywords (len ≥ 3). */
251
307
  function tokenize(query?: string): string[] {
252
308
  if (!query) return [];
@@ -735,6 +791,7 @@ export async function distillSessionMemory(
735
791
  await fs.rename(memoryFilePath(cwd), `${memoryFilePath(cwd)}.bak`).catch(() => {});
736
792
  }
737
793
  await cleanupStalePendingFiles(bundleDir);
794
+ invalidateConceptCache();
738
795
  return { updated: true };
739
796
  } else {
740
797
  // JSON extraction failed (text-only models often wrap or drop the JSON).
@@ -754,6 +811,7 @@ export async function distillSessionMemory(
754
811
  const tmp = `${file}.tmp-${process.pid}`;
755
812
  await fs.writeFile(tmp, doc + "\n", "utf-8");
756
813
  await fs.rename(tmp, file);
814
+ invalidateConceptCache();
757
815
  return { updated: true };
758
816
  }
759
817
  } catch (err: any) {
package/src/autopilot.ts CHANGED
@@ -142,6 +142,17 @@ function isImprovement(goal: Goal, score: number, best: number | undefined): boo
142
142
  return true; // gate handled via passed, not score
143
143
  }
144
144
 
145
+ /** Fold one KEPT step's score into the running best, mirroring currentBest's
146
+ * reduction so an in-memory best (loop hot path) never diverges from a fresh
147
+ * log re-scan. NaN scores never become the best; gate tracks the last value. */
148
+ export function foldBest(goal: Goal, best: number | undefined, score: number): number | undefined {
149
+ if (Number.isNaN(score)) return best;
150
+ if (best === undefined) return score;
151
+ if (goal === "min") return Math.min(best, score);
152
+ if (goal === "max") return Math.max(best, score);
153
+ return score;
154
+ }
155
+
145
156
  /**
146
157
  * Single source of truth for the ratchet keep/revert decision. Shared by step,
147
158
  * loop, and status so they can never diverge.
@@ -246,6 +257,10 @@ function cmdLoop(flags: Record<string, string>): void {
246
257
  }
247
258
 
248
259
  let sinceImprove = 0;
260
+ // Keep `best` in memory across iterations instead of re-reading and re-parsing
261
+ // the whole append-only log on every step (currentBest scans LOG each call).
262
+ // Folded forward on each kept step to match a fresh currentBest() exactly.
263
+ let best = currentBest(s);
249
264
  for (let i = 1; i <= max; i++) {
250
265
  // mutate: runner makes exactly one change
251
266
  let runnerOk = true;
@@ -260,7 +275,6 @@ function cmdLoop(flags: Record<string, string>): void {
260
275
  return;
261
276
  }
262
277
 
263
- const best = currentBest(s);
264
278
  const { score, passed, output } = runEval(s);
265
279
  const decision = decideStep(s.goal, score, passed, best);
266
280
 
@@ -274,6 +288,7 @@ function cmdLoop(flags: Record<string, string>): void {
274
288
  appendLog({ type: "step", iteration: i, change: `loop#${i}`, score, passed, decision, prevBest: best ?? null, output });
275
289
  // A keep is forward progress (min/max: provably an improvement; gate: a pass).
276
290
  // Anything else extends the no-progress streak toward convergence.
291
+ if (decision === "keep") best = foldBest(s.goal, best, score);
277
292
  sinceImprove = decision === "keep" ? 0 : sinceImprove + 1;
278
293
  console.log(`jeo autopilot: loop ${i}/${max} ${decision.toUpperCase()} score=${fmt(score)} (sinceImprove=${sinceImprove})`);
279
294
 
@@ -12,25 +12,47 @@ export interface TableRenderOptions {
12
12
  maxColWidth?: number;
13
13
  }
14
14
 
15
- /** A line is a table row when it has at least one unescaped `|` and trims to start/contain cells. */
16
- function isTableRow(line: string): boolean {
17
- const t = line.trim();
18
- return t.includes("|") && /\|/.test(t);
15
+ /** A line carries an *unescaped* `|` — a real cell separator, not a literal `\|`. */
16
+ function hasUnescapedPipe(line: string): boolean {
17
+ return /(?:^|[^\\])\|/.test(line);
19
18
  }
20
19
 
21
- /** A line is the GFM delimiter row, e.g. `| --- | :--: | ---: |`. */
22
- function isDelimiterRow(line: string): boolean {
23
- const t = line.trim().replace(/^\||\|$/g, "");
24
- if (!t.includes("-")) return false;
25
- return t.split("|").every(cell => /^\s*:?-{1,}:?\s*$/.test(cell));
20
+ /** A line is a table row when it has at least one unescaped `|`. */
21
+ function isTableRow(line: string): boolean {
22
+ return hasUnescapedPipe(line.trim());
26
23
  }
27
24
 
28
- /** Split a markdown table row into trimmed cell strings (drop the outer pipes). */
25
+ /** Split a markdown table row into trimmed cell strings, honoring escaped `\|`. */
29
26
  function splitCells(line: string): string[] {
30
- let t = line.trim();
31
- if (t.startsWith("|")) t = t.slice(1);
32
- if (t.endsWith("|")) t = t.slice(0, -1);
33
- return t.split("|").map(c => c.trim());
27
+ let s = line.trim();
28
+ if (s.startsWith("|")) s = s.slice(1);
29
+ // Only strip a trailing pipe that actually closes the row (not an escaped `\|`).
30
+ if (s.endsWith("|") && !s.endsWith("\\|")) s = s.slice(0, -1);
31
+ const cells: string[] = [];
32
+ let cur = "";
33
+ for (let i = 0; i < s.length; i++) {
34
+ const ch = s[i]!;
35
+ if (ch === "\\" && s[i + 1] === "|") {
36
+ cur += "|";
37
+ i++;
38
+ continue;
39
+ }
40
+ if (ch === "|") {
41
+ cells.push(cur);
42
+ cur = "";
43
+ continue;
44
+ }
45
+ cur += ch;
46
+ }
47
+ cells.push(cur);
48
+ return cells.map(c => c.trim());
49
+ }
50
+
51
+ /** A line is the GFM delimiter row, e.g. `| --- | :--: | ---: |`. */
52
+ function isDelimiterRow(line: string): boolean {
53
+ if (!isTableRow(line)) return false;
54
+ const cells = splitCells(line);
55
+ return cells.length > 0 && cells.every(cell => /^:?-{1,}:?$/.test(cell));
34
56
  }
35
57
 
36
58
  type Align = "left" | "center" | "right";
@@ -1,4 +1,5 @@
1
1
  import pkg from "../../package.json";
2
+ import changelogText from "../../CHANGELOG.md" with { type: "text" };
2
3
  import { compareVersions } from "../commands/update";
3
4
  import * as os from "node:os";
4
5
  import * as path from "node:path";
@@ -198,9 +199,12 @@ export function renderWhatsNew(sections: ChangelogSection[], opts?: WhatsNewRend
198
199
 
199
200
  /** Read the CHANGELOG.md that ships next to this package (running version's notes). */
200
201
  export async function loadBundledChangelog(): Promise<string | null> {
201
- // This module lives at src/util/whats-new.ts; CHANGELOG.md sits at the package
202
- // root, i.e. two levels up. Resolve against the module dir so it works from a
203
- // global install path just as from the repo.
202
+ // CHANGELOG.md is embedded at build time via the text import above, so the
203
+ // running version's notes are available offline AND inside the `--compile`
204
+ // single binary — where `import.meta.dir` resolves to the bunfs virtual root
205
+ // and the on-disk path below does not exist.
206
+ if (changelogText && changelogText.trim().length > 0) return changelogText;
207
+ // Dev fallback: read from the package root two levels up if the embed is empty.
204
208
  const candidate = path.join(import.meta.dir, "..", "..", "CHANGELOG.md");
205
209
  try {
206
210
  return await readFile(candidate, "utf-8");