@locusai/telegram 0.9.10 → 0.9.12

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 +3947 -39
  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);
@@ -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,3902 @@ 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
+ const staged = this.git("diff --cached --name-only", worktreePath).trim();
40523
+ if (!staged) {
40524
+ this.log("No changes to commit", "info");
40525
+ return null;
40526
+ }
40527
+ this.gitExec(["commit", "-m", message], worktreePath);
40528
+ const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
40529
+ this.log(`Committed: ${hash2.slice(0, 8)}`, "success");
40530
+ return hash2;
40531
+ }
40532
+ pushBranch(worktreePath, remote = "origin") {
40533
+ const branch = this.getBranch(worktreePath);
40534
+ this.log(`Pushing branch ${branch} to ${remote}`, "info");
40535
+ try {
40536
+ this.gitExec(["push", "-u", remote, branch], worktreePath);
40537
+ this.log(`Pushed ${branch} to ${remote}`, "success");
40538
+ return branch;
40539
+ } catch (error48) {
40540
+ if (!this.isNonFastForwardPushError(error48)) {
40541
+ throw error48;
40542
+ }
40543
+ this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
40544
+ try {
40545
+ this.gitExec(["fetch", remote, branch], worktreePath);
40546
+ } catch {}
40547
+ this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
40548
+ this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
40549
+ return branch;
40550
+ }
40551
+ }
40552
+ getBranch(worktreePath) {
40553
+ return this.git("rev-parse --abbrev-ref HEAD", worktreePath).trim();
40554
+ }
40555
+ hasWorktreeForTask(taskId) {
40556
+ return this.listAgentWorktrees().some((wt) => wt.branch.includes(taskId) || wt.path.includes(taskId));
40557
+ }
40558
+ branchExists(branchName) {
40559
+ try {
40560
+ this.git(`rev-parse --verify "refs/heads/${branchName}"`, this.projectPath);
40561
+ return true;
40562
+ } catch {
40563
+ return false;
40564
+ }
40565
+ }
40566
+ getCurrentBranch() {
40567
+ return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
40568
+ }
40569
+ isManagedWorktreePath(worktreePath) {
40570
+ const rootPath = resolve2(this.rootPath);
40571
+ const candidate = resolve2(worktreePath);
40572
+ const rootWithSep = rootPath.endsWith(sep) ? rootPath : `${rootPath}${sep}`;
40573
+ return candidate.startsWith(rootWithSep);
40574
+ }
40575
+ ensureDirectory(dirPath, label) {
40576
+ if (existsSync5(dirPath)) {
40577
+ if (!statSync2(dirPath).isDirectory()) {
40578
+ throw new Error(`${label} exists but is not a directory: ${dirPath}`);
40579
+ }
40580
+ return;
40581
+ }
40582
+ mkdirSync2(dirPath, { recursive: true });
40583
+ }
40584
+ isMissingDirectoryError(error48) {
40585
+ const message = error48 instanceof Error ? error48.message : String(error48);
40586
+ return message.includes("cannot create directory") || message.includes("No such file or directory");
40587
+ }
40588
+ cleanupFailedWorktree(worktreePath, branch) {
40589
+ try {
40590
+ this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
40591
+ } catch {}
40592
+ if (existsSync5(worktreePath)) {
40593
+ rmSync(worktreePath, { recursive: true, force: true });
40594
+ }
40595
+ try {
40596
+ this.git("worktree prune", this.projectPath);
40597
+ } catch {}
40598
+ if (this.branchExists(branch)) {
40599
+ try {
40600
+ this.git(`branch -D "${branch}"`, this.projectPath);
40601
+ } catch {}
40602
+ }
40603
+ }
40604
+ isNonFastForwardPushError(error48) {
40605
+ const message = error48 instanceof Error ? error48.message : String(error48);
40606
+ return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
40607
+ }
40608
+ git(args, cwd) {
40609
+ return execSync(`git ${args}`, {
40610
+ cwd,
40611
+ encoding: "utf-8",
40612
+ stdio: ["pipe", "pipe", "pipe"]
40613
+ });
40614
+ }
40615
+ gitExec(args, cwd) {
40616
+ return execFileSync3("git", args, {
40617
+ cwd,
40618
+ encoding: "utf-8",
40619
+ stdio: ["pipe", "pipe", "pipe"]
40620
+ });
40621
+ }
40622
+ }
40623
+
40624
+ // ../sdk/src/agent/worker.ts
40625
+ function resolveProvider2(value) {
40626
+ if (!value || value.startsWith("--")) {
40627
+ console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
40628
+ return PROVIDER.CLAUDE;
40629
+ }
40630
+ if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
40631
+ return value;
40632
+ console.warn(`Warning: invalid --provider value '${value}'. Falling back to 'claude'.`);
40633
+ return PROVIDER.CLAUDE;
40634
+ }
40635
+
40636
+ class AgentWorker {
40637
+ config;
40638
+ client;
40639
+ aiRunner;
40640
+ taskExecutor;
40641
+ knowledgeBase;
40642
+ worktreeManager = null;
40643
+ prService = null;
40644
+ maxTasks = 50;
40645
+ tasksCompleted = 0;
40646
+ heartbeatInterval = null;
40647
+ currentTaskId = null;
40648
+ currentWorktreePath = null;
40649
+ postCleanupDelayMs = 5000;
40650
+ ghUsername = null;
40651
+ constructor(config2) {
40652
+ this.config = config2;
40653
+ const projectPath = config2.projectPath || process.cwd();
40654
+ this.client = new LocusClient({
40655
+ baseUrl: config2.apiBase,
40656
+ token: config2.apiKey,
40657
+ retryOptions: {
40658
+ maxRetries: 3,
40659
+ initialDelay: 1000,
40660
+ maxDelay: 5000,
40661
+ factor: 2
40662
+ }
40663
+ });
40664
+ const log2 = this.log.bind(this);
40665
+ if (config2.useWorktrees && !isGitAvailable()) {
40666
+ this.log("git is not installed — worktree isolation will not work", "error");
40667
+ config2.useWorktrees = false;
40668
+ }
40669
+ if (config2.autoPush && !isGhAvailable(projectPath)) {
40670
+ 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");
40671
+ }
40672
+ if (config2.autoPush) {
40673
+ this.ghUsername = getGhUsername();
40674
+ if (this.ghUsername) {
40675
+ this.log(`GitHub user: ${this.ghUsername}`, "info");
40676
+ }
40677
+ }
40678
+ const provider = config2.provider ?? PROVIDER.CLAUDE;
40679
+ this.aiRunner = createAiRunner(provider, {
40680
+ projectPath,
40681
+ model: config2.model,
40682
+ log: log2
40683
+ });
40684
+ this.taskExecutor = new TaskExecutor({
40685
+ aiRunner: this.aiRunner,
40686
+ projectPath,
40687
+ log: log2
40688
+ });
40689
+ this.knowledgeBase = new KnowledgeBase(projectPath);
40690
+ if (config2.useWorktrees) {
40691
+ this.worktreeManager = new WorktreeManager(projectPath, {
40692
+ cleanupPolicy: "auto"
40693
+ });
40694
+ }
40695
+ if (config2.autoPush) {
40696
+ this.prService = new PrService(projectPath, log2);
40697
+ }
40698
+ const providerLabel = provider === "codex" ? "Codex" : "Claude";
40699
+ this.log(`Using ${providerLabel} CLI for all phases`, "info");
40700
+ if (config2.useWorktrees) {
40701
+ this.log("Per-task worktree isolation enabled", "info");
40702
+ if (config2.autoPush) {
40703
+ this.log("Auto-push enabled: branches will be pushed to remote", "info");
40704
+ }
40705
+ }
40706
+ }
40707
+ log(message, level = "info") {
40708
+ const timestamp2 = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
40709
+ const colorFn = {
40710
+ info: c.cyan,
40711
+ success: c.green,
40712
+ warn: c.yellow,
40713
+ error: c.red
40714
+ }[level];
40715
+ const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
40716
+ console.log(`${c.dim(`[${timestamp2}]`)} ${c.bold(`[${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
40717
+ }
40718
+ async getActiveSprint() {
40719
+ try {
40720
+ if (this.config.sprintId) {
40721
+ return await this.client.sprints.getById(this.config.sprintId, this.config.workspaceId);
40722
+ }
40723
+ return await this.client.sprints.getActive(this.config.workspaceId);
40724
+ } catch (_error) {
40725
+ return null;
40726
+ }
40727
+ }
40728
+ async getNextTask() {
40729
+ const maxRetries = 10;
40730
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
40731
+ try {
40732
+ const task2 = await this.client.workspaces.dispatch(this.config.workspaceId, this.config.agentId, this.config.sprintId);
40733
+ return task2;
40734
+ } catch (error48) {
40735
+ const isAxiosError2 = error48 != null && typeof error48 === "object" && "response" in error48 && typeof error48.response?.status === "number";
40736
+ const status = isAxiosError2 ? error48.response.status : 0;
40737
+ if (status === 404) {
40738
+ this.log("No tasks available in the backlog.", "info");
40739
+ return null;
40740
+ }
40741
+ const msg = error48 instanceof Error ? error48.message : String(error48);
40742
+ if (attempt < maxRetries) {
40743
+ this.log(`Nothing dispatched (attempt ${attempt}/${maxRetries}): ${msg}. Retrying in 30s...`, "warn");
40744
+ await new Promise((r) => setTimeout(r, 30000));
40745
+ } else {
40746
+ this.log(`Nothing dispatched after ${maxRetries} attempts: ${msg}`, "warn");
40747
+ return null;
40748
+ }
40749
+ }
40750
+ }
40751
+ return null;
40752
+ }
40753
+ createTaskWorktree(task2) {
40754
+ if (!this.worktreeManager) {
40755
+ return {
40756
+ worktreePath: null,
40757
+ baseBranch: null,
40758
+ executor: this.taskExecutor
40759
+ };
40760
+ }
40761
+ const slug = task2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
40762
+ const result = this.worktreeManager.create({
40763
+ taskId: task2.id,
40764
+ taskSlug: slug,
40765
+ agentId: this.config.agentId
40766
+ });
40767
+ this.log(`Worktree created: ${result.worktreePath} (${result.branch})`, "info");
40768
+ const log2 = this.log.bind(this);
40769
+ const provider = this.config.provider ?? PROVIDER.CLAUDE;
40770
+ const taskAiRunner = createAiRunner(provider, {
40771
+ projectPath: result.worktreePath,
40772
+ model: this.config.model,
40773
+ log: log2
40774
+ });
40775
+ const taskExecutor = new TaskExecutor({
40776
+ aiRunner: taskAiRunner,
40777
+ projectPath: result.worktreePath,
40778
+ log: log2
40779
+ });
40780
+ return {
40781
+ worktreePath: result.worktreePath,
40782
+ baseBranch: result.baseBranch,
40783
+ executor: taskExecutor
40784
+ };
40785
+ }
40786
+ commitAndPushWorktree(worktreePath, task2, baseBranch) {
40787
+ if (!this.worktreeManager) {
40788
+ return { branch: null, pushed: false, pushFailed: false };
40789
+ }
40790
+ try {
40791
+ const trailers = [
40792
+ `Task-ID: ${task2.id}`,
40793
+ `Agent: ${this.config.agentId}`,
40794
+ "Co-authored-by: LocusAI <noreply@locusai.dev>"
40795
+ ];
40796
+ if (this.ghUsername) {
40797
+ trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
40798
+ }
40799
+ const commitMessage = `feat(agent): ${task2.title}
40800
+
40801
+ ${trailers.join(`
40802
+ `)}`;
40803
+ const hash2 = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch);
40804
+ if (!hash2) {
40805
+ this.log("No changes to commit for this task", "info");
40806
+ return {
40807
+ branch: null,
40808
+ pushed: false,
40809
+ pushFailed: false,
40810
+ noChanges: true,
40811
+ skipReason: "No changes were committed, so no branch was pushed."
40812
+ };
40813
+ }
40814
+ const localBranch = this.worktreeManager.getBranch(worktreePath);
40815
+ if (this.config.autoPush) {
40816
+ try {
40817
+ return {
40818
+ branch: this.worktreeManager.pushBranch(worktreePath),
40819
+ pushed: true,
40820
+ pushFailed: false
40821
+ };
40822
+ } catch (err) {
40823
+ const errorMessage = err instanceof Error ? err.message : String(err);
40824
+ this.log(`Git push failed: ${errorMessage}`, "error");
40825
+ return {
40826
+ branch: localBranch,
40827
+ pushed: false,
40828
+ pushFailed: true,
40829
+ pushError: errorMessage
40830
+ };
40831
+ }
40832
+ }
40833
+ this.log("Auto-push disabled; skipping branch push", "info");
40834
+ return {
40835
+ branch: localBranch,
40836
+ pushed: false,
40837
+ pushFailed: false,
40838
+ skipReason: "Auto-push is disabled, so PR creation was skipped."
40839
+ };
40840
+ } catch (err) {
40841
+ const errorMessage = err instanceof Error ? err.message : String(err);
40842
+ this.log(`Git commit failed: ${errorMessage}`, "error");
40843
+ return { branch: null, pushed: false, pushFailed: false };
40844
+ }
40845
+ }
40846
+ createPullRequest(task2, branch, summary, baseBranch) {
40847
+ if (!this.prService) {
40848
+ const errorMessage = "PR service is not initialized. Enable auto-push to allow PR creation.";
40849
+ this.log(`PR creation skipped: ${errorMessage}`, "warn");
40850
+ return { url: null, error: errorMessage };
40851
+ }
40852
+ this.log(`Attempting PR creation from branch: ${branch}`, "info");
40853
+ try {
40854
+ const result = this.prService.createPr({
40855
+ task: task2,
40856
+ branch,
40857
+ baseBranch,
40858
+ agentId: this.config.agentId,
40859
+ summary
40860
+ });
40861
+ return { url: result.url };
40862
+ } catch (err) {
40863
+ const errorMessage = err instanceof Error ? err.message : String(err);
40864
+ this.log(`PR creation failed: ${errorMessage}`, "error");
40865
+ return { url: null, error: errorMessage };
40866
+ }
40867
+ }
40868
+ cleanupTaskWorktree(worktreePath, keepBranch) {
40869
+ if (!this.worktreeManager || !worktreePath)
40870
+ return;
40871
+ try {
40872
+ this.worktreeManager.remove(worktreePath, !keepBranch);
40873
+ this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
40874
+ } catch {
40875
+ this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
40876
+ }
40877
+ this.currentWorktreePath = null;
40878
+ }
40879
+ async executeTask(task2) {
40880
+ const fullTask = await this.client.tasks.getById(task2.id, this.config.workspaceId);
40881
+ const { worktreePath, baseBranch, executor } = this.createTaskWorktree(fullTask);
40882
+ this.currentWorktreePath = worktreePath;
40883
+ let branchPushed = false;
40884
+ let keepBranch = false;
40885
+ let preserveWorktree = false;
40886
+ try {
40887
+ const result = await executor.execute(fullTask);
40888
+ let taskBranch = null;
40889
+ let prUrl = null;
40890
+ let prError = null;
40891
+ let noChanges = false;
40892
+ if (result.success && worktreePath) {
40893
+ const commitResult = this.commitAndPushWorktree(worktreePath, fullTask, baseBranch ?? undefined);
40894
+ taskBranch = commitResult.branch;
40895
+ branchPushed = commitResult.pushed;
40896
+ keepBranch = taskBranch !== null;
40897
+ noChanges = Boolean(commitResult.noChanges);
40898
+ if (commitResult.pushFailed) {
40899
+ preserveWorktree = true;
40900
+ prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
40901
+ this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
40902
+ }
40903
+ if (branchPushed && taskBranch) {
40904
+ const prResult = this.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
40905
+ prUrl = prResult.url;
40906
+ prError = prResult.error ?? null;
40907
+ if (!prUrl) {
40908
+ preserveWorktree = true;
40909
+ this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
40910
+ }
40911
+ } else if (commitResult.skipReason) {
40912
+ this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
40913
+ }
40914
+ } else if (result.success && !worktreePath) {
40915
+ this.log("Skipping commit/push/PR flow because no task worktree is active.", "warn");
40916
+ }
40917
+ return {
40918
+ ...result,
40919
+ branch: taskBranch ?? undefined,
40920
+ prUrl: prUrl ?? undefined,
40921
+ prError: prError ?? undefined,
40922
+ noChanges: noChanges || undefined
40923
+ };
40924
+ } finally {
40925
+ if (preserveWorktree || keepBranch) {
40926
+ this.currentWorktreePath = null;
40927
+ } else {
40928
+ this.cleanupTaskWorktree(worktreePath, keepBranch);
40929
+ }
40930
+ }
40931
+ }
40932
+ updateProgress(task2, success2) {
40933
+ try {
40934
+ if (success2) {
40935
+ this.knowledgeBase.updateProgress({
40936
+ type: "task_completed",
40937
+ title: task2.title,
40938
+ details: `Agent: ${this.config.agentId.slice(-8)}`
40939
+ });
40940
+ this.log(`Updated progress.md: ${task2.title}`, "info");
40941
+ }
40942
+ } catch (err) {
40943
+ this.log(`Failed to update progress: ${err instanceof Error ? err.message : String(err)}`, "warn");
40944
+ }
40945
+ }
40946
+ startHeartbeat() {
40947
+ this.sendHeartbeat();
40948
+ this.heartbeatInterval = setInterval(() => {
40949
+ this.sendHeartbeat();
40950
+ }, 60000);
40951
+ }
40952
+ stopHeartbeat() {
40953
+ if (this.heartbeatInterval) {
40954
+ clearInterval(this.heartbeatInterval);
40955
+ this.heartbeatInterval = null;
40956
+ }
40957
+ }
40958
+ sendHeartbeat() {
40959
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
40960
+ this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
40961
+ });
40962
+ }
40963
+ async delayAfterCleanup() {
40964
+ if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
40965
+ return;
40966
+ this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
40967
+ await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
40968
+ }
40969
+ async run() {
40970
+ this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
40971
+ const handleShutdown = () => {
40972
+ this.log("Received shutdown signal. Aborting...", "warn");
40973
+ this.aiRunner.abort();
40974
+ this.stopHeartbeat();
40975
+ this.cleanupTaskWorktree(this.currentWorktreePath, false);
40976
+ process.exit(1);
40977
+ };
40978
+ process.on("SIGTERM", handleShutdown);
40979
+ process.on("SIGINT", handleShutdown);
40980
+ this.startHeartbeat();
40981
+ const sprint2 = await this.getActiveSprint();
40982
+ if (sprint2) {
40983
+ this.log(`Active sprint found: ${sprint2.name}`, "info");
40984
+ } else {
40985
+ this.log("No active sprint found.", "warn");
40986
+ }
40987
+ while (this.tasksCompleted < this.maxTasks) {
40988
+ const task2 = await this.getNextTask();
40989
+ if (!task2) {
40990
+ this.log("No more tasks to process. Exiting.", "info");
40991
+ break;
40992
+ }
40993
+ this.log(`Claimed: ${task2.title}`, "success");
40994
+ this.currentTaskId = task2.id;
40995
+ this.sendHeartbeat();
40996
+ const result = await this.executeTask(task2);
40997
+ if (result.success) {
40998
+ if (result.noChanges) {
40999
+ this.log(`Blocked: ${task2.title} - execution produced no file changes`, "warn");
41000
+ await this.client.tasks.update(task2.id, this.config.workspaceId, {
41001
+ status: "BLOCKED" /* BLOCKED */,
41002
+ assignedTo: null
41003
+ });
41004
+ await this.client.tasks.addComment(task2.id, this.config.workspaceId, {
41005
+ author: this.config.agentId,
41006
+ text: `⚠️ Agent execution finished with no file changes, so no commit/branch/PR was created.
41007
+
41008
+ ${result.summary}`
41009
+ });
41010
+ } else {
41011
+ this.log(`Completed: ${task2.title}`, "success");
41012
+ const updatePayload = {
41013
+ status: "IN_REVIEW" /* IN_REVIEW */
41014
+ };
41015
+ if (result.prUrl) {
41016
+ updatePayload.prUrl = result.prUrl;
41017
+ }
41018
+ await this.client.tasks.update(task2.id, this.config.workspaceId, updatePayload);
41019
+ const branchInfo = result.branch ? `
41020
+
41021
+ Branch: \`${result.branch}\`` : "";
41022
+ const prInfo = result.prUrl ? `
41023
+ PR: ${result.prUrl}` : "";
41024
+ const prErrorInfo = result.prError ? `
41025
+ PR automation error: ${result.prError}` : "";
41026
+ await this.client.tasks.addComment(task2.id, this.config.workspaceId, {
41027
+ author: this.config.agentId,
41028
+ text: `✅ ${result.summary}${branchInfo}${prInfo}${prErrorInfo}`
41029
+ });
41030
+ this.tasksCompleted++;
41031
+ this.updateProgress(task2, true);
41032
+ if (result.prUrl) {
41033
+ try {
41034
+ this.knowledgeBase.updateProgress({
41035
+ type: "pr_opened",
41036
+ title: task2.title,
41037
+ details: `PR: ${result.prUrl}`
41038
+ });
41039
+ } catch {}
41040
+ }
41041
+ }
41042
+ } else {
41043
+ this.log(`Failed: ${task2.title} - ${result.summary}`, "error");
41044
+ await this.client.tasks.update(task2.id, this.config.workspaceId, {
41045
+ status: "BACKLOG" /* BACKLOG */,
41046
+ assignedTo: null
41047
+ });
41048
+ await this.client.tasks.addComment(task2.id, this.config.workspaceId, {
41049
+ author: this.config.agentId,
41050
+ text: `❌ ${result.summary}`
41051
+ });
41052
+ }
41053
+ this.currentTaskId = null;
41054
+ this.sendHeartbeat();
41055
+ await this.delayAfterCleanup();
41056
+ }
41057
+ this.currentTaskId = null;
41058
+ this.stopHeartbeat();
41059
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
41060
+ process.exit(0);
41061
+ }
41062
+ }
41063
+ var workerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
41064
+ if (workerEntrypoint === "worker.js" || workerEntrypoint === "worker.ts") {
41065
+ process.title = "locus-worker";
41066
+ const args = process.argv.slice(2);
41067
+ const config2 = {};
41068
+ for (let i = 0;i < args.length; i++) {
41069
+ const arg = args[i];
41070
+ if (arg === "--agent-id")
41071
+ config2.agentId = args[++i];
41072
+ else if (arg === "--workspace-id")
41073
+ config2.workspaceId = args[++i];
41074
+ else if (arg === "--sprint-id")
41075
+ config2.sprintId = args[++i];
41076
+ else if (arg === "--api-url")
41077
+ config2.apiBase = args[++i];
41078
+ else if (arg === "--api-key")
41079
+ config2.apiKey = args[++i];
41080
+ else if (arg === "--project-path")
41081
+ config2.projectPath = args[++i];
41082
+ else if (arg === "--main-project-path")
41083
+ config2.mainProjectPath = args[++i];
41084
+ else if (arg === "--model")
41085
+ config2.model = args[++i];
41086
+ else if (arg === "--use-worktrees")
41087
+ config2.useWorktrees = true;
41088
+ else if (arg === "--auto-push")
41089
+ config2.autoPush = true;
41090
+ else if (arg === "--provider") {
41091
+ const value = args[i + 1];
41092
+ if (value && !value.startsWith("--"))
41093
+ i++;
41094
+ config2.provider = resolveProvider2(value);
41095
+ }
41096
+ }
41097
+ if (!config2.agentId || !config2.workspaceId || !config2.apiBase || !config2.apiKey || !config2.projectPath) {
41098
+ console.error("Missing required arguments");
41099
+ process.exit(1);
41100
+ }
41101
+ const worker = new AgentWorker(config2);
41102
+ worker.run().catch((err) => {
41103
+ console.error("Fatal worker error:", err);
41104
+ process.exit(1);
41105
+ });
41106
+ }
41107
+ // ../sdk/src/exec/context-tracker.ts
41108
+ var REFERENCE_ALIASES = {
41109
+ plan: ["the plan", "sprint plan", "project plan", "implementation plan"],
41110
+ document: ["the doc", "that doc", "the document", "that document"],
41111
+ code: ["the code", "that code", "the implementation"],
41112
+ "task-list": ["the tasks", "the task list", "todo list", "todos"],
41113
+ diagram: ["the diagram", "that diagram", "the chart"],
41114
+ config: ["the config", "configuration", "settings"],
41115
+ report: ["the report", "that report"]
41116
+ };
41117
+ function generateArtifactId() {
41118
+ const timestamp2 = Date.now().toString(36);
41119
+ const random = Math.random().toString(36).substring(2, 7);
41120
+ return `artifact-${timestamp2}-${random}`;
41121
+ }
41122
+ function generateTaskId() {
41123
+ const timestamp2 = Date.now().toString(36);
41124
+ const random = Math.random().toString(36).substring(2, 7);
41125
+ return `task-${timestamp2}-${random}`;
41126
+ }
41127
+
41128
+ class ContextTracker {
41129
+ artifacts = new Map;
41130
+ tasks = new Map;
41131
+ createArtifact(params) {
41132
+ const now = Date.now();
41133
+ const artifact = {
41134
+ ...params,
41135
+ id: generateArtifactId(),
41136
+ createdAt: now,
41137
+ updatedAt: now
41138
+ };
41139
+ this.artifacts.set(artifact.id, artifact);
41140
+ return artifact;
41141
+ }
41142
+ trackArtifact(artifact) {
41143
+ this.artifacts.set(artifact.id, artifact);
41144
+ }
41145
+ updateArtifact(id, updates) {
41146
+ const artifact = this.artifacts.get(id);
41147
+ if (!artifact) {
41148
+ return null;
41149
+ }
41150
+ const updated = {
41151
+ ...artifact,
41152
+ ...updates,
41153
+ updatedAt: Date.now()
41154
+ };
41155
+ this.artifacts.set(id, updated);
41156
+ return updated;
41157
+ }
41158
+ getArtifact(id) {
41159
+ return this.artifacts.get(id) ?? null;
41160
+ }
41161
+ getAllArtifacts() {
41162
+ return Array.from(this.artifacts.values());
41163
+ }
41164
+ createTask(params) {
41165
+ const now = Date.now();
41166
+ const task2 = {
41167
+ ...params,
41168
+ id: generateTaskId(),
41169
+ createdAt: now,
41170
+ updatedAt: now
41171
+ };
41172
+ this.tasks.set(task2.id, task2);
41173
+ return task2;
41174
+ }
41175
+ trackTask(task2) {
41176
+ this.tasks.set(task2.id, task2);
41177
+ }
41178
+ updateTask(id, updates) {
41179
+ const task2 = this.tasks.get(id);
41180
+ if (!task2) {
41181
+ return null;
41182
+ }
41183
+ const updated = {
41184
+ ...task2,
41185
+ ...updates,
41186
+ updatedAt: Date.now()
41187
+ };
41188
+ this.tasks.set(id, updated);
41189
+ return updated;
41190
+ }
41191
+ getTask(id) {
41192
+ return this.tasks.get(id) ?? null;
41193
+ }
41194
+ getAllTasks() {
41195
+ return Array.from(this.tasks.values());
41196
+ }
41197
+ getTasksByStatus(status) {
41198
+ return Array.from(this.tasks.values()).filter((t) => t.status === status);
41199
+ }
41200
+ getReferencedArtifact(reference) {
41201
+ const normalizedRef = reference.toLowerCase().trim();
41202
+ const byId = this.artifacts.get(reference);
41203
+ if (byId) {
41204
+ return byId;
41205
+ }
41206
+ for (const artifact of this.artifacts.values()) {
41207
+ if (artifact.title.toLowerCase().includes(normalizedRef)) {
41208
+ return artifact;
41209
+ }
41210
+ }
41211
+ for (const [type, aliases] of Object.entries(REFERENCE_ALIASES)) {
41212
+ if (aliases.some((alias) => normalizedRef.includes(alias))) {
41213
+ const ofType = Array.from(this.artifacts.values()).filter((a) => a.type === type).sort((a, b) => b.updatedAt - a.updatedAt);
41214
+ if (ofType.length > 0) {
41215
+ return ofType[0];
41216
+ }
41217
+ }
41218
+ }
41219
+ const keywords = normalizedRef.split(/\s+/).filter((w) => w.length > 2);
41220
+ for (const artifact of this.artifacts.values()) {
41221
+ const titleLower = artifact.title.toLowerCase();
41222
+ if (keywords.some((kw) => titleLower.includes(kw))) {
41223
+ return artifact;
41224
+ }
41225
+ }
41226
+ return null;
41227
+ }
41228
+ getReferencedTask(reference) {
41229
+ const normalizedRef = reference.toLowerCase().trim();
41230
+ const byId = this.tasks.get(reference);
41231
+ if (byId) {
41232
+ return byId;
41233
+ }
41234
+ for (const task2 of this.tasks.values()) {
41235
+ if (task2.title.toLowerCase().includes(normalizedRef)) {
41236
+ return task2;
41237
+ }
41238
+ }
41239
+ const keywords = normalizedRef.split(/\s+/).filter((w) => w.length > 2);
41240
+ for (const task2 of this.tasks.values()) {
41241
+ const titleLower = task2.title.toLowerCase();
41242
+ if (keywords.some((kw) => titleLower.includes(kw))) {
41243
+ return task2;
41244
+ }
41245
+ }
41246
+ return null;
41247
+ }
41248
+ buildContextSummary() {
41249
+ const artifacts = Array.from(this.artifacts.values());
41250
+ const tasks2 = Array.from(this.tasks.values());
41251
+ if (artifacts.length === 0 && tasks2.length === 0) {
41252
+ return "";
41253
+ }
41254
+ const sections = [];
41255
+ sections.push("## Active Context");
41256
+ if (artifacts.length > 0) {
41257
+ sections.push("");
41258
+ sections.push("### Artifacts Created");
41259
+ for (const artifact of artifacts) {
41260
+ const filePath = artifact.filePath ? ` [${artifact.filePath}]` : "";
41261
+ sections.push(`- ${artifact.title} (${artifact.type})${filePath}`);
41262
+ }
41263
+ }
41264
+ if (tasks2.length > 0) {
41265
+ sections.push("");
41266
+ sections.push("### Tasks");
41267
+ const byStatus = {
41268
+ pending: [],
41269
+ in_progress: [],
41270
+ completed: [],
41271
+ cancelled: []
41272
+ };
41273
+ for (const task2 of tasks2) {
41274
+ byStatus[task2.status].push(task2);
41275
+ }
41276
+ const statusOrder = [
41277
+ "in_progress",
41278
+ "pending",
41279
+ "completed",
41280
+ "cancelled"
41281
+ ];
41282
+ for (const status of statusOrder) {
41283
+ const statusTasks = byStatus[status];
41284
+ if (statusTasks.length > 0) {
41285
+ for (const task2 of statusTasks) {
41286
+ const icon = this.getStatusIcon(task2.status);
41287
+ sections.push(`- ${icon} ${task2.title}`);
41288
+ }
41289
+ }
41290
+ }
41291
+ }
41292
+ return sections.join(`
41293
+ `);
41294
+ }
41295
+ getStatusIcon(status) {
41296
+ switch (status) {
41297
+ case "pending":
41298
+ return "○";
41299
+ case "in_progress":
41300
+ return "◐";
41301
+ case "completed":
41302
+ return "●";
41303
+ case "cancelled":
41304
+ return "✕";
41305
+ }
41306
+ }
41307
+ hasContent() {
41308
+ return this.artifacts.size > 0 || this.tasks.size > 0;
41309
+ }
41310
+ clear() {
41311
+ this.artifacts.clear();
41312
+ this.tasks.clear();
41313
+ }
41314
+ toJSON() {
41315
+ return {
41316
+ artifacts: Array.from(this.artifacts.values()),
41317
+ tasks: Array.from(this.tasks.values())
41318
+ };
41319
+ }
41320
+ static fromJSON(state) {
41321
+ const tracker = new ContextTracker;
41322
+ for (const artifact of state.artifacts) {
41323
+ tracker.artifacts.set(artifact.id, artifact);
41324
+ }
41325
+ for (const task2 of state.tasks) {
41326
+ tracker.tasks.set(task2.id, task2);
41327
+ }
41328
+ return tracker;
41329
+ }
41330
+ restore(state) {
41331
+ this.clear();
41332
+ for (const artifact of state.artifacts) {
41333
+ this.artifacts.set(artifact.id, artifact);
41334
+ }
41335
+ for (const task2 of state.tasks) {
41336
+ this.tasks.set(task2.id, task2);
41337
+ }
41338
+ }
41339
+ }
41340
+ // ../sdk/src/exec/event-emitter.ts
41341
+ import { EventEmitter as EventEmitter3 } from "node:events";
41342
+ function generateSessionId() {
41343
+ return `exec-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
41344
+ }
41345
+
41346
+ class ExecEventEmitter {
41347
+ emitter;
41348
+ sessionId;
41349
+ isSessionActive = false;
41350
+ eventLog = [];
41351
+ debugMode = false;
41352
+ constructor(options) {
41353
+ this.emitter = new EventEmitter3;
41354
+ this.sessionId = generateSessionId();
41355
+ this.debugMode = options?.debug ?? false;
41356
+ }
41357
+ getSessionId() {
41358
+ return this.sessionId;
41359
+ }
41360
+ isActive() {
41361
+ return this.isSessionActive;
41362
+ }
41363
+ getEventLog() {
41364
+ return [...this.eventLog];
41365
+ }
41366
+ clearEventLog() {
41367
+ this.eventLog = [];
41368
+ }
41369
+ on(eventType, listener) {
41370
+ this.emitter.on(eventType, listener);
41371
+ return this;
41372
+ }
41373
+ once(eventType, listener) {
41374
+ this.emitter.once(eventType, listener);
41375
+ return this;
41376
+ }
41377
+ off(eventType, listener) {
41378
+ this.emitter.off(eventType, listener);
41379
+ return this;
41380
+ }
41381
+ removeAllListeners(eventType) {
41382
+ if (eventType) {
41383
+ this.emitter.removeAllListeners(eventType);
41384
+ } else {
41385
+ this.emitter.removeAllListeners();
41386
+ }
41387
+ return this;
41388
+ }
41389
+ emit(event) {
41390
+ if (this.debugMode) {
41391
+ this.eventLog.push(event);
41392
+ }
41393
+ this.emitter.emit(event.type, event);
41394
+ }
41395
+ createEventBase(type) {
41396
+ return {
41397
+ type,
41398
+ timestamp: Date.now()
41399
+ };
41400
+ }
41401
+ emitSessionStarted(options) {
41402
+ this.isSessionActive = true;
41403
+ this.emit({
41404
+ ...this.createEventBase("session:started" /* SESSION_STARTED */),
41405
+ data: {
41406
+ sessionId: this.sessionId,
41407
+ model: options?.model,
41408
+ provider: options?.provider
41409
+ }
41410
+ });
41411
+ }
41412
+ emitPromptSubmitted(prompt, truncated = false) {
41413
+ this.emit({
41414
+ ...this.createEventBase("prompt:submitted" /* PROMPT_SUBMITTED */),
41415
+ data: {
41416
+ prompt: truncated ? `${prompt.substring(0, 500)}...` : prompt,
41417
+ truncated
41418
+ }
41419
+ });
41420
+ }
41421
+ emitThinkingStarted(content) {
41422
+ this.emit({
41423
+ ...this.createEventBase("thinking:started" /* THINKING_STARTED */),
41424
+ data: {
41425
+ content
41426
+ }
41427
+ });
41428
+ }
41429
+ emitThinkingStoped() {
41430
+ this.emit({
41431
+ ...this.createEventBase("thinking:stopped" /* THINKING_STOPPED */),
41432
+ data: {}
41433
+ });
41434
+ }
41435
+ emitToolStarted(toolName, toolId) {
41436
+ this.emit({
41437
+ ...this.createEventBase("tool:started" /* TOOL_STARTED */),
41438
+ data: {
41439
+ toolName,
41440
+ toolId
41441
+ }
41442
+ });
41443
+ }
41444
+ emitToolCompleted(toolName, toolId, result, duration3) {
41445
+ this.emit({
41446
+ ...this.createEventBase("tool:completed" /* TOOL_COMPLETED */),
41447
+ data: {
41448
+ toolName,
41449
+ toolId,
41450
+ result,
41451
+ duration: duration3
41452
+ }
41453
+ });
41454
+ }
41455
+ emitToolFailed(toolName, error48, toolId) {
41456
+ this.emit({
41457
+ ...this.createEventBase("tool:failed" /* TOOL_FAILED */),
41458
+ data: {
41459
+ toolName,
41460
+ toolId,
41461
+ error: error48
41462
+ }
41463
+ });
41464
+ }
41465
+ emitTextDelta(content) {
41466
+ this.emit({
41467
+ ...this.createEventBase("text:delta" /* TEXT_DELTA */),
41468
+ data: {
41469
+ content
41470
+ }
41471
+ });
41472
+ }
41473
+ emitResponseCompleted(content) {
41474
+ this.emit({
41475
+ ...this.createEventBase("response:completed" /* RESPONSE_COMPLETED */),
41476
+ data: {
41477
+ content
41478
+ }
41479
+ });
41480
+ }
41481
+ emitErrorOccurred(error48, code) {
41482
+ this.emit({
41483
+ ...this.createEventBase("error:occurred" /* ERROR_OCCURRED */),
41484
+ data: {
41485
+ error: error48,
41486
+ code
41487
+ }
41488
+ });
41489
+ }
41490
+ emitSessionEnded(success2) {
41491
+ this.isSessionActive = false;
41492
+ this.emit({
41493
+ ...this.createEventBase("session:ended" /* SESSION_ENDED */),
41494
+ data: {
41495
+ sessionId: this.sessionId,
41496
+ success: success2
41497
+ }
41498
+ });
41499
+ }
41500
+ }
41501
+ // ../sdk/src/exec/history-manager.ts
41502
+ import {
41503
+ existsSync as existsSync6,
41504
+ mkdirSync as mkdirSync3,
41505
+ readdirSync as readdirSync2,
41506
+ readFileSync as readFileSync4,
41507
+ rmSync as rmSync2,
41508
+ writeFileSync as writeFileSync2
41509
+ } from "node:fs";
41510
+ import { join as join7 } from "node:path";
41511
+ var DEFAULT_MAX_SESSIONS = 30;
41512
+ function generateSessionId2() {
41513
+ const timestamp2 = Date.now().toString(36);
41514
+ const random = Math.random().toString(36).substring(2, 9);
41515
+ return `session-${timestamp2}-${random}`;
41516
+ }
41517
+
41518
+ class HistoryManager {
41519
+ historyDir;
41520
+ maxSessions;
41521
+ constructor(projectPath, options) {
41522
+ this.historyDir = options?.historyDir ?? join7(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
41523
+ this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
41524
+ this.ensureHistoryDir();
41525
+ }
41526
+ ensureHistoryDir() {
41527
+ if (!existsSync6(this.historyDir)) {
41528
+ mkdirSync3(this.historyDir, { recursive: true });
41529
+ }
41530
+ }
41531
+ getSessionPath(sessionId) {
41532
+ return join7(this.historyDir, `${sessionId}.json`);
41533
+ }
41534
+ saveSession(session) {
41535
+ const filePath = this.getSessionPath(session.id);
41536
+ session.updatedAt = Date.now();
41537
+ writeFileSync2(filePath, JSON.stringify(session, null, 2), "utf-8");
41538
+ }
41539
+ loadSession(sessionId) {
41540
+ const filePath = this.getSessionPath(sessionId);
41541
+ if (!existsSync6(filePath)) {
41542
+ return null;
41543
+ }
41544
+ try {
41545
+ const content = readFileSync4(filePath, "utf-8");
41546
+ return JSON.parse(content);
41547
+ } catch {
41548
+ return null;
41549
+ }
41550
+ }
41551
+ deleteSession(sessionId) {
41552
+ const filePath = this.getSessionPath(sessionId);
41553
+ if (!existsSync6(filePath)) {
41554
+ return false;
41555
+ }
41556
+ try {
41557
+ rmSync2(filePath);
41558
+ return true;
41559
+ } catch {
41560
+ return false;
41561
+ }
41562
+ }
41563
+ listSessions(options) {
41564
+ const files = readdirSync2(this.historyDir);
41565
+ let sessions = [];
41566
+ for (const file2 of files) {
41567
+ if (file2.endsWith(".json")) {
41568
+ const session = this.loadSession(file2.replace(".json", ""));
41569
+ if (session) {
41570
+ sessions.push(session);
41571
+ }
41572
+ }
41573
+ }
41574
+ sessions.sort((a, b) => b.updatedAt - a.updatedAt);
41575
+ if (options) {
41576
+ sessions = this.filterSessions(sessions, options);
41577
+ }
41578
+ return sessions;
41579
+ }
41580
+ filterSessions(sessions, options) {
41581
+ let filtered = sessions;
41582
+ if (options.after !== undefined) {
41583
+ const after = options.after;
41584
+ filtered = filtered.filter((s) => s.createdAt >= after);
41585
+ }
41586
+ if (options.before !== undefined) {
41587
+ const before = options.before;
41588
+ filtered = filtered.filter((s) => s.createdAt <= before);
41589
+ }
41590
+ if (options.query) {
41591
+ const query = options.query.toLowerCase();
41592
+ filtered = filtered.filter((session) => session.messages.some((msg) => msg.content.toLowerCase().includes(query)));
41593
+ }
41594
+ const offset = options.offset ?? 0;
41595
+ const limit = options.limit ?? filtered.length;
41596
+ filtered = filtered.slice(offset, offset + limit);
41597
+ return filtered;
41598
+ }
41599
+ searchSessions(query, limit) {
41600
+ return this.listSessions({ query, limit });
41601
+ }
41602
+ getCurrentSession(model = "claude-sonnet-4-5", provider = "claude") {
41603
+ const sessions = this.listSessions({ limit: 1 });
41604
+ if (sessions.length > 0) {
41605
+ return sessions[0];
41606
+ }
41607
+ return this.createNewSession(model, provider);
41608
+ }
41609
+ createNewSession(model = "claude-sonnet-4-5", provider = "claude") {
41610
+ const now = Date.now();
41611
+ return {
41612
+ id: generateSessionId2(),
41613
+ projectPath: this.historyDir.replace(`/${LOCUS_CONFIG.dir}/${LOCUS_CONFIG.sessionsDir}`, ""),
41614
+ messages: [],
41615
+ createdAt: now,
41616
+ updatedAt: now,
41617
+ metadata: {
41618
+ model,
41619
+ provider
41620
+ }
41621
+ };
41622
+ }
41623
+ pruneSessions() {
41624
+ const sessions = this.listSessions();
41625
+ let deleted = 0;
41626
+ if (sessions.length > this.maxSessions) {
41627
+ const sessionsToDelete = sessions.slice(this.maxSessions);
41628
+ for (const session of sessionsToDelete) {
41629
+ if (this.deleteSession(session.id)) {
41630
+ deleted++;
41631
+ }
41632
+ }
41633
+ }
41634
+ return deleted;
41635
+ }
41636
+ getSessionCount() {
41637
+ const files = readdirSync2(this.historyDir);
41638
+ return files.filter((f) => f.endsWith(".json")).length;
41639
+ }
41640
+ sessionExists(sessionId) {
41641
+ return existsSync6(this.getSessionPath(sessionId));
41642
+ }
41643
+ findSessionByPartialId(partialId) {
41644
+ const sessions = this.listSessions();
41645
+ const exact = sessions.find((s) => s.id === partialId);
41646
+ if (exact)
41647
+ return exact;
41648
+ const partial2 = sessions.find((s) => s.id.includes(partialId) || s.id.startsWith(`session-${partialId}`));
41649
+ return partial2 ?? null;
41650
+ }
41651
+ getHistoryDir() {
41652
+ return this.historyDir;
41653
+ }
41654
+ clearAllSessions() {
41655
+ const files = readdirSync2(this.historyDir);
41656
+ let deleted = 0;
41657
+ for (const file2 of files) {
41658
+ if (file2.endsWith(".json")) {
41659
+ try {
41660
+ rmSync2(join7(this.historyDir, file2));
41661
+ deleted++;
41662
+ } catch {}
41663
+ }
41664
+ }
41665
+ return deleted;
41666
+ }
41667
+ }
41668
+
41669
+ // ../sdk/src/exec/exec-session.ts
41670
+ var DEFAULT_MAX_CONTEXT_MESSAGES = 10;
41671
+
41672
+ class ExecSession {
41673
+ aiRunner;
41674
+ history;
41675
+ currentSession = null;
41676
+ eventEmitter;
41677
+ contextTracker;
41678
+ maxContextMessages;
41679
+ model;
41680
+ provider;
41681
+ sessionId;
41682
+ toolStartTimes = new Map;
41683
+ constructor(config2) {
41684
+ this.aiRunner = config2.aiRunner;
41685
+ this.history = new HistoryManager(config2.projectPath);
41686
+ this.eventEmitter = new ExecEventEmitter({ debug: config2.debug });
41687
+ this.contextTracker = new ContextTracker;
41688
+ this.maxContextMessages = config2.maxContextMessages ?? DEFAULT_MAX_CONTEXT_MESSAGES;
41689
+ this.model = config2.model;
41690
+ this.provider = config2.provider;
41691
+ this.sessionId = config2.sessionId;
41692
+ }
41693
+ initialize() {
41694
+ if (this.sessionId) {
41695
+ const loaded = this.history.loadSession(this.sessionId);
41696
+ if (loaded) {
41697
+ this.currentSession = loaded;
41698
+ const metadata = loaded.metadata;
41699
+ if (metadata.contextTracker) {
41700
+ this.contextTracker.restore(metadata.contextTracker);
41701
+ }
41702
+ } else {
41703
+ this.currentSession = this.history.createNewSession(this.model, this.provider);
41704
+ }
41705
+ } else {
41706
+ this.currentSession = this.history.createNewSession(this.model, this.provider);
41707
+ }
41708
+ this.eventEmitter.emitSessionStarted({
41709
+ model: this.model,
41710
+ provider: this.provider
41711
+ });
41712
+ }
41713
+ getSession() {
41714
+ return this.currentSession;
41715
+ }
41716
+ getSessionId() {
41717
+ return this.currentSession?.id ?? null;
41718
+ }
41719
+ getEventEmitter() {
41720
+ return this.eventEmitter;
41721
+ }
41722
+ getHistoryManager() {
41723
+ return this.history;
41724
+ }
41725
+ getContextTracker() {
41726
+ return this.contextTracker;
41727
+ }
41728
+ createArtifact(params) {
41729
+ return this.contextTracker.createArtifact(params);
41730
+ }
41731
+ createTask(params) {
41732
+ return this.contextTracker.createTask(params);
41733
+ }
41734
+ resolveArtifactReference(reference) {
41735
+ return this.contextTracker.getReferencedArtifact(reference);
41736
+ }
41737
+ resolveTaskReference(reference) {
41738
+ return this.contextTracker.getReferencedTask(reference);
41739
+ }
41740
+ getMessages() {
41741
+ return this.currentSession?.messages ?? [];
41742
+ }
41743
+ addMessage(message) {
41744
+ if (!this.currentSession) {
41745
+ throw new Error("Session not initialized. Call initialize() first.");
41746
+ }
41747
+ this.currentSession.messages.push({
41748
+ ...message,
41749
+ timestamp: Date.now()
41750
+ });
41751
+ }
41752
+ async* executeStreaming(userPrompt) {
41753
+ if (!this.currentSession) {
41754
+ throw new Error("Session not initialized. Call initialize() first.");
41755
+ }
41756
+ const startTime = Date.now();
41757
+ this.eventEmitter.emitPromptSubmitted(userPrompt, userPrompt.length > 500);
41758
+ this.currentSession.messages.push({
41759
+ role: "user",
41760
+ content: userPrompt,
41761
+ timestamp: Date.now()
41762
+ });
41763
+ const fullPrompt = this.buildPromptWithHistory(userPrompt);
41764
+ const assistantMessage = {
41765
+ role: "assistant",
41766
+ content: "",
41767
+ timestamp: Date.now(),
41768
+ metadata: { toolsUsed: [], duration: 0 }
41769
+ };
41770
+ let hasError = false;
41771
+ let errorMessage = "";
41772
+ try {
41773
+ const stream4 = this.aiRunner.runStream(fullPrompt);
41774
+ for await (const chunk of stream4) {
41775
+ switch (chunk.type) {
41776
+ case "text_delta":
41777
+ assistantMessage.content += chunk.content;
41778
+ this.eventEmitter.emitTextDelta(chunk.content);
41779
+ break;
41780
+ case "tool_use": {
41781
+ assistantMessage.metadata?.toolsUsed?.push(chunk.tool);
41782
+ const toolKey = chunk.id ?? `${chunk.tool}-${Date.now()}`;
41783
+ this.toolStartTimes.set(toolKey, Date.now());
41784
+ this.eventEmitter.emitToolStarted(chunk.tool, chunk.id);
41785
+ break;
41786
+ }
41787
+ case "thinking":
41788
+ this.eventEmitter.emitThinkingStarted(chunk.content);
41789
+ break;
41790
+ case "tool_result": {
41791
+ const resultKey = chunk.id ?? chunk.tool;
41792
+ const startTime2 = this.toolStartTimes.get(resultKey);
41793
+ const duration4 = startTime2 ? Date.now() - startTime2 : undefined;
41794
+ if (chunk.success) {
41795
+ this.eventEmitter.emitToolCompleted(chunk.tool, chunk.id, undefined, duration4);
41796
+ } else {
41797
+ this.eventEmitter.emitToolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
41798
+ }
41799
+ if (resultKey) {
41800
+ this.toolStartTimes.delete(resultKey);
41801
+ }
41802
+ break;
41803
+ }
41804
+ case "result":
41805
+ this.eventEmitter.emitResponseCompleted(chunk.content);
41806
+ break;
41807
+ case "error":
41808
+ hasError = true;
41809
+ errorMessage = chunk.error;
41810
+ this.eventEmitter.emitErrorOccurred(chunk.error);
41811
+ break;
41812
+ }
41813
+ yield chunk;
41814
+ }
41815
+ } catch (error48) {
41816
+ hasError = true;
41817
+ errorMessage = error48 instanceof Error ? error48.message : String(error48);
41818
+ this.eventEmitter.emitErrorOccurred(errorMessage);
41819
+ yield {
41820
+ type: "error",
41821
+ error: errorMessage
41822
+ };
41823
+ }
41824
+ const duration3 = Date.now() - startTime;
41825
+ if (assistantMessage.metadata) {
41826
+ assistantMessage.metadata.duration = duration3;
41827
+ }
41828
+ if (assistantMessage.content || hasError) {
41829
+ if (hasError && !assistantMessage.content) {
41830
+ assistantMessage.content = `Error: ${errorMessage}`;
41831
+ }
41832
+ this.currentSession.messages.push(assistantMessage);
41833
+ }
41834
+ this.currentSession.updatedAt = Date.now();
41835
+ }
41836
+ async execute(userPrompt) {
41837
+ const chunks = [];
41838
+ let content = "";
41839
+ const toolsUsed = [];
41840
+ const startTime = Date.now();
41841
+ let hasError = false;
41842
+ let errorMessage = "";
41843
+ for await (const chunk of this.executeStreaming(userPrompt)) {
41844
+ chunks.push(chunk);
41845
+ if (chunk.type === "text_delta") {
41846
+ content += chunk.content;
41847
+ } else if (chunk.type === "tool_use") {
41848
+ toolsUsed.push(chunk.tool);
41849
+ } else if (chunk.type === "error") {
41850
+ hasError = true;
41851
+ errorMessage = chunk.error;
41852
+ }
41853
+ }
41854
+ return {
41855
+ content,
41856
+ toolsUsed,
41857
+ duration: Date.now() - startTime,
41858
+ success: !hasError,
41859
+ error: hasError ? errorMessage : undefined
41860
+ };
41861
+ }
41862
+ buildPromptWithHistory(currentPrompt) {
41863
+ const sections = [];
41864
+ const contextSummary = this.contextTracker.buildContextSummary();
41865
+ if (contextSummary) {
41866
+ sections.push(contextSummary);
41867
+ }
41868
+ if (this.currentSession && this.currentSession.messages.length > 1) {
41869
+ const recentMessages = this.currentSession.messages.slice(-(this.maxContextMessages + 1), -1).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join(`
41870
+
41871
+ `);
41872
+ if (recentMessages) {
41873
+ sections.push(`## Conversation History
41874
+ ${recentMessages}`);
41875
+ }
41876
+ }
41877
+ if (sections.length === 0) {
41878
+ return currentPrompt;
41879
+ }
41880
+ sections.push(`## Current Request
41881
+ ${currentPrompt}`);
41882
+ return sections.join(`
41883
+
41884
+ `);
41885
+ }
41886
+ save() {
41887
+ if (!this.currentSession) {
41888
+ throw new Error("Session not initialized. Call initialize() first.");
41889
+ }
41890
+ if (this.contextTracker.hasContent()) {
41891
+ this.currentSession.metadata.contextTracker = this.contextTracker.toJSON();
41892
+ }
41893
+ this.history.saveSession(this.currentSession);
41894
+ this.history.pruneSessions();
41895
+ }
41896
+ reset() {
41897
+ if (!this.currentSession) {
41898
+ throw new Error("Session not initialized. Call initialize() first.");
41899
+ }
41900
+ this.currentSession.messages = [];
41901
+ this.currentSession.updatedAt = Date.now();
41902
+ this.contextTracker.clear();
41903
+ }
41904
+ startNewSession() {
41905
+ this.currentSession = this.history.createNewSession(this.model, this.provider);
41906
+ this.contextTracker.clear();
41907
+ this.eventEmitter.emitSessionStarted({
41908
+ model: this.model,
41909
+ provider: this.provider
41910
+ });
41911
+ }
41912
+ end(success2 = true) {
41913
+ this.eventEmitter.emitSessionEnded(success2);
41914
+ }
41915
+ on(eventType, listener) {
41916
+ this.eventEmitter.on(eventType, listener);
41917
+ return this;
41918
+ }
41919
+ off(eventType, listener) {
41920
+ this.eventEmitter.off(eventType, listener);
41921
+ return this;
41922
+ }
41923
+ }
41924
+ // ../sdk/src/orchestrator.ts
41925
+ import { spawn as spawn4 } from "node:child_process";
41926
+ import { existsSync as existsSync7 } from "node:fs";
41927
+ import { dirname as dirname2, join as join8 } from "node:path";
41928
+ import { fileURLToPath } from "node:url";
41929
+ import { EventEmitter as EventEmitter4 } from "events";
41930
+ var MAX_AGENTS = 5;
41931
+
41932
+ class AgentOrchestrator extends EventEmitter4 {
41933
+ client;
41934
+ config;
41935
+ agents = new Map;
41936
+ isRunning = false;
41937
+ processedTasks = new Set;
41938
+ resolvedSprintId = null;
41939
+ worktreeManager = null;
41940
+ heartbeatInterval = null;
41941
+ constructor(config2) {
41942
+ super();
41943
+ this.config = config2;
41944
+ this.client = new LocusClient({
41945
+ baseUrl: config2.apiBase,
41946
+ token: config2.apiKey
41947
+ });
41948
+ }
41949
+ get agentCount() {
41950
+ return Math.min(Math.max(this.config.agentCount ?? 1, 1), MAX_AGENTS);
41951
+ }
41952
+ get useWorktrees() {
41953
+ return this.config.useWorktrees ?? true;
41954
+ }
41955
+ get worktreeCleanupPolicy() {
41956
+ return this.config.worktreeCleanupPolicy ?? "retain-on-failure";
41957
+ }
41958
+ async resolveSprintId() {
41959
+ if (this.config.sprintId) {
41960
+ return this.config.sprintId;
41961
+ }
41962
+ try {
41963
+ const sprint2 = await this.client.sprints.getActive(this.config.workspaceId);
41964
+ if (sprint2?.id) {
41965
+ console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint2.name}`));
41966
+ return sprint2.id;
41967
+ }
41968
+ } catch {}
41969
+ console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
41970
+ return "";
41971
+ }
41972
+ async start() {
41973
+ if (this.isRunning) {
41974
+ throw new Error("Orchestrator is already running");
41975
+ }
41976
+ this.isRunning = true;
41977
+ this.processedTasks.clear();
41978
+ try {
41979
+ await this.orchestrationLoop();
41980
+ } catch (error48) {
41981
+ this.emit("error", error48);
41982
+ throw error48;
41983
+ } finally {
41984
+ await this.cleanup();
41985
+ }
41986
+ }
41987
+ async orchestrationLoop() {
41988
+ this.resolvedSprintId = await this.resolveSprintId();
41989
+ this.emit("started", {
41990
+ timestamp: new Date,
41991
+ config: this.config,
41992
+ sprintId: this.resolvedSprintId
41993
+ });
41994
+ console.log(`
41995
+ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
41996
+ console.log(c.dim("----------------------------------------------"));
41997
+ console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
41998
+ if (this.resolvedSprintId) {
41999
+ console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
42000
+ }
42001
+ console.log(`${c.bold("Agents:")} ${this.agentCount}`);
42002
+ console.log(`${c.bold("Worktrees:")} ${this.useWorktrees ? "enabled" : "disabled"}`);
42003
+ if (this.useWorktrees) {
42004
+ console.log(`${c.bold("Cleanup policy:")} ${this.worktreeCleanupPolicy}`);
42005
+ console.log(`${c.bold("Auto-push:")} ${this.config.autoPush ? "enabled" : "disabled"}`);
42006
+ }
42007
+ console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
42008
+ console.log(c.dim(`----------------------------------------------
42009
+ `));
42010
+ const tasks2 = await this.getAvailableTasks();
42011
+ if (tasks2.length === 0) {
42012
+ console.log(c.dim("ℹ No available tasks found in the backlog."));
42013
+ return;
42014
+ }
42015
+ if (tasks2.length > 0 && this.useWorktrees && !isGitAvailable()) {
42016
+ console.log(c.error("git is not installed. Worktree isolation requires git. Install from https://git-scm.com/"));
42017
+ return;
42018
+ }
42019
+ if (tasks2.length > 0 && this.config.autoPush && !isGhAvailable(this.config.projectPath)) {
42020
+ 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/"));
42021
+ }
42022
+ if (tasks2.length > 0 && this.useWorktrees) {
42023
+ this.worktreeManager = new WorktreeManager(this.config.projectPath, {
42024
+ cleanupPolicy: this.worktreeCleanupPolicy
42025
+ });
42026
+ }
42027
+ this.startHeartbeatMonitor();
42028
+ const agentsToSpawn = Math.min(this.agentCount, tasks2.length);
42029
+ const SPAWN_DELAY_MS = 5000;
42030
+ const spawnPromises = [];
42031
+ for (let i = 0;i < agentsToSpawn; i++) {
42032
+ if (i > 0) {
42033
+ await this.sleep(SPAWN_DELAY_MS);
42034
+ }
42035
+ spawnPromises.push(this.spawnAgent(i));
42036
+ }
42037
+ await Promise.all(spawnPromises);
42038
+ while (this.agents.size > 0 && this.isRunning) {
42039
+ if (this.agents.size === 0) {
42040
+ break;
42041
+ }
42042
+ await this.sleep(2000);
42043
+ }
42044
+ console.log(`
42045
+ ${c.success("✅ Orchestrator finished")}`);
42046
+ }
42047
+ async spawnAgent(index) {
42048
+ const agentId = `agent-${index}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
42049
+ const agentState = {
42050
+ id: agentId,
42051
+ status: "IDLE",
42052
+ currentTaskId: null,
42053
+ tasksCompleted: 0,
42054
+ tasksFailed: 0,
42055
+ lastHeartbeat: new Date
42056
+ };
42057
+ this.agents.set(agentId, agentState);
42058
+ console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
42059
+ `);
42060
+ const workerPath = this.resolveWorkerPath();
42061
+ if (!workerPath) {
42062
+ throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
42063
+ }
42064
+ const workerArgs = [
42065
+ "--agent-id",
42066
+ agentId,
42067
+ "--workspace-id",
42068
+ this.config.workspaceId,
42069
+ "--api-url",
42070
+ this.config.apiBase,
42071
+ "--api-key",
42072
+ this.config.apiKey,
42073
+ "--project-path",
42074
+ this.config.projectPath
42075
+ ];
42076
+ if (this.config.model) {
42077
+ workerArgs.push("--model", this.config.model);
42078
+ }
42079
+ if (this.config.provider) {
42080
+ workerArgs.push("--provider", this.config.provider);
42081
+ }
42082
+ if (this.resolvedSprintId) {
42083
+ workerArgs.push("--sprint-id", this.resolvedSprintId);
42084
+ }
42085
+ if (this.useWorktrees) {
42086
+ workerArgs.push("--use-worktrees");
42087
+ }
42088
+ if (this.config.autoPush) {
42089
+ workerArgs.push("--auto-push");
42090
+ }
42091
+ const agentProcess = spawn4(process.execPath, [workerPath, ...workerArgs], {
42092
+ stdio: ["pipe", "pipe", "pipe"],
42093
+ detached: true,
42094
+ env: {
42095
+ ...process.env,
42096
+ FORCE_COLOR: "1",
42097
+ TERM: "xterm-256color",
42098
+ LOCUS_WORKER: agentId,
42099
+ LOCUS_WORKSPACE: this.config.workspaceId
42100
+ }
42101
+ });
42102
+ agentState.process = agentProcess;
42103
+ agentProcess.on("message", (msg) => {
42104
+ if (msg.type === "stats") {
42105
+ agentState.tasksCompleted = msg.tasksCompleted || 0;
42106
+ agentState.tasksFailed = msg.tasksFailed || 0;
42107
+ }
42108
+ if (msg.type === "heartbeat") {
42109
+ agentState.lastHeartbeat = new Date;
42110
+ }
42111
+ });
42112
+ agentProcess.stdout?.on("data", (data) => {
42113
+ process.stdout.write(data.toString());
42114
+ });
42115
+ agentProcess.stderr?.on("data", (data) => {
42116
+ process.stderr.write(data.toString());
42117
+ });
42118
+ agentProcess.on("exit", (code) => {
42119
+ console.log(`
42120
+ ${agentId} finished (exit code: ${code})`);
42121
+ const agent2 = this.agents.get(agentId);
42122
+ if (agent2) {
42123
+ agent2.status = code === 0 ? "COMPLETED" : "FAILED";
42124
+ this.emit("agent:completed", {
42125
+ agentId,
42126
+ status: agent2.status,
42127
+ tasksCompleted: agent2.tasksCompleted,
42128
+ tasksFailed: agent2.tasksFailed
42129
+ });
42130
+ this.agents.delete(agentId);
42131
+ }
42132
+ });
42133
+ this.emit("agent:spawned", { agentId });
42134
+ }
42135
+ resolveWorkerPath() {
42136
+ const currentModulePath = fileURLToPath(import.meta.url);
42137
+ const currentModuleDir = dirname2(currentModulePath);
42138
+ const potentialPaths = [
42139
+ join8(currentModuleDir, "agent", "worker.js"),
42140
+ join8(currentModuleDir, "worker.js"),
42141
+ join8(currentModuleDir, "agent", "worker.ts")
42142
+ ];
42143
+ return potentialPaths.find((p) => existsSync7(p));
42144
+ }
42145
+ startHeartbeatMonitor() {
42146
+ this.heartbeatInterval = setInterval(() => {
42147
+ const now = Date.now();
42148
+ for (const [agentId, agent2] of this.agents.entries()) {
42149
+ if (agent2.status === "WORKING" && now - agent2.lastHeartbeat.getTime() > STALE_AGENT_TIMEOUT_MS) {
42150
+ console.log(c.error(`Agent ${agentId} is stale (no heartbeat for 10 minutes). Killing.`));
42151
+ if (agent2.process && !agent2.process.killed) {
42152
+ this.killProcessTree(agent2.process);
42153
+ }
42154
+ this.emit("agent:stale", { agentId });
42155
+ }
42156
+ }
42157
+ }, 60000);
42158
+ }
42159
+ async getAvailableTasks() {
42160
+ try {
42161
+ const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
42162
+ return tasks2.filter((task2) => !this.processedTasks.has(task2.id));
42163
+ } catch (error48) {
42164
+ this.emit("error", error48);
42165
+ return [];
42166
+ }
42167
+ }
42168
+ async assignTaskToAgent(agentId) {
42169
+ const agent2 = this.agents.get(agentId);
42170
+ if (!agent2)
42171
+ return null;
42172
+ try {
42173
+ const tasks2 = await this.getAvailableTasks();
42174
+ const priorityOrder = [
42175
+ "CRITICAL" /* CRITICAL */,
42176
+ "HIGH" /* HIGH */,
42177
+ "MEDIUM" /* MEDIUM */,
42178
+ "LOW" /* LOW */
42179
+ ];
42180
+ let task2 = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
42181
+ if (!task2 && tasks2.length > 0) {
42182
+ task2 = tasks2[0];
42183
+ }
42184
+ if (!task2)
42185
+ return null;
42186
+ agent2.currentTaskId = task2.id;
42187
+ agent2.status = "WORKING";
42188
+ this.emit("task:assigned", {
42189
+ agentId,
42190
+ taskId: task2.id,
42191
+ title: task2.title
42192
+ });
42193
+ return task2;
42194
+ } catch (error48) {
42195
+ this.emit("error", error48);
42196
+ return null;
42197
+ }
42198
+ }
42199
+ async completeTask(taskId, agentId, summary) {
42200
+ try {
42201
+ await this.client.tasks.update(taskId, this.config.workspaceId, {
42202
+ status: "IN_REVIEW" /* IN_REVIEW */
42203
+ });
42204
+ if (summary) {
42205
+ await this.client.tasks.addComment(taskId, this.config.workspaceId, {
42206
+ author: agentId,
42207
+ text: `✅ Task completed
42208
+
42209
+ ${summary}`
42210
+ });
42211
+ }
42212
+ this.processedTasks.add(taskId);
42213
+ const agent2 = this.agents.get(agentId);
42214
+ if (agent2) {
42215
+ agent2.tasksCompleted += 1;
42216
+ agent2.currentTaskId = null;
42217
+ agent2.status = "IDLE";
42218
+ }
42219
+ this.emit("task:completed", { agentId, taskId });
42220
+ } catch (error48) {
42221
+ this.emit("error", error48);
42222
+ }
42223
+ }
42224
+ async failTask(taskId, agentId, error48) {
42225
+ try {
42226
+ await this.client.tasks.update(taskId, this.config.workspaceId, {
42227
+ status: "BACKLOG" /* BACKLOG */,
42228
+ assignedTo: null
42229
+ });
42230
+ await this.client.tasks.addComment(taskId, this.config.workspaceId, {
42231
+ author: agentId,
42232
+ text: `❌ Agent failed: ${error48}`
42233
+ });
42234
+ const agent2 = this.agents.get(agentId);
42235
+ if (agent2) {
42236
+ agent2.tasksFailed += 1;
42237
+ agent2.currentTaskId = null;
42238
+ agent2.status = "IDLE";
42239
+ }
42240
+ this.emit("task:failed", { agentId, taskId, error: error48 });
42241
+ } catch (error49) {
42242
+ this.emit("error", error49);
42243
+ }
42244
+ }
42245
+ async stop() {
42246
+ this.isRunning = false;
42247
+ await this.cleanup();
42248
+ this.emit("stopped", { timestamp: new Date });
42249
+ }
42250
+ stopAgent(agentId) {
42251
+ const agent2 = this.agents.get(agentId);
42252
+ if (!agent2)
42253
+ return false;
42254
+ if (agent2.process && !agent2.process.killed) {
42255
+ this.killProcessTree(agent2.process);
42256
+ }
42257
+ return true;
42258
+ }
42259
+ killProcessTree(proc) {
42260
+ if (!proc.pid || proc.killed)
42261
+ return;
42262
+ try {
42263
+ process.kill(-proc.pid, "SIGTERM");
42264
+ } catch {
42265
+ try {
42266
+ proc.kill("SIGTERM");
42267
+ } catch {}
42268
+ }
42269
+ }
42270
+ async cleanup() {
42271
+ if (this.heartbeatInterval) {
42272
+ clearInterval(this.heartbeatInterval);
42273
+ this.heartbeatInterval = null;
42274
+ }
42275
+ for (const [agentId, agent2] of this.agents.entries()) {
42276
+ if (agent2.process && !agent2.process.killed) {
42277
+ console.log(`Killing agent: ${agentId}`);
42278
+ this.killProcessTree(agent2.process);
42279
+ }
42280
+ }
42281
+ if (this.worktreeManager) {
42282
+ try {
42283
+ if (this.worktreeCleanupPolicy === "auto") {
42284
+ const removed = this.worktreeManager.removeAll();
42285
+ if (removed > 0) {
42286
+ console.log(c.dim(`Cleaned up ${removed} worktree(s)`));
42287
+ }
42288
+ } else if (this.worktreeCleanupPolicy === "retain-on-failure") {
42289
+ this.worktreeManager.prune();
42290
+ console.log(c.dim("Retaining worktrees for failure analysis (cleanup policy: retain-on-failure)"));
42291
+ } else {
42292
+ console.log(c.dim("Skipping worktree cleanup (cleanup policy: manual)"));
42293
+ }
42294
+ } catch {
42295
+ console.log(c.dim("Could not clean up some worktrees"));
42296
+ }
42297
+ }
42298
+ this.agents.clear();
42299
+ }
42300
+ getStats() {
42301
+ return {
42302
+ activeAgents: this.agents.size,
42303
+ agentCount: this.agentCount,
42304
+ useWorktrees: this.useWorktrees,
42305
+ processedTasks: this.processedTasks.size,
42306
+ totalTasksCompleted: Array.from(this.agents.values()).reduce((sum, agent2) => sum + agent2.tasksCompleted, 0),
42307
+ totalTasksFailed: Array.from(this.agents.values()).reduce((sum, agent2) => sum + agent2.tasksFailed, 0)
42308
+ };
42309
+ }
42310
+ getAgentStates() {
42311
+ return Array.from(this.agents.values());
42312
+ }
42313
+ sleep(ms) {
42314
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
42315
+ }
42316
+ }
42317
+ // src/commands/worktree.ts
42318
+ function createWorktreeManager(config2) {
42319
+ return new WorktreeManager(config2.projectPath);
42320
+ }
42321
+ async function worktreesCommand(ctx, config2) {
42322
+ console.log("[worktrees] Listing agent worktrees");
42323
+ try {
42324
+ const manager = createWorktreeManager(config2);
42325
+ const worktrees = manager.listAgentWorktrees();
42326
+ if (worktrees.length === 0) {
42327
+ await ctx.reply(formatInfo("No agent worktrees found."), {
42328
+ parse_mode: "HTML"
42329
+ });
42330
+ return;
42331
+ }
42332
+ let msg = `<b>Agent Worktrees (${worktrees.length})</b>
42333
+
42334
+ `;
42335
+ for (let i = 0;i < worktrees.length; i++) {
42336
+ const wt = worktrees[i];
42337
+ const status = wt.isPrunable ? " ⚠️ stale" : "";
42338
+ msg += `<b>${i + 1}.</b> <code>${escapeHtml(wt.branch)}</code>${status}
42339
+ `;
42340
+ msg += ` HEAD: <code>${wt.head.slice(0, 8)}</code>
42341
+ `;
42342
+ msg += ` Path: <code>${escapeHtml(wt.path)}</code>
42343
+
42344
+ `;
42345
+ }
42346
+ msg += `Use /worktree &lt;number&gt; to view details
42347
+ `;
42348
+ msg += "Use /rmworktree &lt;number|all&gt; to remove";
42349
+ await ctx.reply(msg, { parse_mode: "HTML" });
42350
+ } catch (err) {
42351
+ console.error("[worktrees] Failed:", err);
42352
+ await ctx.reply(formatError(`Failed to list worktrees: ${err instanceof Error ? err.message : String(err)}`), { parse_mode: "HTML" });
42353
+ }
42354
+ }
42355
+ async function worktreeCommand(ctx, config2) {
42356
+ const text = (ctx.message && "text" in ctx.message ? ctx.message.text : "") || "";
42357
+ const arg = text.replace(/^\/worktree\s*/, "").trim();
42358
+ console.log(`[worktree] Select: ${arg || "(empty)"}`);
42359
+ if (!arg) {
42360
+ await ctx.reply(formatError(`Usage: /worktree &lt;number&gt;
42361
+ Run /worktrees to see the list.`), { parse_mode: "HTML" });
42362
+ return;
42363
+ }
42364
+ const index = Number.parseInt(arg, 10);
42365
+ if (Number.isNaN(index) || index < 1) {
42366
+ await ctx.reply(formatError("Please provide a valid worktree number."), {
42367
+ parse_mode: "HTML"
42368
+ });
42369
+ return;
42370
+ }
42371
+ try {
42372
+ const manager = createWorktreeManager(config2);
42373
+ const worktrees = manager.listAgentWorktrees();
42374
+ if (index > worktrees.length) {
42375
+ await ctx.reply(formatError(`Worktree #${index} does not exist. There are ${worktrees.length} worktree(s).`), { parse_mode: "HTML" });
42376
+ return;
42377
+ }
42378
+ const wt = worktrees[index - 1];
42379
+ const hasChanges = !wt.isPrunable && manager.hasChanges(wt.path);
42380
+ let msg = `<b>Worktree #${index}</b>
42381
+
42382
+ `;
42383
+ msg += `<b>Branch:</b> <code>${escapeHtml(wt.branch)}</code>
42384
+ `;
42385
+ msg += `<b>HEAD:</b> <code>${wt.head}</code>
42386
+ `;
42387
+ msg += `<b>Path:</b> <code>${escapeHtml(wt.path)}</code>
42388
+ `;
42389
+ msg += `<b>Status:</b> ${wt.isPrunable ? "⚠️ stale (directory missing)" : hasChanges ? "\uD83D\uDCDD has uncommitted changes" : "✅ clean"}
42390
+ `;
42391
+ await ctx.reply(msg, { parse_mode: "HTML" });
42392
+ } catch (err) {
42393
+ console.error("[worktree] Failed:", err);
42394
+ await ctx.reply(formatError(`Failed to get worktree details: ${err instanceof Error ? err.message : String(err)}`), { parse_mode: "HTML" });
42395
+ }
42396
+ }
42397
+ async function rmworktreeCommand(ctx, config2) {
42398
+ const text = (ctx.message && "text" in ctx.message ? ctx.message.text : "") || "";
42399
+ const arg = text.replace(/^\/rmworktree\s*/, "").trim();
42400
+ console.log(`[rmworktree] Remove: ${arg || "(empty)"}`);
42401
+ if (!arg) {
42402
+ await ctx.reply(formatError(`Usage: /rmworktree &lt;number|all&gt;
42403
+ Run /worktrees to see the list.`), { parse_mode: "HTML" });
42404
+ return;
42405
+ }
42406
+ try {
42407
+ const manager = createWorktreeManager(config2);
42408
+ if (arg === "all") {
42409
+ const count = manager.removeAll();
42410
+ await ctx.reply(formatSuccess(`Removed ${count} worktree(s).`), { parse_mode: "HTML" });
42411
+ return;
42412
+ }
42413
+ const index = Number.parseInt(arg, 10);
42414
+ if (Number.isNaN(index) || index < 1) {
42415
+ await ctx.reply(formatError("Please provide a valid worktree number or 'all'."), { parse_mode: "HTML" });
42416
+ return;
42417
+ }
42418
+ const worktrees = manager.listAgentWorktrees();
42419
+ if (index > worktrees.length) {
42420
+ await ctx.reply(formatError(`Worktree #${index} does not exist. There are ${worktrees.length} worktree(s).`), { parse_mode: "HTML" });
42421
+ return;
42422
+ }
42423
+ const wt = worktrees[index - 1];
42424
+ manager.remove(wt.path, true);
42425
+ await ctx.reply(formatSuccess(`Removed worktree #${index} (${wt.branch}) and its branch.`), { parse_mode: "HTML" });
42426
+ } catch (err) {
42427
+ console.error("[rmworktree] Failed:", err);
42428
+ await ctx.reply(formatError(`Failed to remove worktree: ${err instanceof Error ? err.message : String(err)}`), { parse_mode: "HTML" });
42429
+ }
42430
+ }
42431
+ // src/executor.ts
42432
+ import { spawn as spawn5 } from "node:child_process";
42433
+ import { join as join9 } from "node:path";
38529
42434
  function timestamp2() {
38530
42435
  return new Date().toLocaleTimeString("en-GB", { hour12: false });
38531
42436
  }
@@ -38541,7 +42446,7 @@ class CliExecutor {
38541
42446
  }
38542
42447
  resolveCommand(args) {
38543
42448
  if (this.config.testMode) {
38544
- const cliPath = join2(this.config.projectPath, "packages/cli/src/cli.ts");
42449
+ const cliPath = join9(this.config.projectPath, "packages/cli/src/cli.ts");
38545
42450
  return { cmd: "bun", cmdArgs: ["run", cliPath, ...args] };
38546
42451
  }
38547
42452
  return { cmd: "locus", cmdArgs: args };
@@ -38553,8 +42458,8 @@ class CliExecutor {
38553
42458
  const fullCommand = `${cmd} ${cmdArgs.join(" ")}`;
38554
42459
  const startTime = Date.now();
38555
42460
  log2(id, `Process started: ${fullCommand}`);
38556
- return new Promise((resolve) => {
38557
- const proc = spawn2(cmd, cmdArgs, {
42461
+ return new Promise((resolve3) => {
42462
+ const proc = spawn5(cmd, cmdArgs, {
38558
42463
  cwd: this.config.projectPath,
38559
42464
  env: buildSpawnEnv(),
38560
42465
  stdio: ["pipe", "pipe", "pipe"]
@@ -38582,13 +42487,13 @@ class CliExecutor {
38582
42487
  this.runningProcesses.delete(id);
38583
42488
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
38584
42489
  log2(id, `Process exited (code: ${exitCode}, ${elapsed}s)${killed ? " [killed]" : ""}`);
38585
- resolve({ stdout, stderr, exitCode, killed });
42490
+ resolve3({ stdout, stderr, exitCode, killed });
38586
42491
  });
38587
42492
  proc.on("error", (err) => {
38588
42493
  clearTimeout(timer);
38589
42494
  this.runningProcesses.delete(id);
38590
42495
  log2(id, `Process error: ${err.message}`);
38591
- resolve({
42496
+ resolve3({
38592
42497
  stdout,
38593
42498
  stderr: stderr || err.message,
38594
42499
  exitCode: 1,
@@ -38604,7 +42509,7 @@ class CliExecutor {
38604
42509
  const fullCommand = `${cmd} ${cmdArgs.join(" ")}`;
38605
42510
  const startTime = Date.now();
38606
42511
  log2(id, `Process started (streaming): ${fullCommand}`);
38607
- const proc = spawn2(cmd, cmdArgs, {
42512
+ const proc = spawn5(cmd, cmdArgs, {
38608
42513
  cwd: this.config.projectPath,
38609
42514
  env: buildSpawnEnv(),
38610
42515
  stdio: ["pipe", "pipe", "pipe"]
@@ -38631,19 +42536,19 @@ class CliExecutor {
38631
42536
  killed = true;
38632
42537
  proc.kill("SIGTERM");
38633
42538
  }, timeout);
38634
- const done = new Promise((resolve) => {
42539
+ const done = new Promise((resolve3) => {
38635
42540
  proc.on("close", (exitCode) => {
38636
42541
  clearTimeout(timer);
38637
42542
  this.runningProcesses.delete(id);
38638
42543
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
38639
42544
  log2(id, `Process exited (code: ${exitCode}, ${elapsed}s)${killed ? " [killed]" : ""}`);
38640
- resolve({ stdout, stderr, exitCode, killed });
42545
+ resolve3({ stdout, stderr, exitCode, killed });
38641
42546
  });
38642
42547
  proc.on("error", (err) => {
38643
42548
  clearTimeout(timer);
38644
42549
  this.runningProcesses.delete(id);
38645
42550
  log2(id, `Process error: ${err.message}`);
38646
- resolve({
42551
+ resolve3({
38647
42552
  stdout,
38648
42553
  stderr: stderr || err.message,
38649
42554
  exitCode: 1,
@@ -38736,22 +42641,25 @@ function createBot(config2) {
38736
42641
  bot.command("dev", (ctx) => devCommand(ctx, config2));
38737
42642
  bot.command("status", (ctx) => statusCommand(ctx, executor));
38738
42643
  bot.command("agents", (ctx) => agentsCommand(ctx, executor));
42644
+ bot.command("worktrees", (ctx) => worktreesCommand(ctx, config2));
42645
+ bot.command("worktree", (ctx) => worktreeCommand(ctx, config2));
42646
+ bot.command("rmworktree", (ctx) => rmworktreeCommand(ctx, config2));
38739
42647
  return bot;
38740
42648
  }
38741
42649
 
38742
42650
  // src/config.ts
38743
42651
  var import_dotenv = __toESM(require_main(), 1);
38744
- import { existsSync, readFileSync } from "node:fs";
38745
- import { join as join3 } from "node:path";
42652
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
42653
+ import { join as join10 } from "node:path";
38746
42654
  import_dotenv.default.config();
38747
42655
  var SETTINGS_FILE = "settings.json";
38748
42656
  var CONFIG_DIR = ".locus";
38749
42657
  function loadSettings(projectPath) {
38750
- const settingsPath = join3(projectPath, CONFIG_DIR, SETTINGS_FILE);
38751
- if (!existsSync(settingsPath)) {
42658
+ const settingsPath = join10(projectPath, CONFIG_DIR, SETTINGS_FILE);
42659
+ if (!existsSync8(settingsPath)) {
38752
42660
  return null;
38753
42661
  }
38754
- const raw = readFileSync(settingsPath, "utf-8");
42662
+ const raw = readFileSync5(settingsPath, "utf-8");
38755
42663
  return JSON.parse(raw);
38756
42664
  }
38757
42665
  function resolveConfig() {