@mycompbox/compbox-cli 1.18.0 → 1.19.0

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.
Files changed (48) hide show
  1. package/.tsbuildinfo +1 -1
  2. package/commands/configure.d.ts.map +1 -1
  3. package/commands/configure.js +31 -11
  4. package/commands/configure.js.map +1 -1
  5. package/commands/doctor.d.ts.map +1 -1
  6. package/commands/doctor.js +1 -1
  7. package/commands/doctor.js.map +1 -1
  8. package/commands/login.d.ts +2 -0
  9. package/commands/login.d.ts.map +1 -1
  10. package/commands/login.js +28 -6
  11. package/commands/login.js.map +1 -1
  12. package/commands/run.d.ts.map +1 -1
  13. package/commands/run.js +37 -2
  14. package/commands/run.js.map +1 -1
  15. package/index.js +887 -184
  16. package/package.json +1 -1
  17. package/utils/agent-invoker.d.ts +31 -58
  18. package/utils/agent-invoker.d.ts.map +1 -1
  19. package/utils/agent-invoker.js +95 -186
  20. package/utils/agent-invoker.js.map +1 -1
  21. package/utils/autostart.d.ts +41 -0
  22. package/utils/autostart.d.ts.map +1 -0
  23. package/utils/autostart.js +122 -0
  24. package/utils/autostart.js.map +1 -0
  25. package/utils/key-run.d.ts +32 -0
  26. package/utils/key-run.d.ts.map +1 -0
  27. package/utils/key-run.js +77 -0
  28. package/utils/key-run.js.map +1 -0
  29. package/utils/loopback.d.ts +3 -3
  30. package/utils/loopback.d.ts.map +1 -1
  31. package/utils/loopback.js +3 -3
  32. package/utils/loopback.js.map +1 -1
  33. package/utils/mailbox-client.d.ts +21 -0
  34. package/utils/mailbox-client.d.ts.map +1 -1
  35. package/utils/mailbox-client.js +34 -0
  36. package/utils/mailbox-client.js.map +1 -1
  37. package/utils/mcp-config-writer.d.ts +2 -0
  38. package/utils/mcp-config-writer.d.ts.map +1 -1
  39. package/utils/mcp-config-writer.js +9 -1
  40. package/utils/mcp-config-writer.js.map +1 -1
  41. package/utils/repo-setup.d.ts +44 -0
  42. package/utils/repo-setup.d.ts.map +1 -0
  43. package/utils/repo-setup.js +178 -0
  44. package/utils/repo-setup.js.map +1 -0
  45. package/utils/run-workspace.d.ts +114 -0
  46. package/utils/run-workspace.d.ts.map +1 -0
  47. package/utils/run-workspace.js +264 -0
  48. package/utils/run-workspace.js.map +1 -0
package/index.js CHANGED
@@ -5635,6 +5635,130 @@ function interviewFieldKeysFor(definition) {
5635
5635
  return out;
5636
5636
  }
5637
5637
 
5638
+ // ../shared/dist/interview-completion.js
5639
+ var TARGET_COMPLIANCE_TIER = "pass";
5640
+ function meetsComplianceTarget(compliance) {
5641
+ return !compliance || compliance.tier === TARGET_COMPLIANCE_TIER;
5642
+ }
5643
+ function buildAuditFeedback(compliance) {
5644
+ const ordered = [
5645
+ ...compliance.issues.filter((i) => i.tier === "critical"),
5646
+ ...compliance.issues.filter((i) => i.tier === "advisory")
5647
+ ].slice(0, 8);
5648
+ const list = ordered.length > 0 ? ordered.map((i) => `- ${i.message}`).join("\n") : "- (no specific issues returned; expand the required fields with more concrete detail)";
5649
+ return `The epic request you just produced scored ${String(compliance.overall)}/100 (tier: ${compliance.tier}). It must reach 90/100 with ZERO critical issues before the interview can finish. Fix these:
5650
+ ${list}
5651
+
5652
+ If you can resolve these by EXPANDING what the employee already told you \u2014 a longer, more complete and concrete problemStatement / proposedSolution / impactAssessment / description that comfortably clear the length minimums \u2014 re-emit the completion JSON now with richer fields. Do NOT re-ask something you already have an answer for. Only if you genuinely need NEW information from the employee, ask ONE focused question to get it.`;
5653
+ }
5654
+ var OPTIONAL_DRAFT_KEYS = [
5655
+ "successMetrics",
5656
+ "targetAudience",
5657
+ "alternatives",
5658
+ "dependencies",
5659
+ "estimatedEffort"
5660
+ ];
5661
+ function isEpicRequestDraft(value) {
5662
+ if (typeof value !== "object" || value === null)
5663
+ return false;
5664
+ const v = value;
5665
+ const nonEmpty = (x) => typeof x === "string" && x.trim().length > 0;
5666
+ const requiredOk = nonEmpty(v.title) && nonEmpty(v.problemStatement) && nonEmpty(v.proposedSolution) && nonEmpty(v.impactAssessment) && nonEmpty(v.description);
5667
+ if (!requiredOk)
5668
+ return false;
5669
+ for (const key of OPTIONAL_DRAFT_KEYS) {
5670
+ if (key in v && v[key] !== void 0 && typeof v[key] !== "string")
5671
+ return false;
5672
+ }
5673
+ return true;
5674
+ }
5675
+ function stripTrailingCommas(json) {
5676
+ return json.replace(/,(\s*[}\]])/g, "$1");
5677
+ }
5678
+ function escapeRawControlCharsInStrings(json) {
5679
+ let out = "";
5680
+ let inString = false;
5681
+ let escaped = false;
5682
+ for (const ch of json) {
5683
+ if (escaped) {
5684
+ out += ch;
5685
+ escaped = false;
5686
+ continue;
5687
+ }
5688
+ if (ch === "\\") {
5689
+ out += ch;
5690
+ escaped = true;
5691
+ continue;
5692
+ }
5693
+ if (ch === '"') {
5694
+ inString = !inString;
5695
+ out += ch;
5696
+ continue;
5697
+ }
5698
+ if (inString && (ch === "\n" || ch === "\r" || ch === " ")) {
5699
+ out += ch === "\n" ? "\\n" : ch === "\r" ? "\\r" : "\\t";
5700
+ continue;
5701
+ }
5702
+ out += ch;
5703
+ }
5704
+ return out;
5705
+ }
5706
+ function completionJsonCandidates(text) {
5707
+ const candidates = [];
5708
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
5709
+ if (fenced?.[1])
5710
+ candidates.push(fenced[1]);
5711
+ const trimmed = text.trim();
5712
+ if (trimmed.startsWith("{"))
5713
+ candidates.push(trimmed);
5714
+ const first = text.indexOf("{");
5715
+ const last = text.lastIndexOf("}");
5716
+ if (first !== -1 && last > first)
5717
+ candidates.push(text.slice(first, last + 1));
5718
+ return candidates;
5719
+ }
5720
+ function toDraft(parsed) {
5721
+ const draft = {
5722
+ title: parsed.title,
5723
+ problemStatement: parsed.problemStatement,
5724
+ proposedSolution: parsed.proposedSolution,
5725
+ impactAssessment: parsed.impactAssessment,
5726
+ description: parsed.description
5727
+ };
5728
+ for (const key of OPTIONAL_DRAFT_KEYS) {
5729
+ const raw = parsed[key];
5730
+ if (typeof raw === "string" && raw.trim().length > 0) {
5731
+ draft[key] = raw;
5732
+ }
5733
+ }
5734
+ return draft;
5735
+ }
5736
+ function extractCompletionJson(text) {
5737
+ for (const candidate of completionJsonCandidates(text)) {
5738
+ for (const variant of [
5739
+ candidate,
5740
+ stripTrailingCommas(candidate),
5741
+ escapeRawControlCharsInStrings(candidate),
5742
+ escapeRawControlCharsInStrings(stripTrailingCommas(candidate))
5743
+ ]) {
5744
+ try {
5745
+ const parsed = JSON.parse(variant);
5746
+ if (isEpicRequestDraft(parsed))
5747
+ return toDraft(parsed);
5748
+ } catch {
5749
+ }
5750
+ }
5751
+ }
5752
+ return null;
5753
+ }
5754
+ function looksLikeCompletionAttempt(text) {
5755
+ const hasObject = /```(?:json)?\s*\{/i.test(text) || text.trim().startsWith("{");
5756
+ if (!hasObject)
5757
+ return false;
5758
+ return /"(?:done|title|problemStatement|proposedSolution|impactAssessment|description)"\s*:/i.test(text);
5759
+ }
5760
+ var COMPLETION_REPAIR_INSTRUCTION = 'Your last message looked like a completion but I could NOT parse it as a valid JSON object with all required fields. Re-emit ONLY a single valid JSON object inside a ```json code fence \u2014 no other text \u2014 with every string value properly escaped (escape any internal double-quotes as \\" and any line breaks as \\n), and make sure title, problemStatement, proposedSolution, impactAssessment, and description are all present and non-empty:\n```json\n{"done": true, "title": "...", "problemStatement": "...", "proposedSolution": "...", "impactAssessment": "...", "description": "..."}\n```';
5761
+
5638
5762
  // src/utils/config-store.ts
5639
5763
  var CONFIG_DIR_ENV = "COMPBOX_CONFIG_DIR";
5640
5764
  var PROJECT_DIR_ENV = "COMPBOX_PROJECT_DIR";
@@ -5779,8 +5903,8 @@ async function promptText(question, options = {}) {
5779
5903
  const rendered = defaultValue !== void 0 ? `${question} [${defaultValue}]: ` : question;
5780
5904
  const rl = readline.createInterface({ input: stdin, output: stdout });
5781
5905
  try {
5782
- const answer = await new Promise((resolve2) => {
5783
- rl.question(rendered, resolve2);
5906
+ const answer = await new Promise((resolve3) => {
5907
+ rl.question(rendered, resolve3);
5784
5908
  });
5785
5909
  const trimmed = answer.trim();
5786
5910
  if (trimmed.length === 0 && defaultValue !== void 0) {
@@ -5792,7 +5916,7 @@ async function promptText(question, options = {}) {
5792
5916
  }
5793
5917
  }
5794
5918
  function promptSecret(question) {
5795
- return new Promise((resolve2) => {
5919
+ return new Promise((resolve3) => {
5796
5920
  const rl = readline.createInterface({
5797
5921
  input: stdin,
5798
5922
  output: stdout,
@@ -5804,7 +5928,7 @@ function promptSecret(question) {
5804
5928
  rl.question("", (answer) => {
5805
5929
  rl.close();
5806
5930
  stdout.write("\n");
5807
- resolve2(answer.trim());
5931
+ resolve3(answer.trim());
5808
5932
  });
5809
5933
  });
5810
5934
  }
@@ -5842,7 +5966,7 @@ async function promptConfirm(question, defaultYes = true) {
5842
5966
  }
5843
5967
 
5844
5968
  // src/utils/loopback.ts
5845
- var LOOPBACK_IMPACT = "your work will NOT reach the cloud (mycompbox.web.app)";
5969
+ var LOOPBACK_IMPACT = "your work will NOT reach the cloud (mycompbox.com)";
5846
5970
  var LOOPBACK_NOTICE = `This is a LOCAL dev endpoint \u2014 ${LOOPBACK_IMPACT}. Use \`compbox login --url <cloud-url>\` for production.`;
5847
5971
  function isLoopbackHostname(hostname3) {
5848
5972
  let host = hostname3.trim().toLowerCase();
@@ -5882,6 +6006,7 @@ function isLoopbackUrl(rawUrl) {
5882
6006
  }
5883
6007
 
5884
6008
  // src/commands/login.ts
6009
+ var DEFAULT_BASE_URL = "https://mycompbox.com";
5885
6010
  async function resolveBaseUrl(option) {
5886
6011
  if (option && option.trim().length > 0) {
5887
6012
  return option.trim();
@@ -5890,19 +6015,40 @@ async function resolveBaseUrl(option) {
5890
6015
  if (existing && existing.trim().length > 0) {
5891
6016
  return existing.trim();
5892
6017
  }
5893
- const entered = await promptText("Central CompBox API URL: ");
5894
- if (entered.length === 0) {
5895
- throw new Error("A central API URL is required.");
5896
- }
5897
- return entered;
6018
+ const entered = await promptText(`CompBox URL [${DEFAULT_BASE_URL}]: `);
6019
+ return entered.length === 0 ? DEFAULT_BASE_URL : entered;
5898
6020
  }
5899
6021
  var loginCommand = new Command5("login").description("Authenticate to central CompBox and store your pairing config").option("--url <url>", "Central CompBox API base URL").option("--email <email>", "Account email (otherwise prompted)").option(
5900
6022
  "--password <password>",
5901
6023
  "Account password (otherwise prompted; prefer the prompt to keep it out of shell history)"
5902
- ).option("--token-name <name>", "Name for the generated API token").action(async (options) => {
6024
+ ).option("--token-name <name>", "Name for the generated API token").option(
6025
+ "--api-token <token>",
6026
+ "Sign in with an existing CompBox API token (st_...) from web Settings \u2192 API Keys \u2014 use this if you sign in with Google"
6027
+ ).action(async (options) => {
5903
6028
  try {
5904
6029
  const baseUrl = await resolveBaseUrl(options.url);
5905
6030
  const client = new AuthClient(baseUrl);
6031
+ if (options.apiToken && options.apiToken.trim().length > 0) {
6032
+ const token = options.apiToken.trim();
6033
+ console.log(chalk6.gray(`
6034
+ Verifying the API token against ${baseUrl}...`));
6035
+ const me = await client.getMe(token);
6036
+ updateConfig({
6037
+ apiBaseUrl: client.baseUrl,
6038
+ apiToken: token,
6039
+ userId: me.id,
6040
+ email: me.email,
6041
+ userName: me.name
6042
+ });
6043
+ console.log(chalk6.green(`
6044
+ \u2714 Paired as ${me.name} <${me.email}> using your API token`));
6045
+ console.log(chalk6.gray(` Central URL: ${client.baseUrl}`));
6046
+ console.log("\n" + chalk6.bold("Next step:"));
6047
+ console.log(
6048
+ chalk6.gray(" Run `compbox configure` to select your AI mode, pair this device, and write the MCP config.")
6049
+ );
6050
+ return;
6051
+ }
5906
6052
  const email = options.email && options.email.trim().length > 0 ? options.email.trim() : await promptText("Email: ");
5907
6053
  if (email.length === 0) {
5908
6054
  throw new Error("Email is required.");
@@ -5952,7 +6098,17 @@ Authenticating to ${baseUrl}...`));
5952
6098
  console.error(chalk6.red(`
5953
6099
  Login failed: ${error.message}`));
5954
6100
  if (error.status === 401) {
5955
- console.error(chalk6.yellow("Tip: double-check your email and password."));
6101
+ console.error(
6102
+ chalk6.yellow(
6103
+ "Tip: double-check your email and password. If you sign in with Google, use `compbox login --api-token <st_...>` with a key from web Settings \u2192 API Keys."
6104
+ )
6105
+ );
6106
+ } else if (error.status === 403) {
6107
+ console.error(
6108
+ chalk6.yellow(
6109
+ "Tip: if you just signed up, open the verification link we emailed you, then run `compbox login` again."
6110
+ )
6111
+ );
5956
6112
  } else if (error.status === 0) {
5957
6113
  console.error(chalk6.yellow("Tip: verify the central API URL and your network connection."));
5958
6114
  }
@@ -5969,7 +6125,7 @@ Login failed: ${error.message}`));
5969
6125
  // src/commands/configure.ts
5970
6126
  import { Command as Command6 } from "commander";
5971
6127
  import chalk7 from "chalk";
5972
- import { execFileSync as execFileSync3 } from "child_process";
6128
+ import { execFileSync as execFileSync4 } from "child_process";
5973
6129
  import { hostname as hostname2 } from "os";
5974
6130
 
5975
6131
  // src/utils/keychain.ts
@@ -6300,16 +6456,57 @@ var MailboxClient = class {
6300
6456
  const data = res.data;
6301
6457
  return data.data;
6302
6458
  }
6459
+ /**
6460
+ * The person's saved API key for one job sent with "Run with: My API key".
6461
+ * Kept in memory and passed to Claude Code's environment only — never
6462
+ * written to disk or logged.
6463
+ */
6464
+ async getAiKey(machineToken, jobId) {
6465
+ let res;
6466
+ try {
6467
+ res = await this.http.post(
6468
+ `/api/v1/jobs/${jobId}/ai-key`,
6469
+ {},
6470
+ { headers: { Authorization: `Bearer ${machineToken}` } }
6471
+ );
6472
+ } catch (error) {
6473
+ throw toMailboxError(error, "ai-key request failed");
6474
+ }
6475
+ if (res.status < 200 || res.status >= 300) {
6476
+ throw errorFromResponse(res, "ai-key failed");
6477
+ }
6478
+ const data = res.data;
6479
+ return data.data;
6480
+ }
6481
+ /** Reports what a run on the person's API key spent. */
6482
+ async reportAiUsage(machineToken, jobId, usage) {
6483
+ let res;
6484
+ try {
6485
+ res = await this.http.post(`/api/v1/jobs/${jobId}/ai-usage`, usage, {
6486
+ headers: { Authorization: `Bearer ${machineToken}` }
6487
+ });
6488
+ } catch (error) {
6489
+ throw toMailboxError(error, "ai-usage request failed");
6490
+ }
6491
+ if (res.status < 200 || res.status >= 300) {
6492
+ throw errorFromResponse(res, "ai-usage failed");
6493
+ }
6494
+ }
6303
6495
  };
6304
6496
 
6305
6497
  // src/utils/mcp-config-writer.ts
6306
6498
  import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
6307
- import { dirname as dirname2, join as join6 } from "path";
6499
+ import { basename, dirname as dirname2, join as join6 } from "path";
6308
6500
  var DEFAULT_MCP_CONFIG_FILENAME = ".mcp.json";
6309
6501
  var DEFAULT_SERVER_KEY = "compbox";
6310
6502
  var MCP_SERVER_PACKAGE = "@mycompbox/compbox-mcp";
6311
6503
  function resolveInstalledMcpBundle(opts) {
6312
6504
  const exists = opts?.exists ?? existsSync6;
6505
+ const self = opts && "self" in opts ? opts.self : process.argv[1];
6506
+ if (self && basename(self) === "compbox.cjs") {
6507
+ const sibling = join6(dirname2(self), "compbox-mcp.cjs");
6508
+ if (exists(sibling)) return sibling;
6509
+ }
6313
6510
  const prefixes = [];
6314
6511
  if (opts?.prefix) prefixes.push(opts.prefix);
6315
6512
  if (process.env.HOMEBREW_PREFIX) prefixes.push(process.env.HOMEBREW_PREFIX);
@@ -6454,6 +6651,100 @@ function findBrew(deps = {}) {
6454
6651
  return pathProbe() ? "brew" : null;
6455
6652
  }
6456
6653
 
6654
+ // src/utils/autostart.ts
6655
+ import { execFileSync as execFileSync3 } from "node:child_process";
6656
+ import { mkdirSync as mkdirSync4, realpathSync, writeFileSync as writeFileSync4 } from "node:fs";
6657
+ import { homedir as homedir3 } from "node:os";
6658
+ import { dirname as dirname3, join as join7 } from "node:path";
6659
+ var LAUNCHD_LABEL = "com.mycompbox.agent-runner";
6660
+ var SYSTEMD_UNIT = "compbox-agent-runner.service";
6661
+ function scriptInstallPath(home = homedir3()) {
6662
+ return join7(home, ".compbox", "bin", "compbox.cjs");
6663
+ }
6664
+ function isScriptInstall(entry = process.argv[1], home = homedir3()) {
6665
+ if (!entry) return false;
6666
+ try {
6667
+ return realpathSync(entry) === realpathSync(scriptInstallPath(home));
6668
+ } catch {
6669
+ return false;
6670
+ }
6671
+ }
6672
+ function xmlEscape(value) {
6673
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6674
+ }
6675
+ function launchdPlist(opts) {
6676
+ const args = [opts.node, opts.script, "run"].map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
6677
+ return `<?xml version="1.0" encoding="UTF-8"?>
6678
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
6679
+ <plist version="1.0">
6680
+ <dict>
6681
+ <key>Label</key>
6682
+ <string>${LAUNCHD_LABEL}</string>
6683
+ <key>ProgramArguments</key>
6684
+ <array>
6685
+ ${args}
6686
+ </array>
6687
+ <key>EnvironmentVariables</key>
6688
+ <dict>
6689
+ <key>PATH</key>
6690
+ <string>${xmlEscape(opts.path)}</string>
6691
+ </dict>
6692
+ <key>RunAtLoad</key>
6693
+ <true/>
6694
+ <key>KeepAlive</key>
6695
+ <true/>
6696
+ <key>StandardOutPath</key>
6697
+ <string>${xmlEscape(opts.logFile)}</string>
6698
+ <key>StandardErrorPath</key>
6699
+ <string>${xmlEscape(opts.logFile)}</string>
6700
+ </dict>
6701
+ </plist>
6702
+ `;
6703
+ }
6704
+ function systemdUnit(opts) {
6705
+ const quote = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
6706
+ return `[Unit]
6707
+ Description=CompBox Agent Runner
6708
+
6709
+ [Service]
6710
+ ExecStart=${quote(opts.node)} ${quote(opts.script)} run
6711
+ Environment=${quote(`PATH=${opts.path}`)}
6712
+ Restart=always
6713
+ RestartSec=10
6714
+
6715
+ [Install]
6716
+ WantedBy=default.target
6717
+ `;
6718
+ }
6719
+ function installAutostart() {
6720
+ const home = homedir3();
6721
+ const script = scriptInstallPath(home);
6722
+ const node = process.execPath;
6723
+ const path3 = process.env.PATH ?? "/usr/bin:/bin";
6724
+ if (process.platform === "darwin") {
6725
+ const plistPath = join7(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
6726
+ const logFile = join7(home, ".compbox", "agent-runner.log");
6727
+ mkdirSync4(dirname3(plistPath), { recursive: true });
6728
+ writeFileSync4(plistPath, launchdPlist({ node, script, path: path3, logFile }));
6729
+ const domain = `gui/${String(process.getuid?.() ?? 0)}`;
6730
+ try {
6731
+ execFileSync3("launchctl", ["bootout", domain, plistPath], { stdio: "ignore" });
6732
+ } catch {
6733
+ }
6734
+ execFileSync3("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
6735
+ return `it starts at every login (stop it with \`launchctl bootout ${domain} ${plistPath}\`; log: ${logFile})`;
6736
+ }
6737
+ if (process.platform === "linux") {
6738
+ const unitPath = join7(home, ".config", "systemd", "user", SYSTEMD_UNIT);
6739
+ mkdirSync4(dirname3(unitPath), { recursive: true });
6740
+ writeFileSync4(unitPath, systemdUnit({ node, script, path: path3 }));
6741
+ execFileSync3("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
6742
+ execFileSync3("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT], { stdio: "pipe" });
6743
+ return `it starts at every login (stop it with \`systemctl --user disable --now ${SYSTEMD_UNIT}\`)`;
6744
+ }
6745
+ throw new Error(`Automatic start isn't supported on ${process.platform}.`);
6746
+ }
6747
+
6457
6748
  // src/commands/configure.ts
6458
6749
  function parseAiMode(value) {
6459
6750
  if (value === "claude-subscription" || value === "api-key") {
@@ -6550,13 +6841,13 @@ Device pairing failed: ${message}`));
6550
6841
  Device already paired as "${config.machineName ?? config.machineId}".`));
6551
6842
  }
6552
6843
  const defaultDir = isGitRepoDir(process.cwd()) ? process.cwd() : void 0;
6553
- console.log("\n" + chalk7.bold("Project directory for dispatched runs:"));
6844
+ console.log("\n" + chalk7.bold("Project folder:"));
6554
6845
  console.log(
6555
6846
  chalk7.gray(
6556
- " This is where dispatched @planner/@orchestrator runs read, edit, and commit \u2014 leave blank to skip."
6847
+ " The folder with your code (a git project) where the AI plans, edits and commits.\n Leave blank to skip; add folders per project later with `compbox machines dirs add`."
6557
6848
  )
6558
6849
  );
6559
- const projectDirQuestion = "Which local repository should dispatched plan/orchestrate runs use?";
6850
+ const projectDirQuestion = "Which project folder should the AI work in?";
6560
6851
  const MAX_PROJECT_DIR_ATTEMPTS = 5;
6561
6852
  let projectDirPersisted = false;
6562
6853
  let projectDirSkipped = false;
@@ -6673,8 +6964,8 @@ Device already paired as "${config.machineName ?? config.machineId}".`));
6673
6964
  " Agent traffic would pass through it; if it is not running, calls fall back to a direct connection automatically."
6674
6965
  )
6675
6966
  );
6676
- updateConfig({ pxpipeEnabled: enablePxpipe === true });
6677
- if (enablePxpipe === true) {
6967
+ updateConfig({ pxpipeEnabled: enablePxpipe });
6968
+ if (enablePxpipe) {
6678
6969
  console.log(chalk7.green("\u2714 pxpipe routing enabled for this machine."));
6679
6970
  console.log(
6680
6971
  chalk7.yellow(" Note: which models are actually compressed is pxpipe's own setting, not CompBox's.")
@@ -6693,9 +6984,26 @@ Device already paired as "${config.machineName ?? config.machineId}".`));
6693
6984
  chalk7.gray(" Re-run `compbox configure` to change this later.")
6694
6985
  );
6695
6986
  }
6696
- const startServiceCmd = process.platform === "darwin" ? "brew services start compbox" : process.platform === "win32" ? "compbox service install" : "compbox run";
6987
+ const scriptInstall = process.platform !== "win32" && isScriptInstall();
6988
+ const startServiceCmd = process.platform === "win32" ? "compbox service install" : process.platform === "darwin" && !scriptInstall ? "brew services start compbox" : "compbox run";
6697
6989
  let daemonStarted = false;
6698
- if (process.platform === "darwin") {
6990
+ if (scriptInstall) {
6991
+ const startNow = await promptConfirm(
6992
+ "Start the Agent Runner now and at every login? (lets CompBox send work to this computer)",
6993
+ true
6994
+ );
6995
+ if (startNow) {
6996
+ try {
6997
+ const how = installAutostart();
6998
+ daemonStarted = true;
6999
+ console.log(chalk7.green(`\u2714 Agent Runner started \u2014 ${how}.`));
7000
+ } catch (error) {
7001
+ const message = (error instanceof Error ? error.message.split("\n")[0] : void 0) ?? "unknown error";
7002
+ console.warn(chalk7.yellow(`\u26A0 Could not start the Agent Runner automatically: ${message}`));
7003
+ console.warn(chalk7.gray(" Run `compbox run` in a terminal and keep it open instead."));
7004
+ }
7005
+ }
7006
+ } else if (process.platform === "darwin") {
6699
7007
  const brew = findBrew();
6700
7008
  if (brew !== null) {
6701
7009
  const startNow = await promptConfirm(
@@ -6704,7 +7012,7 @@ Device already paired as "${config.machineName ?? config.machineId}".`));
6704
7012
  );
6705
7013
  if (startNow) {
6706
7014
  try {
6707
- execFileSync3(brew, ["services", "start", "compbox"], {
7015
+ execFileSync4(brew, ["services", "start", "compbox"], {
6708
7016
  encoding: "utf-8",
6709
7017
  timeout: 3e4,
6710
7018
  stdio: ["ignore", "pipe", "pipe"]
@@ -6716,7 +7024,7 @@ Device already paired as "${config.machineName ?? config.machineId}".`));
6716
7024
  )
6717
7025
  );
6718
7026
  } catch (error) {
6719
- const message = error instanceof Error ? error.message.split("\n")[0] : "unknown error";
7027
+ const message = (error instanceof Error ? error.message.split("\n")[0] : void 0) ?? "unknown error";
6720
7028
  console.warn(chalk7.yellow(`\u26A0 Could not start the service: ${message}`));
6721
7029
  console.warn(chalk7.gray(` Start it manually with \`${startServiceCmd}\`.`));
6722
7030
  }
@@ -6743,22 +7051,22 @@ Configuration failed: ${error.message}`));
6743
7051
  });
6744
7052
 
6745
7053
  // src/commands/doctor.ts
6746
- import { execFileSync as execFileSync5, spawn as spawn2 } from "child_process";
7054
+ import { execFileSync as execFileSync6, spawn as spawn2 } from "child_process";
6747
7055
  import { existsSync as existsSync8 } from "fs";
6748
7056
  import { Command as Command7 } from "commander";
6749
7057
  import chalk8 from "chalk";
6750
7058
 
6751
7059
  // src/utils/claude-runtime.ts
6752
7060
  import { spawn } from "node:child_process";
6753
- import { execFileSync as execFileSync4 } from "node:child_process";
7061
+ import { execFileSync as execFileSync5 } from "node:child_process";
6754
7062
  import { accessSync, constants as fsConstants, statSync as statSync2 } from "node:fs";
6755
- import { homedir as homedir3 } from "node:os";
6756
- import { delimiter, join as join7 } from "node:path";
7063
+ import { homedir as homedir4 } from "node:os";
7064
+ import { delimiter, join as join8 } from "node:path";
6757
7065
  var WELL_KNOWN_DIRS = buildWellKnownDirs();
6758
7066
  function buildWellKnownDirs() {
6759
- const home = homedir3();
7067
+ const home = homedir4();
6760
7068
  const dirs = [
6761
- join7(home, ".local", "bin"),
7069
+ join8(home, ".local", "bin"),
6762
7070
  // default Claude Code install location
6763
7071
  "/opt/homebrew/bin",
6764
7072
  // Homebrew on Apple Silicon
@@ -6766,14 +7074,14 @@ function buildWellKnownDirs() {
6766
7074
  // Homebrew on Intel / common Unix prefix
6767
7075
  ];
6768
7076
  if (process.env.npm_config_prefix) {
6769
- dirs.push(join7(process.env.npm_config_prefix, "bin"));
7077
+ dirs.push(join8(process.env.npm_config_prefix, "bin"));
6770
7078
  }
6771
- dirs.push(join7(home, ".npm-global", "bin"));
7079
+ dirs.push(join8(home, ".npm-global", "bin"));
6772
7080
  if (process.env.PNPM_HOME) {
6773
7081
  dirs.push(process.env.PNPM_HOME);
6774
7082
  }
6775
- dirs.push(join7(home, "Library", "pnpm"));
6776
- dirs.push(join7(home, ".local", "share", "pnpm"));
7083
+ dirs.push(join8(home, "Library", "pnpm"));
7084
+ dirs.push(join8(home, ".local", "share", "pnpm"));
6777
7085
  return dirs;
6778
7086
  }
6779
7087
  var LOGIN_SHELL_PATH_TIMEOUT_MS = 2e3;
@@ -6801,7 +7109,7 @@ function getLoginShellPath(exec = defaultExecFileSync) {
6801
7109
  return cachedLoginShellPath;
6802
7110
  }
6803
7111
  function defaultExecFileSync(file, args, options) {
6804
- return execFileSync4(file, args, {
7112
+ return execFileSync5(file, args, {
6805
7113
  timeout: options.timeout,
6806
7114
  encoding: options.encoding,
6807
7115
  stdio: ["ignore", "pipe", "ignore"]
@@ -6839,7 +7147,7 @@ function resolveClaudeBinary(deps = {}) {
6839
7147
  continue;
6840
7148
  }
6841
7149
  seen.add(dir);
6842
- const candidate = join7(dir, "claude");
7150
+ const candidate = join8(dir, "claude");
6843
7151
  if (isExec(candidate)) {
6844
7152
  return candidate;
6845
7153
  }
@@ -6878,7 +7186,7 @@ function probeAgentRuntime(deps = {}) {
6878
7186
  checkedAt: (/* @__PURE__ */ new Date()).toISOString()
6879
7187
  });
6880
7188
  }
6881
- return new Promise((resolve2) => {
7189
+ return new Promise((resolve3) => {
6882
7190
  let settled = false;
6883
7191
  let child;
6884
7192
  let stdout2 = "";
@@ -6893,7 +7201,7 @@ function probeAgentRuntime(deps = {}) {
6893
7201
  child.kill("SIGKILL");
6894
7202
  } catch {
6895
7203
  }
6896
- resolve2({ ...result, checkedAt: (/* @__PURE__ */ new Date()).toISOString() });
7204
+ resolve3({ ...result, checkedAt: (/* @__PURE__ */ new Date()).toISOString() });
6897
7205
  };
6898
7206
  const timer = setTimeout(() => {
6899
7207
  settle({
@@ -6905,7 +7213,7 @@ function probeAgentRuntime(deps = {}) {
6905
7213
  child = spawnFn(abs, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
6906
7214
  } catch (error) {
6907
7215
  clearTimeout(timer);
6908
- resolve2({
7216
+ resolve3({
6909
7217
  status: "error",
6910
7218
  detail: error instanceof Error ? error.message : "failed to spawn claude",
6911
7219
  checkedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -6997,7 +7305,7 @@ function countFailures(results) {
6997
7305
  }
6998
7306
  var LAUNCH_PROBE_TIMEOUT_MS = 5e3;
6999
7307
  function probeLaunchTarget(command, args, timeoutMs = LAUNCH_PROBE_TIMEOUT_MS) {
7000
- return new Promise((resolve2) => {
7308
+ return new Promise((resolve3) => {
7001
7309
  let settled = false;
7002
7310
  const child = spawn2(command, args, {
7003
7311
  stdio: ["ignore", "pipe", "pipe"],
@@ -7019,7 +7327,7 @@ function probeLaunchTarget(command, args, timeoutMs = LAUNCH_PROBE_TIMEOUT_MS) {
7019
7327
  child.kill("SIGKILL");
7020
7328
  } catch {
7021
7329
  }
7022
- resolve2(result);
7330
+ resolve3(result);
7023
7331
  };
7024
7332
  const timer = setTimeout(() => {
7025
7333
  settle({ status: "timeout" });
@@ -7108,7 +7416,7 @@ var BREW_SERVICE_LABEL = "homebrew.mxcl.compbox";
7108
7416
  function checkBrewService(deps = {}) {
7109
7417
  const name = "Homebrew service running (macOS)";
7110
7418
  const locateBrew = deps.findBrewFn ?? findBrew;
7111
- const exec = deps.execFn ?? ((file, args) => execFileSync5(file, args, {
7419
+ const exec = deps.execFn ?? ((file, args) => execFileSync6(file, args, {
7112
7420
  encoding: "utf-8",
7113
7421
  timeout: 1e4,
7114
7422
  stdio: ["ignore", "pipe", "ignore"]
@@ -7126,7 +7434,7 @@ function checkBrewService(deps = {}) {
7126
7434
  try {
7127
7435
  output = exec(brew, ["services", "list"]);
7128
7436
  } catch (error) {
7129
- const cause = error instanceof Error ? error.message.split("\n")[0] : "unknown error";
7437
+ const cause = (error instanceof Error ? error.message.split("\n")[0] : void 0) ?? "unknown error";
7130
7438
  try {
7131
7439
  exec("launchctl", ["list", BREW_SERVICE_LABEL]);
7132
7440
  return {
@@ -7416,13 +7724,68 @@ import chalk9 from "chalk";
7416
7724
  // src/utils/agent-invoker.ts
7417
7725
  import Anthropic from "@anthropic-ai/sdk";
7418
7726
  import { spawn as spawn3, spawnSync } from "node:child_process";
7419
- import { mkdirSync as mkdirSync4 } from "node:fs";
7420
- import { homedir as homedir4 } from "node:os";
7421
- import { join as join8 } from "node:path";
7727
+ import { mkdirSync as mkdirSync5 } from "node:fs";
7728
+ import { homedir as homedir5 } from "node:os";
7729
+ import { join as join9 } from "node:path";
7730
+
7731
+ // src/utils/key-run.ts
7732
+ function keyRunEnv(env, keyRun) {
7733
+ const next = { ...env, ANTHROPIC_API_KEY: keyRun.apiKey, ANTHROPIC_MODEL: keyRun.model };
7734
+ delete next.ANTHROPIC_AUTH_TOKEN;
7735
+ delete next.CLAUDE_CODE_OAUTH_TOKEN;
7736
+ return next;
7737
+ }
7738
+ function keyRunArgs(args) {
7739
+ return [...args, "--output-format", "json"];
7740
+ }
7741
+ function num(value) {
7742
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
7743
+ }
7744
+ function parseClaudeJsonResult(stdout2, fallbackModel) {
7745
+ let data;
7746
+ try {
7747
+ const start = stdout2.indexOf("{");
7748
+ if (start === -1) return null;
7749
+ data = JSON.parse(stdout2.slice(start));
7750
+ } catch {
7751
+ return null;
7752
+ }
7753
+ if (typeof data.result !== "string" && data.type !== "result") return null;
7754
+ const usage = {
7755
+ model: fallbackModel,
7756
+ inputTokens: 0,
7757
+ outputTokens: 0,
7758
+ cacheReadTokens: 0,
7759
+ cacheWriteTokens: 0,
7760
+ costUsd: typeof data.total_cost_usd === "number" ? data.total_cost_usd : null
7761
+ };
7762
+ const byModel = data.modelUsage;
7763
+ if (typeof byModel === "object" && byModel !== null && Object.keys(byModel).length > 0) {
7764
+ let topCost = -1;
7765
+ for (const [model, raw] of Object.entries(byModel)) {
7766
+ usage.inputTokens += num(raw.inputTokens);
7767
+ usage.outputTokens += num(raw.outputTokens);
7768
+ usage.cacheReadTokens += num(raw.cacheReadInputTokens);
7769
+ usage.cacheWriteTokens += num(raw.cacheCreationInputTokens);
7770
+ if (num(raw.costUSD) > topCost) {
7771
+ topCost = num(raw.costUSD);
7772
+ usage.model = model;
7773
+ }
7774
+ }
7775
+ } else if (typeof data.usage === "object" && data.usage !== null) {
7776
+ const u = data.usage;
7777
+ usage.inputTokens = num(u.input_tokens);
7778
+ usage.outputTokens = num(u.output_tokens);
7779
+ usage.cacheReadTokens = num(u.cache_read_input_tokens);
7780
+ usage.cacheWriteTokens = num(u.cache_creation_input_tokens);
7781
+ }
7782
+ return { text: typeof data.result === "string" ? data.result : "", usage };
7783
+ }
7784
+
7785
+ // src/utils/agent-invoker.ts
7422
7786
  var ANTHROPIC_MODEL = "claude-fable-5";
7423
7787
  var MAX_INTERVIEW_TURNS = 18;
7424
7788
  var MAX_QUALITY_ROUNDS = 4;
7425
- var TARGET_COMPLIANCE_TIER = "pass";
7426
7789
  var REPLY_POLL_INTERVAL_MS = 3e3;
7427
7790
  var REPLY_TIMEOUT_MS = 10 * 60 * 1e3;
7428
7791
  var CLAUDE_CLI_TURN_TIMEOUT_MS = 2 * 60 * 1e3;
@@ -7553,124 +7916,9 @@ async function submitAndScore(ctx, currentErId, draft) {
7553
7916
  const created = await createPersonalEpicRequest(ctx.apiBaseUrl, ctx.userApiToken, draft);
7554
7917
  return { erId: created.id, compliance: created.compliance };
7555
7918
  }
7556
- function meetsComplianceTarget(compliance) {
7557
- return !compliance || compliance.tier === TARGET_COMPLIANCE_TIER;
7558
- }
7559
- function buildAuditFeedback(compliance) {
7560
- const ordered = [
7561
- ...compliance.issues.filter((i) => i.tier === "critical"),
7562
- ...compliance.issues.filter((i) => i.tier === "advisory")
7563
- ].slice(0, 8);
7564
- const list = ordered.length > 0 ? ordered.map((i) => `- ${i.message}`).join("\n") : "- (no specific issues returned; expand the required fields with more concrete detail)";
7565
- return `The epic request you just produced scored ${String(compliance.overall)}/100 (tier: ${compliance.tier}). It must reach 90/100 with ZERO critical issues before the interview can finish. Fix these:
7566
- ${list}
7567
-
7568
- If you can resolve these by EXPANDING what the employee already told you \u2014 a longer, more complete and concrete problemStatement / proposedSolution / impactAssessment / description that comfortably clear the length minimums \u2014 re-emit the completion JSON now with richer fields. Do NOT re-ask something you already have an answer for. Only if you genuinely need NEW information from the employee, ask ONE focused question to get it.`;
7569
- }
7570
- var OPTIONAL_DRAFT_KEYS = [
7571
- "successMetrics",
7572
- "targetAudience",
7573
- "alternatives",
7574
- "dependencies",
7575
- "estimatedEffort"
7576
- ];
7577
- function isEpicRequestDraft(value) {
7578
- if (typeof value !== "object" || value === null) return false;
7579
- const v = value;
7580
- const nonEmpty = (x) => typeof x === "string" && x.trim().length > 0;
7581
- const requiredOk = nonEmpty(v.title) && nonEmpty(v.problemStatement) && nonEmpty(v.proposedSolution) && nonEmpty(v.impactAssessment) && nonEmpty(v.description);
7582
- if (!requiredOk) return false;
7583
- for (const key of OPTIONAL_DRAFT_KEYS) {
7584
- if (key in v && v[key] !== void 0 && typeof v[key] !== "string") return false;
7585
- }
7586
- return true;
7587
- }
7588
- function stripTrailingCommas(json) {
7589
- return json.replace(/,(\s*[}\]])/g, "$1");
7590
- }
7591
- function escapeRawControlCharsInStrings(json) {
7592
- let out = "";
7593
- let inString = false;
7594
- let escaped = false;
7595
- for (const ch of json) {
7596
- if (escaped) {
7597
- out += ch;
7598
- escaped = false;
7599
- continue;
7600
- }
7601
- if (ch === "\\") {
7602
- out += ch;
7603
- escaped = true;
7604
- continue;
7605
- }
7606
- if (ch === '"') {
7607
- inString = !inString;
7608
- out += ch;
7609
- continue;
7610
- }
7611
- if (inString && (ch === "\n" || ch === "\r" || ch === " ")) {
7612
- out += ch === "\n" ? "\\n" : ch === "\r" ? "\\r" : "\\t";
7613
- continue;
7614
- }
7615
- out += ch;
7616
- }
7617
- return out;
7618
- }
7619
- function completionJsonCandidates(text) {
7620
- const candidates = [];
7621
- const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
7622
- if (fenced?.[1]) candidates.push(fenced[1]);
7623
- const trimmed = text.trim();
7624
- if (trimmed.startsWith("{")) candidates.push(trimmed);
7625
- const first = text.indexOf("{");
7626
- const last = text.lastIndexOf("}");
7627
- if (first !== -1 && last > first) candidates.push(text.slice(first, last + 1));
7628
- return candidates;
7629
- }
7630
- function toDraft(parsed) {
7631
- const draft = {
7632
- title: parsed.title,
7633
- problemStatement: parsed.problemStatement,
7634
- proposedSolution: parsed.proposedSolution,
7635
- impactAssessment: parsed.impactAssessment,
7636
- description: parsed.description
7637
- };
7638
- for (const key of OPTIONAL_DRAFT_KEYS) {
7639
- const raw = parsed[key];
7640
- if (typeof raw === "string" && raw.trim().length > 0) {
7641
- draft[key] = raw;
7642
- }
7643
- }
7644
- return draft;
7645
- }
7646
- function extractCompletionJson(text) {
7647
- for (const candidate of completionJsonCandidates(text)) {
7648
- for (const variant of [
7649
- candidate,
7650
- stripTrailingCommas(candidate),
7651
- escapeRawControlCharsInStrings(candidate),
7652
- escapeRawControlCharsInStrings(stripTrailingCommas(candidate))
7653
- ]) {
7654
- try {
7655
- const parsed = JSON.parse(variant);
7656
- if (isEpicRequestDraft(parsed)) return toDraft(parsed);
7657
- } catch {
7658
- }
7659
- }
7660
- }
7661
- return null;
7662
- }
7663
- function looksLikeCompletionAttempt(text) {
7664
- const hasObject = /```(?:json)?\s*\{/i.test(text) || text.trim().startsWith("{");
7665
- if (!hasObject) return false;
7666
- return /"(?:done|title|problemStatement|proposedSolution|impactAssessment|description)"\s*:/i.test(
7667
- text
7668
- );
7669
- }
7670
- var COMPLETION_REPAIR_INSTRUCTION = 'Your last message looked like a completion but I could NOT parse it as a valid JSON object with all required fields. Re-emit ONLY a single valid JSON object inside a ```json code fence \u2014 no other text \u2014 with every string value properly escaped (escape any internal double-quotes as \\" and any line breaks as \\n), and make sure title, problemStatement, proposedSolution, impactAssessment, and description are all present and non-empty:\n```json\n{"done": true, "title": "...", "problemStatement": "...", "proposedSolution": "...", "impactAssessment": "...", "description": "..."}\n```';
7671
7919
  var MAX_JSON_REPAIRS = 3;
7672
7920
  function sleep(ms) {
7673
- return new Promise((resolve2) => setTimeout(resolve2, ms));
7921
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
7674
7922
  }
7675
7923
  async function waitForUserReply(client, machineToken, jobId, turnsSoFar) {
7676
7924
  const deadline = Date.now() + REPLY_TIMEOUT_MS;
@@ -7781,15 +8029,15 @@ async function invokeEpicRequestViaApi(job, ctx) {
7781
8029
  };
7782
8030
  }
7783
8031
  function claudeCliSessionCwd() {
7784
- const dir = join8(homedir4(), ".compbox", "claude-sessions");
8032
+ const dir = join9(homedir5(), ".compbox", "claude-sessions");
7785
8033
  try {
7786
- mkdirSync4(dir, { recursive: true });
8034
+ mkdirSync5(dir, { recursive: true });
7787
8035
  } catch {
7788
8036
  }
7789
8037
  return dir;
7790
8038
  }
7791
8039
  function runClaudeCliTurn(prompt, opts) {
7792
- return new Promise((resolve2, reject) => {
8040
+ return new Promise((resolve3, reject) => {
7793
8041
  const args = [
7794
8042
  "-p",
7795
8043
  prompt,
@@ -7864,7 +8112,7 @@ function runClaudeCliTurn(prompt, opts) {
7864
8112
  reject(new Error("claude CLI returned an unexpected JSON shape (missing result/session_id)."));
7865
8113
  return;
7866
8114
  }
7867
- resolve2({ text: shaped.result, sessionId: shaped.session_id });
8115
+ resolve3({ text: shaped.result, sessionId: shaped.session_id });
7868
8116
  });
7869
8117
  });
7870
8118
  }
@@ -7958,6 +8206,17 @@ var PLANNER_RUN_TIMEOUT_MS = 30 * 60 * 1e3;
7958
8206
  var PLANNER_HEARTBEAT_INTERVAL_MS = 20 * 1e3;
7959
8207
  var PLANNER_FINAL_TURN_MAX_CHARS = 4e3;
7960
8208
  var ORCHESTRATOR_REVIEW_MARKER = "COMPBOX_NEEDS_HUMAN_REVIEW";
8209
+ function takeKeyRunResult(ctx, keyRun, stdout2) {
8210
+ const parsed = parseClaudeJsonResult(stdout2, keyRun.model);
8211
+ if (!parsed) return { text: stdout2, reported: Promise.resolve() };
8212
+ const reported = ctx.mailboxClient.reportAiUsage(ctx.machineToken, ctx.jobId, parsed.usage).catch(() => void 0);
8213
+ return { text: parsed.text, reported };
8214
+ }
8215
+ async function settleKeyRun(ctx, keyRun, stdout2) {
8216
+ const { text, reported } = takeKeyRunResult(ctx, keyRun, stdout2);
8217
+ await reported;
8218
+ return text;
8219
+ }
7961
8220
  function buildPlannerArgs(epicRequestId) {
7962
8221
  return [
7963
8222
  "-p",
@@ -8078,13 +8337,14 @@ async function invokeStartExecutionPlan(ctx, deps = {}) {
8078
8337
  }
8079
8338
  const routing = await resolveRoutingImpl();
8080
8339
  const command = resolveClaudeBinary() ?? "claude";
8081
- const args = buildPlannerArgs(ctx.epicRequestId);
8340
+ const args = ctx.keyRun ? keyRunArgs(buildPlannerArgs(ctx.epicRequestId)) : buildPlannerArgs(ctx.epicRequestId);
8082
8341
  const routedEnv = claudeSpawnEnv(process.env, routing);
8083
- const childEnv = {
8342
+ const baseEnv = {
8084
8343
  ...routedEnv,
8085
8344
  COMPBOX_NONINTERACTIVE: "1",
8086
8345
  PATH: claudeChildEnvPath(routedEnv.PATH)
8087
8346
  };
8347
+ const childEnv = ctx.keyRun ? keyRunEnv(baseEnv, ctx.keyRun) : baseEnv;
8088
8348
  try {
8089
8349
  await ctx.mailboxClient.postTurn(
8090
8350
  ctx.machineToken,
@@ -8093,7 +8353,7 @@ async function invokeStartExecutionPlan(ctx, deps = {}) {
8093
8353
  );
8094
8354
  } catch {
8095
8355
  }
8096
- const outcome = await new Promise((resolve2) => {
8356
+ const outcome = await new Promise((resolve3) => {
8097
8357
  let stdout2 = "";
8098
8358
  let stderr = "";
8099
8359
  let settled = false;
@@ -8111,7 +8371,7 @@ async function invokeStartExecutionPlan(ctx, deps = {}) {
8111
8371
  settled = true;
8112
8372
  if (heartbeatTimer) clearInterval(heartbeatTimer);
8113
8373
  clearTimeout(timer);
8114
- resolve2(result);
8374
+ resolve3(result);
8115
8375
  };
8116
8376
  const timer = setTimeout(() => {
8117
8377
  child.kill("SIGKILL");
@@ -8138,6 +8398,7 @@ async function invokeStartExecutionPlan(ctx, deps = {}) {
8138
8398
  });
8139
8399
  child.on("close", (code) => {
8140
8400
  void (async () => {
8401
+ if (ctx.keyRun) stdout2 = await settleKeyRun(ctx, ctx.keyRun, stdout2);
8141
8402
  const combined = `${stdout2}
8142
8403
  ${stderr}`;
8143
8404
  const review = detectNeedsHumanReview(combined);
@@ -8248,6 +8509,13 @@ function defaultToolchainPresent(_dir) {
8248
8509
  }
8249
8510
  return false;
8250
8511
  }
8512
+ function describeRunBranch(finish) {
8513
+ if (finish.prUrl) return `Pull request: ${finish.prUrl}`;
8514
+ if (finish.compareUrl) return `Branch ${finish.branch} was pushed. Open the pull request here: ${finish.compareUrl}`;
8515
+ if (finish.commits === 0) return "No code changes were made.";
8516
+ if (finish.pushed) return `Branch ${finish.branch} was pushed.`;
8517
+ return `The work is on branch ${finish.branch}${finish.keptWorktree ? ` (in ${finish.keptWorktree})` : ""}.`;
8518
+ }
8251
8519
  function markerString(marker, key) {
8252
8520
  const v = marker?.[key];
8253
8521
  return typeof v === "string" && v.length > 0 ? v : void 0;
@@ -8289,29 +8557,41 @@ async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
8289
8557
  if (!toolchainPresent(projectDir)) {
8290
8558
  return { status: "failed", result: { error: toolchainMissingMessage(projectDir) } };
8291
8559
  }
8560
+ let workspace = null;
8561
+ let workspaceNotes = [];
8562
+ if (deps.workspace) {
8563
+ const prepared = await deps.workspace.prepare(projectDir, ctx.epicId);
8564
+ if (!prepared.ok) {
8565
+ return { status: "failed", result: { error: prepared.error } };
8566
+ }
8567
+ workspace = prepared.workspace;
8568
+ workspaceNotes = prepared.notes;
8569
+ }
8570
+ const runDir = workspace?.dir ?? projectDir;
8292
8571
  const routing = await resolveRoutingImpl();
8293
8572
  const command = resolveClaudeBinary() ?? "claude";
8294
- const args = buildOrchestratorArgs(ctx.epicId);
8573
+ const args = ctx.keyRun ? keyRunArgs(buildOrchestratorArgs(ctx.epicId)) : buildOrchestratorArgs(ctx.epicId);
8295
8574
  const routedEnv = claudeSpawnEnv(process.env, routing);
8296
- const childEnv = {
8575
+ const baseEnv = {
8297
8576
  ...routedEnv,
8298
8577
  COMPBOX_NONINTERACTIVE: "1",
8299
8578
  PATH: claudeChildEnvPath(routedEnv.PATH)
8300
8579
  };
8580
+ const childEnv = ctx.keyRun ? keyRunEnv(baseEnv, ctx.keyRun) : baseEnv;
8301
8581
  try {
8302
8582
  await ctx.mailboxClient.postTurn(
8303
8583
  ctx.machineToken,
8304
8584
  ctx.jobId,
8305
- `Started the orchestrator for epic ${ctx.epicId} (headless, non-interactive). This executes the full epic \u2014 every phase and feature \u2014 and may take a long time.`
8585
+ `Started the orchestrator for epic ${ctx.epicId} (headless, non-interactive). This executes the full epic \u2014 every phase and feature \u2014 and may take a long time.` + (workspace ? ` Working on a new branch, ${workspace.branch}, which will end as a pull request into ${workspace.baseBranch}.` : "") + (workspaceNotes.length > 0 ? ` Note: ${workspaceNotes.join(" ")}` : "")
8306
8586
  );
8307
8587
  } catch {
8308
8588
  }
8309
- const outcome = await new Promise((resolve2) => {
8589
+ const outcome = await new Promise((resolve3) => {
8310
8590
  let stdout2 = "";
8311
8591
  let stderr = "";
8312
8592
  let settled = false;
8313
8593
  const child = spawnImpl(command, args, {
8314
- cwd: projectDir,
8594
+ cwd: runDir,
8315
8595
  stdio: ["ignore", "pipe", "pipe"],
8316
8596
  env: childEnv
8317
8597
  });
@@ -8324,7 +8604,7 @@ async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
8324
8604
  settled = true;
8325
8605
  if (heartbeatTimer) clearInterval(heartbeatTimer);
8326
8606
  clearTimeout(timer);
8327
- resolve2(result);
8607
+ resolve3(result);
8328
8608
  };
8329
8609
  const timer = setTimeout(() => {
8330
8610
  child.kill("SIGKILL");
@@ -8350,6 +8630,7 @@ async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
8350
8630
  });
8351
8631
  });
8352
8632
  child.on("close", (code) => {
8633
+ if (ctx.keyRun) stdout2 = takeKeyRunResult(ctx, ctx.keyRun, stdout2).text;
8353
8634
  const combined = `${stdout2}
8354
8635
  ${stderr}`;
8355
8636
  const review = detectNeedsHumanReview(combined);
@@ -8372,8 +8653,31 @@ ${stderr}`;
8372
8653
  finish({ status: "done", result: { epicId: ctx.epicId } });
8373
8654
  });
8374
8655
  });
8656
+ let branchLine = "";
8657
+ if (deps.workspace && workspace) {
8658
+ const resultError = outcome.result;
8659
+ const stopReason = typeof resultError.message === "string" ? resultError.message : typeof resultError.error === "string" ? resultError.error.split("\n")[0] : void 0;
8660
+ const finished = await deps.workspace.finish({
8661
+ workspace,
8662
+ epicId: ctx.epicId,
8663
+ jobId: ctx.jobId,
8664
+ completed: outcome.status === "done",
8665
+ ...stopReason ? { stopReason } : {}
8666
+ });
8667
+ branchLine = describeRunBranch(finished);
8668
+ outcome.result = {
8669
+ ...outcome.result,
8670
+ branch: finished.branch,
8671
+ commits: finished.commits,
8672
+ pushed: finished.pushed,
8673
+ ...finished.prUrl ? { prUrl: finished.prUrl } : {},
8674
+ ...finished.compareUrl ? { compareUrl: finished.compareUrl } : {},
8675
+ ...finished.notes.length > 0 ? { branchNotes: finished.notes } : {}
8676
+ };
8677
+ }
8375
8678
  try {
8376
- const summary = outcome.status === "done" ? `Execution complete \u2014 epic ${ctx.epicId} finished all phases.` : `Orchestrator run ended: ${JSON.stringify(outcome.result).slice(0, ORCHESTRATOR_FINAL_TURN_MAX_CHARS)}`;
8679
+ const summary = (outcome.status === "done" ? `Execution complete \u2014 epic ${ctx.epicId} finished all phases.` : `Orchestrator run ended: ${JSON.stringify(outcome.result).slice(0, ORCHESTRATOR_FINAL_TURN_MAX_CHARS)}`) + (branchLine ? `
8680
+ ${branchLine}` : "");
8377
8681
  await ctx.mailboxClient.postTurn(ctx.machineToken, ctx.jobId, summary);
8378
8682
  } catch {
8379
8683
  }
@@ -8462,6 +8766,377 @@ async function updatePersonalEpicRequest(apiBaseUrl, userApiToken, epicRequestId
8462
8766
  return parseComplianceFromData(data?.data);
8463
8767
  }
8464
8768
 
8769
+ // src/utils/run-workspace.ts
8770
+ import { spawn as spawn4 } from "node:child_process";
8771
+ import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "node:fs";
8772
+ import { basename as basename2, dirname as dirname4, join as join10 } from "node:path";
8773
+ var RUN_PUSH_ENV = "COMPBOX_RUN_PUSH";
8774
+ var INSTALL_TIMEOUT_MS = 20 * 60 * 1e3;
8775
+ var GIT_TIMEOUT_MS = 5 * 60 * 1e3;
8776
+ var defaultCommandRunner = (command, args, cwd) => new Promise((resolve3) => {
8777
+ let stdout2 = "";
8778
+ let stderr = "";
8779
+ let settled = false;
8780
+ const done = (result) => {
8781
+ if (settled) return;
8782
+ settled = true;
8783
+ clearTimeout(timer);
8784
+ resolve3(result);
8785
+ };
8786
+ const child = spawn4(command, args, {
8787
+ cwd,
8788
+ stdio: ["ignore", "pipe", "pipe"],
8789
+ env: { ...process.env, PATH: claudeChildEnvPath(process.env.PATH), GIT_TERMINAL_PROMPT: "0" }
8790
+ });
8791
+ const timer = setTimeout(
8792
+ () => {
8793
+ child.kill("SIGKILL");
8794
+ done({ status: 1, stdout: stdout2, stderr: `${command} timed out` });
8795
+ },
8796
+ command === "git" || command === "gh" ? GIT_TIMEOUT_MS : INSTALL_TIMEOUT_MS
8797
+ );
8798
+ child.stdout.on("data", (chunk) => {
8799
+ stdout2 += chunk.toString("utf-8");
8800
+ });
8801
+ child.stderr.on("data", (chunk) => {
8802
+ stderr += chunk.toString("utf-8");
8803
+ });
8804
+ child.on("error", (error) => {
8805
+ done({ status: 1, stdout: stdout2, stderr: error.message });
8806
+ });
8807
+ child.on("close", (code) => {
8808
+ done({ status: code ?? 1, stdout: stdout2, stderr });
8809
+ });
8810
+ });
8811
+ function isLocalConfigFile(path3) {
8812
+ if (path3.split("/").includes("node_modules")) return false;
8813
+ const name = basename2(path3);
8814
+ return name === ".mcp.json" || name === "CLAUDE.local.md" || path3 === ".claude/settings.local.json" || name === ".env" || name.startsWith(".env.") && !name.endsWith(".example") && !name.endsWith(".sample");
8815
+ }
8816
+ function slugForBranch(value) {
8817
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "run";
8818
+ }
8819
+ function stamp(now) {
8820
+ const pad = (n) => String(n).padStart(2, "0");
8821
+ return `${String(now.getFullYear())}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
8822
+ }
8823
+ function firstLine(text) {
8824
+ return text.trim().split("\n")[0] ?? "";
8825
+ }
8826
+ function installCommand2(dir, exists = existsSync9) {
8827
+ if (exists(join10(dir, "pnpm-lock.yaml"))) return ["pnpm", ["install", "--frozen-lockfile", "--prefer-offline"]];
8828
+ if (exists(join10(dir, "package-lock.json"))) return ["npm", ["ci", "--prefer-offline"]];
8829
+ if (exists(join10(dir, "yarn.lock"))) return ["yarn", ["install", "--frozen-lockfile"]];
8830
+ return null;
8831
+ }
8832
+ function githubCompareUrl(remoteUrl, baseBranch, branch) {
8833
+ const m = /^https?:\/\/(?:[^@/]+@)?github\.com\/([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(remoteUrl.trim()) ?? /^(?:ssh:\/\/)?git@github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(remoteUrl.trim());
8834
+ if (!m) return null;
8835
+ return `https://github.com/${m[1] ?? ""}/${m[2] ?? ""}/compare/${encodeURIComponent(baseBranch)}...${encodeURIComponent(branch)}?expand=1`;
8836
+ }
8837
+ var RunWorkspaceManager = class {
8838
+ run;
8839
+ now;
8840
+ runsRoot;
8841
+ exists;
8842
+ copyFile;
8843
+ env;
8844
+ constructor(deps = {}) {
8845
+ this.run = deps.run ?? defaultCommandRunner;
8846
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
8847
+ this.runsRoot = deps.runsRoot ?? join10(getConfigDir(), "runs");
8848
+ this.exists = deps.exists ?? existsSync9;
8849
+ this.copyFile = deps.copyFile ?? ((from, to) => {
8850
+ mkdirSync6(dirname4(to), { recursive: true });
8851
+ copyFileSync(from, to);
8852
+ });
8853
+ this.env = deps.env ?? process.env;
8854
+ }
8855
+ git(args, cwd) {
8856
+ return this.run("git", args, cwd);
8857
+ }
8858
+ /** Creates the branch + worktree for a run. Never touches the person's checkout. */
8859
+ async prepare(projectDir, epicId) {
8860
+ const notes = [];
8861
+ const remotes = (await this.git(["remote"], projectDir)).stdout.split("\n").map((r) => r.trim()).filter(Boolean);
8862
+ const remote = remotes.includes("origin") ? "origin" : remotes[0] ?? null;
8863
+ let baseBranch = "";
8864
+ let baseRef = "HEAD";
8865
+ if (remote) {
8866
+ if ((await this.git(["fetch", remote, "--quiet"], projectDir)).status !== 0) {
8867
+ notes.push(`Couldn't fetch from ${remote}, so the run starts from the last fetched copy.`);
8868
+ }
8869
+ const head = await this.git(["symbolic-ref", "--quiet", "--short", `refs/remotes/${remote}/HEAD`], projectDir);
8870
+ if (head.status === 0 && head.stdout.trim().startsWith(`${remote}/`)) {
8871
+ baseBranch = head.stdout.trim().slice(remote.length + 1);
8872
+ } else {
8873
+ for (const candidate of ["main", "master"]) {
8874
+ if ((await this.git(["rev-parse", "--verify", "--quiet", `${remote}/${candidate}`], projectDir)).status === 0) {
8875
+ baseBranch = candidate;
8876
+ break;
8877
+ }
8878
+ }
8879
+ }
8880
+ if (baseBranch) baseRef = `${remote}/${baseBranch}`;
8881
+ }
8882
+ if (!baseBranch) {
8883
+ baseBranch = (await this.git(["rev-parse", "--abbrev-ref", "HEAD"], projectDir)).stdout.trim() || "main";
8884
+ baseRef = "HEAD";
8885
+ }
8886
+ const when = stamp(this.now());
8887
+ const branch = `compbox/epic-${slugForBranch(epicId)}-${when}`;
8888
+ const dir = join10(this.runsRoot, `${slugForBranch(basename2(projectDir))}-${slugForBranch(epicId)}-${when}`);
8889
+ mkdirSync6(this.runsRoot, { recursive: true });
8890
+ const add = await this.git(["worktree", "add", "-b", branch, dir, baseRef], projectDir);
8891
+ if (add.status !== 0) {
8892
+ return {
8893
+ ok: false,
8894
+ error: [
8895
+ `Couldn't create a separate workspace for this run (git worktree add failed): ${firstLine(add.stderr) || "unknown error"}`,
8896
+ "The run was not started, and your project folder was not changed."
8897
+ ].join("\n")
8898
+ };
8899
+ }
8900
+ const ignored = await this.git(["ls-files", "--others", "--ignored", "--exclude-standard", "--directory"], projectDir);
8901
+ const untracked = await this.git(["ls-files", "--others", "--exclude-standard"], projectDir);
8902
+ const candidates = new Set(
8903
+ `${ignored.stdout}
8904
+ ${untracked.stdout}`.split("\n").map((p) => p.trim()).filter((p) => p && !p.endsWith("/"))
8905
+ );
8906
+ for (const path3 of candidates) {
8907
+ if (!isLocalConfigFile(path3)) continue;
8908
+ try {
8909
+ this.copyFile(join10(projectDir, path3), join10(dir, path3));
8910
+ } catch {
8911
+ notes.push(`Couldn't copy ${path3} into the run workspace.`);
8912
+ }
8913
+ }
8914
+ const install = installCommand2(dir, this.exists);
8915
+ if (install) {
8916
+ const [command, args] = install;
8917
+ const res = await this.run(command, args, dir);
8918
+ if (res.status !== 0) {
8919
+ await this.git(["worktree", "remove", "--force", dir], projectDir);
8920
+ await this.git(["branch", "-D", branch], projectDir);
8921
+ return {
8922
+ ok: false,
8923
+ error: [
8924
+ `Installing the project's dependencies failed (${command} ${args.join(" ")}): ${firstLine(res.stderr) || firstLine(res.stdout) || "unknown error"}`,
8925
+ "The run was not started. Check that the same command works in your project folder."
8926
+ ].join("\n")
8927
+ };
8928
+ }
8929
+ }
8930
+ return { ok: true, workspace: { dir, projectDir, branch, baseRef, baseBranch, remote }, notes };
8931
+ }
8932
+ /** Commits, pushes and opens the pull request; then cleans up the worktree. */
8933
+ async finish(input) {
8934
+ const { workspace: ws } = input;
8935
+ const notes = [];
8936
+ if ((await this.git(["status", "--porcelain"], ws.dir)).stdout.trim()) {
8937
+ await this.git(["add", "-A"], ws.dir);
8938
+ const commit = await this.git(
8939
+ ["commit", "-m", `chore(compbox): save work left uncommitted by the orchestrator (epic ${input.epicId})`],
8940
+ ws.dir
8941
+ );
8942
+ if (commit.status !== 0) {
8943
+ notes.push(`Couldn't commit the remaining changes: ${firstLine(commit.stderr) || firstLine(commit.stdout)}`);
8944
+ }
8945
+ }
8946
+ const commits = Number.parseInt((await this.git(["rev-list", "--count", `${ws.baseRef}..HEAD`], ws.dir)).stdout.trim(), 10) || 0;
8947
+ const base = { branch: ws.branch, commits, pushed: false, notes };
8948
+ if (commits === 0) {
8949
+ await this.git(["worktree", "remove", "--force", ws.dir], ws.projectDir);
8950
+ await this.git(["branch", "-D", ws.branch], ws.projectDir);
8951
+ notes.push("The run made no code changes, so there is nothing to review.");
8952
+ return base;
8953
+ }
8954
+ if (!ws.remote || this.env[RUN_PUSH_ENV] === "0") {
8955
+ await this.git(["worktree", "remove", "--force", ws.dir], ws.projectDir);
8956
+ notes.push(
8957
+ ws.remote ? `Pushing is turned off (${RUN_PUSH_ENV}=0). The work is on the local branch ${ws.branch}.` : `The repository has no remote, so the work is on the local branch ${ws.branch}.`
8958
+ );
8959
+ return base;
8960
+ }
8961
+ const push = await this.git(["push", "--set-upstream", ws.remote, ws.branch], ws.dir);
8962
+ if (push.status !== 0) {
8963
+ notes.push(
8964
+ `Couldn't push the branch to ${ws.remote}: ${firstLine(push.stderr) || "unknown error"}. The work is safe in ${ws.dir} on branch ${ws.branch}.`
8965
+ );
8966
+ return { ...base, keptWorktree: ws.dir };
8967
+ }
8968
+ const title = `CompBox: epic ${input.epicId}${input.completed ? "" : " (stopped early, needs review)"}`;
8969
+ const body = [
8970
+ `Code written by the CompBox orchestrator for epic **${input.epicId}** (job \`${input.jobId}\`).`,
8971
+ "",
8972
+ input.completed ? "The orchestrator finished every phase. Review the changes, check that CI passes, then merge." : `The orchestrator stopped before finishing${input.stopReason ? `: ${input.stopReason}` : "."} This is a draft so the partial work can be reviewed.`,
8973
+ "",
8974
+ `${String(commits)} commit${commits === 1 ? "" : "s"} on \`${ws.branch}\`, based on \`${ws.baseBranch}\`.`
8975
+ ].join("\n");
8976
+ const prArgs = ["pr", "create", "--base", ws.baseBranch, "--head", ws.branch, "--title", title, "--body", body];
8977
+ if (!input.completed) prArgs.push("--draft");
8978
+ const pr = await this.run("gh", prArgs, ws.dir);
8979
+ const prUrl = pr.status === 0 ? /https:\/\/\S+/.exec(pr.stdout)?.[0] : void 0;
8980
+ let compareUrl;
8981
+ if (!prUrl) {
8982
+ const remoteUrl = (await this.git(["remote", "get-url", ws.remote], ws.dir)).stdout.trim();
8983
+ compareUrl = githubCompareUrl(remoteUrl, ws.baseBranch, ws.branch) ?? void 0;
8984
+ notes.push(
8985
+ `The branch was pushed, but the pull request couldn't be opened automatically${firstLine(pr.stderr) ? ` (${firstLine(pr.stderr)})` : ""}.` + (compareUrl ? " Use the link to open it." : ` Open a pull request for ${ws.branch} on your git host.`)
8986
+ );
8987
+ }
8988
+ await this.git(["worktree", "remove", "--force", ws.dir], ws.projectDir);
8989
+ return { ...base, pushed: true, ...prUrl ? { prUrl } : {}, ...compareUrl ? { compareUrl } : {} };
8990
+ }
8991
+ };
8992
+
8993
+ // src/utils/repo-setup.ts
8994
+ import { spawn as spawn5 } from "node:child_process";
8995
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync } from "node:fs";
8996
+ import { homedir as homedir6 } from "node:os";
8997
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join11, resolve as resolve2 } from "node:path";
8998
+ var CLONE_TIMEOUT_MS = 15 * 60 * 1e3;
8999
+ var defaultGitCloner = (url, target) => new Promise((done) => {
9000
+ let stderr = "";
9001
+ const child = spawn5("git", ["clone", "--quiet", url, target], {
9002
+ stdio: ["ignore", "ignore", "pipe"],
9003
+ // Never hang on a username/password prompt: fail and explain instead.
9004
+ env: { ...process.env, PATH: claudeChildEnvPath(process.env.PATH), GIT_TERMINAL_PROMPT: "0" }
9005
+ });
9006
+ const timer = setTimeout(() => {
9007
+ child.kill("SIGKILL");
9008
+ stderr += "git clone timed out";
9009
+ }, CLONE_TIMEOUT_MS);
9010
+ child.stderr.on("data", (chunk) => {
9011
+ stderr += chunk.toString("utf-8");
9012
+ });
9013
+ child.on("error", (error) => {
9014
+ clearTimeout(timer);
9015
+ done({ status: 1, stderr: error.message });
9016
+ });
9017
+ child.on("close", (code) => {
9018
+ clearTimeout(timer);
9019
+ done({ status: code ?? 1, stderr });
9020
+ });
9021
+ });
9022
+ function repoFolderName(remote) {
9023
+ const last = remote.replace(/\/+$/, "").split("/").pop() ?? "";
9024
+ return last.replace(/[^A-Za-z0-9._-]+/g, "-") || "project";
9025
+ }
9026
+ function resolveFolder(folder, home) {
9027
+ const trimmed = folder.trim();
9028
+ if (trimmed === "~") return home;
9029
+ if (trimmed.startsWith("~/")) return join11(home, trimmed.slice(2));
9030
+ return isAbsolute2(trimmed) ? resolve2(trimmed) : join11(home, trimmed);
9031
+ }
9032
+ function cloneUrls(remote) {
9033
+ const urls = [`${remote}.git`];
9034
+ const gh = /^https:\/\/github\.com\/([^/]+)\/([^/]+)$/.exec(remote);
9035
+ if (gh) urls.push(`git@github.com:${gh[1] ?? ""}/${gh[2] ?? ""}.git`);
9036
+ return urls;
9037
+ }
9038
+ function parsePayload(payload) {
9039
+ if (typeof payload !== "object" || payload === null) return null;
9040
+ const p = payload;
9041
+ if (typeof p.teamId !== "string" || typeof p.projectRemote !== "string") return null;
9042
+ if (!p.projectRemote.startsWith("https://")) return null;
9043
+ return {
9044
+ teamId: p.teamId,
9045
+ projectRemote: p.projectRemote,
9046
+ ...typeof p.folder === "string" ? { folder: p.folder } : {},
9047
+ ...typeof p.projectName === "string" ? { projectName: p.projectName } : {}
9048
+ };
9049
+ }
9050
+ function firstLine2(text) {
9051
+ return text.trim().split("\n").filter(Boolean).pop() ?? "";
9052
+ }
9053
+ async function invokeConnectRepo(payload, deps = {}) {
9054
+ const clone = deps.clone ?? defaultGitCloner;
9055
+ const home = deps.home ?? homedir6();
9056
+ const exists = deps.exists ?? existsSync10;
9057
+ const isEmptyDir = deps.isEmptyDir ?? ((path3) => {
9058
+ try {
9059
+ return readdirSync(path3).length === 0;
9060
+ } catch {
9061
+ return false;
9062
+ }
9063
+ });
9064
+ const isGitRepo = deps.isGitRepo ?? isGitRepoDir;
9065
+ const readOrigin = deps.readOrigin ?? readOriginRemote;
9066
+ const listDirs = deps.listDirs ?? listProjectDirs;
9067
+ const addDir = deps.addDir ?? ((path3) => addProjectDir(path3));
9068
+ const mkdir2 = deps.mkdir ?? ((path3) => void mkdirSync7(path3, { recursive: true }));
9069
+ const p = parsePayload(payload);
9070
+ if (!p) {
9071
+ return { status: "failed", result: { error: "The job was missing the project repository. Try again from the project page." } };
9072
+ }
9073
+ const remote = normalizeGitRemote(p.projectRemote);
9074
+ const name = p.projectName ?? repoFolderName(remote);
9075
+ const existing = listDirs().find((entry) => entry.remote === remote && exists(entry.path));
9076
+ if (existing) {
9077
+ return {
9078
+ status: "done",
9079
+ result: { path: existing.path, remote, outcome: "already_set_up", message: `${name} is already set up on this computer at ${existing.path}.` }
9080
+ };
9081
+ }
9082
+ const target = resolveFolder(p.folder ?? join11("CompBox", repoFolderName(remote)), home);
9083
+ try {
9084
+ if (exists(target) && !isEmptyDir(target)) {
9085
+ let origin = "";
9086
+ try {
9087
+ origin = isGitRepo(target) ? normalizeGitRemote(readOrigin(target)) : "";
9088
+ } catch {
9089
+ origin = "";
9090
+ }
9091
+ if (origin !== remote) {
9092
+ return {
9093
+ status: "failed",
9094
+ result: {
9095
+ error: `The folder ${target} already exists and isn't a copy of ${remote}. Choose a different folder and try again.`
9096
+ }
9097
+ };
9098
+ }
9099
+ const entry2 = addDir(target);
9100
+ return {
9101
+ status: "done",
9102
+ result: { path: entry2.path, remote, outcome: "adopted", message: `Found an existing copy of ${name} at ${entry2.path} and set it up.` }
9103
+ };
9104
+ }
9105
+ mkdir2(dirname5(target));
9106
+ const errors = [];
9107
+ let cloned = false;
9108
+ for (const url of cloneUrls(remote)) {
9109
+ const res = await clone(url, target);
9110
+ if (res.status === 0) {
9111
+ cloned = true;
9112
+ break;
9113
+ }
9114
+ errors.push(firstLine2(res.stderr));
9115
+ }
9116
+ if (!cloned) {
9117
+ return {
9118
+ status: "failed",
9119
+ result: {
9120
+ error: [
9121
+ `This computer couldn't download ${remote}: ${errors.filter(Boolean).join(" / ") || "unknown error"}.`,
9122
+ "Make sure this computer can access the repository, for example by signing in with `gh auth login` or adding an SSH key to your Git account, then try again."
9123
+ ].join(" ")
9124
+ }
9125
+ };
9126
+ }
9127
+ const entry = addDir(target);
9128
+ return {
9129
+ status: "done",
9130
+ result: { path: entry.path, remote, outcome: "cloned", message: `Downloaded ${name} to ${entry.path} and set it up.` }
9131
+ };
9132
+ } catch (error) {
9133
+ return {
9134
+ status: "failed",
9135
+ result: { error: `Setting up ${name} failed: ${error instanceof Error ? error.message : String(error)}` }
9136
+ };
9137
+ }
9138
+ }
9139
+
8465
9140
  // src/commands/run.ts
8466
9141
  var POLL_WAIT_MS = 0;
8467
9142
  var IDLE_POLL_INTERVAL_MS = 15e3;
@@ -8478,7 +9153,7 @@ function runtimeHeartbeatDue(nowMs, lastRuntimeSentMs, intervalMs = HEARTBEAT_IN
8478
9153
  return nowMs - lastRuntimeSentMs >= intervalMs;
8479
9154
  }
8480
9155
  function sleep2(ms) {
8481
- return new Promise((resolve2) => setTimeout(resolve2, ms));
9156
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
8482
9157
  }
8483
9158
  function backoffDelay(consecutiveErrors) {
8484
9159
  return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** (consecutiveErrors - 1));
@@ -8523,11 +9198,12 @@ function parseStartExecutionPayload(payload) {
8523
9198
  const p = parsed;
8524
9199
  if (!START_EXECUTION_KINDS.includes(p.kind)) return null;
8525
9200
  const projectRemote = typeof p.projectRemote === "string" && p.projectRemote.length > 0 ? p.projectRemote : void 0;
9201
+ const useAiKey = p.useAiKey === true;
8526
9202
  if (p.kind === "plan" && typeof p.epicRequestId === "string" && p.epicRequestId.length > 0) {
8527
- return { kind: "plan", epicRequestId: p.epicRequestId, ...projectRemote && { projectRemote } };
9203
+ return { kind: "plan", epicRequestId: p.epicRequestId, ...projectRemote && { projectRemote }, ...useAiKey && { useAiKey } };
8528
9204
  }
8529
9205
  if (p.kind === "orchestrate" && typeof p.epicId === "string" && p.epicId.length > 0) {
8530
- return { kind: "orchestrate", epicId: p.epicId, ...projectRemote && { projectRemote } };
9206
+ return { kind: "orchestrate", epicId: p.epicId, ...projectRemote && { projectRemote }, ...useAiKey && { useAiKey } };
8531
9207
  }
8532
9208
  return null;
8533
9209
  }
@@ -8550,6 +9226,18 @@ async function handleStartExecution(ctx, job) {
8550
9226
  return;
8551
9227
  }
8552
9228
  const jobProjectDir = routed.status === "registered" || routed.status === "fallback" ? routed.path : void 0;
9229
+ let keyRun;
9230
+ if (payload.useAiKey) {
9231
+ try {
9232
+ const key = await ctx.client.getAiKey(ctx.machineToken, job.id);
9233
+ keyRun = { apiKey: key.apiKey, model: key.model };
9234
+ console.log(chalk9.gray(` Running on your saved API key (${key.provider}, ${key.model}).`));
9235
+ } catch (error) {
9236
+ const message = error instanceof MailboxClientError ? error.message : "the key request failed";
9237
+ await completeWithFailure(ctx, job, `Couldn't get your saved API key from CompBox (${message}). Check Settings \u2192 AI, or run with this computer's own sign-in.`);
9238
+ return;
9239
+ }
9240
+ }
8553
9241
  if (payload.kind === "plan") {
8554
9242
  const userApiToken2 = ctx.userApiToken;
8555
9243
  if (!userApiToken2) {
@@ -8572,7 +9260,8 @@ async function handleStartExecution(ctx, job) {
8572
9260
  apiBaseUrl: ctx.apiBaseUrl,
8573
9261
  userApiToken: userApiToken2,
8574
9262
  epicRequestId: payload.epicRequestId,
8575
- projectDir: jobProjectDir
9263
+ projectDir: jobProjectDir,
9264
+ ...keyRun && { keyRun }
8576
9265
  },
8577
9266
  {
8578
9267
  // H4: keep this machine online during the long planner run — the poll
@@ -8602,12 +9291,15 @@ async function handleStartExecution(ctx, job) {
8602
9291
  machineToken: ctx.machineToken,
8603
9292
  jobId: job.id,
8604
9293
  epicId: payload.epicId,
8605
- projectDir: jobProjectDir
9294
+ projectDir: jobProjectDir,
9295
+ ...keyRun && { keyRun }
8606
9296
  },
8607
9297
  {
8608
9298
  // H4: keep this machine online during the long orchestrator run — the
8609
9299
  // poll loop (its own heartbeat) is blocked for the whole job.
8610
- heartbeat: () => ctx.client.heartbeat(ctx.machineToken).catch(() => void 0)
9300
+ heartbeat: () => ctx.client.heartbeat(ctx.machineToken).catch(() => void 0),
9301
+ // Own branch + worktree per run, finishing with a pull request.
9302
+ workspace: new RunWorkspaceManager()
8611
9303
  }
8612
9304
  )
8613
9305
  );
@@ -8675,6 +9367,17 @@ async function handleJob(ctx, job) {
8675
9367
  return;
8676
9368
  }
8677
9369
  }
9370
+ if (job.type === "connect_repo") {
9371
+ console.log(chalk9.gray(" Setting up a project repository on this computer (connect_repo)..."));
9372
+ await runInvokerAndComplete(ctx, job, async () => {
9373
+ const outcome = await invokeConnectRepo(job.payload);
9374
+ if (outcome.status === "done") {
9375
+ await ctx.client.heartbeat(ctx.machineToken, void 0, gatherServicedRemotes()).catch(() => void 0);
9376
+ }
9377
+ return outcome;
9378
+ });
9379
+ return;
9380
+ }
8678
9381
  if (job.type === "start_execution") {
8679
9382
  await handleStartExecution(ctx, job);
8680
9383
  return;
@@ -9090,7 +9793,7 @@ machinesCommand.command("rm <id>").description("Remove (revoke) a paired machine
9090
9793
  // src/index.ts
9091
9794
  function resolveCliVersion() {
9092
9795
  if (true) {
9093
- return "1.18.0";
9796
+ return "1.19.0";
9094
9797
  }
9095
9798
  const require2 = createRequire(import.meta.url);
9096
9799
  const pkg = require2("../package.json");