@locusai/telegram 0.9.9 → 0.9.11
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/bin/telegram.js +3953 -42
- package/package.json +2 -2
package/bin/telegram.js
CHANGED
|
@@ -19454,7 +19454,7 @@ var require_follow_redirects = __commonJS((exports, module) => {
|
|
|
19454
19454
|
});
|
|
19455
19455
|
|
|
19456
19456
|
// src/index.ts
|
|
19457
|
-
var
|
|
19457
|
+
var import_config13 = __toESM(require_config(), 1);
|
|
19458
19458
|
|
|
19459
19459
|
// src/bot.ts
|
|
19460
19460
|
var import_telegraf = __toESM(require_lib3(), 1);
|
|
@@ -19944,9 +19944,9 @@ function executeShellCommand(command, options) {
|
|
|
19944
19944
|
}
|
|
19945
19945
|
|
|
19946
19946
|
// src/timeouts.ts
|
|
19947
|
-
var HANDLER_TIMEOUT =
|
|
19948
|
-
var EXECUTE_DEFAULT_TIMEOUT =
|
|
19949
|
-
var PLAN_TIMEOUT =
|
|
19947
|
+
var HANDLER_TIMEOUT = 3600000;
|
|
19948
|
+
var EXECUTE_DEFAULT_TIMEOUT = 3600000;
|
|
19949
|
+
var PLAN_TIMEOUT = 3600000;
|
|
19950
19950
|
var STREAMING_DEFAULT_TIMEOUT = 3600000;
|
|
19951
19951
|
var GIT_TIMEOUT = 60000;
|
|
19952
19952
|
var DEV_TIMEOUT = 600000;
|
|
@@ -20103,6 +20103,11 @@ var HELP_TEXT = `<b>Locus Bot — Command Center</b>
|
|
|
20103
20103
|
/git <command> — Run whitelisted git/gh commands
|
|
20104
20104
|
/dev <command> — Run lint, typecheck, build, test
|
|
20105
20105
|
|
|
20106
|
+
<b>Worktrees:</b>
|
|
20107
|
+
/worktrees — List agent worktrees
|
|
20108
|
+
/worktree <number> — View worktree details
|
|
20109
|
+
/rmworktree <number|all> — Remove a worktree
|
|
20110
|
+
|
|
20106
20111
|
<b>Status:</b>
|
|
20107
20112
|
/status — Show running processes
|
|
20108
20113
|
/agents — List agent worktrees
|
|
@@ -20268,29 +20273,36 @@ async function runCommand(ctx, executor, config) {
|
|
|
20268
20273
|
outputBuffer += chunk;
|
|
20269
20274
|
});
|
|
20270
20275
|
activeRunKill = kill;
|
|
20271
|
-
|
|
20272
|
-
|
|
20273
|
-
|
|
20274
|
-
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
20276
|
+
done.then(async (result) => {
|
|
20277
|
+
activeRunKill = null;
|
|
20278
|
+
clearInterval(sendInterval);
|
|
20279
|
+
if (outputBuffer.length > lastSentLength) {
|
|
20280
|
+
const remaining = stripAnsi(outputBuffer.slice(lastSentLength));
|
|
20281
|
+
const messages = splitMessage(`<pre>${escapeHtml(remaining)}</pre>`, 4000);
|
|
20282
|
+
for (const msg of messages) {
|
|
20283
|
+
try {
|
|
20284
|
+
await ctx.reply(msg, { parse_mode: "HTML" });
|
|
20285
|
+
} catch {}
|
|
20286
|
+
}
|
|
20281
20287
|
}
|
|
20282
|
-
|
|
20283
|
-
|
|
20284
|
-
|
|
20285
|
-
|
|
20286
|
-
})
|
|
20287
|
-
|
|
20288
|
-
|
|
20289
|
-
|
|
20290
|
-
}
|
|
20291
|
-
|
|
20292
|
-
|
|
20293
|
-
}
|
|
20288
|
+
if (result.exitCode === 0) {
|
|
20289
|
+
await ctx.reply(formatSuccess("Agents finished successfully."), {
|
|
20290
|
+
parse_mode: "HTML"
|
|
20291
|
+
});
|
|
20292
|
+
} else if (result.killed) {
|
|
20293
|
+
await ctx.reply(formatInfo("Agents were stopped."), {
|
|
20294
|
+
parse_mode: "HTML"
|
|
20295
|
+
});
|
|
20296
|
+
} else {
|
|
20297
|
+
await ctx.reply(formatError(`Agents exited with code ${result.exitCode}.`), { parse_mode: "HTML" });
|
|
20298
|
+
}
|
|
20299
|
+
}, async (err) => {
|
|
20300
|
+
activeRunKill = null;
|
|
20301
|
+
clearInterval(sendInterval);
|
|
20302
|
+
try {
|
|
20303
|
+
await ctx.reply(formatError(`Run failed: ${err instanceof Error ? err.message : String(err)}`), { parse_mode: "HTML" });
|
|
20304
|
+
} catch {}
|
|
20305
|
+
});
|
|
20294
20306
|
}
|
|
20295
20307
|
async function stopCommand(ctx, executor) {
|
|
20296
20308
|
console.log("[stop] Stopping all processes");
|
|
@@ -38523,9 +38535,3905 @@ Feedback: <i>${escapeHtml(feedback)}</i>`, { parse_mode: "HTML" });
|
|
|
38523
38535
|
await ctx.reply(formatError(`Failed to reject task: ${err instanceof Error ? err.message : String(err)}`), { parse_mode: "HTML" });
|
|
38524
38536
|
}
|
|
38525
38537
|
}
|
|
38526
|
-
// src/
|
|
38527
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
38538
|
+
// ../sdk/src/core/config.ts
|
|
38528
38539
|
import { join as join2 } from "node:path";
|
|
38540
|
+
var PROVIDER = {
|
|
38541
|
+
CLAUDE: "claude",
|
|
38542
|
+
CODEX: "codex"
|
|
38543
|
+
};
|
|
38544
|
+
var DEFAULT_MODEL = {
|
|
38545
|
+
[PROVIDER.CLAUDE]: "opus",
|
|
38546
|
+
[PROVIDER.CODEX]: "gpt-5.3-codex"
|
|
38547
|
+
};
|
|
38548
|
+
var LOCUS_SCHEMA_BASE_URL = "https://locusai.dev/schemas";
|
|
38549
|
+
var LOCUS_SCHEMAS = {
|
|
38550
|
+
config: `${LOCUS_SCHEMA_BASE_URL}/config.schema.json`,
|
|
38551
|
+
settings: `${LOCUS_SCHEMA_BASE_URL}/settings.schema.json`
|
|
38552
|
+
};
|
|
38553
|
+
var LOCUS_CONFIG = {
|
|
38554
|
+
dir: ".locus",
|
|
38555
|
+
configFile: "config.json",
|
|
38556
|
+
settingsFile: "settings.json",
|
|
38557
|
+
indexFile: "codebase-index.json",
|
|
38558
|
+
contextFile: "LOCUS.md",
|
|
38559
|
+
artifactsDir: "artifacts",
|
|
38560
|
+
documentsDir: "documents",
|
|
38561
|
+
sessionsDir: "sessions",
|
|
38562
|
+
reviewsDir: "reviews",
|
|
38563
|
+
plansDir: "plans",
|
|
38564
|
+
projectDir: "project",
|
|
38565
|
+
projectContextFile: "context.md",
|
|
38566
|
+
projectProgressFile: "progress.md"
|
|
38567
|
+
};
|
|
38568
|
+
function getLocusPath(projectPath, fileName) {
|
|
38569
|
+
if (fileName === "projectContextFile" || fileName === "projectProgressFile") {
|
|
38570
|
+
return join2(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.projectDir, LOCUS_CONFIG[fileName]);
|
|
38571
|
+
}
|
|
38572
|
+
return join2(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
|
|
38573
|
+
}
|
|
38574
|
+
// ../sdk/src/ai/claude-runner.ts
|
|
38575
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
38576
|
+
import { resolve } from "node:path";
|
|
38577
|
+
|
|
38578
|
+
// ../sdk/src/utils/colors.ts
|
|
38579
|
+
var ESC = "\x1B[";
|
|
38580
|
+
var RESET = `${ESC}0m`;
|
|
38581
|
+
var colors = {
|
|
38582
|
+
reset: RESET,
|
|
38583
|
+
bold: `${ESC}1m`,
|
|
38584
|
+
dim: `${ESC}2m`,
|
|
38585
|
+
italic: `${ESC}3m`,
|
|
38586
|
+
underline: `${ESC}4m`,
|
|
38587
|
+
black: `${ESC}30m`,
|
|
38588
|
+
red: `${ESC}31m`,
|
|
38589
|
+
green: `${ESC}32m`,
|
|
38590
|
+
yellow: `${ESC}33m`,
|
|
38591
|
+
blue: `${ESC}34m`,
|
|
38592
|
+
magenta: `${ESC}35m`,
|
|
38593
|
+
cyan: `${ESC}36m`,
|
|
38594
|
+
white: `${ESC}37m`,
|
|
38595
|
+
gray: `${ESC}90m`,
|
|
38596
|
+
brightRed: `${ESC}91m`,
|
|
38597
|
+
brightGreen: `${ESC}92m`,
|
|
38598
|
+
brightYellow: `${ESC}93m`,
|
|
38599
|
+
brightBlue: `${ESC}94m`,
|
|
38600
|
+
brightMagenta: `${ESC}95m`,
|
|
38601
|
+
brightCyan: `${ESC}96m`,
|
|
38602
|
+
brightWhite: `${ESC}97m`,
|
|
38603
|
+
bgBlack: `${ESC}40m`,
|
|
38604
|
+
bgRed: `${ESC}41m`,
|
|
38605
|
+
bgGreen: `${ESC}42m`,
|
|
38606
|
+
bgYellow: `${ESC}43m`,
|
|
38607
|
+
bgBlue: `${ESC}44m`,
|
|
38608
|
+
bgMagenta: `${ESC}45m`,
|
|
38609
|
+
bgCyan: `${ESC}46m`,
|
|
38610
|
+
bgWhite: `${ESC}47m`
|
|
38611
|
+
};
|
|
38612
|
+
var c = {
|
|
38613
|
+
text: (text, ...colorNames) => {
|
|
38614
|
+
const codes = colorNames.map((name) => colors[name]).join("");
|
|
38615
|
+
return `${codes}${text}${RESET}`;
|
|
38616
|
+
},
|
|
38617
|
+
bold: (t) => c.text(t, "bold"),
|
|
38618
|
+
dim: (t) => c.text(t, "dim"),
|
|
38619
|
+
red: (t) => c.text(t, "red"),
|
|
38620
|
+
green: (t) => c.text(t, "green"),
|
|
38621
|
+
yellow: (t) => c.text(t, "yellow"),
|
|
38622
|
+
blue: (t) => c.text(t, "blue"),
|
|
38623
|
+
magenta: (t) => c.text(t, "magenta"),
|
|
38624
|
+
cyan: (t) => c.text(t, "cyan"),
|
|
38625
|
+
gray: (t) => c.text(t, "gray"),
|
|
38626
|
+
white: (t) => c.text(t, "white"),
|
|
38627
|
+
brightBlue: (t) => c.text(t, "brightBlue"),
|
|
38628
|
+
bgBlue: (t) => c.text(t, "bgBlue", "white", "bold"),
|
|
38629
|
+
success: (t) => c.text(t, "green", "bold"),
|
|
38630
|
+
error: (t) => c.text(t, "red", "bold"),
|
|
38631
|
+
warning: (t) => c.text(t, "yellow", "bold"),
|
|
38632
|
+
info: (t) => c.text(t, "cyan", "bold"),
|
|
38633
|
+
primary: (t) => c.text(t, "blue", "bold"),
|
|
38634
|
+
secondary: (t) => c.text(t, "magenta", "bold"),
|
|
38635
|
+
header: (t) => c.text(` ${t} `, "bgBlue", "white", "bold"),
|
|
38636
|
+
step: (t) => c.text(` ${t} `, "bgCyan", "black", "bold"),
|
|
38637
|
+
underline: (t) => c.text(t, "underline")
|
|
38638
|
+
};
|
|
38639
|
+
|
|
38640
|
+
// ../sdk/src/utils/resolve-bin.ts
|
|
38641
|
+
import { existsSync } from "node:fs";
|
|
38642
|
+
import { homedir as homedir2 } from "node:os";
|
|
38643
|
+
import { delimiter, join as join3 } from "node:path";
|
|
38644
|
+
var EXTRA_BIN_DIRS = [
|
|
38645
|
+
join3(homedir2(), ".local", "bin"),
|
|
38646
|
+
join3(homedir2(), ".npm", "bin"),
|
|
38647
|
+
join3(homedir2(), ".npm-global", "bin"),
|
|
38648
|
+
join3(homedir2(), ".yarn", "bin"),
|
|
38649
|
+
"/usr/local/bin"
|
|
38650
|
+
];
|
|
38651
|
+
function getNodeManagerDirs() {
|
|
38652
|
+
const dirs = [];
|
|
38653
|
+
const nvmDir = process.env.NVM_DIR || join3(homedir2(), ".nvm");
|
|
38654
|
+
const nvmCurrent = join3(nvmDir, "current", "bin");
|
|
38655
|
+
if (existsSync(nvmCurrent)) {
|
|
38656
|
+
dirs.push(nvmCurrent);
|
|
38657
|
+
}
|
|
38658
|
+
const fnmDir = process.env.FNM_DIR || join3(homedir2(), ".fnm");
|
|
38659
|
+
const fnmCurrent = join3(fnmDir, "current", "bin");
|
|
38660
|
+
if (existsSync(fnmCurrent)) {
|
|
38661
|
+
dirs.push(fnmCurrent);
|
|
38662
|
+
}
|
|
38663
|
+
return dirs;
|
|
38664
|
+
}
|
|
38665
|
+
function getAugmentedPath() {
|
|
38666
|
+
const currentPath = process.env.PATH || "";
|
|
38667
|
+
const currentDirs = new Set(currentPath.split(delimiter));
|
|
38668
|
+
const extra = [...EXTRA_BIN_DIRS, ...getNodeManagerDirs()].filter((dir) => !currentDirs.has(dir) && existsSync(dir));
|
|
38669
|
+
if (extra.length === 0)
|
|
38670
|
+
return currentPath;
|
|
38671
|
+
return currentPath + delimiter + extra.join(delimiter);
|
|
38672
|
+
}
|
|
38673
|
+
function getAugmentedEnv(overrides = {}) {
|
|
38674
|
+
return {
|
|
38675
|
+
...process.env,
|
|
38676
|
+
...overrides,
|
|
38677
|
+
PATH: getAugmentedPath()
|
|
38678
|
+
};
|
|
38679
|
+
}
|
|
38680
|
+
|
|
38681
|
+
// ../sdk/src/ai/claude-runner.ts
|
|
38682
|
+
var SANDBOX_SETTINGS = JSON.stringify({
|
|
38683
|
+
sandbox: {
|
|
38684
|
+
enabled: true,
|
|
38685
|
+
autoAllow: true,
|
|
38686
|
+
allowUnsandboxedCommands: false
|
|
38687
|
+
}
|
|
38688
|
+
});
|
|
38689
|
+
|
|
38690
|
+
class ClaudeRunner {
|
|
38691
|
+
model;
|
|
38692
|
+
log;
|
|
38693
|
+
projectPath;
|
|
38694
|
+
eventEmitter;
|
|
38695
|
+
currentToolName;
|
|
38696
|
+
activeTools = new Map;
|
|
38697
|
+
activeProcess = null;
|
|
38698
|
+
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log2) {
|
|
38699
|
+
this.model = model;
|
|
38700
|
+
this.log = log2;
|
|
38701
|
+
this.projectPath = resolve(projectPath);
|
|
38702
|
+
}
|
|
38703
|
+
setEventEmitter(emitter) {
|
|
38704
|
+
this.eventEmitter = emitter;
|
|
38705
|
+
}
|
|
38706
|
+
abort() {
|
|
38707
|
+
if (this.activeProcess && !this.activeProcess.killed) {
|
|
38708
|
+
this.activeProcess.kill("SIGTERM");
|
|
38709
|
+
this.activeProcess = null;
|
|
38710
|
+
}
|
|
38711
|
+
}
|
|
38712
|
+
async run(prompt) {
|
|
38713
|
+
const maxRetries = 3;
|
|
38714
|
+
let lastError = null;
|
|
38715
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
38716
|
+
try {
|
|
38717
|
+
return await this.executeRun(prompt);
|
|
38718
|
+
} catch (error48) {
|
|
38719
|
+
const err = error48;
|
|
38720
|
+
lastError = err;
|
|
38721
|
+
const isLastAttempt = attempt === maxRetries;
|
|
38722
|
+
if (!isLastAttempt) {
|
|
38723
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
38724
|
+
console.warn(`Claude CLI attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
|
|
38725
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
38726
|
+
}
|
|
38727
|
+
}
|
|
38728
|
+
}
|
|
38729
|
+
throw lastError || new Error("Claude CLI failed after multiple attempts");
|
|
38730
|
+
}
|
|
38731
|
+
async* runStream(prompt) {
|
|
38732
|
+
const args = [
|
|
38733
|
+
"--dangerously-skip-permissions",
|
|
38734
|
+
"--print",
|
|
38735
|
+
"--verbose",
|
|
38736
|
+
"--output-format",
|
|
38737
|
+
"stream-json",
|
|
38738
|
+
"--include-partial-messages",
|
|
38739
|
+
"--model",
|
|
38740
|
+
this.model,
|
|
38741
|
+
"--settings",
|
|
38742
|
+
SANDBOX_SETTINGS
|
|
38743
|
+
];
|
|
38744
|
+
const env = getAugmentedEnv({
|
|
38745
|
+
FORCE_COLOR: "1",
|
|
38746
|
+
TERM: "xterm-256color"
|
|
38747
|
+
});
|
|
38748
|
+
this.eventEmitter?.emitSessionStarted({
|
|
38749
|
+
model: this.model,
|
|
38750
|
+
provider: "claude"
|
|
38751
|
+
});
|
|
38752
|
+
this.eventEmitter?.emitPromptSubmitted(prompt, prompt.length > 500);
|
|
38753
|
+
const claude = spawn2("claude", args, {
|
|
38754
|
+
cwd: this.projectPath,
|
|
38755
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38756
|
+
env
|
|
38757
|
+
});
|
|
38758
|
+
this.activeProcess = claude;
|
|
38759
|
+
let buffer = "";
|
|
38760
|
+
let stderrBuffer = "";
|
|
38761
|
+
let resolveChunk = null;
|
|
38762
|
+
const chunkQueue = [];
|
|
38763
|
+
let processEnded = false;
|
|
38764
|
+
let errorMessage = "";
|
|
38765
|
+
let finalContent = "";
|
|
38766
|
+
let isThinking = false;
|
|
38767
|
+
const enqueueChunk = (chunk) => {
|
|
38768
|
+
this.emitEventForChunk(chunk, isThinking);
|
|
38769
|
+
if (chunk.type === "thinking") {
|
|
38770
|
+
isThinking = true;
|
|
38771
|
+
} else if (chunk.type === "text_delta" || chunk.type === "tool_use") {
|
|
38772
|
+
if (isThinking) {
|
|
38773
|
+
this.eventEmitter?.emitThinkingStoped();
|
|
38774
|
+
isThinking = false;
|
|
38775
|
+
}
|
|
38776
|
+
}
|
|
38777
|
+
if (chunk.type === "text_delta") {
|
|
38778
|
+
finalContent += chunk.content;
|
|
38779
|
+
}
|
|
38780
|
+
if (resolveChunk) {
|
|
38781
|
+
const resolve2 = resolveChunk;
|
|
38782
|
+
resolveChunk = null;
|
|
38783
|
+
resolve2(chunk);
|
|
38784
|
+
} else {
|
|
38785
|
+
chunkQueue.push(chunk);
|
|
38786
|
+
}
|
|
38787
|
+
};
|
|
38788
|
+
const signalEnd = () => {
|
|
38789
|
+
processEnded = true;
|
|
38790
|
+
if (resolveChunk) {
|
|
38791
|
+
resolveChunk(null);
|
|
38792
|
+
resolveChunk = null;
|
|
38793
|
+
}
|
|
38794
|
+
};
|
|
38795
|
+
claude.stdout.on("data", (data) => {
|
|
38796
|
+
buffer += data.toString();
|
|
38797
|
+
const lines = buffer.split(`
|
|
38798
|
+
`);
|
|
38799
|
+
buffer = lines.pop() || "";
|
|
38800
|
+
for (const line of lines) {
|
|
38801
|
+
const chunk = this.parseStreamLineToChunk(line);
|
|
38802
|
+
if (chunk) {
|
|
38803
|
+
enqueueChunk(chunk);
|
|
38804
|
+
}
|
|
38805
|
+
}
|
|
38806
|
+
});
|
|
38807
|
+
claude.stderr.on("data", (data) => {
|
|
38808
|
+
const chunk = data.toString();
|
|
38809
|
+
stderrBuffer += chunk;
|
|
38810
|
+
const lines = stderrBuffer.split(`
|
|
38811
|
+
`);
|
|
38812
|
+
stderrBuffer = lines.pop() || "";
|
|
38813
|
+
for (const line of lines) {
|
|
38814
|
+
if (!this.shouldSuppressLine(line)) {
|
|
38815
|
+
process.stderr.write(`${line}
|
|
38816
|
+
`);
|
|
38817
|
+
}
|
|
38818
|
+
}
|
|
38819
|
+
});
|
|
38820
|
+
claude.on("error", (err) => {
|
|
38821
|
+
errorMessage = `Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`;
|
|
38822
|
+
this.eventEmitter?.emitErrorOccurred(errorMessage, "SPAWN_ERROR");
|
|
38823
|
+
signalEnd();
|
|
38824
|
+
});
|
|
38825
|
+
claude.on("close", (code) => {
|
|
38826
|
+
this.activeProcess = null;
|
|
38827
|
+
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
38828
|
+
process.stderr.write(`${stderrBuffer}
|
|
38829
|
+
`);
|
|
38830
|
+
}
|
|
38831
|
+
if (code !== 0 && !errorMessage) {
|
|
38832
|
+
errorMessage = this.createExecutionError(code, stderrBuffer).message;
|
|
38833
|
+
this.eventEmitter?.emitErrorOccurred(errorMessage, `EXIT_${code}`);
|
|
38834
|
+
}
|
|
38835
|
+
signalEnd();
|
|
38836
|
+
});
|
|
38837
|
+
claude.stdin.write(prompt);
|
|
38838
|
+
claude.stdin.end();
|
|
38839
|
+
while (true) {
|
|
38840
|
+
if (chunkQueue.length > 0) {
|
|
38841
|
+
const chunk = chunkQueue.shift();
|
|
38842
|
+
if (chunk)
|
|
38843
|
+
yield chunk;
|
|
38844
|
+
} else if (processEnded) {
|
|
38845
|
+
if (errorMessage) {
|
|
38846
|
+
yield { type: "error", error: errorMessage };
|
|
38847
|
+
this.eventEmitter?.emitSessionEnded(false);
|
|
38848
|
+
} else {
|
|
38849
|
+
if (finalContent) {
|
|
38850
|
+
this.eventEmitter?.emitResponseCompleted(finalContent);
|
|
38851
|
+
}
|
|
38852
|
+
this.eventEmitter?.emitSessionEnded(true);
|
|
38853
|
+
}
|
|
38854
|
+
break;
|
|
38855
|
+
} else {
|
|
38856
|
+
const chunk = await new Promise((resolve2) => {
|
|
38857
|
+
resolveChunk = resolve2;
|
|
38858
|
+
});
|
|
38859
|
+
if (chunk === null) {
|
|
38860
|
+
if (errorMessage) {
|
|
38861
|
+
yield { type: "error", error: errorMessage };
|
|
38862
|
+
this.eventEmitter?.emitSessionEnded(false);
|
|
38863
|
+
} else {
|
|
38864
|
+
if (finalContent) {
|
|
38865
|
+
this.eventEmitter?.emitResponseCompleted(finalContent);
|
|
38866
|
+
}
|
|
38867
|
+
this.eventEmitter?.emitSessionEnded(true);
|
|
38868
|
+
}
|
|
38869
|
+
break;
|
|
38870
|
+
}
|
|
38871
|
+
yield chunk;
|
|
38872
|
+
}
|
|
38873
|
+
}
|
|
38874
|
+
}
|
|
38875
|
+
emitEventForChunk(chunk, isThinking) {
|
|
38876
|
+
if (!this.eventEmitter)
|
|
38877
|
+
return;
|
|
38878
|
+
switch (chunk.type) {
|
|
38879
|
+
case "text_delta":
|
|
38880
|
+
this.eventEmitter.emitTextDelta(chunk.content);
|
|
38881
|
+
break;
|
|
38882
|
+
case "tool_use":
|
|
38883
|
+
if (this.currentToolName) {
|
|
38884
|
+
this.eventEmitter.emitToolCompleted(this.currentToolName);
|
|
38885
|
+
}
|
|
38886
|
+
this.currentToolName = chunk.tool;
|
|
38887
|
+
this.eventEmitter.emitToolStarted(chunk.tool, chunk.id);
|
|
38888
|
+
break;
|
|
38889
|
+
case "thinking":
|
|
38890
|
+
if (!isThinking) {
|
|
38891
|
+
this.eventEmitter.emitThinkingStarted(chunk.content);
|
|
38892
|
+
}
|
|
38893
|
+
break;
|
|
38894
|
+
case "result":
|
|
38895
|
+
if (this.currentToolName) {
|
|
38896
|
+
this.eventEmitter.emitToolCompleted(this.currentToolName);
|
|
38897
|
+
this.currentToolName = undefined;
|
|
38898
|
+
}
|
|
38899
|
+
break;
|
|
38900
|
+
case "error":
|
|
38901
|
+
this.eventEmitter.emitErrorOccurred(chunk.error);
|
|
38902
|
+
break;
|
|
38903
|
+
}
|
|
38904
|
+
}
|
|
38905
|
+
parseStreamLineToChunk(line) {
|
|
38906
|
+
if (!line.trim())
|
|
38907
|
+
return null;
|
|
38908
|
+
try {
|
|
38909
|
+
const item = JSON.parse(line);
|
|
38910
|
+
return this.processStreamItemToChunk(item);
|
|
38911
|
+
} catch {
|
|
38912
|
+
return null;
|
|
38913
|
+
}
|
|
38914
|
+
}
|
|
38915
|
+
processStreamItemToChunk(item) {
|
|
38916
|
+
if (item.type === "result") {
|
|
38917
|
+
return { type: "result", content: item.result || "" };
|
|
38918
|
+
}
|
|
38919
|
+
if (item.type === "stream_event" && item.event) {
|
|
38920
|
+
return this.handleEventToChunk(item.event);
|
|
38921
|
+
}
|
|
38922
|
+
return null;
|
|
38923
|
+
}
|
|
38924
|
+
handleEventToChunk(event) {
|
|
38925
|
+
const { type, delta, content_block, index } = event;
|
|
38926
|
+
if (type === "content_block_delta" && delta?.type === "text_delta") {
|
|
38927
|
+
return { type: "text_delta", content: delta.text || "" };
|
|
38928
|
+
}
|
|
38929
|
+
if (type === "content_block_delta" && delta?.type === "input_json_delta" && delta.partial_json !== undefined && index !== undefined) {
|
|
38930
|
+
const activeTool = this.activeTools.get(index);
|
|
38931
|
+
if (activeTool) {
|
|
38932
|
+
activeTool.parameterJson += delta.partial_json;
|
|
38933
|
+
}
|
|
38934
|
+
return null;
|
|
38935
|
+
}
|
|
38936
|
+
if (type === "content_block_start" && content_block) {
|
|
38937
|
+
if (content_block.type === "tool_use" && content_block.name) {
|
|
38938
|
+
if (index !== undefined) {
|
|
38939
|
+
this.activeTools.set(index, {
|
|
38940
|
+
name: content_block.name,
|
|
38941
|
+
id: content_block.id,
|
|
38942
|
+
index,
|
|
38943
|
+
parameterJson: "",
|
|
38944
|
+
startTime: Date.now()
|
|
38945
|
+
});
|
|
38946
|
+
}
|
|
38947
|
+
return {
|
|
38948
|
+
type: "tool_use",
|
|
38949
|
+
tool: content_block.name,
|
|
38950
|
+
id: content_block.id
|
|
38951
|
+
};
|
|
38952
|
+
}
|
|
38953
|
+
if (content_block.type === "thinking") {
|
|
38954
|
+
return { type: "thinking" };
|
|
38955
|
+
}
|
|
38956
|
+
}
|
|
38957
|
+
if (type === "content_block_stop" && index !== undefined) {
|
|
38958
|
+
const activeTool = this.activeTools.get(index);
|
|
38959
|
+
if (activeTool?.parameterJson) {
|
|
38960
|
+
try {
|
|
38961
|
+
const parameters = JSON.parse(activeTool.parameterJson);
|
|
38962
|
+
return {
|
|
38963
|
+
type: "tool_parameters",
|
|
38964
|
+
tool: activeTool.name,
|
|
38965
|
+
id: activeTool.id,
|
|
38966
|
+
parameters
|
|
38967
|
+
};
|
|
38968
|
+
} catch {}
|
|
38969
|
+
}
|
|
38970
|
+
return null;
|
|
38971
|
+
}
|
|
38972
|
+
return null;
|
|
38973
|
+
}
|
|
38974
|
+
executeRun(prompt) {
|
|
38975
|
+
return new Promise((resolve2, reject) => {
|
|
38976
|
+
const args = [
|
|
38977
|
+
"--dangerously-skip-permissions",
|
|
38978
|
+
"--print",
|
|
38979
|
+
"--verbose",
|
|
38980
|
+
"--output-format",
|
|
38981
|
+
"stream-json",
|
|
38982
|
+
"--include-partial-messages",
|
|
38983
|
+
"--model",
|
|
38984
|
+
this.model,
|
|
38985
|
+
"--settings",
|
|
38986
|
+
SANDBOX_SETTINGS
|
|
38987
|
+
];
|
|
38988
|
+
const env = getAugmentedEnv({
|
|
38989
|
+
FORCE_COLOR: "1",
|
|
38990
|
+
TERM: "xterm-256color"
|
|
38991
|
+
});
|
|
38992
|
+
const claude = spawn2("claude", args, {
|
|
38993
|
+
cwd: this.projectPath,
|
|
38994
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38995
|
+
env
|
|
38996
|
+
});
|
|
38997
|
+
this.activeProcess = claude;
|
|
38998
|
+
let finalResult = "";
|
|
38999
|
+
let errorOutput = "";
|
|
39000
|
+
let buffer = "";
|
|
39001
|
+
let stderrBuffer = "";
|
|
39002
|
+
claude.stdout.on("data", (data) => {
|
|
39003
|
+
buffer += data.toString();
|
|
39004
|
+
const lines = buffer.split(`
|
|
39005
|
+
`);
|
|
39006
|
+
buffer = lines.pop() || "";
|
|
39007
|
+
for (const line of lines) {
|
|
39008
|
+
const result = this.handleStreamLine(line);
|
|
39009
|
+
if (result)
|
|
39010
|
+
finalResult = result;
|
|
39011
|
+
}
|
|
39012
|
+
});
|
|
39013
|
+
claude.stderr.on("data", (data) => {
|
|
39014
|
+
const chunk = data.toString();
|
|
39015
|
+
errorOutput += chunk;
|
|
39016
|
+
stderrBuffer += chunk;
|
|
39017
|
+
const lines = stderrBuffer.split(`
|
|
39018
|
+
`);
|
|
39019
|
+
stderrBuffer = lines.pop() || "";
|
|
39020
|
+
for (const line of lines) {
|
|
39021
|
+
if (!this.shouldSuppressLine(line)) {
|
|
39022
|
+
process.stderr.write(`${line}
|
|
39023
|
+
`);
|
|
39024
|
+
}
|
|
39025
|
+
}
|
|
39026
|
+
});
|
|
39027
|
+
claude.on("error", (err) => {
|
|
39028
|
+
reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
|
|
39029
|
+
});
|
|
39030
|
+
claude.on("close", (code) => {
|
|
39031
|
+
this.activeProcess = null;
|
|
39032
|
+
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
39033
|
+
process.stderr.write(`${stderrBuffer}
|
|
39034
|
+
`);
|
|
39035
|
+
}
|
|
39036
|
+
process.stdout.write(`
|
|
39037
|
+
`);
|
|
39038
|
+
if (code === 0) {
|
|
39039
|
+
resolve2(finalResult);
|
|
39040
|
+
} else {
|
|
39041
|
+
reject(this.createExecutionError(code, errorOutput));
|
|
39042
|
+
}
|
|
39043
|
+
});
|
|
39044
|
+
claude.stdin.write(prompt);
|
|
39045
|
+
claude.stdin.end();
|
|
39046
|
+
});
|
|
39047
|
+
}
|
|
39048
|
+
handleStreamLine(line) {
|
|
39049
|
+
if (!line.trim())
|
|
39050
|
+
return null;
|
|
39051
|
+
try {
|
|
39052
|
+
const item = JSON.parse(line);
|
|
39053
|
+
return this.processStreamItem(item);
|
|
39054
|
+
} catch {
|
|
39055
|
+
return null;
|
|
39056
|
+
}
|
|
39057
|
+
}
|
|
39058
|
+
processStreamItem(item) {
|
|
39059
|
+
if (item.type === "result") {
|
|
39060
|
+
return item.result || "";
|
|
39061
|
+
}
|
|
39062
|
+
if (item.type === "stream_event" && item.event) {
|
|
39063
|
+
this.handleEvent(item.event);
|
|
39064
|
+
}
|
|
39065
|
+
return null;
|
|
39066
|
+
}
|
|
39067
|
+
handleEvent(event) {
|
|
39068
|
+
const { type, content_block } = event;
|
|
39069
|
+
if (type === "content_block_start" && content_block) {
|
|
39070
|
+
if (content_block.type === "tool_use" && content_block.name) {
|
|
39071
|
+
this.log?.(`
|
|
39072
|
+
${c.primary("[Claude]")} ${c.bold(`Running ${content_block.name}...`)}
|
|
39073
|
+
`, "info");
|
|
39074
|
+
}
|
|
39075
|
+
}
|
|
39076
|
+
}
|
|
39077
|
+
shouldSuppressLine(line) {
|
|
39078
|
+
const infoLogRegex = /^\[\d{2}:\d{2}:\d{2}\]\s\[.*?\]\sℹ\s*$/;
|
|
39079
|
+
return infoLogRegex.test(line.trim());
|
|
39080
|
+
}
|
|
39081
|
+
createExecutionError(code, detail) {
|
|
39082
|
+
const errorMsg = detail.trim();
|
|
39083
|
+
const message = errorMsg ? `Claude CLI error (exit code ${code}): ${errorMsg}` : `Claude CLI exited with code ${code}. Please ensure the Claude CLI is installed and you are logged in.`;
|
|
39084
|
+
return new Error(message);
|
|
39085
|
+
}
|
|
39086
|
+
}
|
|
39087
|
+
|
|
39088
|
+
// ../sdk/src/ai/codex-runner.ts
|
|
39089
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
39090
|
+
import { randomUUID } from "node:crypto";
|
|
39091
|
+
import { existsSync as existsSync2, readFileSync, unlinkSync } from "node:fs";
|
|
39092
|
+
import { tmpdir } from "node:os";
|
|
39093
|
+
import { join as join4 } from "node:path";
|
|
39094
|
+
class CodexRunner {
|
|
39095
|
+
projectPath;
|
|
39096
|
+
model;
|
|
39097
|
+
log;
|
|
39098
|
+
activeProcess = null;
|
|
39099
|
+
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log2) {
|
|
39100
|
+
this.projectPath = projectPath;
|
|
39101
|
+
this.model = model;
|
|
39102
|
+
this.log = log2;
|
|
39103
|
+
}
|
|
39104
|
+
abort() {
|
|
39105
|
+
if (this.activeProcess && !this.activeProcess.killed) {
|
|
39106
|
+
this.activeProcess.kill("SIGTERM");
|
|
39107
|
+
this.activeProcess = null;
|
|
39108
|
+
}
|
|
39109
|
+
}
|
|
39110
|
+
async run(prompt) {
|
|
39111
|
+
const maxRetries = 3;
|
|
39112
|
+
let lastError = null;
|
|
39113
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
39114
|
+
try {
|
|
39115
|
+
return await this.executeRun(prompt);
|
|
39116
|
+
} catch (error48) {
|
|
39117
|
+
lastError = error48;
|
|
39118
|
+
if (attempt < maxRetries) {
|
|
39119
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
39120
|
+
console.warn(`Codex CLI attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`);
|
|
39121
|
+
await this.sleep(delay);
|
|
39122
|
+
}
|
|
39123
|
+
}
|
|
39124
|
+
}
|
|
39125
|
+
throw lastError || new Error("Codex CLI failed after multiple attempts");
|
|
39126
|
+
}
|
|
39127
|
+
async* runStream(prompt) {
|
|
39128
|
+
const outputPath = join4(tmpdir(), `locus-codex-${randomUUID()}.txt`);
|
|
39129
|
+
const args = this.buildArgs(outputPath);
|
|
39130
|
+
const codex = spawn3("codex", args, {
|
|
39131
|
+
cwd: this.projectPath,
|
|
39132
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
39133
|
+
env: getAugmentedEnv(),
|
|
39134
|
+
shell: false
|
|
39135
|
+
});
|
|
39136
|
+
this.activeProcess = codex;
|
|
39137
|
+
let resolveChunk = null;
|
|
39138
|
+
const chunkQueue = [];
|
|
39139
|
+
let processEnded = false;
|
|
39140
|
+
let errorMessage = "";
|
|
39141
|
+
let finalOutput = "";
|
|
39142
|
+
const enqueueChunk = (chunk) => {
|
|
39143
|
+
if (resolveChunk) {
|
|
39144
|
+
const resolve2 = resolveChunk;
|
|
39145
|
+
resolveChunk = null;
|
|
39146
|
+
resolve2(chunk);
|
|
39147
|
+
} else {
|
|
39148
|
+
chunkQueue.push(chunk);
|
|
39149
|
+
}
|
|
39150
|
+
};
|
|
39151
|
+
const signalEnd = () => {
|
|
39152
|
+
processEnded = true;
|
|
39153
|
+
if (resolveChunk) {
|
|
39154
|
+
resolveChunk(null);
|
|
39155
|
+
resolveChunk = null;
|
|
39156
|
+
}
|
|
39157
|
+
};
|
|
39158
|
+
const processOutput = (data) => {
|
|
39159
|
+
const msg = data.toString();
|
|
39160
|
+
finalOutput += msg;
|
|
39161
|
+
for (const rawLine of msg.split(`
|
|
39162
|
+
`)) {
|
|
39163
|
+
const line = rawLine.trim();
|
|
39164
|
+
if (!line)
|
|
39165
|
+
continue;
|
|
39166
|
+
if (/^thinking\b/i.test(line)) {
|
|
39167
|
+
enqueueChunk({ type: "thinking", content: line });
|
|
39168
|
+
} else if (/^[→•✓]/.test(line) || /^Plan update\b/.test(line)) {
|
|
39169
|
+
enqueueChunk({
|
|
39170
|
+
type: "tool_use",
|
|
39171
|
+
tool: line.replace(/^[→•✓]\s*/, "")
|
|
39172
|
+
});
|
|
39173
|
+
} else if (this.shouldDisplay(line)) {
|
|
39174
|
+
enqueueChunk({ type: "text_delta", content: `${line}
|
|
39175
|
+
` });
|
|
39176
|
+
}
|
|
39177
|
+
}
|
|
39178
|
+
};
|
|
39179
|
+
codex.stdout.on("data", processOutput);
|
|
39180
|
+
codex.stderr.on("data", processOutput);
|
|
39181
|
+
codex.on("error", (err) => {
|
|
39182
|
+
errorMessage = `Failed to start Codex CLI: ${err.message}. Ensure 'codex' is installed and available in PATH.`;
|
|
39183
|
+
signalEnd();
|
|
39184
|
+
});
|
|
39185
|
+
codex.on("close", (code) => {
|
|
39186
|
+
this.activeProcess = null;
|
|
39187
|
+
this.cleanupTempFile(outputPath);
|
|
39188
|
+
if (code === 0) {
|
|
39189
|
+
const result = this.readOutput(outputPath, finalOutput);
|
|
39190
|
+
enqueueChunk({ type: "result", content: result });
|
|
39191
|
+
} else if (!errorMessage) {
|
|
39192
|
+
errorMessage = this.createErrorFromOutput(code, finalOutput).message;
|
|
39193
|
+
}
|
|
39194
|
+
signalEnd();
|
|
39195
|
+
});
|
|
39196
|
+
codex.stdin.write(prompt);
|
|
39197
|
+
codex.stdin.end();
|
|
39198
|
+
while (true) {
|
|
39199
|
+
if (chunkQueue.length > 0) {
|
|
39200
|
+
const chunk = chunkQueue.shift();
|
|
39201
|
+
if (chunk)
|
|
39202
|
+
yield chunk;
|
|
39203
|
+
} else if (processEnded) {
|
|
39204
|
+
if (errorMessage) {
|
|
39205
|
+
yield { type: "error", error: errorMessage };
|
|
39206
|
+
}
|
|
39207
|
+
break;
|
|
39208
|
+
} else {
|
|
39209
|
+
const chunk = await new Promise((resolve2) => {
|
|
39210
|
+
resolveChunk = resolve2;
|
|
39211
|
+
});
|
|
39212
|
+
if (chunk === null) {
|
|
39213
|
+
if (errorMessage) {
|
|
39214
|
+
yield { type: "error", error: errorMessage };
|
|
39215
|
+
}
|
|
39216
|
+
break;
|
|
39217
|
+
}
|
|
39218
|
+
yield chunk;
|
|
39219
|
+
}
|
|
39220
|
+
}
|
|
39221
|
+
}
|
|
39222
|
+
executeRun(prompt) {
|
|
39223
|
+
return new Promise((resolve2, reject) => {
|
|
39224
|
+
const outputPath = join4(tmpdir(), `locus-codex-${randomUUID()}.txt`);
|
|
39225
|
+
const args = this.buildArgs(outputPath);
|
|
39226
|
+
const codex = spawn3("codex", args, {
|
|
39227
|
+
cwd: this.projectPath,
|
|
39228
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
39229
|
+
env: getAugmentedEnv(),
|
|
39230
|
+
shell: false
|
|
39231
|
+
});
|
|
39232
|
+
this.activeProcess = codex;
|
|
39233
|
+
let output = "";
|
|
39234
|
+
let errorOutput = "";
|
|
39235
|
+
const handleOutput = (data) => {
|
|
39236
|
+
const msg = data.toString();
|
|
39237
|
+
output += msg;
|
|
39238
|
+
this.streamToConsole(msg);
|
|
39239
|
+
};
|
|
39240
|
+
codex.stdout.on("data", handleOutput);
|
|
39241
|
+
codex.stderr.on("data", (data) => {
|
|
39242
|
+
const msg = data.toString();
|
|
39243
|
+
errorOutput += msg;
|
|
39244
|
+
this.streamToConsole(msg);
|
|
39245
|
+
});
|
|
39246
|
+
codex.on("error", (err) => {
|
|
39247
|
+
reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
|
|
39248
|
+
});
|
|
39249
|
+
codex.on("close", (code) => {
|
|
39250
|
+
this.activeProcess = null;
|
|
39251
|
+
this.cleanupTempFile(outputPath);
|
|
39252
|
+
if (code === 0) {
|
|
39253
|
+
resolve2(this.readOutput(outputPath, output));
|
|
39254
|
+
} else {
|
|
39255
|
+
reject(this.createErrorFromOutput(code, errorOutput));
|
|
39256
|
+
}
|
|
39257
|
+
});
|
|
39258
|
+
codex.stdin.write(prompt);
|
|
39259
|
+
codex.stdin.end();
|
|
39260
|
+
});
|
|
39261
|
+
}
|
|
39262
|
+
buildArgs(outputPath) {
|
|
39263
|
+
const args = [
|
|
39264
|
+
"exec",
|
|
39265
|
+
"--sandbox",
|
|
39266
|
+
"workspace-write",
|
|
39267
|
+
"--skip-git-repo-check",
|
|
39268
|
+
"--output-last-message",
|
|
39269
|
+
outputPath
|
|
39270
|
+
];
|
|
39271
|
+
if (this.model) {
|
|
39272
|
+
args.push("--model", this.model);
|
|
39273
|
+
}
|
|
39274
|
+
args.push("-");
|
|
39275
|
+
return args;
|
|
39276
|
+
}
|
|
39277
|
+
streamToConsole(chunk) {
|
|
39278
|
+
for (const rawLine of chunk.split(`
|
|
39279
|
+
`)) {
|
|
39280
|
+
const line = rawLine.trim();
|
|
39281
|
+
if (line && this.shouldDisplay(line)) {
|
|
39282
|
+
const formattedLine = "[Codex]: ".concat(line.replace(/\*/g, ""));
|
|
39283
|
+
this.log?.(formattedLine, "info");
|
|
39284
|
+
}
|
|
39285
|
+
}
|
|
39286
|
+
}
|
|
39287
|
+
shouldDisplay(line) {
|
|
39288
|
+
return [
|
|
39289
|
+
/^thinking\b/,
|
|
39290
|
+
/^\*\*/,
|
|
39291
|
+
/^Plan update\b/,
|
|
39292
|
+
/^[→•✓]/
|
|
39293
|
+
].some((pattern) => pattern.test(line));
|
|
39294
|
+
}
|
|
39295
|
+
readOutput(outputPath, fallback) {
|
|
39296
|
+
if (existsSync2(outputPath)) {
|
|
39297
|
+
try {
|
|
39298
|
+
const text = readFileSync(outputPath, "utf-8").trim();
|
|
39299
|
+
if (text)
|
|
39300
|
+
return text;
|
|
39301
|
+
} catch {}
|
|
39302
|
+
}
|
|
39303
|
+
return fallback.trim();
|
|
39304
|
+
}
|
|
39305
|
+
createErrorFromOutput(code, errorOutput) {
|
|
39306
|
+
const detail = errorOutput.trim();
|
|
39307
|
+
const message = detail ? `Codex CLI error (exit code ${code}): ${detail}` : `Codex CLI exited with code ${code}. ` + `Ensure Codex CLI is installed and you are logged in.`;
|
|
39308
|
+
return new Error(message);
|
|
39309
|
+
}
|
|
39310
|
+
cleanupTempFile(path) {
|
|
39311
|
+
try {
|
|
39312
|
+
if (existsSync2(path))
|
|
39313
|
+
unlinkSync(path);
|
|
39314
|
+
} catch {}
|
|
39315
|
+
}
|
|
39316
|
+
sleep(ms) {
|
|
39317
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
39318
|
+
}
|
|
39319
|
+
}
|
|
39320
|
+
|
|
39321
|
+
// ../sdk/src/ai/factory.ts
|
|
39322
|
+
function createAiRunner(provider, config2) {
|
|
39323
|
+
const resolvedProvider = provider ?? PROVIDER.CLAUDE;
|
|
39324
|
+
const model = config2.model ?? DEFAULT_MODEL[resolvedProvider];
|
|
39325
|
+
switch (resolvedProvider) {
|
|
39326
|
+
case PROVIDER.CODEX:
|
|
39327
|
+
return new CodexRunner(config2.projectPath, model, config2.log);
|
|
39328
|
+
default:
|
|
39329
|
+
return new ClaudeRunner(config2.projectPath, model, config2.log);
|
|
39330
|
+
}
|
|
39331
|
+
}
|
|
39332
|
+
|
|
39333
|
+
// ../sdk/src/git/git-utils.ts
|
|
39334
|
+
import { execFileSync } from "node:child_process";
|
|
39335
|
+
function isGitAvailable() {
|
|
39336
|
+
try {
|
|
39337
|
+
execFileSync("git", ["--version"], {
|
|
39338
|
+
encoding: "utf-8",
|
|
39339
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39340
|
+
});
|
|
39341
|
+
return true;
|
|
39342
|
+
} catch {
|
|
39343
|
+
return false;
|
|
39344
|
+
}
|
|
39345
|
+
}
|
|
39346
|
+
function isGhAvailable(projectPath) {
|
|
39347
|
+
try {
|
|
39348
|
+
execFileSync("gh", ["auth", "status"], {
|
|
39349
|
+
cwd: projectPath,
|
|
39350
|
+
encoding: "utf-8",
|
|
39351
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39352
|
+
});
|
|
39353
|
+
return true;
|
|
39354
|
+
} catch {
|
|
39355
|
+
return false;
|
|
39356
|
+
}
|
|
39357
|
+
}
|
|
39358
|
+
function getGhUsername() {
|
|
39359
|
+
try {
|
|
39360
|
+
const output = execFileSync("gh", ["api", "user", "--jq", ".login"], {
|
|
39361
|
+
encoding: "utf-8",
|
|
39362
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39363
|
+
}).trim();
|
|
39364
|
+
return output || null;
|
|
39365
|
+
} catch {
|
|
39366
|
+
return null;
|
|
39367
|
+
}
|
|
39368
|
+
}
|
|
39369
|
+
function detectRemoteProvider(projectPath) {
|
|
39370
|
+
const url3 = getRemoteUrl(projectPath);
|
|
39371
|
+
if (!url3)
|
|
39372
|
+
return "unknown";
|
|
39373
|
+
if (url3.includes("github.com"))
|
|
39374
|
+
return "github";
|
|
39375
|
+
if (url3.includes("gitlab.com") || url3.includes("gitlab"))
|
|
39376
|
+
return "gitlab";
|
|
39377
|
+
if (url3.includes("bitbucket.org"))
|
|
39378
|
+
return "bitbucket";
|
|
39379
|
+
return "unknown";
|
|
39380
|
+
}
|
|
39381
|
+
function getRemoteUrl(projectPath, remote = "origin") {
|
|
39382
|
+
try {
|
|
39383
|
+
return execFileSync("git", ["remote", "get-url", remote], {
|
|
39384
|
+
cwd: projectPath,
|
|
39385
|
+
encoding: "utf-8",
|
|
39386
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39387
|
+
}).trim();
|
|
39388
|
+
} catch {
|
|
39389
|
+
return null;
|
|
39390
|
+
}
|
|
39391
|
+
}
|
|
39392
|
+
function getCurrentBranch(projectPath) {
|
|
39393
|
+
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
39394
|
+
cwd: projectPath,
|
|
39395
|
+
encoding: "utf-8",
|
|
39396
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39397
|
+
}).trim();
|
|
39398
|
+
}
|
|
39399
|
+
function getDefaultBranch(projectPath, remote = "origin") {
|
|
39400
|
+
try {
|
|
39401
|
+
const ref = execFileSync("git", ["symbolic-ref", `refs/remotes/${remote}/HEAD`], {
|
|
39402
|
+
cwd: projectPath,
|
|
39403
|
+
encoding: "utf-8",
|
|
39404
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39405
|
+
}).trim();
|
|
39406
|
+
return ref.replace(`refs/remotes/${remote}/`, "");
|
|
39407
|
+
} catch {
|
|
39408
|
+
for (const candidate of ["main", "master"]) {
|
|
39409
|
+
try {
|
|
39410
|
+
execFileSync("git", ["ls-remote", "--exit-code", "--heads", remote, candidate], {
|
|
39411
|
+
cwd: projectPath,
|
|
39412
|
+
encoding: "utf-8",
|
|
39413
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39414
|
+
});
|
|
39415
|
+
return candidate;
|
|
39416
|
+
} catch {}
|
|
39417
|
+
}
|
|
39418
|
+
try {
|
|
39419
|
+
return getCurrentBranch(projectPath);
|
|
39420
|
+
} catch {
|
|
39421
|
+
return "main";
|
|
39422
|
+
}
|
|
39423
|
+
}
|
|
39424
|
+
}
|
|
39425
|
+
|
|
39426
|
+
// ../sdk/src/git/pr-service.ts
|
|
39427
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
39428
|
+
class PrService {
|
|
39429
|
+
projectPath;
|
|
39430
|
+
log;
|
|
39431
|
+
constructor(projectPath, log2) {
|
|
39432
|
+
this.projectPath = projectPath;
|
|
39433
|
+
this.log = log2;
|
|
39434
|
+
}
|
|
39435
|
+
createPr(options) {
|
|
39436
|
+
const {
|
|
39437
|
+
task: task2,
|
|
39438
|
+
branch,
|
|
39439
|
+
baseBranch: requestedBaseBranch,
|
|
39440
|
+
agentId,
|
|
39441
|
+
summary
|
|
39442
|
+
} = options;
|
|
39443
|
+
const provider = detectRemoteProvider(this.projectPath);
|
|
39444
|
+
if (provider !== "github") {
|
|
39445
|
+
throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
|
|
39446
|
+
}
|
|
39447
|
+
if (!isGhAvailable(this.projectPath)) {
|
|
39448
|
+
throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
|
|
39449
|
+
}
|
|
39450
|
+
const title = `[Locus] ${task2.title}`;
|
|
39451
|
+
const body = this.buildPrBody(task2, agentId, summary);
|
|
39452
|
+
const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
|
|
39453
|
+
this.validateCreatePrInputs(baseBranch, branch);
|
|
39454
|
+
this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
|
|
39455
|
+
const output = execFileSync2("gh", [
|
|
39456
|
+
"pr",
|
|
39457
|
+
"create",
|
|
39458
|
+
"--title",
|
|
39459
|
+
title,
|
|
39460
|
+
"--body",
|
|
39461
|
+
body,
|
|
39462
|
+
"--base",
|
|
39463
|
+
baseBranch,
|
|
39464
|
+
"--head",
|
|
39465
|
+
branch
|
|
39466
|
+
], {
|
|
39467
|
+
cwd: this.projectPath,
|
|
39468
|
+
encoding: "utf-8",
|
|
39469
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39470
|
+
}).trim();
|
|
39471
|
+
const url3 = output;
|
|
39472
|
+
const prNumber = this.extractPrNumber(url3);
|
|
39473
|
+
this.log(`PR created: ${url3}`, "success");
|
|
39474
|
+
return { url: url3, number: prNumber };
|
|
39475
|
+
}
|
|
39476
|
+
validateCreatePrInputs(baseBranch, headBranch) {
|
|
39477
|
+
if (!this.hasRemoteBranch(baseBranch)) {
|
|
39478
|
+
throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
|
|
39479
|
+
}
|
|
39480
|
+
if (!this.hasRemoteBranch(headBranch)) {
|
|
39481
|
+
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
39482
|
+
}
|
|
39483
|
+
const baseRef = this.resolveBranchRef(baseBranch);
|
|
39484
|
+
const headRef = this.resolveBranchRef(headBranch);
|
|
39485
|
+
if (!baseRef) {
|
|
39486
|
+
throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
|
|
39487
|
+
}
|
|
39488
|
+
if (!headRef) {
|
|
39489
|
+
throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
|
|
39490
|
+
}
|
|
39491
|
+
const commitsAhead = this.countCommitsAhead(baseRef, headRef);
|
|
39492
|
+
if (commitsAhead <= 0) {
|
|
39493
|
+
throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
|
|
39494
|
+
}
|
|
39495
|
+
}
|
|
39496
|
+
countCommitsAhead(baseRef, headRef) {
|
|
39497
|
+
const output = execFileSync2("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
39498
|
+
cwd: this.projectPath,
|
|
39499
|
+
encoding: "utf-8",
|
|
39500
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39501
|
+
}).trim();
|
|
39502
|
+
const value = Number.parseInt(output || "0", 10);
|
|
39503
|
+
return Number.isNaN(value) ? 0 : value;
|
|
39504
|
+
}
|
|
39505
|
+
resolveBranchRef(branch) {
|
|
39506
|
+
if (this.hasLocalBranch(branch)) {
|
|
39507
|
+
return branch;
|
|
39508
|
+
}
|
|
39509
|
+
if (this.hasRemoteTrackingBranch(branch)) {
|
|
39510
|
+
return `origin/${branch}`;
|
|
39511
|
+
}
|
|
39512
|
+
return null;
|
|
39513
|
+
}
|
|
39514
|
+
hasLocalBranch(branch) {
|
|
39515
|
+
try {
|
|
39516
|
+
execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
39517
|
+
cwd: this.projectPath,
|
|
39518
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39519
|
+
});
|
|
39520
|
+
return true;
|
|
39521
|
+
} catch {
|
|
39522
|
+
return false;
|
|
39523
|
+
}
|
|
39524
|
+
}
|
|
39525
|
+
hasRemoteTrackingBranch(branch) {
|
|
39526
|
+
try {
|
|
39527
|
+
execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], {
|
|
39528
|
+
cwd: this.projectPath,
|
|
39529
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39530
|
+
});
|
|
39531
|
+
return true;
|
|
39532
|
+
} catch {
|
|
39533
|
+
return false;
|
|
39534
|
+
}
|
|
39535
|
+
}
|
|
39536
|
+
hasRemoteBranch(branch) {
|
|
39537
|
+
try {
|
|
39538
|
+
execFileSync2("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
|
|
39539
|
+
cwd: this.projectPath,
|
|
39540
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39541
|
+
});
|
|
39542
|
+
return true;
|
|
39543
|
+
} catch {
|
|
39544
|
+
return false;
|
|
39545
|
+
}
|
|
39546
|
+
}
|
|
39547
|
+
getPrDiff(branch) {
|
|
39548
|
+
return execFileSync2("gh", ["pr", "diff", branch], {
|
|
39549
|
+
cwd: this.projectPath,
|
|
39550
|
+
encoding: "utf-8",
|
|
39551
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
39552
|
+
maxBuffer: 10 * 1024 * 1024
|
|
39553
|
+
});
|
|
39554
|
+
}
|
|
39555
|
+
submitReview(prIdentifier, body, event) {
|
|
39556
|
+
try {
|
|
39557
|
+
execFileSync2("gh", [
|
|
39558
|
+
"pr",
|
|
39559
|
+
"review",
|
|
39560
|
+
prIdentifier,
|
|
39561
|
+
"--body",
|
|
39562
|
+
body,
|
|
39563
|
+
`--${event.toLowerCase().replace("_", "-")}`
|
|
39564
|
+
], {
|
|
39565
|
+
cwd: this.projectPath,
|
|
39566
|
+
encoding: "utf-8",
|
|
39567
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39568
|
+
});
|
|
39569
|
+
} catch (err) {
|
|
39570
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39571
|
+
if (event === "REQUEST_CHANGES" && msg.includes("own pull request")) {
|
|
39572
|
+
execFileSync2("gh", ["pr", "review", prIdentifier, "--body", body, "--comment"], {
|
|
39573
|
+
cwd: this.projectPath,
|
|
39574
|
+
encoding: "utf-8",
|
|
39575
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39576
|
+
});
|
|
39577
|
+
return;
|
|
39578
|
+
}
|
|
39579
|
+
throw err;
|
|
39580
|
+
}
|
|
39581
|
+
}
|
|
39582
|
+
listLocusPrs() {
|
|
39583
|
+
try {
|
|
39584
|
+
const output = execFileSync2("gh", [
|
|
39585
|
+
"pr",
|
|
39586
|
+
"list",
|
|
39587
|
+
"--search",
|
|
39588
|
+
"[Locus] in:title",
|
|
39589
|
+
"--state",
|
|
39590
|
+
"open",
|
|
39591
|
+
"--json",
|
|
39592
|
+
"number,title,url,headRefName"
|
|
39593
|
+
], {
|
|
39594
|
+
cwd: this.projectPath,
|
|
39595
|
+
encoding: "utf-8",
|
|
39596
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39597
|
+
}).trim();
|
|
39598
|
+
const prs = JSON.parse(output || "[]");
|
|
39599
|
+
return prs.map((pr) => ({
|
|
39600
|
+
number: pr.number,
|
|
39601
|
+
title: pr.title,
|
|
39602
|
+
url: pr.url,
|
|
39603
|
+
branch: pr.headRefName
|
|
39604
|
+
}));
|
|
39605
|
+
} catch {
|
|
39606
|
+
this.log("Failed to list Locus PRs", "warn");
|
|
39607
|
+
return [];
|
|
39608
|
+
}
|
|
39609
|
+
}
|
|
39610
|
+
hasLocusReview(prNumber) {
|
|
39611
|
+
try {
|
|
39612
|
+
const output = execFileSync2("gh", ["pr", "view", prNumber, "--json", "reviews"], {
|
|
39613
|
+
cwd: this.projectPath,
|
|
39614
|
+
encoding: "utf-8",
|
|
39615
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39616
|
+
}).trim();
|
|
39617
|
+
const data = JSON.parse(output || "{}");
|
|
39618
|
+
return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
|
|
39619
|
+
} catch {
|
|
39620
|
+
return false;
|
|
39621
|
+
}
|
|
39622
|
+
}
|
|
39623
|
+
listUnreviewedLocusPrs() {
|
|
39624
|
+
const allPrs = this.listLocusPrs();
|
|
39625
|
+
return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
|
|
39626
|
+
}
|
|
39627
|
+
buildPrBody(task2, agentId, summary) {
|
|
39628
|
+
const sections = [];
|
|
39629
|
+
sections.push(`## Task: ${task2.title}`);
|
|
39630
|
+
sections.push("");
|
|
39631
|
+
if (task2.description) {
|
|
39632
|
+
sections.push(task2.description);
|
|
39633
|
+
sections.push("");
|
|
39634
|
+
}
|
|
39635
|
+
if (task2.acceptanceChecklist?.length > 0) {
|
|
39636
|
+
sections.push("## Acceptance Criteria");
|
|
39637
|
+
for (const item of task2.acceptanceChecklist) {
|
|
39638
|
+
sections.push(`- [ ] ${item.text}`);
|
|
39639
|
+
}
|
|
39640
|
+
sections.push("");
|
|
39641
|
+
}
|
|
39642
|
+
if (summary) {
|
|
39643
|
+
sections.push("## Agent Summary");
|
|
39644
|
+
sections.push(summary);
|
|
39645
|
+
sections.push("");
|
|
39646
|
+
}
|
|
39647
|
+
sections.push("---");
|
|
39648
|
+
sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}\`* | Task ID: \`${task2.id}\``);
|
|
39649
|
+
return sections.join(`
|
|
39650
|
+
`);
|
|
39651
|
+
}
|
|
39652
|
+
extractPrNumber(url3) {
|
|
39653
|
+
const match = url3.match(/\/pull\/(\d+)/);
|
|
39654
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
39655
|
+
}
|
|
39656
|
+
}
|
|
39657
|
+
|
|
39658
|
+
// ../sdk/src/project/knowledge-base.ts
|
|
39659
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
39660
|
+
import { dirname } from "node:path";
|
|
39661
|
+
class KnowledgeBase {
|
|
39662
|
+
contextPath;
|
|
39663
|
+
progressPath;
|
|
39664
|
+
constructor(projectPath) {
|
|
39665
|
+
this.contextPath = getLocusPath(projectPath, "projectContextFile");
|
|
39666
|
+
this.progressPath = getLocusPath(projectPath, "projectProgressFile");
|
|
39667
|
+
}
|
|
39668
|
+
readContext() {
|
|
39669
|
+
if (!existsSync3(this.contextPath)) {
|
|
39670
|
+
return "";
|
|
39671
|
+
}
|
|
39672
|
+
return readFileSync2(this.contextPath, "utf-8");
|
|
39673
|
+
}
|
|
39674
|
+
readProgress() {
|
|
39675
|
+
if (!existsSync3(this.progressPath)) {
|
|
39676
|
+
return "";
|
|
39677
|
+
}
|
|
39678
|
+
return readFileSync2(this.progressPath, "utf-8");
|
|
39679
|
+
}
|
|
39680
|
+
updateContext(content) {
|
|
39681
|
+
this.ensureDir(this.contextPath);
|
|
39682
|
+
writeFileSync(this.contextPath, content);
|
|
39683
|
+
}
|
|
39684
|
+
updateProgress(event) {
|
|
39685
|
+
this.ensureDir(this.progressPath);
|
|
39686
|
+
const existing = this.readProgress();
|
|
39687
|
+
const timestamp2 = (event.timestamp ?? new Date).toISOString();
|
|
39688
|
+
let entry = "";
|
|
39689
|
+
switch (event.type) {
|
|
39690
|
+
case "task_completed":
|
|
39691
|
+
entry = `- [x] ${event.title} — completed ${timestamp2}`;
|
|
39692
|
+
break;
|
|
39693
|
+
case "sprint_started":
|
|
39694
|
+
entry = `
|
|
39695
|
+
## Current Sprint: ${event.title}
|
|
39696
|
+
**Status:** ACTIVE | Started: ${timestamp2}
|
|
39697
|
+
`;
|
|
39698
|
+
break;
|
|
39699
|
+
case "sprint_completed":
|
|
39700
|
+
entry = `
|
|
39701
|
+
### Sprint Completed: ${event.title} — ${timestamp2}
|
|
39702
|
+
`;
|
|
39703
|
+
break;
|
|
39704
|
+
case "blocker":
|
|
39705
|
+
entry = `- BLOCKER: ${event.title}`;
|
|
39706
|
+
break;
|
|
39707
|
+
case "pr_opened":
|
|
39708
|
+
entry = `- [ ] ${event.title} — PR opened ${timestamp2}`;
|
|
39709
|
+
break;
|
|
39710
|
+
case "pr_reviewed":
|
|
39711
|
+
entry = `- ${event.title} — reviewed ${timestamp2}`;
|
|
39712
|
+
break;
|
|
39713
|
+
case "pr_merged":
|
|
39714
|
+
entry = `- [x] ${event.title} — PR merged ${timestamp2}`;
|
|
39715
|
+
break;
|
|
39716
|
+
case "exec_completed":
|
|
39717
|
+
entry = `- [x] ${event.title} — exec ${timestamp2}`;
|
|
39718
|
+
break;
|
|
39719
|
+
}
|
|
39720
|
+
if (event.details) {
|
|
39721
|
+
entry += `
|
|
39722
|
+
${event.details}`;
|
|
39723
|
+
}
|
|
39724
|
+
const updated = existing ? `${existing}
|
|
39725
|
+
${entry}` : `# Project Progress
|
|
39726
|
+
|
|
39727
|
+
${entry}`;
|
|
39728
|
+
writeFileSync(this.progressPath, updated);
|
|
39729
|
+
}
|
|
39730
|
+
getFullContext() {
|
|
39731
|
+
const context = this.readContext();
|
|
39732
|
+
const progress = this.readProgress();
|
|
39733
|
+
const parts = [];
|
|
39734
|
+
if (context.trim()) {
|
|
39735
|
+
parts.push(context.trim());
|
|
39736
|
+
}
|
|
39737
|
+
if (progress.trim()) {
|
|
39738
|
+
parts.push(progress.trim());
|
|
39739
|
+
}
|
|
39740
|
+
return parts.join(`
|
|
39741
|
+
|
|
39742
|
+
---
|
|
39743
|
+
|
|
39744
|
+
`);
|
|
39745
|
+
}
|
|
39746
|
+
initialize(info) {
|
|
39747
|
+
this.ensureDir(this.contextPath);
|
|
39748
|
+
this.ensureDir(this.progressPath);
|
|
39749
|
+
const techStackList = info.techStack.map((t) => `- ${t}`).join(`
|
|
39750
|
+
`);
|
|
39751
|
+
const contextContent = `# Project: ${info.name}
|
|
39752
|
+
|
|
39753
|
+
## Mission
|
|
39754
|
+
${info.mission}
|
|
39755
|
+
|
|
39756
|
+
## Tech Stack
|
|
39757
|
+
${techStackList}
|
|
39758
|
+
|
|
39759
|
+
## Architecture
|
|
39760
|
+
<!-- Describe your high-level architecture here -->
|
|
39761
|
+
|
|
39762
|
+
## Key Decisions
|
|
39763
|
+
<!-- Document important technical decisions and their rationale -->
|
|
39764
|
+
|
|
39765
|
+
## Feature Areas
|
|
39766
|
+
<!-- List your main feature areas and their status -->
|
|
39767
|
+
`;
|
|
39768
|
+
const progressContent = `# Project Progress
|
|
39769
|
+
|
|
39770
|
+
No sprints started yet.
|
|
39771
|
+
`;
|
|
39772
|
+
writeFileSync(this.contextPath, contextContent);
|
|
39773
|
+
writeFileSync(this.progressPath, progressContent);
|
|
39774
|
+
}
|
|
39775
|
+
get exists() {
|
|
39776
|
+
return existsSync3(this.contextPath) || existsSync3(this.progressPath);
|
|
39777
|
+
}
|
|
39778
|
+
ensureDir(filePath) {
|
|
39779
|
+
const dir = dirname(filePath);
|
|
39780
|
+
if (!existsSync3(dir)) {
|
|
39781
|
+
mkdirSync(dir, { recursive: true });
|
|
39782
|
+
}
|
|
39783
|
+
}
|
|
39784
|
+
}
|
|
39785
|
+
|
|
39786
|
+
// ../sdk/src/agent/reviewer-worker.ts
|
|
39787
|
+
function resolveProvider(value) {
|
|
39788
|
+
if (!value || value.startsWith("--"))
|
|
39789
|
+
return PROVIDER.CLAUDE;
|
|
39790
|
+
if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
|
|
39791
|
+
return value;
|
|
39792
|
+
return PROVIDER.CLAUDE;
|
|
39793
|
+
}
|
|
39794
|
+
|
|
39795
|
+
class ReviewerWorker {
|
|
39796
|
+
config;
|
|
39797
|
+
client;
|
|
39798
|
+
aiRunner;
|
|
39799
|
+
prService;
|
|
39800
|
+
knowledgeBase;
|
|
39801
|
+
heartbeatInterval = null;
|
|
39802
|
+
currentTaskId = null;
|
|
39803
|
+
maxReviews = 50;
|
|
39804
|
+
reviewsCompleted = 0;
|
|
39805
|
+
constructor(config2) {
|
|
39806
|
+
this.config = config2;
|
|
39807
|
+
const projectPath = config2.projectPath || process.cwd();
|
|
39808
|
+
this.client = new LocusClient({
|
|
39809
|
+
baseUrl: config2.apiBase,
|
|
39810
|
+
token: config2.apiKey,
|
|
39811
|
+
retryOptions: {
|
|
39812
|
+
maxRetries: 3,
|
|
39813
|
+
initialDelay: 1000,
|
|
39814
|
+
maxDelay: 5000,
|
|
39815
|
+
factor: 2
|
|
39816
|
+
}
|
|
39817
|
+
});
|
|
39818
|
+
const log2 = this.log.bind(this);
|
|
39819
|
+
const provider = config2.provider ?? PROVIDER.CLAUDE;
|
|
39820
|
+
this.aiRunner = createAiRunner(provider, {
|
|
39821
|
+
projectPath,
|
|
39822
|
+
model: config2.model,
|
|
39823
|
+
log: log2
|
|
39824
|
+
});
|
|
39825
|
+
this.prService = new PrService(projectPath, log2);
|
|
39826
|
+
this.knowledgeBase = new KnowledgeBase(projectPath);
|
|
39827
|
+
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
39828
|
+
this.log(`Reviewer agent using ${providerLabel} CLI`, "info");
|
|
39829
|
+
}
|
|
39830
|
+
log(message, level = "info") {
|
|
39831
|
+
const timestamp2 = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
39832
|
+
const colorFn = {
|
|
39833
|
+
info: c.cyan,
|
|
39834
|
+
success: c.green,
|
|
39835
|
+
warn: c.yellow,
|
|
39836
|
+
error: c.red
|
|
39837
|
+
}[level];
|
|
39838
|
+
const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
|
|
39839
|
+
console.log(`${c.dim(`[${timestamp2}]`)} ${c.bold(`[R:${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
|
|
39840
|
+
}
|
|
39841
|
+
getNextUnreviewedPr() {
|
|
39842
|
+
const prs = this.prService.listUnreviewedLocusPrs();
|
|
39843
|
+
return prs.length > 0 ? prs[0] : null;
|
|
39844
|
+
}
|
|
39845
|
+
async reviewPr(pr) {
|
|
39846
|
+
const prNumber = String(pr.number);
|
|
39847
|
+
this.log(`Reviewing PR #${prNumber}: ${pr.title}`, "info");
|
|
39848
|
+
let diff;
|
|
39849
|
+
try {
|
|
39850
|
+
diff = this.prService.getPrDiff(prNumber);
|
|
39851
|
+
} catch (err) {
|
|
39852
|
+
return {
|
|
39853
|
+
reviewed: false,
|
|
39854
|
+
approved: false,
|
|
39855
|
+
summary: `Failed to get PR diff: ${err instanceof Error ? err.message : String(err)}`
|
|
39856
|
+
};
|
|
39857
|
+
}
|
|
39858
|
+
if (!diff.trim()) {
|
|
39859
|
+
return {
|
|
39860
|
+
reviewed: true,
|
|
39861
|
+
approved: true,
|
|
39862
|
+
summary: "PR has no changes (empty diff)"
|
|
39863
|
+
};
|
|
39864
|
+
}
|
|
39865
|
+
const reviewPrompt = `# Code Review Request
|
|
39866
|
+
|
|
39867
|
+
## PR: ${pr.title}
|
|
39868
|
+
|
|
39869
|
+
## PR Diff
|
|
39870
|
+
\`\`\`diff
|
|
39871
|
+
${diff.slice(0, 1e5)}
|
|
39872
|
+
\`\`\`
|
|
39873
|
+
|
|
39874
|
+
## Instructions
|
|
39875
|
+
You are a code reviewer. Review the PR diff above for:
|
|
39876
|
+
|
|
39877
|
+
1. **Correctness** — Does the code do what the PR title suggests?
|
|
39878
|
+
2. **Code Quality** — Naming, structure, complexity, readability.
|
|
39879
|
+
3. **Potential Issues** — Bugs, security issues, edge cases, regressions.
|
|
39880
|
+
|
|
39881
|
+
Output your review in this exact format:
|
|
39882
|
+
|
|
39883
|
+
VERDICT: APPROVE or REQUEST_CHANGES
|
|
39884
|
+
|
|
39885
|
+
Then provide a concise review with specific findings. Keep it actionable and focused.`;
|
|
39886
|
+
const output = await this.aiRunner.run(reviewPrompt);
|
|
39887
|
+
const approved = output.includes("VERDICT: APPROVE");
|
|
39888
|
+
const summary = output.replace(/VERDICT:\s*(APPROVE|REQUEST_CHANGES)\n?/, "").trim();
|
|
39889
|
+
try {
|
|
39890
|
+
const event = approved ? "APPROVE" : "REQUEST_CHANGES";
|
|
39891
|
+
const reviewBody = `## Locus Agent Review
|
|
39892
|
+
|
|
39893
|
+
${summary}`;
|
|
39894
|
+
this.prService.submitReview(prNumber, reviewBody, event);
|
|
39895
|
+
this.log(`Review posted on PR #${prNumber}: ${approved ? "APPROVED" : "CHANGES REQUESTED"}`, approved ? "success" : "warn");
|
|
39896
|
+
} catch (err) {
|
|
39897
|
+
this.log(`Failed to post PR review: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
39898
|
+
}
|
|
39899
|
+
return { reviewed: true, approved, summary };
|
|
39900
|
+
}
|
|
39901
|
+
startHeartbeat() {
|
|
39902
|
+
this.sendHeartbeat();
|
|
39903
|
+
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), 60000);
|
|
39904
|
+
}
|
|
39905
|
+
stopHeartbeat() {
|
|
39906
|
+
if (this.heartbeatInterval) {
|
|
39907
|
+
clearInterval(this.heartbeatInterval);
|
|
39908
|
+
this.heartbeatInterval = null;
|
|
39909
|
+
}
|
|
39910
|
+
}
|
|
39911
|
+
sendHeartbeat() {
|
|
39912
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
|
|
39913
|
+
this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
39914
|
+
});
|
|
39915
|
+
}
|
|
39916
|
+
async run() {
|
|
39917
|
+
this.log(`Reviewer agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
39918
|
+
if (!isGhAvailable(this.config.projectPath)) {
|
|
39919
|
+
this.log("GitHub CLI (gh) not available — reviewer agent cannot operate", "error");
|
|
39920
|
+
process.exit(1);
|
|
39921
|
+
}
|
|
39922
|
+
const handleShutdown = () => {
|
|
39923
|
+
this.log("Received shutdown signal. Aborting...", "warn");
|
|
39924
|
+
this.aiRunner.abort();
|
|
39925
|
+
this.stopHeartbeat();
|
|
39926
|
+
process.exit(1);
|
|
39927
|
+
};
|
|
39928
|
+
process.on("SIGTERM", handleShutdown);
|
|
39929
|
+
process.on("SIGINT", handleShutdown);
|
|
39930
|
+
this.startHeartbeat();
|
|
39931
|
+
while (this.reviewsCompleted < this.maxReviews) {
|
|
39932
|
+
const pr = this.getNextUnreviewedPr();
|
|
39933
|
+
if (!pr) {
|
|
39934
|
+
this.log("No unreviewed PRs found. Waiting 30s...", "info");
|
|
39935
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
39936
|
+
continue;
|
|
39937
|
+
}
|
|
39938
|
+
this.log(`Reviewing: ${pr.title} (PR #${pr.number})`, "success");
|
|
39939
|
+
this.sendHeartbeat();
|
|
39940
|
+
const result = await this.reviewPr(pr);
|
|
39941
|
+
if (result.reviewed) {
|
|
39942
|
+
const status = result.approved ? "APPROVED" : "CHANGES REQUESTED";
|
|
39943
|
+
try {
|
|
39944
|
+
this.knowledgeBase.updateProgress({
|
|
39945
|
+
type: "pr_reviewed",
|
|
39946
|
+
title: pr.title,
|
|
39947
|
+
details: `Review: ${status}`
|
|
39948
|
+
});
|
|
39949
|
+
} catch {}
|
|
39950
|
+
this.reviewsCompleted++;
|
|
39951
|
+
} else {
|
|
39952
|
+
this.log(`Review skipped: ${result.summary}`, "warn");
|
|
39953
|
+
}
|
|
39954
|
+
this.currentTaskId = null;
|
|
39955
|
+
}
|
|
39956
|
+
this.stopHeartbeat();
|
|
39957
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
39958
|
+
process.exit(0);
|
|
39959
|
+
}
|
|
39960
|
+
}
|
|
39961
|
+
var reviewerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
|
|
39962
|
+
if (reviewerEntrypoint === "reviewer-worker.js" || reviewerEntrypoint === "reviewer-worker.ts") {
|
|
39963
|
+
process.title = "locus-reviewer";
|
|
39964
|
+
const args = process.argv.slice(2);
|
|
39965
|
+
const config2 = {};
|
|
39966
|
+
for (let i = 0;i < args.length; i++) {
|
|
39967
|
+
const arg = args[i];
|
|
39968
|
+
if (arg === "--agent-id")
|
|
39969
|
+
config2.agentId = args[++i];
|
|
39970
|
+
else if (arg === "--workspace-id")
|
|
39971
|
+
config2.workspaceId = args[++i];
|
|
39972
|
+
else if (arg === "--sprint-id")
|
|
39973
|
+
config2.sprintId = args[++i];
|
|
39974
|
+
else if (arg === "--api-url")
|
|
39975
|
+
config2.apiBase = args[++i];
|
|
39976
|
+
else if (arg === "--api-key")
|
|
39977
|
+
config2.apiKey = args[++i];
|
|
39978
|
+
else if (arg === "--project-path")
|
|
39979
|
+
config2.projectPath = args[++i];
|
|
39980
|
+
else if (arg === "--model")
|
|
39981
|
+
config2.model = args[++i];
|
|
39982
|
+
else if (arg === "--provider") {
|
|
39983
|
+
const value = args[i + 1];
|
|
39984
|
+
if (value && !value.startsWith("--"))
|
|
39985
|
+
i++;
|
|
39986
|
+
config2.provider = resolveProvider(value);
|
|
39987
|
+
}
|
|
39988
|
+
}
|
|
39989
|
+
if (!config2.agentId || !config2.workspaceId || !config2.apiBase || !config2.apiKey || !config2.projectPath) {
|
|
39990
|
+
console.error("Missing required arguments");
|
|
39991
|
+
process.exit(1);
|
|
39992
|
+
}
|
|
39993
|
+
const worker = new ReviewerWorker(config2);
|
|
39994
|
+
worker.run().catch((err) => {
|
|
39995
|
+
console.error("Fatal reviewer error:", err);
|
|
39996
|
+
process.exit(1);
|
|
39997
|
+
});
|
|
39998
|
+
}
|
|
39999
|
+
// ../sdk/src/core/prompt-builder.ts
|
|
40000
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
40001
|
+
import { join as join5 } from "node:path";
|
|
40002
|
+
class PromptBuilder {
|
|
40003
|
+
projectPath;
|
|
40004
|
+
constructor(projectPath) {
|
|
40005
|
+
this.projectPath = projectPath;
|
|
40006
|
+
}
|
|
40007
|
+
async build(task2, options = {}) {
|
|
40008
|
+
let prompt = `# Task: ${task2.title}
|
|
40009
|
+
|
|
40010
|
+
`;
|
|
40011
|
+
const roleText = this.roleToText(task2.assigneeRole);
|
|
40012
|
+
if (roleText) {
|
|
40013
|
+
prompt += `## Role
|
|
40014
|
+
You are acting as a ${roleText}.
|
|
40015
|
+
|
|
40016
|
+
`;
|
|
40017
|
+
}
|
|
40018
|
+
prompt += `## Description
|
|
40019
|
+
${task2.description || "No description provided."}
|
|
40020
|
+
|
|
40021
|
+
`;
|
|
40022
|
+
const projectConfig = this.getProjectConfig();
|
|
40023
|
+
if (projectConfig) {
|
|
40024
|
+
prompt += `## Project Metadata
|
|
40025
|
+
`;
|
|
40026
|
+
prompt += `- Version: ${projectConfig.version || "Unknown"}
|
|
40027
|
+
`;
|
|
40028
|
+
prompt += `- Created At: ${projectConfig.createdAt || "Unknown"}
|
|
40029
|
+
|
|
40030
|
+
`;
|
|
40031
|
+
}
|
|
40032
|
+
let serverContext = null;
|
|
40033
|
+
if (options.taskContext) {
|
|
40034
|
+
try {
|
|
40035
|
+
serverContext = JSON.parse(options.taskContext);
|
|
40036
|
+
} catch {
|
|
40037
|
+
serverContext = { context: options.taskContext };
|
|
40038
|
+
}
|
|
40039
|
+
}
|
|
40040
|
+
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
40041
|
+
let hasLocalContext = false;
|
|
40042
|
+
if (existsSync4(contextPath)) {
|
|
40043
|
+
try {
|
|
40044
|
+
const context = readFileSync3(contextPath, "utf-8");
|
|
40045
|
+
if (context.trim().length > 20) {
|
|
40046
|
+
prompt += `## Project Context (Local)
|
|
40047
|
+
${context}
|
|
40048
|
+
|
|
40049
|
+
`;
|
|
40050
|
+
hasLocalContext = true;
|
|
40051
|
+
}
|
|
40052
|
+
} catch (err) {
|
|
40053
|
+
console.warn(`Warning: Could not read context file: ${err}`);
|
|
40054
|
+
}
|
|
40055
|
+
}
|
|
40056
|
+
if (!hasLocalContext) {
|
|
40057
|
+
const fallback = this.getFallbackContext();
|
|
40058
|
+
if (fallback) {
|
|
40059
|
+
prompt += `## Project Context (README Fallback)
|
|
40060
|
+
${fallback}
|
|
40061
|
+
|
|
40062
|
+
`;
|
|
40063
|
+
}
|
|
40064
|
+
}
|
|
40065
|
+
if (serverContext) {
|
|
40066
|
+
prompt += `## Project Context (Server)
|
|
40067
|
+
`;
|
|
40068
|
+
const project = serverContext.project;
|
|
40069
|
+
if (project) {
|
|
40070
|
+
prompt += `- Project: ${project.name || "Unknown"}
|
|
40071
|
+
`;
|
|
40072
|
+
if (!hasLocalContext && project.techStack?.length) {
|
|
40073
|
+
prompt += `- Tech Stack: ${project.techStack.join(", ")}
|
|
40074
|
+
`;
|
|
40075
|
+
}
|
|
40076
|
+
}
|
|
40077
|
+
if (serverContext.context) {
|
|
40078
|
+
prompt += `
|
|
40079
|
+
${serverContext.context}
|
|
40080
|
+
`;
|
|
40081
|
+
}
|
|
40082
|
+
prompt += `
|
|
40083
|
+
`;
|
|
40084
|
+
}
|
|
40085
|
+
prompt += this.getProjectStructure();
|
|
40086
|
+
prompt += `## Project Knowledge Base
|
|
40087
|
+
`;
|
|
40088
|
+
prompt += `You have access to the following documentation directories for context:
|
|
40089
|
+
`;
|
|
40090
|
+
prompt += `- Artifacts: \`.locus/artifacts\`
|
|
40091
|
+
`;
|
|
40092
|
+
prompt += `- Documents: \`.locus/documents\`
|
|
40093
|
+
`;
|
|
40094
|
+
prompt += `If you need more information about the project strategies, plans, or architecture, please read files in these directories.
|
|
40095
|
+
|
|
40096
|
+
`;
|
|
40097
|
+
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
40098
|
+
if (existsSync4(indexPath)) {
|
|
40099
|
+
prompt += `## Codebase Overview
|
|
40100
|
+
There is an index file in the .locus/codebase-index.json and if you need you can check it.
|
|
40101
|
+
|
|
40102
|
+
`;
|
|
40103
|
+
}
|
|
40104
|
+
if (task2.docs && task2.docs.length > 0) {
|
|
40105
|
+
prompt += `## Attached Documents (Summarized)
|
|
40106
|
+
`;
|
|
40107
|
+
prompt += `> Full content available on server. Rely on Task Description for specific requirements.
|
|
40108
|
+
|
|
40109
|
+
`;
|
|
40110
|
+
for (const doc3 of task2.docs) {
|
|
40111
|
+
const content = doc3.content || "";
|
|
40112
|
+
const limit = 800;
|
|
40113
|
+
const preview = content.slice(0, limit);
|
|
40114
|
+
const isTruncated = content.length > limit;
|
|
40115
|
+
prompt += `### Doc: ${doc3.title}
|
|
40116
|
+
${preview}${isTruncated ? `
|
|
40117
|
+
...(truncated)...` : ""}
|
|
40118
|
+
|
|
40119
|
+
`;
|
|
40120
|
+
}
|
|
40121
|
+
}
|
|
40122
|
+
if (task2.acceptanceChecklist && task2.acceptanceChecklist.length > 0) {
|
|
40123
|
+
prompt += `## Acceptance Criteria
|
|
40124
|
+
`;
|
|
40125
|
+
for (const item of task2.acceptanceChecklist) {
|
|
40126
|
+
prompt += `- ${item.done ? "[x]" : "[ ]"} ${item.text}
|
|
40127
|
+
`;
|
|
40128
|
+
}
|
|
40129
|
+
prompt += `
|
|
40130
|
+
`;
|
|
40131
|
+
}
|
|
40132
|
+
if (task2.comments && task2.comments.length > 0) {
|
|
40133
|
+
const comments = task2.comments.slice(0, 3);
|
|
40134
|
+
prompt += `## Task History & Feedback
|
|
40135
|
+
`;
|
|
40136
|
+
prompt += `Review the following comments for context or rejection feedback:
|
|
40137
|
+
|
|
40138
|
+
`;
|
|
40139
|
+
for (const comment of comments) {
|
|
40140
|
+
const date5 = new Date(comment.createdAt).toLocaleString();
|
|
40141
|
+
prompt += `### ${comment.author} (${date5})
|
|
40142
|
+
${comment.text}
|
|
40143
|
+
|
|
40144
|
+
`;
|
|
40145
|
+
}
|
|
40146
|
+
}
|
|
40147
|
+
prompt += `## Instructions
|
|
40148
|
+
1. Complete this task.
|
|
40149
|
+
2. **Artifact Management**: If you create any high-level documentation (PRDs, technical drafts, architecture docs), you MUST save them in \`.locus/artifacts/\`. Do NOT create them in the root directory.
|
|
40150
|
+
3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
|
|
40151
|
+
4. **Git**: Do NOT run \`git add\`, \`git commit\`, \`git push\`, or create branches. The Locus system handles all git operations automatically after your execution completes.
|
|
40152
|
+
5. **Progress**: Do NOT modify \`.locus/project/progress.md\`. The system updates it automatically.`;
|
|
40153
|
+
return prompt;
|
|
40154
|
+
}
|
|
40155
|
+
async buildGenericPrompt(query) {
|
|
40156
|
+
let prompt = `# Direct Execution
|
|
40157
|
+
|
|
40158
|
+
`;
|
|
40159
|
+
prompt += `## Prompt
|
|
40160
|
+
${query}
|
|
40161
|
+
|
|
40162
|
+
`;
|
|
40163
|
+
const projectConfig = this.getProjectConfig();
|
|
40164
|
+
if (projectConfig) {
|
|
40165
|
+
prompt += `## Project Metadata
|
|
40166
|
+
`;
|
|
40167
|
+
prompt += `- Version: ${projectConfig.version || "Unknown"}
|
|
40168
|
+
`;
|
|
40169
|
+
prompt += `- Created At: ${projectConfig.createdAt || "Unknown"}
|
|
40170
|
+
|
|
40171
|
+
`;
|
|
40172
|
+
}
|
|
40173
|
+
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
40174
|
+
let hasLocalContext = false;
|
|
40175
|
+
if (existsSync4(contextPath)) {
|
|
40176
|
+
try {
|
|
40177
|
+
const context = readFileSync3(contextPath, "utf-8");
|
|
40178
|
+
if (context.trim().length > 20) {
|
|
40179
|
+
prompt += `## Project Context (Local)
|
|
40180
|
+
${context}
|
|
40181
|
+
|
|
40182
|
+
`;
|
|
40183
|
+
hasLocalContext = true;
|
|
40184
|
+
}
|
|
40185
|
+
} catch (err) {
|
|
40186
|
+
console.warn(`Warning: Could not read context file: ${err}`);
|
|
40187
|
+
}
|
|
40188
|
+
}
|
|
40189
|
+
if (!hasLocalContext) {
|
|
40190
|
+
const fallback = this.getFallbackContext();
|
|
40191
|
+
if (fallback) {
|
|
40192
|
+
prompt += `## Project Context (README Fallback)
|
|
40193
|
+
${fallback}
|
|
40194
|
+
|
|
40195
|
+
`;
|
|
40196
|
+
}
|
|
40197
|
+
}
|
|
40198
|
+
prompt += this.getProjectStructure();
|
|
40199
|
+
prompt += `## Project Knowledge Base
|
|
40200
|
+
`;
|
|
40201
|
+
prompt += `You have access to the following documentation directories for context:
|
|
40202
|
+
`;
|
|
40203
|
+
prompt += `- Artifacts: \`.locus/artifacts\` (local-only, not synced to cloud)
|
|
40204
|
+
`;
|
|
40205
|
+
prompt += `- Documents: \`.locus/documents\` (synced from cloud)
|
|
40206
|
+
`;
|
|
40207
|
+
prompt += `If you need more information about the project strategies, plans, or architecture, please read files in these directories.
|
|
40208
|
+
|
|
40209
|
+
`;
|
|
40210
|
+
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
40211
|
+
if (existsSync4(indexPath)) {
|
|
40212
|
+
prompt += `## Codebase Overview
|
|
40213
|
+
There is an index file in the .locus/codebase-index.json and if you need you can check it.
|
|
40214
|
+
|
|
40215
|
+
`;
|
|
40216
|
+
}
|
|
40217
|
+
prompt += `## Instructions
|
|
40218
|
+
1. Execute the prompt based on the provided project context.
|
|
40219
|
+
2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
|
|
40220
|
+
3. **Git**: Do NOT run \`git add\`, \`git commit\`, \`git push\`, or create branches. The Locus system handles all git operations automatically after your execution completes.
|
|
40221
|
+
4. **Progress**: Do NOT modify \`.locus/project/progress.md\`. The system updates it automatically.`;
|
|
40222
|
+
return prompt;
|
|
40223
|
+
}
|
|
40224
|
+
getProjectConfig() {
|
|
40225
|
+
const configPath = getLocusPath(this.projectPath, "configFile");
|
|
40226
|
+
if (existsSync4(configPath)) {
|
|
40227
|
+
try {
|
|
40228
|
+
return JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
40229
|
+
} catch {
|
|
40230
|
+
return null;
|
|
40231
|
+
}
|
|
40232
|
+
}
|
|
40233
|
+
return null;
|
|
40234
|
+
}
|
|
40235
|
+
getFallbackContext() {
|
|
40236
|
+
const readmePath = join5(this.projectPath, "README.md");
|
|
40237
|
+
if (existsSync4(readmePath)) {
|
|
40238
|
+
try {
|
|
40239
|
+
const content = readFileSync3(readmePath, "utf-8");
|
|
40240
|
+
const limit = 1000;
|
|
40241
|
+
return content.slice(0, limit) + (content.length > limit ? `
|
|
40242
|
+
...(truncated)...` : "");
|
|
40243
|
+
} catch {
|
|
40244
|
+
return "";
|
|
40245
|
+
}
|
|
40246
|
+
}
|
|
40247
|
+
return "";
|
|
40248
|
+
}
|
|
40249
|
+
getProjectStructure() {
|
|
40250
|
+
try {
|
|
40251
|
+
const entries = readdirSync(this.projectPath);
|
|
40252
|
+
const folders = entries.filter((e) => {
|
|
40253
|
+
if (e.startsWith(".") || e === "node_modules")
|
|
40254
|
+
return false;
|
|
40255
|
+
try {
|
|
40256
|
+
return statSync(join5(this.projectPath, e)).isDirectory();
|
|
40257
|
+
} catch {
|
|
40258
|
+
return false;
|
|
40259
|
+
}
|
|
40260
|
+
});
|
|
40261
|
+
if (folders.length === 0)
|
|
40262
|
+
return "";
|
|
40263
|
+
let structure = `## Project Structure
|
|
40264
|
+
`;
|
|
40265
|
+
structure += `Key directories in this project:
|
|
40266
|
+
`;
|
|
40267
|
+
for (const folder of folders) {
|
|
40268
|
+
structure += `- \`${folder}/\`
|
|
40269
|
+
`;
|
|
40270
|
+
}
|
|
40271
|
+
return `${structure}
|
|
40272
|
+
`;
|
|
40273
|
+
} catch {
|
|
40274
|
+
return "";
|
|
40275
|
+
}
|
|
40276
|
+
}
|
|
40277
|
+
roleToText(role) {
|
|
40278
|
+
if (!role) {
|
|
40279
|
+
return null;
|
|
40280
|
+
}
|
|
40281
|
+
switch (role) {
|
|
40282
|
+
case "BACKEND" /* BACKEND */:
|
|
40283
|
+
return "Backend Engineer";
|
|
40284
|
+
case "FRONTEND" /* FRONTEND */:
|
|
40285
|
+
return "Frontend Engineer";
|
|
40286
|
+
case "PM" /* PM */:
|
|
40287
|
+
return "Product Manager";
|
|
40288
|
+
case "QA" /* QA */:
|
|
40289
|
+
return "QA Engineer";
|
|
40290
|
+
case "DESIGN" /* DESIGN */:
|
|
40291
|
+
return "Product Designer";
|
|
40292
|
+
default:
|
|
40293
|
+
return "engineer";
|
|
40294
|
+
}
|
|
40295
|
+
}
|
|
40296
|
+
}
|
|
40297
|
+
|
|
40298
|
+
// ../sdk/src/agent/task-executor.ts
|
|
40299
|
+
class TaskExecutor {
|
|
40300
|
+
deps;
|
|
40301
|
+
promptBuilder;
|
|
40302
|
+
constructor(deps) {
|
|
40303
|
+
this.deps = deps;
|
|
40304
|
+
this.promptBuilder = new PromptBuilder(deps.projectPath);
|
|
40305
|
+
}
|
|
40306
|
+
async execute(task2) {
|
|
40307
|
+
this.deps.log(`Executing: ${task2.title}`, "info");
|
|
40308
|
+
const basePrompt = await this.promptBuilder.build(task2);
|
|
40309
|
+
try {
|
|
40310
|
+
this.deps.log("Starting Execution...", "info");
|
|
40311
|
+
await this.deps.aiRunner.run(basePrompt);
|
|
40312
|
+
return {
|
|
40313
|
+
success: true,
|
|
40314
|
+
summary: "Task completed by the agent"
|
|
40315
|
+
};
|
|
40316
|
+
} catch (error48) {
|
|
40317
|
+
return { success: false, summary: `Error: ${error48}` };
|
|
40318
|
+
}
|
|
40319
|
+
}
|
|
40320
|
+
}
|
|
40321
|
+
// ../sdk/src/worktree/worktree-manager.ts
|
|
40322
|
+
import { execFileSync as execFileSync3, execSync } from "node:child_process";
|
|
40323
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, rmSync, statSync as statSync2 } from "node:fs";
|
|
40324
|
+
import { join as join6, resolve as resolve2, sep } from "node:path";
|
|
40325
|
+
|
|
40326
|
+
// ../sdk/src/worktree/worktree-config.ts
|
|
40327
|
+
var WORKTREE_ROOT_DIR = ".locus-worktrees";
|
|
40328
|
+
var WORKTREE_BRANCH_PREFIX = "agent";
|
|
40329
|
+
var DEFAULT_WORKTREE_CONFIG = {
|
|
40330
|
+
rootDir: WORKTREE_ROOT_DIR,
|
|
40331
|
+
branchPrefix: WORKTREE_BRANCH_PREFIX,
|
|
40332
|
+
cleanupPolicy: "retain-on-failure"
|
|
40333
|
+
};
|
|
40334
|
+
|
|
40335
|
+
// ../sdk/src/worktree/worktree-manager.ts
|
|
40336
|
+
class WorktreeManager {
|
|
40337
|
+
config;
|
|
40338
|
+
projectPath;
|
|
40339
|
+
log;
|
|
40340
|
+
constructor(projectPath, config2, log2) {
|
|
40341
|
+
this.projectPath = resolve2(projectPath);
|
|
40342
|
+
this.config = { ...DEFAULT_WORKTREE_CONFIG, ...config2 };
|
|
40343
|
+
this.log = log2 ?? ((_msg) => {
|
|
40344
|
+
return;
|
|
40345
|
+
});
|
|
40346
|
+
}
|
|
40347
|
+
get rootPath() {
|
|
40348
|
+
return join6(this.projectPath, this.config.rootDir);
|
|
40349
|
+
}
|
|
40350
|
+
buildBranchName(taskId, taskSlug) {
|
|
40351
|
+
const sanitized = taskSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
40352
|
+
return `${this.config.branchPrefix}/${taskId}-${sanitized}`;
|
|
40353
|
+
}
|
|
40354
|
+
create(options) {
|
|
40355
|
+
const branch = this.buildBranchName(options.taskId, options.taskSlug);
|
|
40356
|
+
const worktreeDir = `${options.agentId}-${options.taskId}`;
|
|
40357
|
+
const worktreePath = join6(this.rootPath, worktreeDir);
|
|
40358
|
+
this.ensureDirectory(this.rootPath, "Worktree root");
|
|
40359
|
+
const baseBranch = options.baseBranch ?? this.config.baseBranch ?? this.getCurrentBranch();
|
|
40360
|
+
this.log(`Creating worktree: ${worktreeDir} (branch: ${branch}, base: ${baseBranch})`, "info");
|
|
40361
|
+
if (existsSync5(worktreePath)) {
|
|
40362
|
+
this.log(`Removing stale worktree directory: ${worktreePath}`, "warn");
|
|
40363
|
+
try {
|
|
40364
|
+
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
40365
|
+
} catch {
|
|
40366
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
40367
|
+
this.git("worktree prune", this.projectPath);
|
|
40368
|
+
}
|
|
40369
|
+
}
|
|
40370
|
+
if (this.branchExists(branch)) {
|
|
40371
|
+
this.log(`Deleting existing branch: ${branch}`, "warn");
|
|
40372
|
+
const branchWorktrees = this.list().filter((wt) => wt.branch === branch);
|
|
40373
|
+
for (const wt of branchWorktrees) {
|
|
40374
|
+
const worktreePath2 = resolve2(wt.path);
|
|
40375
|
+
if (wt.isMain || !this.isManagedWorktreePath(worktreePath2)) {
|
|
40376
|
+
throw new Error(`Branch "${branch}" is checked out at "${worktreePath2}". Remove or detach that worktree before retrying.`);
|
|
40377
|
+
}
|
|
40378
|
+
this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
|
|
40379
|
+
this.remove(worktreePath2, false);
|
|
40380
|
+
}
|
|
40381
|
+
try {
|
|
40382
|
+
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
40383
|
+
} catch {
|
|
40384
|
+
this.git("worktree prune", this.projectPath);
|
|
40385
|
+
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
40386
|
+
}
|
|
40387
|
+
}
|
|
40388
|
+
const addWorktree = () => this.git(`worktree add "${worktreePath}" -b "${branch}" "${baseBranch}"`, this.projectPath);
|
|
40389
|
+
try {
|
|
40390
|
+
addWorktree();
|
|
40391
|
+
} catch (error48) {
|
|
40392
|
+
if (!this.isMissingDirectoryError(error48)) {
|
|
40393
|
+
throw error48;
|
|
40394
|
+
}
|
|
40395
|
+
this.log(`Worktree creation failed due to missing directories. Retrying after cleanup: ${worktreePath}`, "warn");
|
|
40396
|
+
this.cleanupFailedWorktree(worktreePath, branch);
|
|
40397
|
+
this.ensureDirectory(this.rootPath, "Worktree root");
|
|
40398
|
+
addWorktree();
|
|
40399
|
+
}
|
|
40400
|
+
this.log(`Worktree created at ${worktreePath}`, "success");
|
|
40401
|
+
return { worktreePath, branch, baseBranch };
|
|
40402
|
+
}
|
|
40403
|
+
list() {
|
|
40404
|
+
const output = this.git("worktree list --porcelain", this.projectPath);
|
|
40405
|
+
const worktrees = [];
|
|
40406
|
+
const blocks = output.trim().split(`
|
|
40407
|
+
|
|
40408
|
+
`);
|
|
40409
|
+
for (const block of blocks) {
|
|
40410
|
+
if (!block.trim())
|
|
40411
|
+
continue;
|
|
40412
|
+
const lines = block.trim().split(`
|
|
40413
|
+
`);
|
|
40414
|
+
let path = "";
|
|
40415
|
+
let head = "";
|
|
40416
|
+
let branch = "";
|
|
40417
|
+
let isMain = false;
|
|
40418
|
+
let isPrunable = false;
|
|
40419
|
+
for (const line of lines) {
|
|
40420
|
+
if (line.startsWith("worktree ")) {
|
|
40421
|
+
path = line.slice("worktree ".length);
|
|
40422
|
+
} else if (line.startsWith("HEAD ")) {
|
|
40423
|
+
head = line.slice("HEAD ".length);
|
|
40424
|
+
} else if (line.startsWith("branch ")) {
|
|
40425
|
+
branch = line.slice("branch ".length).replace("refs/heads/", "");
|
|
40426
|
+
} else if (line === "bare" || path === this.projectPath) {
|
|
40427
|
+
isMain = true;
|
|
40428
|
+
} else if (line === "prunable") {
|
|
40429
|
+
isPrunable = true;
|
|
40430
|
+
} else if (line === "detached") {
|
|
40431
|
+
branch = "(detached)";
|
|
40432
|
+
}
|
|
40433
|
+
}
|
|
40434
|
+
if (resolve2(path) === this.projectPath) {
|
|
40435
|
+
isMain = true;
|
|
40436
|
+
}
|
|
40437
|
+
if (path) {
|
|
40438
|
+
worktrees.push({ path, branch, head, isMain, isPrunable });
|
|
40439
|
+
}
|
|
40440
|
+
}
|
|
40441
|
+
return worktrees;
|
|
40442
|
+
}
|
|
40443
|
+
listAgentWorktrees() {
|
|
40444
|
+
return this.list().filter((wt) => !wt.isMain);
|
|
40445
|
+
}
|
|
40446
|
+
remove(worktreePath, deleteBranch = true) {
|
|
40447
|
+
const absolutePath = resolve2(worktreePath);
|
|
40448
|
+
const worktrees = this.list();
|
|
40449
|
+
const worktree = worktrees.find((wt) => resolve2(wt.path) === absolutePath);
|
|
40450
|
+
const branchToDelete = worktree?.branch;
|
|
40451
|
+
this.log(`Removing worktree: ${absolutePath}`, "info");
|
|
40452
|
+
try {
|
|
40453
|
+
this.git(`worktree remove "${absolutePath}" --force`, this.projectPath);
|
|
40454
|
+
} catch {
|
|
40455
|
+
if (existsSync5(absolutePath)) {
|
|
40456
|
+
rmSync(absolutePath, { recursive: true, force: true });
|
|
40457
|
+
}
|
|
40458
|
+
this.git("worktree prune", this.projectPath);
|
|
40459
|
+
}
|
|
40460
|
+
if (deleteBranch && branchToDelete && !branchToDelete.startsWith("(")) {
|
|
40461
|
+
try {
|
|
40462
|
+
this.git(`branch -D "${branchToDelete}"`, this.projectPath);
|
|
40463
|
+
this.log(`Deleted branch: ${branchToDelete}`, "success");
|
|
40464
|
+
} catch {
|
|
40465
|
+
this.log(`Could not delete branch: ${branchToDelete} (may already be deleted)`, "warn");
|
|
40466
|
+
}
|
|
40467
|
+
}
|
|
40468
|
+
this.log("Worktree removed", "success");
|
|
40469
|
+
}
|
|
40470
|
+
prune() {
|
|
40471
|
+
const before = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
40472
|
+
this.git("worktree prune", this.projectPath);
|
|
40473
|
+
const after = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
40474
|
+
const pruned = before - after;
|
|
40475
|
+
if (pruned > 0) {
|
|
40476
|
+
this.log(`Pruned ${pruned} stale worktree(s)`, "success");
|
|
40477
|
+
}
|
|
40478
|
+
return pruned;
|
|
40479
|
+
}
|
|
40480
|
+
removeAll() {
|
|
40481
|
+
const agentWorktrees = this.listAgentWorktrees();
|
|
40482
|
+
let removed = 0;
|
|
40483
|
+
for (const wt of agentWorktrees) {
|
|
40484
|
+
try {
|
|
40485
|
+
this.remove(wt.path, true);
|
|
40486
|
+
removed++;
|
|
40487
|
+
} catch {
|
|
40488
|
+
this.log(`Failed to remove worktree: ${wt.path}`, "warn");
|
|
40489
|
+
}
|
|
40490
|
+
}
|
|
40491
|
+
if (existsSync5(this.rootPath)) {
|
|
40492
|
+
try {
|
|
40493
|
+
rmSync(this.rootPath, { recursive: true, force: true });
|
|
40494
|
+
} catch {}
|
|
40495
|
+
}
|
|
40496
|
+
return removed;
|
|
40497
|
+
}
|
|
40498
|
+
hasChanges(worktreePath) {
|
|
40499
|
+
const status = this.git("status --porcelain", worktreePath).trim();
|
|
40500
|
+
return status.length > 0;
|
|
40501
|
+
}
|
|
40502
|
+
hasCommitsAhead(worktreePath, baseBranch) {
|
|
40503
|
+
try {
|
|
40504
|
+
const count = this.git(`rev-list --count "${baseBranch}..HEAD"`, worktreePath).trim();
|
|
40505
|
+
return Number.parseInt(count, 10) > 0;
|
|
40506
|
+
} catch {
|
|
40507
|
+
return false;
|
|
40508
|
+
}
|
|
40509
|
+
}
|
|
40510
|
+
commitChanges(worktreePath, message, baseBranch) {
|
|
40511
|
+
const hasUncommittedChanges = this.hasChanges(worktreePath);
|
|
40512
|
+
if (!hasUncommittedChanges) {
|
|
40513
|
+
if (baseBranch && this.hasCommitsAhead(worktreePath, baseBranch)) {
|
|
40514
|
+
const hash3 = this.git("rev-parse HEAD", worktreePath).trim();
|
|
40515
|
+
this.log(`Agent already committed changes (${hash3.slice(0, 8)}); skipping additional commit`, "info");
|
|
40516
|
+
return hash3;
|
|
40517
|
+
}
|
|
40518
|
+
this.log("No changes to commit", "info");
|
|
40519
|
+
return null;
|
|
40520
|
+
}
|
|
40521
|
+
this.git("add -A", worktreePath);
|
|
40522
|
+
try {
|
|
40523
|
+
this.git("reset HEAD -- .locus/project/progress.md", worktreePath);
|
|
40524
|
+
} catch {}
|
|
40525
|
+
const staged = this.git("diff --cached --name-only", worktreePath).trim();
|
|
40526
|
+
if (!staged) {
|
|
40527
|
+
this.log("No changes to commit (only progress.md was modified)", "info");
|
|
40528
|
+
return null;
|
|
40529
|
+
}
|
|
40530
|
+
this.gitExec(["commit", "-m", message], worktreePath);
|
|
40531
|
+
const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
|
|
40532
|
+
this.log(`Committed: ${hash2.slice(0, 8)}`, "success");
|
|
40533
|
+
return hash2;
|
|
40534
|
+
}
|
|
40535
|
+
pushBranch(worktreePath, remote = "origin") {
|
|
40536
|
+
const branch = this.getBranch(worktreePath);
|
|
40537
|
+
this.log(`Pushing branch ${branch} to ${remote}`, "info");
|
|
40538
|
+
try {
|
|
40539
|
+
this.gitExec(["push", "-u", remote, branch], worktreePath);
|
|
40540
|
+
this.log(`Pushed ${branch} to ${remote}`, "success");
|
|
40541
|
+
return branch;
|
|
40542
|
+
} catch (error48) {
|
|
40543
|
+
if (!this.isNonFastForwardPushError(error48)) {
|
|
40544
|
+
throw error48;
|
|
40545
|
+
}
|
|
40546
|
+
this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
|
|
40547
|
+
try {
|
|
40548
|
+
this.gitExec(["fetch", remote, branch], worktreePath);
|
|
40549
|
+
} catch {}
|
|
40550
|
+
this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
|
|
40551
|
+
this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
|
|
40552
|
+
}
|
|
40553
|
+
return branch;
|
|
40554
|
+
}
|
|
40555
|
+
getBranch(worktreePath) {
|
|
40556
|
+
return this.git("rev-parse --abbrev-ref HEAD", worktreePath).trim();
|
|
40557
|
+
}
|
|
40558
|
+
hasWorktreeForTask(taskId) {
|
|
40559
|
+
return this.listAgentWorktrees().some((wt) => wt.branch.includes(taskId) || wt.path.includes(taskId));
|
|
40560
|
+
}
|
|
40561
|
+
branchExists(branchName) {
|
|
40562
|
+
try {
|
|
40563
|
+
this.git(`rev-parse --verify "refs/heads/${branchName}"`, this.projectPath);
|
|
40564
|
+
return true;
|
|
40565
|
+
} catch {
|
|
40566
|
+
return false;
|
|
40567
|
+
}
|
|
40568
|
+
}
|
|
40569
|
+
getCurrentBranch() {
|
|
40570
|
+
return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
|
|
40571
|
+
}
|
|
40572
|
+
isManagedWorktreePath(worktreePath) {
|
|
40573
|
+
const rootPath = resolve2(this.rootPath);
|
|
40574
|
+
const candidate = resolve2(worktreePath);
|
|
40575
|
+
const rootWithSep = rootPath.endsWith(sep) ? rootPath : `${rootPath}${sep}`;
|
|
40576
|
+
return candidate.startsWith(rootWithSep);
|
|
40577
|
+
}
|
|
40578
|
+
ensureDirectory(dirPath, label) {
|
|
40579
|
+
if (existsSync5(dirPath)) {
|
|
40580
|
+
if (!statSync2(dirPath).isDirectory()) {
|
|
40581
|
+
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
40582
|
+
}
|
|
40583
|
+
return;
|
|
40584
|
+
}
|
|
40585
|
+
mkdirSync2(dirPath, { recursive: true });
|
|
40586
|
+
}
|
|
40587
|
+
isMissingDirectoryError(error48) {
|
|
40588
|
+
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
40589
|
+
return message.includes("cannot create directory") || message.includes("No such file or directory");
|
|
40590
|
+
}
|
|
40591
|
+
cleanupFailedWorktree(worktreePath, branch) {
|
|
40592
|
+
try {
|
|
40593
|
+
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
40594
|
+
} catch {}
|
|
40595
|
+
if (existsSync5(worktreePath)) {
|
|
40596
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
40597
|
+
}
|
|
40598
|
+
try {
|
|
40599
|
+
this.git("worktree prune", this.projectPath);
|
|
40600
|
+
} catch {}
|
|
40601
|
+
if (this.branchExists(branch)) {
|
|
40602
|
+
try {
|
|
40603
|
+
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
40604
|
+
} catch {}
|
|
40605
|
+
}
|
|
40606
|
+
}
|
|
40607
|
+
isNonFastForwardPushError(error48) {
|
|
40608
|
+
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
40609
|
+
return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
|
|
40610
|
+
}
|
|
40611
|
+
git(args, cwd) {
|
|
40612
|
+
return execSync(`git ${args}`, {
|
|
40613
|
+
cwd,
|
|
40614
|
+
encoding: "utf-8",
|
|
40615
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
40616
|
+
});
|
|
40617
|
+
}
|
|
40618
|
+
gitExec(args, cwd) {
|
|
40619
|
+
return execFileSync3("git", args, {
|
|
40620
|
+
cwd,
|
|
40621
|
+
encoding: "utf-8",
|
|
40622
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
40623
|
+
});
|
|
40624
|
+
}
|
|
40625
|
+
}
|
|
40626
|
+
|
|
40627
|
+
// ../sdk/src/agent/worker.ts
|
|
40628
|
+
function resolveProvider2(value) {
|
|
40629
|
+
if (!value || value.startsWith("--")) {
|
|
40630
|
+
console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
|
|
40631
|
+
return PROVIDER.CLAUDE;
|
|
40632
|
+
}
|
|
40633
|
+
if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
|
|
40634
|
+
return value;
|
|
40635
|
+
console.warn(`Warning: invalid --provider value '${value}'. Falling back to 'claude'.`);
|
|
40636
|
+
return PROVIDER.CLAUDE;
|
|
40637
|
+
}
|
|
40638
|
+
|
|
40639
|
+
class AgentWorker {
|
|
40640
|
+
config;
|
|
40641
|
+
client;
|
|
40642
|
+
aiRunner;
|
|
40643
|
+
taskExecutor;
|
|
40644
|
+
knowledgeBase;
|
|
40645
|
+
worktreeManager = null;
|
|
40646
|
+
prService = null;
|
|
40647
|
+
maxTasks = 50;
|
|
40648
|
+
tasksCompleted = 0;
|
|
40649
|
+
heartbeatInterval = null;
|
|
40650
|
+
currentTaskId = null;
|
|
40651
|
+
currentWorktreePath = null;
|
|
40652
|
+
postCleanupDelayMs = 5000;
|
|
40653
|
+
ghUsername = null;
|
|
40654
|
+
constructor(config2) {
|
|
40655
|
+
this.config = config2;
|
|
40656
|
+
const projectPath = config2.projectPath || process.cwd();
|
|
40657
|
+
this.client = new LocusClient({
|
|
40658
|
+
baseUrl: config2.apiBase,
|
|
40659
|
+
token: config2.apiKey,
|
|
40660
|
+
retryOptions: {
|
|
40661
|
+
maxRetries: 3,
|
|
40662
|
+
initialDelay: 1000,
|
|
40663
|
+
maxDelay: 5000,
|
|
40664
|
+
factor: 2
|
|
40665
|
+
}
|
|
40666
|
+
});
|
|
40667
|
+
const log2 = this.log.bind(this);
|
|
40668
|
+
if (config2.useWorktrees && !isGitAvailable()) {
|
|
40669
|
+
this.log("git is not installed — worktree isolation will not work", "error");
|
|
40670
|
+
config2.useWorktrees = false;
|
|
40671
|
+
}
|
|
40672
|
+
if (config2.autoPush && !isGhAvailable(projectPath)) {
|
|
40673
|
+
this.log("GitHub CLI (gh) not available or not authenticated. Branch push can continue, but automatic PR creation may fail until gh is configured. Install from https://cli.github.com/", "warn");
|
|
40674
|
+
}
|
|
40675
|
+
if (config2.autoPush) {
|
|
40676
|
+
this.ghUsername = getGhUsername();
|
|
40677
|
+
if (this.ghUsername) {
|
|
40678
|
+
this.log(`GitHub user: ${this.ghUsername}`, "info");
|
|
40679
|
+
}
|
|
40680
|
+
}
|
|
40681
|
+
const provider = config2.provider ?? PROVIDER.CLAUDE;
|
|
40682
|
+
this.aiRunner = createAiRunner(provider, {
|
|
40683
|
+
projectPath,
|
|
40684
|
+
model: config2.model,
|
|
40685
|
+
log: log2
|
|
40686
|
+
});
|
|
40687
|
+
this.taskExecutor = new TaskExecutor({
|
|
40688
|
+
aiRunner: this.aiRunner,
|
|
40689
|
+
projectPath,
|
|
40690
|
+
log: log2
|
|
40691
|
+
});
|
|
40692
|
+
this.knowledgeBase = new KnowledgeBase(projectPath);
|
|
40693
|
+
if (config2.useWorktrees) {
|
|
40694
|
+
this.worktreeManager = new WorktreeManager(projectPath, {
|
|
40695
|
+
cleanupPolicy: "auto"
|
|
40696
|
+
});
|
|
40697
|
+
}
|
|
40698
|
+
if (config2.autoPush) {
|
|
40699
|
+
this.prService = new PrService(projectPath, log2);
|
|
40700
|
+
}
|
|
40701
|
+
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
40702
|
+
this.log(`Using ${providerLabel} CLI for all phases`, "info");
|
|
40703
|
+
if (config2.useWorktrees) {
|
|
40704
|
+
this.log("Per-task worktree isolation enabled", "info");
|
|
40705
|
+
if (config2.autoPush) {
|
|
40706
|
+
this.log("Auto-push enabled: branches will be pushed to remote", "info");
|
|
40707
|
+
}
|
|
40708
|
+
}
|
|
40709
|
+
}
|
|
40710
|
+
log(message, level = "info") {
|
|
40711
|
+
const timestamp2 = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
40712
|
+
const colorFn = {
|
|
40713
|
+
info: c.cyan,
|
|
40714
|
+
success: c.green,
|
|
40715
|
+
warn: c.yellow,
|
|
40716
|
+
error: c.red
|
|
40717
|
+
}[level];
|
|
40718
|
+
const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
|
|
40719
|
+
console.log(`${c.dim(`[${timestamp2}]`)} ${c.bold(`[${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
|
|
40720
|
+
}
|
|
40721
|
+
async getActiveSprint() {
|
|
40722
|
+
try {
|
|
40723
|
+
if (this.config.sprintId) {
|
|
40724
|
+
return await this.client.sprints.getById(this.config.sprintId, this.config.workspaceId);
|
|
40725
|
+
}
|
|
40726
|
+
return await this.client.sprints.getActive(this.config.workspaceId);
|
|
40727
|
+
} catch (_error) {
|
|
40728
|
+
return null;
|
|
40729
|
+
}
|
|
40730
|
+
}
|
|
40731
|
+
async getNextTask() {
|
|
40732
|
+
const maxRetries = 10;
|
|
40733
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
40734
|
+
try {
|
|
40735
|
+
const task2 = await this.client.workspaces.dispatch(this.config.workspaceId, this.config.agentId, this.config.sprintId);
|
|
40736
|
+
return task2;
|
|
40737
|
+
} catch (error48) {
|
|
40738
|
+
const isAxiosError2 = error48 != null && typeof error48 === "object" && "response" in error48 && typeof error48.response?.status === "number";
|
|
40739
|
+
const status = isAxiosError2 ? error48.response.status : 0;
|
|
40740
|
+
if (status === 404) {
|
|
40741
|
+
this.log("No tasks available in the backlog.", "info");
|
|
40742
|
+
return null;
|
|
40743
|
+
}
|
|
40744
|
+
const msg = error48 instanceof Error ? error48.message : String(error48);
|
|
40745
|
+
if (attempt < maxRetries) {
|
|
40746
|
+
this.log(`Nothing dispatched (attempt ${attempt}/${maxRetries}): ${msg}. Retrying in 30s...`, "warn");
|
|
40747
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
40748
|
+
} else {
|
|
40749
|
+
this.log(`Nothing dispatched after ${maxRetries} attempts: ${msg}`, "warn");
|
|
40750
|
+
return null;
|
|
40751
|
+
}
|
|
40752
|
+
}
|
|
40753
|
+
}
|
|
40754
|
+
return null;
|
|
40755
|
+
}
|
|
40756
|
+
createTaskWorktree(task2) {
|
|
40757
|
+
if (!this.worktreeManager) {
|
|
40758
|
+
return {
|
|
40759
|
+
worktreePath: null,
|
|
40760
|
+
baseBranch: null,
|
|
40761
|
+
executor: this.taskExecutor
|
|
40762
|
+
};
|
|
40763
|
+
}
|
|
40764
|
+
const slug = task2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
40765
|
+
const result = this.worktreeManager.create({
|
|
40766
|
+
taskId: task2.id,
|
|
40767
|
+
taskSlug: slug,
|
|
40768
|
+
agentId: this.config.agentId
|
|
40769
|
+
});
|
|
40770
|
+
this.log(`Worktree created: ${result.worktreePath} (${result.branch})`, "info");
|
|
40771
|
+
const log2 = this.log.bind(this);
|
|
40772
|
+
const provider = this.config.provider ?? PROVIDER.CLAUDE;
|
|
40773
|
+
const taskAiRunner = createAiRunner(provider, {
|
|
40774
|
+
projectPath: result.worktreePath,
|
|
40775
|
+
model: this.config.model,
|
|
40776
|
+
log: log2
|
|
40777
|
+
});
|
|
40778
|
+
const taskExecutor = new TaskExecutor({
|
|
40779
|
+
aiRunner: taskAiRunner,
|
|
40780
|
+
projectPath: result.worktreePath,
|
|
40781
|
+
log: log2
|
|
40782
|
+
});
|
|
40783
|
+
return {
|
|
40784
|
+
worktreePath: result.worktreePath,
|
|
40785
|
+
baseBranch: result.baseBranch,
|
|
40786
|
+
executor: taskExecutor
|
|
40787
|
+
};
|
|
40788
|
+
}
|
|
40789
|
+
commitAndPushWorktree(worktreePath, task2, baseBranch) {
|
|
40790
|
+
if (!this.worktreeManager) {
|
|
40791
|
+
return { branch: null, pushed: false, pushFailed: false };
|
|
40792
|
+
}
|
|
40793
|
+
try {
|
|
40794
|
+
const trailers = [
|
|
40795
|
+
`Task-ID: ${task2.id}`,
|
|
40796
|
+
`Agent: ${this.config.agentId}`,
|
|
40797
|
+
"Co-authored-by: LocusAI <noreply@locusai.dev>"
|
|
40798
|
+
];
|
|
40799
|
+
if (this.ghUsername) {
|
|
40800
|
+
trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
|
|
40801
|
+
}
|
|
40802
|
+
const commitMessage = `feat(agent): ${task2.title}
|
|
40803
|
+
|
|
40804
|
+
${trailers.join(`
|
|
40805
|
+
`)}`;
|
|
40806
|
+
const hash2 = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch);
|
|
40807
|
+
if (!hash2) {
|
|
40808
|
+
this.log("No changes to commit for this task", "info");
|
|
40809
|
+
return {
|
|
40810
|
+
branch: null,
|
|
40811
|
+
pushed: false,
|
|
40812
|
+
pushFailed: false,
|
|
40813
|
+
noChanges: true,
|
|
40814
|
+
skipReason: "No changes were committed, so no branch was pushed."
|
|
40815
|
+
};
|
|
40816
|
+
}
|
|
40817
|
+
const localBranch = this.worktreeManager.getBranch(worktreePath);
|
|
40818
|
+
if (this.config.autoPush) {
|
|
40819
|
+
try {
|
|
40820
|
+
return {
|
|
40821
|
+
branch: this.worktreeManager.pushBranch(worktreePath),
|
|
40822
|
+
pushed: true,
|
|
40823
|
+
pushFailed: false
|
|
40824
|
+
};
|
|
40825
|
+
} catch (err) {
|
|
40826
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
40827
|
+
this.log(`Git push failed: ${errorMessage}`, "error");
|
|
40828
|
+
return {
|
|
40829
|
+
branch: localBranch,
|
|
40830
|
+
pushed: false,
|
|
40831
|
+
pushFailed: true,
|
|
40832
|
+
pushError: errorMessage
|
|
40833
|
+
};
|
|
40834
|
+
}
|
|
40835
|
+
}
|
|
40836
|
+
this.log("Auto-push disabled; skipping branch push", "info");
|
|
40837
|
+
return {
|
|
40838
|
+
branch: localBranch,
|
|
40839
|
+
pushed: false,
|
|
40840
|
+
pushFailed: false,
|
|
40841
|
+
skipReason: "Auto-push is disabled, so PR creation was skipped."
|
|
40842
|
+
};
|
|
40843
|
+
} catch (err) {
|
|
40844
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
40845
|
+
this.log(`Git commit failed: ${errorMessage}`, "error");
|
|
40846
|
+
return { branch: null, pushed: false, pushFailed: false };
|
|
40847
|
+
}
|
|
40848
|
+
}
|
|
40849
|
+
createPullRequest(task2, branch, summary, baseBranch) {
|
|
40850
|
+
if (!this.prService) {
|
|
40851
|
+
const errorMessage = "PR service is not initialized. Enable auto-push to allow PR creation.";
|
|
40852
|
+
this.log(`PR creation skipped: ${errorMessage}`, "warn");
|
|
40853
|
+
return { url: null, error: errorMessage };
|
|
40854
|
+
}
|
|
40855
|
+
this.log(`Attempting PR creation from branch: ${branch}`, "info");
|
|
40856
|
+
try {
|
|
40857
|
+
const result = this.prService.createPr({
|
|
40858
|
+
task: task2,
|
|
40859
|
+
branch,
|
|
40860
|
+
baseBranch,
|
|
40861
|
+
agentId: this.config.agentId,
|
|
40862
|
+
summary
|
|
40863
|
+
});
|
|
40864
|
+
return { url: result.url };
|
|
40865
|
+
} catch (err) {
|
|
40866
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
40867
|
+
this.log(`PR creation failed: ${errorMessage}`, "error");
|
|
40868
|
+
return { url: null, error: errorMessage };
|
|
40869
|
+
}
|
|
40870
|
+
}
|
|
40871
|
+
cleanupTaskWorktree(worktreePath, keepBranch) {
|
|
40872
|
+
if (!this.worktreeManager || !worktreePath)
|
|
40873
|
+
return;
|
|
40874
|
+
try {
|
|
40875
|
+
this.worktreeManager.remove(worktreePath, !keepBranch);
|
|
40876
|
+
this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
|
|
40877
|
+
} catch {
|
|
40878
|
+
this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
|
|
40879
|
+
}
|
|
40880
|
+
this.currentWorktreePath = null;
|
|
40881
|
+
}
|
|
40882
|
+
async executeTask(task2) {
|
|
40883
|
+
const fullTask = await this.client.tasks.getById(task2.id, this.config.workspaceId);
|
|
40884
|
+
const { worktreePath, baseBranch, executor } = this.createTaskWorktree(fullTask);
|
|
40885
|
+
this.currentWorktreePath = worktreePath;
|
|
40886
|
+
let branchPushed = false;
|
|
40887
|
+
let keepBranch = false;
|
|
40888
|
+
let preserveWorktree = false;
|
|
40889
|
+
try {
|
|
40890
|
+
const result = await executor.execute(fullTask);
|
|
40891
|
+
let taskBranch = null;
|
|
40892
|
+
let prUrl = null;
|
|
40893
|
+
let prError = null;
|
|
40894
|
+
let noChanges = false;
|
|
40895
|
+
if (result.success && worktreePath) {
|
|
40896
|
+
const commitResult = this.commitAndPushWorktree(worktreePath, fullTask, baseBranch ?? undefined);
|
|
40897
|
+
taskBranch = commitResult.branch;
|
|
40898
|
+
branchPushed = commitResult.pushed;
|
|
40899
|
+
keepBranch = taskBranch !== null;
|
|
40900
|
+
noChanges = Boolean(commitResult.noChanges);
|
|
40901
|
+
if (commitResult.pushFailed) {
|
|
40902
|
+
preserveWorktree = true;
|
|
40903
|
+
prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
|
|
40904
|
+
this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
|
|
40905
|
+
}
|
|
40906
|
+
if (branchPushed && taskBranch) {
|
|
40907
|
+
const prResult = this.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
|
|
40908
|
+
prUrl = prResult.url;
|
|
40909
|
+
prError = prResult.error ?? null;
|
|
40910
|
+
if (!prUrl) {
|
|
40911
|
+
preserveWorktree = true;
|
|
40912
|
+
this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
|
|
40913
|
+
}
|
|
40914
|
+
} else if (commitResult.skipReason) {
|
|
40915
|
+
this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
|
|
40916
|
+
}
|
|
40917
|
+
} else if (result.success && !worktreePath) {
|
|
40918
|
+
this.log("Skipping commit/push/PR flow because no task worktree is active.", "warn");
|
|
40919
|
+
}
|
|
40920
|
+
return {
|
|
40921
|
+
...result,
|
|
40922
|
+
branch: taskBranch ?? undefined,
|
|
40923
|
+
prUrl: prUrl ?? undefined,
|
|
40924
|
+
prError: prError ?? undefined,
|
|
40925
|
+
noChanges: noChanges || undefined
|
|
40926
|
+
};
|
|
40927
|
+
} finally {
|
|
40928
|
+
if (preserveWorktree || keepBranch) {
|
|
40929
|
+
this.currentWorktreePath = null;
|
|
40930
|
+
} else {
|
|
40931
|
+
this.cleanupTaskWorktree(worktreePath, keepBranch);
|
|
40932
|
+
}
|
|
40933
|
+
}
|
|
40934
|
+
}
|
|
40935
|
+
updateProgress(task2, success2) {
|
|
40936
|
+
try {
|
|
40937
|
+
if (success2) {
|
|
40938
|
+
this.knowledgeBase.updateProgress({
|
|
40939
|
+
type: "task_completed",
|
|
40940
|
+
title: task2.title,
|
|
40941
|
+
details: `Agent: ${this.config.agentId.slice(-8)}`
|
|
40942
|
+
});
|
|
40943
|
+
this.log(`Updated progress.md: ${task2.title}`, "info");
|
|
40944
|
+
}
|
|
40945
|
+
} catch (err) {
|
|
40946
|
+
this.log(`Failed to update progress: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
40947
|
+
}
|
|
40948
|
+
}
|
|
40949
|
+
startHeartbeat() {
|
|
40950
|
+
this.sendHeartbeat();
|
|
40951
|
+
this.heartbeatInterval = setInterval(() => {
|
|
40952
|
+
this.sendHeartbeat();
|
|
40953
|
+
}, 60000);
|
|
40954
|
+
}
|
|
40955
|
+
stopHeartbeat() {
|
|
40956
|
+
if (this.heartbeatInterval) {
|
|
40957
|
+
clearInterval(this.heartbeatInterval);
|
|
40958
|
+
this.heartbeatInterval = null;
|
|
40959
|
+
}
|
|
40960
|
+
}
|
|
40961
|
+
sendHeartbeat() {
|
|
40962
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
|
|
40963
|
+
this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
40964
|
+
});
|
|
40965
|
+
}
|
|
40966
|
+
async delayAfterCleanup() {
|
|
40967
|
+
if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
|
|
40968
|
+
return;
|
|
40969
|
+
this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
|
|
40970
|
+
await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
|
|
40971
|
+
}
|
|
40972
|
+
async run() {
|
|
40973
|
+
this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
40974
|
+
const handleShutdown = () => {
|
|
40975
|
+
this.log("Received shutdown signal. Aborting...", "warn");
|
|
40976
|
+
this.aiRunner.abort();
|
|
40977
|
+
this.stopHeartbeat();
|
|
40978
|
+
this.cleanupTaskWorktree(this.currentWorktreePath, false);
|
|
40979
|
+
process.exit(1);
|
|
40980
|
+
};
|
|
40981
|
+
process.on("SIGTERM", handleShutdown);
|
|
40982
|
+
process.on("SIGINT", handleShutdown);
|
|
40983
|
+
this.startHeartbeat();
|
|
40984
|
+
const sprint2 = await this.getActiveSprint();
|
|
40985
|
+
if (sprint2) {
|
|
40986
|
+
this.log(`Active sprint found: ${sprint2.name}`, "info");
|
|
40987
|
+
} else {
|
|
40988
|
+
this.log("No active sprint found.", "warn");
|
|
40989
|
+
}
|
|
40990
|
+
while (this.tasksCompleted < this.maxTasks) {
|
|
40991
|
+
const task2 = await this.getNextTask();
|
|
40992
|
+
if (!task2) {
|
|
40993
|
+
this.log("No more tasks to process. Exiting.", "info");
|
|
40994
|
+
break;
|
|
40995
|
+
}
|
|
40996
|
+
this.log(`Claimed: ${task2.title}`, "success");
|
|
40997
|
+
this.currentTaskId = task2.id;
|
|
40998
|
+
this.sendHeartbeat();
|
|
40999
|
+
const result = await this.executeTask(task2);
|
|
41000
|
+
if (result.success) {
|
|
41001
|
+
if (result.noChanges) {
|
|
41002
|
+
this.log(`Blocked: ${task2.title} - execution produced no file changes`, "warn");
|
|
41003
|
+
await this.client.tasks.update(task2.id, this.config.workspaceId, {
|
|
41004
|
+
status: "BLOCKED" /* BLOCKED */,
|
|
41005
|
+
assignedTo: null
|
|
41006
|
+
});
|
|
41007
|
+
await this.client.tasks.addComment(task2.id, this.config.workspaceId, {
|
|
41008
|
+
author: this.config.agentId,
|
|
41009
|
+
text: `⚠️ Agent execution finished with no file changes, so no commit/branch/PR was created.
|
|
41010
|
+
|
|
41011
|
+
${result.summary}`
|
|
41012
|
+
});
|
|
41013
|
+
} else {
|
|
41014
|
+
this.log(`Completed: ${task2.title}`, "success");
|
|
41015
|
+
const updatePayload = {
|
|
41016
|
+
status: "IN_REVIEW" /* IN_REVIEW */
|
|
41017
|
+
};
|
|
41018
|
+
if (result.prUrl) {
|
|
41019
|
+
updatePayload.prUrl = result.prUrl;
|
|
41020
|
+
}
|
|
41021
|
+
await this.client.tasks.update(task2.id, this.config.workspaceId, updatePayload);
|
|
41022
|
+
const branchInfo = result.branch ? `
|
|
41023
|
+
|
|
41024
|
+
Branch: \`${result.branch}\`` : "";
|
|
41025
|
+
const prInfo = result.prUrl ? `
|
|
41026
|
+
PR: ${result.prUrl}` : "";
|
|
41027
|
+
const prErrorInfo = result.prError ? `
|
|
41028
|
+
PR automation error: ${result.prError}` : "";
|
|
41029
|
+
await this.client.tasks.addComment(task2.id, this.config.workspaceId, {
|
|
41030
|
+
author: this.config.agentId,
|
|
41031
|
+
text: `✅ ${result.summary}${branchInfo}${prInfo}${prErrorInfo}`
|
|
41032
|
+
});
|
|
41033
|
+
this.tasksCompleted++;
|
|
41034
|
+
this.updateProgress(task2, true);
|
|
41035
|
+
if (result.prUrl) {
|
|
41036
|
+
try {
|
|
41037
|
+
this.knowledgeBase.updateProgress({
|
|
41038
|
+
type: "pr_opened",
|
|
41039
|
+
title: task2.title,
|
|
41040
|
+
details: `PR: ${result.prUrl}`
|
|
41041
|
+
});
|
|
41042
|
+
} catch {}
|
|
41043
|
+
}
|
|
41044
|
+
}
|
|
41045
|
+
} else {
|
|
41046
|
+
this.log(`Failed: ${task2.title} - ${result.summary}`, "error");
|
|
41047
|
+
await this.client.tasks.update(task2.id, this.config.workspaceId, {
|
|
41048
|
+
status: "BACKLOG" /* BACKLOG */,
|
|
41049
|
+
assignedTo: null
|
|
41050
|
+
});
|
|
41051
|
+
await this.client.tasks.addComment(task2.id, this.config.workspaceId, {
|
|
41052
|
+
author: this.config.agentId,
|
|
41053
|
+
text: `❌ ${result.summary}`
|
|
41054
|
+
});
|
|
41055
|
+
}
|
|
41056
|
+
this.currentTaskId = null;
|
|
41057
|
+
this.sendHeartbeat();
|
|
41058
|
+
await this.delayAfterCleanup();
|
|
41059
|
+
}
|
|
41060
|
+
this.currentTaskId = null;
|
|
41061
|
+
this.stopHeartbeat();
|
|
41062
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
41063
|
+
process.exit(0);
|
|
41064
|
+
}
|
|
41065
|
+
}
|
|
41066
|
+
var workerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
|
|
41067
|
+
if (workerEntrypoint === "worker.js" || workerEntrypoint === "worker.ts") {
|
|
41068
|
+
process.title = "locus-worker";
|
|
41069
|
+
const args = process.argv.slice(2);
|
|
41070
|
+
const config2 = {};
|
|
41071
|
+
for (let i = 0;i < args.length; i++) {
|
|
41072
|
+
const arg = args[i];
|
|
41073
|
+
if (arg === "--agent-id")
|
|
41074
|
+
config2.agentId = args[++i];
|
|
41075
|
+
else if (arg === "--workspace-id")
|
|
41076
|
+
config2.workspaceId = args[++i];
|
|
41077
|
+
else if (arg === "--sprint-id")
|
|
41078
|
+
config2.sprintId = args[++i];
|
|
41079
|
+
else if (arg === "--api-url")
|
|
41080
|
+
config2.apiBase = args[++i];
|
|
41081
|
+
else if (arg === "--api-key")
|
|
41082
|
+
config2.apiKey = args[++i];
|
|
41083
|
+
else if (arg === "--project-path")
|
|
41084
|
+
config2.projectPath = args[++i];
|
|
41085
|
+
else if (arg === "--main-project-path")
|
|
41086
|
+
config2.mainProjectPath = args[++i];
|
|
41087
|
+
else if (arg === "--model")
|
|
41088
|
+
config2.model = args[++i];
|
|
41089
|
+
else if (arg === "--use-worktrees")
|
|
41090
|
+
config2.useWorktrees = true;
|
|
41091
|
+
else if (arg === "--auto-push")
|
|
41092
|
+
config2.autoPush = true;
|
|
41093
|
+
else if (arg === "--provider") {
|
|
41094
|
+
const value = args[i + 1];
|
|
41095
|
+
if (value && !value.startsWith("--"))
|
|
41096
|
+
i++;
|
|
41097
|
+
config2.provider = resolveProvider2(value);
|
|
41098
|
+
}
|
|
41099
|
+
}
|
|
41100
|
+
if (!config2.agentId || !config2.workspaceId || !config2.apiBase || !config2.apiKey || !config2.projectPath) {
|
|
41101
|
+
console.error("Missing required arguments");
|
|
41102
|
+
process.exit(1);
|
|
41103
|
+
}
|
|
41104
|
+
const worker = new AgentWorker(config2);
|
|
41105
|
+
worker.run().catch((err) => {
|
|
41106
|
+
console.error("Fatal worker error:", err);
|
|
41107
|
+
process.exit(1);
|
|
41108
|
+
});
|
|
41109
|
+
}
|
|
41110
|
+
// ../sdk/src/exec/context-tracker.ts
|
|
41111
|
+
var REFERENCE_ALIASES = {
|
|
41112
|
+
plan: ["the plan", "sprint plan", "project plan", "implementation plan"],
|
|
41113
|
+
document: ["the doc", "that doc", "the document", "that document"],
|
|
41114
|
+
code: ["the code", "that code", "the implementation"],
|
|
41115
|
+
"task-list": ["the tasks", "the task list", "todo list", "todos"],
|
|
41116
|
+
diagram: ["the diagram", "that diagram", "the chart"],
|
|
41117
|
+
config: ["the config", "configuration", "settings"],
|
|
41118
|
+
report: ["the report", "that report"]
|
|
41119
|
+
};
|
|
41120
|
+
function generateArtifactId() {
|
|
41121
|
+
const timestamp2 = Date.now().toString(36);
|
|
41122
|
+
const random = Math.random().toString(36).substring(2, 7);
|
|
41123
|
+
return `artifact-${timestamp2}-${random}`;
|
|
41124
|
+
}
|
|
41125
|
+
function generateTaskId() {
|
|
41126
|
+
const timestamp2 = Date.now().toString(36);
|
|
41127
|
+
const random = Math.random().toString(36).substring(2, 7);
|
|
41128
|
+
return `task-${timestamp2}-${random}`;
|
|
41129
|
+
}
|
|
41130
|
+
|
|
41131
|
+
class ContextTracker {
|
|
41132
|
+
artifacts = new Map;
|
|
41133
|
+
tasks = new Map;
|
|
41134
|
+
createArtifact(params) {
|
|
41135
|
+
const now = Date.now();
|
|
41136
|
+
const artifact = {
|
|
41137
|
+
...params,
|
|
41138
|
+
id: generateArtifactId(),
|
|
41139
|
+
createdAt: now,
|
|
41140
|
+
updatedAt: now
|
|
41141
|
+
};
|
|
41142
|
+
this.artifacts.set(artifact.id, artifact);
|
|
41143
|
+
return artifact;
|
|
41144
|
+
}
|
|
41145
|
+
trackArtifact(artifact) {
|
|
41146
|
+
this.artifacts.set(artifact.id, artifact);
|
|
41147
|
+
}
|
|
41148
|
+
updateArtifact(id, updates) {
|
|
41149
|
+
const artifact = this.artifacts.get(id);
|
|
41150
|
+
if (!artifact) {
|
|
41151
|
+
return null;
|
|
41152
|
+
}
|
|
41153
|
+
const updated = {
|
|
41154
|
+
...artifact,
|
|
41155
|
+
...updates,
|
|
41156
|
+
updatedAt: Date.now()
|
|
41157
|
+
};
|
|
41158
|
+
this.artifacts.set(id, updated);
|
|
41159
|
+
return updated;
|
|
41160
|
+
}
|
|
41161
|
+
getArtifact(id) {
|
|
41162
|
+
return this.artifacts.get(id) ?? null;
|
|
41163
|
+
}
|
|
41164
|
+
getAllArtifacts() {
|
|
41165
|
+
return Array.from(this.artifacts.values());
|
|
41166
|
+
}
|
|
41167
|
+
createTask(params) {
|
|
41168
|
+
const now = Date.now();
|
|
41169
|
+
const task2 = {
|
|
41170
|
+
...params,
|
|
41171
|
+
id: generateTaskId(),
|
|
41172
|
+
createdAt: now,
|
|
41173
|
+
updatedAt: now
|
|
41174
|
+
};
|
|
41175
|
+
this.tasks.set(task2.id, task2);
|
|
41176
|
+
return task2;
|
|
41177
|
+
}
|
|
41178
|
+
trackTask(task2) {
|
|
41179
|
+
this.tasks.set(task2.id, task2);
|
|
41180
|
+
}
|
|
41181
|
+
updateTask(id, updates) {
|
|
41182
|
+
const task2 = this.tasks.get(id);
|
|
41183
|
+
if (!task2) {
|
|
41184
|
+
return null;
|
|
41185
|
+
}
|
|
41186
|
+
const updated = {
|
|
41187
|
+
...task2,
|
|
41188
|
+
...updates,
|
|
41189
|
+
updatedAt: Date.now()
|
|
41190
|
+
};
|
|
41191
|
+
this.tasks.set(id, updated);
|
|
41192
|
+
return updated;
|
|
41193
|
+
}
|
|
41194
|
+
getTask(id) {
|
|
41195
|
+
return this.tasks.get(id) ?? null;
|
|
41196
|
+
}
|
|
41197
|
+
getAllTasks() {
|
|
41198
|
+
return Array.from(this.tasks.values());
|
|
41199
|
+
}
|
|
41200
|
+
getTasksByStatus(status) {
|
|
41201
|
+
return Array.from(this.tasks.values()).filter((t) => t.status === status);
|
|
41202
|
+
}
|
|
41203
|
+
getReferencedArtifact(reference) {
|
|
41204
|
+
const normalizedRef = reference.toLowerCase().trim();
|
|
41205
|
+
const byId = this.artifacts.get(reference);
|
|
41206
|
+
if (byId) {
|
|
41207
|
+
return byId;
|
|
41208
|
+
}
|
|
41209
|
+
for (const artifact of this.artifacts.values()) {
|
|
41210
|
+
if (artifact.title.toLowerCase().includes(normalizedRef)) {
|
|
41211
|
+
return artifact;
|
|
41212
|
+
}
|
|
41213
|
+
}
|
|
41214
|
+
for (const [type, aliases] of Object.entries(REFERENCE_ALIASES)) {
|
|
41215
|
+
if (aliases.some((alias) => normalizedRef.includes(alias))) {
|
|
41216
|
+
const ofType = Array.from(this.artifacts.values()).filter((a) => a.type === type).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
41217
|
+
if (ofType.length > 0) {
|
|
41218
|
+
return ofType[0];
|
|
41219
|
+
}
|
|
41220
|
+
}
|
|
41221
|
+
}
|
|
41222
|
+
const keywords = normalizedRef.split(/\s+/).filter((w) => w.length > 2);
|
|
41223
|
+
for (const artifact of this.artifacts.values()) {
|
|
41224
|
+
const titleLower = artifact.title.toLowerCase();
|
|
41225
|
+
if (keywords.some((kw) => titleLower.includes(kw))) {
|
|
41226
|
+
return artifact;
|
|
41227
|
+
}
|
|
41228
|
+
}
|
|
41229
|
+
return null;
|
|
41230
|
+
}
|
|
41231
|
+
getReferencedTask(reference) {
|
|
41232
|
+
const normalizedRef = reference.toLowerCase().trim();
|
|
41233
|
+
const byId = this.tasks.get(reference);
|
|
41234
|
+
if (byId) {
|
|
41235
|
+
return byId;
|
|
41236
|
+
}
|
|
41237
|
+
for (const task2 of this.tasks.values()) {
|
|
41238
|
+
if (task2.title.toLowerCase().includes(normalizedRef)) {
|
|
41239
|
+
return task2;
|
|
41240
|
+
}
|
|
41241
|
+
}
|
|
41242
|
+
const keywords = normalizedRef.split(/\s+/).filter((w) => w.length > 2);
|
|
41243
|
+
for (const task2 of this.tasks.values()) {
|
|
41244
|
+
const titleLower = task2.title.toLowerCase();
|
|
41245
|
+
if (keywords.some((kw) => titleLower.includes(kw))) {
|
|
41246
|
+
return task2;
|
|
41247
|
+
}
|
|
41248
|
+
}
|
|
41249
|
+
return null;
|
|
41250
|
+
}
|
|
41251
|
+
buildContextSummary() {
|
|
41252
|
+
const artifacts = Array.from(this.artifacts.values());
|
|
41253
|
+
const tasks2 = Array.from(this.tasks.values());
|
|
41254
|
+
if (artifacts.length === 0 && tasks2.length === 0) {
|
|
41255
|
+
return "";
|
|
41256
|
+
}
|
|
41257
|
+
const sections = [];
|
|
41258
|
+
sections.push("## Active Context");
|
|
41259
|
+
if (artifacts.length > 0) {
|
|
41260
|
+
sections.push("");
|
|
41261
|
+
sections.push("### Artifacts Created");
|
|
41262
|
+
for (const artifact of artifacts) {
|
|
41263
|
+
const filePath = artifact.filePath ? ` [${artifact.filePath}]` : "";
|
|
41264
|
+
sections.push(`- ${artifact.title} (${artifact.type})${filePath}`);
|
|
41265
|
+
}
|
|
41266
|
+
}
|
|
41267
|
+
if (tasks2.length > 0) {
|
|
41268
|
+
sections.push("");
|
|
41269
|
+
sections.push("### Tasks");
|
|
41270
|
+
const byStatus = {
|
|
41271
|
+
pending: [],
|
|
41272
|
+
in_progress: [],
|
|
41273
|
+
completed: [],
|
|
41274
|
+
cancelled: []
|
|
41275
|
+
};
|
|
41276
|
+
for (const task2 of tasks2) {
|
|
41277
|
+
byStatus[task2.status].push(task2);
|
|
41278
|
+
}
|
|
41279
|
+
const statusOrder = [
|
|
41280
|
+
"in_progress",
|
|
41281
|
+
"pending",
|
|
41282
|
+
"completed",
|
|
41283
|
+
"cancelled"
|
|
41284
|
+
];
|
|
41285
|
+
for (const status of statusOrder) {
|
|
41286
|
+
const statusTasks = byStatus[status];
|
|
41287
|
+
if (statusTasks.length > 0) {
|
|
41288
|
+
for (const task2 of statusTasks) {
|
|
41289
|
+
const icon = this.getStatusIcon(task2.status);
|
|
41290
|
+
sections.push(`- ${icon} ${task2.title}`);
|
|
41291
|
+
}
|
|
41292
|
+
}
|
|
41293
|
+
}
|
|
41294
|
+
}
|
|
41295
|
+
return sections.join(`
|
|
41296
|
+
`);
|
|
41297
|
+
}
|
|
41298
|
+
getStatusIcon(status) {
|
|
41299
|
+
switch (status) {
|
|
41300
|
+
case "pending":
|
|
41301
|
+
return "○";
|
|
41302
|
+
case "in_progress":
|
|
41303
|
+
return "◐";
|
|
41304
|
+
case "completed":
|
|
41305
|
+
return "●";
|
|
41306
|
+
case "cancelled":
|
|
41307
|
+
return "✕";
|
|
41308
|
+
}
|
|
41309
|
+
}
|
|
41310
|
+
hasContent() {
|
|
41311
|
+
return this.artifacts.size > 0 || this.tasks.size > 0;
|
|
41312
|
+
}
|
|
41313
|
+
clear() {
|
|
41314
|
+
this.artifacts.clear();
|
|
41315
|
+
this.tasks.clear();
|
|
41316
|
+
}
|
|
41317
|
+
toJSON() {
|
|
41318
|
+
return {
|
|
41319
|
+
artifacts: Array.from(this.artifacts.values()),
|
|
41320
|
+
tasks: Array.from(this.tasks.values())
|
|
41321
|
+
};
|
|
41322
|
+
}
|
|
41323
|
+
static fromJSON(state) {
|
|
41324
|
+
const tracker = new ContextTracker;
|
|
41325
|
+
for (const artifact of state.artifacts) {
|
|
41326
|
+
tracker.artifacts.set(artifact.id, artifact);
|
|
41327
|
+
}
|
|
41328
|
+
for (const task2 of state.tasks) {
|
|
41329
|
+
tracker.tasks.set(task2.id, task2);
|
|
41330
|
+
}
|
|
41331
|
+
return tracker;
|
|
41332
|
+
}
|
|
41333
|
+
restore(state) {
|
|
41334
|
+
this.clear();
|
|
41335
|
+
for (const artifact of state.artifacts) {
|
|
41336
|
+
this.artifacts.set(artifact.id, artifact);
|
|
41337
|
+
}
|
|
41338
|
+
for (const task2 of state.tasks) {
|
|
41339
|
+
this.tasks.set(task2.id, task2);
|
|
41340
|
+
}
|
|
41341
|
+
}
|
|
41342
|
+
}
|
|
41343
|
+
// ../sdk/src/exec/event-emitter.ts
|
|
41344
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
41345
|
+
function generateSessionId() {
|
|
41346
|
+
return `exec-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
41347
|
+
}
|
|
41348
|
+
|
|
41349
|
+
class ExecEventEmitter {
|
|
41350
|
+
emitter;
|
|
41351
|
+
sessionId;
|
|
41352
|
+
isSessionActive = false;
|
|
41353
|
+
eventLog = [];
|
|
41354
|
+
debugMode = false;
|
|
41355
|
+
constructor(options) {
|
|
41356
|
+
this.emitter = new EventEmitter3;
|
|
41357
|
+
this.sessionId = generateSessionId();
|
|
41358
|
+
this.debugMode = options?.debug ?? false;
|
|
41359
|
+
}
|
|
41360
|
+
getSessionId() {
|
|
41361
|
+
return this.sessionId;
|
|
41362
|
+
}
|
|
41363
|
+
isActive() {
|
|
41364
|
+
return this.isSessionActive;
|
|
41365
|
+
}
|
|
41366
|
+
getEventLog() {
|
|
41367
|
+
return [...this.eventLog];
|
|
41368
|
+
}
|
|
41369
|
+
clearEventLog() {
|
|
41370
|
+
this.eventLog = [];
|
|
41371
|
+
}
|
|
41372
|
+
on(eventType, listener) {
|
|
41373
|
+
this.emitter.on(eventType, listener);
|
|
41374
|
+
return this;
|
|
41375
|
+
}
|
|
41376
|
+
once(eventType, listener) {
|
|
41377
|
+
this.emitter.once(eventType, listener);
|
|
41378
|
+
return this;
|
|
41379
|
+
}
|
|
41380
|
+
off(eventType, listener) {
|
|
41381
|
+
this.emitter.off(eventType, listener);
|
|
41382
|
+
return this;
|
|
41383
|
+
}
|
|
41384
|
+
removeAllListeners(eventType) {
|
|
41385
|
+
if (eventType) {
|
|
41386
|
+
this.emitter.removeAllListeners(eventType);
|
|
41387
|
+
} else {
|
|
41388
|
+
this.emitter.removeAllListeners();
|
|
41389
|
+
}
|
|
41390
|
+
return this;
|
|
41391
|
+
}
|
|
41392
|
+
emit(event) {
|
|
41393
|
+
if (this.debugMode) {
|
|
41394
|
+
this.eventLog.push(event);
|
|
41395
|
+
}
|
|
41396
|
+
this.emitter.emit(event.type, event);
|
|
41397
|
+
}
|
|
41398
|
+
createEventBase(type) {
|
|
41399
|
+
return {
|
|
41400
|
+
type,
|
|
41401
|
+
timestamp: Date.now()
|
|
41402
|
+
};
|
|
41403
|
+
}
|
|
41404
|
+
emitSessionStarted(options) {
|
|
41405
|
+
this.isSessionActive = true;
|
|
41406
|
+
this.emit({
|
|
41407
|
+
...this.createEventBase("session:started" /* SESSION_STARTED */),
|
|
41408
|
+
data: {
|
|
41409
|
+
sessionId: this.sessionId,
|
|
41410
|
+
model: options?.model,
|
|
41411
|
+
provider: options?.provider
|
|
41412
|
+
}
|
|
41413
|
+
});
|
|
41414
|
+
}
|
|
41415
|
+
emitPromptSubmitted(prompt, truncated = false) {
|
|
41416
|
+
this.emit({
|
|
41417
|
+
...this.createEventBase("prompt:submitted" /* PROMPT_SUBMITTED */),
|
|
41418
|
+
data: {
|
|
41419
|
+
prompt: truncated ? `${prompt.substring(0, 500)}...` : prompt,
|
|
41420
|
+
truncated
|
|
41421
|
+
}
|
|
41422
|
+
});
|
|
41423
|
+
}
|
|
41424
|
+
emitThinkingStarted(content) {
|
|
41425
|
+
this.emit({
|
|
41426
|
+
...this.createEventBase("thinking:started" /* THINKING_STARTED */),
|
|
41427
|
+
data: {
|
|
41428
|
+
content
|
|
41429
|
+
}
|
|
41430
|
+
});
|
|
41431
|
+
}
|
|
41432
|
+
emitThinkingStoped() {
|
|
41433
|
+
this.emit({
|
|
41434
|
+
...this.createEventBase("thinking:stopped" /* THINKING_STOPPED */),
|
|
41435
|
+
data: {}
|
|
41436
|
+
});
|
|
41437
|
+
}
|
|
41438
|
+
emitToolStarted(toolName, toolId) {
|
|
41439
|
+
this.emit({
|
|
41440
|
+
...this.createEventBase("tool:started" /* TOOL_STARTED */),
|
|
41441
|
+
data: {
|
|
41442
|
+
toolName,
|
|
41443
|
+
toolId
|
|
41444
|
+
}
|
|
41445
|
+
});
|
|
41446
|
+
}
|
|
41447
|
+
emitToolCompleted(toolName, toolId, result, duration3) {
|
|
41448
|
+
this.emit({
|
|
41449
|
+
...this.createEventBase("tool:completed" /* TOOL_COMPLETED */),
|
|
41450
|
+
data: {
|
|
41451
|
+
toolName,
|
|
41452
|
+
toolId,
|
|
41453
|
+
result,
|
|
41454
|
+
duration: duration3
|
|
41455
|
+
}
|
|
41456
|
+
});
|
|
41457
|
+
}
|
|
41458
|
+
emitToolFailed(toolName, error48, toolId) {
|
|
41459
|
+
this.emit({
|
|
41460
|
+
...this.createEventBase("tool:failed" /* TOOL_FAILED */),
|
|
41461
|
+
data: {
|
|
41462
|
+
toolName,
|
|
41463
|
+
toolId,
|
|
41464
|
+
error: error48
|
|
41465
|
+
}
|
|
41466
|
+
});
|
|
41467
|
+
}
|
|
41468
|
+
emitTextDelta(content) {
|
|
41469
|
+
this.emit({
|
|
41470
|
+
...this.createEventBase("text:delta" /* TEXT_DELTA */),
|
|
41471
|
+
data: {
|
|
41472
|
+
content
|
|
41473
|
+
}
|
|
41474
|
+
});
|
|
41475
|
+
}
|
|
41476
|
+
emitResponseCompleted(content) {
|
|
41477
|
+
this.emit({
|
|
41478
|
+
...this.createEventBase("response:completed" /* RESPONSE_COMPLETED */),
|
|
41479
|
+
data: {
|
|
41480
|
+
content
|
|
41481
|
+
}
|
|
41482
|
+
});
|
|
41483
|
+
}
|
|
41484
|
+
emitErrorOccurred(error48, code) {
|
|
41485
|
+
this.emit({
|
|
41486
|
+
...this.createEventBase("error:occurred" /* ERROR_OCCURRED */),
|
|
41487
|
+
data: {
|
|
41488
|
+
error: error48,
|
|
41489
|
+
code
|
|
41490
|
+
}
|
|
41491
|
+
});
|
|
41492
|
+
}
|
|
41493
|
+
emitSessionEnded(success2) {
|
|
41494
|
+
this.isSessionActive = false;
|
|
41495
|
+
this.emit({
|
|
41496
|
+
...this.createEventBase("session:ended" /* SESSION_ENDED */),
|
|
41497
|
+
data: {
|
|
41498
|
+
sessionId: this.sessionId,
|
|
41499
|
+
success: success2
|
|
41500
|
+
}
|
|
41501
|
+
});
|
|
41502
|
+
}
|
|
41503
|
+
}
|
|
41504
|
+
// ../sdk/src/exec/history-manager.ts
|
|
41505
|
+
import {
|
|
41506
|
+
existsSync as existsSync6,
|
|
41507
|
+
mkdirSync as mkdirSync3,
|
|
41508
|
+
readdirSync as readdirSync2,
|
|
41509
|
+
readFileSync as readFileSync4,
|
|
41510
|
+
rmSync as rmSync2,
|
|
41511
|
+
writeFileSync as writeFileSync2
|
|
41512
|
+
} from "node:fs";
|
|
41513
|
+
import { join as join7 } from "node:path";
|
|
41514
|
+
var DEFAULT_MAX_SESSIONS = 30;
|
|
41515
|
+
function generateSessionId2() {
|
|
41516
|
+
const timestamp2 = Date.now().toString(36);
|
|
41517
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
41518
|
+
return `session-${timestamp2}-${random}`;
|
|
41519
|
+
}
|
|
41520
|
+
|
|
41521
|
+
class HistoryManager {
|
|
41522
|
+
historyDir;
|
|
41523
|
+
maxSessions;
|
|
41524
|
+
constructor(projectPath, options) {
|
|
41525
|
+
this.historyDir = options?.historyDir ?? join7(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
|
|
41526
|
+
this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
41527
|
+
this.ensureHistoryDir();
|
|
41528
|
+
}
|
|
41529
|
+
ensureHistoryDir() {
|
|
41530
|
+
if (!existsSync6(this.historyDir)) {
|
|
41531
|
+
mkdirSync3(this.historyDir, { recursive: true });
|
|
41532
|
+
}
|
|
41533
|
+
}
|
|
41534
|
+
getSessionPath(sessionId) {
|
|
41535
|
+
return join7(this.historyDir, `${sessionId}.json`);
|
|
41536
|
+
}
|
|
41537
|
+
saveSession(session) {
|
|
41538
|
+
const filePath = this.getSessionPath(session.id);
|
|
41539
|
+
session.updatedAt = Date.now();
|
|
41540
|
+
writeFileSync2(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
41541
|
+
}
|
|
41542
|
+
loadSession(sessionId) {
|
|
41543
|
+
const filePath = this.getSessionPath(sessionId);
|
|
41544
|
+
if (!existsSync6(filePath)) {
|
|
41545
|
+
return null;
|
|
41546
|
+
}
|
|
41547
|
+
try {
|
|
41548
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
41549
|
+
return JSON.parse(content);
|
|
41550
|
+
} catch {
|
|
41551
|
+
return null;
|
|
41552
|
+
}
|
|
41553
|
+
}
|
|
41554
|
+
deleteSession(sessionId) {
|
|
41555
|
+
const filePath = this.getSessionPath(sessionId);
|
|
41556
|
+
if (!existsSync6(filePath)) {
|
|
41557
|
+
return false;
|
|
41558
|
+
}
|
|
41559
|
+
try {
|
|
41560
|
+
rmSync2(filePath);
|
|
41561
|
+
return true;
|
|
41562
|
+
} catch {
|
|
41563
|
+
return false;
|
|
41564
|
+
}
|
|
41565
|
+
}
|
|
41566
|
+
listSessions(options) {
|
|
41567
|
+
const files = readdirSync2(this.historyDir);
|
|
41568
|
+
let sessions = [];
|
|
41569
|
+
for (const file2 of files) {
|
|
41570
|
+
if (file2.endsWith(".json")) {
|
|
41571
|
+
const session = this.loadSession(file2.replace(".json", ""));
|
|
41572
|
+
if (session) {
|
|
41573
|
+
sessions.push(session);
|
|
41574
|
+
}
|
|
41575
|
+
}
|
|
41576
|
+
}
|
|
41577
|
+
sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
41578
|
+
if (options) {
|
|
41579
|
+
sessions = this.filterSessions(sessions, options);
|
|
41580
|
+
}
|
|
41581
|
+
return sessions;
|
|
41582
|
+
}
|
|
41583
|
+
filterSessions(sessions, options) {
|
|
41584
|
+
let filtered = sessions;
|
|
41585
|
+
if (options.after !== undefined) {
|
|
41586
|
+
const after = options.after;
|
|
41587
|
+
filtered = filtered.filter((s) => s.createdAt >= after);
|
|
41588
|
+
}
|
|
41589
|
+
if (options.before !== undefined) {
|
|
41590
|
+
const before = options.before;
|
|
41591
|
+
filtered = filtered.filter((s) => s.createdAt <= before);
|
|
41592
|
+
}
|
|
41593
|
+
if (options.query) {
|
|
41594
|
+
const query = options.query.toLowerCase();
|
|
41595
|
+
filtered = filtered.filter((session) => session.messages.some((msg) => msg.content.toLowerCase().includes(query)));
|
|
41596
|
+
}
|
|
41597
|
+
const offset = options.offset ?? 0;
|
|
41598
|
+
const limit = options.limit ?? filtered.length;
|
|
41599
|
+
filtered = filtered.slice(offset, offset + limit);
|
|
41600
|
+
return filtered;
|
|
41601
|
+
}
|
|
41602
|
+
searchSessions(query, limit) {
|
|
41603
|
+
return this.listSessions({ query, limit });
|
|
41604
|
+
}
|
|
41605
|
+
getCurrentSession(model = "claude-sonnet-4-5", provider = "claude") {
|
|
41606
|
+
const sessions = this.listSessions({ limit: 1 });
|
|
41607
|
+
if (sessions.length > 0) {
|
|
41608
|
+
return sessions[0];
|
|
41609
|
+
}
|
|
41610
|
+
return this.createNewSession(model, provider);
|
|
41611
|
+
}
|
|
41612
|
+
createNewSession(model = "claude-sonnet-4-5", provider = "claude") {
|
|
41613
|
+
const now = Date.now();
|
|
41614
|
+
return {
|
|
41615
|
+
id: generateSessionId2(),
|
|
41616
|
+
projectPath: this.historyDir.replace(`/${LOCUS_CONFIG.dir}/${LOCUS_CONFIG.sessionsDir}`, ""),
|
|
41617
|
+
messages: [],
|
|
41618
|
+
createdAt: now,
|
|
41619
|
+
updatedAt: now,
|
|
41620
|
+
metadata: {
|
|
41621
|
+
model,
|
|
41622
|
+
provider
|
|
41623
|
+
}
|
|
41624
|
+
};
|
|
41625
|
+
}
|
|
41626
|
+
pruneSessions() {
|
|
41627
|
+
const sessions = this.listSessions();
|
|
41628
|
+
let deleted = 0;
|
|
41629
|
+
if (sessions.length > this.maxSessions) {
|
|
41630
|
+
const sessionsToDelete = sessions.slice(this.maxSessions);
|
|
41631
|
+
for (const session of sessionsToDelete) {
|
|
41632
|
+
if (this.deleteSession(session.id)) {
|
|
41633
|
+
deleted++;
|
|
41634
|
+
}
|
|
41635
|
+
}
|
|
41636
|
+
}
|
|
41637
|
+
return deleted;
|
|
41638
|
+
}
|
|
41639
|
+
getSessionCount() {
|
|
41640
|
+
const files = readdirSync2(this.historyDir);
|
|
41641
|
+
return files.filter((f) => f.endsWith(".json")).length;
|
|
41642
|
+
}
|
|
41643
|
+
sessionExists(sessionId) {
|
|
41644
|
+
return existsSync6(this.getSessionPath(sessionId));
|
|
41645
|
+
}
|
|
41646
|
+
findSessionByPartialId(partialId) {
|
|
41647
|
+
const sessions = this.listSessions();
|
|
41648
|
+
const exact = sessions.find((s) => s.id === partialId);
|
|
41649
|
+
if (exact)
|
|
41650
|
+
return exact;
|
|
41651
|
+
const partial2 = sessions.find((s) => s.id.includes(partialId) || s.id.startsWith(`session-${partialId}`));
|
|
41652
|
+
return partial2 ?? null;
|
|
41653
|
+
}
|
|
41654
|
+
getHistoryDir() {
|
|
41655
|
+
return this.historyDir;
|
|
41656
|
+
}
|
|
41657
|
+
clearAllSessions() {
|
|
41658
|
+
const files = readdirSync2(this.historyDir);
|
|
41659
|
+
let deleted = 0;
|
|
41660
|
+
for (const file2 of files) {
|
|
41661
|
+
if (file2.endsWith(".json")) {
|
|
41662
|
+
try {
|
|
41663
|
+
rmSync2(join7(this.historyDir, file2));
|
|
41664
|
+
deleted++;
|
|
41665
|
+
} catch {}
|
|
41666
|
+
}
|
|
41667
|
+
}
|
|
41668
|
+
return deleted;
|
|
41669
|
+
}
|
|
41670
|
+
}
|
|
41671
|
+
|
|
41672
|
+
// ../sdk/src/exec/exec-session.ts
|
|
41673
|
+
var DEFAULT_MAX_CONTEXT_MESSAGES = 10;
|
|
41674
|
+
|
|
41675
|
+
class ExecSession {
|
|
41676
|
+
aiRunner;
|
|
41677
|
+
history;
|
|
41678
|
+
currentSession = null;
|
|
41679
|
+
eventEmitter;
|
|
41680
|
+
contextTracker;
|
|
41681
|
+
maxContextMessages;
|
|
41682
|
+
model;
|
|
41683
|
+
provider;
|
|
41684
|
+
sessionId;
|
|
41685
|
+
toolStartTimes = new Map;
|
|
41686
|
+
constructor(config2) {
|
|
41687
|
+
this.aiRunner = config2.aiRunner;
|
|
41688
|
+
this.history = new HistoryManager(config2.projectPath);
|
|
41689
|
+
this.eventEmitter = new ExecEventEmitter({ debug: config2.debug });
|
|
41690
|
+
this.contextTracker = new ContextTracker;
|
|
41691
|
+
this.maxContextMessages = config2.maxContextMessages ?? DEFAULT_MAX_CONTEXT_MESSAGES;
|
|
41692
|
+
this.model = config2.model;
|
|
41693
|
+
this.provider = config2.provider;
|
|
41694
|
+
this.sessionId = config2.sessionId;
|
|
41695
|
+
}
|
|
41696
|
+
initialize() {
|
|
41697
|
+
if (this.sessionId) {
|
|
41698
|
+
const loaded = this.history.loadSession(this.sessionId);
|
|
41699
|
+
if (loaded) {
|
|
41700
|
+
this.currentSession = loaded;
|
|
41701
|
+
const metadata = loaded.metadata;
|
|
41702
|
+
if (metadata.contextTracker) {
|
|
41703
|
+
this.contextTracker.restore(metadata.contextTracker);
|
|
41704
|
+
}
|
|
41705
|
+
} else {
|
|
41706
|
+
this.currentSession = this.history.createNewSession(this.model, this.provider);
|
|
41707
|
+
}
|
|
41708
|
+
} else {
|
|
41709
|
+
this.currentSession = this.history.createNewSession(this.model, this.provider);
|
|
41710
|
+
}
|
|
41711
|
+
this.eventEmitter.emitSessionStarted({
|
|
41712
|
+
model: this.model,
|
|
41713
|
+
provider: this.provider
|
|
41714
|
+
});
|
|
41715
|
+
}
|
|
41716
|
+
getSession() {
|
|
41717
|
+
return this.currentSession;
|
|
41718
|
+
}
|
|
41719
|
+
getSessionId() {
|
|
41720
|
+
return this.currentSession?.id ?? null;
|
|
41721
|
+
}
|
|
41722
|
+
getEventEmitter() {
|
|
41723
|
+
return this.eventEmitter;
|
|
41724
|
+
}
|
|
41725
|
+
getHistoryManager() {
|
|
41726
|
+
return this.history;
|
|
41727
|
+
}
|
|
41728
|
+
getContextTracker() {
|
|
41729
|
+
return this.contextTracker;
|
|
41730
|
+
}
|
|
41731
|
+
createArtifact(params) {
|
|
41732
|
+
return this.contextTracker.createArtifact(params);
|
|
41733
|
+
}
|
|
41734
|
+
createTask(params) {
|
|
41735
|
+
return this.contextTracker.createTask(params);
|
|
41736
|
+
}
|
|
41737
|
+
resolveArtifactReference(reference) {
|
|
41738
|
+
return this.contextTracker.getReferencedArtifact(reference);
|
|
41739
|
+
}
|
|
41740
|
+
resolveTaskReference(reference) {
|
|
41741
|
+
return this.contextTracker.getReferencedTask(reference);
|
|
41742
|
+
}
|
|
41743
|
+
getMessages() {
|
|
41744
|
+
return this.currentSession?.messages ?? [];
|
|
41745
|
+
}
|
|
41746
|
+
addMessage(message) {
|
|
41747
|
+
if (!this.currentSession) {
|
|
41748
|
+
throw new Error("Session not initialized. Call initialize() first.");
|
|
41749
|
+
}
|
|
41750
|
+
this.currentSession.messages.push({
|
|
41751
|
+
...message,
|
|
41752
|
+
timestamp: Date.now()
|
|
41753
|
+
});
|
|
41754
|
+
}
|
|
41755
|
+
async* executeStreaming(userPrompt) {
|
|
41756
|
+
if (!this.currentSession) {
|
|
41757
|
+
throw new Error("Session not initialized. Call initialize() first.");
|
|
41758
|
+
}
|
|
41759
|
+
const startTime = Date.now();
|
|
41760
|
+
this.eventEmitter.emitPromptSubmitted(userPrompt, userPrompt.length > 500);
|
|
41761
|
+
this.currentSession.messages.push({
|
|
41762
|
+
role: "user",
|
|
41763
|
+
content: userPrompt,
|
|
41764
|
+
timestamp: Date.now()
|
|
41765
|
+
});
|
|
41766
|
+
const fullPrompt = this.buildPromptWithHistory(userPrompt);
|
|
41767
|
+
const assistantMessage = {
|
|
41768
|
+
role: "assistant",
|
|
41769
|
+
content: "",
|
|
41770
|
+
timestamp: Date.now(),
|
|
41771
|
+
metadata: { toolsUsed: [], duration: 0 }
|
|
41772
|
+
};
|
|
41773
|
+
let hasError = false;
|
|
41774
|
+
let errorMessage = "";
|
|
41775
|
+
try {
|
|
41776
|
+
const stream4 = this.aiRunner.runStream(fullPrompt);
|
|
41777
|
+
for await (const chunk of stream4) {
|
|
41778
|
+
switch (chunk.type) {
|
|
41779
|
+
case "text_delta":
|
|
41780
|
+
assistantMessage.content += chunk.content;
|
|
41781
|
+
this.eventEmitter.emitTextDelta(chunk.content);
|
|
41782
|
+
break;
|
|
41783
|
+
case "tool_use": {
|
|
41784
|
+
assistantMessage.metadata?.toolsUsed?.push(chunk.tool);
|
|
41785
|
+
const toolKey = chunk.id ?? `${chunk.tool}-${Date.now()}`;
|
|
41786
|
+
this.toolStartTimes.set(toolKey, Date.now());
|
|
41787
|
+
this.eventEmitter.emitToolStarted(chunk.tool, chunk.id);
|
|
41788
|
+
break;
|
|
41789
|
+
}
|
|
41790
|
+
case "thinking":
|
|
41791
|
+
this.eventEmitter.emitThinkingStarted(chunk.content);
|
|
41792
|
+
break;
|
|
41793
|
+
case "tool_result": {
|
|
41794
|
+
const resultKey = chunk.id ?? chunk.tool;
|
|
41795
|
+
const startTime2 = this.toolStartTimes.get(resultKey);
|
|
41796
|
+
const duration4 = startTime2 ? Date.now() - startTime2 : undefined;
|
|
41797
|
+
if (chunk.success) {
|
|
41798
|
+
this.eventEmitter.emitToolCompleted(chunk.tool, chunk.id, undefined, duration4);
|
|
41799
|
+
} else {
|
|
41800
|
+
this.eventEmitter.emitToolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
|
|
41801
|
+
}
|
|
41802
|
+
if (resultKey) {
|
|
41803
|
+
this.toolStartTimes.delete(resultKey);
|
|
41804
|
+
}
|
|
41805
|
+
break;
|
|
41806
|
+
}
|
|
41807
|
+
case "result":
|
|
41808
|
+
this.eventEmitter.emitResponseCompleted(chunk.content);
|
|
41809
|
+
break;
|
|
41810
|
+
case "error":
|
|
41811
|
+
hasError = true;
|
|
41812
|
+
errorMessage = chunk.error;
|
|
41813
|
+
this.eventEmitter.emitErrorOccurred(chunk.error);
|
|
41814
|
+
break;
|
|
41815
|
+
}
|
|
41816
|
+
yield chunk;
|
|
41817
|
+
}
|
|
41818
|
+
} catch (error48) {
|
|
41819
|
+
hasError = true;
|
|
41820
|
+
errorMessage = error48 instanceof Error ? error48.message : String(error48);
|
|
41821
|
+
this.eventEmitter.emitErrorOccurred(errorMessage);
|
|
41822
|
+
yield {
|
|
41823
|
+
type: "error",
|
|
41824
|
+
error: errorMessage
|
|
41825
|
+
};
|
|
41826
|
+
}
|
|
41827
|
+
const duration3 = Date.now() - startTime;
|
|
41828
|
+
if (assistantMessage.metadata) {
|
|
41829
|
+
assistantMessage.metadata.duration = duration3;
|
|
41830
|
+
}
|
|
41831
|
+
if (assistantMessage.content || hasError) {
|
|
41832
|
+
if (hasError && !assistantMessage.content) {
|
|
41833
|
+
assistantMessage.content = `Error: ${errorMessage}`;
|
|
41834
|
+
}
|
|
41835
|
+
this.currentSession.messages.push(assistantMessage);
|
|
41836
|
+
}
|
|
41837
|
+
this.currentSession.updatedAt = Date.now();
|
|
41838
|
+
}
|
|
41839
|
+
async execute(userPrompt) {
|
|
41840
|
+
const chunks = [];
|
|
41841
|
+
let content = "";
|
|
41842
|
+
const toolsUsed = [];
|
|
41843
|
+
const startTime = Date.now();
|
|
41844
|
+
let hasError = false;
|
|
41845
|
+
let errorMessage = "";
|
|
41846
|
+
for await (const chunk of this.executeStreaming(userPrompt)) {
|
|
41847
|
+
chunks.push(chunk);
|
|
41848
|
+
if (chunk.type === "text_delta") {
|
|
41849
|
+
content += chunk.content;
|
|
41850
|
+
} else if (chunk.type === "tool_use") {
|
|
41851
|
+
toolsUsed.push(chunk.tool);
|
|
41852
|
+
} else if (chunk.type === "error") {
|
|
41853
|
+
hasError = true;
|
|
41854
|
+
errorMessage = chunk.error;
|
|
41855
|
+
}
|
|
41856
|
+
}
|
|
41857
|
+
return {
|
|
41858
|
+
content,
|
|
41859
|
+
toolsUsed,
|
|
41860
|
+
duration: Date.now() - startTime,
|
|
41861
|
+
success: !hasError,
|
|
41862
|
+
error: hasError ? errorMessage : undefined
|
|
41863
|
+
};
|
|
41864
|
+
}
|
|
41865
|
+
buildPromptWithHistory(currentPrompt) {
|
|
41866
|
+
const sections = [];
|
|
41867
|
+
const contextSummary = this.contextTracker.buildContextSummary();
|
|
41868
|
+
if (contextSummary) {
|
|
41869
|
+
sections.push(contextSummary);
|
|
41870
|
+
}
|
|
41871
|
+
if (this.currentSession && this.currentSession.messages.length > 1) {
|
|
41872
|
+
const recentMessages = this.currentSession.messages.slice(-(this.maxContextMessages + 1), -1).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join(`
|
|
41873
|
+
|
|
41874
|
+
`);
|
|
41875
|
+
if (recentMessages) {
|
|
41876
|
+
sections.push(`## Conversation History
|
|
41877
|
+
${recentMessages}`);
|
|
41878
|
+
}
|
|
41879
|
+
}
|
|
41880
|
+
if (sections.length === 0) {
|
|
41881
|
+
return currentPrompt;
|
|
41882
|
+
}
|
|
41883
|
+
sections.push(`## Current Request
|
|
41884
|
+
${currentPrompt}`);
|
|
41885
|
+
return sections.join(`
|
|
41886
|
+
|
|
41887
|
+
`);
|
|
41888
|
+
}
|
|
41889
|
+
save() {
|
|
41890
|
+
if (!this.currentSession) {
|
|
41891
|
+
throw new Error("Session not initialized. Call initialize() first.");
|
|
41892
|
+
}
|
|
41893
|
+
if (this.contextTracker.hasContent()) {
|
|
41894
|
+
this.currentSession.metadata.contextTracker = this.contextTracker.toJSON();
|
|
41895
|
+
}
|
|
41896
|
+
this.history.saveSession(this.currentSession);
|
|
41897
|
+
this.history.pruneSessions();
|
|
41898
|
+
}
|
|
41899
|
+
reset() {
|
|
41900
|
+
if (!this.currentSession) {
|
|
41901
|
+
throw new Error("Session not initialized. Call initialize() first.");
|
|
41902
|
+
}
|
|
41903
|
+
this.currentSession.messages = [];
|
|
41904
|
+
this.currentSession.updatedAt = Date.now();
|
|
41905
|
+
this.contextTracker.clear();
|
|
41906
|
+
}
|
|
41907
|
+
startNewSession() {
|
|
41908
|
+
this.currentSession = this.history.createNewSession(this.model, this.provider);
|
|
41909
|
+
this.contextTracker.clear();
|
|
41910
|
+
this.eventEmitter.emitSessionStarted({
|
|
41911
|
+
model: this.model,
|
|
41912
|
+
provider: this.provider
|
|
41913
|
+
});
|
|
41914
|
+
}
|
|
41915
|
+
end(success2 = true) {
|
|
41916
|
+
this.eventEmitter.emitSessionEnded(success2);
|
|
41917
|
+
}
|
|
41918
|
+
on(eventType, listener) {
|
|
41919
|
+
this.eventEmitter.on(eventType, listener);
|
|
41920
|
+
return this;
|
|
41921
|
+
}
|
|
41922
|
+
off(eventType, listener) {
|
|
41923
|
+
this.eventEmitter.off(eventType, listener);
|
|
41924
|
+
return this;
|
|
41925
|
+
}
|
|
41926
|
+
}
|
|
41927
|
+
// ../sdk/src/orchestrator.ts
|
|
41928
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
41929
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
41930
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
41931
|
+
import { fileURLToPath } from "node:url";
|
|
41932
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
41933
|
+
var MAX_AGENTS = 5;
|
|
41934
|
+
|
|
41935
|
+
class AgentOrchestrator extends EventEmitter4 {
|
|
41936
|
+
client;
|
|
41937
|
+
config;
|
|
41938
|
+
agents = new Map;
|
|
41939
|
+
isRunning = false;
|
|
41940
|
+
processedTasks = new Set;
|
|
41941
|
+
resolvedSprintId = null;
|
|
41942
|
+
worktreeManager = null;
|
|
41943
|
+
heartbeatInterval = null;
|
|
41944
|
+
constructor(config2) {
|
|
41945
|
+
super();
|
|
41946
|
+
this.config = config2;
|
|
41947
|
+
this.client = new LocusClient({
|
|
41948
|
+
baseUrl: config2.apiBase,
|
|
41949
|
+
token: config2.apiKey
|
|
41950
|
+
});
|
|
41951
|
+
}
|
|
41952
|
+
get agentCount() {
|
|
41953
|
+
return Math.min(Math.max(this.config.agentCount ?? 1, 1), MAX_AGENTS);
|
|
41954
|
+
}
|
|
41955
|
+
get useWorktrees() {
|
|
41956
|
+
return this.config.useWorktrees ?? true;
|
|
41957
|
+
}
|
|
41958
|
+
get worktreeCleanupPolicy() {
|
|
41959
|
+
return this.config.worktreeCleanupPolicy ?? "retain-on-failure";
|
|
41960
|
+
}
|
|
41961
|
+
async resolveSprintId() {
|
|
41962
|
+
if (this.config.sprintId) {
|
|
41963
|
+
return this.config.sprintId;
|
|
41964
|
+
}
|
|
41965
|
+
try {
|
|
41966
|
+
const sprint2 = await this.client.sprints.getActive(this.config.workspaceId);
|
|
41967
|
+
if (sprint2?.id) {
|
|
41968
|
+
console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint2.name}`));
|
|
41969
|
+
return sprint2.id;
|
|
41970
|
+
}
|
|
41971
|
+
} catch {}
|
|
41972
|
+
console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
|
|
41973
|
+
return "";
|
|
41974
|
+
}
|
|
41975
|
+
async start() {
|
|
41976
|
+
if (this.isRunning) {
|
|
41977
|
+
throw new Error("Orchestrator is already running");
|
|
41978
|
+
}
|
|
41979
|
+
this.isRunning = true;
|
|
41980
|
+
this.processedTasks.clear();
|
|
41981
|
+
try {
|
|
41982
|
+
await this.orchestrationLoop();
|
|
41983
|
+
} catch (error48) {
|
|
41984
|
+
this.emit("error", error48);
|
|
41985
|
+
throw error48;
|
|
41986
|
+
} finally {
|
|
41987
|
+
await this.cleanup();
|
|
41988
|
+
}
|
|
41989
|
+
}
|
|
41990
|
+
async orchestrationLoop() {
|
|
41991
|
+
this.resolvedSprintId = await this.resolveSprintId();
|
|
41992
|
+
this.emit("started", {
|
|
41993
|
+
timestamp: new Date,
|
|
41994
|
+
config: this.config,
|
|
41995
|
+
sprintId: this.resolvedSprintId
|
|
41996
|
+
});
|
|
41997
|
+
console.log(`
|
|
41998
|
+
${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
41999
|
+
console.log(c.dim("----------------------------------------------"));
|
|
42000
|
+
console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
|
|
42001
|
+
if (this.resolvedSprintId) {
|
|
42002
|
+
console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
|
|
42003
|
+
}
|
|
42004
|
+
console.log(`${c.bold("Agents:")} ${this.agentCount}`);
|
|
42005
|
+
console.log(`${c.bold("Worktrees:")} ${this.useWorktrees ? "enabled" : "disabled"}`);
|
|
42006
|
+
if (this.useWorktrees) {
|
|
42007
|
+
console.log(`${c.bold("Cleanup policy:")} ${this.worktreeCleanupPolicy}`);
|
|
42008
|
+
console.log(`${c.bold("Auto-push:")} ${this.config.autoPush ? "enabled" : "disabled"}`);
|
|
42009
|
+
}
|
|
42010
|
+
console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
|
|
42011
|
+
console.log(c.dim(`----------------------------------------------
|
|
42012
|
+
`));
|
|
42013
|
+
const tasks2 = await this.getAvailableTasks();
|
|
42014
|
+
if (tasks2.length === 0) {
|
|
42015
|
+
console.log(c.dim("ℹ No available tasks found in the backlog."));
|
|
42016
|
+
return;
|
|
42017
|
+
}
|
|
42018
|
+
if (tasks2.length > 0 && this.useWorktrees && !isGitAvailable()) {
|
|
42019
|
+
console.log(c.error("git is not installed. Worktree isolation requires git. Install from https://git-scm.com/"));
|
|
42020
|
+
return;
|
|
42021
|
+
}
|
|
42022
|
+
if (tasks2.length > 0 && this.config.autoPush && !isGhAvailable(this.config.projectPath)) {
|
|
42023
|
+
console.log(c.warning("GitHub CLI (gh) not available or not authenticated. Branch push can continue, but automatic PR creation may fail until gh is configured. Install from https://cli.github.com/"));
|
|
42024
|
+
}
|
|
42025
|
+
if (tasks2.length > 0 && this.useWorktrees) {
|
|
42026
|
+
this.worktreeManager = new WorktreeManager(this.config.projectPath, {
|
|
42027
|
+
cleanupPolicy: this.worktreeCleanupPolicy
|
|
42028
|
+
});
|
|
42029
|
+
}
|
|
42030
|
+
this.startHeartbeatMonitor();
|
|
42031
|
+
const agentsToSpawn = Math.min(this.agentCount, tasks2.length);
|
|
42032
|
+
const SPAWN_DELAY_MS = 5000;
|
|
42033
|
+
const spawnPromises = [];
|
|
42034
|
+
for (let i = 0;i < agentsToSpawn; i++) {
|
|
42035
|
+
if (i > 0) {
|
|
42036
|
+
await this.sleep(SPAWN_DELAY_MS);
|
|
42037
|
+
}
|
|
42038
|
+
spawnPromises.push(this.spawnAgent(i));
|
|
42039
|
+
}
|
|
42040
|
+
await Promise.all(spawnPromises);
|
|
42041
|
+
while (this.agents.size > 0 && this.isRunning) {
|
|
42042
|
+
if (this.agents.size === 0) {
|
|
42043
|
+
break;
|
|
42044
|
+
}
|
|
42045
|
+
await this.sleep(2000);
|
|
42046
|
+
}
|
|
42047
|
+
console.log(`
|
|
42048
|
+
${c.success("✅ Orchestrator finished")}`);
|
|
42049
|
+
}
|
|
42050
|
+
async spawnAgent(index) {
|
|
42051
|
+
const agentId = `agent-${index}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
42052
|
+
const agentState = {
|
|
42053
|
+
id: agentId,
|
|
42054
|
+
status: "IDLE",
|
|
42055
|
+
currentTaskId: null,
|
|
42056
|
+
tasksCompleted: 0,
|
|
42057
|
+
tasksFailed: 0,
|
|
42058
|
+
lastHeartbeat: new Date
|
|
42059
|
+
};
|
|
42060
|
+
this.agents.set(agentId, agentState);
|
|
42061
|
+
console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
|
|
42062
|
+
`);
|
|
42063
|
+
const workerPath = this.resolveWorkerPath();
|
|
42064
|
+
if (!workerPath) {
|
|
42065
|
+
throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
|
|
42066
|
+
}
|
|
42067
|
+
const workerArgs = [
|
|
42068
|
+
"--agent-id",
|
|
42069
|
+
agentId,
|
|
42070
|
+
"--workspace-id",
|
|
42071
|
+
this.config.workspaceId,
|
|
42072
|
+
"--api-url",
|
|
42073
|
+
this.config.apiBase,
|
|
42074
|
+
"--api-key",
|
|
42075
|
+
this.config.apiKey,
|
|
42076
|
+
"--project-path",
|
|
42077
|
+
this.config.projectPath
|
|
42078
|
+
];
|
|
42079
|
+
if (this.config.model) {
|
|
42080
|
+
workerArgs.push("--model", this.config.model);
|
|
42081
|
+
}
|
|
42082
|
+
if (this.config.provider) {
|
|
42083
|
+
workerArgs.push("--provider", this.config.provider);
|
|
42084
|
+
}
|
|
42085
|
+
if (this.resolvedSprintId) {
|
|
42086
|
+
workerArgs.push("--sprint-id", this.resolvedSprintId);
|
|
42087
|
+
}
|
|
42088
|
+
if (this.useWorktrees) {
|
|
42089
|
+
workerArgs.push("--use-worktrees");
|
|
42090
|
+
}
|
|
42091
|
+
if (this.config.autoPush) {
|
|
42092
|
+
workerArgs.push("--auto-push");
|
|
42093
|
+
}
|
|
42094
|
+
const agentProcess = spawn4(process.execPath, [workerPath, ...workerArgs], {
|
|
42095
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
42096
|
+
detached: true,
|
|
42097
|
+
env: {
|
|
42098
|
+
...process.env,
|
|
42099
|
+
FORCE_COLOR: "1",
|
|
42100
|
+
TERM: "xterm-256color",
|
|
42101
|
+
LOCUS_WORKER: agentId,
|
|
42102
|
+
LOCUS_WORKSPACE: this.config.workspaceId
|
|
42103
|
+
}
|
|
42104
|
+
});
|
|
42105
|
+
agentState.process = agentProcess;
|
|
42106
|
+
agentProcess.on("message", (msg) => {
|
|
42107
|
+
if (msg.type === "stats") {
|
|
42108
|
+
agentState.tasksCompleted = msg.tasksCompleted || 0;
|
|
42109
|
+
agentState.tasksFailed = msg.tasksFailed || 0;
|
|
42110
|
+
}
|
|
42111
|
+
if (msg.type === "heartbeat") {
|
|
42112
|
+
agentState.lastHeartbeat = new Date;
|
|
42113
|
+
}
|
|
42114
|
+
});
|
|
42115
|
+
agentProcess.stdout?.on("data", (data) => {
|
|
42116
|
+
process.stdout.write(data.toString());
|
|
42117
|
+
});
|
|
42118
|
+
agentProcess.stderr?.on("data", (data) => {
|
|
42119
|
+
process.stderr.write(data.toString());
|
|
42120
|
+
});
|
|
42121
|
+
agentProcess.on("exit", (code) => {
|
|
42122
|
+
console.log(`
|
|
42123
|
+
${agentId} finished (exit code: ${code})`);
|
|
42124
|
+
const agent2 = this.agents.get(agentId);
|
|
42125
|
+
if (agent2) {
|
|
42126
|
+
agent2.status = code === 0 ? "COMPLETED" : "FAILED";
|
|
42127
|
+
this.emit("agent:completed", {
|
|
42128
|
+
agentId,
|
|
42129
|
+
status: agent2.status,
|
|
42130
|
+
tasksCompleted: agent2.tasksCompleted,
|
|
42131
|
+
tasksFailed: agent2.tasksFailed
|
|
42132
|
+
});
|
|
42133
|
+
this.agents.delete(agentId);
|
|
42134
|
+
}
|
|
42135
|
+
});
|
|
42136
|
+
this.emit("agent:spawned", { agentId });
|
|
42137
|
+
}
|
|
42138
|
+
resolveWorkerPath() {
|
|
42139
|
+
const currentModulePath = fileURLToPath(import.meta.url);
|
|
42140
|
+
const currentModuleDir = dirname2(currentModulePath);
|
|
42141
|
+
const potentialPaths = [
|
|
42142
|
+
join8(currentModuleDir, "agent", "worker.js"),
|
|
42143
|
+
join8(currentModuleDir, "worker.js"),
|
|
42144
|
+
join8(currentModuleDir, "agent", "worker.ts")
|
|
42145
|
+
];
|
|
42146
|
+
return potentialPaths.find((p) => existsSync7(p));
|
|
42147
|
+
}
|
|
42148
|
+
startHeartbeatMonitor() {
|
|
42149
|
+
this.heartbeatInterval = setInterval(() => {
|
|
42150
|
+
const now = Date.now();
|
|
42151
|
+
for (const [agentId, agent2] of this.agents.entries()) {
|
|
42152
|
+
if (agent2.status === "WORKING" && now - agent2.lastHeartbeat.getTime() > STALE_AGENT_TIMEOUT_MS) {
|
|
42153
|
+
console.log(c.error(`Agent ${agentId} is stale (no heartbeat for 10 minutes). Killing.`));
|
|
42154
|
+
if (agent2.process && !agent2.process.killed) {
|
|
42155
|
+
this.killProcessTree(agent2.process);
|
|
42156
|
+
}
|
|
42157
|
+
this.emit("agent:stale", { agentId });
|
|
42158
|
+
}
|
|
42159
|
+
}
|
|
42160
|
+
}, 60000);
|
|
42161
|
+
}
|
|
42162
|
+
async getAvailableTasks() {
|
|
42163
|
+
try {
|
|
42164
|
+
const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
|
|
42165
|
+
return tasks2.filter((task2) => !this.processedTasks.has(task2.id));
|
|
42166
|
+
} catch (error48) {
|
|
42167
|
+
this.emit("error", error48);
|
|
42168
|
+
return [];
|
|
42169
|
+
}
|
|
42170
|
+
}
|
|
42171
|
+
async assignTaskToAgent(agentId) {
|
|
42172
|
+
const agent2 = this.agents.get(agentId);
|
|
42173
|
+
if (!agent2)
|
|
42174
|
+
return null;
|
|
42175
|
+
try {
|
|
42176
|
+
const tasks2 = await this.getAvailableTasks();
|
|
42177
|
+
const priorityOrder = [
|
|
42178
|
+
"CRITICAL" /* CRITICAL */,
|
|
42179
|
+
"HIGH" /* HIGH */,
|
|
42180
|
+
"MEDIUM" /* MEDIUM */,
|
|
42181
|
+
"LOW" /* LOW */
|
|
42182
|
+
];
|
|
42183
|
+
let task2 = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
|
|
42184
|
+
if (!task2 && tasks2.length > 0) {
|
|
42185
|
+
task2 = tasks2[0];
|
|
42186
|
+
}
|
|
42187
|
+
if (!task2)
|
|
42188
|
+
return null;
|
|
42189
|
+
agent2.currentTaskId = task2.id;
|
|
42190
|
+
agent2.status = "WORKING";
|
|
42191
|
+
this.emit("task:assigned", {
|
|
42192
|
+
agentId,
|
|
42193
|
+
taskId: task2.id,
|
|
42194
|
+
title: task2.title
|
|
42195
|
+
});
|
|
42196
|
+
return task2;
|
|
42197
|
+
} catch (error48) {
|
|
42198
|
+
this.emit("error", error48);
|
|
42199
|
+
return null;
|
|
42200
|
+
}
|
|
42201
|
+
}
|
|
42202
|
+
async completeTask(taskId, agentId, summary) {
|
|
42203
|
+
try {
|
|
42204
|
+
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
42205
|
+
status: "IN_REVIEW" /* IN_REVIEW */
|
|
42206
|
+
});
|
|
42207
|
+
if (summary) {
|
|
42208
|
+
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
42209
|
+
author: agentId,
|
|
42210
|
+
text: `✅ Task completed
|
|
42211
|
+
|
|
42212
|
+
${summary}`
|
|
42213
|
+
});
|
|
42214
|
+
}
|
|
42215
|
+
this.processedTasks.add(taskId);
|
|
42216
|
+
const agent2 = this.agents.get(agentId);
|
|
42217
|
+
if (agent2) {
|
|
42218
|
+
agent2.tasksCompleted += 1;
|
|
42219
|
+
agent2.currentTaskId = null;
|
|
42220
|
+
agent2.status = "IDLE";
|
|
42221
|
+
}
|
|
42222
|
+
this.emit("task:completed", { agentId, taskId });
|
|
42223
|
+
} catch (error48) {
|
|
42224
|
+
this.emit("error", error48);
|
|
42225
|
+
}
|
|
42226
|
+
}
|
|
42227
|
+
async failTask(taskId, agentId, error48) {
|
|
42228
|
+
try {
|
|
42229
|
+
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
42230
|
+
status: "BACKLOG" /* BACKLOG */,
|
|
42231
|
+
assignedTo: null
|
|
42232
|
+
});
|
|
42233
|
+
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
42234
|
+
author: agentId,
|
|
42235
|
+
text: `❌ Agent failed: ${error48}`
|
|
42236
|
+
});
|
|
42237
|
+
const agent2 = this.agents.get(agentId);
|
|
42238
|
+
if (agent2) {
|
|
42239
|
+
agent2.tasksFailed += 1;
|
|
42240
|
+
agent2.currentTaskId = null;
|
|
42241
|
+
agent2.status = "IDLE";
|
|
42242
|
+
}
|
|
42243
|
+
this.emit("task:failed", { agentId, taskId, error: error48 });
|
|
42244
|
+
} catch (error49) {
|
|
42245
|
+
this.emit("error", error49);
|
|
42246
|
+
}
|
|
42247
|
+
}
|
|
42248
|
+
async stop() {
|
|
42249
|
+
this.isRunning = false;
|
|
42250
|
+
await this.cleanup();
|
|
42251
|
+
this.emit("stopped", { timestamp: new Date });
|
|
42252
|
+
}
|
|
42253
|
+
stopAgent(agentId) {
|
|
42254
|
+
const agent2 = this.agents.get(agentId);
|
|
42255
|
+
if (!agent2)
|
|
42256
|
+
return false;
|
|
42257
|
+
if (agent2.process && !agent2.process.killed) {
|
|
42258
|
+
this.killProcessTree(agent2.process);
|
|
42259
|
+
}
|
|
42260
|
+
return true;
|
|
42261
|
+
}
|
|
42262
|
+
killProcessTree(proc) {
|
|
42263
|
+
if (!proc.pid || proc.killed)
|
|
42264
|
+
return;
|
|
42265
|
+
try {
|
|
42266
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
42267
|
+
} catch {
|
|
42268
|
+
try {
|
|
42269
|
+
proc.kill("SIGTERM");
|
|
42270
|
+
} catch {}
|
|
42271
|
+
}
|
|
42272
|
+
}
|
|
42273
|
+
async cleanup() {
|
|
42274
|
+
if (this.heartbeatInterval) {
|
|
42275
|
+
clearInterval(this.heartbeatInterval);
|
|
42276
|
+
this.heartbeatInterval = null;
|
|
42277
|
+
}
|
|
42278
|
+
for (const [agentId, agent2] of this.agents.entries()) {
|
|
42279
|
+
if (agent2.process && !agent2.process.killed) {
|
|
42280
|
+
console.log(`Killing agent: ${agentId}`);
|
|
42281
|
+
this.killProcessTree(agent2.process);
|
|
42282
|
+
}
|
|
42283
|
+
}
|
|
42284
|
+
if (this.worktreeManager) {
|
|
42285
|
+
try {
|
|
42286
|
+
if (this.worktreeCleanupPolicy === "auto") {
|
|
42287
|
+
const removed = this.worktreeManager.removeAll();
|
|
42288
|
+
if (removed > 0) {
|
|
42289
|
+
console.log(c.dim(`Cleaned up ${removed} worktree(s)`));
|
|
42290
|
+
}
|
|
42291
|
+
} else if (this.worktreeCleanupPolicy === "retain-on-failure") {
|
|
42292
|
+
this.worktreeManager.prune();
|
|
42293
|
+
console.log(c.dim("Retaining worktrees for failure analysis (cleanup policy: retain-on-failure)"));
|
|
42294
|
+
} else {
|
|
42295
|
+
console.log(c.dim("Skipping worktree cleanup (cleanup policy: manual)"));
|
|
42296
|
+
}
|
|
42297
|
+
} catch {
|
|
42298
|
+
console.log(c.dim("Could not clean up some worktrees"));
|
|
42299
|
+
}
|
|
42300
|
+
}
|
|
42301
|
+
this.agents.clear();
|
|
42302
|
+
}
|
|
42303
|
+
getStats() {
|
|
42304
|
+
return {
|
|
42305
|
+
activeAgents: this.agents.size,
|
|
42306
|
+
agentCount: this.agentCount,
|
|
42307
|
+
useWorktrees: this.useWorktrees,
|
|
42308
|
+
processedTasks: this.processedTasks.size,
|
|
42309
|
+
totalTasksCompleted: Array.from(this.agents.values()).reduce((sum, agent2) => sum + agent2.tasksCompleted, 0),
|
|
42310
|
+
totalTasksFailed: Array.from(this.agents.values()).reduce((sum, agent2) => sum + agent2.tasksFailed, 0)
|
|
42311
|
+
};
|
|
42312
|
+
}
|
|
42313
|
+
getAgentStates() {
|
|
42314
|
+
return Array.from(this.agents.values());
|
|
42315
|
+
}
|
|
42316
|
+
sleep(ms) {
|
|
42317
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
42318
|
+
}
|
|
42319
|
+
}
|
|
42320
|
+
// src/commands/worktree.ts
|
|
42321
|
+
function createWorktreeManager(config2) {
|
|
42322
|
+
return new WorktreeManager(config2.projectPath);
|
|
42323
|
+
}
|
|
42324
|
+
async function worktreesCommand(ctx, config2) {
|
|
42325
|
+
console.log("[worktrees] Listing agent worktrees");
|
|
42326
|
+
try {
|
|
42327
|
+
const manager = createWorktreeManager(config2);
|
|
42328
|
+
const worktrees = manager.listAgentWorktrees();
|
|
42329
|
+
if (worktrees.length === 0) {
|
|
42330
|
+
await ctx.reply(formatInfo("No agent worktrees found."), {
|
|
42331
|
+
parse_mode: "HTML"
|
|
42332
|
+
});
|
|
42333
|
+
return;
|
|
42334
|
+
}
|
|
42335
|
+
let msg = `<b>Agent Worktrees (${worktrees.length})</b>
|
|
42336
|
+
|
|
42337
|
+
`;
|
|
42338
|
+
for (let i = 0;i < worktrees.length; i++) {
|
|
42339
|
+
const wt = worktrees[i];
|
|
42340
|
+
const status = wt.isPrunable ? " ⚠️ stale" : "";
|
|
42341
|
+
msg += `<b>${i + 1}.</b> <code>${escapeHtml(wt.branch)}</code>${status}
|
|
42342
|
+
`;
|
|
42343
|
+
msg += ` HEAD: <code>${wt.head.slice(0, 8)}</code>
|
|
42344
|
+
`;
|
|
42345
|
+
msg += ` Path: <code>${escapeHtml(wt.path)}</code>
|
|
42346
|
+
|
|
42347
|
+
`;
|
|
42348
|
+
}
|
|
42349
|
+
msg += `Use /worktree <number> to view details
|
|
42350
|
+
`;
|
|
42351
|
+
msg += "Use /rmworktree <number|all> to remove";
|
|
42352
|
+
await ctx.reply(msg, { parse_mode: "HTML" });
|
|
42353
|
+
} catch (err) {
|
|
42354
|
+
console.error("[worktrees] Failed:", err);
|
|
42355
|
+
await ctx.reply(formatError(`Failed to list worktrees: ${err instanceof Error ? err.message : String(err)}`), { parse_mode: "HTML" });
|
|
42356
|
+
}
|
|
42357
|
+
}
|
|
42358
|
+
async function worktreeCommand(ctx, config2) {
|
|
42359
|
+
const text = (ctx.message && "text" in ctx.message ? ctx.message.text : "") || "";
|
|
42360
|
+
const arg = text.replace(/^\/worktree\s*/, "").trim();
|
|
42361
|
+
console.log(`[worktree] Select: ${arg || "(empty)"}`);
|
|
42362
|
+
if (!arg) {
|
|
42363
|
+
await ctx.reply(formatError(`Usage: /worktree <number>
|
|
42364
|
+
Run /worktrees to see the list.`), { parse_mode: "HTML" });
|
|
42365
|
+
return;
|
|
42366
|
+
}
|
|
42367
|
+
const index = Number.parseInt(arg, 10);
|
|
42368
|
+
if (Number.isNaN(index) || index < 1) {
|
|
42369
|
+
await ctx.reply(formatError("Please provide a valid worktree number."), {
|
|
42370
|
+
parse_mode: "HTML"
|
|
42371
|
+
});
|
|
42372
|
+
return;
|
|
42373
|
+
}
|
|
42374
|
+
try {
|
|
42375
|
+
const manager = createWorktreeManager(config2);
|
|
42376
|
+
const worktrees = manager.listAgentWorktrees();
|
|
42377
|
+
if (index > worktrees.length) {
|
|
42378
|
+
await ctx.reply(formatError(`Worktree #${index} does not exist. There are ${worktrees.length} worktree(s).`), { parse_mode: "HTML" });
|
|
42379
|
+
return;
|
|
42380
|
+
}
|
|
42381
|
+
const wt = worktrees[index - 1];
|
|
42382
|
+
const hasChanges = !wt.isPrunable && manager.hasChanges(wt.path);
|
|
42383
|
+
let msg = `<b>Worktree #${index}</b>
|
|
42384
|
+
|
|
42385
|
+
`;
|
|
42386
|
+
msg += `<b>Branch:</b> <code>${escapeHtml(wt.branch)}</code>
|
|
42387
|
+
`;
|
|
42388
|
+
msg += `<b>HEAD:</b> <code>${wt.head}</code>
|
|
42389
|
+
`;
|
|
42390
|
+
msg += `<b>Path:</b> <code>${escapeHtml(wt.path)}</code>
|
|
42391
|
+
`;
|
|
42392
|
+
msg += `<b>Status:</b> ${wt.isPrunable ? "⚠️ stale (directory missing)" : hasChanges ? "\uD83D\uDCDD has uncommitted changes" : "✅ clean"}
|
|
42393
|
+
`;
|
|
42394
|
+
await ctx.reply(msg, { parse_mode: "HTML" });
|
|
42395
|
+
} catch (err) {
|
|
42396
|
+
console.error("[worktree] Failed:", err);
|
|
42397
|
+
await ctx.reply(formatError(`Failed to get worktree details: ${err instanceof Error ? err.message : String(err)}`), { parse_mode: "HTML" });
|
|
42398
|
+
}
|
|
42399
|
+
}
|
|
42400
|
+
async function rmworktreeCommand(ctx, config2) {
|
|
42401
|
+
const text = (ctx.message && "text" in ctx.message ? ctx.message.text : "") || "";
|
|
42402
|
+
const arg = text.replace(/^\/rmworktree\s*/, "").trim();
|
|
42403
|
+
console.log(`[rmworktree] Remove: ${arg || "(empty)"}`);
|
|
42404
|
+
if (!arg) {
|
|
42405
|
+
await ctx.reply(formatError(`Usage: /rmworktree <number|all>
|
|
42406
|
+
Run /worktrees to see the list.`), { parse_mode: "HTML" });
|
|
42407
|
+
return;
|
|
42408
|
+
}
|
|
42409
|
+
try {
|
|
42410
|
+
const manager = createWorktreeManager(config2);
|
|
42411
|
+
if (arg === "all") {
|
|
42412
|
+
const count = manager.removeAll();
|
|
42413
|
+
await ctx.reply(formatSuccess(`Removed ${count} worktree(s).`), { parse_mode: "HTML" });
|
|
42414
|
+
return;
|
|
42415
|
+
}
|
|
42416
|
+
const index = Number.parseInt(arg, 10);
|
|
42417
|
+
if (Number.isNaN(index) || index < 1) {
|
|
42418
|
+
await ctx.reply(formatError("Please provide a valid worktree number or 'all'."), { parse_mode: "HTML" });
|
|
42419
|
+
return;
|
|
42420
|
+
}
|
|
42421
|
+
const worktrees = manager.listAgentWorktrees();
|
|
42422
|
+
if (index > worktrees.length) {
|
|
42423
|
+
await ctx.reply(formatError(`Worktree #${index} does not exist. There are ${worktrees.length} worktree(s).`), { parse_mode: "HTML" });
|
|
42424
|
+
return;
|
|
42425
|
+
}
|
|
42426
|
+
const wt = worktrees[index - 1];
|
|
42427
|
+
manager.remove(wt.path, true);
|
|
42428
|
+
await ctx.reply(formatSuccess(`Removed worktree #${index} (${wt.branch}) and its branch.`), { parse_mode: "HTML" });
|
|
42429
|
+
} catch (err) {
|
|
42430
|
+
console.error("[rmworktree] Failed:", err);
|
|
42431
|
+
await ctx.reply(formatError(`Failed to remove worktree: ${err instanceof Error ? err.message : String(err)}`), { parse_mode: "HTML" });
|
|
42432
|
+
}
|
|
42433
|
+
}
|
|
42434
|
+
// src/executor.ts
|
|
42435
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
42436
|
+
import { join as join9 } from "node:path";
|
|
38529
42437
|
function timestamp2() {
|
|
38530
42438
|
return new Date().toLocaleTimeString("en-GB", { hour12: false });
|
|
38531
42439
|
}
|
|
@@ -38541,7 +42449,7 @@ class CliExecutor {
|
|
|
38541
42449
|
}
|
|
38542
42450
|
resolveCommand(args) {
|
|
38543
42451
|
if (this.config.testMode) {
|
|
38544
|
-
const cliPath =
|
|
42452
|
+
const cliPath = join9(this.config.projectPath, "packages/cli/src/cli.ts");
|
|
38545
42453
|
return { cmd: "bun", cmdArgs: ["run", cliPath, ...args] };
|
|
38546
42454
|
}
|
|
38547
42455
|
return { cmd: "locus", cmdArgs: args };
|
|
@@ -38553,8 +42461,8 @@ class CliExecutor {
|
|
|
38553
42461
|
const fullCommand = `${cmd} ${cmdArgs.join(" ")}`;
|
|
38554
42462
|
const startTime = Date.now();
|
|
38555
42463
|
log2(id, `Process started: ${fullCommand}`);
|
|
38556
|
-
return new Promise((
|
|
38557
|
-
const proc =
|
|
42464
|
+
return new Promise((resolve3) => {
|
|
42465
|
+
const proc = spawn5(cmd, cmdArgs, {
|
|
38558
42466
|
cwd: this.config.projectPath,
|
|
38559
42467
|
env: buildSpawnEnv(),
|
|
38560
42468
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -38582,13 +42490,13 @@ class CliExecutor {
|
|
|
38582
42490
|
this.runningProcesses.delete(id);
|
|
38583
42491
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
38584
42492
|
log2(id, `Process exited (code: ${exitCode}, ${elapsed}s)${killed ? " [killed]" : ""}`);
|
|
38585
|
-
|
|
42493
|
+
resolve3({ stdout, stderr, exitCode, killed });
|
|
38586
42494
|
});
|
|
38587
42495
|
proc.on("error", (err) => {
|
|
38588
42496
|
clearTimeout(timer);
|
|
38589
42497
|
this.runningProcesses.delete(id);
|
|
38590
42498
|
log2(id, `Process error: ${err.message}`);
|
|
38591
|
-
|
|
42499
|
+
resolve3({
|
|
38592
42500
|
stdout,
|
|
38593
42501
|
stderr: stderr || err.message,
|
|
38594
42502
|
exitCode: 1,
|
|
@@ -38604,7 +42512,7 @@ class CliExecutor {
|
|
|
38604
42512
|
const fullCommand = `${cmd} ${cmdArgs.join(" ")}`;
|
|
38605
42513
|
const startTime = Date.now();
|
|
38606
42514
|
log2(id, `Process started (streaming): ${fullCommand}`);
|
|
38607
|
-
const proc =
|
|
42515
|
+
const proc = spawn5(cmd, cmdArgs, {
|
|
38608
42516
|
cwd: this.config.projectPath,
|
|
38609
42517
|
env: buildSpawnEnv(),
|
|
38610
42518
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -38631,19 +42539,19 @@ class CliExecutor {
|
|
|
38631
42539
|
killed = true;
|
|
38632
42540
|
proc.kill("SIGTERM");
|
|
38633
42541
|
}, timeout);
|
|
38634
|
-
const done = new Promise((
|
|
42542
|
+
const done = new Promise((resolve3) => {
|
|
38635
42543
|
proc.on("close", (exitCode) => {
|
|
38636
42544
|
clearTimeout(timer);
|
|
38637
42545
|
this.runningProcesses.delete(id);
|
|
38638
42546
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
38639
42547
|
log2(id, `Process exited (code: ${exitCode}, ${elapsed}s)${killed ? " [killed]" : ""}`);
|
|
38640
|
-
|
|
42548
|
+
resolve3({ stdout, stderr, exitCode, killed });
|
|
38641
42549
|
});
|
|
38642
42550
|
proc.on("error", (err) => {
|
|
38643
42551
|
clearTimeout(timer);
|
|
38644
42552
|
this.runningProcesses.delete(id);
|
|
38645
42553
|
log2(id, `Process error: ${err.message}`);
|
|
38646
|
-
|
|
42554
|
+
resolve3({
|
|
38647
42555
|
stdout,
|
|
38648
42556
|
stderr: stderr || err.message,
|
|
38649
42557
|
exitCode: 1,
|
|
@@ -38736,22 +42644,25 @@ function createBot(config2) {
|
|
|
38736
42644
|
bot.command("dev", (ctx) => devCommand(ctx, config2));
|
|
38737
42645
|
bot.command("status", (ctx) => statusCommand(ctx, executor));
|
|
38738
42646
|
bot.command("agents", (ctx) => agentsCommand(ctx, executor));
|
|
42647
|
+
bot.command("worktrees", (ctx) => worktreesCommand(ctx, config2));
|
|
42648
|
+
bot.command("worktree", (ctx) => worktreeCommand(ctx, config2));
|
|
42649
|
+
bot.command("rmworktree", (ctx) => rmworktreeCommand(ctx, config2));
|
|
38739
42650
|
return bot;
|
|
38740
42651
|
}
|
|
38741
42652
|
|
|
38742
42653
|
// src/config.ts
|
|
38743
42654
|
var import_dotenv = __toESM(require_main(), 1);
|
|
38744
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
38745
|
-
import { join as
|
|
42655
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
|
|
42656
|
+
import { join as join10 } from "node:path";
|
|
38746
42657
|
import_dotenv.default.config();
|
|
38747
42658
|
var SETTINGS_FILE = "settings.json";
|
|
38748
42659
|
var CONFIG_DIR = ".locus";
|
|
38749
42660
|
function loadSettings(projectPath) {
|
|
38750
|
-
const settingsPath =
|
|
38751
|
-
if (!
|
|
42661
|
+
const settingsPath = join10(projectPath, CONFIG_DIR, SETTINGS_FILE);
|
|
42662
|
+
if (!existsSync8(settingsPath)) {
|
|
38752
42663
|
return null;
|
|
38753
42664
|
}
|
|
38754
|
-
const raw =
|
|
42665
|
+
const raw = readFileSync5(settingsPath, "utf-8");
|
|
38755
42666
|
return JSON.parse(raw);
|
|
38756
42667
|
}
|
|
38757
42668
|
function resolveConfig() {
|