@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.
- package/.tsbuildinfo +1 -1
- package/commands/configure.d.ts.map +1 -1
- package/commands/configure.js +31 -11
- package/commands/configure.js.map +1 -1
- package/commands/doctor.d.ts.map +1 -1
- package/commands/doctor.js +1 -1
- package/commands/doctor.js.map +1 -1
- package/commands/login.d.ts +2 -0
- package/commands/login.d.ts.map +1 -1
- package/commands/login.js +28 -6
- package/commands/login.js.map +1 -1
- package/commands/run.d.ts.map +1 -1
- package/commands/run.js +37 -2
- package/commands/run.js.map +1 -1
- package/index.js +887 -184
- package/package.json +1 -1
- package/utils/agent-invoker.d.ts +31 -58
- package/utils/agent-invoker.d.ts.map +1 -1
- package/utils/agent-invoker.js +95 -186
- package/utils/agent-invoker.js.map +1 -1
- package/utils/autostart.d.ts +41 -0
- package/utils/autostart.d.ts.map +1 -0
- package/utils/autostart.js +122 -0
- package/utils/autostart.js.map +1 -0
- package/utils/key-run.d.ts +32 -0
- package/utils/key-run.d.ts.map +1 -0
- package/utils/key-run.js +77 -0
- package/utils/key-run.js.map +1 -0
- package/utils/loopback.d.ts +3 -3
- package/utils/loopback.d.ts.map +1 -1
- package/utils/loopback.js +3 -3
- package/utils/loopback.js.map +1 -1
- package/utils/mailbox-client.d.ts +21 -0
- package/utils/mailbox-client.d.ts.map +1 -1
- package/utils/mailbox-client.js +34 -0
- package/utils/mailbox-client.js.map +1 -1
- package/utils/mcp-config-writer.d.ts +2 -0
- package/utils/mcp-config-writer.d.ts.map +1 -1
- package/utils/mcp-config-writer.js +9 -1
- package/utils/mcp-config-writer.js.map +1 -1
- package/utils/repo-setup.d.ts +44 -0
- package/utils/repo-setup.d.ts.map +1 -0
- package/utils/repo-setup.js +178 -0
- package/utils/repo-setup.js.map +1 -0
- package/utils/run-workspace.d.ts +114 -0
- package/utils/run-workspace.d.ts.map +1 -0
- package/utils/run-workspace.js +264 -0
- 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((
|
|
5783
|
-
rl.question(rendered,
|
|
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((
|
|
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
|
-
|
|
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.
|
|
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(
|
|
5894
|
-
|
|
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").
|
|
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(
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
6844
|
+
console.log("\n" + chalk7.bold("Project folder:"));
|
|
6554
6845
|
console.log(
|
|
6555
6846
|
chalk7.gray(
|
|
6556
|
-
"
|
|
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
|
|
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
|
|
6677
|
-
if (enablePxpipe
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
6756
|
-
import { delimiter, join as
|
|
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 =
|
|
7067
|
+
const home = homedir4();
|
|
6760
7068
|
const dirs = [
|
|
6761
|
-
|
|
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(
|
|
7077
|
+
dirs.push(join8(process.env.npm_config_prefix, "bin"));
|
|
6770
7078
|
}
|
|
6771
|
-
dirs.push(
|
|
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(
|
|
6776
|
-
dirs.push(
|
|
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
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
7420
|
-
import { homedir as
|
|
7421
|
-
import { join as
|
|
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((
|
|
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 =
|
|
8032
|
+
const dir = join9(homedir5(), ".compbox", "claude-sessions");
|
|
7785
8033
|
try {
|
|
7786
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
|
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((
|
|
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:
|
|
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
|
-
|
|
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((
|
|
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.
|
|
9796
|
+
return "1.19.0";
|
|
9094
9797
|
}
|
|
9095
9798
|
const require2 = createRequire(import.meta.url);
|
|
9096
9799
|
const pkg = require2("../package.json");
|