@locusai/sdk 0.4.16 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/artifact-syncer.d.ts +2 -1
- package/dist/agent/artifact-syncer.d.ts.map +1 -1
- package/dist/agent/codebase-indexer-service.d.ts +5 -6
- package/dist/agent/codebase-indexer-service.d.ts.map +1 -1
- package/dist/agent/sprint-planner.d.ts +4 -5
- package/dist/agent/sprint-planner.d.ts.map +1 -1
- package/dist/agent/task-executor.d.ts +5 -5
- package/dist/agent/task-executor.d.ts.map +1 -1
- package/dist/agent/worker.d.ts +4 -3
- package/dist/agent/worker.d.ts.map +1 -1
- package/dist/agent/worker.js +492 -238
- package/dist/ai/claude-runner.d.ts +10 -3
- package/dist/ai/claude-runner.d.ts.map +1 -1
- package/dist/ai/codex-runner.d.ts +22 -0
- package/dist/ai/codex-runner.d.ts.map +1 -0
- package/dist/ai/factory.d.ts +9 -0
- package/dist/ai/factory.d.ts.map +1 -0
- package/dist/ai/index.d.ts +4 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/runner.d.ts +6 -0
- package/dist/ai/runner.d.ts.map +1 -0
- package/dist/core/config.d.ts +6 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/indexer.d.ts +15 -2
- package/dist/core/indexer.d.ts.map +1 -1
- package/dist/index-node.js +524 -258
- package/dist/orchestrator.d.ts +3 -1
- package/dist/orchestrator.d.ts.map +1 -1
- package/package.json +7 -6
- package/dist/ai/anthropic-client.d.ts +0 -33
- package/dist/ai/anthropic-client.d.ts.map +0 -1
package/dist/index-node.js
CHANGED
|
@@ -439,13 +439,18 @@ __export(exports_worker, {
|
|
|
439
439
|
AgentWorker: () => AgentWorker
|
|
440
440
|
});
|
|
441
441
|
module.exports = __toCommonJS(exports_worker);
|
|
442
|
-
|
|
443
|
-
// src/ai/anthropic-client.ts
|
|
444
|
-
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
442
|
+
var import_shared3 = require("@locusai/shared");
|
|
445
443
|
|
|
446
444
|
// src/core/config.ts
|
|
447
445
|
var import_node_path = require("node:path");
|
|
448
|
-
var
|
|
446
|
+
var PROVIDER = {
|
|
447
|
+
CLAUDE: "claude",
|
|
448
|
+
CODEX: "codex"
|
|
449
|
+
};
|
|
450
|
+
var DEFAULT_MODEL = {
|
|
451
|
+
[PROVIDER.CLAUDE]: "sonnet",
|
|
452
|
+
[PROVIDER.CODEX]: "gpt-5.1-codex-mini"
|
|
453
|
+
};
|
|
449
454
|
var LOCUS_CONFIG = {
|
|
450
455
|
dir: ".locus",
|
|
451
456
|
configFile: "config.json",
|
|
@@ -460,122 +465,9 @@ function getLocusPath(projectPath, fileName) {
|
|
|
460
465
|
return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
|
|
461
466
|
}
|
|
462
467
|
|
|
463
|
-
// src/ai/anthropic-client.ts
|
|
464
|
-
class AnthropicClient {
|
|
465
|
-
client;
|
|
466
|
-
model;
|
|
467
|
-
constructor(config) {
|
|
468
|
-
this.client = new import_sdk.default({
|
|
469
|
-
apiKey: config.apiKey
|
|
470
|
-
});
|
|
471
|
-
this.model = config.model || DEFAULT_MODEL;
|
|
472
|
-
}
|
|
473
|
-
async run(options) {
|
|
474
|
-
const { systemPrompt, cacheableContext = [], userPrompt } = options;
|
|
475
|
-
const systemContent = [];
|
|
476
|
-
if (systemPrompt) {
|
|
477
|
-
systemContent.push({
|
|
478
|
-
type: "text",
|
|
479
|
-
text: systemPrompt
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
for (let i = 0;i < cacheableContext.length; i++) {
|
|
483
|
-
const isLast = i === cacheableContext.length - 1;
|
|
484
|
-
systemContent.push({
|
|
485
|
-
type: "text",
|
|
486
|
-
text: cacheableContext[i],
|
|
487
|
-
...isLast && {
|
|
488
|
-
cache_control: { type: "ephemeral" }
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
const response = await this.client.messages.create({
|
|
493
|
-
model: this.model,
|
|
494
|
-
max_tokens: 8000,
|
|
495
|
-
system: systemContent,
|
|
496
|
-
messages: [
|
|
497
|
-
{
|
|
498
|
-
role: "user",
|
|
499
|
-
content: userPrompt
|
|
500
|
-
}
|
|
501
|
-
]
|
|
502
|
-
});
|
|
503
|
-
const textBlocks = response.content.filter((block) => block.type === "text");
|
|
504
|
-
return textBlocks.map((block) => block.text).join(`
|
|
505
|
-
`);
|
|
506
|
-
}
|
|
507
|
-
async runSimple(prompt) {
|
|
508
|
-
return this.run({
|
|
509
|
-
userPrompt: prompt
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
468
|
// src/ai/claude-runner.ts
|
|
515
469
|
var import_node_child_process = require("node:child_process");
|
|
516
|
-
|
|
517
|
-
projectPath;
|
|
518
|
-
model;
|
|
519
|
-
constructor(projectPath, model = DEFAULT_MODEL) {
|
|
520
|
-
this.projectPath = projectPath;
|
|
521
|
-
this.model = model;
|
|
522
|
-
}
|
|
523
|
-
async run(prompt, _isPlanning = false) {
|
|
524
|
-
const maxRetries = 3;
|
|
525
|
-
let lastError = null;
|
|
526
|
-
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
527
|
-
try {
|
|
528
|
-
return await this.executeRun(prompt);
|
|
529
|
-
} catch (error) {
|
|
530
|
-
const err = error;
|
|
531
|
-
lastError = err;
|
|
532
|
-
const isLastAttempt = attempt === maxRetries;
|
|
533
|
-
if (!isLastAttempt) {
|
|
534
|
-
const delay = Math.pow(2, attempt) * 1000;
|
|
535
|
-
console.warn(`Claude CLI attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
|
|
536
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
throw lastError || new Error("Claude CLI failed after multiple attempts");
|
|
541
|
-
}
|
|
542
|
-
executeRun(prompt) {
|
|
543
|
-
return new Promise((resolve, reject) => {
|
|
544
|
-
const args = [
|
|
545
|
-
"--dangerously-skip-permissions",
|
|
546
|
-
"--print",
|
|
547
|
-
"--model",
|
|
548
|
-
this.model
|
|
549
|
-
];
|
|
550
|
-
const claude = import_node_child_process.spawn("claude", args, {
|
|
551
|
-
cwd: this.projectPath,
|
|
552
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
553
|
-
env: process.env,
|
|
554
|
-
shell: true
|
|
555
|
-
});
|
|
556
|
-
let output = "";
|
|
557
|
-
let errorOutput = "";
|
|
558
|
-
claude.stdout.on("data", (data) => {
|
|
559
|
-
output += data.toString();
|
|
560
|
-
});
|
|
561
|
-
claude.stderr.on("data", (data) => {
|
|
562
|
-
errorOutput += data.toString();
|
|
563
|
-
});
|
|
564
|
-
claude.on("error", (err) => reject(new Error(`Failed to start Claude CLI (shell: true): ${err.message}. Please ensure the 'claude' command is available in your PATH.`)));
|
|
565
|
-
claude.on("close", (code) => {
|
|
566
|
-
if (code === 0)
|
|
567
|
-
resolve(output);
|
|
568
|
-
else {
|
|
569
|
-
const detail = errorOutput.trim();
|
|
570
|
-
const message = detail ? `Claude CLI error (exit code ${code}): ${detail}` : `Claude CLI exited with code ${code}. Please ensure the Claude CLI is installed and you are logged in (run 'claude' manually to check).`;
|
|
571
|
-
reject(new Error(message));
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
claude.stdin.write(prompt);
|
|
575
|
-
claude.stdin.end();
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
470
|
+
var import_node_path2 = require("node:path");
|
|
579
471
|
|
|
580
472
|
// src/utils/colors.ts
|
|
581
473
|
var ESC = "\x1B[";
|
|
@@ -625,9 +517,270 @@ var c = {
|
|
|
625
517
|
underline: (t) => c.text(t, "underline")
|
|
626
518
|
};
|
|
627
519
|
|
|
628
|
-
// src/
|
|
520
|
+
// src/ai/claude-runner.ts
|
|
521
|
+
class ClaudeRunner {
|
|
522
|
+
model;
|
|
523
|
+
log;
|
|
524
|
+
projectPath;
|
|
525
|
+
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
|
|
526
|
+
this.model = model;
|
|
527
|
+
this.log = log;
|
|
528
|
+
this.projectPath = import_node_path2.resolve(projectPath);
|
|
529
|
+
}
|
|
530
|
+
async run(prompt, _isPlanning = false) {
|
|
531
|
+
const maxRetries = 3;
|
|
532
|
+
let lastError = null;
|
|
533
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
534
|
+
try {
|
|
535
|
+
return await this.executeRun(prompt);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
const err = error;
|
|
538
|
+
lastError = err;
|
|
539
|
+
const isLastAttempt = attempt === maxRetries;
|
|
540
|
+
if (!isLastAttempt) {
|
|
541
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
542
|
+
console.warn(`Claude CLI attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
|
|
543
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
throw lastError || new Error("Claude CLI failed after multiple attempts");
|
|
548
|
+
}
|
|
549
|
+
executeRun(prompt) {
|
|
550
|
+
return new Promise((resolve2, reject) => {
|
|
551
|
+
const args = [
|
|
552
|
+
"--dangerously-skip-permissions",
|
|
553
|
+
"--print",
|
|
554
|
+
"--verbose",
|
|
555
|
+
"--output-format",
|
|
556
|
+
"stream-json",
|
|
557
|
+
"--include-partial-messages",
|
|
558
|
+
"--model",
|
|
559
|
+
this.model
|
|
560
|
+
];
|
|
561
|
+
const env = {
|
|
562
|
+
...process.env,
|
|
563
|
+
FORCE_COLOR: "1",
|
|
564
|
+
TERM: "xterm-256color"
|
|
565
|
+
};
|
|
566
|
+
const claude = import_node_child_process.spawn("claude", args, {
|
|
567
|
+
cwd: this.projectPath,
|
|
568
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
569
|
+
env
|
|
570
|
+
});
|
|
571
|
+
let finalResult = "";
|
|
572
|
+
let errorOutput = "";
|
|
573
|
+
let buffer = "";
|
|
574
|
+
claude.stdout.on("data", (data) => {
|
|
575
|
+
buffer += data.toString();
|
|
576
|
+
const lines = buffer.split(`
|
|
577
|
+
`);
|
|
578
|
+
buffer = lines.pop() || "";
|
|
579
|
+
for (const line of lines) {
|
|
580
|
+
const result = this.handleStreamLine(line);
|
|
581
|
+
if (result)
|
|
582
|
+
finalResult = result;
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
claude.stderr.on("data", (data) => {
|
|
586
|
+
const msg = data.toString();
|
|
587
|
+
errorOutput += msg;
|
|
588
|
+
process.stderr.write(msg);
|
|
589
|
+
});
|
|
590
|
+
claude.on("error", (err) => {
|
|
591
|
+
reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
|
|
592
|
+
});
|
|
593
|
+
claude.on("close", (code) => {
|
|
594
|
+
process.stdout.write(`
|
|
595
|
+
`);
|
|
596
|
+
if (code === 0) {
|
|
597
|
+
resolve2(finalResult);
|
|
598
|
+
} else {
|
|
599
|
+
reject(this.createExecutionError(code, errorOutput));
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
claude.stdin.write(prompt);
|
|
603
|
+
claude.stdin.end();
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
handleStreamLine(line) {
|
|
607
|
+
if (!line.trim())
|
|
608
|
+
return null;
|
|
609
|
+
try {
|
|
610
|
+
const item = JSON.parse(line);
|
|
611
|
+
return this.processStreamItem(item);
|
|
612
|
+
} catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
processStreamItem(item) {
|
|
617
|
+
if (item.type === "result") {
|
|
618
|
+
return item.result || "";
|
|
619
|
+
}
|
|
620
|
+
if (item.type === "stream_event" && item.event) {
|
|
621
|
+
this.handleEvent(item.event);
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
handleEvent(event) {
|
|
626
|
+
const { type, delta, content_block } = event;
|
|
627
|
+
if (type === "content_block_delta" && delta) {
|
|
628
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
629
|
+
this.log?.(delta.text, "info");
|
|
630
|
+
}
|
|
631
|
+
} else if (type === "content_block_start" && content_block) {
|
|
632
|
+
if (content_block.type === "tool_use" && content_block.name) {
|
|
633
|
+
this.log?.(`
|
|
634
|
+
|
|
635
|
+
${c.primary("[Claude]")} ${c.bold(`Running ${content_block.name}...`)}
|
|
636
|
+
`, "info");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
createExecutionError(code, detail) {
|
|
641
|
+
const errorMsg = detail.trim();
|
|
642
|
+
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.`;
|
|
643
|
+
return new Error(message);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/ai/codex-runner.ts
|
|
648
|
+
var import_node_child_process2 = require("node:child_process");
|
|
649
|
+
var import_node_crypto = require("node:crypto");
|
|
629
650
|
var import_node_fs = require("node:fs");
|
|
630
|
-
var
|
|
651
|
+
var import_node_os = require("node:os");
|
|
652
|
+
var import_node_path3 = require("node:path");
|
|
653
|
+
class CodexRunner {
|
|
654
|
+
projectPath;
|
|
655
|
+
model;
|
|
656
|
+
log;
|
|
657
|
+
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
|
|
658
|
+
this.projectPath = projectPath;
|
|
659
|
+
this.model = model;
|
|
660
|
+
this.log = log;
|
|
661
|
+
}
|
|
662
|
+
async run(prompt) {
|
|
663
|
+
const maxRetries = 3;
|
|
664
|
+
let lastError = null;
|
|
665
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
666
|
+
try {
|
|
667
|
+
return await this.executeRun(prompt);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
lastError = error;
|
|
670
|
+
if (attempt < maxRetries) {
|
|
671
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
672
|
+
console.warn(`Codex CLI attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`);
|
|
673
|
+
await this.sleep(delay);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
throw lastError || new Error("Codex CLI failed after multiple attempts");
|
|
678
|
+
}
|
|
679
|
+
executeRun(prompt) {
|
|
680
|
+
return new Promise((resolve2, reject) => {
|
|
681
|
+
const outputPath = import_node_path3.join(import_node_os.tmpdir(), `locus-codex-${import_node_crypto.randomUUID()}.txt`);
|
|
682
|
+
const args = this.buildArgs(outputPath);
|
|
683
|
+
const codex = import_node_child_process2.spawn("codex", args, {
|
|
684
|
+
cwd: this.projectPath,
|
|
685
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
686
|
+
env: process.env,
|
|
687
|
+
shell: false
|
|
688
|
+
});
|
|
689
|
+
let output = "";
|
|
690
|
+
let errorOutput = "";
|
|
691
|
+
const handleOutput = (data) => {
|
|
692
|
+
const msg = data.toString();
|
|
693
|
+
output += msg;
|
|
694
|
+
this.streamToConsole(msg);
|
|
695
|
+
};
|
|
696
|
+
codex.stdout.on("data", handleOutput);
|
|
697
|
+
codex.stderr.on("data", (data) => {
|
|
698
|
+
const msg = data.toString();
|
|
699
|
+
errorOutput += msg;
|
|
700
|
+
this.streamToConsole(msg);
|
|
701
|
+
});
|
|
702
|
+
codex.on("error", (err) => {
|
|
703
|
+
reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
|
|
704
|
+
});
|
|
705
|
+
codex.on("close", (code) => {
|
|
706
|
+
this.cleanupTempFile(outputPath);
|
|
707
|
+
if (code === 0) {
|
|
708
|
+
resolve2(this.readOutput(outputPath, output));
|
|
709
|
+
} else {
|
|
710
|
+
reject(this.createErrorFromOutput(code, errorOutput));
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
codex.stdin.write(prompt);
|
|
714
|
+
codex.stdin.end();
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
buildArgs(outputPath) {
|
|
718
|
+
const args = ["exec", "--full-auto", "--output-last-message", outputPath];
|
|
719
|
+
if (this.model) {
|
|
720
|
+
args.push("--model", this.model);
|
|
721
|
+
}
|
|
722
|
+
args.push("-");
|
|
723
|
+
return args;
|
|
724
|
+
}
|
|
725
|
+
streamToConsole(chunk) {
|
|
726
|
+
for (const rawLine of chunk.split(`
|
|
727
|
+
`)) {
|
|
728
|
+
const line = rawLine.trim();
|
|
729
|
+
if (line && this.shouldDisplay(line)) {
|
|
730
|
+
const formattedLine = "[Codex]: ".concat(line.replace(/\*/g, ""));
|
|
731
|
+
this.log?.(formattedLine, "info");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
shouldDisplay(line) {
|
|
736
|
+
return [
|
|
737
|
+
/^thinking\b/,
|
|
738
|
+
/^\*\*/,
|
|
739
|
+
/^Plan update\b/,
|
|
740
|
+
/^[→•✓]/
|
|
741
|
+
].some((pattern) => pattern.test(line));
|
|
742
|
+
}
|
|
743
|
+
readOutput(outputPath, fallback) {
|
|
744
|
+
if (import_node_fs.existsSync(outputPath)) {
|
|
745
|
+
try {
|
|
746
|
+
const text = import_node_fs.readFileSync(outputPath, "utf-8").trim();
|
|
747
|
+
if (text)
|
|
748
|
+
return text;
|
|
749
|
+
} catch {}
|
|
750
|
+
}
|
|
751
|
+
return fallback.trim();
|
|
752
|
+
}
|
|
753
|
+
createErrorFromOutput(code, errorOutput) {
|
|
754
|
+
const detail = errorOutput.trim();
|
|
755
|
+
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.`;
|
|
756
|
+
return new Error(message);
|
|
757
|
+
}
|
|
758
|
+
cleanupTempFile(path) {
|
|
759
|
+
try {
|
|
760
|
+
if (import_node_fs.existsSync(path))
|
|
761
|
+
import_node_fs.unlinkSync(path);
|
|
762
|
+
} catch {}
|
|
763
|
+
}
|
|
764
|
+
sleep(ms) {
|
|
765
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/ai/factory.ts
|
|
770
|
+
function createAiRunner(provider, config) {
|
|
771
|
+
const resolvedProvider = provider ?? PROVIDER.CLAUDE;
|
|
772
|
+
const model = config.model ?? DEFAULT_MODEL[resolvedProvider];
|
|
773
|
+
switch (resolvedProvider) {
|
|
774
|
+
case PROVIDER.CODEX:
|
|
775
|
+
return new CodexRunner(config.projectPath, model, config.log);
|
|
776
|
+
default:
|
|
777
|
+
return new ClaudeRunner(config.projectPath, model, config.log);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/agent/artifact-syncer.ts
|
|
782
|
+
var import_node_fs2 = require("node:fs");
|
|
783
|
+
var import_node_path4 = require("node:path");
|
|
631
784
|
class ArtifactSyncer {
|
|
632
785
|
deps;
|
|
633
786
|
constructor(deps) {
|
|
@@ -653,21 +806,21 @@ class ArtifactSyncer {
|
|
|
653
806
|
}
|
|
654
807
|
async sync() {
|
|
655
808
|
const artifactsDir = getLocusPath(this.deps.projectPath, "artifactsDir");
|
|
656
|
-
if (!
|
|
657
|
-
|
|
809
|
+
if (!import_node_fs2.existsSync(artifactsDir)) {
|
|
810
|
+
import_node_fs2.mkdirSync(artifactsDir, { recursive: true });
|
|
658
811
|
return;
|
|
659
812
|
}
|
|
660
813
|
try {
|
|
661
|
-
const files =
|
|
814
|
+
const files = import_node_fs2.readdirSync(artifactsDir);
|
|
662
815
|
if (files.length === 0)
|
|
663
816
|
return;
|
|
664
817
|
this.deps.log(`Syncing ${files.length} artifacts to server...`, "info");
|
|
665
818
|
const artifactsGroupId = await this.getOrCreateArtifactsGroup();
|
|
666
819
|
const existingDocs = await this.deps.client.docs.list(this.deps.workspaceId);
|
|
667
820
|
for (const file of files) {
|
|
668
|
-
const filePath =
|
|
669
|
-
if (
|
|
670
|
-
const content =
|
|
821
|
+
const filePath = import_node_path4.join(artifactsDir, file);
|
|
822
|
+
if (import_node_fs2.statSync(filePath).isFile()) {
|
|
823
|
+
const content = import_node_fs2.readFileSync(filePath, "utf-8");
|
|
671
824
|
const title = file.replace(/\.md$/, "").trim();
|
|
672
825
|
if (!title)
|
|
673
826
|
continue;
|
|
@@ -694,28 +847,77 @@ class ArtifactSyncer {
|
|
|
694
847
|
}
|
|
695
848
|
|
|
696
849
|
// src/core/indexer.ts
|
|
697
|
-
var
|
|
698
|
-
var
|
|
850
|
+
var import_node_crypto2 = require("node:crypto");
|
|
851
|
+
var import_node_fs3 = require("node:fs");
|
|
852
|
+
var import_node_path5 = require("node:path");
|
|
699
853
|
var import_globby = require("globby");
|
|
700
854
|
|
|
701
855
|
class CodebaseIndexer {
|
|
702
856
|
projectPath;
|
|
703
857
|
indexPath;
|
|
858
|
+
fullReindexRatioThreshold = 0.2;
|
|
704
859
|
constructor(projectPath) {
|
|
705
860
|
this.projectPath = projectPath;
|
|
706
|
-
this.indexPath =
|
|
861
|
+
this.indexPath = import_node_path5.join(projectPath, ".locus", "codebase-index.json");
|
|
707
862
|
}
|
|
708
|
-
async index(onProgress, treeSummarizer) {
|
|
863
|
+
async index(onProgress, treeSummarizer, force = false) {
|
|
709
864
|
if (!treeSummarizer) {
|
|
710
865
|
throw new Error("A treeSummarizer is required for this indexing method.");
|
|
711
866
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
867
|
+
onProgress?.("Generating file tree...");
|
|
868
|
+
const currentFiles = await this.getFileTree();
|
|
869
|
+
const treeString = currentFiles.join(`
|
|
870
|
+
`);
|
|
871
|
+
const newTreeHash = this.hashTree(treeString);
|
|
872
|
+
const existingIndex = this.loadIndex();
|
|
873
|
+
if (!force && existingIndex?.treeHash === newTreeHash) {
|
|
874
|
+
onProgress?.("No file changes detected, skipping reindex");
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
const currentHashes = this.computeFileHashes(currentFiles);
|
|
878
|
+
const existingHashes = existingIndex?.fileHashes;
|
|
879
|
+
const canIncremental = !force && existingIndex && existingHashes;
|
|
880
|
+
if (canIncremental) {
|
|
881
|
+
onProgress?.("Performing incremental update");
|
|
882
|
+
const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
|
|
883
|
+
const changedFiles = [...added, ...modified];
|
|
884
|
+
const totalChanges = changedFiles.length + deleted.length;
|
|
885
|
+
const existingFileCount = Object.keys(existingHashes).length;
|
|
886
|
+
onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
|
|
887
|
+
if (existingFileCount > 0) {
|
|
888
|
+
const changeRatio = totalChanges / existingFileCount;
|
|
889
|
+
if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
|
|
890
|
+
onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
|
|
891
|
+
const incrementalIndex = await treeSummarizer(changedFiles.join(`
|
|
892
|
+
`));
|
|
893
|
+
const updatedIndex = this.cloneIndex(existingIndex);
|
|
894
|
+
this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
|
|
895
|
+
return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
|
|
896
|
+
}
|
|
897
|
+
if (changedFiles.length === 0 && deleted.length > 0) {
|
|
898
|
+
onProgress?.(`Removing ${deleted.length} deleted files from index`);
|
|
899
|
+
const updatedIndex = this.cloneIndex(existingIndex);
|
|
900
|
+
this.removeFilesFromIndex(updatedIndex, deleted);
|
|
901
|
+
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
902
|
+
}
|
|
903
|
+
if (changedFiles.length === 0 && deleted.length === 0) {
|
|
904
|
+
onProgress?.("No actual file changes, updating hashes only");
|
|
905
|
+
const updatedIndex = this.cloneIndex(existingIndex);
|
|
906
|
+
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
907
|
+
}
|
|
908
|
+
onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
onProgress?.("AI is analyzing codebase structure...");
|
|
912
|
+
const index = await treeSummarizer(treeString);
|
|
913
|
+
return this.applyIndexMetadata(index, currentHashes, newTreeHash);
|
|
914
|
+
}
|
|
915
|
+
async getFileTree() {
|
|
916
|
+
const gitmodulesPath = import_node_path5.join(this.projectPath, ".gitmodules");
|
|
715
917
|
const submoduleIgnores = [];
|
|
716
|
-
if (
|
|
918
|
+
if (import_node_fs3.existsSync(gitmodulesPath)) {
|
|
717
919
|
try {
|
|
718
|
-
const content =
|
|
920
|
+
const content = import_node_fs3.readFileSync(gitmodulesPath, "utf-8");
|
|
719
921
|
const lines = content.split(`
|
|
720
922
|
`);
|
|
721
923
|
for (const line of lines) {
|
|
@@ -728,7 +930,7 @@ class CodebaseIndexer {
|
|
|
728
930
|
}
|
|
729
931
|
} catch {}
|
|
730
932
|
}
|
|
731
|
-
|
|
933
|
+
return import_globby.globby(["**/*"], {
|
|
732
934
|
cwd: this.projectPath,
|
|
733
935
|
gitignore: true,
|
|
734
936
|
ignore: [
|
|
@@ -769,18 +971,11 @@ class CodebaseIndexer {
|
|
|
769
971
|
"**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
|
|
770
972
|
]
|
|
771
973
|
});
|
|
772
|
-
const treeString = files.join(`
|
|
773
|
-
`);
|
|
774
|
-
if (onProgress)
|
|
775
|
-
onProgress("AI is analyzing codebase structure...");
|
|
776
|
-
const index = await treeSummarizer(treeString);
|
|
777
|
-
index.lastIndexed = new Date().toISOString();
|
|
778
|
-
return index;
|
|
779
974
|
}
|
|
780
975
|
loadIndex() {
|
|
781
|
-
if (
|
|
976
|
+
if (import_node_fs3.existsSync(this.indexPath)) {
|
|
782
977
|
try {
|
|
783
|
-
return JSON.parse(
|
|
978
|
+
return JSON.parse(import_node_fs3.readFileSync(this.indexPath, "utf-8"));
|
|
784
979
|
} catch {
|
|
785
980
|
return null;
|
|
786
981
|
}
|
|
@@ -788,11 +983,84 @@ class CodebaseIndexer {
|
|
|
788
983
|
return null;
|
|
789
984
|
}
|
|
790
985
|
saveIndex(index) {
|
|
791
|
-
const dir =
|
|
792
|
-
if (!
|
|
793
|
-
|
|
986
|
+
const dir = import_node_path5.dirname(this.indexPath);
|
|
987
|
+
if (!import_node_fs3.existsSync(dir)) {
|
|
988
|
+
import_node_fs3.mkdirSync(dir, { recursive: true });
|
|
989
|
+
}
|
|
990
|
+
import_node_fs3.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
991
|
+
}
|
|
992
|
+
cloneIndex(index) {
|
|
993
|
+
return JSON.parse(JSON.stringify(index));
|
|
994
|
+
}
|
|
995
|
+
applyIndexMetadata(index, fileHashes, treeHash) {
|
|
996
|
+
index.lastIndexed = new Date().toISOString();
|
|
997
|
+
index.treeHash = treeHash;
|
|
998
|
+
index.fileHashes = fileHashes;
|
|
999
|
+
return index;
|
|
1000
|
+
}
|
|
1001
|
+
hashTree(tree) {
|
|
1002
|
+
return import_node_crypto2.createHash("sha256").update(tree).digest("hex");
|
|
1003
|
+
}
|
|
1004
|
+
hashFile(filePath) {
|
|
1005
|
+
try {
|
|
1006
|
+
const content = import_node_fs3.readFileSync(import_node_path5.join(this.projectPath, filePath), "utf-8");
|
|
1007
|
+
return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
1008
|
+
} catch {
|
|
1009
|
+
return null;
|
|
794
1010
|
}
|
|
795
|
-
|
|
1011
|
+
}
|
|
1012
|
+
computeFileHashes(files) {
|
|
1013
|
+
const hashes = {};
|
|
1014
|
+
for (const file of files) {
|
|
1015
|
+
const hash = this.hashFile(file);
|
|
1016
|
+
if (hash !== null) {
|
|
1017
|
+
hashes[file] = hash;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return hashes;
|
|
1021
|
+
}
|
|
1022
|
+
diffFiles(currentHashes, existingHashes) {
|
|
1023
|
+
const currentFiles = Object.keys(currentHashes);
|
|
1024
|
+
const existingFiles = Object.keys(existingHashes);
|
|
1025
|
+
const existingSet = new Set(existingFiles);
|
|
1026
|
+
const currentSet = new Set(currentFiles);
|
|
1027
|
+
const added = currentFiles.filter((f) => !existingSet.has(f));
|
|
1028
|
+
const deleted = existingFiles.filter((f) => !currentSet.has(f));
|
|
1029
|
+
const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
|
|
1030
|
+
return { added, deleted, modified };
|
|
1031
|
+
}
|
|
1032
|
+
removeFilesFromIndex(index, files) {
|
|
1033
|
+
const fileSet = new Set(files);
|
|
1034
|
+
for (const file of files) {
|
|
1035
|
+
delete index.responsibilities[file];
|
|
1036
|
+
}
|
|
1037
|
+
for (const [symbol, paths] of Object.entries(index.symbols)) {
|
|
1038
|
+
index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
|
|
1039
|
+
if (index.symbols[symbol].length === 0) {
|
|
1040
|
+
delete index.symbols[symbol];
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
mergeIndex(existing, incremental, newHashes, newTreeHash) {
|
|
1045
|
+
const mergedSymbols = { ...existing.symbols };
|
|
1046
|
+
for (const [symbol, paths] of Object.entries(incremental.symbols)) {
|
|
1047
|
+
if (mergedSymbols[symbol]) {
|
|
1048
|
+
mergedSymbols[symbol] = [
|
|
1049
|
+
...new Set([...mergedSymbols[symbol], ...paths])
|
|
1050
|
+
];
|
|
1051
|
+
} else {
|
|
1052
|
+
mergedSymbols[symbol] = paths;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const merged = {
|
|
1056
|
+
symbols: mergedSymbols,
|
|
1057
|
+
responsibilities: {
|
|
1058
|
+
...existing.responsibilities,
|
|
1059
|
+
...incremental.responsibilities
|
|
1060
|
+
},
|
|
1061
|
+
lastIndexed: ""
|
|
1062
|
+
};
|
|
1063
|
+
return this.applyIndexMetadata(merged, newHashes, newTreeHash);
|
|
796
1064
|
}
|
|
797
1065
|
}
|
|
798
1066
|
|
|
@@ -804,9 +1072,8 @@ class CodebaseIndexerService {
|
|
|
804
1072
|
this.deps = deps;
|
|
805
1073
|
this.indexer = new CodebaseIndexer(deps.projectPath);
|
|
806
1074
|
}
|
|
807
|
-
async reindex() {
|
|
1075
|
+
async reindex(force = false) {
|
|
808
1076
|
try {
|
|
809
|
-
this.deps.log("Reindexing codebase...", "info");
|
|
810
1077
|
const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
|
|
811
1078
|
const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
|
|
812
1079
|
1. Key symbols (classes, functions, types) and their locations
|
|
@@ -821,21 +1088,17 @@ File tree:
|
|
|
821
1088
|
${tree}
|
|
822
1089
|
|
|
823
1090
|
Return ONLY valid JSON, no markdown formatting.`;
|
|
824
|
-
|
|
825
|
-
if (this.deps.anthropicClient) {
|
|
826
|
-
response = await this.deps.anthropicClient.run({
|
|
827
|
-
systemPrompt: "You are a codebase analysis expert specialized in extracting structure and symbols from file trees.",
|
|
828
|
-
userPrompt: prompt
|
|
829
|
-
});
|
|
830
|
-
} else {
|
|
831
|
-
response = await this.deps.claudeRunner.run(prompt, true);
|
|
832
|
-
}
|
|
1091
|
+
const response = await this.deps.aiRunner.run(prompt, true);
|
|
833
1092
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
834
1093
|
if (jsonMatch) {
|
|
835
1094
|
return JSON.parse(jsonMatch[0]);
|
|
836
1095
|
}
|
|
837
1096
|
return { symbols: {}, responsibilities: {}, lastIndexed: "" };
|
|
838
|
-
});
|
|
1097
|
+
}, force);
|
|
1098
|
+
if (index === null) {
|
|
1099
|
+
this.deps.log("No changes detected, skipping reindex", "info");
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
839
1102
|
this.indexer.saveIndex(index);
|
|
840
1103
|
this.deps.log("Codebase reindexed successfully", "success");
|
|
841
1104
|
} catch (error) {
|
|
@@ -855,30 +1118,7 @@ class SprintPlanner {
|
|
|
855
1118
|
try {
|
|
856
1119
|
const taskList = tasks2.map((t) => `- [${t.id}] ${t.title}: ${t.description || "No description"}`).join(`
|
|
857
1120
|
`);
|
|
858
|
-
|
|
859
|
-
if (this.deps.anthropicClient) {
|
|
860
|
-
const systemPrompt = `You are an expert project manager and lead engineer specialized in sprint planning and task prioritization.`;
|
|
861
|
-
const userPrompt = `# Sprint Planning: ${sprint.name}
|
|
862
|
-
|
|
863
|
-
## Tasks
|
|
864
|
-
${taskList}
|
|
865
|
-
|
|
866
|
-
## Instructions
|
|
867
|
-
1. Analyze dependencies between these tasks.
|
|
868
|
-
2. Prioritize them for the most efficient execution.
|
|
869
|
-
3. Create a mindmap (in Markdown or Mermaid format) that visualizes the sprint structure.
|
|
870
|
-
4. Output your final plan. The plan should clearly state the order of execution.
|
|
871
|
-
|
|
872
|
-
**IMPORTANT**:
|
|
873
|
-
- Do NOT create any files on the filesystem during this planning phase.
|
|
874
|
-
- Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
|
|
875
|
-
- Your output will be saved as the official sprint mindmap on the server.`;
|
|
876
|
-
plan = await this.deps.anthropicClient.run({
|
|
877
|
-
systemPrompt,
|
|
878
|
-
userPrompt
|
|
879
|
-
});
|
|
880
|
-
} else {
|
|
881
|
-
const planningPrompt = `# Sprint Planning: ${sprint.name}
|
|
1121
|
+
const planningPrompt = `# Sprint Planning: ${sprint.name}
|
|
882
1122
|
|
|
883
1123
|
You are an expert project manager and lead engineer. You need to create a mindmap and execution plan for the following tasks in this sprint.
|
|
884
1124
|
|
|
@@ -895,8 +1135,7 @@ ${taskList}
|
|
|
895
1135
|
- Do NOT create any files on the filesystem during this planning phase.
|
|
896
1136
|
- Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
|
|
897
1137
|
- Your output will be saved as the official sprint mindmap on the server.`;
|
|
898
|
-
|
|
899
|
-
}
|
|
1138
|
+
const plan = await this.deps.aiRunner.run(planningPrompt, true);
|
|
900
1139
|
this.deps.log("Sprint mindmap generated and posted to server.", "success");
|
|
901
1140
|
return plan;
|
|
902
1141
|
} catch (error) {
|
|
@@ -907,7 +1146,7 @@ ${taskList}
|
|
|
907
1146
|
}
|
|
908
1147
|
|
|
909
1148
|
// src/core/prompt-builder.ts
|
|
910
|
-
var
|
|
1149
|
+
var import_node_fs4 = require("node:fs");
|
|
911
1150
|
var import_shared2 = require("@locusai/shared");
|
|
912
1151
|
class PromptBuilder {
|
|
913
1152
|
projectPath;
|
|
@@ -930,9 +1169,9 @@ ${task.description || "No description provided."}
|
|
|
930
1169
|
|
|
931
1170
|
`;
|
|
932
1171
|
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
933
|
-
if (
|
|
1172
|
+
if (import_node_fs4.existsSync(contextPath)) {
|
|
934
1173
|
try {
|
|
935
|
-
const context =
|
|
1174
|
+
const context = import_node_fs4.readFileSync(contextPath, "utf-8");
|
|
936
1175
|
prompt += `## Project Context (from CLAUDE.md)
|
|
937
1176
|
${context}
|
|
938
1177
|
|
|
@@ -942,7 +1181,7 @@ ${context}
|
|
|
942
1181
|
}
|
|
943
1182
|
}
|
|
944
1183
|
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
945
|
-
if (
|
|
1184
|
+
if (import_node_fs4.existsSync(indexPath)) {
|
|
946
1185
|
prompt += `## Codebase Overview
|
|
947
1186
|
There is an index file in the .locus/codebase-index.json and if you need you can check it.
|
|
948
1187
|
|
|
@@ -1033,22 +1272,20 @@ ${this.deps.sprintPlan}
|
|
|
1033
1272
|
${basePrompt}`;
|
|
1034
1273
|
}
|
|
1035
1274
|
try {
|
|
1036
|
-
let plan =
|
|
1037
|
-
if (this.deps.
|
|
1038
|
-
this.deps.log("Phase 1: Planning (
|
|
1039
|
-
const cacheableContext = [basePrompt];
|
|
1040
|
-
plan = await this.deps.anthropicClient.run({
|
|
1041
|
-
systemPrompt: "You are an expert software engineer. Analyze the task carefully and create a detailed implementation plan.",
|
|
1042
|
-
cacheableContext,
|
|
1043
|
-
userPrompt: `## Phase 1: Planning
|
|
1044
|
-
Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`
|
|
1045
|
-
});
|
|
1275
|
+
let plan = null;
|
|
1276
|
+
if (this.deps.skipPlanning) {
|
|
1277
|
+
this.deps.log("Skipping Phase 1: Planning (CLI)...", "info");
|
|
1046
1278
|
} else {
|
|
1047
|
-
this.deps.log("
|
|
1279
|
+
this.deps.log("Phase 1: Planning (CLI)...", "info");
|
|
1280
|
+
const planningPrompt = `${basePrompt}
|
|
1281
|
+
|
|
1282
|
+
## Phase 1: Planning
|
|
1283
|
+
Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
|
|
1284
|
+
plan = await this.deps.aiRunner.run(planningPrompt, true);
|
|
1048
1285
|
}
|
|
1049
1286
|
this.deps.log("Starting Execution...", "info");
|
|
1050
1287
|
let executionPrompt = basePrompt;
|
|
1051
|
-
if (plan) {
|
|
1288
|
+
if (plan != null) {
|
|
1052
1289
|
executionPrompt += `
|
|
1053
1290
|
|
|
1054
1291
|
## Phase 2: Execution
|
|
@@ -1064,11 +1301,11 @@ Execute the task directly.`;
|
|
|
1064
1301
|
executionPrompt += `
|
|
1065
1302
|
|
|
1066
1303
|
When finished, output: <promise>COMPLETE</promise>`;
|
|
1067
|
-
const output = await this.deps.
|
|
1304
|
+
const output = await this.deps.aiRunner.run(executionPrompt);
|
|
1068
1305
|
const success = output.includes("<promise>COMPLETE</promise>");
|
|
1069
1306
|
return {
|
|
1070
1307
|
success,
|
|
1071
|
-
summary: success ? "Task completed by
|
|
1308
|
+
summary: success ? "Task completed by the agent" : "The agent did not signal completion"
|
|
1072
1309
|
};
|
|
1073
1310
|
} catch (error) {
|
|
1074
1311
|
return { success: false, summary: `Error: ${error}` };
|
|
@@ -1077,17 +1314,27 @@ When finished, output: <promise>COMPLETE</promise>`;
|
|
|
1077
1314
|
}
|
|
1078
1315
|
|
|
1079
1316
|
// src/agent/worker.ts
|
|
1317
|
+
function resolveProvider(value) {
|
|
1318
|
+
if (!value || value.startsWith("--")) {
|
|
1319
|
+
console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
|
|
1320
|
+
return PROVIDER.CLAUDE;
|
|
1321
|
+
}
|
|
1322
|
+
if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
|
|
1323
|
+
return value;
|
|
1324
|
+
console.warn(`Warning: invalid --provider value '${value}'. Falling back to 'claude'.`);
|
|
1325
|
+
return PROVIDER.CLAUDE;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1080
1328
|
class AgentWorker {
|
|
1081
1329
|
config;
|
|
1082
1330
|
client;
|
|
1083
|
-
|
|
1084
|
-
anthropicClient;
|
|
1331
|
+
aiRunner;
|
|
1085
1332
|
sprintPlanner;
|
|
1086
1333
|
indexerService;
|
|
1087
1334
|
artifactSyncer;
|
|
1088
1335
|
taskExecutor;
|
|
1089
1336
|
consecutiveEmpty = 0;
|
|
1090
|
-
maxEmpty =
|
|
1337
|
+
maxEmpty = 5;
|
|
1091
1338
|
maxTasks = 50;
|
|
1092
1339
|
tasksCompleted = 0;
|
|
1093
1340
|
pollInterval = 1e4;
|
|
@@ -1105,41 +1352,37 @@ class AgentWorker {
|
|
|
1105
1352
|
factor: 2
|
|
1106
1353
|
}
|
|
1107
1354
|
});
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1355
|
+
const log = this.log.bind(this);
|
|
1356
|
+
const provider = config.provider ?? PROVIDER.CLAUDE;
|
|
1357
|
+
this.aiRunner = createAiRunner(provider, {
|
|
1358
|
+
projectPath,
|
|
1359
|
+
model: config.model,
|
|
1360
|
+
log
|
|
1361
|
+
});
|
|
1114
1362
|
this.sprintPlanner = new SprintPlanner({
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
log: logFn
|
|
1363
|
+
aiRunner: this.aiRunner,
|
|
1364
|
+
log
|
|
1118
1365
|
});
|
|
1119
1366
|
this.indexerService = new CodebaseIndexerService({
|
|
1120
|
-
|
|
1121
|
-
claudeRunner: this.claudeRunner,
|
|
1367
|
+
aiRunner: this.aiRunner,
|
|
1122
1368
|
projectPath,
|
|
1123
|
-
log
|
|
1369
|
+
log
|
|
1124
1370
|
});
|
|
1125
1371
|
this.artifactSyncer = new ArtifactSyncer({
|
|
1126
1372
|
client: this.client,
|
|
1127
1373
|
workspaceId: config.workspaceId,
|
|
1128
1374
|
projectPath,
|
|
1129
|
-
log
|
|
1375
|
+
log
|
|
1130
1376
|
});
|
|
1131
1377
|
this.taskExecutor = new TaskExecutor({
|
|
1132
|
-
|
|
1133
|
-
claudeRunner: this.claudeRunner,
|
|
1378
|
+
aiRunner: this.aiRunner,
|
|
1134
1379
|
projectPath,
|
|
1135
1380
|
sprintPlan: null,
|
|
1136
|
-
|
|
1381
|
+
skipPlanning: config.skipPlanning,
|
|
1382
|
+
log
|
|
1137
1383
|
});
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
} else {
|
|
1141
|
-
this.log("Using Claude CLI for all phases", "info");
|
|
1142
|
-
}
|
|
1384
|
+
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
1385
|
+
this.log(`Using ${providerLabel} CLI for all phases`, "info");
|
|
1143
1386
|
}
|
|
1144
1387
|
log(message, level = "info") {
|
|
1145
1388
|
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
@@ -1186,16 +1429,17 @@ class AgentWorker {
|
|
|
1186
1429
|
const tasks2 = await this.client.tasks.list(this.config.workspaceId, {
|
|
1187
1430
|
sprintId: sprint.id
|
|
1188
1431
|
});
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
return taskDate > latest ? taskDate : latest;
|
|
1193
|
-
}, new Date(0));
|
|
1194
|
-
const mindmapDate = sprint.mindmapUpdatedAt ? new Date(sprint.mindmapUpdatedAt) : new Date(0);
|
|
1195
|
-
if (tasks2.length <= 1) {
|
|
1432
|
+
const activeTasks = tasks2.filter((t) => t.status === import_shared3.TaskStatus.BACKLOG || t.status === import_shared3.TaskStatus.IN_PROGRESS);
|
|
1433
|
+
this.log(`Sprint tasks found: ${activeTasks.length}`, "info");
|
|
1434
|
+
if (activeTasks.length <= 1) {
|
|
1196
1435
|
this.log("Skipping mindmap generation (only one task in sprint).", "info");
|
|
1197
1436
|
this.sprintPlan = null;
|
|
1198
1437
|
} else {
|
|
1438
|
+
const latestTaskCreation = activeTasks.reduce((latest, task) => {
|
|
1439
|
+
const taskDate = new Date(task.createdAt);
|
|
1440
|
+
return taskDate > latest ? taskDate : latest;
|
|
1441
|
+
}, new Date(0));
|
|
1442
|
+
const mindmapDate = sprint.mindmapUpdatedAt ? new Date(sprint.mindmapUpdatedAt) : new Date(0);
|
|
1199
1443
|
const needsPlanning = !sprint.mindmap || sprint.mindmap.trim() === "" || latestTaskCreation > mindmapDate;
|
|
1200
1444
|
if (needsPlanning) {
|
|
1201
1445
|
if (sprint.mindmap && latestTaskCreation > mindmapDate) {
|
|
@@ -1217,6 +1461,9 @@ class AgentWorker {
|
|
|
1217
1461
|
while (this.tasksCompleted < this.maxTasks && this.consecutiveEmpty < this.maxEmpty) {
|
|
1218
1462
|
const task = await this.getNextTask();
|
|
1219
1463
|
if (!task) {
|
|
1464
|
+
if (this.consecutiveEmpty === 0) {
|
|
1465
|
+
this.log("Queue empty, waiting for tasks...", "info");
|
|
1466
|
+
}
|
|
1220
1467
|
this.consecutiveEmpty++;
|
|
1221
1468
|
if (this.consecutiveEmpty >= this.maxEmpty)
|
|
1222
1469
|
break;
|
|
@@ -1228,6 +1475,7 @@ class AgentWorker {
|
|
|
1228
1475
|
const result = await this.executeTask(task);
|
|
1229
1476
|
await this.artifactSyncer.sync();
|
|
1230
1477
|
if (result.success) {
|
|
1478
|
+
this.log(`Completed: ${task.title}`, "success");
|
|
1231
1479
|
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
1232
1480
|
status: "VERIFICATION"
|
|
1233
1481
|
});
|
|
@@ -1237,6 +1485,7 @@ class AgentWorker {
|
|
|
1237
1485
|
});
|
|
1238
1486
|
this.tasksCompleted++;
|
|
1239
1487
|
} else {
|
|
1488
|
+
this.log(`Failed: ${task.title} - ${result.summary}`, "error");
|
|
1240
1489
|
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
1241
1490
|
status: "BACKLOG",
|
|
1242
1491
|
assignedTo: null
|
|
@@ -1265,12 +1514,17 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
|
|
|
1265
1514
|
config.apiBase = args[++i];
|
|
1266
1515
|
else if (arg === "--api-key")
|
|
1267
1516
|
config.apiKey = args[++i];
|
|
1268
|
-
else if (arg === "--anthropic-api-key")
|
|
1269
|
-
config.anthropicApiKey = args[++i];
|
|
1270
1517
|
else if (arg === "--project-path")
|
|
1271
1518
|
config.projectPath = args[++i];
|
|
1272
1519
|
else if (arg === "--model")
|
|
1273
1520
|
config.model = args[++i];
|
|
1521
|
+
else if (arg === "--provider") {
|
|
1522
|
+
const value = args[i + 1];
|
|
1523
|
+
if (value && !value.startsWith("--"))
|
|
1524
|
+
i++;
|
|
1525
|
+
config.provider = resolveProvider(value);
|
|
1526
|
+
} else if (arg === "--skip-planning")
|
|
1527
|
+
config.skipPlanning = true;
|
|
1274
1528
|
}
|
|
1275
1529
|
if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
|
|
1276
1530
|
console.error("Missing required arguments");
|
|
@@ -1287,6 +1541,7 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
|
|
|
1287
1541
|
var exports_index_node = {};
|
|
1288
1542
|
__export(exports_index_node, {
|
|
1289
1543
|
getLocusPath: () => getLocusPath,
|
|
1544
|
+
createAiRunner: () => createAiRunner,
|
|
1290
1545
|
c: () => c,
|
|
1291
1546
|
WorkspacesModule: () => WorkspacesModule,
|
|
1292
1547
|
TasksModule: () => TasksModule,
|
|
@@ -1294,6 +1549,7 @@ __export(exports_index_node, {
|
|
|
1294
1549
|
SprintsModule: () => SprintsModule,
|
|
1295
1550
|
SprintPlanner: () => SprintPlanner,
|
|
1296
1551
|
PromptBuilder: () => PromptBuilder,
|
|
1552
|
+
PROVIDER: () => PROVIDER,
|
|
1297
1553
|
OrganizationsModule: () => OrganizationsModule,
|
|
1298
1554
|
LocusEvent: () => LocusEvent,
|
|
1299
1555
|
LocusEmitter: () => LocusEmitter,
|
|
@@ -1302,23 +1558,23 @@ __export(exports_index_node, {
|
|
|
1302
1558
|
InvitationsModule: () => InvitationsModule,
|
|
1303
1559
|
DocsModule: () => DocsModule,
|
|
1304
1560
|
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
1561
|
+
CodexRunner: () => CodexRunner,
|
|
1305
1562
|
CodebaseIndexerService: () => CodebaseIndexerService,
|
|
1306
1563
|
CodebaseIndexer: () => CodebaseIndexer,
|
|
1307
1564
|
ClaudeRunner: () => ClaudeRunner,
|
|
1308
1565
|
CiModule: () => CiModule,
|
|
1309
1566
|
AuthModule: () => AuthModule,
|
|
1310
1567
|
ArtifactSyncer: () => ArtifactSyncer,
|
|
1311
|
-
AnthropicClient: () => AnthropicClient,
|
|
1312
1568
|
AgentWorker: () => AgentWorker,
|
|
1313
1569
|
AgentOrchestrator: () => AgentOrchestrator
|
|
1314
1570
|
});
|
|
1315
1571
|
module.exports = __toCommonJS(exports_index_node);
|
|
1316
1572
|
// src/orchestrator.ts
|
|
1317
|
-
var
|
|
1318
|
-
var
|
|
1319
|
-
var
|
|
1573
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1574
|
+
var import_node_fs5 = require("node:fs");
|
|
1575
|
+
var import_node_path6 = require("node:path");
|
|
1320
1576
|
var import_node_url = require("node:url");
|
|
1321
|
-
var
|
|
1577
|
+
var import_shared4 = require("@locusai/shared");
|
|
1322
1578
|
var import_events3 = require("events");
|
|
1323
1579
|
class AgentOrchestrator extends import_events3.EventEmitter {
|
|
1324
1580
|
client;
|
|
@@ -1412,9 +1668,9 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
1412
1668
|
`);
|
|
1413
1669
|
const potentialPaths = [];
|
|
1414
1670
|
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
|
|
1415
|
-
const currentModuleDir =
|
|
1416
|
-
potentialPaths.push(
|
|
1417
|
-
const workerPath = potentialPaths.find((p) =>
|
|
1671
|
+
const currentModuleDir = import_node_path6.dirname(currentModulePath);
|
|
1672
|
+
potentialPaths.push(import_node_path6.join(currentModuleDir, "agent", "worker.js"), import_node_path6.join(currentModuleDir, "worker.js"), import_node_path6.join(currentModuleDir, "agent", "worker.ts"));
|
|
1673
|
+
const workerPath = potentialPaths.find((p) => import_node_fs5.existsSync(p));
|
|
1418
1674
|
if (!workerPath) {
|
|
1419
1675
|
throw new Error(`Worker file not found. Checked: ${potentialPaths.join(", ")}. ` + `Make sure the SDK is properly built and installed.`);
|
|
1420
1676
|
}
|
|
@@ -1430,16 +1686,26 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
1430
1686
|
"--project-path",
|
|
1431
1687
|
this.config.projectPath
|
|
1432
1688
|
];
|
|
1433
|
-
if (this.config.anthropicApiKey) {
|
|
1434
|
-
workerArgs.push("--anthropic-api-key", this.config.anthropicApiKey);
|
|
1435
|
-
}
|
|
1436
1689
|
if (this.config.model) {
|
|
1437
1690
|
workerArgs.push("--model", this.config.model);
|
|
1438
1691
|
}
|
|
1692
|
+
if (this.config.provider) {
|
|
1693
|
+
workerArgs.push("--provider", this.config.provider);
|
|
1694
|
+
}
|
|
1695
|
+
if (this.config.skipPlanning) {
|
|
1696
|
+
workerArgs.push("--skip-planning");
|
|
1697
|
+
}
|
|
1439
1698
|
if (this.resolvedSprintId) {
|
|
1440
1699
|
workerArgs.push("--sprint-id", this.resolvedSprintId);
|
|
1441
1700
|
}
|
|
1442
|
-
const agentProcess =
|
|
1701
|
+
const agentProcess = import_node_child_process3.spawn(process.execPath, [workerPath, ...workerArgs], {
|
|
1702
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1703
|
+
env: {
|
|
1704
|
+
...process.env,
|
|
1705
|
+
FORCE_COLOR: "1",
|
|
1706
|
+
TERM: "xterm-256color"
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1443
1709
|
agentState.process = agentProcess;
|
|
1444
1710
|
agentProcess.on("message", (msg) => {
|
|
1445
1711
|
if (msg.type === "stats") {
|
|
@@ -1451,7 +1717,7 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
1451
1717
|
process.stdout.write(data.toString());
|
|
1452
1718
|
});
|
|
1453
1719
|
agentProcess.stderr?.on("data", (data) => {
|
|
1454
|
-
process.stderr.write(
|
|
1720
|
+
process.stderr.write(data.toString());
|
|
1455
1721
|
});
|
|
1456
1722
|
agentProcess.on("exit", (code) => {
|
|
1457
1723
|
console.log(`
|
|
@@ -1487,10 +1753,10 @@ ${agentId} finished (exit code: ${code})`);
|
|
|
1487
1753
|
try {
|
|
1488
1754
|
const tasks2 = await this.getAvailableTasks();
|
|
1489
1755
|
const priorityOrder = [
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1756
|
+
import_shared4.TaskPriority.CRITICAL,
|
|
1757
|
+
import_shared4.TaskPriority.HIGH,
|
|
1758
|
+
import_shared4.TaskPriority.MEDIUM,
|
|
1759
|
+
import_shared4.TaskPriority.LOW
|
|
1494
1760
|
];
|
|
1495
1761
|
let task = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
|
|
1496
1762
|
if (!task && tasks2.length > 0) {
|
|
@@ -1514,7 +1780,7 @@ ${agentId} finished (exit code: ${code})`);
|
|
|
1514
1780
|
async completeTask(taskId, agentId, summary) {
|
|
1515
1781
|
try {
|
|
1516
1782
|
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
1517
|
-
status:
|
|
1783
|
+
status: import_shared4.TaskStatus.VERIFICATION
|
|
1518
1784
|
});
|
|
1519
1785
|
if (summary) {
|
|
1520
1786
|
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
@@ -1539,7 +1805,7 @@ ${summary}`
|
|
|
1539
1805
|
async failTask(taskId, agentId, error) {
|
|
1540
1806
|
try {
|
|
1541
1807
|
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
1542
|
-
status:
|
|
1808
|
+
status: import_shared4.TaskStatus.BACKLOG,
|
|
1543
1809
|
assignedTo: null
|
|
1544
1810
|
});
|
|
1545
1811
|
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
@@ -1580,6 +1846,6 @@ ${summary}`
|
|
|
1580
1846
|
};
|
|
1581
1847
|
}
|
|
1582
1848
|
sleep(ms) {
|
|
1583
|
-
return new Promise((
|
|
1849
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1584
1850
|
}
|
|
1585
1851
|
}
|