reasonix 0.24.1 → 0.25.1

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/dist/index.js CHANGED
@@ -47,8 +47,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
47
47
  }
48
48
  function sleep(ms, signal) {
49
49
  if (ms <= 0) return Promise.resolve();
50
- return new Promise((resolve9, reject) => {
51
- const timer = setTimeout(resolve9, ms);
50
+ return new Promise((resolve10, reject) => {
51
+ const timer = setTimeout(resolve10, ms);
52
52
  if (signal) {
53
53
  const onAbort = () => {
54
54
  clearTimeout(timer);
@@ -523,7 +523,7 @@ function matchesTool(hook, toolName) {
523
523
  }
524
524
  var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
525
525
  function defaultSpawner(input) {
526
- return new Promise((resolve9) => {
526
+ return new Promise((resolve10) => {
527
527
  const child = spawn(input.command, {
528
528
  cwd: input.cwd,
529
529
  shell: true,
@@ -568,7 +568,7 @@ function defaultSpawner(input) {
568
568
  child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
569
569
  child.once("error", (err) => {
570
570
  clearTimeout(timer);
571
- resolve9({
571
+ resolve10({
572
572
  exitCode: null,
573
573
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
574
574
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
@@ -579,7 +579,7 @@ function defaultSpawner(input) {
579
579
  });
580
580
  child.once("close", (code) => {
581
581
  clearTimeout(timer);
582
- resolve9({
582
+ resolve10({
583
583
  exitCode: code,
584
584
  stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
585
585
  stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
@@ -2310,8 +2310,8 @@ var CacheFirstLoop = class {
2310
2310
  }
2311
2311
  );
2312
2312
  for (let k = 0; k < budget; k++) {
2313
- const sample = queue.shift() ?? await new Promise((resolve9) => {
2314
- waiter = resolve9;
2313
+ const sample = queue.shift() ?? await new Promise((resolve10) => {
2314
+ waiter = resolve10;
2315
2315
  });
2316
2316
  yield {
2317
2317
  turn: this._turn,
@@ -4926,33 +4926,6 @@ var PlanProposedError = class extends Error {
4926
4926
  return payload;
4927
4927
  }
4928
4928
  };
4929
- var PlanCheckpointError = class extends Error {
4930
- stepId;
4931
- title;
4932
- result;
4933
- notes;
4934
- constructor(update) {
4935
- super(
4936
- "PlanCheckpointError: step complete \u2014 STOP calling tools. The TUI has paused the plan for user review. Wait for the next user message; it will either say continue (proceed to the next step), request a revision (adjust the remaining plan), or stop (summarize and end)."
4937
- );
4938
- this.name = "PlanCheckpointError";
4939
- this.stepId = update.stepId;
4940
- this.title = update.title;
4941
- this.result = update.result;
4942
- this.notes = update.notes;
4943
- }
4944
- toToolResult() {
4945
- const payload = {
4946
- error: `${this.name}: ${this.message}`,
4947
- kind: "step_completed",
4948
- stepId: this.stepId,
4949
- result: this.result
4950
- };
4951
- if (this.title) payload.title = this.title;
4952
- if (this.notes) payload.notes = this.notes;
4953
- return payload;
4954
- }
4955
- };
4956
4929
  var PlanRevisionProposedError = class extends Error {
4957
4930
  reason;
4958
4931
  remainingSteps;
@@ -4979,7 +4952,7 @@ var PlanRevisionProposedError = class extends Error {
4979
4952
 
4980
4953
  // src/tools/plan-core.ts
4981
4954
  var SUBMIT_PLAN_DESCRIPTION = "Submit ONE concrete plan you've already decided on. Use this for tasks that warrant a review gate \u2014 multi-file refactors, architecture changes, anything that would be expensive or confusing to undo. Skip it for small fixes (one-line typo, obvious bug with a clear fix) \u2014 just make the change. The user will either approve (you then implement it), ask for refinement, or cancel. If the user has already enabled /plan mode, writes are blocked at dispatch and you MUST use this. CRITICAL: do NOT use submit_plan to present alternative routes (A/B/C, option 1/2/3) for the user to pick from \u2014 the picker only exposes approve/refine/cancel, so a menu plan strands the user with no way to choose. For branching decisions, call `ask_choice` instead; only call submit_plan once the user has picked a direction and you have a single actionable plan. Write the plan as markdown with a one-line summary, a bulleted list of files to touch and what will change, and any risks or open questions. STRONGLY PREFERRED: pass `steps` \u2014 an array of {id, title, action, risk?} \u2014 so the UI renders a structured step list above the approval picker and tracks per-step progress. Use risk='high' for steps that touch prod data / break public APIs / are hard to undo; 'med' for non-trivial but reversible (multi-file edits, schema tweaks); 'low' for safe local work. After each step, call `mark_step_complete` so the user sees progress ticks.";
4982
- var MARK_STEP_COMPLETE_DESCRIPTION = "Mark one step of the approved plan as done AND pause for the user to review. Call this after finishing each step. The TUI shows a \u2713 progress row and mounts a Continue / Revise / Stop picker \u2014 you MUST stop calling tools after this fires and wait for the next user message. Pass the `stepId` from the plan's steps array, a short `result` (what you did), and optional `notes` for anything surprising (errors, scope changes, follow-ups). This tool doesn't change any files. Don't call it if the plan didn't include structured steps, and don't invent ids that weren't in the original plan.";
4955
+ var MARK_STEP_COMPLETE_DESCRIPTION = "Mark one step of the approved plan as done. Call this after finishing each step, then immediately continue with the NEXT step \u2014 do not stop or wait for the user. The TUI updates the plan card's progress in place. After the FINAL step, write a brief reply summarizing what was done and end the turn. Pass the `stepId` from the plan's steps array, a short `result` (what you did), and optional `notes` for anything surprising (errors, scope changes, follow-ups). This tool doesn't change any files. Don't call it if the plan didn't include structured steps, and don't invent ids that weren't in the original plan.";
4983
4956
  var REVISE_PLAN_DESCRIPTION = "Surgically replace the REMAINING steps of an in-flight plan. Call this when the user has given feedback at a checkpoint that warrants a structured plan change \u2014 skip a step, swap two steps, add a new step, change risk, etc. Pass: `reason` (one sentence why), `remainingSteps` (the new tail of the plan, replacing whatever steps haven't been done yet), and optional `summary` (updated one-line plan summary). Done steps are NEVER touched \u2014 keep them out of `remainingSteps`. The TUI shows a diff (removed in red, kept in gray, added in green) and the user accepts or rejects. Don't call this for trivial mid-step adjustments \u2014 just keep executing. Don't call submit_plan for revisions either \u2014 that resets the whole plan including completed steps. Use submit_plan only when the entire approach has changed; use revise_plan when the tail needs editing.";
4984
4957
  var STEP_ITEM_SCHEMA = {
4985
4958
  type: "object",
@@ -5096,7 +5069,7 @@ function registerMarkStepComplete(registry, opts) {
5096
5069
  if (title) update.title = title;
5097
5070
  if (notes) update.notes = notes;
5098
5071
  opts.onStepCompleted?.(update);
5099
- throw new PlanCheckpointError({ stepId, title, result, notes });
5072
+ return update;
5100
5073
  }
5101
5074
  });
5102
5075
  }
@@ -5394,9 +5367,9 @@ function forkRegistryExcluding(parent, exclude) {
5394
5367
  }
5395
5368
 
5396
5369
  // src/tools/shell.ts
5397
- import { spawn as spawn3, spawnSync } from "child_process";
5370
+ import { spawn as spawn4, spawnSync } from "child_process";
5398
5371
  import { existsSync as existsSync8, statSync as statSync4 } from "fs";
5399
- import * as pathMod3 from "path";
5372
+ import * as pathMod4 from "path";
5400
5373
 
5401
5374
  // src/tools/jobs.ts
5402
5375
  import { spawn as spawn2 } from "child_process";
@@ -5686,6 +5659,417 @@ function snapshot(job) {
5686
5659
  };
5687
5660
  }
5688
5661
 
5662
+ // src/tools/shell-chain.ts
5663
+ import { spawn as spawn3 } from "child_process";
5664
+ import { closeSync, openSync } from "fs";
5665
+ import * as pathMod3 from "path";
5666
+ var UnsupportedSyntaxError = class extends Error {
5667
+ constructor(detail) {
5668
+ super(`run_command: ${detail}`);
5669
+ this.name = "UnsupportedSyntaxError";
5670
+ }
5671
+ };
5672
+ function splitOnChainOps(cmd) {
5673
+ const segs = [];
5674
+ const ops = [];
5675
+ let segStart = 0;
5676
+ let i = 0;
5677
+ let quote = null;
5678
+ let atTokenStart = true;
5679
+ while (i < cmd.length) {
5680
+ const ch = cmd[i];
5681
+ if (quote) {
5682
+ if (ch === quote) quote = null;
5683
+ else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) i++;
5684
+ i++;
5685
+ atTokenStart = false;
5686
+ continue;
5687
+ }
5688
+ if (ch === '"' || ch === "'") {
5689
+ quote = ch;
5690
+ i++;
5691
+ atTokenStart = false;
5692
+ continue;
5693
+ }
5694
+ if (ch === " " || ch === " ") {
5695
+ i++;
5696
+ atTokenStart = true;
5697
+ continue;
5698
+ }
5699
+ if (atTokenStart) {
5700
+ let op = null;
5701
+ let opLen = 0;
5702
+ const next = cmd[i + 1];
5703
+ if (ch === "|" && next === "|") {
5704
+ op = "||";
5705
+ opLen = 2;
5706
+ } else if (ch === "&" && next === "&") {
5707
+ op = "&&";
5708
+ opLen = 2;
5709
+ } else if (ch === "|") {
5710
+ op = "|";
5711
+ opLen = 1;
5712
+ } else if (ch === ";") {
5713
+ op = ";";
5714
+ opLen = 1;
5715
+ }
5716
+ if (op !== null) {
5717
+ segs.push(cmd.slice(segStart, i));
5718
+ ops.push(op);
5719
+ i += opLen;
5720
+ segStart = i;
5721
+ atTokenStart = true;
5722
+ continue;
5723
+ }
5724
+ }
5725
+ i++;
5726
+ atTokenStart = false;
5727
+ }
5728
+ segs.push(cmd.slice(segStart));
5729
+ return { segs, ops };
5730
+ }
5731
+ function parseSegment(segStr) {
5732
+ const argv = [];
5733
+ const redirects = [];
5734
+ let cur = "";
5735
+ let curHasContent = false;
5736
+ let pending = null;
5737
+ let quote = null;
5738
+ const flush = () => {
5739
+ if (!curHasContent && cur.length === 0) return;
5740
+ if (pending) {
5741
+ redirects.push({ kind: pending, target: cur });
5742
+ pending = null;
5743
+ } else {
5744
+ argv.push(cur);
5745
+ }
5746
+ cur = "";
5747
+ curHasContent = false;
5748
+ };
5749
+ let i = 0;
5750
+ while (i < segStr.length) {
5751
+ const ch = segStr[i];
5752
+ if (quote) {
5753
+ if (ch === quote) {
5754
+ quote = null;
5755
+ } else if (ch === "\\" && quote === '"' && i + 1 < segStr.length) {
5756
+ cur += segStr[++i] ?? "";
5757
+ curHasContent = true;
5758
+ } else {
5759
+ cur += ch;
5760
+ curHasContent = true;
5761
+ }
5762
+ i++;
5763
+ continue;
5764
+ }
5765
+ if (ch === '"' || ch === "'") {
5766
+ quote = ch;
5767
+ curHasContent = true;
5768
+ i++;
5769
+ continue;
5770
+ }
5771
+ if (ch === " " || ch === " ") {
5772
+ flush();
5773
+ i++;
5774
+ continue;
5775
+ }
5776
+ if (cur.length === 0 && !curHasContent) {
5777
+ const remaining = segStr.slice(i);
5778
+ let matched = null;
5779
+ if (remaining.startsWith("2>&1")) matched = { op: "2>&1", len: 4 };
5780
+ else if (remaining.startsWith("&>")) matched = { op: "&>", len: 2 };
5781
+ else if (remaining.startsWith("2>>")) matched = { op: "2>>", len: 3 };
5782
+ else if (remaining.startsWith("2>")) matched = { op: "2>", len: 2 };
5783
+ else if (remaining.startsWith(">>")) matched = { op: ">>", len: 2 };
5784
+ else if (remaining.startsWith(">")) matched = { op: ">", len: 1 };
5785
+ else if (remaining.startsWith("<<")) {
5786
+ throw new UnsupportedSyntaxError(
5787
+ `shell operator "<<" is not supported \u2014 heredoc / here-string is not implemented; pass input via a "<" file or the binary's --input flag`
5788
+ );
5789
+ } else if (remaining.startsWith("<")) matched = { op: "<", len: 1 };
5790
+ if (matched) {
5791
+ if (pending !== null) {
5792
+ throw new UnsupportedSyntaxError(
5793
+ `redirect "${pending}" is missing a target file before "${matched.op}"`
5794
+ );
5795
+ }
5796
+ if (matched.op === "2>&1") {
5797
+ redirects.push({ kind: "2>&1", target: "" });
5798
+ } else {
5799
+ pending = matched.op;
5800
+ }
5801
+ i += matched.len;
5802
+ continue;
5803
+ }
5804
+ if (ch === "&") {
5805
+ throw new UnsupportedSyntaxError(
5806
+ 'shell operator "&" is not supported \u2014 background runs need run_background, not run_command. Wrap a literal `&` arg in quotes.'
5807
+ );
5808
+ }
5809
+ }
5810
+ cur += ch;
5811
+ curHasContent = true;
5812
+ i++;
5813
+ }
5814
+ if (quote) throw new Error(`unclosed ${quote} in command`);
5815
+ flush();
5816
+ if (pending) throw new UnsupportedSyntaxError(`redirect "${pending}" is missing a target file`);
5817
+ if (argv.length === 0 && redirects.length > 0) {
5818
+ throw new UnsupportedSyntaxError(
5819
+ "redirect without a command \u2014 segment must have at least one program argument"
5820
+ );
5821
+ }
5822
+ validateRedirectFds(redirects);
5823
+ return { argv, redirects };
5824
+ }
5825
+ function validateRedirectFds(redirects) {
5826
+ let stdin = 0;
5827
+ let stdout = 0;
5828
+ let stderr = 0;
5829
+ for (const r of redirects) {
5830
+ if (r.kind === "<") stdin++;
5831
+ else if (r.kind === ">" || r.kind === ">>") stdout++;
5832
+ else if (r.kind === "2>" || r.kind === "2>>" || r.kind === "2>&1") stderr++;
5833
+ else if (r.kind === "&>") {
5834
+ stdout++;
5835
+ stderr++;
5836
+ }
5837
+ }
5838
+ if (stdin > 1) throw new UnsupportedSyntaxError("multiple `<` stdin redirects in one segment");
5839
+ if (stdout > 1)
5840
+ throw new UnsupportedSyntaxError(
5841
+ "multiple stdout redirects in one segment (`>` / `>>` / `&>` conflict)"
5842
+ );
5843
+ if (stderr > 1)
5844
+ throw new UnsupportedSyntaxError(
5845
+ "multiple stderr redirects in one segment (`2>` / `2>>` / `&>` / `2>&1` conflict)"
5846
+ );
5847
+ }
5848
+ function parseCommandChain(cmd) {
5849
+ const { segs, ops } = splitOnChainOps(cmd);
5850
+ const segments = [];
5851
+ for (let i = 0; i < segs.length; i++) {
5852
+ const trimmed = segs[i].trim();
5853
+ if (trimmed.length === 0) {
5854
+ const op = i === 0 ? ops[0] : ops[i - 1];
5855
+ throw new UnsupportedSyntaxError(
5856
+ i === 0 ? `empty segment before "${op}"` : i === segs.length - 1 ? `chain ends with "${op}"` : `empty segment between "${ops[i - 1]}" and "${ops[i]}"`
5857
+ );
5858
+ }
5859
+ segments.push(parseSegment(trimmed));
5860
+ }
5861
+ if (ops.length === 0 && segments[0].redirects.length === 0) return null;
5862
+ return { segments, ops };
5863
+ }
5864
+ function chainAllowed(chain, isAllowed2) {
5865
+ for (const seg of chain.segments) {
5866
+ if (!isAllowed2(seg.argv.join(" "))) return false;
5867
+ }
5868
+ return true;
5869
+ }
5870
+ function groupChain(chain) {
5871
+ const groups = [{ segments: [chain.segments[0]], opBefore: null }];
5872
+ for (let i = 0; i < chain.ops.length; i++) {
5873
+ const op = chain.ops[i];
5874
+ const next = chain.segments[i + 1];
5875
+ if (op === "|") {
5876
+ groups[groups.length - 1].segments.push(next);
5877
+ } else {
5878
+ groups.push({ segments: [next], opBefore: op });
5879
+ }
5880
+ }
5881
+ return groups;
5882
+ }
5883
+ async function runChain(chain, opts) {
5884
+ const groups = groupChain(chain);
5885
+ const buf = new OutputBuffer(opts.maxOutputChars * 2 * 4);
5886
+ const deadline = Date.now() + opts.timeoutSec * 1e3;
5887
+ let lastExit = 0;
5888
+ let timedOut = false;
5889
+ for (const group of groups) {
5890
+ if (group.opBefore === "&&" && lastExit !== 0) continue;
5891
+ if (group.opBefore === "||" && lastExit === 0) continue;
5892
+ const remainingMs = deadline - Date.now();
5893
+ if (remainingMs <= 0) {
5894
+ timedOut = true;
5895
+ break;
5896
+ }
5897
+ const result = await runPipeGroup(group.segments, {
5898
+ cwd: opts.cwd,
5899
+ timeoutMs: remainingMs,
5900
+ buf,
5901
+ signal: opts.signal
5902
+ });
5903
+ lastExit = result.exitCode;
5904
+ if (result.timedOut) {
5905
+ timedOut = true;
5906
+ break;
5907
+ }
5908
+ if (opts.signal?.aborted) break;
5909
+ }
5910
+ const output = buf.toString();
5911
+ const truncated = output.length > opts.maxOutputChars ? `${output.slice(0, opts.maxOutputChars)}
5912
+
5913
+ [\u2026 truncated ${output.length - opts.maxOutputChars} chars \u2026]` : output;
5914
+ return { exitCode: lastExit, output: truncated, timedOut };
5915
+ }
5916
+ function openRedirects(redirects, cwd) {
5917
+ let stdinFd = null;
5918
+ let stdoutFd = null;
5919
+ let stderrFd = null;
5920
+ let mergeStderrToStdout = false;
5921
+ let bothFd = null;
5922
+ const toClose = [];
5923
+ const open = (target, flags) => {
5924
+ const resolved = pathMod3.resolve(cwd, target);
5925
+ const fd = openSync(resolved, flags);
5926
+ toClose.push(fd);
5927
+ return fd;
5928
+ };
5929
+ for (const r of redirects) {
5930
+ if (r.kind === "<") stdinFd = open(r.target, "r");
5931
+ else if (r.kind === ">") stdoutFd = open(r.target, "w");
5932
+ else if (r.kind === ">>") stdoutFd = open(r.target, "a");
5933
+ else if (r.kind === "2>") stderrFd = open(r.target, "w");
5934
+ else if (r.kind === "2>>") stderrFd = open(r.target, "a");
5935
+ else if (r.kind === "&>") {
5936
+ bothFd = open(r.target, "w");
5937
+ stdoutFd = bothFd;
5938
+ stderrFd = bothFd;
5939
+ } else if (r.kind === "2>&1") {
5940
+ mergeStderrToStdout = true;
5941
+ }
5942
+ }
5943
+ return { stdinFd, stdoutFd, stderrFd, mergeStderrToStdout, toClose };
5944
+ }
5945
+ async function runPipeGroup(segments, opts) {
5946
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
5947
+ const children = [];
5948
+ const allFds = [];
5949
+ let timedOut = false;
5950
+ const killAll = () => {
5951
+ for (const c of children) killProcessTree2(c);
5952
+ };
5953
+ const killTimer = setTimeout(() => {
5954
+ timedOut = true;
5955
+ killAll();
5956
+ }, opts.timeoutMs);
5957
+ const onAbort = () => killAll();
5958
+ if (opts.signal?.aborted) {
5959
+ onAbort();
5960
+ } else {
5961
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
5962
+ }
5963
+ try {
5964
+ for (let i = 0; i < segments.length; i++) {
5965
+ const isFirst = i === 0;
5966
+ const isLast = i === segments.length - 1;
5967
+ const seg = segments[i];
5968
+ const io = openRedirects(seg.redirects, opts.cwd);
5969
+ allFds.push(...io.toClose);
5970
+ const { bin, args, spawnOverrides } = prepareSpawn(seg.argv);
5971
+ const stdoutSpec = io.stdoutFd !== null ? io.stdoutFd : "pipe";
5972
+ const stderrSpec = io.stderrFd !== null ? io.stderrFd : io.mergeStderrToStdout ? stdoutSpec : "pipe";
5973
+ const stdinSpec = io.stdinFd !== null ? io.stdinFd : isFirst ? "ignore" : "pipe";
5974
+ const spawnOpts = {
5975
+ cwd: opts.cwd,
5976
+ shell: false,
5977
+ windowsHide: true,
5978
+ env,
5979
+ stdio: [stdinSpec, stdoutSpec, stderrSpec],
5980
+ ...spawnOverrides
5981
+ };
5982
+ let child;
5983
+ try {
5984
+ child = spawn3(bin, args, spawnOpts);
5985
+ } catch (err) {
5986
+ for (const fd of allFds) tryClose(fd);
5987
+ killAll();
5988
+ clearTimeout(killTimer);
5989
+ opts.signal?.removeEventListener("abort", onAbort);
5990
+ throw err;
5991
+ }
5992
+ children.push(child);
5993
+ if (!isFirst && io.stdinFd === null) {
5994
+ const prev = children[i - 1];
5995
+ prev.stdout?.on("error", () => {
5996
+ });
5997
+ child.stdin?.on("error", () => {
5998
+ });
5999
+ const prevMergesStderr = segments[i - 1].redirects.some((r) => r.kind === "2>&1") && !!prev.stderr;
6000
+ if (prevMergesStderr && prev.stderr) {
6001
+ prev.stderr.on("error", () => {
6002
+ });
6003
+ let openSources = 2;
6004
+ const closeIfDone = () => {
6005
+ if (--openSources === 0) child.stdin?.end();
6006
+ };
6007
+ prev.stdout?.pipe(child.stdin, { end: false });
6008
+ prev.stderr.pipe(child.stdin, { end: false });
6009
+ prev.stdout?.once("end", closeIfDone);
6010
+ prev.stderr.once("end", closeIfDone);
6011
+ } else {
6012
+ prev.stdout?.pipe(child.stdin);
6013
+ }
6014
+ }
6015
+ if (child.stderr && io.stderrFd === null && !(io.mergeStderrToStdout && !isLast)) {
6016
+ child.stderr.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
6017
+ }
6018
+ if (isLast && child.stdout && io.stdoutFd === null) {
6019
+ child.stdout.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
6020
+ if (io.mergeStderrToStdout && child.stderr && io.stderrFd === null) {
6021
+ child.stderr.removeAllListeners("data");
6022
+ child.stderr.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
6023
+ }
6024
+ }
6025
+ }
6026
+ const exits = await Promise.all(
6027
+ children.map(
6028
+ (c) => new Promise((resolve10) => {
6029
+ c.once("error", () => resolve10(null));
6030
+ c.once("close", (code) => resolve10(code));
6031
+ })
6032
+ )
6033
+ );
6034
+ return { exitCode: exits[exits.length - 1] ?? null, timedOut };
6035
+ } finally {
6036
+ for (const fd of allFds) tryClose(fd);
6037
+ clearTimeout(killTimer);
6038
+ opts.signal?.removeEventListener("abort", onAbort);
6039
+ }
6040
+ }
6041
+ function tryClose(fd) {
6042
+ try {
6043
+ closeSync(fd);
6044
+ } catch {
6045
+ }
6046
+ }
6047
+ function toBuf(chunk) {
6048
+ return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
6049
+ }
6050
+ var OutputBuffer = class {
6051
+ constructor(cap) {
6052
+ this.cap = cap;
6053
+ }
6054
+ cap;
6055
+ chunks = [];
6056
+ bytes = 0;
6057
+ push(b) {
6058
+ if (this.bytes >= this.cap) return;
6059
+ const remaining = this.cap - this.bytes;
6060
+ if (b.length > remaining) {
6061
+ this.chunks.push(b.subarray(0, remaining));
6062
+ this.bytes = this.cap;
6063
+ } else {
6064
+ this.chunks.push(b);
6065
+ this.bytes += b.length;
6066
+ }
6067
+ }
6068
+ toString() {
6069
+ return smartDecodeOutput(Buffer.concat(this.chunks));
6070
+ }
6071
+ };
6072
+
5689
6073
  // src/tools/shell.ts
5690
6074
  function killProcessTree2(child) {
5691
6075
  if (!child.pid || child.killed) return;
@@ -5857,17 +6241,31 @@ function isAllowed(cmd, extra = []) {
5857
6241
  }
5858
6242
  return false;
5859
6243
  }
6244
+ function isCommandAllowed(cmd, extra = []) {
6245
+ let chain;
6246
+ try {
6247
+ chain = parseCommandChain(cmd);
6248
+ } catch {
6249
+ return false;
6250
+ }
6251
+ if (chain === null) return isAllowed(cmd, extra);
6252
+ return chainAllowed(chain, (seg) => isAllowed(seg, extra));
6253
+ }
5860
6254
  async function runCommand(cmd, opts) {
6255
+ const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
6256
+ const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
5861
6257
  const argv = tokenizeCommand(cmd);
5862
6258
  if (argv.length === 0) throw new Error("run_command: empty command");
5863
- const operator = detectShellOperator(cmd);
5864
- if (operator !== null) {
5865
- throw new Error(
5866
- `run_command: shell operator "${operator}" is not supported \u2014 this tool spawns one process, no shell expansion. Split into separate run_command calls and combine the output in your reasoning (e.g. instead of \`grep foo *.ts | wc -l\`, call \`grep -c foo *.ts\` or two separate commands). To pass "${operator}" as a literal argument, wrap it in quotes.`
5867
- );
6259
+ const chain = parseCommandChain(cmd);
6260
+ if (chain !== null) {
6261
+ return await runChain(chain, {
6262
+ cwd: opts.cwd,
6263
+ timeoutSec,
6264
+ maxOutputChars: maxChars,
6265
+ signal: opts.signal
6266
+ });
5868
6267
  }
5869
- const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
5870
- const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
6268
+ const timeoutMs = timeoutSec * 1e3;
5871
6269
  const spawnOpts = {
5872
6270
  cwd: opts.cwd,
5873
6271
  shell: false,
@@ -5885,10 +6283,10 @@ async function runCommand(cmd, opts) {
5885
6283
  };
5886
6284
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
5887
6285
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
5888
- return await new Promise((resolve9, reject) => {
6286
+ return await new Promise((resolve10, reject) => {
5889
6287
  let child;
5890
6288
  try {
5891
- child = spawn3(bin, args, effectiveSpawnOpts);
6289
+ child = spawn4(bin, args, effectiveSpawnOpts);
5892
6290
  } catch (err) {
5893
6291
  reject(err);
5894
6292
  return;
@@ -5939,7 +6337,7 @@ async function runCommand(cmd, opts) {
5939
6337
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
5940
6338
 
5941
6339
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
5942
- resolve9({ exitCode: code, output, timedOut });
6340
+ resolve10({ exitCode: code, output, timedOut });
5943
6341
  });
5944
6342
  });
5945
6343
  }
@@ -5961,16 +6359,16 @@ function resolveExecutable(cmd, opts = {}) {
5961
6359
  const platform = opts.platform ?? process.platform;
5962
6360
  if (platform !== "win32") return cmd;
5963
6361
  if (!cmd) return cmd;
5964
- if (cmd.includes("/") || cmd.includes("\\") || pathMod3.isAbsolute(cmd)) return cmd;
5965
- if (pathMod3.extname(cmd)) return cmd;
6362
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod4.isAbsolute(cmd)) return cmd;
6363
+ if (pathMod4.extname(cmd)) return cmd;
5966
6364
  const env = opts.env ?? process.env;
5967
6365
  const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
5968
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod3.delimiter);
6366
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod4.delimiter);
5969
6367
  const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
5970
6368
  const isFile = opts.isFile ?? defaultIsFile;
5971
6369
  for (const dir of pathDirs) {
5972
6370
  for (const ext of pathExt) {
5973
- const full = pathMod3.win32.join(dir, cmd + ext);
6371
+ const full = pathMod4.win32.join(dir, cmd + ext);
5974
6372
  if (isFile(full)) return full;
5975
6373
  }
5976
6374
  }
@@ -6040,8 +6438,8 @@ function withUtf8Codepage(cmdline) {
6040
6438
  function isBareWindowsName(s) {
6041
6439
  if (!s) return false;
6042
6440
  if (s.includes("/") || s.includes("\\")) return false;
6043
- if (pathMod3.isAbsolute(s)) return false;
6044
- if (pathMod3.extname(s)) return false;
6441
+ if (pathMod4.isAbsolute(s)) return false;
6442
+ if (pathMod4.extname(s)) return false;
6045
6443
  return true;
6046
6444
  }
6047
6445
  function quoteForCmdExe(arg) {
@@ -6060,7 +6458,7 @@ var NeedsConfirmationError = class extends Error {
6060
6458
  }
6061
6459
  };
6062
6460
  function registerShellTools(registry, opts) {
6063
- const rootDir = pathMod3.resolve(opts.rootDir);
6461
+ const rootDir = pathMod4.resolve(opts.rootDir);
6064
6462
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
6065
6463
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
6066
6464
  const jobs = opts.jobs ?? new JobRegistry();
@@ -6071,7 +6469,7 @@ function registerShellTools(registry, opts) {
6071
6469
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
6072
6470
  registry.register({
6073
6471
  name: "run_command",
6074
- description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 ONE process per call, NO shell expansion. `&&`, `||`, `|`, `;`, `>`, `<`, `2>&1` are all rejected up-front \u2014 split into separate calls and combine results in reasoning. Example: instead of `grep foo *.ts | wc -l`, use `grep -c foo *.ts`; instead of `cd sub && npm test`, use `npm test --prefix sub` (or whatever --cwd flag the binary accepts).\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. If a tool needs a subdirectory, pass it via the tool's own flag (`npm --prefix`, `cargo -C`, `git -C`, `pytest tests/\u2026`), NOT via a preceding `cd`.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
6472
+ description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 Chain operators `|`, `||`, `&&`, `;` ARE supported \u2014 parsed natively, no shell invoked, so semantics are identical on Windows / macOS / Linux. Each chain segment is allowlist-checked individually: `git status | grep main` runs if both halves are allowed.\n\u2022 File redirects ARE supported: `>` truncate, `>>` append, `<` stdin from file, `2>` / `2>>` stderr to file, `2>&1` merge stderr\u2192stdout, `&>` both to file. Targets resolve relative to the project root. At most one redirect per fd per segment.\n\u2022 Background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, and process substitution `<(\u2026)` are NOT supported. Wrap a literal `&` arg in quotes; for input use a `<` file or the binary's own --input flag.\n\u2022 Env-var expansion `$VAR` is NOT performed \u2014 `$VAR` is passed as a literal string. Use the binary's own --env flag or substitute the value yourself.\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. If a tool needs a subdirectory, pass it via the tool's own flag (`npm --prefix`, `cargo -C`, `git -C`, `pytest tests/\u2026`), NOT via a preceding `cd`.\n\u2022 Glob patterns (`*.ts`) are passed through as literal arguments \u2014 no shell expansion. Use `grep -r`, `rg`, `find -name`, etc.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
6075
6473
  // Plan-mode gate: allow allowlisted commands through (git status,
6076
6474
  // cargo check, ls, grep …) so the model can actually investigate
6077
6475
  // during planning. Anything that would otherwise trigger a
@@ -6080,14 +6478,14 @@ function registerShellTools(registry, opts) {
6080
6478
  if (isAllowAll()) return true;
6081
6479
  const cmd = typeof args?.command === "string" ? args.command.trim() : "";
6082
6480
  if (!cmd) return false;
6083
- return isAllowed(cmd, getExtraAllowed());
6481
+ return isCommandAllowed(cmd, getExtraAllowed());
6084
6482
  },
6085
6483
  parameters: {
6086
6484
  type: "object",
6087
6485
  properties: {
6088
6486
  command: {
6089
6487
  type: "string",
6090
- description: 'Full command line. Tokenized with POSIX-ish quoting; no shell expansion. Pipes (`|`), redirects (`>`, `<`, `2>`), and `&&`/`||` chaining are rejected with an error \u2014 split into separate calls instead. To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
6488
+ description: 'Full command line. POSIX-ish quoting. Chain operators `|`, `||`, `&&`, `;` and file redirects `>` / `>>` / `<` / `2>` / `2>>` / `2>&1` / `&>` work natively (no shell). Background `&`, heredoc `<<`, env-var expansion `$VAR`, and command substitution `$(\u2026)` are rejected (or passed through as literal in the case of `$VAR`). To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
6091
6489
  },
6092
6490
  timeoutSec: {
6093
6491
  type: "integer",
@@ -6099,7 +6497,7 @@ function registerShellTools(registry, opts) {
6099
6497
  fn: async (args, ctx) => {
6100
6498
  const cmd = args.command.trim();
6101
6499
  if (!cmd) throw new Error("run_command: empty command");
6102
- if (!isAllowAll() && !isAllowed(cmd, getExtraAllowed())) {
6500
+ if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
6103
6501
  throw new NeedsConfirmationError(cmd);
6104
6502
  }
6105
6503
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
@@ -6463,11 +6861,11 @@ ${i + 1}. ${r.title}`);
6463
6861
 
6464
6862
  // src/env.ts
6465
6863
  import { readFileSync as readFileSync9 } from "fs";
6466
- import { resolve as resolve7 } from "path";
6864
+ import { resolve as resolve8 } from "path";
6467
6865
  function loadDotenv(path2 = ".env") {
6468
6866
  let raw;
6469
6867
  try {
6470
- raw = readFileSync9(resolve7(process.cwd(), path2), "utf8");
6868
+ raw = readFileSync9(resolve8(process.cwd(), path2), "utf8");
6471
6869
  } catch {
6472
6870
  return;
6473
6871
  }
@@ -7221,7 +7619,7 @@ var McpClient = class {
7221
7619
  const id = this.nextId++;
7222
7620
  const frame = { jsonrpc: "2.0", id, method, params };
7223
7621
  let abortHandler = null;
7224
- const promise = new Promise((resolve9, reject) => {
7622
+ const promise = new Promise((resolve10, reject) => {
7225
7623
  const timeout = setTimeout(() => {
7226
7624
  this.pending.delete(id);
7227
7625
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -7230,7 +7628,7 @@ var McpClient = class {
7230
7628
  );
7231
7629
  }, this.requestTimeoutMs);
7232
7630
  this.pending.set(id, {
7233
- resolve: resolve9,
7631
+ resolve: resolve10,
7234
7632
  reject,
7235
7633
  timeout
7236
7634
  });
@@ -7312,7 +7710,7 @@ var McpClient = class {
7312
7710
  };
7313
7711
 
7314
7712
  // src/mcp/stdio.ts
7315
- import { spawn as spawn4 } from "child_process";
7713
+ import { spawn as spawn5 } from "child_process";
7316
7714
  var StdioTransport = class {
7317
7715
  child;
7318
7716
  queue = [];
@@ -7327,14 +7725,14 @@ var StdioTransport = class {
7327
7725
  opts.command,
7328
7726
  ...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
7329
7727
  ].join(" ");
7330
- this.child = spawn4(line, [], {
7728
+ this.child = spawn5(line, [], {
7331
7729
  env,
7332
7730
  cwd: opts.cwd,
7333
7731
  stdio: ["pipe", "pipe", "inherit"],
7334
7732
  shell: true
7335
7733
  });
7336
7734
  } else {
7337
- this.child = spawn4(opts.command, opts.args ?? [], {
7735
+ this.child = spawn5(opts.command, opts.args ?? [], {
7338
7736
  env,
7339
7737
  cwd: opts.cwd,
7340
7738
  stdio: ["pipe", "pipe", "inherit"]
@@ -7353,12 +7751,12 @@ var StdioTransport = class {
7353
7751
  }
7354
7752
  async send(message) {
7355
7753
  if (this.closed) throw new Error("MCP transport is closed");
7356
- return new Promise((resolve9, reject) => {
7754
+ return new Promise((resolve10, reject) => {
7357
7755
  const line = `${JSON.stringify(message)}
7358
7756
  `;
7359
7757
  this.child.stdin.write(line, "utf8", (err) => {
7360
7758
  if (err) reject(err);
7361
- else resolve9();
7759
+ else resolve10();
7362
7760
  });
7363
7761
  });
7364
7762
  }
@@ -7369,8 +7767,8 @@ var StdioTransport = class {
7369
7767
  continue;
7370
7768
  }
7371
7769
  if (this.closed) return;
7372
- const next = await new Promise((resolve9) => {
7373
- this.waiters.push(resolve9);
7770
+ const next = await new Promise((resolve10) => {
7771
+ this.waiters.push(resolve10);
7374
7772
  });
7375
7773
  if (next === null) return;
7376
7774
  yield next;
@@ -7439,8 +7837,8 @@ var SseTransport = class {
7439
7837
  constructor(opts) {
7440
7838
  this.url = opts.url;
7441
7839
  this.headers = opts.headers ?? {};
7442
- this.endpointReady = new Promise((resolve9, reject) => {
7443
- this.resolveEndpoint = resolve9;
7840
+ this.endpointReady = new Promise((resolve10, reject) => {
7841
+ this.resolveEndpoint = resolve10;
7444
7842
  this.rejectEndpoint = reject;
7445
7843
  });
7446
7844
  this.endpointReady.catch(() => void 0);
@@ -7467,8 +7865,8 @@ var SseTransport = class {
7467
7865
  continue;
7468
7866
  }
7469
7867
  if (this.closed) return;
7470
- const next = await new Promise((resolve9) => {
7471
- this.waiters.push(resolve9);
7868
+ const next = await new Promise((resolve10) => {
7869
+ this.waiters.push(resolve10);
7472
7870
  });
7473
7871
  if (next === null) return;
7474
7872
  yield next;
@@ -7654,8 +8052,8 @@ var StreamableHttpTransport = class {
7654
8052
  continue;
7655
8053
  }
7656
8054
  if (this.closed) return;
7657
- const next = await new Promise((resolve9) => {
7658
- this.waiters.push(resolve9);
8055
+ const next = await new Promise((resolve10) => {
8056
+ this.waiters.push(resolve10);
7659
8057
  });
7660
8058
  if (next === null) return;
7661
8059
  yield next;
@@ -7826,7 +8224,7 @@ async function trySection(load) {
7826
8224
 
7827
8225
  // src/code/edit-blocks.ts
7828
8226
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync12, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
7829
- import { dirname as dirname5, resolve as resolve8 } from "path";
8227
+ import { dirname as dirname5, resolve as resolve9 } from "path";
7830
8228
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
7831
8229
  function parseEditBlocks(text) {
7832
8230
  const out = [];
@@ -7844,8 +8242,8 @@ function parseEditBlocks(text) {
7844
8242
  return out;
7845
8243
  }
7846
8244
  function applyEditBlock(block, rootDir) {
7847
- const absRoot = resolve8(rootDir);
7848
- const absTarget = resolve8(absRoot, block.path);
8245
+ const absRoot = resolve9(rootDir);
8246
+ const absTarget = resolve9(absRoot, block.path);
7849
8247
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
7850
8248
  return {
7851
8249
  path: block.path,
@@ -7898,13 +8296,13 @@ function applyEditBlocks(blocks, rootDir) {
7898
8296
  return blocks.map((b) => applyEditBlock(b, rootDir));
7899
8297
  }
7900
8298
  function snapshotBeforeEdits(blocks, rootDir) {
7901
- const absRoot = resolve8(rootDir);
8299
+ const absRoot = resolve9(rootDir);
7902
8300
  const seen = /* @__PURE__ */ new Set();
7903
8301
  const snapshots = [];
7904
8302
  for (const b of blocks) {
7905
8303
  if (seen.has(b.path)) continue;
7906
8304
  seen.add(b.path);
7907
- const abs = resolve8(absRoot, b.path);
8305
+ const abs = resolve9(absRoot, b.path);
7908
8306
  if (!existsSync10(abs)) {
7909
8307
  snapshots.push({ path: b.path, prevContent: null });
7910
8308
  continue;
@@ -7918,9 +8316,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
7918
8316
  return snapshots;
7919
8317
  }
7920
8318
  function restoreSnapshots(snapshots, rootDir) {
7921
- const absRoot = resolve8(rootDir);
8319
+ const absRoot = resolve9(rootDir);
7922
8320
  return snapshots.map((snap) => {
7923
- const abs = resolve8(absRoot, snap.path);
8321
+ const abs = resolve9(absRoot, snap.path);
7924
8322
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
7925
8323
  return {
7926
8324
  path: snap.path,
@@ -8457,7 +8855,6 @@ export {
8457
8855
  NeedsConfirmationError,
8458
8856
  PROJECT_MEMORY_FILE,
8459
8857
  PROJECT_MEMORY_MAX_CHARS,
8460
- PlanCheckpointError,
8461
8858
  PlanProposedError,
8462
8859
  PlanRevisionProposedError,
8463
8860
  SessionStats,