jeo-code 0.12.16 → 0.12.17

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
@@ -9,6 +9,18 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.12.17] - 2026-11-04
13
+
14
+ ### Fixed
15
+ - **Windows sandbox unmapped host paths** — a host path that matches no approved mount (`C:\…`, drive-relative `C:name`, UNC `\\server\share`, or a leading `\`) was forwarded to the Linux guest unchanged, where it is a *relative* name; a write to it succeeded and created an oddly named file inside the read-write workspace mount instead of being denied. It stayed inside the mount (no host path was written), but it violated the "unmounted paths are denied" contract. The supervisor now rejects such paths (tool arguments, `cwd`, and `exec` cwd) with `SANDBOX_POLICY: host path is outside approved mounts` before anything reaches the guest. POSIX hosts and mounted paths are unchanged. Found by `verify:sandbox` (`unmounted-sentinel`) on a native Windows host.
16
+ - **Windows shell resolution** — the `bash` tool, background jobs/monitors, and post-turn hooks spawned a bare `bash`, which on Windows resolves to WSL's `System32\bash.exe` (no `bun`, Windows paths absent). They now use the existing Git for Windows resolver, so verification commands no longer fail with exit 127 and hooks no longer time out.
17
+ - **Mid-turn premature stop** — plain-text progress narration after tools have run, or while mutations/hooks are pending, now bounces for a JSON tool call instead of ending the turn; a post-verification completion grace step and verification-cycle step-budget extensions keep iterative edit-test-fix loops from being cut off.
18
+ - **Live input** — mouse reports and terminal capability replies are stripped before they can reach the live draft; mid-turn file paths (`/tmp/a.ts`) and dollar amounts (`$100`) are steered to the model rather than treated as commands that abort the turn.
19
+ - **Subagents** — executors, read-only roles, and critics get bounded step-budget extensions, and a subagent whose `done` lacks the required role-contract sections is bounced once to complete them.
20
+
21
+ ### Known issues
22
+ - **Windows `verify:discord`** — 27 of 101 focused Discord tests fail on a native Windows host, both with and without the changes in this release (measured on the pre-change commit; not measured on other platforms), so its authoritative report is `failed` there. At least the attachment-staging check rejects a private directory because Windows reports mode `0o666`; the remainder are not yet triaged.
23
+
12
24
  ## [0.12.16] - 2026-11-04
13
25
 
14
26
  ### Upgrade
@@ -2682,7 +2694,8 @@ _Initial release._
2682
2694
  ### Added
2683
2695
  - Initial jeo-code agent and CLI.
2684
2696
 
2685
- [Unreleased]: https://github.com/akillness/jeo-code/compare/v0.12.16...HEAD
2697
+ [Unreleased]: https://github.com/akillness/jeo-code/compare/v0.12.17...HEAD
2698
+ [0.12.17]: https://github.com/akillness/jeo-code/releases/tag/v0.12.17
2686
2699
  [0.12.16]: https://github.com/akillness/jeo-code/releases/tag/v0.12.16
2687
2700
  [0.12.6]: https://github.com/akillness/jeo-code/releases/tag/v0.12.6
2688
2701
  [0.11.4]: https://github.com/akillness/jeo-code/releases/tag/v0.11.4
package/README.ja.md CHANGED
@@ -447,10 +447,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
447
447
 
448
448
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
449
449
  - **[Unreleased]**
450
+ - **[0.12.17]** (2026-11-04)
450
451
  - **[0.12.16]** (2026-11-04)
451
452
  - **[0.12.15]** (2026-11-03)
452
453
  - **[0.12.14]** (2026-11-03)
453
- - **[0.12.13]** (2026-11-03)
454
454
 
455
455
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
456
456
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -502,10 +502,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
502
502
 
503
503
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
504
504
  - **[Unreleased]**
505
+ - **[0.12.17]** (2026-11-04)
505
506
  - **[0.12.16]** (2026-11-04)
506
507
  - **[0.12.15]** (2026-11-03)
507
508
  - **[0.12.14]** (2026-11-03)
508
- - **[0.12.13]** (2026-11-03)
509
509
 
510
510
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
511
511
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -37,6 +37,15 @@
37
37
  <a href="README.zh.md">中文</a>
38
38
  </p>
39
39
 
40
+ ## Install
41
+
42
+ Requires Bun `1.3.14+`.
43
+
44
+ ```bash
45
+ bun install -g jeo-code
46
+ jeo --version
47
+ ```
48
+
40
49
  Run `jeo` inside a repository and it reads files, edits them, runs commands, and drives the task to completion — streaming every step live in an inline, scrollback-friendly TUI.
41
50
 
42
51
  ## Documentation
@@ -105,15 +114,6 @@ jeo-code is a **local-first AI coding agent CLI** designed to safely drive entir
105
114
  - **macOS low file-descriptor-limit warning** — a low `ulimit -n` (BSD's 256/1024 default) risks opaque `EMFILE` failures from file watching, the browser tool, or a broad repo scan; jeo now warns once at launch (stderr only, never piped `-p` output) with concrete `ulimit`/`launchctl` guidance. Opt out with `JEO_SKIP_NOFILE_CHECK=1`.
106
115
 
107
116
 
108
- ## Install
109
-
110
- Requires Bun `1.3.14+`.
111
-
112
- ```bash
113
- bun install -g jeo-code
114
- jeo --version
115
- ```
116
-
117
117
 
118
118
  ## Quick start
119
119
 
@@ -491,10 +491,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
491
491
 
492
492
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
493
493
  - **[Unreleased]**
494
+ - **[0.12.17]** (2026-11-04)
494
495
  - **[0.12.16]** (2026-11-04)
495
496
  - **[0.12.15]** (2026-11-03)
496
497
  - **[0.12.14]** (2026-11-03)
497
- - **[0.12.13]** (2026-11-03)
498
498
 
499
499
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
500
500
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -445,10 +445,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
445
445
 
446
446
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
447
447
  - **[Unreleased]**
448
+ - **[0.12.17]** (2026-11-04)
448
449
  - **[0.12.16]** (2026-11-04)
449
450
  - **[0.12.15]** (2026-11-03)
450
451
  - **[0.12.14]** (2026-11-03)
451
- - **[0.12.13]** (2026-11-03)
452
452
 
453
453
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
454
454
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.12.16",
3
+ "version": "0.12.17",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
 
@@ -35,6 +35,13 @@ import { jeoEnv } from "../util/env";
35
35
  import { GUARD_LIMITS, isVerificationSignal, verificationProducedEvidence, repeatHint, classifyDoneGate } from "./loop-guards";
36
36
  import { stripLeakedReasoningTags } from "../ai/think-tags";
37
37
 
38
+ const MID_TASK_PROSE_RE =
39
+ /\b(?:i\s+will\s+now|i'll\s+now|let\s+me\s+(?:now\s+)?(?:read|check|inspect|search|find|edit|write|run|verify|update|fix|implement|create|test)|next,?\s+i\s+(?:will|need\s+to)|now\s+i\s+(?:will|need\s+to))\b|(?:이제|다음으로|먼저|이어서)\s+[^\n]{0,80}(?:수정|작성|실행|확인|구현|검증|진행|추가|변경|점검)(?:하겠|할게|해\s*보겠)/i;
40
+
41
+ export function looksLikeMidTaskProse(text: string): boolean {
42
+ return MID_TASK_PROSE_RE.test(text.trim());
43
+ }
44
+
38
45
 
39
46
  async function invokeCallLlm(history: Message[], options: {
40
47
  jsonMode: boolean;
@@ -653,6 +660,8 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
653
660
  let mutVerSeq = 0;
654
661
  let lastMutationSeq = 0;
655
662
  let lastVerificationSeq = 0;
663
+ let verificationCycles = 0;
664
+ let completionGraceUsed = false;
656
665
  let doneCorrections = 0;
657
666
  // F1 (round 4): the run-command of the most recent post-turn hook FAILURE whose
658
667
  // diagnostics the model saw but has not yet resolved (a later clean hook run
@@ -715,12 +724,17 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
715
724
  if (step > budget.limit()) {
716
725
  const decision = budget.tryExtend();
717
726
  if (!decision.extend) {
718
- budgetStopReason = decision.reason;
719
- break;
727
+ if (!completionGraceUsed && sawMutation && sawVerification && lastMutationSeq < lastVerificationSeq && !pendingHookFailure) {
728
+ completionGraceUsed = true;
729
+ } else {
730
+ budgetStopReason = decision.reason;
731
+ break;
732
+ }
733
+ } else {
734
+ // One surface per sink: budget-aware consumers get onBudget; others the notice.
735
+ if (ev.onBudget) ev.onBudget(decision.limit, decision.reason);
736
+ else ev.onNotice?.(decision.reason);
720
737
  }
721
- // One surface per sink: budget-aware consumers get onBudget; others the notice.
722
- if (ev.onBudget) ev.onBudget(decision.limit, decision.reason);
723
- else ev.onNotice?.(decision.reason);
724
738
  }
725
739
  if (opts.signal?.aborted) {
726
740
  return finish({ done: false, steps: step - 1, doneReason: "Cancelled." });
@@ -1043,11 +1057,45 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1043
1057
  } catch (err) {
1044
1058
  // Preserve chat-style final answers, but subject them to the same completion
1045
1059
  // gates as explicit done calls rather than treating prose as proof of success.
1060
+ // Mid-turn prose (after tools have already run, while mutations/hooks are
1061
+ // pending, or when the text narrates an upcoming action) must bounce for a
1062
+ // JSON tool call first instead of prematurely terminating the turn on step 1.
1046
1063
  const trimmed = responseText.trim();
1047
1064
  parseFailures++;
1048
- if (trimmed && (!trimmed.includes("{") || parseFailures > MAX_PARSE_BOUNCES)) {
1065
+ const hasTools = Object.keys(tools).length > 0;
1066
+ const midTurnInProgress =
1067
+ (step > 1 && hasTools) ||
1068
+ sawMutation ||
1069
+ pendingHookFailure !== null ||
1070
+ (hasTools && looksLikeMidTaskProse(trimmed));
1071
+ if (trimmed && (parseFailures > MAX_PARSE_BOUNCES || (!trimmed.includes("{") && !midTurnInProgress))) {
1072
+ const candidateReason = stripLeakedReasoningTags(trimmed) || trimmed;
1073
+ if (parseFailures <= MAX_PARSE_BOUNCES && ev.onBeforeDone) {
1074
+ const preCheck = await ev.onBeforeDone(candidateReason, {
1075
+ sawMutation,
1076
+ sawVerification,
1077
+ verificationStale: sawMutation && lastMutationSeq > lastVerificationSeq,
1078
+ });
1079
+ if (preCheck) {
1080
+ if (doneCorrections >= GUARD_LIMITS.MAX_DONE_CORRECTIONS) {
1081
+ return finish({ done: false, steps: step, doneReason: `Stopped: completion rejected after ${doneCorrections} corrections. ${preCheck}` });
1082
+ }
1083
+ doneCorrections++;
1084
+ ev.onNotice?.(`done deferred — completion correction ${doneCorrections}/${GUARD_LIMITS.MAX_DONE_CORRECTIONS}`);
1085
+ ev.onAssistant?.(responseText, null);
1086
+ pushAssistantTurn(history, responseText, reasonBuf, artifactBuf);
1087
+ history.push({
1088
+ role: "user",
1089
+ content:
1090
+ `Your last reply was plain text instead of a JSON tool call, and completion cannot finish yet (${preCheck}). ` +
1091
+ `Do NOT apologize. Reply with ONLY the next JSON tool call: {"tool":"<name>","arguments":{...}}.`,
1092
+ });
1093
+ step++;
1094
+ continue;
1095
+ }
1096
+ }
1049
1097
  salvagedAnswer = true;
1050
- invocation = { tool: "done", arguments: { reason: stripLeakedReasoningTags(trimmed) || trimmed } };
1098
+ invocation = { tool: "done", arguments: { reason: candidateReason } };
1051
1099
  } else {
1052
1100
  ev.onAssistant?.(responseText, null);
1053
1101
  pushAssistantTurn(history, responseText, reasonBuf, artifactBuf);
@@ -1187,8 +1235,10 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1187
1235
  // coarse target" and let `budget.record` fall back to the exact-args signature
1188
1236
  // instead (unchanged legacy novelty for tools this helper doesn't specifically know).
1189
1237
  const callTargets = toolCalls.map(c => {
1238
+ if ((c.tool || "").toLowerCase() === "task") return undefined;
1190
1239
  const t = toolTarget(c.tool, c.arguments);
1191
- return t.toLowerCase() === (c.tool || "").toLowerCase() ? undefined : t;
1240
+ if (t.toLowerCase() === (c.tool || "").toLowerCase()) return undefined;
1241
+ return verificationCycles > 0 ? `${t}@v${verificationCycles}` : t;
1192
1242
  });
1193
1243
 
1194
1244
 
@@ -1484,9 +1534,11 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1484
1534
  const command = String(toolCalls[i].arguments?.command ?? toolCalls[i].arguments?.cmd ?? "");
1485
1535
  const verificationCall = t === "bash" && isVerificationSignal(command);
1486
1536
  if (verificationCall) {
1537
+ if (sawMutation && verificationCycles < 4) verificationCycles++;
1487
1538
  if (results[i].success && verificationProducedEvidence(command, String(results[i].output ?? ""))) {
1488
1539
  sawVerification = true;
1489
1540
  lastVerificationSeq = ++mutVerSeq;
1541
+ if (!pendingHookFailure) doneCorrections = 0;
1490
1542
  } else {
1491
1543
  sawVerification = false;
1492
1544
  if (results[i].mutationMayHaveOccurred === true) {
@@ -1496,6 +1548,9 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1496
1548
  }
1497
1549
  continue;
1498
1550
  }
1551
+ if (t === "todo" && results[i].success && doneCorrections > 0 && (!sawMutation || (sawVerification && lastMutationSeq < lastVerificationSeq)) && !pendingHookFailure) {
1552
+ doneCorrections = 0;
1553
+ }
1499
1554
 
1500
1555
  const output = String(results[i].output ?? "").trim();
1501
1556
  const noOpMutation = results[i].success && (
@@ -21,6 +21,7 @@ export function withSandboxExecution<T>(scope: SandboxExecutionScope, run: () =>
21
21
 
22
22
  import * as fs from "node:fs/promises";
23
23
  import * as path from "node:path";
24
+ import { resolvePosixTool } from "../util/posix-tools";
24
25
  import { readGlobalConfig } from "./state";
25
26
  import type { HookConfig } from "./state";
26
27
 
@@ -134,7 +135,7 @@ async function runHookCommand(
134
135
  };
135
136
  }
136
137
 
137
- const proc = Bun.spawn(["bash", "-c", runCmd], {
138
+ const proc = Bun.spawn([resolvePosixTool("bash"), "-c", runCmd], {
138
139
  cwd,
139
140
  stdin: "pipe",
140
141
  stdout: "pipe",
@@ -72,20 +72,31 @@ export const GUARD_LIMITS = Object.freeze({
72
72
  * Recognized standalone verification commands. Matching the full command prevents
73
73
  * filenames, prose, and shell output from impersonating verification evidence.
74
74
  */
75
- export const VERIFY_SIGNAL_RE = /^(?:(?:bun|npm|pnpm|yarn)\s+(?:(?:run|exec|x|dlx)\s+)?(?:test|tests|typecheck|lint|build|check|spec|tsc|eslint|vitest|jest)(?::[\w-]+)?|(?:npx|bunx)\s+(?:tsc|eslint|vitest|jest|pytest)|(?:tsc|eslint|vitest|jest|pytest)|(?:python(?:3(?:\.\d+)?)?|uv)\s+(?:-m\s+)?pytest|cargo\s+(?:test|check|build|clippy)|go\s+(?:test|vet|build)|(?:make|just)\s+(?:test|check|lint|build|typecheck))(?:\s+[A-Za-z0-9_./:@+*?,=-]+)*$/i;
75
+ export const VERIFY_SIGNAL_RE = /^(?:(?:bun|npm|pnpm|yarn)\s+(?:(?:run|exec|x|dlx)\s+)?(?:test|tests|typecheck|lint|build|check|spec|tsc|eslint|vitest|jest)(?::[\w-]+)?|(?:npx|bunx)\s+(?:tsc|eslint|vitest|jest|pytest)|(?:tsc|eslint|vitest|jest|pytest)|(?:python(?:3(?:\.\d+)?)?|uv)\s+(?:(?:-m\s+)?pytest|-m\s+(?:unittest|py_compile|mypy|flake8|ruff\s+check))|node\s+(?:--test|--check|-c)|deno\s+(?:test|check|lint)|cargo\s+(?:test|check|build|clippy)|go\s+(?:test|vet|build)|(?:make|just)\s+(?:test|check|lint|build|typecheck))(?:\s+[A-Za-z0-9_./:@+*?,=-]+)*$/i;
76
76
  const NONEXECUTING_VERIFY_FLAG_RE = /(?:^|\s)(?:--help|-h|--version|-version|--list(?:-tests|Tests)?|-list|--collect-only|--co|--show-?config|--init|--dry-run|--no-run|--passWithNoTests|--if-present)(?=$|\s|=)/i;
77
77
  const MUTATING_VERIFY_FLAG_RE = /(?:^|\s)(?:--fix|--write|--update|-u|--update[-_]snapshots?|--updateSnapshots?|--snapshot[-_]updates?)(?=$|\s|=)/i;
78
78
  const MUTATING_VERIFY_SCRIPT_RE = /^(?:bun|npm|pnpm|yarn)\s+(?:(?:run|exec|x|dlx)\s+)?[\w-]+:[^\s]*(?:fix|update|write|deploy|publish)[^\s]*/i;
79
79
 
80
+ function isSingleVerificationSegment(segment: string): boolean {
81
+ const command = segment.trim();
82
+ if (!command) return false;
83
+ const shortNoOp = /^(?:(?:npx|bunx|bun|npm|pnpm|yarn)\s+(?:(?:run|exec|x|dlx)\s+)?)?tsc\b.*(?:^|\s)-[vV](?=\s|$)/.test(command)
84
+ || /^(?:make|just|go)\s+.*(?:^|\s)-n(?=\s|$)/.test(command);
85
+ return !shortNoOp && !/[;&|<>`$(){}\n\r]/.test(command) && VERIFY_SIGNAL_RE.test(command) && !NONEXECUTING_VERIFY_FLAG_RE.test(command) && !MUTATING_VERIFY_FLAG_RE.test(command) && !MUTATING_VERIFY_SCRIPT_RE.test(command);
86
+ }
87
+
80
88
  /**
81
- * True only when the command itself is a recognized verifier invocation. Output
89
+ * True only when the command itself is a recognized verifier invocation (or a
90
+ * pure `&&` chain where every segment is a recognized verifier invocation). Output
82
91
  * is never evidence: arbitrary commands can print words such as "tests passed".
83
92
  */
84
93
  export function isVerificationSignal(cmd: string): boolean {
85
94
  const command = cmd.trim();
86
- const shortNoOp = /^(?:(?:npx|bunx|bun|npm|pnpm|yarn)\s+(?:(?:run|exec|x|dlx)\s+)?)?tsc\b.*(?:^|\s)-[vV](?=\s|$)/.test(command)
87
- || /^(?:make|just|go)\s+.*(?:^|\s)-n(?=\s|$)/.test(command);
88
- return !shortNoOp && !/[;&|<>`$(){}\n\r]/.test(command) && VERIFY_SIGNAL_RE.test(command) && !NONEXECUTING_VERIFY_FLAG_RE.test(command) && !MUTATING_VERIFY_FLAG_RE.test(command) && !MUTATING_VERIFY_SCRIPT_RE.test(command);
95
+ if (!command) return false;
96
+ const withoutAndChain = command.replace(/&&/g, " ");
97
+ if (/[;&|<>`$(){}\n\r]/.test(withoutAndChain)) return false;
98
+ const segments = command.split("&&").map(s => s.trim());
99
+ return segments.length > 0 && segments.every(isSingleVerificationSegment);
89
100
  }
90
101
 
91
102
  /** Require test-run evidence for Go and Cargo's recognized text output. */
@@ -181,7 +192,7 @@ export function classifyDoneGate(input: DoneGateInput): DoneGateVerdict {
181
192
  block: true,
182
193
  message:
183
194
  "You verified earlier, but then modified files again — your last passing test/build no longer reflects the current tree. " +
184
- "Re-run the narrowest verification command against the latest changes, then call done.",
195
+ "Re-run the narrowest verification command against the latest changes (e.g. a standalone bash call such as 'bun test', 'bun run typecheck', 'tsc --noEmit', 'npm test', 'pytest', 'cargo test', or 'go test ./...'), then call done.",
185
196
  };
186
197
  }
187
198
  return {
@@ -189,6 +200,6 @@ export function classifyDoneGate(input: DoneGateInput): DoneGateVerdict {
189
200
  block: true,
190
201
  message:
191
202
  "You modified files this turn but have no passing verification (no test/build/typecheck command succeeded, or the latest recognized verification failed). " +
192
- "Run the narrowest command that proves your change works, then call done.",
203
+ "Run the narrowest command that proves your change works (e.g. a standalone bash call such as 'bun test', 'bun run typecheck', 'tsc --noEmit', 'npm test', 'pytest', 'cargo test', or 'go test ./...'), then call done.",
193
204
  };
194
205
  }