ralphctl 0.8.4 → 0.8.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/dist/cli.mjs CHANGED
@@ -778,8 +778,11 @@ var createJsonSettingsRepository = (deps) => {
778
778
  };
779
779
  };
780
780
 
781
+ // src/integration/io/cross-platform-spawn.ts
782
+ import spawn from "cross-spawn";
783
+ var crossPlatformSpawn = (command, args, options) => spawn(command, [...args], options);
784
+
781
785
  // src/integration/io/git-runner.ts
782
- import { spawn as nodeSpawn } from "child_process";
783
786
  var DEFAULT_GIT_TIMEOUT_MS = 3e4;
784
787
  var createGitRunner = (deps = {}) => {
785
788
  const spawn3 = deps.spawn ?? defaultSpawn;
@@ -857,11 +860,11 @@ var createGitRunner = (deps = {}) => {
857
860
  });
858
861
  return { run };
859
862
  };
860
- var defaultSpawn = (command, args, options) => nodeSpawn(command, [...args], { ...options, stdio: [...options.stdio] });
863
+ var defaultSpawn = (command, args, options) => crossPlatformSpawn(command, args, { ...options, stdio: [...options.stdio] });
861
864
  var stringifyError = (cause) => cause instanceof Error ? cause.message : String(cause);
862
865
 
863
866
  // src/integration/io/shell-script-runner.ts
864
- import { spawn as nodeSpawn2 } from "child_process";
867
+ import { spawn as nodeSpawn } from "child_process";
865
868
  var DEFAULT_SHELL_TIMEOUT_MS = 5 * 6e4;
866
869
  var MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
867
870
  var createShellScriptRunner = (deps = {}) => {
@@ -987,7 +990,7 @@ ${marker}` : marker : base;
987
990
  });
988
991
  return { run };
989
992
  };
990
- var defaultSpawn2 = (command, args, options) => nodeSpawn2(command, [...args], { ...options, stdio: [...options.stdio] });
993
+ var defaultSpawn2 = (command, args, options) => nodeSpawn(command, [...args], { ...options, stdio: [...options.stdio] });
991
994
  var stringifyError2 = (cause) => cause instanceof Error ? cause.message : String(cause);
992
995
 
993
996
  // src/integration/persistence/project/project.schema.ts
@@ -2217,12 +2220,6 @@ var createAppendFile = () => {
2217
2220
  };
2218
2221
  };
2219
2222
 
2220
- // src/application/bootstrap/wire.ts
2221
- import { spawn as nodeSpawn9 } from "child_process";
2222
-
2223
- // src/integration/ai/providers/claude/headless.ts
2224
- import { spawn as nodeSpawn3 } from "child_process";
2225
-
2226
2223
  // src/integration/ai/providers/_engine/resolve-roots.ts
2227
2224
  var resolveWritableRoots = (session) => {
2228
2225
  const declared = session.additionalRoots ?? [];
@@ -2908,13 +2905,12 @@ var spawnAttempt = async (input) => {
2908
2905
  onSuccess
2909
2906
  });
2910
2907
  };
2911
- var defaultSpawn3 = (command, args, options) => nodeSpawn3(command, [...args], {
2908
+ var defaultSpawn3 = (command, args, options) => crossPlatformSpawn(command, args, {
2912
2909
  stdio: [...options.stdio],
2913
2910
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {}
2914
2911
  });
2915
2912
 
2916
2913
  // src/integration/ai/providers/codex/headless.ts
2917
- import { spawn as nodeSpawn4 } from "child_process";
2918
2914
  import { promises as fs6 } from "fs";
2919
2915
  import { tmpdir } from "os";
2920
2916
  import { join as join6 } from "path";
@@ -3291,14 +3287,11 @@ var spawnAttempt2 = async (input) => {
3291
3287
  onSuccess
3292
3288
  });
3293
3289
  };
3294
- var defaultSpawn4 = (command, args, options) => nodeSpawn4(command, [...args], {
3290
+ var defaultSpawn4 = (command, args, options) => crossPlatformSpawn(command, args, {
3295
3291
  stdio: [...options.stdio],
3296
3292
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {}
3297
3293
  });
3298
3294
 
3299
- // src/integration/ai/providers/copilot/headless.ts
3300
- import { spawn as nodeSpawn5 } from "child_process";
3301
-
3302
3295
  // src/integration/ai/providers/copilot/parse-stream.ts
3303
3296
  var stringField3 = (obj, ...names) => {
3304
3297
  for (const name of names) {
@@ -3650,7 +3643,7 @@ var spawnAttempt3 = async (input) => {
3650
3643
  onSuccess
3651
3644
  });
3652
3645
  };
3653
- var defaultSpawn5 = (command, args, options) => nodeSpawn5(command, [...args], {
3646
+ var defaultSpawn5 = (command, args, options) => crossPlatformSpawn(command, args, {
3654
3647
  stdio: [...options.stdio],
3655
3648
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {}
3656
3649
  });
@@ -3689,12 +3682,20 @@ var createAiProvider = (deps) => {
3689
3682
  };
3690
3683
 
3691
3684
  // src/integration/ai/providers/claude/interactive.ts
3692
- import { spawn as nodeSpawn6 } from "child_process";
3685
+ import { promises as fs7 } from "fs";
3686
+ import "child_process";
3693
3687
  import { dirname as dirname5 } from "path";
3694
- var defaultSpawn6 = (command, args, options) => nodeSpawn6(command, [...args], { stdio: options.stdio, cwd: options.cwd });
3688
+ var defaultSpawn6 = (command, args, options) => (
3689
+ // Route through the shared cross-platform primitive so `claude.cmd` shims resolve on
3690
+ // Windows and the positional prompt argument (which may contain spaces or shell
3691
+ // metacharacters) is escaped correctly — without a shell. See cross-platform-spawn.ts.
3692
+ crossPlatformSpawn(command, args, { stdio: options.stdio, cwd: options.cwd })
3693
+ );
3694
+ var defaultReadFile = (path) => fs7.readFile(path, "utf8");
3695
3695
  var createInteractiveClaudeProvider = (deps) => {
3696
3696
  const spawnFn = deps.spawn ?? defaultSpawn6;
3697
- const command = deps.command ?? "bash";
3697
+ const command = deps.command ?? "claude";
3698
+ const readFile2 = deps.readFile ?? defaultReadFile;
3698
3699
  const newSessionId = deps.newSessionId ?? uuidv7;
3699
3700
  return {
3700
3701
  async run(input) {
@@ -3708,6 +3709,18 @@ var createInteractiveClaudeProvider = (deps) => {
3708
3709
  })
3709
3710
  );
3710
3711
  }
3712
+ let prompt;
3713
+ try {
3714
+ prompt = await readFile2(String(input.promptFile));
3715
+ } catch (cause) {
3716
+ return Result.error(
3717
+ new StorageError({
3718
+ subCode: "io",
3719
+ message: `interactive-claude: failed to read prompt file ${String(input.promptFile)} \u2014 ${stringifyError4(cause)}`,
3720
+ cause
3721
+ })
3722
+ );
3723
+ }
3711
3724
  const allRoots = [
3712
3725
  String(input.cwd),
3713
3726
  ...input.additionalRoots?.map((r) => String(r)) ?? [],
@@ -3719,19 +3732,18 @@ var createInteractiveClaudeProvider = (deps) => {
3719
3732
  if (seen.has(p)) return false;
3720
3733
  seen.add(p);
3721
3734
  return true;
3722
- }).flatMap((p) => ["--add-dir", shellQuote(p)]);
3735
+ }).flatMap((p) => ["--add-dir", p]);
3723
3736
  const sessionId2 = newSessionId();
3724
- const inner = [
3725
- "claude",
3737
+ const args = [
3726
3738
  ...dirFlags,
3727
3739
  "--model",
3728
- shellQuote(input.model),
3740
+ input.model,
3729
3741
  "--permission-mode",
3730
3742
  "acceptEdits",
3731
3743
  "--session-id",
3732
- shellQuote(sessionId2),
3733
- `"$(cat ${shellQuote(String(input.promptFile))})"`
3734
- ].join(" ");
3744
+ sessionId2,
3745
+ prompt
3746
+ ];
3735
3747
  deps.eventBus.publish({
3736
3748
  type: "log",
3737
3749
  level: "info",
@@ -3741,7 +3753,7 @@ var createInteractiveClaudeProvider = (deps) => {
3741
3753
  });
3742
3754
  let child;
3743
3755
  try {
3744
- child = spawnFn(command, ["-lc", inner], { stdio: "inherit", cwd: String(input.cwd) });
3756
+ child = spawnFn(command, args, { stdio: "inherit", cwd: String(input.cwd) });
3745
3757
  } catch (cause) {
3746
3758
  return Result.error(
3747
3759
  new StorageError({
@@ -3785,16 +3797,23 @@ var createInteractiveClaudeProvider = (deps) => {
3785
3797
  }
3786
3798
  };
3787
3799
  };
3788
- var shellQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
3789
3800
  var stringifyError4 = (cause) => cause instanceof Error ? cause.message : String(cause);
3790
3801
 
3791
3802
  // src/integration/ai/providers/codex/interactive.ts
3792
- import { spawn as nodeSpawn7 } from "child_process";
3803
+ import { promises as fs8 } from "fs";
3804
+ import "child_process";
3793
3805
  import { dirname as dirname6 } from "path";
3794
- var defaultSpawn7 = (command, args, options) => nodeSpawn7(command, [...args], { stdio: options.stdio, cwd: options.cwd });
3806
+ var defaultSpawn7 = (command, args, options) => (
3807
+ // Route through the shared cross-platform primitive so `codex.cmd` shims resolve on
3808
+ // Windows and the positional prompt argument is escaped correctly — without a shell.
3809
+ // See cross-platform-spawn.ts.
3810
+ crossPlatformSpawn(command, args, { stdio: options.stdio, cwd: options.cwd })
3811
+ );
3812
+ var defaultReadFile2 = (path) => fs8.readFile(path, "utf8");
3795
3813
  var createInteractiveCodexProvider = (deps) => {
3796
3814
  const spawnFn = deps.spawn ?? defaultSpawn7;
3797
- const command = deps.command ?? "bash";
3815
+ const command = deps.command ?? "codex";
3816
+ const readFile2 = deps.readFile ?? defaultReadFile2;
3798
3817
  return {
3799
3818
  async run(input) {
3800
3819
  if (!isCodexModel(input.model)) {
@@ -3807,6 +3826,18 @@ var createInteractiveCodexProvider = (deps) => {
3807
3826
  })
3808
3827
  );
3809
3828
  }
3829
+ let prompt;
3830
+ try {
3831
+ prompt = await readFile2(String(input.promptFile));
3832
+ } catch (cause) {
3833
+ return Result.error(
3834
+ new StorageError({
3835
+ subCode: "io",
3836
+ message: `interactive-codex: failed to read prompt file ${String(input.promptFile)} \u2014 ${stringifyError5(cause)}`,
3837
+ cause
3838
+ })
3839
+ );
3840
+ }
3810
3841
  const allRoots = [
3811
3842
  String(input.cwd),
3812
3843
  ...input.additionalRoots?.map((r) => String(r)) ?? [],
@@ -3818,20 +3849,19 @@ var createInteractiveCodexProvider = (deps) => {
3818
3849
  if (seen.has(p)) return false;
3819
3850
  seen.add(p);
3820
3851
  return true;
3821
- }).flatMap((p) => ["--add-dir", shellQuote2(p)]);
3822
- const inner = [
3823
- "codex",
3852
+ }).flatMap((p) => ["--add-dir", p]);
3853
+ const args = [
3824
3854
  "--cd",
3825
- shellQuote2(String(input.cwd)),
3855
+ String(input.cwd),
3826
3856
  ...dirFlags,
3827
3857
  "--model",
3828
- shellQuote2(input.model),
3858
+ input.model,
3829
3859
  "-s",
3830
3860
  "workspace-write",
3831
3861
  "-a",
3832
3862
  "never",
3833
- `"$(cat ${shellQuote2(String(input.promptFile))})"`
3834
- ].join(" ");
3863
+ prompt
3864
+ ];
3835
3865
  deps.eventBus.publish({
3836
3866
  type: "log",
3837
3867
  level: "info",
@@ -3841,7 +3871,7 @@ var createInteractiveCodexProvider = (deps) => {
3841
3871
  });
3842
3872
  let child;
3843
3873
  try {
3844
- child = spawnFn(command, ["-lc", inner], { stdio: "inherit", cwd: String(input.cwd) });
3874
+ child = spawnFn(command, args, { stdio: "inherit", cwd: String(input.cwd) });
3845
3875
  } catch (cause) {
3846
3876
  return Result.error(
3847
3877
  new StorageError({
@@ -3873,19 +3903,23 @@ var createInteractiveCodexProvider = (deps) => {
3873
3903
  }
3874
3904
  };
3875
3905
  };
3876
- var shellQuote2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
3877
3906
  var stringifyError5 = (cause) => cause instanceof Error ? cause.message : String(cause);
3878
3907
 
3879
3908
  // src/integration/ai/providers/copilot/interactive.ts
3880
- import { promises as fs7 } from "fs";
3881
- import { spawn as nodeSpawn8 } from "child_process";
3909
+ import { promises as fs9 } from "fs";
3910
+ import "child_process";
3882
3911
  import { dirname as dirname7 } from "path";
3883
- var defaultSpawn8 = (command, args, options) => nodeSpawn8(command, [...args], { stdio: options.stdio, cwd: options.cwd });
3884
- var defaultReadFile = (path) => fs7.readFile(path, "utf8");
3912
+ var defaultSpawn8 = (command, args, options) => (
3913
+ // Route through the shared cross-platform primitive so `copilot.cmd` shims resolve on
3914
+ // Windows and the seeded prompt argument is escaped correctly — without a shell.
3915
+ // See cross-platform-spawn.ts.
3916
+ crossPlatformSpawn(command, args, { stdio: options.stdio, cwd: options.cwd })
3917
+ );
3918
+ var defaultReadFile3 = (path) => fs9.readFile(path, "utf8");
3885
3919
  var createInteractiveCopilotProvider = (deps) => {
3886
3920
  const spawnFn = deps.spawn ?? defaultSpawn8;
3887
3921
  const command = deps.command ?? "copilot";
3888
- const readFile2 = deps.readFile ?? defaultReadFile;
3922
+ const readFile2 = deps.readFile ?? defaultReadFile3;
3889
3923
  const newSessionId = deps.newSessionId ?? uuidv7;
3890
3924
  return {
3891
3925
  async run(input) {
@@ -4512,7 +4546,7 @@ var createPullRequestCreator = (deps) => async (input) => {
4512
4546
  };
4513
4547
 
4514
4548
  // src/integration/ai/prompts/_engine/fs-template-loader.ts
4515
- import { promises as fs8 } from "fs";
4549
+ import { promises as fs10 } from "fs";
4516
4550
  import { dirname as dirname8, join as join7 } from "path";
4517
4551
  import { fileURLToPath } from "url";
4518
4552
  var createFsTemplateLoader = (templatesDir) => ({
@@ -4536,7 +4570,7 @@ var createFsTemplateLoader = (templatesDir) => ({
4536
4570
  });
4537
4571
  var tryRead = async (path) => {
4538
4572
  try {
4539
- const content = await fs8.readFile(path, "utf8");
4573
+ const content = await fs10.readFile(path, "utf8");
4540
4574
  return { kind: "ok", value: content };
4541
4575
  } catch (cause) {
4542
4576
  if (isNodeErrnoCode2(cause, "ENOENT")) return { kind: "missing" };
@@ -4582,7 +4616,7 @@ var ProbeError = class extends Error {
4582
4616
  };
4583
4617
 
4584
4618
  // src/integration/ai/readiness/_engine/probe-fs.ts
4585
- import { promises as fs9 } from "fs";
4619
+ import { promises as fs11 } from "fs";
4586
4620
  import { basename, join as join8 } from "path";
4587
4621
 
4588
4622
  // src/domain/value/kebab-case.ts
@@ -4591,7 +4625,7 @@ var toKebabCase = (input) => input.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").
4591
4625
  // src/integration/ai/readiness/_engine/probe-fs.ts
4592
4626
  var probeFile = async (path) => {
4593
4627
  try {
4594
- const stat = await fs9.stat(path);
4628
+ const stat = await fs11.stat(path);
4595
4629
  if (!stat.isFile()) return Result.ok(void 0);
4596
4630
  return Result.ok({ path });
4597
4631
  } catch (cause) {
@@ -4642,7 +4676,7 @@ var probeNamedFileCollection = async (dir) => {
4642
4676
  };
4643
4677
  var listDir2 = async (dir) => {
4644
4678
  try {
4645
- return Result.ok(await fs9.readdir(dir));
4679
+ return Result.ok(await fs11.readdir(dir));
4646
4680
  } catch (cause) {
4647
4681
  if (isNodeErrnoCode(cause, "ENOENT") || isNodeErrnoCode(cause, "ENOTDIR")) return Result.ok([]);
4648
4682
  if (isNodeErrnoCode(cause, "EACCES")) {
@@ -4655,7 +4689,7 @@ var listDir2 = async (dir) => {
4655
4689
  };
4656
4690
  var statSafely = async (path) => {
4657
4691
  try {
4658
- return Result.ok(await fs9.stat(path));
4692
+ return Result.ok(await fs11.stat(path));
4659
4693
  } catch (cause) {
4660
4694
  if (isNodeErrnoCode(cause, "ENOENT")) return Result.ok(void 0);
4661
4695
  if (isNodeErrnoCode(cause, "EACCES")) {
@@ -4668,7 +4702,7 @@ var statSafely = async (path) => {
4668
4702
  };
4669
4703
  var readFileSafely = async (path) => {
4670
4704
  try {
4671
- return Result.ok(await fs9.readFile(path, "utf8"));
4705
+ return Result.ok(await fs11.readFile(path, "utf8"));
4672
4706
  } catch (cause) {
4673
4707
  if (isNodeErrnoCode(cause, "ENOENT")) return Result.ok(void 0);
4674
4708
  if (isNodeErrnoCode(cause, "EACCES")) {
@@ -4793,14 +4827,14 @@ var codexProbe = {
4793
4827
  };
4794
4828
 
4795
4829
  // src/integration/ai/readiness/copilot/probe.ts
4796
- import { promises as fs10 } from "fs";
4830
+ import { promises as fs12 } from "fs";
4797
4831
  import { join as join11 } from "path";
4798
4832
  var copilotProbe = {
4799
4833
  tool: "copilot",
4800
4834
  async evaluate(repository, now) {
4801
4835
  const path = join11(repository.path, ".github/copilot-instructions.md");
4802
4836
  try {
4803
- const stat = await fs10.stat(path);
4837
+ const stat = await fs12.stat(path);
4804
4838
  if (!stat.isFile()) return Result.ok(absentState(now));
4805
4839
  const artifacts = { tool: "copilot", copilotInstructions: { path } };
4806
4840
  return Result.ok(hasAnyCopilotArtifact(artifacts) ? presentState(now, artifacts) : absentState(now));
@@ -4958,7 +4992,7 @@ var createNpmVersionChecker = (deps) => {
4958
4992
  // package.json
4959
4993
  var package_default = {
4960
4994
  name: "ralphctl",
4961
- version: "0.8.4",
4995
+ version: "0.8.6",
4962
4996
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code, GitHub Copilot, and OpenAI Codex across repositories",
4963
4997
  homepage: "https://github.com/lukas-grigis/ralphctl",
4964
4998
  type: "module",
@@ -5019,6 +5053,7 @@ var package_default = {
5019
5053
  },
5020
5054
  dependencies: {
5021
5055
  commander: "^14.0.3",
5056
+ "cross-spawn": "^7.0.6",
5022
5057
  ink: "^7.0.3",
5023
5058
  react: "^19.2.6",
5024
5059
  "typescript-result": "^3.5.2",
@@ -5026,6 +5061,7 @@ var package_default = {
5026
5061
  },
5027
5062
  devDependencies: {
5028
5063
  "@eslint/js": "^10.0.1",
5064
+ "@types/cross-spawn": "^6.0.6",
5029
5065
  "@types/node": "^25.8.0",
5030
5066
  "@types/react": "^19.2.14",
5031
5067
  "@vitest/coverage-v8": "^4.1.6",
@@ -5087,14 +5123,14 @@ import { mkdir, rm, rmdir, writeFile } from "fs/promises";
5087
5123
  import { join as join14 } from "path";
5088
5124
 
5089
5125
  // src/integration/io/git-exclude.ts
5090
- import { promises as fs11 } from "fs";
5126
+ import { promises as fs13 } from "fs";
5091
5127
  import { isAbsolute as isAbsolute2, join as join13 } from "path";
5092
5128
  var ensureGitExcludeWildcard = async (repoRoot, pattern) => {
5093
5129
  const resolved = await resolveExcludePath(String(repoRoot));
5094
5130
  if (resolved === void 0) return Result.ok(void 0);
5095
5131
  let existing = "";
5096
5132
  try {
5097
- existing = await fs11.readFile(resolved, "utf8");
5133
+ existing = await fs13.readFile(resolved, "utf8");
5098
5134
  } catch (cause) {
5099
5135
  if (isNodeErrnoCode3(cause, "ENOENT")) {
5100
5136
  } else {
@@ -5120,7 +5156,7 @@ var resolveExcludePath = async (repoRoot) => {
5120
5156
  const gitMarker = join13(repoRoot, ".git");
5121
5157
  let stat;
5122
5158
  try {
5123
- stat = await fs11.stat(gitMarker);
5159
+ stat = await fs13.stat(gitMarker);
5124
5160
  } catch (cause) {
5125
5161
  if (isNodeErrnoCode3(cause, "ENOENT") || isNodeErrnoCode3(cause, "ENOTDIR")) return void 0;
5126
5162
  throw cause;
@@ -5131,7 +5167,7 @@ var resolveExcludePath = async (repoRoot) => {
5131
5167
  if (!stat.isFile()) return void 0;
5132
5168
  let pointer;
5133
5169
  try {
5134
- pointer = await fs11.readFile(gitMarker, "utf8");
5170
+ pointer = await fs13.readFile(gitMarker, "utf8");
5135
5171
  } catch {
5136
5172
  return void 0;
5137
5173
  }
@@ -5582,7 +5618,7 @@ var NOOP_CHAIN_LOG_SINK = {
5582
5618
  }
5583
5619
  };
5584
5620
  var isTruthyEnvFlag = (value) => typeof value === "string" && value.length > 0;
5585
- var defaultPipeSpawn = (command, args, options) => nodeSpawn9(command, [...args], {
5621
+ var defaultPipeSpawn = (command, args, options) => crossPlatformSpawn(command, args, {
5586
5622
  ...options,
5587
5623
  stdio: [...options.stdio]
5588
5624
  });
@@ -6626,6 +6662,103 @@ var UnknownViewFallback = ({ id }) => /* @__PURE__ */ jsxs(Box, { flexDirection:
6626
6662
  // src/application/ui/tui/runtime/system-status-context.tsx
6627
6663
  import { createContext as createContext10, useCallback as useCallback5, useContext as useContext10, useEffect as useEffect4, useState as useState6 } from "react";
6628
6664
 
6665
+ // src/integration/io/command-exists.ts
6666
+ import { spawn as spawn2 } from "child_process";
6667
+ var commandExists = (name) => new Promise((resolve) => {
6668
+ const child = process.platform === "win32" ? spawn2("where", [name], { stdio: "ignore" }) : spawn2("command", ["-v", name], { stdio: "ignore", shell: true });
6669
+ let settled = false;
6670
+ const settle = (value) => {
6671
+ if (settled) return;
6672
+ settled = true;
6673
+ resolve(value);
6674
+ };
6675
+ child.on("error", () => settle(false));
6676
+ child.on("exit", (code) => settle(code === 0));
6677
+ });
6678
+
6679
+ // src/integration/system/detect-cli.ts
6680
+ var PROVIDER_BINARY = {
6681
+ "claude-code": "claude",
6682
+ "github-copilot": "copilot",
6683
+ "openai-codex": "codex"
6684
+ };
6685
+ var PROVIDER_INSTALL_GUIDANCE = {
6686
+ "claude-code": {
6687
+ docsUrl: "https://docs.claude.com/en/docs/claude-code/setup",
6688
+ commandsByPlatform: {
6689
+ darwin: [
6690
+ "brew install --cask claude-code",
6691
+ "curl -fsSL https://claude.ai/install.sh | bash",
6692
+ "npm install -g @anthropic-ai/claude-code"
6693
+ ],
6694
+ linux: ["curl -fsSL https://claude.ai/install.sh | bash", "npm install -g @anthropic-ai/claude-code"],
6695
+ win32: [
6696
+ "winget install Anthropic.ClaudeCode",
6697
+ "irm https://claude.ai/install.ps1 | iex",
6698
+ "npm install -g @anthropic-ai/claude-code"
6699
+ ]
6700
+ }
6701
+ },
6702
+ "github-copilot": {
6703
+ docsUrl: "https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli",
6704
+ commandsByPlatform: {
6705
+ darwin: ["brew install copilot-cli", "npm install -g @github/copilot"],
6706
+ linux: ["npm install -g @github/copilot", "brew install copilot-cli"],
6707
+ win32: ["winget install GitHub.Copilot", "npm install -g @github/copilot"]
6708
+ }
6709
+ },
6710
+ "openai-codex": {
6711
+ docsUrl: "https://github.com/openai/codex",
6712
+ commandsByPlatform: {
6713
+ darwin: [
6714
+ "brew install --cask codex",
6715
+ "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
6716
+ "npm install -g @openai/codex"
6717
+ ],
6718
+ linux: ["curl -fsSL https://chatgpt.com/codex/install.sh | sh", "npm install -g @openai/codex"],
6719
+ win32: [
6720
+ 'powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"',
6721
+ "npm install -g @openai/codex"
6722
+ ]
6723
+ }
6724
+ }
6725
+ };
6726
+ var resolveInstallPlatform = (platform = process.platform) => {
6727
+ if (platform === "darwin" || platform === "win32") return platform;
6728
+ return "linux";
6729
+ };
6730
+ var primaryInstallCommand = (provider, platform = process.platform) => {
6731
+ const os = resolveInstallPlatform(platform);
6732
+ const list = PROVIDER_INSTALL_GUIDANCE[provider].commandsByPlatform[os];
6733
+ const first = list[0];
6734
+ if (first === void 0) {
6735
+ throw new Error(`No install command registered for ${provider} on ${os}`);
6736
+ }
6737
+ return first;
6738
+ };
6739
+ var renderProviderInstallGuidance = (provider, platform = process.platform) => {
6740
+ const os = resolveInstallPlatform(platform);
6741
+ const guidance = PROVIDER_INSTALL_GUIDANCE[provider];
6742
+ const commands = guidance.commandsByPlatform[os];
6743
+ const header = `${provider} CLI (${PROVIDER_BINARY[provider]}) not on PATH`;
6744
+ const bullets = commands.map((c) => ` \u2022 ${c}`).join("\n");
6745
+ return `${header}
6746
+ Install options (${os}):
6747
+ ${bullets}
6748
+ Docs: ${guidance.docsUrl}`;
6749
+ };
6750
+ var defaultWhich = commandExists;
6751
+ var detectInstalledProviders = async (options = {}) => {
6752
+ const which = options.which ?? defaultWhich;
6753
+ const providers = Object.keys(PROVIDER_BINARY);
6754
+ const results = await Promise.all(providers.map(async (p) => [p, await which(PROVIDER_BINARY[p])]));
6755
+ const installed = /* @__PURE__ */ new Set();
6756
+ for (const [provider, present] of results) {
6757
+ if (present) installed.add(provider);
6758
+ }
6759
+ return installed;
6760
+ };
6761
+
6629
6762
  // src/application/chain/trace.ts
6630
6763
  var abortedEntry = (elementName, reason) => ({
6631
6764
  elementName,
@@ -6707,11 +6840,6 @@ var isDomainError = (cause) => cause instanceof Error && typeof cause.code === "
6707
6840
  var MIN_NODE_MAJOR = 24;
6708
6841
 
6709
6842
  // src/application/flows/doctor/flow.ts
6710
- var PROVIDER_BINARY = {
6711
- "claude-code": "claude",
6712
- "github-copilot": "copilot",
6713
- "openai-codex": "codex"
6714
- };
6715
6843
  var PROVIDER_LABEL = {
6716
6844
  "claude-code": "Claude Code",
6717
6845
  "github-copilot": "GitHub Copilot",
@@ -7066,30 +7194,47 @@ var probeSprintExecutionPairing = async (sprints, sprintExecutionRepo) => {
7066
7194
  };
7067
7195
  };
7068
7196
 
7069
- // src/integration/io/command-exists.ts
7070
- import { spawn } from "child_process";
7071
- var commandExists = (name) => new Promise((resolve) => {
7072
- const child = spawn(name, ["--version"], { stdio: "ignore" });
7197
+ // src/integration/io/run-command.ts
7198
+ var PROBE_TIMEOUT_MS = 5e3;
7199
+ var runCommand = (name, args) => new Promise((resolve) => {
7200
+ let child;
7201
+ try {
7202
+ child = crossPlatformSpawn(name, args, { stdio: ["ignore", "pipe", "pipe"] });
7203
+ } catch {
7204
+ resolve({ ok: false, code: null, stdout: "", stderr: "" });
7205
+ return;
7206
+ }
7207
+ const stdoutChunks = [];
7208
+ const stderrChunks = [];
7073
7209
  let settled = false;
7074
- const settle = (value) => {
7210
+ const settle = (result) => {
7075
7211
  if (settled) return;
7076
7212
  settled = true;
7077
- resolve(value);
7213
+ clearTimeout(timer);
7214
+ resolve(result);
7078
7215
  };
7079
- child.on("error", () => settle(false));
7080
- child.on("exit", () => settle(true));
7081
- });
7082
-
7083
- // src/integration/io/run-command.ts
7084
- import { execFile } from "child_process";
7085
- var runCommand = (name, args) => new Promise((resolve) => {
7086
- execFile(name, [...args], { timeout: 5e3, encoding: "utf8" }, (err, stdout, stderr) => {
7087
- if (err === null) {
7088
- resolve({ ok: true, code: 0, stdout, stderr });
7089
- return;
7216
+ const timer = setTimeout(() => {
7217
+ try {
7218
+ child.kill("SIGTERM");
7219
+ } catch {
7090
7220
  }
7091
- const code = typeof err.code === "number" ? err.code : null;
7092
- resolve({ ok: false, code, stdout, stderr });
7221
+ settle({
7222
+ ok: false,
7223
+ code: null,
7224
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
7225
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
7226
+ });
7227
+ }, PROBE_TIMEOUT_MS);
7228
+ child.stdout?.on("data", (c) => stdoutChunks.push(c));
7229
+ child.stderr?.on("data", (c) => stderrChunks.push(c));
7230
+ child.on("error", () => settle({ ok: false, code: null, stdout: "", stderr: "" }));
7231
+ child.on("close", (code) => {
7232
+ settle({
7233
+ ok: code === 0,
7234
+ code,
7235
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
7236
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
7237
+ });
7093
7238
  });
7094
7239
  });
7095
7240
 
@@ -11206,16 +11351,16 @@ import { join as join23 } from "path";
11206
11351
 
11207
11352
  // src/application/flows/refine/flow.ts
11208
11353
  import { dirname as dirname11, join as join22 } from "path";
11209
- import { promises as fs15 } from "fs";
11354
+ import { promises as fs17 } from "fs";
11210
11355
 
11211
11356
  // src/application/flows/_shared/build-unit.ts
11212
- import { promises as fs12 } from "fs";
11357
+ import { promises as fs14 } from "fs";
11213
11358
  import { join as join17 } from "path";
11214
11359
  var buildUnitLeaf = (opts) => leaf(opts.name, {
11215
11360
  useCase: {
11216
11361
  execute: async (input) => {
11217
11362
  try {
11218
- await fs12.mkdir(input.path, { recursive: true });
11363
+ await fs14.mkdir(input.path, { recursive: true });
11219
11364
  } catch (cause) {
11220
11365
  return Result.error(
11221
11366
  new StorageError({
@@ -11327,7 +11472,7 @@ var fetchIssueContextLeaf = (deps, ticket) => leaf(`fetch-issue-context-${String
11327
11472
  });
11328
11473
 
11329
11474
  // src/application/flows/refine/leaves/refine-ticket-interactive.ts
11330
- import { promises as fs14 } from "fs";
11475
+ import { promises as fs16 } from "fs";
11331
11476
  import { dirname as dirname10, join as join20 } from "path";
11332
11477
 
11333
11478
  // src/business/ticket/refine-ticket.ts
@@ -11425,14 +11570,14 @@ var renderFilename = (filename, index, multiplicity) => {
11425
11570
  };
11426
11571
 
11427
11572
  // src/integration/ai/contract/_engine/validate-signals-file.ts
11428
- import { promises as fs13 } from "fs";
11573
+ import { promises as fs15 } from "fs";
11429
11574
  import { join as join19 } from "path";
11430
11575
  var SIGNALS_FILENAME = "signals.json";
11431
11576
  var validateSignalsFile = async (outputDir, contract) => {
11432
11577
  const path = join19(String(outputDir), SIGNALS_FILENAME);
11433
11578
  let bytes;
11434
11579
  try {
11435
- bytes = await fs13.readFile(path, "utf8");
11580
+ bytes = await fs15.readFile(path, "utf8");
11436
11581
  } catch (cause) {
11437
11582
  if (isNodeErrnoCode(cause, "ENOENT") || isNodeErrnoCode(cause, "ENOTDIR")) {
11438
11583
  return Result.error(
@@ -11608,7 +11753,7 @@ var remapRefineSignalsError = (error) => {
11608
11753
  };
11609
11754
  var warnDroppedSignals = async (deps, outputDir) => {
11610
11755
  try {
11611
- const bytes = await fs14.readFile(join20(String(outputDir), "signals.json"), "utf8");
11756
+ const bytes = await fs16.readFile(join20(String(outputDir), "signals.json"), "utf8");
11612
11757
  const raw = JSON.parse(bytes);
11613
11758
  const inner = Array.isArray(raw) ? raw : raw.signals;
11614
11759
  if (!Array.isArray(inner)) return;
@@ -11999,7 +12144,7 @@ var stampSessionMetaLeaf = (deps, opts) => leaf(opts.name, {
11999
12144
  var readSprintProgress = async (refinementRoot) => {
12000
12145
  const sprintDir2 = dirname11(String(refinementRoot));
12001
12146
  try {
12002
- return await fs15.readFile(join22(sprintDir2, "progress.md"), "utf8");
12147
+ return await fs17.readFile(join22(sprintDir2, "progress.md"), "utf8");
12003
12148
  } catch {
12004
12149
  return "";
12005
12150
  }
@@ -12138,106 +12283,6 @@ var createRefineFlow = (deps, opts) => {
12138
12283
  ]);
12139
12284
  };
12140
12285
 
12141
- // src/integration/system/detect-cli.ts
12142
- import { spawn as spawn2 } from "child_process";
12143
- var PROVIDER_BINARY2 = {
12144
- "claude-code": "claude",
12145
- "github-copilot": "gh",
12146
- "openai-codex": "codex"
12147
- };
12148
- var PROVIDER_INSTALL_GUIDANCE = {
12149
- "claude-code": {
12150
- docsUrl: "https://docs.claude.com/en/docs/claude-code/setup",
12151
- commandsByPlatform: {
12152
- darwin: [
12153
- "brew install --cask claude-code",
12154
- "curl -fsSL https://claude.ai/install.sh | bash",
12155
- "npm install -g @anthropic-ai/claude-code"
12156
- ],
12157
- linux: ["curl -fsSL https://claude.ai/install.sh | bash", "npm install -g @anthropic-ai/claude-code"],
12158
- win32: [
12159
- "winget install Anthropic.ClaudeCode",
12160
- "irm https://claude.ai/install.ps1 | iex",
12161
- "npm install -g @anthropic-ai/claude-code"
12162
- ]
12163
- }
12164
- },
12165
- "github-copilot": {
12166
- docsUrl: "https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-in-the-cli",
12167
- commandsByPlatform: {
12168
- darwin: ["brew install gh && gh extension install github/gh-copilot", "gh extension install github/gh-copilot"],
12169
- linux: [
12170
- "install gh from https://github.com/cli/cli/blob/trunk/docs/install_linux.md, then: gh extension install github/gh-copilot",
12171
- "gh extension install github/gh-copilot"
12172
- ],
12173
- win32: [
12174
- "winget install --id GitHub.cli && gh extension install github/gh-copilot",
12175
- "gh extension install github/gh-copilot"
12176
- ]
12177
- }
12178
- },
12179
- "openai-codex": {
12180
- docsUrl: "https://github.com/openai/codex",
12181
- commandsByPlatform: {
12182
- darwin: [
12183
- "brew install --cask codex",
12184
- "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
12185
- "npm install -g @openai/codex"
12186
- ],
12187
- linux: ["curl -fsSL https://chatgpt.com/codex/install.sh | sh", "npm install -g @openai/codex"],
12188
- win32: [
12189
- 'powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"',
12190
- "npm install -g @openai/codex"
12191
- ]
12192
- }
12193
- }
12194
- };
12195
- var resolveInstallPlatform = (platform = process.platform) => {
12196
- if (platform === "darwin" || platform === "win32") return platform;
12197
- return "linux";
12198
- };
12199
- var primaryInstallCommand = (provider, platform = process.platform) => {
12200
- const os = resolveInstallPlatform(platform);
12201
- const list = PROVIDER_INSTALL_GUIDANCE[provider].commandsByPlatform[os];
12202
- const first = list[0];
12203
- if (first === void 0) {
12204
- throw new Error(`No install command registered for ${provider} on ${os}`);
12205
- }
12206
- return first;
12207
- };
12208
- var renderProviderInstallGuidance = (provider, platform = process.platform) => {
12209
- const os = resolveInstallPlatform(platform);
12210
- const guidance = PROVIDER_INSTALL_GUIDANCE[provider];
12211
- const commands = guidance.commandsByPlatform[os];
12212
- const header = `${provider} CLI (${PROVIDER_BINARY2[provider]}) not on PATH`;
12213
- const bullets = commands.map((c) => ` \u2022 ${c}`).join("\n");
12214
- return `${header}
12215
- Install options (${os}):
12216
- ${bullets}
12217
- Docs: ${guidance.docsUrl}`;
12218
- };
12219
- var defaultWhich = (binary) => new Promise((resolve) => {
12220
- const child = spawn2("command", ["-v", binary], { stdio: "pipe", shell: true });
12221
- let settled = false;
12222
- const settle = (value) => {
12223
- if (settled) return;
12224
- settled = true;
12225
- resolve(value);
12226
- };
12227
- child.on("error", () => settle(false));
12228
- child.on("exit", (code) => settle(code === 0));
12229
- });
12230
- var detectInstalledProviders = async (options = {}) => {
12231
- const which = options.which ?? defaultWhich;
12232
- const providers = Object.keys(PROVIDER_BINARY2);
12233
- const results = await Promise.all(providers.map(async (p) => [p, await which(PROVIDER_BINARY2[p])]));
12234
- const installed = /* @__PURE__ */ new Set();
12235
- for (const [provider, present] of results) {
12236
- if (present) installed.add(provider);
12237
- }
12238
- return installed;
12239
- };
12240
-
12241
12286
  // src/application/ui/shared/launch/check-cli.ts
12242
12287
  var aiFlowIdForCheck = (flowId) => {
12243
12288
  switch (flowId) {
@@ -12283,7 +12328,7 @@ var rowExpectationsFor = (aiFlow, settings, options) => {
12283
12328
  };
12284
12329
  var renderMissing = (missing, aiFlow) => {
12285
12330
  const formatOne = (m) => {
12286
- const binary = PROVIDER_BINARY2[m.provider];
12331
+ const binary = PROVIDER_BINARY[m.provider];
12287
12332
  const installHint = primaryInstallCommand(m.provider);
12288
12333
  const docsUrl = PROVIDER_INSTALL_GUIDANCE[m.provider].docsUrl;
12289
12334
  const roleSuffix = m.role !== void 0 ? ` (${m.role})` : "";
@@ -12402,7 +12447,7 @@ import { join as join25 } from "path";
12402
12447
 
12403
12448
  // src/application/flows/plan/flow.ts
12404
12449
  import { dirname as dirname13, join as join24 } from "path";
12405
- import { promises as fs16 } from "fs";
12450
+ import { promises as fs18 } from "fs";
12406
12451
 
12407
12452
  // src/application/flows/_shared/sprint/load-execution.ts
12408
12453
  var loadSprintExecutionLeaf = (deps, name = "load-sprint-execution") => leaf(name, {
@@ -13183,7 +13228,7 @@ var callPlannerInteractiveLeaf = (deps) => leaf("call-planner-interactive", {
13183
13228
  var readSprintProgress2 = async (planRoot) => {
13184
13229
  const sprintDir2 = dirname13(String(planRoot));
13185
13230
  try {
13186
- return await fs16.readFile(join24(sprintDir2, "progress.md"), "utf8");
13231
+ return await fs18.readFile(join24(sprintDir2, "progress.md"), "utf8");
13187
13232
  } catch {
13188
13233
  return "";
13189
13234
  }
@@ -13688,7 +13733,7 @@ var branchPreflightLeaf = (deps, opts, name = "branch-preflight") => leaf(name,
13688
13733
  });
13689
13734
 
13690
13735
  // src/application/flows/implement/leaves/build-task-workspace.ts
13691
- import { promises as fs17 } from "fs";
13736
+ import { promises as fs19 } from "fs";
13692
13737
  import { join as join26 } from "path";
13693
13738
 
13694
13739
  // src/integration/ai/prompts/_engine/renderers/task.ts
@@ -13998,8 +14043,8 @@ ${body}
13998
14043
  // src/application/flows/implement/leaves/build-task-workspace.ts
13999
14044
  var writeOrError = async (path, content) => {
14000
14045
  try {
14001
- await fs17.mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
14002
- await fs17.writeFile(path, content, "utf8");
14046
+ await fs19.mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
14047
+ await fs19.writeFile(path, content, "utf8");
14003
14048
  return Result.ok(void 0);
14004
14049
  } catch (cause) {
14005
14050
  return Result.error(
@@ -14723,7 +14768,7 @@ var loop = (name, body, opts = {}) => ({
14723
14768
 
14724
14769
  // src/application/flows/implement/leaves/evaluator.ts
14725
14770
  import { dirname as dirname15, join as join28 } from "path";
14726
- import { promises as fs19 } from "fs";
14771
+ import { promises as fs21 } from "fs";
14727
14772
 
14728
14773
  // src/business/task/plateau-detection.ts
14729
14774
  var failedDimensions = (signal) => {
@@ -15174,7 +15219,7 @@ var evaluatorOutputContract = {
15174
15219
  };
15175
15220
 
15176
15221
  // src/application/flows/implement/leaves/round-artifacts.ts
15177
- import { promises as fs18 } from "fs";
15222
+ import { promises as fs20 } from "fs";
15178
15223
  import { join as join27 } from "path";
15179
15224
  var nextRoundNum = async (workspaceRoot) => {
15180
15225
  const entries = await listDir(join27(String(workspaceRoot), "rounds"));
@@ -15191,7 +15236,7 @@ var readRoundSessionId = async (workspaceRoot, round, role) => {
15191
15236
  const path = join27(String(workspaceRoot), "rounds", String(round), role, "session-id.txt");
15192
15237
  let content;
15193
15238
  try {
15194
- content = await fs18.readFile(path, "utf8");
15239
+ content = await fs20.readFile(path, "utf8");
15195
15240
  } catch {
15196
15241
  return void 0;
15197
15242
  }
@@ -15211,7 +15256,7 @@ var writeRoundPrompt = async (workspaceRoot, round, role, prompt, logger) => {
15211
15256
  // src/application/flows/implement/leaves/evaluator.ts
15212
15257
  var readProgressFile = async (path) => {
15213
15258
  try {
15214
- return await fs19.readFile(path, "utf8");
15259
+ return await fs21.readFile(path, "utf8");
15215
15260
  } catch {
15216
15261
  return "";
15217
15262
  }
@@ -15335,7 +15380,7 @@ var evaluatorLeaf = (deps, taskId) => leaf(`evaluator-${String(taskId)}`, {
15335
15380
 
15336
15381
  // src/application/flows/implement/leaves/generator.ts
15337
15382
  import { dirname as dirname16, join as join29 } from "path";
15338
- import { promises as fs20 } from "fs";
15383
+ import { promises as fs22 } from "fs";
15339
15384
 
15340
15385
  // src/business/task/run-generator-turn.ts
15341
15386
  var findTaskBlocked = (signals) => signals.find((s) => s.type === "task-blocked")?.reason;
@@ -15402,7 +15447,7 @@ var latestCritique = (task) => {
15402
15447
  // src/application/flows/implement/leaves/generator.ts
15403
15448
  var readProgressFile2 = async (path) => {
15404
15449
  try {
15405
- return await fs20.readFile(path, "utf8");
15450
+ return await fs22.readFile(path, "utf8");
15406
15451
  } catch {
15407
15452
  return "";
15408
15453
  }
@@ -18036,7 +18081,7 @@ var launchImplement = async (ctx) => {
18036
18081
  import { join as join38 } from "path";
18037
18082
 
18038
18083
  // src/application/flows/review/leaves/ensure-feedback-file.ts
18039
- import { promises as fs21 } from "fs";
18084
+ import { promises as fs23 } from "fs";
18040
18085
 
18041
18086
  // src/business/feedback/md-parser.ts
18042
18087
  var ROUND_HEADING_RE = /^##\s+Round\s+(\d+)\s*$/;
@@ -18096,12 +18141,12 @@ var ensureFeedbackFileLeaf = (feedbackFile) => leaf("ensure-feedback-file", {
18096
18141
  useCase: {
18097
18142
  execute: async (path) => {
18098
18143
  try {
18099
- await fs21.access(String(path));
18144
+ await fs23.access(String(path));
18100
18145
  return Result.ok(void 0);
18101
18146
  } catch {
18102
18147
  }
18103
18148
  try {
18104
- await fs21.writeFile(String(path), TEMPLATE, { flag: "wx" });
18149
+ await fs23.writeFile(String(path), TEMPLATE, { flag: "wx" });
18105
18150
  return Result.ok(void 0);
18106
18151
  } catch (cause) {
18107
18152
  if (typeof cause === "object" && cause !== null && cause.code === "EEXIST") {
@@ -18123,7 +18168,7 @@ var ensureFeedbackFileLeaf = (feedbackFile) => leaf("ensure-feedback-file", {
18123
18168
  });
18124
18169
 
18125
18170
  // src/application/flows/review/leaves/review-round.ts
18126
- import { promises as fs22 } from "fs";
18171
+ import { promises as fs24 } from "fs";
18127
18172
  import { join as join37 } from "path";
18128
18173
 
18129
18174
  // src/business/feedback/apply-feedback.ts
@@ -18306,7 +18351,7 @@ var reviewRoundOutputContract = {
18306
18351
  var readProgressSnippet = async (path) => {
18307
18352
  if (path === void 0) return "_(no progress file)_";
18308
18353
  try {
18309
- const content = await fs22.readFile(String(path), "utf8");
18354
+ const content = await fs24.readFile(String(path), "utf8");
18310
18355
  return content.length > 4e3 ? `${content.slice(0, 4e3)}
18311
18356
  [truncated]` : content;
18312
18357
  } catch {
@@ -18322,7 +18367,7 @@ ${renderEmptyRound(nextIndex)}${ROUND_SEPARATOR}
18322
18367
  };
18323
18368
  var writeRoundBody = async (path, body) => {
18324
18369
  try {
18325
- const content = await fs22.readFile(String(path), "utf8");
18370
+ const content = await fs24.readFile(String(path), "utf8");
18326
18371
  const lastMarker = content.lastIndexOf(MARKER_COMMENT);
18327
18372
  if (lastMarker === -1) {
18328
18373
  return Result.error(
@@ -18339,7 +18384,7 @@ var writeRoundBody = async (path, body) => {
18339
18384
  const next = `${head}
18340
18385
  ${body.replace(/\s+$/u, "")}
18341
18386
  ${tail}`;
18342
- await fs22.writeFile(String(path), next, "utf8");
18387
+ await fs24.writeFile(String(path), next, "utf8");
18343
18388
  return Result.ok(void 0);
18344
18389
  } catch (cause) {
18345
18390
  return Result.error(
@@ -18363,7 +18408,7 @@ var allocateRoundPaths = (reviewRoot, roundIndex) => {
18363
18408
  };
18364
18409
  var ensureRoundDir = async (dir) => {
18365
18410
  try {
18366
- await fs22.mkdir(String(dir), { recursive: true });
18411
+ await fs24.mkdir(String(dir), { recursive: true });
18367
18412
  return Result.ok(void 0);
18368
18413
  } catch (cause) {
18369
18414
  return Result.error(
@@ -18397,7 +18442,7 @@ var reviewRoundLeaf = (deps, opts) => leaf("review-round", {
18397
18442
  if (!wrote.ok) return Result.error(wrote.error);
18398
18443
  return Result.ok(void 0);
18399
18444
  },
18400
- readFeedbackFile: () => fs22.readFile(String(input.feedbackFile), "utf8"),
18445
+ readFeedbackFile: () => fs24.readFile(String(input.feedbackFile), "utf8"),
18401
18446
  readProgressSnippet: () => readProgressSnippet(input.progressFile),
18402
18447
  buildPrompt: async (params) => {
18403
18448
  const outputContractSection = renderContractSectionFor(reviewRoundOutputContract, paths.value.outputDir);
@@ -18875,7 +18920,7 @@ var probeReadinessLeaf = (deps, tool) => leaf(`probe-${tool}`, {
18875
18920
  });
18876
18921
 
18877
18922
  // src/application/flows/readiness/leaves/propose.ts
18878
- import { promises as fs23 } from "fs";
18923
+ import { promises as fs25 } from "fs";
18879
18924
 
18880
18925
  // src/integration/ai/readiness/_engine/setup.ts
18881
18926
  import { join as join40 } from "path";
@@ -19174,7 +19219,7 @@ var proposeReadinessUseCase = async (deps, tool, input) => {
19174
19219
  let existingBody;
19175
19220
  if (existingPath !== void 0) {
19176
19221
  try {
19177
- existingBody = await fs23.readFile(existingPath, "utf8");
19222
+ existingBody = await fs25.readFile(existingPath, "utf8");
19178
19223
  } catch {
19179
19224
  existingBody = void 0;
19180
19225
  }
@@ -19292,7 +19337,7 @@ var proposeReadinessLeaf = (deps, tool) => leaf(`propose-${tool}`, {
19292
19337
  });
19293
19338
 
19294
19339
  // src/application/flows/readiness/leaves/write.ts
19295
- import { promises as fs24 } from "fs";
19340
+ import { promises as fs26 } from "fs";
19296
19341
  var writeReadinessUseCase = async (deps, tool, input) => {
19297
19342
  const log = deps.logger.named(`readiness.write-${tool}`);
19298
19343
  if (!input.accepted || input.proposal === void 0) {
@@ -19326,7 +19371,7 @@ var makeBackupPath = (targetPath, now) => {
19326
19371
  };
19327
19372
  var fileExists = async (path) => {
19328
19373
  try {
19329
- const stat = await fs24.stat(path);
19374
+ const stat = await fs26.stat(path);
19330
19375
  return stat.isFile();
19331
19376
  } catch {
19332
19377
  return false;
@@ -19334,7 +19379,7 @@ var fileExists = async (path) => {
19334
19379
  };
19335
19380
  var safeReadText = async (path) => {
19336
19381
  try {
19337
- return await fs24.readFile(path, "utf8");
19382
+ return await fs26.readFile(path, "utf8");
19338
19383
  } catch {
19339
19384
  return void 0;
19340
19385
  }
@@ -19359,11 +19404,11 @@ var writeReadinessLeaf = (deps, tool) => leaf(`write-${tool}`, {
19359
19404
  });
19360
19405
 
19361
19406
  // src/application/flows/_shared/allocate-run-dir.ts
19362
- import { promises as fs26 } from "fs";
19407
+ import { promises as fs28 } from "fs";
19363
19408
  import { join as join42 } from "path";
19364
19409
 
19365
19410
  // src/integration/ai/runs/_engine/run-artifacts.ts
19366
- import { promises as fs25 } from "fs";
19411
+ import { promises as fs27 } from "fs";
19367
19412
  import { join as join41 } from "path";
19368
19413
  var BODY_PREVIEW_LIMIT = 800;
19369
19414
  var buildRunDirName = () => {
@@ -19374,7 +19419,7 @@ var buildRunDirName = () => {
19374
19419
  var readRunBodyPreview = async (runDir, options) => {
19375
19420
  let raw;
19376
19421
  try {
19377
- raw = await fs25.readFile(join41(String(runDir), "body.txt"), "utf8");
19422
+ raw = await fs27.readFile(join41(String(runDir), "body.txt"), "utf8");
19378
19423
  } catch (cause) {
19379
19424
  if (isErrnoException(cause) && cause.code === "ENOENT") return void 0;
19380
19425
  const code = isErrnoException(cause) ? cause.code : "unknown";
@@ -19394,7 +19439,7 @@ var allocateRunDirLeaf = (opts) => leaf(opts.name, {
19394
19439
  useCase: {
19395
19440
  execute: async (input) => {
19396
19441
  try {
19397
- await fs26.mkdir(input.path, { recursive: true });
19442
+ await fs28.mkdir(input.path, { recursive: true });
19398
19443
  } catch (cause) {
19399
19444
  return Result.error(
19400
19445
  new StorageError({
@@ -20868,7 +20913,7 @@ import { join as join46 } from "path";
20868
20913
 
20869
20914
  // src/application/flows/ideate/flow.ts
20870
20915
  import { dirname as dirname19, join as join45 } from "path";
20871
- import { promises as fs27 } from "fs";
20916
+ import { promises as fs29 } from "fs";
20872
20917
 
20873
20918
  // src/integration/ai/prompts/ideate/definition.ts
20874
20919
  var nonEmpty2 = (field) => (v) => v.trim().length === 0 ? Result.error(new ValidationError({ field, value: v, message: `${field} must not be empty` })) : Result.ok(v);
@@ -21143,7 +21188,7 @@ var ideateAndPlanLeaf = (deps) => leaf("ideate-and-plan", {
21143
21188
  var readSprintProgress3 = async (ideateRoot) => {
21144
21189
  const sprintDir2 = dirname19(String(ideateRoot));
21145
21190
  try {
21146
- return await fs27.readFile(join45(sprintDir2, "progress.md"), "utf8");
21191
+ return await fs29.readFile(join45(sprintDir2, "progress.md"), "utf8");
21147
21192
  } catch {
21148
21193
  return "";
21149
21194
  }
@@ -27087,7 +27132,7 @@ var createSettingsSetProviderFlow = (deps) => leaf("settings-set-provider", {
27087
27132
  new ValidationError({
27088
27133
  field: settingsKey,
27089
27134
  value: input.provider,
27090
- message: `${input.provider} CLI (${PROVIDER_BINARY2[input.provider]}) not on PATH \u2014 cannot set ${settingsKey}`,
27135
+ message: `${input.provider} CLI (${PROVIDER_BINARY[input.provider]}) not on PATH \u2014 cannot set ${settingsKey}`,
27091
27136
  hint: renderProviderInstallGuidance(input.provider)
27092
27137
  })
27093
27138
  );
@@ -28788,7 +28833,7 @@ import { basename as basename5, join as join52 } from "path";
28788
28833
 
28789
28834
  // src/application/ui/tui/prompts/path-picker-prompt.tsx
28790
28835
  import { useEffect as useEffect38, useState as useState44 } from "react";
28791
- import { promises as fs28 } from "fs";
28836
+ import { promises as fs30 } from "fs";
28792
28837
  import { dirname as dirname20, join as join51 } from "path";
28793
28838
  import { homedir } from "os";
28794
28839
  import { Box as Box70, Text as Text69, useInput as useInput28 } from "ink";
@@ -28815,7 +28860,7 @@ var PathPickerPrompt = ({
28815
28860
  useEffect38(() => {
28816
28861
  const load = async () => {
28817
28862
  try {
28818
- const items = await fs28.readdir(cwd, { withFileTypes: true });
28863
+ const items = await fs30.readdir(cwd, { withFileTypes: true });
28819
28864
  const filtered = items.filter((d) => showHidden || !d.name.startsWith(".")).filter((d) => d.isDirectory()).map((d) => ({ name: d.name, isDirectory: true })).sort((a, b) => a.name.localeCompare(b.name));
28820
28865
  setEntries(filtered);
28821
28866
  setError(void 0);
@@ -28898,7 +28943,7 @@ var PathPickerPrompt = ({
28898
28943
  return;
28899
28944
  }
28900
28945
  try {
28901
- const stat = await fs28.stat(expanded);
28946
+ const stat = await fs30.stat(expanded);
28902
28947
  if (!stat.isDirectory()) {
28903
28948
  setError(`${expanded} is not a directory`);
28904
28949
  setTyping(false);
@@ -30225,7 +30270,7 @@ import { useEffect as useEffect45, useMemo as useMemo26, useRef as useRef13 } fr
30225
30270
  import { useApp, useInput as useInput32 } from "ink";
30226
30271
 
30227
30272
  // src/integration/io/clipboard.ts
30228
- import { spawn as nodeSpawn10 } from "child_process";
30273
+ import { spawn as nodeSpawn2 } from "child_process";
30229
30274
  var resolveHelpers = ({ platform, env }) => {
30230
30275
  if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
30231
30276
  if (platform === "win32") return [{ cmd: "clip.exe", args: [] }];
@@ -30290,7 +30335,7 @@ var runHelper = (spawn3, helper, text) => new Promise((resolve) => {
30290
30335
  }
30291
30336
  });
30292
30337
  var createCopyToClipboard = (opts = {}) => {
30293
- const spawn3 = opts.spawn ?? nodeSpawn10;
30338
+ const spawn3 = opts.spawn ?? nodeSpawn2;
30294
30339
  const platform = opts.platform ?? process.platform;
30295
30340
  const env = opts.env ?? process.env;
30296
30341
  const helpers = resolveHelpers({ platform, env });
@@ -30497,7 +30542,7 @@ var ChainLogDegradedBanner = () => {
30497
30542
  };
30498
30543
 
30499
30544
  // src/application/ui/tui/components/progress-overlay.tsx
30500
- import { promises as fs29 } from "fs";
30545
+ import { promises as fs31 } from "fs";
30501
30546
  import { join as join54 } from "path";
30502
30547
  import { useEffect as useEffect48, useMemo as useMemo27, useState as useState53 } from "react";
30503
30548
  import { Box as Box82, Text as Text80, useInput as useInput33 } from "ink";
@@ -30528,7 +30573,7 @@ var ProgressOverlay = () => {
30528
30573
  }
30529
30574
  const load = async () => {
30530
30575
  try {
30531
- const [stat, content] = await Promise.all([fs29.stat(progressPath), fs29.readFile(progressPath, "utf8")]);
30576
+ const [stat, content] = await Promise.all([fs31.stat(progressPath), fs31.readFile(progressPath, "utf8")]);
30532
30577
  if (cancelled) return;
30533
30578
  const modifiedAtMs = stat.mtimeMs;
30534
30579
  if (content.trim().length === 0) {
@@ -30733,7 +30778,7 @@ var resolveInitialState = ({
30733
30778
  };
30734
30779
 
30735
30780
  // src/integration/persistence/selection/last-selection-store.ts
30736
- import { promises as fs30 } from "fs";
30781
+ import { promises as fs32 } from "fs";
30737
30782
  import { join as join55 } from "path";
30738
30783
  var FILE_NAME = "last-selection.json";
30739
30784
  var createLastSelectionStore = (stateRoot) => {
@@ -30741,7 +30786,7 @@ var createLastSelectionStore = (stateRoot) => {
30741
30786
  return {
30742
30787
  async read() {
30743
30788
  try {
30744
- const raw = await fs30.readFile(path, "utf8");
30789
+ const raw = await fs32.readFile(path, "utf8");
30745
30790
  const parsed = JSON.parse(raw);
30746
30791
  if (typeof parsed !== "object" || parsed === null) return void 0;
30747
30792
  const rec = parsed;
@@ -30759,10 +30804,10 @@ var createLastSelectionStore = (stateRoot) => {
30759
30804
  async write(value) {
30760
30805
  try {
30761
30806
  if (value === void 0) {
30762
- await fs30.rm(path, { force: true });
30807
+ await fs32.rm(path, { force: true });
30763
30808
  return;
30764
30809
  }
30765
- await fs30.writeFile(path, JSON.stringify(value, null, 2), "utf8");
30810
+ await fs32.writeFile(path, JSON.stringify(value, null, 2), "utf8");
30766
30811
  } catch {
30767
30812
  }
30768
30813
  }
@@ -30858,7 +30903,7 @@ var startHeapWatchdog = (deps) => {
30858
30903
  import { execFile as execFileCb } from "child_process";
30859
30904
  import { promisify } from "util";
30860
30905
  import { platform as osPlatform } from "os";
30861
- var execFile2 = promisify(execFileCb);
30906
+ var execFile = promisify(execFileCb);
30862
30907
  var SHELL_TIMEOUT_MS = 5e3;
30863
30908
  var BELL = "\x07";
30864
30909
  var defaultEmitBell = () => {
@@ -30867,7 +30912,7 @@ var defaultEmitBell = () => {
30867
30912
  } catch {
30868
30913
  }
30869
30914
  };
30870
- var defaultExecFile = (command, args, options) => execFile2(command, [...args], { timeout: options.timeout }).then((r) => ({
30915
+ var defaultExecFile = (command, args, options) => execFile(command, [...args], { timeout: options.timeout }).then((r) => ({
30871
30916
  stdout: r.stdout.toString(),
30872
30917
  stderr: r.stderr.toString()
30873
30918
  }));
@@ -31987,11 +32032,11 @@ var formatTaskLine = (t) => {
31987
32032
  };
31988
32033
 
31989
32034
  // src/application/ui/cli/commands/runs.ts
31990
- import { promises as fs32 } from "fs";
32035
+ import { promises as fs34 } from "fs";
31991
32036
  import { createInterface } from "readline";
31992
32037
 
31993
32038
  // src/integration/ai/runs/_engine/run-enumeration.ts
31994
- import { promises as fs31 } from "fs";
32039
+ import { promises as fs33 } from "fs";
31995
32040
  import { join as join57 } from "path";
31996
32041
  var parseRunTimestamp = (runDirName) => {
31997
32042
  const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z-/.exec(runDirName);
@@ -32091,7 +32136,7 @@ var listRuns = async (runsRoot) => {
32091
32136
  const root = String(runsRoot);
32092
32137
  let flowDirs;
32093
32138
  try {
32094
- flowDirs = await fs31.readdir(root, { withFileTypes: true });
32139
+ flowDirs = await fs33.readdir(root, { withFileTypes: true });
32095
32140
  } catch (cause) {
32096
32141
  if (isErrnoException2(cause) && cause.code === "ENOENT") return Result.ok([]);
32097
32142
  return Result.error(
@@ -32108,7 +32153,7 @@ var listRuns = async (runsRoot) => {
32108
32153
  const flowPath = join57(root, flowDir.name);
32109
32154
  let runDirs;
32110
32155
  try {
32111
- runDirs = await fs31.readdir(flowPath, { withFileTypes: true });
32156
+ runDirs = await fs33.readdir(flowPath, { withFileTypes: true });
32112
32157
  } catch (cause) {
32113
32158
  if (isErrnoException2(cause) && cause.code === "ENOENT") continue;
32114
32159
  return Result.error(
@@ -32153,7 +32198,7 @@ var computeDirSize = async (dir) => {
32153
32198
  let total = 0;
32154
32199
  let entries;
32155
32200
  try {
32156
- entries = await fs31.readdir(dir, { withFileTypes: true });
32201
+ entries = await fs33.readdir(dir, { withFileTypes: true });
32157
32202
  } catch {
32158
32203
  return 0;
32159
32204
  }
@@ -32164,7 +32209,7 @@ var computeDirSize = async (dir) => {
32164
32209
  continue;
32165
32210
  }
32166
32211
  try {
32167
- const stat = await fs31.lstat(entryPath);
32212
+ const stat = await fs33.lstat(entryPath);
32168
32213
  if (stat.isFile()) total += stat.size;
32169
32214
  } catch {
32170
32215
  }
@@ -32459,7 +32504,7 @@ var performPrune = async (candidates) => {
32459
32504
  let freedCount = 0;
32460
32505
  for (const candidate of candidates) {
32461
32506
  try {
32462
- await fs32.rm(String(candidate.path), { recursive: true, force: false });
32507
+ await fs34.rm(String(candidate.path), { recursive: true, force: false });
32463
32508
  freedBytes += candidate.sizeBytes;
32464
32509
  freedCount += 1;
32465
32510
  } catch (cause) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "generatedAt": "2026-05-28T21:15:32.467Z",
3
+ "generatedAt": "2026-05-29T12:47:39.138Z",
4
4
  "assets": [
5
5
  "prompts/_partials/conventions-agents-md.md",
6
6
  "prompts/_partials/conventions-claude-md.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralphctl",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Agent harness for long-running AI coding tasks — orchestrates Claude Code, GitHub Copilot, and OpenAI Codex across repositories",
5
5
  "homepage": "https://github.com/lukas-grigis/ralphctl",
6
6
  "type": "module",
@@ -39,6 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "commander": "^14.0.3",
42
+ "cross-spawn": "^7.0.6",
42
43
  "ink": "^7.0.3",
43
44
  "react": "^19.2.6",
44
45
  "typescript-result": "^3.5.2",
@@ -46,6 +47,7 @@
46
47
  },
47
48
  "devDependencies": {
48
49
  "@eslint/js": "^10.0.1",
50
+ "@types/cross-spawn": "^6.0.6",
49
51
  "@types/node": "^25.8.0",
50
52
  "@types/react": "^19.2.14",
51
53
  "@vitest/coverage-v8": "^4.1.6",