@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.
Files changed (2) hide show
  1. package/bin/telegram.js +3953 -42
  2. 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 import_config = __toESM(require_config(), 1);
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 = 600000;
19948
- var EXECUTE_DEFAULT_TIMEOUT = 600000;
19949
- var PLAN_TIMEOUT = 600000;
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 &lt;command&gt; — Run whitelisted git/gh commands
20104
20104
  /dev &lt;command&gt; — Run lint, typecheck, build, test
20105
20105
 
20106
+ <b>Worktrees:</b>
20107
+ /worktrees — List agent worktrees
20108
+ /worktree &lt;number&gt; — View worktree details
20109
+ /rmworktree &lt;number|all&gt; — 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
- const result = await done;
20272
- activeRunKill = null;
20273
- clearInterval(sendInterval);
20274
- if (outputBuffer.length > lastSentLength) {
20275
- const remaining = stripAnsi(outputBuffer.slice(lastSentLength));
20276
- const messages = splitMessage(`<pre>${escapeHtml(remaining)}</pre>`, 4000);
20277
- for (const msg of messages) {
20278
- try {
20279
- await ctx.reply(msg, { parse_mode: "HTML" });
20280
- } catch {}
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
- if (result.exitCode === 0) {
20284
- await ctx.reply(formatSuccess("Agents finished successfully."), {
20285
- parse_mode: "HTML"
20286
- });
20287
- } else if (result.killed) {
20288
- await ctx.reply(formatInfo("Agents were stopped."), {
20289
- parse_mode: "HTML"
20290
- });
20291
- } else {
20292
- await ctx.reply(formatError(`Agents exited with code ${result.exitCode}.`), { parse_mode: "HTML" });
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/executor.ts
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 &lt;number&gt; to view details
42350
+ `;
42351
+ msg += "Use /rmworktree &lt;number|all&gt; 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 &lt;number&gt;
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 &lt;number|all&gt;
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 = join2(this.config.projectPath, "packages/cli/src/cli.ts");
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((resolve) => {
38557
- const proc = spawn2(cmd, cmdArgs, {
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
- resolve({ stdout, stderr, exitCode, killed });
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
- resolve({
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 = spawn2(cmd, cmdArgs, {
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((resolve) => {
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
- resolve({ stdout, stderr, exitCode, killed });
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
- resolve({
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 join3 } from "node:path";
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 = join3(projectPath, CONFIG_DIR, SETTINGS_FILE);
38751
- if (!existsSync(settingsPath)) {
42661
+ const settingsPath = join10(projectPath, CONFIG_DIR, SETTINGS_FILE);
42662
+ if (!existsSync8(settingsPath)) {
38752
42663
  return null;
38753
42664
  }
38754
- const raw = readFileSync(settingsPath, "utf-8");
42665
+ const raw = readFileSync5(settingsPath, "utf-8");
38755
42666
  return JSON.parse(raw);
38756
42667
  }
38757
42668
  function resolveConfig() {