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.
@@ -162,20 +162,29 @@ export function createSandboxSupervisor(options: { policy: SandboxPolicy; backen
162
162
  return effect.result.promise;
163
163
  }
164
164
 
165
+ /** A Windows host path that matches no mount is not "relative" to the Linux guest; it must never be forwarded. */
165
166
  function guestPath(value: string): string {
166
- if (!path.isAbsolute(value)) return value;
167
- const mount = [...policy.mounts].sort((a, b) => b.host.length - a.host.length).find(item => sandboxPathContains(item.host, value));
168
- if (!mount) return value;
169
- const relative = path.relative(mount.host, value).split(path.sep).join("/");
170
- return relative ? `${mount.guest}/${relative}` : mount.guest;
167
+ const mount = path.isAbsolute(value) ? [...policy.mounts].sort((a, b) => b.host.length - a.host.length).find(item => sandboxPathContains(item.host, value)) : undefined;
168
+ if (mount) {
169
+ const relative = path.relative(mount.host, value).split(path.sep).join("/");
170
+ return relative ? `${mount.guest}/${relative}` : mount.guest;
171
+ }
172
+ if (process.platform === "win32" && /^(?:[A-Za-z]:|[\\/]{2}|\\)/.test(value)) throw new Error("SANDBOX_POLICY: host path is outside approved mounts");
173
+ return value;
171
174
  }
172
175
 
173
176
  function call(tool: string, args: Record<string, unknown>, callOptions?: SandboxCallOptions): Promise<ToolResult> {
174
177
  const mapped = { ...args };
175
- for (const key of ["filePath", "path", "dirPath", "dir", "targetPath", "cwd", "subdir"]) {
176
- if (typeof mapped[key] === "string") mapped[key] = guestPath(mapped[key]);
178
+ let cwd: string | undefined;
179
+ try {
180
+ for (const key of ["filePath", "path", "dirPath", "dir", "targetPath", "cwd", "subdir"]) {
181
+ if (typeof mapped[key] === "string") mapped[key] = guestPath(mapped[key]);
182
+ }
183
+ if (callOptions?.cwd) cwd = guestPath(callOptions.cwd);
184
+ } catch (error) {
185
+ return Promise.resolve({ success: false, output: "", error: error instanceof Error ? error.message : String(error) });
177
186
  }
178
- return enqueue({ id: crypto.randomUUID(), type: "tool", tool: tool === "approve" ? "__plan_approve" : tool, args: mapped, ...(callOptions?.cwd ? { cwd: guestPath(callOptions.cwd) } : {}) }, callOptions);
187
+ return enqueue({ id: crypto.randomUUID(), type: "tool", tool: tool === "approve" ? "__plan_approve" : tool, args: mapped, ...(cwd ? { cwd } : {}) }, callOptions);
179
188
  }
180
189
 
181
190
  return {
@@ -184,7 +193,10 @@ export function createSandboxSupervisor(options: { policy: SandboxPolicy; backen
184
193
  return Object.fromEntries(names.map(name => [name, (args: Record<string, unknown>, cwd: string, onProgress?: (text: string) => void) => call(name, args, { cwd, onProgress })]));
185
194
  },
186
195
  exec(argv: readonly string[], execOptions: SandboxExecOptions = {}): Promise<SandboxExecResult> {
187
- return enqueue({ id: crypto.randomUUID(), type: "exec", argv: [...argv], ...(execOptions.cwd ? { cwd: guestPath(execOptions.cwd) } : {}), ...(execOptions.env ? { env: execOptions.env } : {}), ...(execOptions.stdin !== undefined ? { stdin: execOptions.stdin } : {}) }, { signal: execOptions.signal });
196
+ let cwd: string | undefined;
197
+ try { if (execOptions.cwd) cwd = guestPath(execOptions.cwd); }
198
+ catch (error) { return Promise.resolve({ success: false, output: "", error: error instanceof Error ? error.message : String(error) }); }
199
+ return enqueue({ id: crypto.randomUUID(), type: "exec", argv: [...argv], ...(cwd ? { cwd } : {}), ...(execOptions.env ? { env: execOptions.env } : {}), ...(execOptions.stdin !== undefined ? { stdin: execOptions.stdin } : {}) }, { signal: execOptions.signal });
188
200
  },
189
201
  async replacePolicy(next): Promise<void> {
190
202
  if (state === "closed" || state === "changing" || next.sessionKey !== policy.sessionKey || next.revision <= policy.revision) throw new Error("SANDBOX_POLICY: closed, changing or invalid policy revision");
@@ -33,6 +33,7 @@
33
33
  */
34
34
  import * as cp from "node:child_process";
35
35
  import * as fs from "node:fs";
36
+ import { resolvePosixTool } from "../util/posix-tools";
36
37
  import * as os from "node:os";
37
38
  import * as path from "node:path";
38
39
  import { Readable } from "node:stream";
@@ -96,7 +97,7 @@ export function spawnLeasedShell(command: string, cwd: string): LeasedShell {
96
97
  }
97
98
 
98
99
  function spawnDirect(command: string, cwd: string): LeasedShell {
99
- const proc = Bun.spawn(["bash", "-c", command], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
100
+ const proc = Bun.spawn([resolvePosixTool("bash"), "-c", command], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
100
101
  const stdout = proc.stdout.getReader();
101
102
  const stderr = proc.stderr.getReader();
102
103
  const reaped = proc.exited.then(() => undefined);
@@ -467,6 +467,7 @@ export async function runSubagentOnce(
467
467
  const failoverEnabled = !!opts.config.routing?.enabled;
468
468
  let activeModel = initialModel;
469
469
  emit({ role: role.id, kind: "start", detail: taskText, maxSteps, model: activeModel });
470
+ let contractCorrections = 0;
470
471
  const runOnce = () => {
471
472
  return runAgentLoop(history, {
472
473
  cwd,
@@ -474,14 +475,27 @@ export async function runSubagentOnce(
474
475
  maxSteps,
475
476
  maxTokens: resolveMaxOutputTokens(activeModel, thinking),
476
477
  sessionKey,
477
- // Bounded delegation: a subagent's step contract stays exact — the parent
478
- // owns any retry/extension decision, so the gjc retry flow is disabled here.
479
- budget: { maxExtensions: 0 },
478
+ // Allow bounded step-budget extensions when a subagent is actively making
479
+ // progress so multi-file edits + test verification do not get cut off mid-run.
480
+ budget: { maxExtensions: role.readOnly ? 1 : 2 },
480
481
  signal,
481
482
  steer,
482
483
  tools: subagentToolset(role),
483
484
  ...(failoverEnabled ? { rateLimitFallbackAvailable, safetyFallbackAvailable } : {}),
484
485
  events: {
486
+ onBeforeDone: reason => {
487
+ const hasToolActivity = fileMutations > 0 || bashRuns > 0 || readOnlyEvidenceCalls > 0;
488
+ if (!hasToolActivity || contractCorrections >= 1) return null;
489
+ const check = validateSubagentDoneReason(role, reason);
490
+ if (!check.ok) {
491
+ contractCorrections++;
492
+ return (
493
+ `Your done.reason is missing required section(s) for ${role.title}: ${check.missing?.join(", ")}. ` +
494
+ `Call done again with non-empty content under every required section header.`
495
+ );
496
+ }
497
+ return null;
498
+ },
485
499
  onEvidence: evidence => {
486
500
  if (evidence.sawMutation) mutationMayHaveOccurred = true;
487
501
  },
@@ -1,4 +1,5 @@
1
1
  import { applyBashFixups } from "./bash-fixups";
2
+ import { resolvePosixTool } from "../util/posix-tools";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
4
5
  import { readWorkflowStateStrict, type WorkflowState } from "./state";
@@ -727,7 +728,7 @@ export async function bashTool(
727
728
  const reapBackground = isBackgroundReapEnabled();
728
729
  const grouped = reapBackground && process.platform !== "win32";
729
730
  if (grouped) ensureBackgroundSweeper();
730
- const proc = Bun.spawn(["bash", "-c", command], {
731
+ const proc = Bun.spawn([resolvePosixTool("bash"), "-c", command], {
731
732
  cwd: runCwd,
732
733
  // Detach stdin (no inherited TTY): a command that reads stdin (`cat`, `read`,
733
734
  // a REPL, an interactive prompt) then gets immediate EOF and exits instead of
@@ -439,7 +439,7 @@ export function captureLivePromptInputChunk(
439
439
  const plain = start === -1 ? rest : rest.slice(0, start);
440
440
  if (start !== -1) state.inPaste = true;
441
441
  rest = start === -1 ? "" : rest.slice(start + PASTE_START.length);
442
- if (feedLivePromptSegment(state, plain)) accepted = true;
442
+ if (feedLivePromptSegment(state, decodeExtendedKeys(stripMouseReports(plain)))) accepted = true;
443
443
  }
444
444
  }
445
445
  return accepted;
@@ -506,10 +506,17 @@ export function createInFlightAbortHarness(opts: AbortHarnessOptions = {}): InFl
506
506
  opts.onBufferedInput?.(raw);
507
507
  return;
508
508
  }
509
+ // Strip mouse reports and terminal capability replies (OSC/CSI device attributes)
510
+ // first so terminal responses arriving mid-turn never leak into the live draft.
511
+ const stripped = stripMouseReports(raw);
512
+ if (!stripped && raw.length > 0) {
513
+ opts.onNoise?.();
514
+ return;
515
+ }
509
516
  // Kitty/modifyOtherKeys re-encodings → legacy bytes FIRST, so Esc (`CSI 27u`) and
510
517
  // Ctrl+C (`CSI 99;5u` / `CSI 27;5;99~`) cancel a live turn on every terminal —
511
518
  // previously they fell into the "noise" branch and did NOTHING mid-turn.
512
- const text = decodeExtendedKeys(raw);
519
+ const text = decodeExtendedKeys(stripped);
513
520
  if (text === "\u000f") {
514
521
  opts.onDetailKey?.();
515
522
  return;
@@ -579,7 +586,22 @@ export function classifyMidTurnLine(line: string): "command" | "steer" | "empty"
579
586
  // A lone sigil ("/" or "$") has no command name — ignore it instead of aborting the
580
587
  // running turn to dispatch an empty command (a stray slash should not interrupt work).
581
588
  if (t === "/" || t === "$") return "empty";
582
- return /^[/$]/.test(t) ? "command" : "steer";
589
+ const firstToken = t.split(/\s+/, 1)[0] ?? "";
590
+ if (firstToken.startsWith("/")) {
591
+ // A slash command is a single leading token (/model, /help, /?); a token containing
592
+ // additional slashes (/Users/foo/bar, /tmp/a.ts, //comment) is a path or comment,
593
+ // never a REPL command that should abort the running turn.
594
+ const name = firstToken.slice(1);
595
+ if (!name || name.includes("/")) return "steer";
596
+ return /^[a-zA-Z?][a-zA-Z0-9_-]*$/.test(name) ? "command" : "steer";
597
+ }
598
+ if (firstToken.startsWith("$")) {
599
+ // A skill invocation starts with a skill identifier ($team, $ralplan), not a
600
+ // numeric dollar amount ($100) or path ($/foo).
601
+ const name = firstToken.slice(1);
602
+ return /^[a-zA-Z][a-zA-Z0-9_:-]*$/.test(name) ? "command" : "steer";
603
+ }
604
+ return "steer";
583
605
  }
584
606
  /** Default window (ms) over which the several delivery paths of ONE physical Ctrl+C
585
607
  * press — readline 'keypress', the rl/process 'SIGINT' event, and the raw \u0003 stdin
@@ -1967,7 +1967,7 @@ async function runLaunchSession(args: string[], registerEndpointCleanup?: (clean
1967
1967
  if ((pendingMutationVerification || evidence.sawMutation) && !(evidence.sawVerification && !evidence.verificationStale)) {
1968
1968
  return (
1969
1969
  `Cannot finish: a workspace mutation from this session still lacks a successful, non-stale verification. ` +
1970
- `Run a relevant test/build/typecheck/lint after the last change, then call done again.`
1970
+ `Run a relevant test/build/typecheck/lint after the last change (e.g. 'bun test', 'bun run typecheck', 'tsc --noEmit', 'npm test', 'pytest', 'cargo test', or 'go test ./...'), then call done again.`
1971
1971
  );
1972
1972
  }
1973
1973
 
@@ -3177,7 +3177,14 @@ async function runLaunchSession(args: string[], registerEndpointCleanup?: (clean
3177
3177
  queueBusySnapshot = () => ({
3178
3178
  text: queuedPromptInput.partial,
3179
3179
  });
3180
- queueBusyClear = () => { queuedPromptInput.partial = ""; };
3180
+ queueBusyClear = () => {
3181
+ queuedPromptInput.partial = "";
3182
+ const r = rl as unknown as { line?: string; cursor?: number };
3183
+ if (typeof r.line === "string") {
3184
+ r.line = "";
3185
+ r.cursor = 0;
3186
+ }
3187
+ };
3181
3188
  queueBusyCommand = (line: string) => {
3182
3189
  // NO_MIDTURN_DISPATCH=1 keeps the legacy prefill path (tee up, press Enter); the
3183
3190
  // default routes to the immediate-dispatch queue served at the top of the loop.
@@ -80,15 +80,28 @@ export async function runConsensusCriticGate(args: {
80
80
  // is gated the SAME way: zero observed read/search/find/ast_grep/lsp calls means
81
81
  // the [OKAY]/[ITERATE]/[REJECT] text is unevidenced regardless of what it claims.
82
82
  let evidenceCalls = 0;
83
+ let contractCorrections = 0;
83
84
  const runOnce = () => runAgentLoop(history, {
84
85
  cwd: args.cwd,
85
86
  model: activeModel,
86
87
  maxSteps,
87
- budget: { maxExtensions: 0 },
88
+ budget: { maxExtensions: 1 },
88
89
  signal: args.signal,
89
90
  tools: subagentToolset(role),
90
91
  rateLimitFallbackAvailable,
91
92
  events: {
93
+ onBeforeDone: reason => {
94
+ if (evidenceCalls === 0 || contractCorrections >= 1) return null;
95
+ const check = validateSubagentDoneReason(role, reason);
96
+ if (!check.ok) {
97
+ contractCorrections++;
98
+ return (
99
+ `Your critic report is missing required section(s): ${check.missing?.join(", ")}. ` +
100
+ `Call done again starting with [OKAY], [ITERATE], or [REJECT] and include a non-empty 'Justification:' section.`
101
+ );
102
+ }
103
+ return null;
104
+ },
92
105
  onToolResult: (tool, success) => {
93
106
  if (success && (tool === "read" || tool === "search" || tool === "find" || tool === "ast_grep" || tool === "lsp")) evidenceCalls++;
94
107
  },
@@ -23,7 +23,7 @@ Bundled system prompts, role definitions, and workflow skill documentation for t
23
23
  - Do not remove existing constraints without explicit reason.
24
24
 
25
25
  ### Testing Requirements
26
- - Prompt changes should be verified via end-to-end testing or `bun run run-evolution.sh`.
26
+ - Prompt changes should be verified via end-to-end testing.
27
27
 
28
28
  ### Common Patterns
29
29
  - XML-like tags for structural organization within prompts.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * POSIX tool resolution for Windows hosts.
3
+ *
4
+ * `Bun.spawn(["bash"|"grep"|"find", …])` resolves from PATH, where Windows
5
+ * ships imposters ahead of Git for Windows:
6
+ * - `C:\Windows\System32\bash.exe` launches WSL — Windows paths
7
+ * (`C:\Users\…`) do not exist inside it and caller env vars are dropped.
8
+ * - `C:\Windows\System32\find.exe` is the legacy DOS FIND (substring
9
+ * filter), not POSIX find.
10
+ *
11
+ * Resolve to real Git for Windows binaries instead, with an env override.
12
+ * Non-Windows platforms pass the bare name through unchanged.
13
+ */
14
+ import { existsSync } from "node:fs";
15
+ import path from "node:path";
16
+
17
+ export type PosixTool = "bash" | "grep" | "find";
18
+
19
+ export interface ResolveOptions {
20
+ /** Platform override for tests (default: process.platform). */
21
+ readonly platform?: NodeJS.Platform;
22
+ /** Env override for tests (default: process.env). */
23
+ readonly env?: Record<string, string | undefined>;
24
+ /** File-existence probe override for tests (default: fs.existsSync). */
25
+ readonly exists?: (p: string) => boolean;
26
+ }
27
+
28
+ /** Candidate absolute paths for a tool, in preference order. */
29
+ export function candidatePaths(
30
+ tool: PosixTool,
31
+ env: Record<string, string | undefined>
32
+ ): string[] {
33
+ const override = env[`JOC_${tool.toUpperCase()}_PATH`];
34
+ const roots = [
35
+ env["ProgramFiles"] && path.join(env["ProgramFiles"], "Git"),
36
+ env["ProgramFiles(x86)"] && path.join(env["ProgramFiles(x86)"], "Git"),
37
+ env["LOCALAPPDATA"] && path.join(env["LOCALAPPDATA"], "Programs", "Git"),
38
+ ].filter((r): r is string => Boolean(r));
39
+ // bash lives in both bin/ and usr/bin/; grep/find only in usr/bin/.
40
+ const subdirs = tool === "bash" ? ["bin", path.join("usr", "bin")] : [path.join("usr", "bin")];
41
+ return [
42
+ ...(override ? [override] : []),
43
+ ...roots.flatMap(root => subdirs.map(sub => path.join(root, sub, `${tool}.exe`))),
44
+ ];
45
+ }
46
+
47
+ const cache = new Map<PosixTool, string>();
48
+
49
+ /**
50
+ * Resolve `tool` to a Unix-semantics executable. Returns an absolute Git
51
+ * for Windows path when found, otherwise the bare name (PATH fallback).
52
+ */
53
+ export function resolvePosixTool(tool: PosixTool, opts: ResolveOptions = {}): string {
54
+ const platform = opts.platform ?? process.platform;
55
+ if (platform !== "win32") return tool;
56
+ const injected = opts.env || opts.exists;
57
+ if (!injected) {
58
+ const hit = cache.get(tool);
59
+ if (hit) return hit;
60
+ }
61
+ const env = opts.env ?? process.env;
62
+ const exists = opts.exists ?? existsSync;
63
+ const found = candidatePaths(tool, env).find(c => exists(c)) ?? tool;
64
+ if (!injected) cache.set(tool, found);
65
+ return found;
66
+ }