@mycompbox/compbox-cli 1.18.0 → 1.20.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 +12 -1
- package/commands/run.d.ts.map +1 -1
- package/commands/run.js +110 -30
- package/commands/run.js.map +1 -1
- package/index.js +1264 -540
- package/package.json +1 -1
- package/utils/agent-invoker.d.ts +59 -101
- package/utils/agent-invoker.d.ts.map +1 -1
- package/utils/agent-invoker.js +183 -508
- 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 +31 -1
- package/utils/mailbox-client.d.ts.map +1 -1
- package/utils/mailbox-client.js +60 -2
- 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 +120 -0
- package/utils/run-workspace.d.ts.map +1 -0
- package/utils/run-workspace.js +298 -0
- package/utils/run-workspace.js.map +1 -0
package/index.js
CHANGED
|
@@ -5555,84 +5555,360 @@ var INTERVIEW_DEFINITION = [
|
|
|
5555
5555
|
fields: ["alternatives"]
|
|
5556
5556
|
}
|
|
5557
5557
|
];
|
|
5558
|
+
function interviewFieldKeys() {
|
|
5559
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5560
|
+
const out = [];
|
|
5561
|
+
for (const section of INTERVIEW_DEFINITION) {
|
|
5562
|
+
for (const f of section.fields) {
|
|
5563
|
+
if (!seen.has(f)) {
|
|
5564
|
+
seen.add(f);
|
|
5565
|
+
out.push(f);
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
return out;
|
|
5570
|
+
}
|
|
5558
5571
|
var INTERVIEW_FIELD_MIN_WORDS = {
|
|
5559
5572
|
problemStatement: 60,
|
|
5560
5573
|
proposedSolution: 100,
|
|
5561
5574
|
impactAssessment: 40,
|
|
5562
5575
|
description: 150
|
|
5563
5576
|
};
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5577
|
+
|
|
5578
|
+
// ../shared/dist/interview-completion.js
|
|
5579
|
+
var TARGET_COMPLIANCE_TIER = "pass";
|
|
5580
|
+
function meetsComplianceTarget(compliance) {
|
|
5581
|
+
return !compliance || compliance.tier === TARGET_COMPLIANCE_TIER;
|
|
5582
|
+
}
|
|
5583
|
+
function buildAuditFeedback(compliance) {
|
|
5584
|
+
const ordered = [
|
|
5585
|
+
...compliance.issues.filter((i) => i.tier === "critical"),
|
|
5586
|
+
...compliance.issues.filter((i) => i.tier === "advisory")
|
|
5587
|
+
].slice(0, 8);
|
|
5588
|
+
const list = ordered.length > 0 ? ordered.map((i) => `- ${i.message}`).join("\n") : "- (no specific issues returned; expand the required fields with more concrete detail)";
|
|
5589
|
+
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:
|
|
5590
|
+
${list}
|
|
5591
|
+
|
|
5592
|
+
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.`;
|
|
5593
|
+
}
|
|
5594
|
+
var OPTIONAL_DRAFT_KEYS = [
|
|
5595
|
+
"successMetrics",
|
|
5596
|
+
"targetAudience",
|
|
5597
|
+
"alternatives",
|
|
5598
|
+
"dependencies",
|
|
5599
|
+
"estimatedEffort"
|
|
5600
|
+
];
|
|
5601
|
+
function isEpicRequestDraft(value) {
|
|
5602
|
+
if (typeof value !== "object" || value === null)
|
|
5603
|
+
return false;
|
|
5604
|
+
const v = value;
|
|
5605
|
+
const nonEmpty = (x) => typeof x === "string" && x.trim().length > 0;
|
|
5606
|
+
const requiredOk = nonEmpty(v.title) && nonEmpty(v.problemStatement) && nonEmpty(v.proposedSolution) && nonEmpty(v.impactAssessment) && nonEmpty(v.description);
|
|
5607
|
+
if (!requiredOk)
|
|
5608
|
+
return false;
|
|
5609
|
+
for (const key of OPTIONAL_DRAFT_KEYS) {
|
|
5610
|
+
if (key in v && v[key] !== void 0 && typeof v[key] !== "string")
|
|
5611
|
+
return false;
|
|
5612
|
+
}
|
|
5613
|
+
return true;
|
|
5614
|
+
}
|
|
5615
|
+
function stripTrailingCommas(json) {
|
|
5616
|
+
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
5617
|
+
}
|
|
5618
|
+
function escapeRawControlCharsInStrings(json) {
|
|
5619
|
+
let out = "";
|
|
5620
|
+
let inString = false;
|
|
5621
|
+
let escaped = false;
|
|
5622
|
+
for (const ch of json) {
|
|
5623
|
+
if (escaped) {
|
|
5624
|
+
out += ch;
|
|
5625
|
+
escaped = false;
|
|
5626
|
+
continue;
|
|
5627
|
+
}
|
|
5628
|
+
if (ch === "\\") {
|
|
5629
|
+
out += ch;
|
|
5630
|
+
escaped = true;
|
|
5631
|
+
continue;
|
|
5632
|
+
}
|
|
5633
|
+
if (ch === '"') {
|
|
5634
|
+
inString = !inString;
|
|
5635
|
+
out += ch;
|
|
5636
|
+
continue;
|
|
5637
|
+
}
|
|
5638
|
+
if (inString && (ch === "\n" || ch === "\r" || ch === " ")) {
|
|
5639
|
+
out += ch === "\n" ? "\\n" : ch === "\r" ? "\\r" : "\\t";
|
|
5640
|
+
continue;
|
|
5641
|
+
}
|
|
5642
|
+
out += ch;
|
|
5643
|
+
}
|
|
5644
|
+
return out;
|
|
5645
|
+
}
|
|
5646
|
+
function completionJsonCandidates(text) {
|
|
5647
|
+
const candidates = [];
|
|
5648
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
5649
|
+
if (fenced?.[1])
|
|
5650
|
+
candidates.push(fenced[1]);
|
|
5651
|
+
const trimmed = text.trim();
|
|
5652
|
+
if (trimmed.startsWith("{"))
|
|
5653
|
+
candidates.push(trimmed);
|
|
5654
|
+
const first = text.indexOf("{");
|
|
5655
|
+
const last = text.lastIndexOf("}");
|
|
5656
|
+
if (first !== -1 && last > first)
|
|
5657
|
+
candidates.push(text.slice(first, last + 1));
|
|
5658
|
+
return candidates;
|
|
5659
|
+
}
|
|
5660
|
+
function toDraft(parsed) {
|
|
5661
|
+
const draft = {
|
|
5662
|
+
title: parsed.title,
|
|
5663
|
+
problemStatement: parsed.problemStatement,
|
|
5664
|
+
proposedSolution: parsed.proposedSolution,
|
|
5665
|
+
impactAssessment: parsed.impactAssessment,
|
|
5666
|
+
description: parsed.description
|
|
5573
5667
|
};
|
|
5574
|
-
const
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5668
|
+
for (const key of OPTIONAL_DRAFT_KEYS) {
|
|
5669
|
+
const raw = parsed[key];
|
|
5670
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
5671
|
+
draft[key] = raw;
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
return draft;
|
|
5675
|
+
}
|
|
5676
|
+
function extractCompletionJson(text) {
|
|
5677
|
+
for (const candidate of completionJsonCandidates(text)) {
|
|
5678
|
+
for (const variant of [
|
|
5679
|
+
candidate,
|
|
5680
|
+
stripTrailingCommas(candidate),
|
|
5681
|
+
escapeRawControlCharsInStrings(candidate),
|
|
5682
|
+
escapeRawControlCharsInStrings(stripTrailingCommas(candidate))
|
|
5683
|
+
]) {
|
|
5684
|
+
try {
|
|
5685
|
+
const parsed = JSON.parse(variant);
|
|
5686
|
+
if (isEpicRequestDraft(parsed))
|
|
5687
|
+
return toDraft(parsed);
|
|
5688
|
+
} catch {
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
return null;
|
|
5693
|
+
}
|
|
5694
|
+
function looksLikeCompletionAttempt(text) {
|
|
5695
|
+
const hasObject = /```(?:json)?\s*\{/i.test(text) || text.trim().startsWith("{");
|
|
5696
|
+
if (!hasObject)
|
|
5697
|
+
return false;
|
|
5698
|
+
return /"(?:done|title|problemStatement|proposedSolution|impactAssessment|description)"\s*:/i.test(text);
|
|
5699
|
+
}
|
|
5700
|
+
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```';
|
|
5701
|
+
|
|
5702
|
+
// ../shared/dist/interview-conversation.js
|
|
5703
|
+
var CONVERSATION_QUALITY_ROUNDS = 2;
|
|
5704
|
+
var JSON_REPAIRS = 3;
|
|
5705
|
+
var STARTER = "Hi! I'd like to propose something.";
|
|
5706
|
+
var NUDGE = "(Please continue.)";
|
|
5707
|
+
function renderSection(s) {
|
|
5708
|
+
const tag = s.required ? "needed" : "nice to have";
|
|
5709
|
+
return `- ${s.title} (${tag}): ${s.purpose} For example: ${s.followUps.slice(0, 2).join(" ")}`;
|
|
5710
|
+
}
|
|
5711
|
+
function renderSaved(saved) {
|
|
5712
|
+
const fieldLines = Object.entries(saved.fields).filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0).map(([key, value]) => `${key}: ${value}`).join("\n");
|
|
5713
|
+
const score = saved.compliance ? `Its quality score is ${String(saved.compliance.overall)}/100 (${saved.compliance.tier}).` : "It has no quality score yet.";
|
|
5714
|
+
const issues = saved.compliance && saved.compliance.issues.length > 0 ? `
|
|
5715
|
+
What the score says is missing or weak:
|
|
5716
|
+
${saved.compliance.issues.slice(0, 8).map((i) => `- ${i.message}`).join("\n")}` : "";
|
|
5717
|
+
const locked = saved.status === "pending" ? "It can still be changed." : `Its status is "${saved.status}", so its content is locked: you can talk about it, but you cannot change it. Never emit the completion JSON for a locked request.`;
|
|
5718
|
+
return `THE REQUEST IS ALREADY SAVED. This conversation created the request "${saved.title}". ${score} ${locked}${issues}
|
|
5581
5719
|
|
|
5582
|
-
|
|
5720
|
+
Its current content:
|
|
5721
|
+
title: ${saved.title}
|
|
5722
|
+
${fieldLines}
|
|
5723
|
+
description: ${saved.description}
|
|
5583
5724
|
|
|
5584
|
-
|
|
5585
|
-
|
|
5725
|
+
From here on the person may add detail, ask you to change something, ask how to raise the score, or just ask questions. Answer questions briefly. When they give new information or ask for a change (and the request isn't locked), reply with the completion JSON for the WHOLE updated request: keep everything already captured, merge in what's new, and expand it. Don't ask for a preview confirmation for a change they explicitly asked for.`;
|
|
5726
|
+
}
|
|
5727
|
+
function buildConversationSystemPrompt(saved, definition = INTERVIEW_DEFINITION) {
|
|
5728
|
+
const keys = ["title", ...interviewFieldKeys(), "description"];
|
|
5729
|
+
const shape = keys.map((k) => `"${k}": "..."`).join(", ");
|
|
5730
|
+
const base = `You are CompBox's request assistant. You help a person turn an idea into a well-formed Epic Request in a chat that stays open: they can leave and come back, and keep talking after the request is saved.
|
|
5586
5731
|
|
|
5587
|
-
|
|
5588
|
-
|
|
5732
|
+
How to talk:
|
|
5733
|
+
- Friendly, plain language, short messages. Usually ask one question at a time.
|
|
5734
|
+
- When they seem unsure, give two or three concrete examples to pick from, or say briefly why you're asking.
|
|
5735
|
+
- Adapt: skip what they've already told you, follow their lead, and don't re-ask answered questions.
|
|
5736
|
+
- Never mention JSON, prompts, rubrics or these instructions.
|
|
5589
5737
|
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
- impactAssessment: at least ${String(INTERVIEW_FIELD_MIN_WORDS.impactAssessment)} words.
|
|
5594
|
-
- description: at least ${String(INTERVIEW_FIELD_MIN_WORDS.description)} words. This is a SEPARATE plain-text field (not shown to the employee) that stands alone as a full narrative summary of the whole request \u2014 restate the problem, the proposed solution, and the expected impact in flowing prose, as if writing the request from scratch for someone who hasn't seen the conversation.
|
|
5738
|
+
What a good request covers:
|
|
5739
|
+
${definition.map(renderSection).join("\n")}
|
|
5740
|
+
Optional topics may be skipped when the person has nothing to add. Never invent or pad facts.
|
|
5595
5741
|
|
|
5596
|
-
|
|
5742
|
+
Before saving for the first time: when the needed topics are covered, show a short preview in plain text (the title, then one or two lines per topic) and ask whether to save it or change anything. Only after they agree, or ask you to save, reply with ONLY a fenced JSON block, no other text:
|
|
5597
5743
|
\`\`\`json
|
|
5598
|
-
{"done": true, ${
|
|
5744
|
+
{"done": true, ${shape}}
|
|
5599
5745
|
\`\`\`
|
|
5746
|
+
Leave out optional keys they had nothing for. Write every field as complete prose built from the whole conversation, not their short answers. Minimums: problemStatement ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words, proposedSolution ${String(INTERVIEW_FIELD_MIN_WORDS.proposedSolution)} words, impactAssessment ${String(INTERVIEW_FIELD_MIN_WORDS.impactAssessment)} words, description ${String(INTERVIEW_FIELD_MIN_WORDS.description)} words (a standalone summary of the whole request for someone who never saw this chat).
|
|
5600
5747
|
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
return base;
|
|
5604
|
-
}
|
|
5605
|
-
return `${renderContinuationHeader(seedContext.priorTranscript)}
|
|
5748
|
+
Any reply that isn't that JSON block is shown to the person as your message.`;
|
|
5749
|
+
return saved ? `${base}
|
|
5606
5750
|
|
|
5607
|
-
${base
|
|
5751
|
+
${renderSaved(saved)}` : base;
|
|
5608
5752
|
}
|
|
5609
|
-
function
|
|
5610
|
-
const
|
|
5611
|
-
|
|
5753
|
+
function toConversationMessages(turns) {
|
|
5754
|
+
const messages = turns.map((t) => ({
|
|
5755
|
+
role: t.role === "agent" ? "assistant" : "user",
|
|
5756
|
+
content: t.content
|
|
5757
|
+
}));
|
|
5758
|
+
if (messages[0]?.role !== "user")
|
|
5759
|
+
messages.unshift({ role: "user", content: STARTER });
|
|
5760
|
+
if (messages[messages.length - 1]?.role !== "user")
|
|
5761
|
+
messages.push({ role: "user", content: NUDGE });
|
|
5762
|
+
return messages;
|
|
5763
|
+
}
|
|
5764
|
+
function renderConversationPrompt(messages) {
|
|
5765
|
+
const lines = messages.map((m) => `${m.role === "assistant" ? "You" : "Them"}: ${m.content}`);
|
|
5766
|
+
return `The conversation so far:
|
|
5612
5767
|
|
|
5613
|
-
|
|
5614
|
-
${transcript}
|
|
5768
|
+
${lines.join("\n\n")}
|
|
5615
5769
|
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5770
|
+
Write your next reply to them.`;
|
|
5771
|
+
}
|
|
5772
|
+
function savedMessage(saved, created, followUp) {
|
|
5773
|
+
const verb = created ? "Saved your request" : "Updated your request";
|
|
5774
|
+
const c = saved.compliance;
|
|
5775
|
+
let text;
|
|
5776
|
+
if (!c) {
|
|
5777
|
+
text = `${verb} "${saved.title}". You can keep talking here to add detail or change anything.`;
|
|
5778
|
+
} else if (meetsComplianceTarget(c)) {
|
|
5779
|
+
text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100, ready for review. You can keep talking here to change anything.`;
|
|
5780
|
+
} else {
|
|
5781
|
+
const gaps = [...c.issues.filter((i) => i.tier === "critical"), ...c.issues.filter((i) => i.tier === "advisory")].slice(0, 4).map((i) => `- ${i.message}`).join("\n");
|
|
5782
|
+
text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100; 90 is the target.${gaps ? ` To raise it:
|
|
5783
|
+
${gaps}` : ""}
|
|
5621
5784
|
|
|
5622
|
-
|
|
5623
|
-
}
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5785
|
+
Tell me more here and I'll update it, or leave it as it is.`;
|
|
5786
|
+
}
|
|
5787
|
+
return followUp ? `${text}
|
|
5788
|
+
|
|
5789
|
+
${followUp}` : text;
|
|
5790
|
+
}
|
|
5791
|
+
async function runConversationStep(input, deps) {
|
|
5792
|
+
const system = buildConversationSystemPrompt(input.saved);
|
|
5793
|
+
const messages = toConversationMessages(input.turns);
|
|
5794
|
+
const locked = input.saved !== null && input.saved.status !== "pending";
|
|
5795
|
+
let requestId = input.saved?.id ?? null;
|
|
5796
|
+
let savedNow = null;
|
|
5797
|
+
let createdHere = false;
|
|
5798
|
+
let qualityRounds = 0;
|
|
5799
|
+
let repairs = 0;
|
|
5800
|
+
for (; ; ) {
|
|
5801
|
+
let reply;
|
|
5802
|
+
try {
|
|
5803
|
+
reply = await deps.reply(system, messages);
|
|
5804
|
+
} catch (error) {
|
|
5805
|
+
if (savedNow)
|
|
5806
|
+
return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow };
|
|
5807
|
+
const reason = error instanceof Error ? error.message : "Something went wrong reaching the AI.";
|
|
5808
|
+
return {
|
|
5809
|
+
kind: "error",
|
|
5810
|
+
text: `I couldn't reply just now: ${reason}
|
|
5811
|
+
|
|
5812
|
+
Send another message (for example "try again") when you're ready and I'll pick up where we left off.`
|
|
5813
|
+
};
|
|
5814
|
+
}
|
|
5815
|
+
const draft = extractCompletionJson(reply);
|
|
5816
|
+
if (draft) {
|
|
5817
|
+
if (locked) {
|
|
5818
|
+
messages.push({ role: "assistant", content: reply }, { role: "user", content: "That request is locked and can't be changed. Reply to me in plain text instead." });
|
|
5819
|
+
if (++repairs > JSON_REPAIRS)
|
|
5820
|
+
return { kind: "error", text: "This request is locked, so it can't be changed any more." };
|
|
5821
|
+
continue;
|
|
5822
|
+
}
|
|
5823
|
+
let saved;
|
|
5824
|
+
try {
|
|
5825
|
+
saved = await deps.save(requestId, draft);
|
|
5826
|
+
} catch (error) {
|
|
5827
|
+
const reason = error instanceof Error ? error.message : "the save failed";
|
|
5828
|
+
return { kind: "error", text: `I couldn't save the request: ${reason}
|
|
5829
|
+
|
|
5830
|
+
Reply "try again" and I'll retry.` };
|
|
5831
|
+
}
|
|
5832
|
+
createdHere ||= requestId === null;
|
|
5833
|
+
savedNow = { saved, created: createdHere };
|
|
5834
|
+
requestId = saved.id;
|
|
5835
|
+
if (meetsComplianceTarget(saved.compliance) || qualityRounds >= CONVERSATION_QUALITY_ROUNDS || !saved.compliance) {
|
|
5836
|
+
return { kind: "saved", text: savedMessage(saved, savedNow.created), ...savedNow };
|
|
5837
|
+
}
|
|
5838
|
+
qualityRounds++;
|
|
5839
|
+
messages.push({ role: "assistant", content: reply }, {
|
|
5840
|
+
role: "user",
|
|
5841
|
+
content: `${buildAuditFeedback(saved.compliance)}
|
|
5842
|
+
|
|
5843
|
+
The request IS already saved, and the person will be shown its score. If you ask them something, ask it as a way to improve the saved request; never say it hasn't been saved.`
|
|
5844
|
+
});
|
|
5845
|
+
continue;
|
|
5846
|
+
}
|
|
5847
|
+
if (looksLikeCompletionAttempt(reply)) {
|
|
5848
|
+
if (++repairs > JSON_REPAIRS) {
|
|
5849
|
+
return savedNow ? { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow } : { kind: "error", text: `I had trouble writing up the request. Reply "try again" and I'll have another go.` };
|
|
5632
5850
|
}
|
|
5851
|
+
messages.push({ role: "assistant", content: reply }, { role: "user", content: COMPLETION_REPAIR_INSTRUCTION });
|
|
5852
|
+
continue;
|
|
5853
|
+
}
|
|
5854
|
+
const text = reply.trim();
|
|
5855
|
+
if (savedNow) {
|
|
5856
|
+
return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created, text || void 0), ...savedNow };
|
|
5633
5857
|
}
|
|
5858
|
+
if (!text)
|
|
5859
|
+
return { kind: "error", text: "I didn't get a reply that time. Send another message and I'll try again." };
|
|
5860
|
+
return { kind: "message", text };
|
|
5634
5861
|
}
|
|
5635
|
-
|
|
5862
|
+
}
|
|
5863
|
+
function savedContextFromRecord(record) {
|
|
5864
|
+
if (typeof record !== "object" || record === null)
|
|
5865
|
+
return null;
|
|
5866
|
+
const r = record;
|
|
5867
|
+
if (typeof r.id !== "string" || typeof r.title !== "string")
|
|
5868
|
+
return null;
|
|
5869
|
+
let structured = r.structuredDesc;
|
|
5870
|
+
if (typeof structured === "string") {
|
|
5871
|
+
try {
|
|
5872
|
+
structured = JSON.parse(structured);
|
|
5873
|
+
} catch {
|
|
5874
|
+
structured = null;
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
const fields = {};
|
|
5878
|
+
if (typeof structured === "object" && structured !== null) {
|
|
5879
|
+
for (const [key, value] of Object.entries(structured)) {
|
|
5880
|
+
if (typeof value === "string")
|
|
5881
|
+
fields[key] = value;
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
let score = r.complianceScore;
|
|
5885
|
+
if (typeof score === "string") {
|
|
5886
|
+
try {
|
|
5887
|
+
score = JSON.parse(score);
|
|
5888
|
+
} catch {
|
|
5889
|
+
score = null;
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
return {
|
|
5893
|
+
id: r.id,
|
|
5894
|
+
title: r.title,
|
|
5895
|
+
status: typeof r.status === "string" ? r.status : "pending",
|
|
5896
|
+
description: typeof r.description === "string" ? r.description : "",
|
|
5897
|
+
fields,
|
|
5898
|
+
compliance: complianceFromRecord(score)
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5901
|
+
function complianceFromRecord(score) {
|
|
5902
|
+
if (typeof score !== "object" || score === null)
|
|
5903
|
+
return null;
|
|
5904
|
+
const s = score;
|
|
5905
|
+
if (typeof s.overall !== "number" || typeof s.tier !== "string")
|
|
5906
|
+
return null;
|
|
5907
|
+
const issues = Array.isArray(s.issues) ? s.issues.filter((i) => typeof i === "object" && i !== null).map((i) => ({
|
|
5908
|
+
message: typeof i.message === "string" ? i.message : typeof i.code === "string" ? i.code : "issue",
|
|
5909
|
+
tier: i.tier === "critical" ? "critical" : "advisory"
|
|
5910
|
+
})) : [];
|
|
5911
|
+
return { overall: s.overall, tier: s.tier, issues };
|
|
5636
5912
|
}
|
|
5637
5913
|
|
|
5638
5914
|
// src/utils/config-store.ts
|
|
@@ -5779,8 +6055,8 @@ async function promptText(question, options = {}) {
|
|
|
5779
6055
|
const rendered = defaultValue !== void 0 ? `${question} [${defaultValue}]: ` : question;
|
|
5780
6056
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
5781
6057
|
try {
|
|
5782
|
-
const answer = await new Promise((
|
|
5783
|
-
rl.question(rendered,
|
|
6058
|
+
const answer = await new Promise((resolve3) => {
|
|
6059
|
+
rl.question(rendered, resolve3);
|
|
5784
6060
|
});
|
|
5785
6061
|
const trimmed = answer.trim();
|
|
5786
6062
|
if (trimmed.length === 0 && defaultValue !== void 0) {
|
|
@@ -5792,7 +6068,7 @@ async function promptText(question, options = {}) {
|
|
|
5792
6068
|
}
|
|
5793
6069
|
}
|
|
5794
6070
|
function promptSecret(question) {
|
|
5795
|
-
return new Promise((
|
|
6071
|
+
return new Promise((resolve3) => {
|
|
5796
6072
|
const rl = readline.createInterface({
|
|
5797
6073
|
input: stdin,
|
|
5798
6074
|
output: stdout,
|
|
@@ -5804,7 +6080,7 @@ function promptSecret(question) {
|
|
|
5804
6080
|
rl.question("", (answer) => {
|
|
5805
6081
|
rl.close();
|
|
5806
6082
|
stdout.write("\n");
|
|
5807
|
-
|
|
6083
|
+
resolve3(answer.trim());
|
|
5808
6084
|
});
|
|
5809
6085
|
});
|
|
5810
6086
|
}
|
|
@@ -5842,7 +6118,7 @@ async function promptConfirm(question, defaultYes = true) {
|
|
|
5842
6118
|
}
|
|
5843
6119
|
|
|
5844
6120
|
// src/utils/loopback.ts
|
|
5845
|
-
var LOOPBACK_IMPACT = "your work will NOT reach the cloud (mycompbox.
|
|
6121
|
+
var LOOPBACK_IMPACT = "your work will NOT reach the cloud (mycompbox.com)";
|
|
5846
6122
|
var LOOPBACK_NOTICE = `This is a LOCAL dev endpoint \u2014 ${LOOPBACK_IMPACT}. Use \`compbox login --url <cloud-url>\` for production.`;
|
|
5847
6123
|
function isLoopbackHostname(hostname3) {
|
|
5848
6124
|
let host = hostname3.trim().toLowerCase();
|
|
@@ -5882,6 +6158,7 @@ function isLoopbackUrl(rawUrl) {
|
|
|
5882
6158
|
}
|
|
5883
6159
|
|
|
5884
6160
|
// src/commands/login.ts
|
|
6161
|
+
var DEFAULT_BASE_URL = "https://mycompbox.com";
|
|
5885
6162
|
async function resolveBaseUrl(option) {
|
|
5886
6163
|
if (option && option.trim().length > 0) {
|
|
5887
6164
|
return option.trim();
|
|
@@ -5890,19 +6167,40 @@ async function resolveBaseUrl(option) {
|
|
|
5890
6167
|
if (existing && existing.trim().length > 0) {
|
|
5891
6168
|
return existing.trim();
|
|
5892
6169
|
}
|
|
5893
|
-
const entered = await promptText(
|
|
5894
|
-
|
|
5895
|
-
throw new Error("A central API URL is required.");
|
|
5896
|
-
}
|
|
5897
|
-
return entered;
|
|
6170
|
+
const entered = await promptText(`CompBox URL [${DEFAULT_BASE_URL}]: `);
|
|
6171
|
+
return entered.length === 0 ? DEFAULT_BASE_URL : entered;
|
|
5898
6172
|
}
|
|
5899
6173
|
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
6174
|
"--password <password>",
|
|
5901
6175
|
"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").
|
|
6176
|
+
).option("--token-name <name>", "Name for the generated API token").option(
|
|
6177
|
+
"--api-token <token>",
|
|
6178
|
+
"Sign in with an existing CompBox API token (st_...) from web Settings \u2192 API Keys \u2014 use this if you sign in with Google"
|
|
6179
|
+
).action(async (options) => {
|
|
5903
6180
|
try {
|
|
5904
6181
|
const baseUrl = await resolveBaseUrl(options.url);
|
|
5905
6182
|
const client = new AuthClient(baseUrl);
|
|
6183
|
+
if (options.apiToken && options.apiToken.trim().length > 0) {
|
|
6184
|
+
const token = options.apiToken.trim();
|
|
6185
|
+
console.log(chalk6.gray(`
|
|
6186
|
+
Verifying the API token against ${baseUrl}...`));
|
|
6187
|
+
const me = await client.getMe(token);
|
|
6188
|
+
updateConfig({
|
|
6189
|
+
apiBaseUrl: client.baseUrl,
|
|
6190
|
+
apiToken: token,
|
|
6191
|
+
userId: me.id,
|
|
6192
|
+
email: me.email,
|
|
6193
|
+
userName: me.name
|
|
6194
|
+
});
|
|
6195
|
+
console.log(chalk6.green(`
|
|
6196
|
+
\u2714 Paired as ${me.name} <${me.email}> using your API token`));
|
|
6197
|
+
console.log(chalk6.gray(` Central URL: ${client.baseUrl}`));
|
|
6198
|
+
console.log("\n" + chalk6.bold("Next step:"));
|
|
6199
|
+
console.log(
|
|
6200
|
+
chalk6.gray(" Run `compbox configure` to select your AI mode, pair this device, and write the MCP config.")
|
|
6201
|
+
);
|
|
6202
|
+
return;
|
|
6203
|
+
}
|
|
5906
6204
|
const email = options.email && options.email.trim().length > 0 ? options.email.trim() : await promptText("Email: ");
|
|
5907
6205
|
if (email.length === 0) {
|
|
5908
6206
|
throw new Error("Email is required.");
|
|
@@ -5952,7 +6250,17 @@ Authenticating to ${baseUrl}...`));
|
|
|
5952
6250
|
console.error(chalk6.red(`
|
|
5953
6251
|
Login failed: ${error.message}`));
|
|
5954
6252
|
if (error.status === 401) {
|
|
5955
|
-
console.error(
|
|
6253
|
+
console.error(
|
|
6254
|
+
chalk6.yellow(
|
|
6255
|
+
"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."
|
|
6256
|
+
)
|
|
6257
|
+
);
|
|
6258
|
+
} else if (error.status === 403) {
|
|
6259
|
+
console.error(
|
|
6260
|
+
chalk6.yellow(
|
|
6261
|
+
"Tip: if you just signed up, open the verification link we emailed you, then run `compbox login` again."
|
|
6262
|
+
)
|
|
6263
|
+
);
|
|
5956
6264
|
} else if (error.status === 0) {
|
|
5957
6265
|
console.error(chalk6.yellow("Tip: verify the central API URL and your network connection."));
|
|
5958
6266
|
}
|
|
@@ -5969,7 +6277,7 @@ Login failed: ${error.message}`));
|
|
|
5969
6277
|
// src/commands/configure.ts
|
|
5970
6278
|
import { Command as Command6 } from "commander";
|
|
5971
6279
|
import chalk7 from "chalk";
|
|
5972
|
-
import { execFileSync as
|
|
6280
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
5973
6281
|
import { hostname as hostname2 } from "os";
|
|
5974
6282
|
|
|
5975
6283
|
// src/utils/keychain.ts
|
|
@@ -6204,12 +6512,18 @@ var MailboxClient = class {
|
|
|
6204
6512
|
* here. Cloud Run bills vCPU for the wall-clock life of a request, so a held
|
|
6205
6513
|
* connection kept one instance permanently awake at ~$64/month.
|
|
6206
6514
|
*/
|
|
6207
|
-
|
|
6515
|
+
/**
|
|
6516
|
+
* `runningIds`: jobs this daemon is already running, so the server hands out
|
|
6517
|
+
* new work instead of resuming one of them.
|
|
6518
|
+
*/
|
|
6519
|
+
async nextJob(machineToken, waitMs = 0, runningIds = []) {
|
|
6208
6520
|
let res;
|
|
6209
6521
|
try {
|
|
6210
6522
|
res = await this.http.get("/api/v1/machines/next-job", {
|
|
6211
6523
|
headers: { Authorization: `Bearer ${machineToken}` },
|
|
6212
|
-
|
|
6524
|
+
// steps=1: this daemon runs request conversations one reply at a time,
|
|
6525
|
+
// so it never wants back a job that is waiting on the person.
|
|
6526
|
+
params: { waitMs, steps: "1", ...runningIds.length > 0 && { running: runningIds.join(",") } }
|
|
6213
6527
|
});
|
|
6214
6528
|
} catch (error) {
|
|
6215
6529
|
throw toMailboxError(error, "next-job request failed");
|
|
@@ -6300,16 +6614,74 @@ var MailboxClient = class {
|
|
|
6300
6614
|
const data = res.data;
|
|
6301
6615
|
return data.data;
|
|
6302
6616
|
}
|
|
6617
|
+
/**
|
|
6618
|
+
* The person's saved API key for one job sent with "Run with: My API key".
|
|
6619
|
+
* Kept in memory and passed to Claude Code's environment only — never
|
|
6620
|
+
* written to disk or logged.
|
|
6621
|
+
*/
|
|
6622
|
+
async getAiKey(machineToken, jobId) {
|
|
6623
|
+
let res;
|
|
6624
|
+
try {
|
|
6625
|
+
res = await this.http.post(
|
|
6626
|
+
`/api/v1/jobs/${jobId}/ai-key`,
|
|
6627
|
+
{},
|
|
6628
|
+
{ headers: { Authorization: `Bearer ${machineToken}` } }
|
|
6629
|
+
);
|
|
6630
|
+
} catch (error) {
|
|
6631
|
+
throw toMailboxError(error, "ai-key request failed");
|
|
6632
|
+
}
|
|
6633
|
+
if (res.status < 200 || res.status >= 300) {
|
|
6634
|
+
throw errorFromResponse(res, "ai-key failed");
|
|
6635
|
+
}
|
|
6636
|
+
const data = res.data;
|
|
6637
|
+
return data.data;
|
|
6638
|
+
}
|
|
6639
|
+
/**
|
|
6640
|
+
* Tells CompBox that a request conversation saved its epic request. The
|
|
6641
|
+
* conversation stays open; this only links the job to the request.
|
|
6642
|
+
*/
|
|
6643
|
+
async markRequestSaved(machineToken, jobId, epicRequestId) {
|
|
6644
|
+
let res;
|
|
6645
|
+
try {
|
|
6646
|
+
res = await this.http.post(`/api/v1/jobs/${jobId}/saved`, { epicRequestId }, {
|
|
6647
|
+
headers: { Authorization: `Bearer ${machineToken}` }
|
|
6648
|
+
});
|
|
6649
|
+
} catch (error) {
|
|
6650
|
+
throw toMailboxError(error, "saved request failed");
|
|
6651
|
+
}
|
|
6652
|
+
if (res.status < 200 || res.status >= 300) {
|
|
6653
|
+
throw errorFromResponse(res, "saved failed");
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
/** Reports what a run on the person's API key spent. */
|
|
6657
|
+
async reportAiUsage(machineToken, jobId, usage) {
|
|
6658
|
+
let res;
|
|
6659
|
+
try {
|
|
6660
|
+
res = await this.http.post(`/api/v1/jobs/${jobId}/ai-usage`, usage, {
|
|
6661
|
+
headers: { Authorization: `Bearer ${machineToken}` }
|
|
6662
|
+
});
|
|
6663
|
+
} catch (error) {
|
|
6664
|
+
throw toMailboxError(error, "ai-usage request failed");
|
|
6665
|
+
}
|
|
6666
|
+
if (res.status < 200 || res.status >= 300) {
|
|
6667
|
+
throw errorFromResponse(res, "ai-usage failed");
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6303
6670
|
};
|
|
6304
6671
|
|
|
6305
6672
|
// src/utils/mcp-config-writer.ts
|
|
6306
6673
|
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";
|
|
6674
|
+
import { basename, dirname as dirname2, join as join6 } from "path";
|
|
6308
6675
|
var DEFAULT_MCP_CONFIG_FILENAME = ".mcp.json";
|
|
6309
6676
|
var DEFAULT_SERVER_KEY = "compbox";
|
|
6310
6677
|
var MCP_SERVER_PACKAGE = "@mycompbox/compbox-mcp";
|
|
6311
6678
|
function resolveInstalledMcpBundle(opts) {
|
|
6312
6679
|
const exists = opts?.exists ?? existsSync6;
|
|
6680
|
+
const self = opts && "self" in opts ? opts.self : process.argv[1];
|
|
6681
|
+
if (self && basename(self) === "compbox.cjs") {
|
|
6682
|
+
const sibling = join6(dirname2(self), "compbox-mcp.cjs");
|
|
6683
|
+
if (exists(sibling)) return sibling;
|
|
6684
|
+
}
|
|
6313
6685
|
const prefixes = [];
|
|
6314
6686
|
if (opts?.prefix) prefixes.push(opts.prefix);
|
|
6315
6687
|
if (process.env.HOMEBREW_PREFIX) prefixes.push(process.env.HOMEBREW_PREFIX);
|
|
@@ -6454,6 +6826,100 @@ function findBrew(deps = {}) {
|
|
|
6454
6826
|
return pathProbe() ? "brew" : null;
|
|
6455
6827
|
}
|
|
6456
6828
|
|
|
6829
|
+
// src/utils/autostart.ts
|
|
6830
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
6831
|
+
import { mkdirSync as mkdirSync4, realpathSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6832
|
+
import { homedir as homedir3 } from "node:os";
|
|
6833
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
6834
|
+
var LAUNCHD_LABEL = "com.mycompbox.agent-runner";
|
|
6835
|
+
var SYSTEMD_UNIT = "compbox-agent-runner.service";
|
|
6836
|
+
function scriptInstallPath(home = homedir3()) {
|
|
6837
|
+
return join7(home, ".compbox", "bin", "compbox.cjs");
|
|
6838
|
+
}
|
|
6839
|
+
function isScriptInstall(entry = process.argv[1], home = homedir3()) {
|
|
6840
|
+
if (!entry) return false;
|
|
6841
|
+
try {
|
|
6842
|
+
return realpathSync(entry) === realpathSync(scriptInstallPath(home));
|
|
6843
|
+
} catch {
|
|
6844
|
+
return false;
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
function xmlEscape(value) {
|
|
6848
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6849
|
+
}
|
|
6850
|
+
function launchdPlist(opts) {
|
|
6851
|
+
const args = [opts.node, opts.script, "run"].map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
6852
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
6853
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
6854
|
+
<plist version="1.0">
|
|
6855
|
+
<dict>
|
|
6856
|
+
<key>Label</key>
|
|
6857
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
6858
|
+
<key>ProgramArguments</key>
|
|
6859
|
+
<array>
|
|
6860
|
+
${args}
|
|
6861
|
+
</array>
|
|
6862
|
+
<key>EnvironmentVariables</key>
|
|
6863
|
+
<dict>
|
|
6864
|
+
<key>PATH</key>
|
|
6865
|
+
<string>${xmlEscape(opts.path)}</string>
|
|
6866
|
+
</dict>
|
|
6867
|
+
<key>RunAtLoad</key>
|
|
6868
|
+
<true/>
|
|
6869
|
+
<key>KeepAlive</key>
|
|
6870
|
+
<true/>
|
|
6871
|
+
<key>StandardOutPath</key>
|
|
6872
|
+
<string>${xmlEscape(opts.logFile)}</string>
|
|
6873
|
+
<key>StandardErrorPath</key>
|
|
6874
|
+
<string>${xmlEscape(opts.logFile)}</string>
|
|
6875
|
+
</dict>
|
|
6876
|
+
</plist>
|
|
6877
|
+
`;
|
|
6878
|
+
}
|
|
6879
|
+
function systemdUnit(opts) {
|
|
6880
|
+
const quote = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
6881
|
+
return `[Unit]
|
|
6882
|
+
Description=CompBox Agent Runner
|
|
6883
|
+
|
|
6884
|
+
[Service]
|
|
6885
|
+
ExecStart=${quote(opts.node)} ${quote(opts.script)} run
|
|
6886
|
+
Environment=${quote(`PATH=${opts.path}`)}
|
|
6887
|
+
Restart=always
|
|
6888
|
+
RestartSec=10
|
|
6889
|
+
|
|
6890
|
+
[Install]
|
|
6891
|
+
WantedBy=default.target
|
|
6892
|
+
`;
|
|
6893
|
+
}
|
|
6894
|
+
function installAutostart() {
|
|
6895
|
+
const home = homedir3();
|
|
6896
|
+
const script = scriptInstallPath(home);
|
|
6897
|
+
const node = process.execPath;
|
|
6898
|
+
const path3 = process.env.PATH ?? "/usr/bin:/bin";
|
|
6899
|
+
if (process.platform === "darwin") {
|
|
6900
|
+
const plistPath = join7(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
6901
|
+
const logFile = join7(home, ".compbox", "agent-runner.log");
|
|
6902
|
+
mkdirSync4(dirname3(plistPath), { recursive: true });
|
|
6903
|
+
writeFileSync4(plistPath, launchdPlist({ node, script, path: path3, logFile }));
|
|
6904
|
+
const domain = `gui/${String(process.getuid?.() ?? 0)}`;
|
|
6905
|
+
try {
|
|
6906
|
+
execFileSync3("launchctl", ["bootout", domain, plistPath], { stdio: "ignore" });
|
|
6907
|
+
} catch {
|
|
6908
|
+
}
|
|
6909
|
+
execFileSync3("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
|
|
6910
|
+
return `it starts at every login (stop it with \`launchctl bootout ${domain} ${plistPath}\`; log: ${logFile})`;
|
|
6911
|
+
}
|
|
6912
|
+
if (process.platform === "linux") {
|
|
6913
|
+
const unitPath = join7(home, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
6914
|
+
mkdirSync4(dirname3(unitPath), { recursive: true });
|
|
6915
|
+
writeFileSync4(unitPath, systemdUnit({ node, script, path: path3 }));
|
|
6916
|
+
execFileSync3("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
6917
|
+
execFileSync3("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT], { stdio: "pipe" });
|
|
6918
|
+
return `it starts at every login (stop it with \`systemctl --user disable --now ${SYSTEMD_UNIT}\`)`;
|
|
6919
|
+
}
|
|
6920
|
+
throw new Error(`Automatic start isn't supported on ${process.platform}.`);
|
|
6921
|
+
}
|
|
6922
|
+
|
|
6457
6923
|
// src/commands/configure.ts
|
|
6458
6924
|
function parseAiMode(value) {
|
|
6459
6925
|
if (value === "claude-subscription" || value === "api-key") {
|
|
@@ -6550,13 +7016,13 @@ Device pairing failed: ${message}`));
|
|
|
6550
7016
|
Device already paired as "${config.machineName ?? config.machineId}".`));
|
|
6551
7017
|
}
|
|
6552
7018
|
const defaultDir = isGitRepoDir(process.cwd()) ? process.cwd() : void 0;
|
|
6553
|
-
console.log("\n" + chalk7.bold("Project
|
|
7019
|
+
console.log("\n" + chalk7.bold("Project folder:"));
|
|
6554
7020
|
console.log(
|
|
6555
7021
|
chalk7.gray(
|
|
6556
|
-
"
|
|
7022
|
+
" 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
7023
|
)
|
|
6558
7024
|
);
|
|
6559
|
-
const projectDirQuestion = "Which
|
|
7025
|
+
const projectDirQuestion = "Which project folder should the AI work in?";
|
|
6560
7026
|
const MAX_PROJECT_DIR_ATTEMPTS = 5;
|
|
6561
7027
|
let projectDirPersisted = false;
|
|
6562
7028
|
let projectDirSkipped = false;
|
|
@@ -6673,8 +7139,8 @@ Device already paired as "${config.machineName ?? config.machineId}".`));
|
|
|
6673
7139
|
" Agent traffic would pass through it; if it is not running, calls fall back to a direct connection automatically."
|
|
6674
7140
|
)
|
|
6675
7141
|
);
|
|
6676
|
-
updateConfig({ pxpipeEnabled: enablePxpipe
|
|
6677
|
-
if (enablePxpipe
|
|
7142
|
+
updateConfig({ pxpipeEnabled: enablePxpipe });
|
|
7143
|
+
if (enablePxpipe) {
|
|
6678
7144
|
console.log(chalk7.green("\u2714 pxpipe routing enabled for this machine."));
|
|
6679
7145
|
console.log(
|
|
6680
7146
|
chalk7.yellow(" Note: which models are actually compressed is pxpipe's own setting, not CompBox's.")
|
|
@@ -6693,9 +7159,26 @@ Device already paired as "${config.machineName ?? config.machineId}".`));
|
|
|
6693
7159
|
chalk7.gray(" Re-run `compbox configure` to change this later.")
|
|
6694
7160
|
);
|
|
6695
7161
|
}
|
|
6696
|
-
const
|
|
7162
|
+
const scriptInstall = process.platform !== "win32" && isScriptInstall();
|
|
7163
|
+
const startServiceCmd = process.platform === "win32" ? "compbox service install" : process.platform === "darwin" && !scriptInstall ? "brew services start compbox" : "compbox run";
|
|
6697
7164
|
let daemonStarted = false;
|
|
6698
|
-
if (
|
|
7165
|
+
if (scriptInstall) {
|
|
7166
|
+
const startNow = await promptConfirm(
|
|
7167
|
+
"Start the Agent Runner now and at every login? (lets CompBox send work to this computer)",
|
|
7168
|
+
true
|
|
7169
|
+
);
|
|
7170
|
+
if (startNow) {
|
|
7171
|
+
try {
|
|
7172
|
+
const how = installAutostart();
|
|
7173
|
+
daemonStarted = true;
|
|
7174
|
+
console.log(chalk7.green(`\u2714 Agent Runner started \u2014 ${how}.`));
|
|
7175
|
+
} catch (error) {
|
|
7176
|
+
const message = (error instanceof Error ? error.message.split("\n")[0] : void 0) ?? "unknown error";
|
|
7177
|
+
console.warn(chalk7.yellow(`\u26A0 Could not start the Agent Runner automatically: ${message}`));
|
|
7178
|
+
console.warn(chalk7.gray(" Run `compbox run` in a terminal and keep it open instead."));
|
|
7179
|
+
}
|
|
7180
|
+
}
|
|
7181
|
+
} else if (process.platform === "darwin") {
|
|
6699
7182
|
const brew = findBrew();
|
|
6700
7183
|
if (brew !== null) {
|
|
6701
7184
|
const startNow = await promptConfirm(
|
|
@@ -6704,7 +7187,7 @@ Device already paired as "${config.machineName ?? config.machineId}".`));
|
|
|
6704
7187
|
);
|
|
6705
7188
|
if (startNow) {
|
|
6706
7189
|
try {
|
|
6707
|
-
|
|
7190
|
+
execFileSync4(brew, ["services", "start", "compbox"], {
|
|
6708
7191
|
encoding: "utf-8",
|
|
6709
7192
|
timeout: 3e4,
|
|
6710
7193
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -6716,7 +7199,7 @@ Device already paired as "${config.machineName ?? config.machineId}".`));
|
|
|
6716
7199
|
)
|
|
6717
7200
|
);
|
|
6718
7201
|
} catch (error) {
|
|
6719
|
-
const message = error instanceof Error ? error.message.split("\n")[0] : "unknown error";
|
|
7202
|
+
const message = (error instanceof Error ? error.message.split("\n")[0] : void 0) ?? "unknown error";
|
|
6720
7203
|
console.warn(chalk7.yellow(`\u26A0 Could not start the service: ${message}`));
|
|
6721
7204
|
console.warn(chalk7.gray(` Start it manually with \`${startServiceCmd}\`.`));
|
|
6722
7205
|
}
|
|
@@ -6743,22 +7226,22 @@ Configuration failed: ${error.message}`));
|
|
|
6743
7226
|
});
|
|
6744
7227
|
|
|
6745
7228
|
// src/commands/doctor.ts
|
|
6746
|
-
import { execFileSync as
|
|
7229
|
+
import { execFileSync as execFileSync6, spawn as spawn2 } from "child_process";
|
|
6747
7230
|
import { existsSync as existsSync8 } from "fs";
|
|
6748
7231
|
import { Command as Command7 } from "commander";
|
|
6749
7232
|
import chalk8 from "chalk";
|
|
6750
7233
|
|
|
6751
7234
|
// src/utils/claude-runtime.ts
|
|
6752
7235
|
import { spawn } from "node:child_process";
|
|
6753
|
-
import { execFileSync as
|
|
7236
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
6754
7237
|
import { accessSync, constants as fsConstants, statSync as statSync2 } from "node:fs";
|
|
6755
|
-
import { homedir as
|
|
6756
|
-
import { delimiter, join as
|
|
7238
|
+
import { homedir as homedir4 } from "node:os";
|
|
7239
|
+
import { delimiter, join as join8 } from "node:path";
|
|
6757
7240
|
var WELL_KNOWN_DIRS = buildWellKnownDirs();
|
|
6758
7241
|
function buildWellKnownDirs() {
|
|
6759
|
-
const home =
|
|
7242
|
+
const home = homedir4();
|
|
6760
7243
|
const dirs = [
|
|
6761
|
-
|
|
7244
|
+
join8(home, ".local", "bin"),
|
|
6762
7245
|
// default Claude Code install location
|
|
6763
7246
|
"/opt/homebrew/bin",
|
|
6764
7247
|
// Homebrew on Apple Silicon
|
|
@@ -6766,14 +7249,14 @@ function buildWellKnownDirs() {
|
|
|
6766
7249
|
// Homebrew on Intel / common Unix prefix
|
|
6767
7250
|
];
|
|
6768
7251
|
if (process.env.npm_config_prefix) {
|
|
6769
|
-
dirs.push(
|
|
7252
|
+
dirs.push(join8(process.env.npm_config_prefix, "bin"));
|
|
6770
7253
|
}
|
|
6771
|
-
dirs.push(
|
|
7254
|
+
dirs.push(join8(home, ".npm-global", "bin"));
|
|
6772
7255
|
if (process.env.PNPM_HOME) {
|
|
6773
7256
|
dirs.push(process.env.PNPM_HOME);
|
|
6774
7257
|
}
|
|
6775
|
-
dirs.push(
|
|
6776
|
-
dirs.push(
|
|
7258
|
+
dirs.push(join8(home, "Library", "pnpm"));
|
|
7259
|
+
dirs.push(join8(home, ".local", "share", "pnpm"));
|
|
6777
7260
|
return dirs;
|
|
6778
7261
|
}
|
|
6779
7262
|
var LOGIN_SHELL_PATH_TIMEOUT_MS = 2e3;
|
|
@@ -6801,7 +7284,7 @@ function getLoginShellPath(exec = defaultExecFileSync) {
|
|
|
6801
7284
|
return cachedLoginShellPath;
|
|
6802
7285
|
}
|
|
6803
7286
|
function defaultExecFileSync(file, args, options) {
|
|
6804
|
-
return
|
|
7287
|
+
return execFileSync5(file, args, {
|
|
6805
7288
|
timeout: options.timeout,
|
|
6806
7289
|
encoding: options.encoding,
|
|
6807
7290
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -6839,7 +7322,7 @@ function resolveClaudeBinary(deps = {}) {
|
|
|
6839
7322
|
continue;
|
|
6840
7323
|
}
|
|
6841
7324
|
seen.add(dir);
|
|
6842
|
-
const candidate =
|
|
7325
|
+
const candidate = join8(dir, "claude");
|
|
6843
7326
|
if (isExec(candidate)) {
|
|
6844
7327
|
return candidate;
|
|
6845
7328
|
}
|
|
@@ -6878,7 +7361,7 @@ function probeAgentRuntime(deps = {}) {
|
|
|
6878
7361
|
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6879
7362
|
});
|
|
6880
7363
|
}
|
|
6881
|
-
return new Promise((
|
|
7364
|
+
return new Promise((resolve3) => {
|
|
6882
7365
|
let settled = false;
|
|
6883
7366
|
let child;
|
|
6884
7367
|
let stdout2 = "";
|
|
@@ -6893,7 +7376,7 @@ function probeAgentRuntime(deps = {}) {
|
|
|
6893
7376
|
child.kill("SIGKILL");
|
|
6894
7377
|
} catch {
|
|
6895
7378
|
}
|
|
6896
|
-
|
|
7379
|
+
resolve3({ ...result, checkedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6897
7380
|
};
|
|
6898
7381
|
const timer = setTimeout(() => {
|
|
6899
7382
|
settle({
|
|
@@ -6905,7 +7388,7 @@ function probeAgentRuntime(deps = {}) {
|
|
|
6905
7388
|
child = spawnFn(abs, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
6906
7389
|
} catch (error) {
|
|
6907
7390
|
clearTimeout(timer);
|
|
6908
|
-
|
|
7391
|
+
resolve3({
|
|
6909
7392
|
status: "error",
|
|
6910
7393
|
detail: error instanceof Error ? error.message : "failed to spawn claude",
|
|
6911
7394
|
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -6997,7 +7480,7 @@ function countFailures(results) {
|
|
|
6997
7480
|
}
|
|
6998
7481
|
var LAUNCH_PROBE_TIMEOUT_MS = 5e3;
|
|
6999
7482
|
function probeLaunchTarget(command, args, timeoutMs = LAUNCH_PROBE_TIMEOUT_MS) {
|
|
7000
|
-
return new Promise((
|
|
7483
|
+
return new Promise((resolve3) => {
|
|
7001
7484
|
let settled = false;
|
|
7002
7485
|
const child = spawn2(command, args, {
|
|
7003
7486
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -7019,7 +7502,7 @@ function probeLaunchTarget(command, args, timeoutMs = LAUNCH_PROBE_TIMEOUT_MS) {
|
|
|
7019
7502
|
child.kill("SIGKILL");
|
|
7020
7503
|
} catch {
|
|
7021
7504
|
}
|
|
7022
|
-
|
|
7505
|
+
resolve3(result);
|
|
7023
7506
|
};
|
|
7024
7507
|
const timer = setTimeout(() => {
|
|
7025
7508
|
settle({ status: "timeout" });
|
|
@@ -7108,7 +7591,7 @@ var BREW_SERVICE_LABEL = "homebrew.mxcl.compbox";
|
|
|
7108
7591
|
function checkBrewService(deps = {}) {
|
|
7109
7592
|
const name = "Homebrew service running (macOS)";
|
|
7110
7593
|
const locateBrew = deps.findBrewFn ?? findBrew;
|
|
7111
|
-
const exec = deps.execFn ?? ((file, args) =>
|
|
7594
|
+
const exec = deps.execFn ?? ((file, args) => execFileSync6(file, args, {
|
|
7112
7595
|
encoding: "utf-8",
|
|
7113
7596
|
timeout: 1e4,
|
|
7114
7597
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -7126,7 +7609,7 @@ function checkBrewService(deps = {}) {
|
|
|
7126
7609
|
try {
|
|
7127
7610
|
output = exec(brew, ["services", "list"]);
|
|
7128
7611
|
} catch (error) {
|
|
7129
|
-
const cause = error instanceof Error ? error.message.split("\n")[0] : "unknown error";
|
|
7612
|
+
const cause = (error instanceof Error ? error.message.split("\n")[0] : void 0) ?? "unknown error";
|
|
7130
7613
|
try {
|
|
7131
7614
|
exec("launchctl", ["list", BREW_SERVICE_LABEL]);
|
|
7132
7615
|
return {
|
|
@@ -7416,16 +7899,67 @@ import chalk9 from "chalk";
|
|
|
7416
7899
|
// src/utils/agent-invoker.ts
|
|
7417
7900
|
import Anthropic from "@anthropic-ai/sdk";
|
|
7418
7901
|
import { spawn as spawn3, spawnSync } from "node:child_process";
|
|
7419
|
-
import { mkdirSync as
|
|
7420
|
-
import { homedir as
|
|
7421
|
-
import { join as
|
|
7902
|
+
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
7903
|
+
import { homedir as homedir5 } from "node:os";
|
|
7904
|
+
import { join as join9 } from "node:path";
|
|
7905
|
+
|
|
7906
|
+
// src/utils/key-run.ts
|
|
7907
|
+
function keyRunEnv(env, keyRun) {
|
|
7908
|
+
const next = { ...env, ANTHROPIC_API_KEY: keyRun.apiKey, ANTHROPIC_MODEL: keyRun.model };
|
|
7909
|
+
delete next.ANTHROPIC_AUTH_TOKEN;
|
|
7910
|
+
delete next.CLAUDE_CODE_OAUTH_TOKEN;
|
|
7911
|
+
return next;
|
|
7912
|
+
}
|
|
7913
|
+
function keyRunArgs(args) {
|
|
7914
|
+
return [...args, "--output-format", "json"];
|
|
7915
|
+
}
|
|
7916
|
+
function num(value) {
|
|
7917
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
7918
|
+
}
|
|
7919
|
+
function parseClaudeJsonResult(stdout2, fallbackModel) {
|
|
7920
|
+
let data;
|
|
7921
|
+
try {
|
|
7922
|
+
const start = stdout2.indexOf("{");
|
|
7923
|
+
if (start === -1) return null;
|
|
7924
|
+
data = JSON.parse(stdout2.slice(start));
|
|
7925
|
+
} catch {
|
|
7926
|
+
return null;
|
|
7927
|
+
}
|
|
7928
|
+
if (typeof data.result !== "string" && data.type !== "result") return null;
|
|
7929
|
+
const usage = {
|
|
7930
|
+
model: fallbackModel,
|
|
7931
|
+
inputTokens: 0,
|
|
7932
|
+
outputTokens: 0,
|
|
7933
|
+
cacheReadTokens: 0,
|
|
7934
|
+
cacheWriteTokens: 0,
|
|
7935
|
+
costUsd: typeof data.total_cost_usd === "number" ? data.total_cost_usd : null
|
|
7936
|
+
};
|
|
7937
|
+
const byModel = data.modelUsage;
|
|
7938
|
+
if (typeof byModel === "object" && byModel !== null && Object.keys(byModel).length > 0) {
|
|
7939
|
+
let topCost = -1;
|
|
7940
|
+
for (const [model, raw] of Object.entries(byModel)) {
|
|
7941
|
+
usage.inputTokens += num(raw.inputTokens);
|
|
7942
|
+
usage.outputTokens += num(raw.outputTokens);
|
|
7943
|
+
usage.cacheReadTokens += num(raw.cacheReadInputTokens);
|
|
7944
|
+
usage.cacheWriteTokens += num(raw.cacheCreationInputTokens);
|
|
7945
|
+
if (num(raw.costUSD) > topCost) {
|
|
7946
|
+
topCost = num(raw.costUSD);
|
|
7947
|
+
usage.model = model;
|
|
7948
|
+
}
|
|
7949
|
+
}
|
|
7950
|
+
} else if (typeof data.usage === "object" && data.usage !== null) {
|
|
7951
|
+
const u = data.usage;
|
|
7952
|
+
usage.inputTokens = num(u.input_tokens);
|
|
7953
|
+
usage.outputTokens = num(u.output_tokens);
|
|
7954
|
+
usage.cacheReadTokens = num(u.cache_read_input_tokens);
|
|
7955
|
+
usage.cacheWriteTokens = num(u.cache_creation_input_tokens);
|
|
7956
|
+
}
|
|
7957
|
+
return { text: typeof data.result === "string" ? data.result : "", usage };
|
|
7958
|
+
}
|
|
7959
|
+
|
|
7960
|
+
// src/utils/agent-invoker.ts
|
|
7422
7961
|
var ANTHROPIC_MODEL = "claude-fable-5";
|
|
7423
|
-
var
|
|
7424
|
-
var MAX_QUALITY_ROUNDS = 4;
|
|
7425
|
-
var TARGET_COMPLIANCE_TIER = "pass";
|
|
7426
|
-
var REPLY_POLL_INTERVAL_MS = 3e3;
|
|
7427
|
-
var REPLY_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
7428
|
-
var CLAUDE_CLI_TURN_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
7962
|
+
var CLAUDE_CLI_TURN_TIMEOUT_MS = 4 * 60 * 1e3;
|
|
7429
7963
|
function anthropicClientOptions(apiKey, routing) {
|
|
7430
7964
|
return routing.mode === "proxy" ? { apiKey, baseURL: routing.anthropicBaseUrl } : { apiKey };
|
|
7431
7965
|
}
|
|
@@ -7438,11 +7972,7 @@ var SUPPORTED_IMAGE_MEDIA_TYPES = [
|
|
|
7438
7972
|
"image/gif",
|
|
7439
7973
|
"image/webp"
|
|
7440
7974
|
];
|
|
7441
|
-
var MAX_IMAGES_PER_TURN = 8;
|
|
7442
7975
|
var MAX_IMAGE_BYTES_PER_TURN = 20 * 1024 * 1024;
|
|
7443
|
-
var defaultWarn = (message) => {
|
|
7444
|
-
console.warn(` \u26A0 ${message}`);
|
|
7445
|
-
};
|
|
7446
7976
|
function normalizeImageMediaType(mime) {
|
|
7447
7977
|
const base = (mime.split(";")[0] ?? "").trim().toLowerCase();
|
|
7448
7978
|
return SUPPORTED_IMAGE_MEDIA_TYPES.includes(base) ? base : null;
|
|
@@ -7450,67 +7980,10 @@ function normalizeImageMediaType(mime) {
|
|
|
7450
7980
|
function isSupportedImageMediaType(mime) {
|
|
7451
7981
|
return normalizeImageMediaType(mime) !== null;
|
|
7452
7982
|
}
|
|
7453
|
-
function selectImageAttachments(attachments) {
|
|
7454
|
-
if (!attachments || attachments.length === 0) return [];
|
|
7455
|
-
const selected = [];
|
|
7456
|
-
let declaredBytes = 0;
|
|
7457
|
-
for (const att of attachments) {
|
|
7458
|
-
if (selected.length >= MAX_IMAGES_PER_TURN) break;
|
|
7459
|
-
if (!isSupportedImageMediaType(att.mimeType)) continue;
|
|
7460
|
-
const size = typeof att.sizeBytes === "number" && att.sizeBytes > 0 ? att.sizeBytes : 0;
|
|
7461
|
-
if (declaredBytes + size > MAX_IMAGE_BYTES_PER_TURN) continue;
|
|
7462
|
-
declaredBytes += size;
|
|
7463
|
-
selected.push(att);
|
|
7464
|
-
}
|
|
7465
|
-
return selected;
|
|
7466
|
-
}
|
|
7467
7983
|
function imageAttachmentCount(attachments) {
|
|
7468
7984
|
if (!attachments || attachments.length === 0) return 0;
|
|
7469
7985
|
return attachments.filter((a) => isSupportedImageMediaType(a.mimeType)).length;
|
|
7470
7986
|
}
|
|
7471
|
-
async function fetchImageBlocks(attachments, warn = defaultWarn, fetchImpl = fetch) {
|
|
7472
|
-
const blocks = [];
|
|
7473
|
-
let totalBytes = 0;
|
|
7474
|
-
for (const att of attachments) {
|
|
7475
|
-
const mediaType = normalizeImageMediaType(att.mimeType);
|
|
7476
|
-
if (!mediaType) continue;
|
|
7477
|
-
try {
|
|
7478
|
-
const res = await fetchImpl(att.signedUrl);
|
|
7479
|
-
if (!res.ok) {
|
|
7480
|
-
warn(`Skipping image attachment ${att.id}: fetch failed (HTTP ${String(res.status)}).`);
|
|
7481
|
-
continue;
|
|
7482
|
-
}
|
|
7483
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
7484
|
-
if (bytes.byteLength === 0) {
|
|
7485
|
-
warn(`Skipping image attachment ${att.id}: empty response body.`);
|
|
7486
|
-
continue;
|
|
7487
|
-
}
|
|
7488
|
-
if (totalBytes + bytes.byteLength > MAX_IMAGE_BYTES_PER_TURN) {
|
|
7489
|
-
warn(`Skipping image attachment ${att.id}: exceeds the per-turn image byte budget.`);
|
|
7490
|
-
continue;
|
|
7491
|
-
}
|
|
7492
|
-
totalBytes += bytes.byteLength;
|
|
7493
|
-
blocks.push({
|
|
7494
|
-
type: "image",
|
|
7495
|
-
source: { type: "base64", media_type: mediaType, data: bytes.toString("base64") }
|
|
7496
|
-
});
|
|
7497
|
-
} catch (error) {
|
|
7498
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
7499
|
-
warn(`Skipping image attachment ${att.id}: ${message}.`);
|
|
7500
|
-
}
|
|
7501
|
-
}
|
|
7502
|
-
return blocks;
|
|
7503
|
-
}
|
|
7504
|
-
function buildApiUserContent(text, imageBlocks) {
|
|
7505
|
-
if (imageBlocks.length === 0) return text;
|
|
7506
|
-
return [...imageBlocks, { type: "text", text }];
|
|
7507
|
-
}
|
|
7508
|
-
async function apiUserTurnContent(turn, warn = defaultWarn, fetchImpl = fetch) {
|
|
7509
|
-
const selected = selectImageAttachments(turn.attachments);
|
|
7510
|
-
if (selected.length === 0) return turn.content;
|
|
7511
|
-
const blocks = await fetchImageBlocks(selected, warn, fetchImpl);
|
|
7512
|
-
return buildApiUserContent(turn.content, blocks);
|
|
7513
|
-
}
|
|
7514
7987
|
function appendAttachmentNote(text, imageCount) {
|
|
7515
7988
|
if (imageCount <= 0) return text;
|
|
7516
7989
|
const noun = imageCount === 1 ? "image" : "images";
|
|
@@ -7519,7 +7992,6 @@ function appendAttachmentNote(text, imageCount) {
|
|
|
7519
7992
|
|
|
7520
7993
|
${note}` : note;
|
|
7521
7994
|
}
|
|
7522
|
-
var SYSTEM_PROMPT = buildInterviewSystemPrompt();
|
|
7523
7995
|
function parseSeedPayload(payload) {
|
|
7524
7996
|
if (typeof payload !== "object" || payload === null) return null;
|
|
7525
7997
|
const p = payload;
|
|
@@ -7535,11 +8007,6 @@ function parseSeedPayload(payload) {
|
|
|
7535
8007
|
priorTranscript
|
|
7536
8008
|
};
|
|
7537
8009
|
}
|
|
7538
|
-
function resolveSystemPrompt(seed) {
|
|
7539
|
-
return seed ? buildInterviewSystemPrompt(INTERVIEW_DEFINITION, {
|
|
7540
|
-
priorTranscript: seed.priorTranscript
|
|
7541
|
-
}) : SYSTEM_PROMPT;
|
|
7542
|
-
}
|
|
7543
8010
|
async function submitAndScore(ctx, currentErId, draft) {
|
|
7544
8011
|
if (currentErId) {
|
|
7545
8012
|
const compliance = await updatePersonalEpicRequest(
|
|
@@ -7553,248 +8020,21 @@ async function submitAndScore(ctx, currentErId, draft) {
|
|
|
7553
8020
|
const created = await createPersonalEpicRequest(ctx.apiBaseUrl, ctx.userApiToken, draft);
|
|
7554
8021
|
return { erId: created.id, compliance: created.compliance };
|
|
7555
8022
|
}
|
|
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
|
-
var MAX_JSON_REPAIRS = 3;
|
|
7672
|
-
function sleep(ms) {
|
|
7673
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7674
|
-
}
|
|
7675
|
-
async function waitForUserReply(client, machineToken, jobId, turnsSoFar) {
|
|
7676
|
-
const deadline = Date.now() + REPLY_TIMEOUT_MS;
|
|
7677
|
-
while (Date.now() < deadline) {
|
|
7678
|
-
await sleep(REPLY_POLL_INTERVAL_MS);
|
|
7679
|
-
const job = await client.nextJob(machineToken, 0);
|
|
7680
|
-
if (job?.id !== jobId) continue;
|
|
7681
|
-
const last = job.turns[job.turns.length - 1];
|
|
7682
|
-
if (job.turns.length > turnsSoFar && last?.role === "user") {
|
|
7683
|
-
return job;
|
|
7684
|
-
}
|
|
7685
|
-
}
|
|
7686
|
-
return null;
|
|
7687
|
-
}
|
|
7688
|
-
async function invokeEpicRequestViaApi(job, ctx) {
|
|
7689
|
-
const routing = await resolveRouting();
|
|
7690
|
-
const anthropic = new Anthropic(anthropicClientOptions(ctx.anthropicApiKey, routing));
|
|
7691
|
-
const seed = parseSeedPayload(job.payload);
|
|
7692
|
-
const systemPrompt = resolveSystemPrompt(seed);
|
|
7693
|
-
let currentErId = seed?.targetEpicRequestId ?? null;
|
|
7694
|
-
let qualityRounds = 0;
|
|
7695
|
-
let jsonRepairs = 0;
|
|
7696
|
-
const messages = [];
|
|
7697
|
-
for (const turn of job.turns) {
|
|
7698
|
-
if (turn.role === "agent") {
|
|
7699
|
-
messages.push({ role: "assistant", content: turn.content });
|
|
7700
|
-
} else {
|
|
7701
|
-
messages.push({ role: "user", content: await apiUserTurnContent(turn) });
|
|
7702
|
-
}
|
|
7703
|
-
}
|
|
7704
|
-
let turnsSoFar = job.turns.length;
|
|
7705
|
-
for (let i = 0; i < MAX_INTERVIEW_TURNS; i++) {
|
|
7706
|
-
let reply;
|
|
7707
|
-
try {
|
|
7708
|
-
const response = await anthropic.messages.create({
|
|
7709
|
-
model: ANTHROPIC_MODEL,
|
|
7710
|
-
max_tokens: 1024,
|
|
7711
|
-
system: systemPrompt,
|
|
7712
|
-
messages: messages.length > 0 ? messages : [{ role: "user", content: "Let's get started." }]
|
|
7713
|
-
});
|
|
7714
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
7715
|
-
reply = textBlock?.type === "text" ? textBlock.text : "";
|
|
7716
|
-
} catch (error) {
|
|
7717
|
-
const message = error instanceof Error ? error.message : "Anthropic API request failed";
|
|
7718
|
-
return { status: "failed", result: { error: `Agent invocation failed: ${message}` } };
|
|
7719
|
-
}
|
|
7720
|
-
const draft = extractCompletionJson(reply);
|
|
7721
|
-
if (draft) {
|
|
7722
|
-
let submitted;
|
|
7723
|
-
try {
|
|
7724
|
-
submitted = await submitAndScore(ctx, currentErId, draft);
|
|
7725
|
-
} catch (error) {
|
|
7726
|
-
const message = error instanceof Error ? error.message : "Failed to save the epic request";
|
|
7727
|
-
return { status: "failed", result: { error: message } };
|
|
7728
|
-
}
|
|
7729
|
-
currentErId = submitted.erId;
|
|
7730
|
-
const compliance = submitted.compliance;
|
|
7731
|
-
if (meetsComplianceTarget(compliance) || qualityRounds >= MAX_QUALITY_ROUNDS) {
|
|
7732
|
-
return { status: "done", result: { epicRequestId: currentErId } };
|
|
7733
|
-
}
|
|
7734
|
-
qualityRounds++;
|
|
7735
|
-
if (compliance !== null) {
|
|
7736
|
-
messages.push({ role: "assistant", content: reply });
|
|
7737
|
-
messages.push({ role: "user", content: buildAuditFeedback(compliance) });
|
|
7738
|
-
}
|
|
7739
|
-
continue;
|
|
7740
|
-
}
|
|
7741
|
-
if (looksLikeCompletionAttempt(reply)) {
|
|
7742
|
-
if (jsonRepairs >= MAX_JSON_REPAIRS) {
|
|
7743
|
-
return {
|
|
7744
|
-
status: "failed",
|
|
7745
|
-
result: {
|
|
7746
|
-
error: "The interview produced malformed completion data and could not be finalized after several attempts."
|
|
7747
|
-
}
|
|
7748
|
-
};
|
|
7749
|
-
}
|
|
7750
|
-
jsonRepairs++;
|
|
7751
|
-
messages.push({ role: "assistant", content: reply });
|
|
7752
|
-
messages.push({ role: "user", content: COMPLETION_REPAIR_INSTRUCTION });
|
|
7753
|
-
continue;
|
|
7754
|
-
}
|
|
7755
|
-
messages.push({ role: "assistant", content: reply });
|
|
7756
|
-
let updated;
|
|
7757
|
-
try {
|
|
7758
|
-
updated = await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, reply);
|
|
7759
|
-
} catch (error) {
|
|
7760
|
-
const message = error instanceof Error ? error.message : "post-turn failed";
|
|
7761
|
-
return { status: "failed", result: { error: message } };
|
|
7762
|
-
}
|
|
7763
|
-
turnsSoFar = updated.turns.length;
|
|
7764
|
-
const answered = await waitForUserReply(ctx.mailboxClient, ctx.machineToken, job.id, turnsSoFar);
|
|
7765
|
-
if (!answered) {
|
|
7766
|
-
return {
|
|
7767
|
-
status: "failed",
|
|
7768
|
-
result: { error: "Timed out waiting for a reply \u2014 no response within 10 minutes." }
|
|
7769
|
-
};
|
|
7770
|
-
}
|
|
7771
|
-
const lastTurn = answered.turns[answered.turns.length - 1];
|
|
7772
|
-
messages.push({
|
|
7773
|
-
role: "user",
|
|
7774
|
-
content: lastTurn ? await apiUserTurnContent(lastTurn) : ""
|
|
7775
|
-
});
|
|
7776
|
-
turnsSoFar = answered.turns.length;
|
|
7777
|
-
}
|
|
7778
|
-
return currentErId ? { status: "done", result: { epicRequestId: currentErId } } : {
|
|
7779
|
-
status: "failed",
|
|
7780
|
-
result: { error: `Interview did not converge after ${String(MAX_INTERVIEW_TURNS)} turns.` }
|
|
7781
|
-
};
|
|
7782
|
-
}
|
|
7783
8023
|
function claudeCliSessionCwd() {
|
|
7784
|
-
const dir =
|
|
8024
|
+
const dir = join9(homedir5(), ".compbox", "claude-sessions");
|
|
7785
8025
|
try {
|
|
7786
|
-
|
|
8026
|
+
mkdirSync5(dir, { recursive: true });
|
|
7787
8027
|
} catch {
|
|
7788
8028
|
}
|
|
7789
8029
|
return dir;
|
|
7790
8030
|
}
|
|
7791
8031
|
function runClaudeCliTurn(prompt, opts) {
|
|
7792
|
-
return new Promise((
|
|
8032
|
+
return new Promise((resolve3, reject) => {
|
|
8033
|
+
const model = opts.model === void 0 ? ANTHROPIC_MODEL : opts.model;
|
|
7793
8034
|
const args = [
|
|
7794
8035
|
"-p",
|
|
7795
8036
|
prompt,
|
|
7796
|
-
"--model",
|
|
7797
|
-
ANTHROPIC_MODEL,
|
|
8037
|
+
...model ? ["--model", model] : [],
|
|
7798
8038
|
"--output-format",
|
|
7799
8039
|
"json",
|
|
7800
8040
|
"--tools",
|
|
@@ -7864,100 +8104,98 @@ function runClaudeCliTurn(prompt, opts) {
|
|
|
7864
8104
|
reject(new Error("claude CLI returned an unexpected JSON shape (missing result/session_id)."));
|
|
7865
8105
|
return;
|
|
7866
8106
|
}
|
|
7867
|
-
|
|
8107
|
+
resolve3({ text: shaped.result, sessionId: shaped.session_id });
|
|
7868
8108
|
});
|
|
7869
8109
|
});
|
|
7870
8110
|
}
|
|
7871
|
-
async function
|
|
7872
|
-
|
|
7873
|
-
|
|
8111
|
+
async function loadSavedRequest(apiBaseUrl, userApiToken, epicRequestId) {
|
|
8112
|
+
const res = await fetch(
|
|
8113
|
+
`${apiBaseUrl.replace(/\/+$/, "")}/api/v1/epic-requests/${encodeURIComponent(epicRequestId)}`,
|
|
8114
|
+
{ headers: { Authorization: `Bearer ${userApiToken}` } }
|
|
8115
|
+
);
|
|
8116
|
+
if (!res.ok) return null;
|
|
8117
|
+
const body = await res.json().catch(() => null);
|
|
8118
|
+
return savedContextFromRecord(body?.data);
|
|
8119
|
+
}
|
|
8120
|
+
function needsDefaultModel(error) {
|
|
8121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8122
|
+
return /requires usage credits|switch to another model|model.*not available|not have access to/i.test(message);
|
|
8123
|
+
}
|
|
8124
|
+
function savedRequestIdOf(job) {
|
|
8125
|
+
const result = job.result;
|
|
8126
|
+
if (typeof result?.epicRequestId === "string") return result.epicRequestId;
|
|
8127
|
+
return parseSeedPayload(job.payload)?.targetEpicRequestId ?? null;
|
|
8128
|
+
}
|
|
8129
|
+
async function invokeEpicRequestStep(job, ctx) {
|
|
8130
|
+
try {
|
|
8131
|
+
return await takeConversationStep(job, ctx);
|
|
8132
|
+
} catch (error) {
|
|
8133
|
+
const reason = error instanceof Error ? error.message : "something went wrong";
|
|
8134
|
+
await ctx.mailboxClient.postTurn(
|
|
8135
|
+
ctx.machineToken,
|
|
8136
|
+
job.id,
|
|
8137
|
+
`I couldn't continue: ${reason}
|
|
8138
|
+
|
|
8139
|
+
Send another message (for example "try again") and I'll pick up where we left off.`
|
|
8140
|
+
);
|
|
8141
|
+
return { status: "yield" };
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8144
|
+
async function takeConversationStep(job, ctx) {
|
|
7874
8145
|
const routing = await resolveRouting();
|
|
7875
|
-
const
|
|
7876
|
-
const
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
submitted = await submitAndScore(ctx, currentErId, draft);
|
|
7905
|
-
} catch (error) {
|
|
7906
|
-
const message = error instanceof Error ? error.message : "Failed to save the epic request";
|
|
7907
|
-
return { status: "failed", result: { error: message } };
|
|
7908
|
-
}
|
|
7909
|
-
currentErId = submitted.erId;
|
|
7910
|
-
const compliance = submitted.compliance;
|
|
7911
|
-
if (meetsComplianceTarget(compliance) || qualityRounds >= MAX_QUALITY_ROUNDS) {
|
|
7912
|
-
return { status: "done", result: { epicRequestId: currentErId } };
|
|
7913
|
-
}
|
|
7914
|
-
qualityRounds++;
|
|
7915
|
-
if (compliance !== null) {
|
|
7916
|
-
nextPrompt = buildAuditFeedback(compliance);
|
|
7917
|
-
}
|
|
7918
|
-
continue;
|
|
7919
|
-
}
|
|
7920
|
-
if (looksLikeCompletionAttempt(turn.text)) {
|
|
7921
|
-
if (jsonRepairs >= MAX_JSON_REPAIRS) {
|
|
7922
|
-
return {
|
|
7923
|
-
status: "failed",
|
|
7924
|
-
result: {
|
|
7925
|
-
error: "The interview produced malformed completion data and could not be finalized after several attempts."
|
|
7926
|
-
}
|
|
7927
|
-
};
|
|
8146
|
+
const requestId = savedRequestIdOf(job);
|
|
8147
|
+
const saved = requestId ? await loadSavedRequest(ctx.apiBaseUrl, ctx.userApiToken, requestId) : null;
|
|
8148
|
+
const turns = job.turns.map((t) => ({
|
|
8149
|
+
role: t.role,
|
|
8150
|
+
content: t.role === "user" ? appendAttachmentNote(t.content, imageAttachmentCount(t.attachments)) : t.content
|
|
8151
|
+
}));
|
|
8152
|
+
const anthropic = ctx.anthropicApiKey ? new Anthropic(anthropicClientOptions(ctx.anthropicApiKey, routing)) : null;
|
|
8153
|
+
let cliModel = ANTHROPIC_MODEL;
|
|
8154
|
+
const outcome = await runConversationStep(
|
|
8155
|
+
{ turns, saved },
|
|
8156
|
+
{
|
|
8157
|
+
reply: async (system, messages) => {
|
|
8158
|
+
if (anthropic) {
|
|
8159
|
+
const response = await anthropic.messages.create({ model: ANTHROPIC_MODEL, max_tokens: 4096, system, messages });
|
|
8160
|
+
const text = response.content.find((b) => b.type === "text");
|
|
8161
|
+
return text?.type === "text" ? text.text : "";
|
|
8162
|
+
}
|
|
8163
|
+
const prompt = renderConversationPrompt(messages);
|
|
8164
|
+
try {
|
|
8165
|
+
return (await runClaudeCliTurn(prompt, { systemPrompt: system, resumeSessionId: void 0, routing, model: cliModel })).text;
|
|
8166
|
+
} catch (error) {
|
|
8167
|
+
if (cliModel === null || !needsDefaultModel(error)) throw error;
|
|
8168
|
+
cliModel = null;
|
|
8169
|
+
return (await runClaudeCliTurn(prompt, { systemPrompt: system, resumeSessionId: void 0, routing, model: null })).text;
|
|
8170
|
+
}
|
|
8171
|
+
},
|
|
8172
|
+
save: async (existingId, draft) => {
|
|
8173
|
+
const submitted = await submitAndScore(ctx, existingId, draft);
|
|
8174
|
+
return { id: submitted.erId, title: draft.title, compliance: submitted.compliance };
|
|
7928
8175
|
}
|
|
7929
|
-
jsonRepairs++;
|
|
7930
|
-
nextPrompt = COMPLETION_REPAIR_INSTRUCTION;
|
|
7931
|
-
continue;
|
|
7932
|
-
}
|
|
7933
|
-
let updated;
|
|
7934
|
-
try {
|
|
7935
|
-
updated = await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, turn.text);
|
|
7936
|
-
} catch (error) {
|
|
7937
|
-
const message = error instanceof Error ? error.message : "post-turn failed";
|
|
7938
|
-
return { status: "failed", result: { error: message } };
|
|
7939
8176
|
}
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
return {
|
|
7944
|
-
status: "failed",
|
|
7945
|
-
result: { error: "Timed out waiting for a reply \u2014 no response within 10 minutes." }
|
|
7946
|
-
};
|
|
7947
|
-
}
|
|
7948
|
-
const lastTurn = answered.turns[answered.turns.length - 1];
|
|
7949
|
-
nextPrompt = lastTurn ? appendAttachmentNote(lastTurn.content, imageAttachmentCount(lastTurn.attachments)) : "";
|
|
7950
|
-
turnsSoFar = answered.turns.length;
|
|
8177
|
+
);
|
|
8178
|
+
if (outcome.kind === "saved") {
|
|
8179
|
+
await ctx.mailboxClient.markRequestSaved(ctx.machineToken, job.id, outcome.saved.id);
|
|
7951
8180
|
}
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
result: { error: `Interview did not converge after ${String(MAX_INTERVIEW_TURNS)} turns.` }
|
|
7955
|
-
};
|
|
8181
|
+
await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, outcome.text);
|
|
8182
|
+
return { status: "yield" };
|
|
7956
8183
|
}
|
|
7957
8184
|
var PLANNER_RUN_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
7958
8185
|
var PLANNER_HEARTBEAT_INTERVAL_MS = 20 * 1e3;
|
|
7959
8186
|
var PLANNER_FINAL_TURN_MAX_CHARS = 4e3;
|
|
7960
8187
|
var ORCHESTRATOR_REVIEW_MARKER = "COMPBOX_NEEDS_HUMAN_REVIEW";
|
|
8188
|
+
function takeKeyRunResult(ctx, keyRun, stdout2) {
|
|
8189
|
+
const parsed = parseClaudeJsonResult(stdout2, keyRun.model);
|
|
8190
|
+
if (!parsed) return { text: stdout2, reported: Promise.resolve() };
|
|
8191
|
+
const reported = ctx.mailboxClient.reportAiUsage(ctx.machineToken, ctx.jobId, parsed.usage).catch(() => void 0);
|
|
8192
|
+
return { text: parsed.text, reported };
|
|
8193
|
+
}
|
|
8194
|
+
async function settleKeyRun(ctx, keyRun, stdout2) {
|
|
8195
|
+
const { text, reported } = takeKeyRunResult(ctx, keyRun, stdout2);
|
|
8196
|
+
await reported;
|
|
8197
|
+
return text;
|
|
8198
|
+
}
|
|
7961
8199
|
function buildPlannerArgs(epicRequestId) {
|
|
7962
8200
|
return [
|
|
7963
8201
|
"-p",
|
|
@@ -8078,13 +8316,14 @@ async function invokeStartExecutionPlan(ctx, deps = {}) {
|
|
|
8078
8316
|
}
|
|
8079
8317
|
const routing = await resolveRoutingImpl();
|
|
8080
8318
|
const command = resolveClaudeBinary() ?? "claude";
|
|
8081
|
-
const args = buildPlannerArgs(ctx.epicRequestId);
|
|
8319
|
+
const args = ctx.keyRun ? keyRunArgs(buildPlannerArgs(ctx.epicRequestId)) : buildPlannerArgs(ctx.epicRequestId);
|
|
8082
8320
|
const routedEnv = claudeSpawnEnv(process.env, routing);
|
|
8083
|
-
const
|
|
8321
|
+
const baseEnv = {
|
|
8084
8322
|
...routedEnv,
|
|
8085
8323
|
COMPBOX_NONINTERACTIVE: "1",
|
|
8086
8324
|
PATH: claudeChildEnvPath(routedEnv.PATH)
|
|
8087
8325
|
};
|
|
8326
|
+
const childEnv = ctx.keyRun ? keyRunEnv(baseEnv, ctx.keyRun) : baseEnv;
|
|
8088
8327
|
try {
|
|
8089
8328
|
await ctx.mailboxClient.postTurn(
|
|
8090
8329
|
ctx.machineToken,
|
|
@@ -8093,7 +8332,7 @@ async function invokeStartExecutionPlan(ctx, deps = {}) {
|
|
|
8093
8332
|
);
|
|
8094
8333
|
} catch {
|
|
8095
8334
|
}
|
|
8096
|
-
const outcome = await new Promise((
|
|
8335
|
+
const outcome = await new Promise((resolve3) => {
|
|
8097
8336
|
let stdout2 = "";
|
|
8098
8337
|
let stderr = "";
|
|
8099
8338
|
let settled = false;
|
|
@@ -8111,7 +8350,7 @@ async function invokeStartExecutionPlan(ctx, deps = {}) {
|
|
|
8111
8350
|
settled = true;
|
|
8112
8351
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
8113
8352
|
clearTimeout(timer);
|
|
8114
|
-
|
|
8353
|
+
resolve3(result);
|
|
8115
8354
|
};
|
|
8116
8355
|
const timer = setTimeout(() => {
|
|
8117
8356
|
child.kill("SIGKILL");
|
|
@@ -8138,6 +8377,7 @@ async function invokeStartExecutionPlan(ctx, deps = {}) {
|
|
|
8138
8377
|
});
|
|
8139
8378
|
child.on("close", (code) => {
|
|
8140
8379
|
void (async () => {
|
|
8380
|
+
if (ctx.keyRun) stdout2 = await settleKeyRun(ctx, ctx.keyRun, stdout2);
|
|
8141
8381
|
const combined = `${stdout2}
|
|
8142
8382
|
${stderr}`;
|
|
8143
8383
|
const review = detectNeedsHumanReview(combined);
|
|
@@ -8248,6 +8488,13 @@ function defaultToolchainPresent(_dir) {
|
|
|
8248
8488
|
}
|
|
8249
8489
|
return false;
|
|
8250
8490
|
}
|
|
8491
|
+
function describeRunBranch(finish) {
|
|
8492
|
+
if (finish.prUrl) return `Pull request: ${finish.prUrl}`;
|
|
8493
|
+
if (finish.compareUrl) return `Branch ${finish.branch} was pushed. Open the pull request here: ${finish.compareUrl}`;
|
|
8494
|
+
if (finish.commits === 0) return "No code changes were made.";
|
|
8495
|
+
if (finish.pushed) return `Branch ${finish.branch} was pushed.`;
|
|
8496
|
+
return `The work is on branch ${finish.branch}${finish.keptWorktree ? ` (in ${finish.keptWorktree})` : ""}.`;
|
|
8497
|
+
}
|
|
8251
8498
|
function markerString(marker, key) {
|
|
8252
8499
|
const v = marker?.[key];
|
|
8253
8500
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
@@ -8289,29 +8536,41 @@ async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
|
|
|
8289
8536
|
if (!toolchainPresent(projectDir)) {
|
|
8290
8537
|
return { status: "failed", result: { error: toolchainMissingMessage(projectDir) } };
|
|
8291
8538
|
}
|
|
8539
|
+
let workspace = null;
|
|
8540
|
+
let workspaceNotes = [];
|
|
8541
|
+
if (deps.workspace) {
|
|
8542
|
+
const prepared = await deps.workspace.prepare(projectDir, ctx.epicId);
|
|
8543
|
+
if (!prepared.ok) {
|
|
8544
|
+
return { status: "failed", result: { error: prepared.error } };
|
|
8545
|
+
}
|
|
8546
|
+
workspace = prepared.workspace;
|
|
8547
|
+
workspaceNotes = prepared.notes;
|
|
8548
|
+
}
|
|
8549
|
+
const runDir = workspace?.dir ?? projectDir;
|
|
8292
8550
|
const routing = await resolveRoutingImpl();
|
|
8293
8551
|
const command = resolveClaudeBinary() ?? "claude";
|
|
8294
|
-
const args = buildOrchestratorArgs(ctx.epicId);
|
|
8552
|
+
const args = ctx.keyRun ? keyRunArgs(buildOrchestratorArgs(ctx.epicId)) : buildOrchestratorArgs(ctx.epicId);
|
|
8295
8553
|
const routedEnv = claudeSpawnEnv(process.env, routing);
|
|
8296
|
-
const
|
|
8554
|
+
const baseEnv = {
|
|
8297
8555
|
...routedEnv,
|
|
8298
8556
|
COMPBOX_NONINTERACTIVE: "1",
|
|
8299
8557
|
PATH: claudeChildEnvPath(routedEnv.PATH)
|
|
8300
8558
|
};
|
|
8559
|
+
const childEnv = ctx.keyRun ? keyRunEnv(baseEnv, ctx.keyRun) : baseEnv;
|
|
8301
8560
|
try {
|
|
8302
8561
|
await ctx.mailboxClient.postTurn(
|
|
8303
8562
|
ctx.machineToken,
|
|
8304
8563
|
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.`
|
|
8564
|
+
`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
8565
|
);
|
|
8307
8566
|
} catch {
|
|
8308
8567
|
}
|
|
8309
|
-
const outcome = await new Promise((
|
|
8568
|
+
const outcome = await new Promise((resolve3) => {
|
|
8310
8569
|
let stdout2 = "";
|
|
8311
8570
|
let stderr = "";
|
|
8312
8571
|
let settled = false;
|
|
8313
8572
|
const child = spawnImpl(command, args, {
|
|
8314
|
-
cwd:
|
|
8573
|
+
cwd: runDir,
|
|
8315
8574
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8316
8575
|
env: childEnv
|
|
8317
8576
|
});
|
|
@@ -8324,7 +8583,7 @@ async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
|
|
|
8324
8583
|
settled = true;
|
|
8325
8584
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
8326
8585
|
clearTimeout(timer);
|
|
8327
|
-
|
|
8586
|
+
resolve3(result);
|
|
8328
8587
|
};
|
|
8329
8588
|
const timer = setTimeout(() => {
|
|
8330
8589
|
child.kill("SIGKILL");
|
|
@@ -8350,6 +8609,7 @@ async function invokeStartExecutionOrchestrate(ctx, deps = {}) {
|
|
|
8350
8609
|
});
|
|
8351
8610
|
});
|
|
8352
8611
|
child.on("close", (code) => {
|
|
8612
|
+
if (ctx.keyRun) stdout2 = takeKeyRunResult(ctx, ctx.keyRun, stdout2).text;
|
|
8353
8613
|
const combined = `${stdout2}
|
|
8354
8614
|
${stderr}`;
|
|
8355
8615
|
const review = detectNeedsHumanReview(combined);
|
|
@@ -8372,8 +8632,31 @@ ${stderr}`;
|
|
|
8372
8632
|
finish({ status: "done", result: { epicId: ctx.epicId } });
|
|
8373
8633
|
});
|
|
8374
8634
|
});
|
|
8635
|
+
let branchLine = "";
|
|
8636
|
+
if (deps.workspace && workspace) {
|
|
8637
|
+
const resultError = outcome.result;
|
|
8638
|
+
const stopReason = typeof resultError.message === "string" ? resultError.message : typeof resultError.error === "string" ? resultError.error.split("\n")[0] : void 0;
|
|
8639
|
+
const finished = await deps.workspace.finish({
|
|
8640
|
+
workspace,
|
|
8641
|
+
epicId: ctx.epicId,
|
|
8642
|
+
jobId: ctx.jobId,
|
|
8643
|
+
completed: outcome.status === "done",
|
|
8644
|
+
...stopReason ? { stopReason } : {}
|
|
8645
|
+
});
|
|
8646
|
+
branchLine = describeRunBranch(finished);
|
|
8647
|
+
outcome.result = {
|
|
8648
|
+
...outcome.result,
|
|
8649
|
+
branch: finished.branch,
|
|
8650
|
+
commits: finished.commits,
|
|
8651
|
+
pushed: finished.pushed,
|
|
8652
|
+
...finished.prUrl ? { prUrl: finished.prUrl } : {},
|
|
8653
|
+
...finished.compareUrl ? { compareUrl: finished.compareUrl } : {},
|
|
8654
|
+
...finished.notes.length > 0 ? { branchNotes: finished.notes } : {}
|
|
8655
|
+
};
|
|
8656
|
+
}
|
|
8375
8657
|
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)}
|
|
8658
|
+
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 ? `
|
|
8659
|
+
${branchLine}` : "");
|
|
8377
8660
|
await ctx.mailboxClient.postTurn(ctx.machineToken, ctx.jobId, summary);
|
|
8378
8661
|
} catch {
|
|
8379
8662
|
}
|
|
@@ -8462,14 +8745,419 @@ async function updatePersonalEpicRequest(apiBaseUrl, userApiToken, epicRequestId
|
|
|
8462
8745
|
return parseComplianceFromData(data?.data);
|
|
8463
8746
|
}
|
|
8464
8747
|
|
|
8748
|
+
// src/utils/run-workspace.ts
|
|
8749
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
8750
|
+
import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "node:fs";
|
|
8751
|
+
import { basename as basename2, dirname as dirname4, join as join10 } from "node:path";
|
|
8752
|
+
var RUN_PUSH_ENV = "COMPBOX_RUN_PUSH";
|
|
8753
|
+
var INSTALL_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
8754
|
+
var GIT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
8755
|
+
var defaultCommandRunner = (command, args, cwd) => new Promise((resolve3) => {
|
|
8756
|
+
let stdout2 = "";
|
|
8757
|
+
let stderr = "";
|
|
8758
|
+
let settled = false;
|
|
8759
|
+
const done = (result) => {
|
|
8760
|
+
if (settled) return;
|
|
8761
|
+
settled = true;
|
|
8762
|
+
clearTimeout(timer);
|
|
8763
|
+
resolve3(result);
|
|
8764
|
+
};
|
|
8765
|
+
const child = spawn4(command, args, {
|
|
8766
|
+
cwd,
|
|
8767
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
8768
|
+
env: { ...process.env, PATH: claudeChildEnvPath(process.env.PATH), GIT_TERMINAL_PROMPT: "0" }
|
|
8769
|
+
});
|
|
8770
|
+
const timer = setTimeout(
|
|
8771
|
+
() => {
|
|
8772
|
+
child.kill("SIGKILL");
|
|
8773
|
+
done({ status: 1, stdout: stdout2, stderr: `${command} timed out` });
|
|
8774
|
+
},
|
|
8775
|
+
command === "git" || command === "gh" ? GIT_TIMEOUT_MS : INSTALL_TIMEOUT_MS
|
|
8776
|
+
);
|
|
8777
|
+
child.stdout.on("data", (chunk) => {
|
|
8778
|
+
stdout2 += chunk.toString("utf-8");
|
|
8779
|
+
});
|
|
8780
|
+
child.stderr.on("data", (chunk) => {
|
|
8781
|
+
stderr += chunk.toString("utf-8");
|
|
8782
|
+
});
|
|
8783
|
+
child.on("error", (error) => {
|
|
8784
|
+
done({ status: 1, stdout: stdout2, stderr: error.message });
|
|
8785
|
+
});
|
|
8786
|
+
child.on("close", (code) => {
|
|
8787
|
+
done({ status: code ?? 1, stdout: stdout2, stderr });
|
|
8788
|
+
});
|
|
8789
|
+
});
|
|
8790
|
+
function isLocalConfigFile(path3) {
|
|
8791
|
+
if (path3.split("/").includes("node_modules")) return false;
|
|
8792
|
+
const name = basename2(path3);
|
|
8793
|
+
return name === ".mcp.json" || name === "CLAUDE.local.md" || path3 === ".claude/settings.local.json" || name === ".env" || name.startsWith(".env.") && !name.endsWith(".example") && !name.endsWith(".sample");
|
|
8794
|
+
}
|
|
8795
|
+
function slugForBranch(value) {
|
|
8796
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "run";
|
|
8797
|
+
}
|
|
8798
|
+
function stamp(now) {
|
|
8799
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8800
|
+
return `${String(now.getFullYear())}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
8801
|
+
}
|
|
8802
|
+
function firstLine(text) {
|
|
8803
|
+
return text.trim().split("\n")[0] ?? "";
|
|
8804
|
+
}
|
|
8805
|
+
function installCommand2(dir, exists = existsSync9) {
|
|
8806
|
+
if (exists(join10(dir, "pnpm-lock.yaml"))) return ["pnpm", ["install", "--frozen-lockfile", "--prefer-offline"]];
|
|
8807
|
+
if (exists(join10(dir, "package-lock.json"))) return ["npm", ["ci", "--prefer-offline"]];
|
|
8808
|
+
if (exists(join10(dir, "yarn.lock"))) return ["yarn", ["install", "--frozen-lockfile"]];
|
|
8809
|
+
return null;
|
|
8810
|
+
}
|
|
8811
|
+
function githubCompareUrl(remoteUrl, baseBranch, branch) {
|
|
8812
|
+
const m = /^https?:\/\/(?:[^@/]+@)?github\.com\/([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(remoteUrl.trim()) ?? /^(?:ssh:\/\/)?git@github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(remoteUrl.trim());
|
|
8813
|
+
if (!m) return null;
|
|
8814
|
+
return `https://github.com/${m[1] ?? ""}/${m[2] ?? ""}/compare/${encodeURIComponent(baseBranch)}...${encodeURIComponent(branch)}?expand=1`;
|
|
8815
|
+
}
|
|
8816
|
+
var repoQueues = /* @__PURE__ */ new Map();
|
|
8817
|
+
function withRepoLock(projectDir, task) {
|
|
8818
|
+
const previous = repoQueues.get(projectDir) ?? Promise.resolve();
|
|
8819
|
+
const result = previous.catch(() => void 0).then(task);
|
|
8820
|
+
const tail = result.catch(() => void 0);
|
|
8821
|
+
repoQueues.set(projectDir, tail);
|
|
8822
|
+
void tail.then(() => {
|
|
8823
|
+
if (repoQueues.get(projectDir) === tail) repoQueues.delete(projectDir);
|
|
8824
|
+
});
|
|
8825
|
+
return result;
|
|
8826
|
+
}
|
|
8827
|
+
var RunWorkspaceManager = class {
|
|
8828
|
+
run;
|
|
8829
|
+
now;
|
|
8830
|
+
runsRoot;
|
|
8831
|
+
exists;
|
|
8832
|
+
copyFile;
|
|
8833
|
+
env;
|
|
8834
|
+
constructor(deps = {}) {
|
|
8835
|
+
this.run = deps.run ?? defaultCommandRunner;
|
|
8836
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
8837
|
+
this.runsRoot = deps.runsRoot ?? join10(getConfigDir(), "runs");
|
|
8838
|
+
this.exists = deps.exists ?? existsSync9;
|
|
8839
|
+
this.copyFile = deps.copyFile ?? ((from, to) => {
|
|
8840
|
+
mkdirSync6(dirname4(to), { recursive: true });
|
|
8841
|
+
copyFileSync(from, to);
|
|
8842
|
+
});
|
|
8843
|
+
this.env = deps.env ?? process.env;
|
|
8844
|
+
}
|
|
8845
|
+
git(args, cwd) {
|
|
8846
|
+
return this.run("git", args, cwd);
|
|
8847
|
+
}
|
|
8848
|
+
/** Creates the branch + worktree for a run. Never touches the person's checkout. */
|
|
8849
|
+
async prepare(projectDir, epicId) {
|
|
8850
|
+
const created = await withRepoLock(projectDir, () => this.createWorktree(projectDir, epicId));
|
|
8851
|
+
if (!created.ok) return created;
|
|
8852
|
+
const { workspace, notes } = created;
|
|
8853
|
+
const { dir, branch } = workspace;
|
|
8854
|
+
const install = installCommand2(dir, this.exists);
|
|
8855
|
+
if (install) {
|
|
8856
|
+
const [command, args] = install;
|
|
8857
|
+
const res = await this.run(command, args, dir);
|
|
8858
|
+
if (res.status !== 0) {
|
|
8859
|
+
await withRepoLock(projectDir, async () => {
|
|
8860
|
+
await this.git(["worktree", "remove", "--force", dir], projectDir);
|
|
8861
|
+
await this.git(["branch", "-D", branch], projectDir);
|
|
8862
|
+
});
|
|
8863
|
+
return {
|
|
8864
|
+
ok: false,
|
|
8865
|
+
error: [
|
|
8866
|
+
`Installing the project's dependencies failed (${command} ${args.join(" ")}): ${firstLine(res.stderr) || firstLine(res.stdout) || "unknown error"}`,
|
|
8867
|
+
"The run was not started. Check that the same command works in your project folder."
|
|
8868
|
+
].join("\n")
|
|
8869
|
+
};
|
|
8870
|
+
}
|
|
8871
|
+
}
|
|
8872
|
+
return { ok: true, workspace, notes };
|
|
8873
|
+
}
|
|
8874
|
+
async createWorktree(projectDir, epicId) {
|
|
8875
|
+
const notes = [];
|
|
8876
|
+
const remotes = (await this.git(["remote"], projectDir)).stdout.split("\n").map((r) => r.trim()).filter(Boolean);
|
|
8877
|
+
const remote = remotes.includes("origin") ? "origin" : remotes[0] ?? null;
|
|
8878
|
+
let baseBranch = "";
|
|
8879
|
+
let baseRef = "HEAD";
|
|
8880
|
+
if (remote) {
|
|
8881
|
+
if ((await this.git(["fetch", remote, "--quiet"], projectDir)).status !== 0) {
|
|
8882
|
+
notes.push(`Couldn't fetch from ${remote}, so the run starts from the last fetched copy.`);
|
|
8883
|
+
}
|
|
8884
|
+
const head = await this.git(["symbolic-ref", "--quiet", "--short", `refs/remotes/${remote}/HEAD`], projectDir);
|
|
8885
|
+
if (head.status === 0 && head.stdout.trim().startsWith(`${remote}/`)) {
|
|
8886
|
+
baseBranch = head.stdout.trim().slice(remote.length + 1);
|
|
8887
|
+
} else {
|
|
8888
|
+
for (const candidate of ["main", "master"]) {
|
|
8889
|
+
if ((await this.git(["rev-parse", "--verify", "--quiet", `${remote}/${candidate}`], projectDir)).status === 0) {
|
|
8890
|
+
baseBranch = candidate;
|
|
8891
|
+
break;
|
|
8892
|
+
}
|
|
8893
|
+
}
|
|
8894
|
+
}
|
|
8895
|
+
if (baseBranch) baseRef = `${remote}/${baseBranch}`;
|
|
8896
|
+
}
|
|
8897
|
+
if (!baseBranch) {
|
|
8898
|
+
baseBranch = (await this.git(["rev-parse", "--abbrev-ref", "HEAD"], projectDir)).stdout.trim() || "main";
|
|
8899
|
+
baseRef = "HEAD";
|
|
8900
|
+
}
|
|
8901
|
+
const when = stamp(this.now());
|
|
8902
|
+
const branch = `compbox/epic-${slugForBranch(epicId)}-${when}`;
|
|
8903
|
+
const dir = join10(this.runsRoot, `${slugForBranch(basename2(projectDir))}-${slugForBranch(epicId)}-${when}`);
|
|
8904
|
+
mkdirSync6(this.runsRoot, { recursive: true });
|
|
8905
|
+
const add = await this.git(["worktree", "add", "-b", branch, dir, baseRef], projectDir);
|
|
8906
|
+
if (add.status !== 0) {
|
|
8907
|
+
return {
|
|
8908
|
+
ok: false,
|
|
8909
|
+
error: [
|
|
8910
|
+
`Couldn't create a separate workspace for this run (git worktree add failed): ${firstLine(add.stderr) || "unknown error"}`,
|
|
8911
|
+
"The run was not started, and your project folder was not changed."
|
|
8912
|
+
].join("\n")
|
|
8913
|
+
};
|
|
8914
|
+
}
|
|
8915
|
+
const ignored = await this.git(["ls-files", "--others", "--ignored", "--exclude-standard", "--directory"], projectDir);
|
|
8916
|
+
const untracked = await this.git(["ls-files", "--others", "--exclude-standard"], projectDir);
|
|
8917
|
+
const candidates = new Set(
|
|
8918
|
+
`${ignored.stdout}
|
|
8919
|
+
${untracked.stdout}`.split("\n").map((p) => p.trim()).filter((p) => p && !p.endsWith("/"))
|
|
8920
|
+
);
|
|
8921
|
+
for (const path3 of candidates) {
|
|
8922
|
+
if (!isLocalConfigFile(path3)) continue;
|
|
8923
|
+
try {
|
|
8924
|
+
this.copyFile(join10(projectDir, path3), join10(dir, path3));
|
|
8925
|
+
} catch {
|
|
8926
|
+
notes.push(`Couldn't copy ${path3} into the run workspace.`);
|
|
8927
|
+
}
|
|
8928
|
+
}
|
|
8929
|
+
return { ok: true, workspace: { dir, projectDir, branch, baseRef, baseBranch, remote }, notes };
|
|
8930
|
+
}
|
|
8931
|
+
/** Commits, pushes and opens the pull request; then cleans up the worktree. */
|
|
8932
|
+
finish(input) {
|
|
8933
|
+
return withRepoLock(input.workspace.projectDir, () => this.finishLocked(input));
|
|
8934
|
+
}
|
|
8935
|
+
async finishLocked(input) {
|
|
8936
|
+
const { workspace: ws } = input;
|
|
8937
|
+
const notes = [];
|
|
8938
|
+
if ((await this.git(["status", "--porcelain"], ws.dir)).stdout.trim()) {
|
|
8939
|
+
await this.git(["add", "-A"], ws.dir);
|
|
8940
|
+
const commit = await this.git(
|
|
8941
|
+
["commit", "-m", `chore(compbox): save work left uncommitted by the orchestrator (epic ${input.epicId})`],
|
|
8942
|
+
ws.dir
|
|
8943
|
+
);
|
|
8944
|
+
if (commit.status !== 0) {
|
|
8945
|
+
notes.push(`Couldn't commit the remaining changes: ${firstLine(commit.stderr) || firstLine(commit.stdout)}`);
|
|
8946
|
+
}
|
|
8947
|
+
}
|
|
8948
|
+
const commits = Number.parseInt((await this.git(["rev-list", "--count", `${ws.baseRef}..HEAD`], ws.dir)).stdout.trim(), 10) || 0;
|
|
8949
|
+
const base = { branch: ws.branch, commits, pushed: false, notes };
|
|
8950
|
+
if (commits === 0) {
|
|
8951
|
+
await this.git(["worktree", "remove", "--force", ws.dir], ws.projectDir);
|
|
8952
|
+
await this.git(["branch", "-D", ws.branch], ws.projectDir);
|
|
8953
|
+
notes.push("The run made no code changes, so there is nothing to review.");
|
|
8954
|
+
return base;
|
|
8955
|
+
}
|
|
8956
|
+
if (!ws.remote || this.env[RUN_PUSH_ENV] === "0") {
|
|
8957
|
+
await this.git(["worktree", "remove", "--force", ws.dir], ws.projectDir);
|
|
8958
|
+
notes.push(
|
|
8959
|
+
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}.`
|
|
8960
|
+
);
|
|
8961
|
+
return base;
|
|
8962
|
+
}
|
|
8963
|
+
const push = await this.git(["push", "--set-upstream", ws.remote, ws.branch], ws.dir);
|
|
8964
|
+
if (push.status !== 0) {
|
|
8965
|
+
notes.push(
|
|
8966
|
+
`Couldn't push the branch to ${ws.remote}: ${firstLine(push.stderr) || "unknown error"}. The work is safe in ${ws.dir} on branch ${ws.branch}.`
|
|
8967
|
+
);
|
|
8968
|
+
return { ...base, keptWorktree: ws.dir };
|
|
8969
|
+
}
|
|
8970
|
+
const title = `CompBox: epic ${input.epicId}${input.completed ? "" : " (stopped early, needs review)"}`;
|
|
8971
|
+
const body = [
|
|
8972
|
+
`Code written by the CompBox orchestrator for epic **${input.epicId}** (job \`${input.jobId}\`).`,
|
|
8973
|
+
"",
|
|
8974
|
+
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.`,
|
|
8975
|
+
"",
|
|
8976
|
+
`${String(commits)} commit${commits === 1 ? "" : "s"} on \`${ws.branch}\`, based on \`${ws.baseBranch}\`.`
|
|
8977
|
+
].join("\n");
|
|
8978
|
+
const prArgs = ["pr", "create", "--base", ws.baseBranch, "--head", ws.branch, "--title", title, "--body", body];
|
|
8979
|
+
if (!input.completed) prArgs.push("--draft");
|
|
8980
|
+
const pr = await this.run("gh", prArgs, ws.dir);
|
|
8981
|
+
const prUrl = pr.status === 0 ? /https:\/\/\S+/.exec(pr.stdout)?.[0] : void 0;
|
|
8982
|
+
let compareUrl;
|
|
8983
|
+
if (!prUrl) {
|
|
8984
|
+
const remoteUrl = (await this.git(["remote", "get-url", ws.remote], ws.dir)).stdout.trim();
|
|
8985
|
+
compareUrl = githubCompareUrl(remoteUrl, ws.baseBranch, ws.branch) ?? void 0;
|
|
8986
|
+
notes.push(
|
|
8987
|
+
`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.`)
|
|
8988
|
+
);
|
|
8989
|
+
}
|
|
8990
|
+
await this.git(["worktree", "remove", "--force", ws.dir], ws.projectDir);
|
|
8991
|
+
return { ...base, pushed: true, ...prUrl ? { prUrl } : {}, ...compareUrl ? { compareUrl } : {} };
|
|
8992
|
+
}
|
|
8993
|
+
};
|
|
8994
|
+
|
|
8995
|
+
// src/utils/repo-setup.ts
|
|
8996
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
8997
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync } from "node:fs";
|
|
8998
|
+
import { homedir as homedir6 } from "node:os";
|
|
8999
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join11, resolve as resolve2 } from "node:path";
|
|
9000
|
+
var CLONE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
9001
|
+
var defaultGitCloner = (url, target) => new Promise((done) => {
|
|
9002
|
+
let stderr = "";
|
|
9003
|
+
const child = spawn5("git", ["clone", "--quiet", url, target], {
|
|
9004
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
9005
|
+
// Never hang on a username/password prompt: fail and explain instead.
|
|
9006
|
+
env: { ...process.env, PATH: claudeChildEnvPath(process.env.PATH), GIT_TERMINAL_PROMPT: "0" }
|
|
9007
|
+
});
|
|
9008
|
+
const timer = setTimeout(() => {
|
|
9009
|
+
child.kill("SIGKILL");
|
|
9010
|
+
stderr += "git clone timed out";
|
|
9011
|
+
}, CLONE_TIMEOUT_MS);
|
|
9012
|
+
child.stderr.on("data", (chunk) => {
|
|
9013
|
+
stderr += chunk.toString("utf-8");
|
|
9014
|
+
});
|
|
9015
|
+
child.on("error", (error) => {
|
|
9016
|
+
clearTimeout(timer);
|
|
9017
|
+
done({ status: 1, stderr: error.message });
|
|
9018
|
+
});
|
|
9019
|
+
child.on("close", (code) => {
|
|
9020
|
+
clearTimeout(timer);
|
|
9021
|
+
done({ status: code ?? 1, stderr });
|
|
9022
|
+
});
|
|
9023
|
+
});
|
|
9024
|
+
function repoFolderName(remote) {
|
|
9025
|
+
const last = remote.replace(/\/+$/, "").split("/").pop() ?? "";
|
|
9026
|
+
return last.replace(/[^A-Za-z0-9._-]+/g, "-") || "project";
|
|
9027
|
+
}
|
|
9028
|
+
function resolveFolder(folder, home) {
|
|
9029
|
+
const trimmed = folder.trim();
|
|
9030
|
+
if (trimmed === "~") return home;
|
|
9031
|
+
if (trimmed.startsWith("~/")) return join11(home, trimmed.slice(2));
|
|
9032
|
+
return isAbsolute2(trimmed) ? resolve2(trimmed) : join11(home, trimmed);
|
|
9033
|
+
}
|
|
9034
|
+
function cloneUrls(remote) {
|
|
9035
|
+
const urls = [`${remote}.git`];
|
|
9036
|
+
const gh = /^https:\/\/github\.com\/([^/]+)\/([^/]+)$/.exec(remote);
|
|
9037
|
+
if (gh) urls.push(`git@github.com:${gh[1] ?? ""}/${gh[2] ?? ""}.git`);
|
|
9038
|
+
return urls;
|
|
9039
|
+
}
|
|
9040
|
+
function parsePayload(payload) {
|
|
9041
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
9042
|
+
const p = payload;
|
|
9043
|
+
if (typeof p.teamId !== "string" || typeof p.projectRemote !== "string") return null;
|
|
9044
|
+
if (!p.projectRemote.startsWith("https://")) return null;
|
|
9045
|
+
return {
|
|
9046
|
+
teamId: p.teamId,
|
|
9047
|
+
projectRemote: p.projectRemote,
|
|
9048
|
+
...typeof p.folder === "string" ? { folder: p.folder } : {},
|
|
9049
|
+
...typeof p.projectName === "string" ? { projectName: p.projectName } : {}
|
|
9050
|
+
};
|
|
9051
|
+
}
|
|
9052
|
+
function firstLine2(text) {
|
|
9053
|
+
return text.trim().split("\n").filter(Boolean).pop() ?? "";
|
|
9054
|
+
}
|
|
9055
|
+
async function invokeConnectRepo(payload, deps = {}) {
|
|
9056
|
+
const clone = deps.clone ?? defaultGitCloner;
|
|
9057
|
+
const home = deps.home ?? homedir6();
|
|
9058
|
+
const exists = deps.exists ?? existsSync10;
|
|
9059
|
+
const isEmptyDir = deps.isEmptyDir ?? ((path3) => {
|
|
9060
|
+
try {
|
|
9061
|
+
return readdirSync(path3).length === 0;
|
|
9062
|
+
} catch {
|
|
9063
|
+
return false;
|
|
9064
|
+
}
|
|
9065
|
+
});
|
|
9066
|
+
const isGitRepo = deps.isGitRepo ?? isGitRepoDir;
|
|
9067
|
+
const readOrigin = deps.readOrigin ?? readOriginRemote;
|
|
9068
|
+
const listDirs = deps.listDirs ?? listProjectDirs;
|
|
9069
|
+
const addDir = deps.addDir ?? ((path3) => addProjectDir(path3));
|
|
9070
|
+
const mkdir2 = deps.mkdir ?? ((path3) => void mkdirSync7(path3, { recursive: true }));
|
|
9071
|
+
const p = parsePayload(payload);
|
|
9072
|
+
if (!p) {
|
|
9073
|
+
return { status: "failed", result: { error: "The job was missing the project repository. Try again from the project page." } };
|
|
9074
|
+
}
|
|
9075
|
+
const remote = normalizeGitRemote(p.projectRemote);
|
|
9076
|
+
const name = p.projectName ?? repoFolderName(remote);
|
|
9077
|
+
const existing = listDirs().find((entry) => entry.remote === remote && exists(entry.path));
|
|
9078
|
+
if (existing) {
|
|
9079
|
+
return {
|
|
9080
|
+
status: "done",
|
|
9081
|
+
result: { path: existing.path, remote, outcome: "already_set_up", message: `${name} is already set up on this computer at ${existing.path}.` }
|
|
9082
|
+
};
|
|
9083
|
+
}
|
|
9084
|
+
const target = resolveFolder(p.folder ?? join11("CompBox", repoFolderName(remote)), home);
|
|
9085
|
+
try {
|
|
9086
|
+
if (exists(target) && !isEmptyDir(target)) {
|
|
9087
|
+
let origin = "";
|
|
9088
|
+
try {
|
|
9089
|
+
origin = isGitRepo(target) ? normalizeGitRemote(readOrigin(target)) : "";
|
|
9090
|
+
} catch {
|
|
9091
|
+
origin = "";
|
|
9092
|
+
}
|
|
9093
|
+
if (origin !== remote) {
|
|
9094
|
+
return {
|
|
9095
|
+
status: "failed",
|
|
9096
|
+
result: {
|
|
9097
|
+
error: `The folder ${target} already exists and isn't a copy of ${remote}. Choose a different folder and try again.`
|
|
9098
|
+
}
|
|
9099
|
+
};
|
|
9100
|
+
}
|
|
9101
|
+
const entry2 = addDir(target);
|
|
9102
|
+
return {
|
|
9103
|
+
status: "done",
|
|
9104
|
+
result: { path: entry2.path, remote, outcome: "adopted", message: `Found an existing copy of ${name} at ${entry2.path} and set it up.` }
|
|
9105
|
+
};
|
|
9106
|
+
}
|
|
9107
|
+
mkdir2(dirname5(target));
|
|
9108
|
+
const errors = [];
|
|
9109
|
+
let cloned = false;
|
|
9110
|
+
for (const url of cloneUrls(remote)) {
|
|
9111
|
+
const res = await clone(url, target);
|
|
9112
|
+
if (res.status === 0) {
|
|
9113
|
+
cloned = true;
|
|
9114
|
+
break;
|
|
9115
|
+
}
|
|
9116
|
+
errors.push(firstLine2(res.stderr));
|
|
9117
|
+
}
|
|
9118
|
+
if (!cloned) {
|
|
9119
|
+
return {
|
|
9120
|
+
status: "failed",
|
|
9121
|
+
result: {
|
|
9122
|
+
error: [
|
|
9123
|
+
`This computer couldn't download ${remote}: ${errors.filter(Boolean).join(" / ") || "unknown error"}.`,
|
|
9124
|
+
"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."
|
|
9125
|
+
].join(" ")
|
|
9126
|
+
}
|
|
9127
|
+
};
|
|
9128
|
+
}
|
|
9129
|
+
const entry = addDir(target);
|
|
9130
|
+
return {
|
|
9131
|
+
status: "done",
|
|
9132
|
+
result: { path: entry.path, remote, outcome: "cloned", message: `Downloaded ${name} to ${entry.path} and set it up.` }
|
|
9133
|
+
};
|
|
9134
|
+
} catch (error) {
|
|
9135
|
+
return {
|
|
9136
|
+
status: "failed",
|
|
9137
|
+
result: { error: `Setting up ${name} failed: ${error instanceof Error ? error.message : String(error)}` }
|
|
9138
|
+
};
|
|
9139
|
+
}
|
|
9140
|
+
}
|
|
9141
|
+
|
|
8465
9142
|
// src/commands/run.ts
|
|
8466
9143
|
var POLL_WAIT_MS = 0;
|
|
8467
9144
|
var IDLE_POLL_INTERVAL_MS = 15e3;
|
|
8468
9145
|
var ACTIVE_POLL_INTERVAL_MS = 2e3;
|
|
8469
9146
|
var ACTIVE_WINDOW_MS = 6e4;
|
|
8470
|
-
|
|
9147
|
+
var CONVERSATION_POLL_INTERVAL_MS = 3e3;
|
|
9148
|
+
var CONVERSATION_WINDOW_MS = 10 * 6e4;
|
|
9149
|
+
function idleDelay(now, lastJobAt, lastConversationAt = 0) {
|
|
8471
9150
|
const warm = lastJobAt > 0 && now - lastJobAt < ACTIVE_WINDOW_MS;
|
|
8472
|
-
|
|
9151
|
+
if (warm) return ACTIVE_POLL_INTERVAL_MS;
|
|
9152
|
+
const talking = lastConversationAt > 0 && now - lastConversationAt < CONVERSATION_WINDOW_MS;
|
|
9153
|
+
return talking ? CONVERSATION_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
|
|
9154
|
+
}
|
|
9155
|
+
var DEFAULT_MAX_PARALLEL_JOBS = 3;
|
|
9156
|
+
var MAX_JOBS_ENV = "COMPBOX_MAX_JOBS";
|
|
9157
|
+
function maxParallelJobs(env = process.env) {
|
|
9158
|
+
const raw = Number.parseInt(env[MAX_JOBS_ENV] ?? "", 10);
|
|
9159
|
+
if (!Number.isFinite(raw) || raw < 1) return DEFAULT_MAX_PARALLEL_JOBS;
|
|
9160
|
+
return Math.min(raw, 10);
|
|
8473
9161
|
}
|
|
8474
9162
|
var BACKOFF_BASE_MS = 1e3;
|
|
8475
9163
|
var BACKOFF_MAX_MS = 3e4;
|
|
@@ -8477,8 +9165,8 @@ var HEARTBEAT_INTERVAL_MS = 2e4;
|
|
|
8477
9165
|
function runtimeHeartbeatDue(nowMs, lastRuntimeSentMs, intervalMs = HEARTBEAT_INTERVAL_MS) {
|
|
8478
9166
|
return nowMs - lastRuntimeSentMs >= intervalMs;
|
|
8479
9167
|
}
|
|
8480
|
-
function
|
|
8481
|
-
return new Promise((
|
|
9168
|
+
function sleep(ms) {
|
|
9169
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
8482
9170
|
}
|
|
8483
9171
|
function backoffDelay(consecutiveErrors) {
|
|
8484
9172
|
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** (consecutiveErrors - 1));
|
|
@@ -8523,11 +9211,12 @@ function parseStartExecutionPayload(payload) {
|
|
|
8523
9211
|
const p = parsed;
|
|
8524
9212
|
if (!START_EXECUTION_KINDS.includes(p.kind)) return null;
|
|
8525
9213
|
const projectRemote = typeof p.projectRemote === "string" && p.projectRemote.length > 0 ? p.projectRemote : void 0;
|
|
9214
|
+
const useAiKey = p.useAiKey === true;
|
|
8526
9215
|
if (p.kind === "plan" && typeof p.epicRequestId === "string" && p.epicRequestId.length > 0) {
|
|
8527
|
-
return { kind: "plan", epicRequestId: p.epicRequestId, ...projectRemote && { projectRemote } };
|
|
9216
|
+
return { kind: "plan", epicRequestId: p.epicRequestId, ...projectRemote && { projectRemote }, ...useAiKey && { useAiKey } };
|
|
8528
9217
|
}
|
|
8529
9218
|
if (p.kind === "orchestrate" && typeof p.epicId === "string" && p.epicId.length > 0) {
|
|
8530
|
-
return { kind: "orchestrate", epicId: p.epicId, ...projectRemote && { projectRemote } };
|
|
9219
|
+
return { kind: "orchestrate", epicId: p.epicId, ...projectRemote && { projectRemote }, ...useAiKey && { useAiKey } };
|
|
8531
9220
|
}
|
|
8532
9221
|
return null;
|
|
8533
9222
|
}
|
|
@@ -8550,6 +9239,18 @@ async function handleStartExecution(ctx, job) {
|
|
|
8550
9239
|
return;
|
|
8551
9240
|
}
|
|
8552
9241
|
const jobProjectDir = routed.status === "registered" || routed.status === "fallback" ? routed.path : void 0;
|
|
9242
|
+
let keyRun;
|
|
9243
|
+
if (payload.useAiKey) {
|
|
9244
|
+
try {
|
|
9245
|
+
const key = await ctx.client.getAiKey(ctx.machineToken, job.id);
|
|
9246
|
+
keyRun = { apiKey: key.apiKey, model: key.model };
|
|
9247
|
+
console.log(chalk9.gray(` Running on your saved API key (${key.provider}, ${key.model}).`));
|
|
9248
|
+
} catch (error) {
|
|
9249
|
+
const message = error instanceof MailboxClientError ? error.message : "the key request failed";
|
|
9250
|
+
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.`);
|
|
9251
|
+
return;
|
|
9252
|
+
}
|
|
9253
|
+
}
|
|
8553
9254
|
if (payload.kind === "plan") {
|
|
8554
9255
|
const userApiToken2 = ctx.userApiToken;
|
|
8555
9256
|
if (!userApiToken2) {
|
|
@@ -8572,7 +9273,8 @@ async function handleStartExecution(ctx, job) {
|
|
|
8572
9273
|
apiBaseUrl: ctx.apiBaseUrl,
|
|
8573
9274
|
userApiToken: userApiToken2,
|
|
8574
9275
|
epicRequestId: payload.epicRequestId,
|
|
8575
|
-
projectDir: jobProjectDir
|
|
9276
|
+
projectDir: jobProjectDir,
|
|
9277
|
+
...keyRun && { keyRun }
|
|
8576
9278
|
},
|
|
8577
9279
|
{
|
|
8578
9280
|
// H4: keep this machine online during the long planner run — the poll
|
|
@@ -8602,12 +9304,15 @@ async function handleStartExecution(ctx, job) {
|
|
|
8602
9304
|
machineToken: ctx.machineToken,
|
|
8603
9305
|
jobId: job.id,
|
|
8604
9306
|
epicId: payload.epicId,
|
|
8605
|
-
projectDir: jobProjectDir
|
|
9307
|
+
projectDir: jobProjectDir,
|
|
9308
|
+
...keyRun && { keyRun }
|
|
8606
9309
|
},
|
|
8607
9310
|
{
|
|
8608
9311
|
// H4: keep this machine online during the long orchestrator run — the
|
|
8609
9312
|
// poll loop (its own heartbeat) is blocked for the whole job.
|
|
8610
|
-
heartbeat: () => ctx.client.heartbeat(ctx.machineToken).catch(() => void 0)
|
|
9313
|
+
heartbeat: () => ctx.client.heartbeat(ctx.machineToken).catch(() => void 0),
|
|
9314
|
+
// Own branch + worktree per run, finishing with a pull request.
|
|
9315
|
+
workspace: new RunWorkspaceManager()
|
|
8611
9316
|
}
|
|
8612
9317
|
)
|
|
8613
9318
|
);
|
|
@@ -8615,6 +9320,10 @@ async function handleStartExecution(ctx, job) {
|
|
|
8615
9320
|
async function runInvokerAndComplete(ctx, job, invoke) {
|
|
8616
9321
|
try {
|
|
8617
9322
|
const outcome = await invoke();
|
|
9323
|
+
if (outcome.status === "yield") {
|
|
9324
|
+
console.log(chalk9.gray(` Job ${job.id}: replied; waiting for the person.`));
|
|
9325
|
+
return;
|
|
9326
|
+
}
|
|
8618
9327
|
await ctx.client.completeJob(ctx.machineToken, job.id, outcome.status, outcome.result);
|
|
8619
9328
|
console.log(
|
|
8620
9329
|
outcome.status === "done" ? chalk9.green(` Job ${job.id} completed: ${JSON.stringify(outcome.result)}`) : chalk9.yellow(` Job ${job.id} failed: ${JSON.stringify(outcome.result)}`)
|
|
@@ -8639,42 +9348,38 @@ async function handleJob(ctx, job) {
|
|
|
8639
9348
|
\u2192 Claimed job ${job.id} (type: ${job.type}, status: ${job.status})`)
|
|
8640
9349
|
);
|
|
8641
9350
|
const userApiToken = ctx.userApiToken;
|
|
8642
|
-
|
|
8643
|
-
|
|
9351
|
+
if (job.type === "epic_request" && userApiToken) {
|
|
9352
|
+
let anthropicApiKey;
|
|
8644
9353
|
if (ctx.aiMode === "api-key") {
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
ctx,
|
|
8650
|
-
job,
|
|
8651
|
-
() => invokeEpicRequestViaApi(job, {
|
|
8652
|
-
mailboxClient: ctx.client,
|
|
8653
|
-
machineToken: ctx.machineToken,
|
|
8654
|
-
apiBaseUrl: ctx.apiBaseUrl,
|
|
8655
|
-
userApiToken,
|
|
8656
|
-
anthropicApiKey
|
|
8657
|
-
})
|
|
8658
|
-
);
|
|
8659
|
-
return;
|
|
8660
|
-
}
|
|
8661
|
-
} else if (ctx.aiMode === "claude-subscription") {
|
|
8662
|
-
console.log(
|
|
8663
|
-
chalk9.gray(" Running the request-formulator interview (claude-subscription mode, local `claude` CLI)...")
|
|
8664
|
-
);
|
|
9354
|
+
anthropicApiKey = (await retrieveSecret(AI_KEY_ACCOUNT)).value ?? void 0;
|
|
9355
|
+
}
|
|
9356
|
+
if (ctx.aiMode === "claude-subscription" || anthropicApiKey) {
|
|
9357
|
+
console.log(chalk9.gray(` Replying in the request conversation (${anthropicApiKey ? "api mode" : "local claude CLI"})...`));
|
|
8665
9358
|
await runInvokerAndComplete(
|
|
8666
9359
|
ctx,
|
|
8667
9360
|
job,
|
|
8668
|
-
() =>
|
|
9361
|
+
() => invokeEpicRequestStep(job, {
|
|
8669
9362
|
mailboxClient: ctx.client,
|
|
8670
9363
|
machineToken: ctx.machineToken,
|
|
8671
9364
|
apiBaseUrl: ctx.apiBaseUrl,
|
|
8672
|
-
userApiToken
|
|
9365
|
+
userApiToken,
|
|
9366
|
+
...anthropicApiKey && { anthropicApiKey }
|
|
8673
9367
|
})
|
|
8674
9368
|
);
|
|
8675
9369
|
return;
|
|
8676
9370
|
}
|
|
8677
9371
|
}
|
|
9372
|
+
if (job.type === "connect_repo") {
|
|
9373
|
+
console.log(chalk9.gray(" Setting up a project repository on this computer (connect_repo)..."));
|
|
9374
|
+
await runInvokerAndComplete(ctx, job, async () => {
|
|
9375
|
+
const outcome = await invokeConnectRepo(job.payload);
|
|
9376
|
+
if (outcome.status === "done") {
|
|
9377
|
+
await ctx.client.heartbeat(ctx.machineToken, void 0, gatherServicedRemotes()).catch(() => void 0);
|
|
9378
|
+
}
|
|
9379
|
+
return outcome;
|
|
9380
|
+
});
|
|
9381
|
+
return;
|
|
9382
|
+
}
|
|
8678
9383
|
if (job.type === "start_execution") {
|
|
8679
9384
|
await handleStartExecution(ctx, job);
|
|
8680
9385
|
return;
|
|
@@ -8756,6 +9461,9 @@ var runCommand = new Command8("run").description("Start the Agent Runner daemon
|
|
|
8756
9461
|
`
|
|
8757
9462
|
)
|
|
8758
9463
|
);
|
|
9464
|
+
const maxJobs = maxParallelJobs();
|
|
9465
|
+
console.log(chalk9.gray(` Runs up to ${String(maxJobs)} job${maxJobs === 1 ? "" : "s"} at once (${MAX_JOBS_ENV} to change).
|
|
9466
|
+
`));
|
|
8759
9467
|
const runtimeProbe = () => probeAgentRuntime();
|
|
8760
9468
|
let agentRuntime = await safeProbeAgentRuntime(runtimeProbe, void 0);
|
|
8761
9469
|
if (agentRuntime) {
|
|
@@ -8786,10 +9494,14 @@ Received ${signal}, shutting down...`));
|
|
|
8786
9494
|
});
|
|
8787
9495
|
let consecutiveErrors = 0;
|
|
8788
9496
|
let lastJobAt = 0;
|
|
9497
|
+
let lastConversationAt = 0;
|
|
9498
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
8789
9499
|
do {
|
|
8790
9500
|
let job = null;
|
|
8791
9501
|
try {
|
|
8792
|
-
|
|
9502
|
+
if (inFlight.size < maxJobs) {
|
|
9503
|
+
job = await client.nextJob(machineToken, waitMs, [...inFlight.keys()]);
|
|
9504
|
+
}
|
|
8793
9505
|
consecutiveErrors = 0;
|
|
8794
9506
|
} catch (error) {
|
|
8795
9507
|
consecutiveErrors += 1;
|
|
@@ -8803,12 +9515,21 @@ Received ${signal}, shutting down...`));
|
|
|
8803
9515
|
);
|
|
8804
9516
|
console.error(chalk9.gray(` Reconnecting in ${String(Math.round(delay / 1e3))}s...`));
|
|
8805
9517
|
if (options.once) break;
|
|
8806
|
-
await
|
|
9518
|
+
await sleep(delay);
|
|
8807
9519
|
continue;
|
|
8808
9520
|
}
|
|
9521
|
+
if (job && inFlight.has(job.id)) job = null;
|
|
8809
9522
|
if (job) {
|
|
8810
|
-
|
|
8811
|
-
|
|
9523
|
+
const jobId = job.id;
|
|
9524
|
+
const isConversation = job.type === "epic_request";
|
|
9525
|
+
const work = handleJob(jobContext, job).catch((error) => {
|
|
9526
|
+
console.error(chalk9.red(` Job ${jobId} stopped unexpectedly: ${error instanceof Error ? error.message : String(error)}`));
|
|
9527
|
+
}).finally(() => {
|
|
9528
|
+
inFlight.delete(jobId);
|
|
9529
|
+
lastJobAt = Date.now();
|
|
9530
|
+
if (isConversation) lastConversationAt = lastJobAt;
|
|
9531
|
+
});
|
|
9532
|
+
inFlight.set(jobId, work);
|
|
8812
9533
|
}
|
|
8813
9534
|
if (runtimeHeartbeatDue(Date.now(), lastRuntimeSentAt)) {
|
|
8814
9535
|
const cycle = await runHeartbeatCycle({
|
|
@@ -8825,9 +9546,12 @@ Received ${signal}, shutting down...`));
|
|
|
8825
9546
|
lastRuntimeSentAt = Date.now();
|
|
8826
9547
|
}
|
|
8827
9548
|
}
|
|
8828
|
-
if (options.once)
|
|
9549
|
+
if (options.once) {
|
|
9550
|
+
await Promise.all(inFlight.values());
|
|
9551
|
+
break;
|
|
9552
|
+
}
|
|
8829
9553
|
if (!job) {
|
|
8830
|
-
await
|
|
9554
|
+
await sleep(idleDelay(Date.now(), lastJobAt, lastConversationAt));
|
|
8831
9555
|
}
|
|
8832
9556
|
} while (running);
|
|
8833
9557
|
console.log(chalk9.gray("Agent Runner stopped."));
|
|
@@ -9090,7 +9814,7 @@ machinesCommand.command("rm <id>").description("Remove (revoke) a paired machine
|
|
|
9090
9814
|
// src/index.ts
|
|
9091
9815
|
function resolveCliVersion() {
|
|
9092
9816
|
if (true) {
|
|
9093
|
-
return "1.
|
|
9817
|
+
return "1.20.0";
|
|
9094
9818
|
}
|
|
9095
9819
|
const require2 = createRequire(import.meta.url);
|
|
9096
9820
|
const pkg = require2("../package.json");
|