@locusai/sdk 0.10.6 → 0.11.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/git-workflow.d.ts +37 -23
- package/dist/agent/git-workflow.d.ts.map +1 -1
- package/dist/agent/task-executor.d.ts.map +1 -1
- package/dist/agent/worker-cli.d.ts.map +1 -1
- package/dist/agent/worker-types.d.ts +0 -10
- package/dist/agent/worker-types.d.ts.map +1 -1
- package/dist/agent/worker.d.ts +8 -8
- package/dist/agent/worker.d.ts.map +1 -1
- package/dist/agent/worker.js +257 -804
- package/dist/ai/codex-runner.d.ts +1 -1
- package/dist/ai/codex-runner.d.ts.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/index-node.d.ts +0 -1
- package/dist/index-node.d.ts.map +1 -1
- package/dist/index-node.js +809 -1536
- package/dist/orchestrator/agent-pool.d.ts +1 -58
- package/dist/orchestrator/agent-pool.d.ts.map +1 -1
- package/dist/orchestrator/execution.d.ts +1 -54
- package/dist/orchestrator/execution.d.ts.map +1 -1
- package/dist/orchestrator/index.d.ts +25 -31
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/tier-merge.d.ts +1 -49
- package/dist/orchestrator/tier-merge.d.ts.map +1 -1
- package/dist/orchestrator/types.d.ts +0 -11
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/planning/agents/architect.d.ts.map +1 -1
- package/dist/planning/agents/cross-task-reviewer.d.ts +2 -6
- package/dist/planning/agents/cross-task-reviewer.d.ts.map +1 -1
- package/dist/planning/agents/sprint-organizer.d.ts +1 -1
- package/dist/planning/agents/sprint-organizer.d.ts.map +1 -1
- package/dist/planning/agents/tech-lead.d.ts.map +1 -1
- package/dist/planning/sprint-plan.d.ts +0 -2
- package/dist/planning/sprint-plan.d.ts.map +1 -1
- package/dist/worktree/index.d.ts +1 -2
- package/dist/worktree/index.d.ts.map +1 -1
- package/dist/worktree/worktree-config.d.ts +1 -59
- package/dist/worktree/worktree-config.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.d.ts +1 -115
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/agent/__tests__/orchestrator.cleanup.test.d.ts +0 -2
- package/dist/agent/__tests__/orchestrator.cleanup.test.d.ts.map +0 -1
- package/dist/agent/__tests__/worker.no-changes.test.d.ts +0 -2
- package/dist/agent/__tests__/worker.no-changes.test.d.ts.map +0 -1
package/dist/index-node.js
CHANGED
|
@@ -574,9 +574,6 @@ var init_config = __esm(() => {
|
|
|
574
574
|
"# Locus AI - Plans (generated per task)",
|
|
575
575
|
".locus/plans/",
|
|
576
576
|
"",
|
|
577
|
-
"# Locus AI - Agent worktrees (parallel execution)",
|
|
578
|
-
".locus-worktrees/",
|
|
579
|
-
"",
|
|
580
577
|
"# Locus AI - Settings (contains API key, telegram config, etc.)",
|
|
581
578
|
".locus/settings.json",
|
|
582
579
|
"",
|
|
@@ -1333,14 +1330,13 @@ class CodexRunner {
|
|
|
1333
1330
|
type: "tool_use",
|
|
1334
1331
|
tool: line.replace(/^[→•✓]\s*/, "")
|
|
1335
1332
|
});
|
|
1336
|
-
} else {
|
|
1337
|
-
enqueueChunk({ type: "text_delta", content: `${line}
|
|
1338
|
-
` });
|
|
1339
1333
|
}
|
|
1340
1334
|
}
|
|
1341
1335
|
};
|
|
1342
1336
|
codex.stdout.on("data", processOutput);
|
|
1343
|
-
codex.stderr.on("data",
|
|
1337
|
+
codex.stderr.on("data", (data) => {
|
|
1338
|
+
finalOutput += data.toString();
|
|
1339
|
+
});
|
|
1344
1340
|
codex.on("error", (err) => {
|
|
1345
1341
|
errorMessage = `Failed to start Codex CLI: ${err.message}. Ensure 'codex' is installed and available in PATH.`;
|
|
1346
1342
|
this.eventEmitter?.emitErrorOccurred(errorMessage, "SPAWN_ERROR");
|
|
@@ -1476,6 +1472,7 @@ class CodexRunner {
|
|
|
1476
1472
|
"exec",
|
|
1477
1473
|
"--full-auto",
|
|
1478
1474
|
"--skip-git-repo-check",
|
|
1475
|
+
"--quiet",
|
|
1479
1476
|
"--output-last-message",
|
|
1480
1477
|
outputPath
|
|
1481
1478
|
];
|
|
@@ -1496,12 +1493,7 @@ class CodexRunner {
|
|
|
1496
1493
|
}
|
|
1497
1494
|
}
|
|
1498
1495
|
shouldDisplay(line) {
|
|
1499
|
-
return
|
|
1500
|
-
/^thinking\b/,
|
|
1501
|
-
/^\*\*/,
|
|
1502
|
-
/^Plan update\b/,
|
|
1503
|
-
/^[→•✓]/
|
|
1504
|
-
].some((pattern) => pattern.test(line));
|
|
1496
|
+
return /^Plan update\b/.test(line);
|
|
1505
1497
|
}
|
|
1506
1498
|
readOutput(outputPath, fallback) {
|
|
1507
1499
|
if (import_node_fs2.existsSync(outputPath)) {
|
|
@@ -1753,583 +1745,247 @@ var init_knowledge_base = __esm(() => {
|
|
|
1753
1745
|
import_node_path5 = require("node:path");
|
|
1754
1746
|
});
|
|
1755
1747
|
|
|
1756
|
-
// src/git
|
|
1757
|
-
class
|
|
1758
|
-
|
|
1748
|
+
// src/agent/git-workflow.ts
|
|
1749
|
+
class GitWorkflow {
|
|
1750
|
+
config;
|
|
1759
1751
|
log;
|
|
1760
|
-
|
|
1761
|
-
|
|
1752
|
+
projectPath;
|
|
1753
|
+
branchName = null;
|
|
1754
|
+
baseBranch = null;
|
|
1755
|
+
ghUsername;
|
|
1756
|
+
constructor(config, log) {
|
|
1757
|
+
this.config = config;
|
|
1762
1758
|
this.log = log;
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
branch,
|
|
1768
|
-
baseBranch: requestedBaseBranch,
|
|
1769
|
-
agentId,
|
|
1770
|
-
summary
|
|
1771
|
-
} = options;
|
|
1772
|
-
const provider = detectRemoteProvider(this.projectPath);
|
|
1773
|
-
if (provider !== "github") {
|
|
1774
|
-
throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
|
|
1775
|
-
}
|
|
1776
|
-
if (!isGhAvailable(this.projectPath)) {
|
|
1777
|
-
throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
|
|
1778
|
-
}
|
|
1779
|
-
const title = `[Locus] ${task.title}`;
|
|
1780
|
-
const body = this.buildPrBody(task, agentId, summary);
|
|
1781
|
-
const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
|
|
1782
|
-
this.validateCreatePrInputs(baseBranch, branch);
|
|
1783
|
-
this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
|
|
1784
|
-
const output = import_node_child_process4.execFileSync("gh", [
|
|
1785
|
-
"pr",
|
|
1786
|
-
"create",
|
|
1787
|
-
"--title",
|
|
1788
|
-
title,
|
|
1789
|
-
"--body",
|
|
1790
|
-
body,
|
|
1791
|
-
"--base",
|
|
1792
|
-
baseBranch,
|
|
1793
|
-
"--head",
|
|
1794
|
-
branch
|
|
1795
|
-
], {
|
|
1796
|
-
cwd: this.projectPath,
|
|
1797
|
-
encoding: "utf-8",
|
|
1798
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1799
|
-
}).trim();
|
|
1800
|
-
const url = output;
|
|
1801
|
-
const prNumber = this.extractPrNumber(url);
|
|
1802
|
-
this.log(`PR created: ${url}`, "success");
|
|
1803
|
-
return { url, number: prNumber };
|
|
1804
|
-
}
|
|
1805
|
-
validateCreatePrInputs(baseBranch, headBranch) {
|
|
1806
|
-
if (!this.hasRemoteBranch(baseBranch)) {
|
|
1807
|
-
throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
|
|
1808
|
-
}
|
|
1809
|
-
if (!this.hasRemoteBranch(headBranch)) {
|
|
1810
|
-
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
1811
|
-
}
|
|
1812
|
-
const baseRef = this.resolveBranchRef(baseBranch);
|
|
1813
|
-
const headRef = this.resolveBranchRef(headBranch);
|
|
1814
|
-
if (!baseRef) {
|
|
1815
|
-
throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
|
|
1816
|
-
}
|
|
1817
|
-
if (!headRef) {
|
|
1818
|
-
throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
|
|
1819
|
-
}
|
|
1820
|
-
const commitsAhead = this.countCommitsAhead(baseRef, headRef);
|
|
1821
|
-
if (commitsAhead <= 0) {
|
|
1822
|
-
throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
countCommitsAhead(baseRef, headRef) {
|
|
1826
|
-
const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
1827
|
-
cwd: this.projectPath,
|
|
1828
|
-
encoding: "utf-8",
|
|
1829
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1830
|
-
}).trim();
|
|
1831
|
-
const value = Number.parseInt(output || "0", 10);
|
|
1832
|
-
return Number.isNaN(value) ? 0 : value;
|
|
1833
|
-
}
|
|
1834
|
-
resolveBranchRef(branch) {
|
|
1835
|
-
if (this.hasLocalBranch(branch)) {
|
|
1836
|
-
return branch;
|
|
1837
|
-
}
|
|
1838
|
-
if (this.hasRemoteTrackingBranch(branch)) {
|
|
1839
|
-
return `origin/${branch}`;
|
|
1759
|
+
this.projectPath = config.projectPath || process.cwd();
|
|
1760
|
+
this.ghUsername = getGhUsername();
|
|
1761
|
+
if (this.ghUsername) {
|
|
1762
|
+
this.log(`GitHub user: ${this.ghUsername}`, "info");
|
|
1840
1763
|
}
|
|
1841
|
-
return null;
|
|
1842
1764
|
}
|
|
1843
|
-
|
|
1765
|
+
createBranch(sprintId) {
|
|
1766
|
+
const defaultBranch = getDefaultBranch(this.projectPath);
|
|
1767
|
+
this.baseBranch = defaultBranch;
|
|
1844
1768
|
try {
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1848
|
-
});
|
|
1849
|
-
return true;
|
|
1769
|
+
this.gitExec(["checkout", defaultBranch]);
|
|
1770
|
+
this.gitExec(["pull", "origin", defaultBranch]);
|
|
1850
1771
|
} catch {
|
|
1851
|
-
|
|
1772
|
+
this.log(`Could not pull latest from ${defaultBranch}, continuing with current state`, "warn");
|
|
1852
1773
|
}
|
|
1774
|
+
const suffix = sprintId ? sprintId.slice(0, 8) : Date.now().toString(36);
|
|
1775
|
+
this.branchName = `locus/${suffix}`;
|
|
1776
|
+
try {
|
|
1777
|
+
this.gitExec(["branch", "-D", this.branchName]);
|
|
1778
|
+
} catch {}
|
|
1779
|
+
this.gitExec(["checkout", "-b", this.branchName]);
|
|
1780
|
+
this.log(`Created branch: ${this.branchName} (from ${defaultBranch})`, "success");
|
|
1781
|
+
return this.branchName;
|
|
1853
1782
|
}
|
|
1854
|
-
|
|
1783
|
+
commitAndPush(task) {
|
|
1784
|
+
if (!this.branchName) {
|
|
1785
|
+
this.log("No branch created yet, skipping commit", "warn");
|
|
1786
|
+
return { branch: null, pushed: false, pushFailed: false };
|
|
1787
|
+
}
|
|
1855
1788
|
try {
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1789
|
+
const status = this.gitExec(["status", "--porcelain"]).trim();
|
|
1790
|
+
if (!status) {
|
|
1791
|
+
const baseBranchCommit = this.getBaseCommit();
|
|
1792
|
+
const headCommit = this.gitExec(["rev-parse", "HEAD"]).trim();
|
|
1793
|
+
if (baseBranchCommit && headCommit !== baseBranchCommit) {
|
|
1794
|
+
return this.pushBranch();
|
|
1795
|
+
}
|
|
1796
|
+
this.log("No changes to commit for this task", "info");
|
|
1797
|
+
return {
|
|
1798
|
+
branch: this.branchName,
|
|
1799
|
+
pushed: false,
|
|
1800
|
+
pushFailed: false,
|
|
1801
|
+
noChanges: true,
|
|
1802
|
+
skipReason: "No changes were made for this task."
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
this.gitExec(["add", "-A"]);
|
|
1806
|
+
const staged = this.gitExec(["diff", "--cached", "--name-only"]).trim();
|
|
1807
|
+
if (!staged) {
|
|
1808
|
+
this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
|
|
1809
|
+
return {
|
|
1810
|
+
branch: this.branchName,
|
|
1811
|
+
pushed: false,
|
|
1812
|
+
pushFailed: false,
|
|
1813
|
+
noChanges: true,
|
|
1814
|
+
skipReason: "All changes were ignored by .gitignore."
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
this.log(`Staging ${staged.split(`
|
|
1818
|
+
`).length} file(s) for commit`, "info");
|
|
1819
|
+
const trailers = [
|
|
1820
|
+
`Task-ID: ${task.id}`,
|
|
1821
|
+
`Agent: ${this.config.agentId}`,
|
|
1822
|
+
"Co-authored-by: LocusAI <agent@locusai.team>"
|
|
1823
|
+
];
|
|
1824
|
+
if (this.ghUsername) {
|
|
1825
|
+
trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
|
|
1826
|
+
}
|
|
1827
|
+
const commitMessage = `feat(agent): ${task.title}
|
|
1828
|
+
|
|
1829
|
+
${trailers.join(`
|
|
1830
|
+
`)}`;
|
|
1831
|
+
this.gitExec(["commit", "-m", commitMessage]);
|
|
1832
|
+
const hash = this.gitExec(["rev-parse", "HEAD"]).trim();
|
|
1833
|
+
this.log(`Committed: ${hash.slice(0, 8)}`, "success");
|
|
1834
|
+
return this.pushBranch();
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1837
|
+
this.log(`Git commit failed: ${errorMessage}`, "error");
|
|
1838
|
+
return {
|
|
1839
|
+
branch: this.branchName,
|
|
1840
|
+
pushed: false,
|
|
1841
|
+
pushFailed: true,
|
|
1842
|
+
pushError: `Git commit/push failed: ${errorMessage}`
|
|
1843
|
+
};
|
|
1863
1844
|
}
|
|
1864
1845
|
}
|
|
1865
|
-
|
|
1846
|
+
pushBranch() {
|
|
1847
|
+
if (!this.branchName) {
|
|
1848
|
+
return { branch: null, pushed: false, pushFailed: false };
|
|
1849
|
+
}
|
|
1866
1850
|
try {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1851
|
+
this.gitExec(["push", "-u", "origin", this.branchName]);
|
|
1852
|
+
this.log(`Pushed ${this.branchName} to origin`, "success");
|
|
1853
|
+
return { branch: this.branchName, pushed: true, pushFailed: false };
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1856
|
+
if (msg.includes("non-fast-forward") || msg.includes("[rejected]") || msg.includes("fetch first")) {
|
|
1857
|
+
this.log(`Push rejected (non-fast-forward). Retrying with --force-with-lease.`, "warn");
|
|
1858
|
+
try {
|
|
1859
|
+
this.gitExec([
|
|
1860
|
+
"push",
|
|
1861
|
+
"--force-with-lease",
|
|
1862
|
+
"-u",
|
|
1863
|
+
"origin",
|
|
1864
|
+
this.branchName
|
|
1865
|
+
]);
|
|
1866
|
+
this.log(`Pushed ${this.branchName} to origin with --force-with-lease`, "success");
|
|
1867
|
+
return { branch: this.branchName, pushed: true, pushFailed: false };
|
|
1868
|
+
} catch (retryErr) {
|
|
1869
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
1870
|
+
this.log(`Git push retry failed: ${retryMsg}`, "error");
|
|
1871
|
+
return {
|
|
1872
|
+
branch: this.branchName,
|
|
1873
|
+
pushed: false,
|
|
1874
|
+
pushFailed: true,
|
|
1875
|
+
pushError: retryMsg
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
this.log(`Git push failed: ${msg}`, "error");
|
|
1880
|
+
return {
|
|
1881
|
+
branch: this.branchName,
|
|
1882
|
+
pushed: false,
|
|
1883
|
+
pushFailed: true,
|
|
1884
|
+
pushError: msg
|
|
1885
|
+
};
|
|
1874
1886
|
}
|
|
1875
1887
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1888
|
+
createPullRequest(completedTasks, summaries) {
|
|
1889
|
+
if (!this.branchName || !this.baseBranch) {
|
|
1890
|
+
return { url: null, error: "No branch or base branch available." };
|
|
1891
|
+
}
|
|
1892
|
+
const provider = detectRemoteProvider(this.projectPath);
|
|
1893
|
+
if (provider !== "github") {
|
|
1894
|
+
return {
|
|
1895
|
+
url: null,
|
|
1896
|
+
error: `PR creation is only supported for GitHub repositories (detected: ${provider})`
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
if (!isGhAvailable(this.projectPath)) {
|
|
1900
|
+
return {
|
|
1901
|
+
url: null,
|
|
1902
|
+
error: "GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/"
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
const title = `[Locus] Sprint tasks (${completedTasks.length} task${completedTasks.length !== 1 ? "s" : ""})`;
|
|
1906
|
+
const body = this.buildPrBody(completedTasks, summaries);
|
|
1907
|
+
this.log(`Creating PR: ${title} (${this.branchName} → ${this.baseBranch})`, "info");
|
|
1885
1908
|
try {
|
|
1886
|
-
import_node_child_process4.execFileSync("gh", [
|
|
1909
|
+
const output = import_node_child_process4.execFileSync("gh", [
|
|
1887
1910
|
"pr",
|
|
1888
|
-
"
|
|
1889
|
-
|
|
1911
|
+
"create",
|
|
1912
|
+
"--title",
|
|
1913
|
+
title,
|
|
1890
1914
|
"--body",
|
|
1891
1915
|
body,
|
|
1892
|
-
|
|
1916
|
+
"--base",
|
|
1917
|
+
this.baseBranch,
|
|
1918
|
+
"--head",
|
|
1919
|
+
this.branchName
|
|
1893
1920
|
], {
|
|
1894
1921
|
cwd: this.projectPath,
|
|
1895
1922
|
encoding: "utf-8",
|
|
1896
1923
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1897
|
-
});
|
|
1924
|
+
}).trim();
|
|
1925
|
+
this.log(`PR created: ${output}`, "success");
|
|
1926
|
+
return { url: output };
|
|
1898
1927
|
} catch (err) {
|
|
1899
|
-
const
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
cwd: this.projectPath,
|
|
1903
|
-
encoding: "utf-8",
|
|
1904
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1905
|
-
});
|
|
1906
|
-
return;
|
|
1907
|
-
}
|
|
1908
|
-
throw err;
|
|
1928
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1929
|
+
this.log(`PR creation failed: ${errorMessage}`, "error");
|
|
1930
|
+
return { url: null, error: errorMessage };
|
|
1909
1931
|
}
|
|
1910
1932
|
}
|
|
1911
|
-
|
|
1933
|
+
checkoutBaseBranch() {
|
|
1934
|
+
if (!this.baseBranch)
|
|
1935
|
+
return;
|
|
1912
1936
|
try {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
"[Locus] in:title",
|
|
1918
|
-
"--state",
|
|
1919
|
-
"open",
|
|
1920
|
-
"--json",
|
|
1921
|
-
"number,title,url,headRefName"
|
|
1922
|
-
], {
|
|
1923
|
-
cwd: this.projectPath,
|
|
1924
|
-
encoding: "utf-8",
|
|
1925
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1926
|
-
}).trim();
|
|
1927
|
-
const prs = JSON.parse(output || "[]");
|
|
1928
|
-
return prs.map((pr) => ({
|
|
1929
|
-
number: pr.number,
|
|
1930
|
-
title: pr.title,
|
|
1931
|
-
url: pr.url,
|
|
1932
|
-
branch: pr.headRefName
|
|
1933
|
-
}));
|
|
1934
|
-
} catch {
|
|
1935
|
-
this.log("Failed to list Locus PRs", "warn");
|
|
1936
|
-
return [];
|
|
1937
|
+
this.gitExec(["checkout", this.baseBranch]);
|
|
1938
|
+
this.log(`Checked out base branch: ${this.baseBranch}`, "info");
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
this.log(`Could not checkout base branch: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
1937
1941
|
}
|
|
1938
1942
|
}
|
|
1939
|
-
|
|
1943
|
+
getBranchName() {
|
|
1944
|
+
return this.branchName;
|
|
1945
|
+
}
|
|
1946
|
+
getBaseBranch() {
|
|
1947
|
+
return this.baseBranch;
|
|
1948
|
+
}
|
|
1949
|
+
getBaseCommit() {
|
|
1950
|
+
if (!this.baseBranch)
|
|
1951
|
+
return null;
|
|
1940
1952
|
try {
|
|
1941
|
-
|
|
1942
|
-
cwd: this.projectPath,
|
|
1943
|
-
encoding: "utf-8",
|
|
1944
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1945
|
-
}).trim();
|
|
1946
|
-
const data = JSON.parse(output || "{}");
|
|
1947
|
-
return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
|
|
1953
|
+
return this.gitExec(["rev-parse", this.baseBranch]).trim();
|
|
1948
1954
|
} catch {
|
|
1949
|
-
return
|
|
1955
|
+
return null;
|
|
1950
1956
|
}
|
|
1951
1957
|
}
|
|
1952
|
-
|
|
1953
|
-
const allPrs = this.listLocusPrs();
|
|
1954
|
-
return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
|
|
1955
|
-
}
|
|
1956
|
-
buildPrBody(task, agentId, summary) {
|
|
1958
|
+
buildPrBody(completedTasks, summaries) {
|
|
1957
1959
|
const sections = [];
|
|
1958
|
-
sections.push(
|
|
1960
|
+
sections.push("## Completed Tasks");
|
|
1959
1961
|
sections.push("");
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
sections.push(
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
sections.push(`- [ ] ${item.text}`);
|
|
1962
|
+
for (let i = 0;i < completedTasks.length; i++) {
|
|
1963
|
+
const task = completedTasks[i];
|
|
1964
|
+
sections.push(`### ${i + 1}. ${task.title}`);
|
|
1965
|
+
sections.push(`Task ID: \`${task.id}\``);
|
|
1966
|
+
if (summaries[i]) {
|
|
1967
|
+
sections.push("");
|
|
1968
|
+
sections.push(summaries[i]);
|
|
1968
1969
|
}
|
|
1969
1970
|
sections.push("");
|
|
1970
1971
|
}
|
|
1971
|
-
if (summary) {
|
|
1972
|
-
sections.push("## Agent Summary");
|
|
1973
|
-
sections.push(summary);
|
|
1974
|
-
sections.push("");
|
|
1975
|
-
}
|
|
1976
1972
|
sections.push("---");
|
|
1977
|
-
sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}
|
|
1973
|
+
sections.push(`*Created by Locus Agent \`${this.config.agentId.slice(-8)}\`*`);
|
|
1978
1974
|
return sections.join(`
|
|
1979
1975
|
`);
|
|
1980
1976
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
var import_node_child_process4;
|
|
1987
|
-
var init_pr_service = __esm(() => {
|
|
1988
|
-
init_git_utils();
|
|
1989
|
-
import_node_child_process4 = require("node:child_process");
|
|
1990
|
-
});
|
|
1991
|
-
|
|
1992
|
-
// src/worktree/worktree-config.ts
|
|
1993
|
-
var WORKTREE_ROOT_DIR = ".locus-worktrees", WORKTREE_BRANCH_PREFIX = "agent", DEFAULT_WORKTREE_CONFIG;
|
|
1994
|
-
var init_worktree_config = __esm(() => {
|
|
1995
|
-
DEFAULT_WORKTREE_CONFIG = {
|
|
1996
|
-
rootDir: WORKTREE_ROOT_DIR,
|
|
1997
|
-
branchPrefix: WORKTREE_BRANCH_PREFIX,
|
|
1998
|
-
cleanupPolicy: "retain-on-failure"
|
|
1999
|
-
};
|
|
2000
|
-
});
|
|
2001
|
-
|
|
2002
|
-
// src/worktree/worktree-manager.ts
|
|
2003
|
-
class WorktreeManager {
|
|
2004
|
-
config;
|
|
2005
|
-
projectPath;
|
|
2006
|
-
log;
|
|
2007
|
-
constructor(projectPath, config, log) {
|
|
2008
|
-
this.projectPath = import_node_path6.resolve(projectPath);
|
|
2009
|
-
this.config = { ...DEFAULT_WORKTREE_CONFIG, ...config };
|
|
2010
|
-
this.log = log ?? ((_msg) => {
|
|
2011
|
-
return;
|
|
2012
|
-
});
|
|
2013
|
-
}
|
|
2014
|
-
get rootPath() {
|
|
2015
|
-
return import_node_path6.join(this.projectPath, this.config.rootDir);
|
|
2016
|
-
}
|
|
2017
|
-
buildBranchName(taskId, taskSlug) {
|
|
2018
|
-
const sanitized = taskSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
2019
|
-
return `${this.config.branchPrefix}/${taskId}-${sanitized}`;
|
|
2020
|
-
}
|
|
2021
|
-
create(options) {
|
|
2022
|
-
const branch = this.buildBranchName(options.taskId, options.taskSlug);
|
|
2023
|
-
const worktreeDir = `${options.agentId}-${options.taskId}`;
|
|
2024
|
-
const worktreePath = import_node_path6.join(this.rootPath, worktreeDir);
|
|
2025
|
-
this.ensureDirectory(this.rootPath, "Worktree root");
|
|
2026
|
-
const baseBranch = options.baseBranch ?? this.config.baseBranch ?? this.getCurrentBranch();
|
|
2027
|
-
if (!this.branchExists(baseBranch)) {
|
|
2028
|
-
this.log(`Base branch "${baseBranch}" not found locally, fetching from origin`, "info");
|
|
2029
|
-
try {
|
|
2030
|
-
this.gitExec(["fetch", "origin", baseBranch], this.projectPath);
|
|
2031
|
-
this.gitExec(["branch", baseBranch, `origin/${baseBranch}`], this.projectPath);
|
|
2032
|
-
} catch {
|
|
2033
|
-
this.log(`Could not fetch/create local branch for "${baseBranch}", falling back to current branch`, "warn");
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
this.log(`Creating worktree: ${worktreeDir} (branch: ${branch}, base: ${baseBranch})`, "info");
|
|
2037
|
-
if (import_node_fs4.existsSync(worktreePath)) {
|
|
2038
|
-
this.log(`Removing stale worktree directory: ${worktreePath}`, "warn");
|
|
2039
|
-
try {
|
|
2040
|
-
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
2041
|
-
} catch {
|
|
2042
|
-
import_node_fs4.rmSync(worktreePath, { recursive: true, force: true });
|
|
2043
|
-
this.git("worktree prune", this.projectPath);
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
if (this.branchExists(branch)) {
|
|
2047
|
-
this.log(`Deleting existing branch: ${branch}`, "warn");
|
|
2048
|
-
const branchWorktrees = this.list().filter((wt) => wt.branch === branch);
|
|
2049
|
-
for (const wt of branchWorktrees) {
|
|
2050
|
-
const worktreePath2 = import_node_path6.resolve(wt.path);
|
|
2051
|
-
if (wt.isMain || !this.isManagedWorktreePath(worktreePath2)) {
|
|
2052
|
-
throw new Error(`Branch "${branch}" is checked out at "${worktreePath2}". Remove or detach that worktree before retrying.`);
|
|
2053
|
-
}
|
|
2054
|
-
this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
|
|
2055
|
-
this.remove(worktreePath2, false);
|
|
2056
|
-
}
|
|
2057
|
-
try {
|
|
2058
|
-
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
2059
|
-
} catch {
|
|
2060
|
-
this.git("worktree prune", this.projectPath);
|
|
2061
|
-
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
const addWorktree = () => this.git(`worktree add "${worktreePath}" -b "${branch}" "${baseBranch}"`, this.projectPath);
|
|
2065
|
-
try {
|
|
2066
|
-
addWorktree();
|
|
2067
|
-
} catch (error) {
|
|
2068
|
-
if (!this.isMissingDirectoryError(error)) {
|
|
2069
|
-
throw error;
|
|
2070
|
-
}
|
|
2071
|
-
this.log(`Worktree creation failed due to missing directories. Retrying after cleanup: ${worktreePath}`, "warn");
|
|
2072
|
-
this.cleanupFailedWorktree(worktreePath, branch);
|
|
2073
|
-
this.ensureDirectory(this.rootPath, "Worktree root");
|
|
2074
|
-
addWorktree();
|
|
2075
|
-
}
|
|
2076
|
-
const baseCommitHash = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2077
|
-
this.log(`Worktree created at ${worktreePath} (base: ${baseCommitHash.slice(0, 8)})`, "success");
|
|
2078
|
-
return { worktreePath, branch, baseBranch, baseCommitHash };
|
|
2079
|
-
}
|
|
2080
|
-
list() {
|
|
2081
|
-
const output = this.git("worktree list --porcelain", this.projectPath);
|
|
2082
|
-
const worktrees = [];
|
|
2083
|
-
const blocks = output.trim().split(`
|
|
2084
|
-
|
|
2085
|
-
`);
|
|
2086
|
-
for (const block of blocks) {
|
|
2087
|
-
if (!block.trim())
|
|
2088
|
-
continue;
|
|
2089
|
-
const lines = block.trim().split(`
|
|
2090
|
-
`);
|
|
2091
|
-
let path = "";
|
|
2092
|
-
let head = "";
|
|
2093
|
-
let branch = "";
|
|
2094
|
-
let isMain = false;
|
|
2095
|
-
let isPrunable = false;
|
|
2096
|
-
for (const line of lines) {
|
|
2097
|
-
if (line.startsWith("worktree ")) {
|
|
2098
|
-
path = line.slice("worktree ".length);
|
|
2099
|
-
} else if (line.startsWith("HEAD ")) {
|
|
2100
|
-
head = line.slice("HEAD ".length);
|
|
2101
|
-
} else if (line.startsWith("branch ")) {
|
|
2102
|
-
branch = line.slice("branch ".length).replace("refs/heads/", "");
|
|
2103
|
-
} else if (line === "bare" || path === this.projectPath) {
|
|
2104
|
-
isMain = true;
|
|
2105
|
-
} else if (line === "prunable") {
|
|
2106
|
-
isPrunable = true;
|
|
2107
|
-
} else if (line === "detached") {
|
|
2108
|
-
branch = "(detached)";
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
if (import_node_path6.resolve(path) === this.projectPath) {
|
|
2112
|
-
isMain = true;
|
|
2113
|
-
}
|
|
2114
|
-
if (path) {
|
|
2115
|
-
worktrees.push({ path, branch, head, isMain, isPrunable });
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
return worktrees;
|
|
2119
|
-
}
|
|
2120
|
-
listAgentWorktrees() {
|
|
2121
|
-
return this.list().filter((wt) => !wt.isMain);
|
|
2122
|
-
}
|
|
2123
|
-
remove(worktreePath, deleteBranch = true) {
|
|
2124
|
-
const absolutePath = import_node_path6.resolve(worktreePath);
|
|
2125
|
-
const worktrees = this.list();
|
|
2126
|
-
const worktree = worktrees.find((wt) => import_node_path6.resolve(wt.path) === absolutePath);
|
|
2127
|
-
const branchToDelete = worktree?.branch;
|
|
2128
|
-
this.log(`Removing worktree: ${absolutePath}`, "info");
|
|
2129
|
-
try {
|
|
2130
|
-
this.git(`worktree remove "${absolutePath}" --force`, this.projectPath);
|
|
2131
|
-
} catch {
|
|
2132
|
-
if (import_node_fs4.existsSync(absolutePath)) {
|
|
2133
|
-
import_node_fs4.rmSync(absolutePath, { recursive: true, force: true });
|
|
2134
|
-
}
|
|
2135
|
-
this.git("worktree prune", this.projectPath);
|
|
2136
|
-
}
|
|
2137
|
-
if (deleteBranch && branchToDelete && !branchToDelete.startsWith("(")) {
|
|
2138
|
-
try {
|
|
2139
|
-
this.git(`branch -D "${branchToDelete}"`, this.projectPath);
|
|
2140
|
-
this.log(`Deleted branch: ${branchToDelete}`, "success");
|
|
2141
|
-
} catch {
|
|
2142
|
-
this.log(`Could not delete branch: ${branchToDelete} (may already be deleted)`, "warn");
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
this.log("Worktree removed", "success");
|
|
2146
|
-
}
|
|
2147
|
-
prune() {
|
|
2148
|
-
const before = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
2149
|
-
this.git("worktree prune", this.projectPath);
|
|
2150
|
-
const after = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
2151
|
-
const pruned = before - after;
|
|
2152
|
-
if (pruned > 0) {
|
|
2153
|
-
this.log(`Pruned ${pruned} stale worktree(s)`, "success");
|
|
2154
|
-
}
|
|
2155
|
-
return pruned;
|
|
2156
|
-
}
|
|
2157
|
-
removeAll() {
|
|
2158
|
-
const agentWorktrees = this.listAgentWorktrees();
|
|
2159
|
-
let removed = 0;
|
|
2160
|
-
for (const wt of agentWorktrees) {
|
|
2161
|
-
try {
|
|
2162
|
-
this.remove(wt.path, true);
|
|
2163
|
-
removed++;
|
|
2164
|
-
} catch {
|
|
2165
|
-
this.log(`Failed to remove worktree: ${wt.path}`, "warn");
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
if (import_node_fs4.existsSync(this.rootPath)) {
|
|
2169
|
-
try {
|
|
2170
|
-
import_node_fs4.rmSync(this.rootPath, { recursive: true, force: true });
|
|
2171
|
-
} catch {}
|
|
2172
|
-
}
|
|
2173
|
-
return removed;
|
|
2174
|
-
}
|
|
2175
|
-
hasChanges(worktreePath) {
|
|
2176
|
-
const status = this.git("status --porcelain", worktreePath).trim();
|
|
2177
|
-
return status.length > 0;
|
|
2178
|
-
}
|
|
2179
|
-
hasCommitsAhead(worktreePath, baseBranch) {
|
|
2180
|
-
try {
|
|
2181
|
-
const count = this.git(`rev-list --count "${baseBranch}..HEAD"`, worktreePath).trim();
|
|
2182
|
-
return Number.parseInt(count, 10) > 0;
|
|
2183
|
-
} catch (err) {
|
|
2184
|
-
this.log(`Could not compare HEAD against base branch "${baseBranch}": ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
2185
|
-
return false;
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
hasCommitsAheadOfHash(worktreePath, baseHash) {
|
|
2189
|
-
try {
|
|
2190
|
-
const headHash = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2191
|
-
return headHash !== baseHash;
|
|
2192
|
-
} catch {
|
|
2193
|
-
return false;
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
commitChanges(worktreePath, message, baseBranch, baseCommitHash) {
|
|
2197
|
-
const hasUncommittedChanges = this.hasChanges(worktreePath);
|
|
2198
|
-
if (hasUncommittedChanges) {
|
|
2199
|
-
const statusOutput = this.git("status --porcelain", worktreePath).trim();
|
|
2200
|
-
this.log(`Detected uncommitted changes:
|
|
2201
|
-
${statusOutput.split(`
|
|
2202
|
-
`).slice(0, 10).join(`
|
|
2203
|
-
`)}${statusOutput.split(`
|
|
2204
|
-
`).length > 10 ? `
|
|
2205
|
-
... and ${statusOutput.split(`
|
|
2206
|
-
`).length - 10} more` : ""}`, "info");
|
|
2207
|
-
}
|
|
2208
|
-
if (!hasUncommittedChanges) {
|
|
2209
|
-
if (baseBranch && this.hasCommitsAhead(worktreePath, baseBranch)) {
|
|
2210
|
-
const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2211
|
-
this.log(`Agent already committed changes (${hash2.slice(0, 8)}); skipping additional commit`, "info");
|
|
2212
|
-
return hash2;
|
|
2213
|
-
}
|
|
2214
|
-
if (baseCommitHash && this.hasCommitsAheadOfHash(worktreePath, baseCommitHash)) {
|
|
2215
|
-
const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2216
|
-
this.log(`Agent already committed changes (${hash2.slice(0, 8)}, detected via base commit hash); skipping additional commit`, "info");
|
|
2217
|
-
return hash2;
|
|
2218
|
-
}
|
|
2219
|
-
const branch = this.getBranch(worktreePath);
|
|
2220
|
-
this.log(`No changes detected in worktree (branch: ${branch}, baseBranch: ${baseBranch ?? "none"}, baseCommitHash: ${baseCommitHash?.slice(0, 8) ?? "none"})`, "warn");
|
|
2221
|
-
return null;
|
|
2222
|
-
}
|
|
2223
|
-
this.git("add -A", worktreePath);
|
|
2224
|
-
const staged = this.git("diff --cached --name-only", worktreePath).trim();
|
|
2225
|
-
if (!staged) {
|
|
2226
|
-
this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
|
|
2227
|
-
return null;
|
|
2228
|
-
}
|
|
2229
|
-
this.log(`Staging ${staged.split(`
|
|
2230
|
-
`).length} file(s) for commit`, "info");
|
|
2231
|
-
this.gitExec(["commit", "-m", message], worktreePath);
|
|
2232
|
-
const hash = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2233
|
-
this.log(`Committed: ${hash.slice(0, 8)}`, "success");
|
|
2234
|
-
return hash;
|
|
2235
|
-
}
|
|
2236
|
-
pushBranch(worktreePath, remote = "origin") {
|
|
2237
|
-
const branch = this.getBranch(worktreePath);
|
|
2238
|
-
this.log(`Pushing branch ${branch} to ${remote}`, "info");
|
|
2239
|
-
try {
|
|
2240
|
-
this.gitExec(["push", "-u", remote, branch], worktreePath);
|
|
2241
|
-
this.log(`Pushed ${branch} to ${remote}`, "success");
|
|
2242
|
-
return branch;
|
|
2243
|
-
} catch (error) {
|
|
2244
|
-
if (!this.isNonFastForwardPushError(error)) {
|
|
2245
|
-
throw error;
|
|
2246
|
-
}
|
|
2247
|
-
this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
|
|
2248
|
-
try {
|
|
2249
|
-
this.gitExec(["fetch", remote, branch], worktreePath);
|
|
2250
|
-
} catch {}
|
|
2251
|
-
this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
|
|
2252
|
-
this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
|
|
2253
|
-
return branch;
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
getBranch(worktreePath) {
|
|
2257
|
-
return this.git("rev-parse --abbrev-ref HEAD", worktreePath).trim();
|
|
2258
|
-
}
|
|
2259
|
-
hasWorktreeForTask(taskId) {
|
|
2260
|
-
return this.listAgentWorktrees().some((wt) => wt.branch.includes(taskId) || wt.path.includes(taskId));
|
|
2261
|
-
}
|
|
2262
|
-
branchExists(branchName) {
|
|
2263
|
-
try {
|
|
2264
|
-
this.git(`rev-parse --verify "refs/heads/${branchName}"`, this.projectPath);
|
|
2265
|
-
return true;
|
|
2266
|
-
} catch {
|
|
2267
|
-
return false;
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
getCurrentBranch() {
|
|
2271
|
-
return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
|
|
2272
|
-
}
|
|
2273
|
-
isManagedWorktreePath(worktreePath) {
|
|
2274
|
-
const rootPath = import_node_path6.resolve(this.rootPath);
|
|
2275
|
-
const candidate = import_node_path6.resolve(worktreePath);
|
|
2276
|
-
const rootWithSep = rootPath.endsWith(import_node_path6.sep) ? rootPath : `${rootPath}${import_node_path6.sep}`;
|
|
2277
|
-
return candidate.startsWith(rootWithSep);
|
|
2278
|
-
}
|
|
2279
|
-
ensureDirectory(dirPath, label) {
|
|
2280
|
-
if (import_node_fs4.existsSync(dirPath)) {
|
|
2281
|
-
if (!import_node_fs4.statSync(dirPath).isDirectory()) {
|
|
2282
|
-
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
2283
|
-
}
|
|
2284
|
-
return;
|
|
2285
|
-
}
|
|
2286
|
-
import_node_fs4.mkdirSync(dirPath, { recursive: true });
|
|
2287
|
-
}
|
|
2288
|
-
isMissingDirectoryError(error) {
|
|
2289
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2290
|
-
return message.includes("cannot create directory") || message.includes("No such file or directory");
|
|
2291
|
-
}
|
|
2292
|
-
cleanupFailedWorktree(worktreePath, branch) {
|
|
2293
|
-
try {
|
|
2294
|
-
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
2295
|
-
} catch {}
|
|
2296
|
-
if (import_node_fs4.existsSync(worktreePath)) {
|
|
2297
|
-
import_node_fs4.rmSync(worktreePath, { recursive: true, force: true });
|
|
2298
|
-
}
|
|
2299
|
-
try {
|
|
2300
|
-
this.git("worktree prune", this.projectPath);
|
|
2301
|
-
} catch {}
|
|
2302
|
-
if (this.branchExists(branch)) {
|
|
2303
|
-
try {
|
|
2304
|
-
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
2305
|
-
} catch {}
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
isNonFastForwardPushError(error) {
|
|
2309
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2310
|
-
return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
|
|
2311
|
-
}
|
|
2312
|
-
git(args, cwd) {
|
|
2313
|
-
return import_node_child_process5.execSync(`git ${args}`, {
|
|
2314
|
-
cwd,
|
|
2315
|
-
encoding: "utf-8",
|
|
2316
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2317
|
-
});
|
|
2318
|
-
}
|
|
2319
|
-
gitExec(args, cwd) {
|
|
2320
|
-
return import_node_child_process5.execFileSync("git", args, {
|
|
2321
|
-
cwd,
|
|
1977
|
+
gitExec(args) {
|
|
1978
|
+
return import_node_child_process4.execFileSync("git", args, {
|
|
1979
|
+
cwd: this.projectPath,
|
|
2322
1980
|
encoding: "utf-8",
|
|
2323
1981
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2324
1982
|
});
|
|
2325
1983
|
}
|
|
2326
1984
|
}
|
|
2327
|
-
var
|
|
2328
|
-
var
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
import_node_fs4 = require("node:fs");
|
|
2332
|
-
import_node_path6 = require("node:path");
|
|
1985
|
+
var import_node_child_process4;
|
|
1986
|
+
var init_git_workflow = __esm(() => {
|
|
1987
|
+
init_git_utils();
|
|
1988
|
+
import_node_child_process4 = require("node:child_process");
|
|
2333
1989
|
});
|
|
2334
1990
|
|
|
2335
1991
|
// src/core/prompt-builder.ts
|
|
@@ -2373,9 +2029,9 @@ ${task.description || "No description provided."}
|
|
|
2373
2029
|
}
|
|
2374
2030
|
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
2375
2031
|
let hasLocalContext = false;
|
|
2376
|
-
if (
|
|
2032
|
+
if (import_node_fs4.existsSync(contextPath)) {
|
|
2377
2033
|
try {
|
|
2378
|
-
const context =
|
|
2034
|
+
const context = import_node_fs4.readFileSync(contextPath, "utf-8");
|
|
2379
2035
|
if (context.trim().length > 20) {
|
|
2380
2036
|
prompt += `## Project Context (Local)
|
|
2381
2037
|
${context}
|
|
@@ -2429,7 +2085,7 @@ ${serverContext.context}
|
|
|
2429
2085
|
|
|
2430
2086
|
`;
|
|
2431
2087
|
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
2432
|
-
if (
|
|
2088
|
+
if (import_node_fs4.existsSync(indexPath)) {
|
|
2433
2089
|
prompt += `## Codebase Overview
|
|
2434
2090
|
There is an index file in the .locus/codebase-index.json and if you need you can check it.
|
|
2435
2091
|
|
|
@@ -2506,9 +2162,9 @@ ${query}
|
|
|
2506
2162
|
}
|
|
2507
2163
|
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
2508
2164
|
let hasLocalContext = false;
|
|
2509
|
-
if (
|
|
2165
|
+
if (import_node_fs4.existsSync(contextPath)) {
|
|
2510
2166
|
try {
|
|
2511
|
-
const context =
|
|
2167
|
+
const context = import_node_fs4.readFileSync(contextPath, "utf-8");
|
|
2512
2168
|
if (context.trim().length > 20) {
|
|
2513
2169
|
prompt += `## Project Context (Local)
|
|
2514
2170
|
${context}
|
|
@@ -2542,7 +2198,7 @@ ${fallback}
|
|
|
2542
2198
|
|
|
2543
2199
|
`;
|
|
2544
2200
|
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
2545
|
-
if (
|
|
2201
|
+
if (import_node_fs4.existsSync(indexPath)) {
|
|
2546
2202
|
prompt += `## Codebase Overview
|
|
2547
2203
|
There is an index file in the .locus/codebase-index.json and if you need you can check it.
|
|
2548
2204
|
|
|
@@ -2557,9 +2213,9 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
2557
2213
|
}
|
|
2558
2214
|
getProjectConfig() {
|
|
2559
2215
|
const configPath = getLocusPath(this.projectPath, "configFile");
|
|
2560
|
-
if (
|
|
2216
|
+
if (import_node_fs4.existsSync(configPath)) {
|
|
2561
2217
|
try {
|
|
2562
|
-
return JSON.parse(
|
|
2218
|
+
return JSON.parse(import_node_fs4.readFileSync(configPath, "utf-8"));
|
|
2563
2219
|
} catch {
|
|
2564
2220
|
return null;
|
|
2565
2221
|
}
|
|
@@ -2567,10 +2223,10 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
2567
2223
|
return null;
|
|
2568
2224
|
}
|
|
2569
2225
|
getFallbackContext() {
|
|
2570
|
-
const readmePath =
|
|
2571
|
-
if (
|
|
2226
|
+
const readmePath = import_node_path6.join(this.projectPath, "README.md");
|
|
2227
|
+
if (import_node_fs4.existsSync(readmePath)) {
|
|
2572
2228
|
try {
|
|
2573
|
-
const content =
|
|
2229
|
+
const content = import_node_fs4.readFileSync(readmePath, "utf-8");
|
|
2574
2230
|
const limit = 1000;
|
|
2575
2231
|
return content.slice(0, limit) + (content.length > limit ? `
|
|
2576
2232
|
...(truncated)...` : "");
|
|
@@ -2582,12 +2238,12 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
2582
2238
|
}
|
|
2583
2239
|
getProjectStructure() {
|
|
2584
2240
|
try {
|
|
2585
|
-
const entries =
|
|
2241
|
+
const entries = import_node_fs4.readdirSync(this.projectPath);
|
|
2586
2242
|
const folders = entries.filter((e) => {
|
|
2587
2243
|
if (e.startsWith(".") || e === "node_modules")
|
|
2588
2244
|
return false;
|
|
2589
2245
|
try {
|
|
2590
|
-
return
|
|
2246
|
+
return import_node_fs4.statSync(import_node_path6.join(this.projectPath, e)).isDirectory();
|
|
2591
2247
|
} catch {
|
|
2592
2248
|
return false;
|
|
2593
2249
|
}
|
|
@@ -2628,11 +2284,11 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
2628
2284
|
}
|
|
2629
2285
|
}
|
|
2630
2286
|
}
|
|
2631
|
-
var
|
|
2287
|
+
var import_node_fs4, import_node_path6, import_shared2;
|
|
2632
2288
|
var init_prompt_builder = __esm(() => {
|
|
2633
2289
|
init_config();
|
|
2634
|
-
|
|
2635
|
-
|
|
2290
|
+
import_node_fs4 = require("node:fs");
|
|
2291
|
+
import_node_path6 = require("node:path");
|
|
2636
2292
|
import_shared2 = require("@locusai/shared");
|
|
2637
2293
|
});
|
|
2638
2294
|
|
|
@@ -2648,7 +2304,6 @@ class TaskExecutor {
|
|
|
2648
2304
|
this.deps.log(`Executing: ${task.title}`, "info");
|
|
2649
2305
|
const basePrompt = await this.promptBuilder.build(task);
|
|
2650
2306
|
try {
|
|
2651
|
-
this.deps.log("Starting Execution...", "info");
|
|
2652
2307
|
const output = await this.deps.aiRunner.run(basePrompt);
|
|
2653
2308
|
const summary = this.extractSummary(output);
|
|
2654
2309
|
return { success: true, summary };
|
|
@@ -2675,162 +2330,6 @@ var init_task_executor = __esm(() => {
|
|
|
2675
2330
|
init_prompt_builder();
|
|
2676
2331
|
});
|
|
2677
2332
|
|
|
2678
|
-
// src/agent/git-workflow.ts
|
|
2679
|
-
class GitWorkflow {
|
|
2680
|
-
config;
|
|
2681
|
-
log;
|
|
2682
|
-
ghUsername;
|
|
2683
|
-
worktreeManager;
|
|
2684
|
-
prService;
|
|
2685
|
-
constructor(config, log, ghUsername) {
|
|
2686
|
-
this.config = config;
|
|
2687
|
-
this.log = log;
|
|
2688
|
-
this.ghUsername = ghUsername;
|
|
2689
|
-
const projectPath = config.projectPath || process.cwd();
|
|
2690
|
-
this.worktreeManager = config.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }, log) : null;
|
|
2691
|
-
this.prService = config.autoPush ? new PrService(projectPath, log) : null;
|
|
2692
|
-
}
|
|
2693
|
-
createTaskWorktree(task, defaultExecutor) {
|
|
2694
|
-
if (!this.worktreeManager) {
|
|
2695
|
-
return {
|
|
2696
|
-
worktreePath: null,
|
|
2697
|
-
baseBranch: null,
|
|
2698
|
-
baseCommitHash: null,
|
|
2699
|
-
executor: defaultExecutor
|
|
2700
|
-
};
|
|
2701
|
-
}
|
|
2702
|
-
const slug = task.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
2703
|
-
const result = this.worktreeManager.create({
|
|
2704
|
-
taskId: task.id,
|
|
2705
|
-
taskSlug: slug,
|
|
2706
|
-
agentId: this.config.agentId,
|
|
2707
|
-
baseBranch: this.config.baseBranch
|
|
2708
|
-
});
|
|
2709
|
-
this.log(`Worktree created: ${result.worktreePath} (${result.branch})`, "info");
|
|
2710
|
-
const provider = this.config.provider ?? PROVIDER.CLAUDE;
|
|
2711
|
-
const taskAiRunner = createAiRunner(provider, {
|
|
2712
|
-
projectPath: result.worktreePath,
|
|
2713
|
-
model: this.config.model,
|
|
2714
|
-
log: this.log
|
|
2715
|
-
});
|
|
2716
|
-
const taskExecutor = new TaskExecutor({
|
|
2717
|
-
aiRunner: taskAiRunner,
|
|
2718
|
-
projectPath: result.worktreePath,
|
|
2719
|
-
log: this.log
|
|
2720
|
-
});
|
|
2721
|
-
return {
|
|
2722
|
-
worktreePath: result.worktreePath,
|
|
2723
|
-
baseBranch: result.baseBranch,
|
|
2724
|
-
baseCommitHash: result.baseCommitHash,
|
|
2725
|
-
executor: taskExecutor
|
|
2726
|
-
};
|
|
2727
|
-
}
|
|
2728
|
-
commitAndPush(worktreePath, task, baseBranch, baseCommitHash) {
|
|
2729
|
-
if (!this.worktreeManager) {
|
|
2730
|
-
return { branch: null, pushed: false, pushFailed: false };
|
|
2731
|
-
}
|
|
2732
|
-
try {
|
|
2733
|
-
const trailers = [
|
|
2734
|
-
`Task-ID: ${task.id}`,
|
|
2735
|
-
`Agent: ${this.config.agentId}`,
|
|
2736
|
-
"Co-authored-by: LocusAI <agent@locusai.team>"
|
|
2737
|
-
];
|
|
2738
|
-
if (this.ghUsername) {
|
|
2739
|
-
trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
|
|
2740
|
-
}
|
|
2741
|
-
const commitMessage = `feat(agent): ${task.title}
|
|
2742
|
-
|
|
2743
|
-
${trailers.join(`
|
|
2744
|
-
`)}`;
|
|
2745
|
-
const hash = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch, baseCommitHash);
|
|
2746
|
-
if (!hash) {
|
|
2747
|
-
this.log("No changes to commit for this task", "info");
|
|
2748
|
-
return {
|
|
2749
|
-
branch: null,
|
|
2750
|
-
pushed: false,
|
|
2751
|
-
pushFailed: false,
|
|
2752
|
-
noChanges: true,
|
|
2753
|
-
skipReason: "No changes were committed, so no branch was pushed."
|
|
2754
|
-
};
|
|
2755
|
-
}
|
|
2756
|
-
const localBranch = this.worktreeManager.getBranch(worktreePath);
|
|
2757
|
-
if (this.config.autoPush) {
|
|
2758
|
-
try {
|
|
2759
|
-
return {
|
|
2760
|
-
branch: this.worktreeManager.pushBranch(worktreePath),
|
|
2761
|
-
pushed: true,
|
|
2762
|
-
pushFailed: false
|
|
2763
|
-
};
|
|
2764
|
-
} catch (err) {
|
|
2765
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2766
|
-
this.log(`Git push failed: ${errorMessage}`, "error");
|
|
2767
|
-
return {
|
|
2768
|
-
branch: localBranch,
|
|
2769
|
-
pushed: false,
|
|
2770
|
-
pushFailed: true,
|
|
2771
|
-
pushError: errorMessage
|
|
2772
|
-
};
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
this.log("Auto-push disabled; skipping branch push", "info");
|
|
2776
|
-
return {
|
|
2777
|
-
branch: localBranch,
|
|
2778
|
-
pushed: false,
|
|
2779
|
-
pushFailed: false,
|
|
2780
|
-
skipReason: "Auto-push is disabled, so PR creation was skipped."
|
|
2781
|
-
};
|
|
2782
|
-
} catch (err) {
|
|
2783
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2784
|
-
this.log(`Git commit failed: ${errorMessage}`, "error");
|
|
2785
|
-
return {
|
|
2786
|
-
branch: null,
|
|
2787
|
-
pushed: false,
|
|
2788
|
-
pushFailed: true,
|
|
2789
|
-
pushError: `Git commit/push failed: ${errorMessage}`
|
|
2790
|
-
};
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
createPullRequest(task, branch, summary, baseBranch) {
|
|
2794
|
-
if (!this.prService) {
|
|
2795
|
-
const errorMessage = "PR service is not initialized. Enable auto-push to allow PR creation.";
|
|
2796
|
-
this.log(`PR creation skipped: ${errorMessage}`, "warn");
|
|
2797
|
-
return { url: null, error: errorMessage };
|
|
2798
|
-
}
|
|
2799
|
-
this.log(`Attempting PR creation from branch: ${branch}`, "info");
|
|
2800
|
-
try {
|
|
2801
|
-
const result = this.prService.createPr({
|
|
2802
|
-
task,
|
|
2803
|
-
branch,
|
|
2804
|
-
baseBranch,
|
|
2805
|
-
agentId: this.config.agentId,
|
|
2806
|
-
summary
|
|
2807
|
-
});
|
|
2808
|
-
return { url: result.url };
|
|
2809
|
-
} catch (err) {
|
|
2810
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2811
|
-
this.log(`PR creation failed: ${errorMessage}`, "error");
|
|
2812
|
-
return { url: null, error: errorMessage };
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
cleanupWorktree(worktreePath, keepBranch) {
|
|
2816
|
-
if (!this.worktreeManager || !worktreePath)
|
|
2817
|
-
return;
|
|
2818
|
-
try {
|
|
2819
|
-
this.worktreeManager.remove(worktreePath, !keepBranch);
|
|
2820
|
-
this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
|
|
2821
|
-
} catch {
|
|
2822
|
-
this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
var init_git_workflow = __esm(() => {
|
|
2827
|
-
init_factory();
|
|
2828
|
-
init_config();
|
|
2829
|
-
init_pr_service();
|
|
2830
|
-
init_worktree_manager();
|
|
2831
|
-
init_task_executor();
|
|
2832
|
-
});
|
|
2833
|
-
|
|
2834
2333
|
// src/agent/worker-cli.ts
|
|
2835
2334
|
var exports_worker_cli = {};
|
|
2836
2335
|
__export(exports_worker_cli, {
|
|
@@ -2863,16 +2362,8 @@ function parseWorkerArgs(argv) {
|
|
|
2863
2362
|
config.apiKey = args[++i];
|
|
2864
2363
|
else if (arg === "--project-path")
|
|
2865
2364
|
config.projectPath = args[++i];
|
|
2866
|
-
else if (arg === "--main-project-path")
|
|
2867
|
-
config.mainProjectPath = args[++i];
|
|
2868
2365
|
else if (arg === "--model")
|
|
2869
2366
|
config.model = args[++i];
|
|
2870
|
-
else if (arg === "--use-worktrees")
|
|
2871
|
-
config.useWorktrees = true;
|
|
2872
|
-
else if (arg === "--auto-push")
|
|
2873
|
-
config.autoPush = true;
|
|
2874
|
-
else if (arg === "--base-branch")
|
|
2875
|
-
config.baseBranch = args[++i];
|
|
2876
2367
|
else if (arg === "--provider") {
|
|
2877
2368
|
const value = args[i + 1];
|
|
2878
2369
|
if (value && !value.startsWith("--"))
|
|
@@ -2920,8 +2411,8 @@ class AgentWorker {
|
|
|
2920
2411
|
tasksCompleted = 0;
|
|
2921
2412
|
heartbeatInterval = null;
|
|
2922
2413
|
currentTaskId = null;
|
|
2923
|
-
|
|
2924
|
-
|
|
2414
|
+
completedTaskList = [];
|
|
2415
|
+
taskSummaries = [];
|
|
2925
2416
|
constructor(config) {
|
|
2926
2417
|
this.config = config;
|
|
2927
2418
|
const projectPath = config.projectPath || process.cwd();
|
|
@@ -2936,17 +2427,12 @@ class AgentWorker {
|
|
|
2936
2427
|
}
|
|
2937
2428
|
});
|
|
2938
2429
|
const log = this.log.bind(this);
|
|
2939
|
-
if (
|
|
2940
|
-
this.log("git is not installed —
|
|
2941
|
-
config.useWorktrees = false;
|
|
2430
|
+
if (!isGitAvailable()) {
|
|
2431
|
+
this.log("git is not installed — branch management will not work", "error");
|
|
2942
2432
|
}
|
|
2943
|
-
if (
|
|
2433
|
+
if (!isGhAvailable(projectPath)) {
|
|
2944
2434
|
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");
|
|
2945
2435
|
}
|
|
2946
|
-
const ghUsername = config.autoPush ? getGhUsername() : null;
|
|
2947
|
-
if (ghUsername) {
|
|
2948
|
-
this.log(`GitHub user: ${ghUsername}`, "info");
|
|
2949
|
-
}
|
|
2950
2436
|
const provider = config.provider ?? PROVIDER.CLAUDE;
|
|
2951
2437
|
this.aiRunner = createAiRunner(provider, {
|
|
2952
2438
|
projectPath,
|
|
@@ -2959,18 +2445,9 @@ class AgentWorker {
|
|
|
2959
2445
|
log
|
|
2960
2446
|
});
|
|
2961
2447
|
this.knowledgeBase = new KnowledgeBase(projectPath);
|
|
2962
|
-
this.gitWorkflow = new GitWorkflow(config, log
|
|
2448
|
+
this.gitWorkflow = new GitWorkflow(config, log);
|
|
2963
2449
|
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
2964
2450
|
this.log(`Using ${providerLabel} CLI for all phases`, "info");
|
|
2965
|
-
if (config.useWorktrees) {
|
|
2966
|
-
this.log("Per-task worktree isolation enabled", "info");
|
|
2967
|
-
if (config.baseBranch) {
|
|
2968
|
-
this.log(`Base branch for worktrees: ${config.baseBranch}`, "info");
|
|
2969
|
-
}
|
|
2970
|
-
if (config.autoPush) {
|
|
2971
|
-
this.log("Auto-push enabled: branches will be pushed to remote", "info");
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
2451
|
}
|
|
2975
2452
|
log(message, level = "info") {
|
|
2976
2453
|
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
@@ -3019,56 +2496,26 @@ class AgentWorker {
|
|
|
3019
2496
|
}
|
|
3020
2497
|
async executeTask(task) {
|
|
3021
2498
|
const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
|
|
3022
|
-
const { worktreePath, baseBranch, baseCommitHash, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
|
|
3023
|
-
this.currentWorktreePath = worktreePath;
|
|
3024
|
-
let branchPushed = false;
|
|
3025
|
-
let keepBranch = false;
|
|
3026
|
-
let preserveWorktree = false;
|
|
3027
2499
|
try {
|
|
3028
|
-
const result = await
|
|
3029
|
-
let taskBranch = null;
|
|
3030
|
-
let prUrl = null;
|
|
3031
|
-
let prError = null;
|
|
2500
|
+
const result = await this.taskExecutor.execute(fullTask);
|
|
3032
2501
|
let noChanges = false;
|
|
3033
|
-
|
|
3034
|
-
|
|
2502
|
+
let taskBranch = null;
|
|
2503
|
+
if (result.success) {
|
|
2504
|
+
const commitResult = this.gitWorkflow.commitAndPush(fullTask);
|
|
3035
2505
|
taskBranch = commitResult.branch;
|
|
3036
|
-
branchPushed = commitResult.pushed;
|
|
3037
|
-
keepBranch = taskBranch !== null;
|
|
3038
2506
|
noChanges = Boolean(commitResult.noChanges);
|
|
3039
|
-
if (commitResult.pushFailed) {
|
|
3040
|
-
preserveWorktree = true;
|
|
3041
|
-
prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
|
|
3042
|
-
this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
|
|
3043
|
-
}
|
|
3044
|
-
if (branchPushed && taskBranch) {
|
|
3045
|
-
const prResult = this.gitWorkflow.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
|
|
3046
|
-
prUrl = prResult.url;
|
|
3047
|
-
prError = prResult.error ?? null;
|
|
3048
|
-
if (!prUrl) {
|
|
3049
|
-
preserveWorktree = true;
|
|
3050
|
-
this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
|
|
3051
|
-
}
|
|
3052
|
-
} else if (commitResult.skipReason) {
|
|
3053
|
-
this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
|
|
3054
|
-
}
|
|
3055
|
-
} else if (result.success && !worktreePath) {
|
|
3056
|
-
this.log("Skipping commit/push/PR flow because no task worktree is active.", "warn");
|
|
3057
2507
|
}
|
|
3058
2508
|
return {
|
|
3059
2509
|
...result,
|
|
3060
2510
|
branch: taskBranch ?? undefined,
|
|
3061
|
-
prUrl: prUrl ?? undefined,
|
|
3062
|
-
prError: prError ?? undefined,
|
|
3063
2511
|
noChanges: noChanges || undefined
|
|
3064
2512
|
};
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
}
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2515
|
+
return {
|
|
2516
|
+
success: false,
|
|
2517
|
+
summary: `Execution error: ${msg}`
|
|
2518
|
+
};
|
|
3072
2519
|
}
|
|
3073
2520
|
}
|
|
3074
2521
|
updateProgress(task, summary) {
|
|
@@ -3081,7 +2528,6 @@ class AgentWorker {
|
|
|
3081
2528
|
role: "assistant",
|
|
3082
2529
|
content: summary
|
|
3083
2530
|
});
|
|
3084
|
-
this.log(`Updated progress.md: ${task.title}`, "info");
|
|
3085
2531
|
} catch (err) {
|
|
3086
2532
|
this.log(`Failed to update progress: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
3087
2533
|
}
|
|
@@ -3101,19 +2547,13 @@ class AgentWorker {
|
|
|
3101
2547
|
this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
3102
2548
|
});
|
|
3103
2549
|
}
|
|
3104
|
-
async delayAfterCleanup() {
|
|
3105
|
-
if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
|
|
3106
|
-
return;
|
|
3107
|
-
this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
|
|
3108
|
-
await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
|
|
3109
|
-
}
|
|
3110
2550
|
async run() {
|
|
3111
2551
|
this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
3112
2552
|
const handleShutdown = () => {
|
|
3113
2553
|
this.log("Received shutdown signal. Aborting...", "warn");
|
|
3114
2554
|
this.aiRunner.abort();
|
|
3115
2555
|
this.stopHeartbeat();
|
|
3116
|
-
this.gitWorkflow.
|
|
2556
|
+
this.gitWorkflow.checkoutBaseBranch();
|
|
3117
2557
|
process.exit(1);
|
|
3118
2558
|
};
|
|
3119
2559
|
process.on("SIGTERM", handleShutdown);
|
|
@@ -3125,10 +2565,12 @@ class AgentWorker {
|
|
|
3125
2565
|
} else {
|
|
3126
2566
|
this.log("No active sprint found.", "warn");
|
|
3127
2567
|
}
|
|
2568
|
+
const branchName = this.gitWorkflow.createBranch(this.config.sprintId);
|
|
2569
|
+
this.log(`Working on branch: ${branchName}`, "info");
|
|
3128
2570
|
while (this.tasksCompleted < this.maxTasks) {
|
|
3129
2571
|
const task = await this.getNextTask();
|
|
3130
2572
|
if (!task) {
|
|
3131
|
-
this.log("No more tasks to process.
|
|
2573
|
+
this.log("No more tasks to process.", "info");
|
|
3132
2574
|
break;
|
|
3133
2575
|
}
|
|
3134
2576
|
this.log(`Claimed: ${task.title}`, "success");
|
|
@@ -3144,7 +2586,7 @@ class AgentWorker {
|
|
|
3144
2586
|
});
|
|
3145
2587
|
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
3146
2588
|
author: this.config.agentId,
|
|
3147
|
-
text: `⚠️ Agent execution finished with no file changes, so no commit
|
|
2589
|
+
text: `⚠️ Agent execution finished with no file changes, so no commit was created.
|
|
3148
2590
|
|
|
3149
2591
|
${result.summary}`
|
|
3150
2592
|
});
|
|
@@ -3153,22 +2595,17 @@ ${result.summary}`
|
|
|
3153
2595
|
const updatePayload = {
|
|
3154
2596
|
status: import_shared3.TaskStatus.IN_REVIEW
|
|
3155
2597
|
};
|
|
3156
|
-
if (result.prUrl) {
|
|
3157
|
-
updatePayload.prUrl = result.prUrl;
|
|
3158
|
-
}
|
|
3159
2598
|
await this.client.tasks.update(task.id, this.config.workspaceId, updatePayload);
|
|
3160
2599
|
const branchInfo = result.branch ? `
|
|
3161
2600
|
|
|
3162
2601
|
Branch: \`${result.branch}\`` : "";
|
|
3163
|
-
const prInfo = result.prUrl ? `
|
|
3164
|
-
PR: ${result.prUrl}` : "";
|
|
3165
|
-
const prErrorInfo = result.prError ? `
|
|
3166
|
-
PR automation error: ${result.prError}` : "";
|
|
3167
2602
|
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
3168
2603
|
author: this.config.agentId,
|
|
3169
|
-
text: `✅ ${result.summary}${branchInfo}
|
|
2604
|
+
text: `✅ ${result.summary}${branchInfo}`
|
|
3170
2605
|
});
|
|
3171
2606
|
this.tasksCompleted++;
|
|
2607
|
+
this.completedTaskList.push({ title: task.title, id: task.id });
|
|
2608
|
+
this.taskSummaries.push(result.summary);
|
|
3172
2609
|
this.updateProgress(task, result.summary);
|
|
3173
2610
|
}
|
|
3174
2611
|
} else {
|
|
@@ -3184,8 +2621,24 @@ PR automation error: ${result.prError}` : "";
|
|
|
3184
2621
|
}
|
|
3185
2622
|
this.currentTaskId = null;
|
|
3186
2623
|
this.sendHeartbeat();
|
|
3187
|
-
await this.delayAfterCleanup();
|
|
3188
2624
|
}
|
|
2625
|
+
if (this.completedTaskList.length > 0) {
|
|
2626
|
+
this.log("All tasks done. Creating pull request...", "info");
|
|
2627
|
+
const prResult = this.gitWorkflow.createPullRequest(this.completedTaskList, this.taskSummaries);
|
|
2628
|
+
if (prResult.url) {
|
|
2629
|
+
this.log(`PR created: ${prResult.url}`, "success");
|
|
2630
|
+
for (const task of this.completedTaskList) {
|
|
2631
|
+
try {
|
|
2632
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
2633
|
+
prUrl: prResult.url
|
|
2634
|
+
});
|
|
2635
|
+
} catch {}
|
|
2636
|
+
}
|
|
2637
|
+
} else if (prResult.error) {
|
|
2638
|
+
this.log(`PR creation failed: ${prResult.error}`, "error");
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
this.gitWorkflow.checkoutBaseBranch();
|
|
3189
2642
|
this.currentTaskId = null;
|
|
3190
2643
|
this.stopHeartbeat();
|
|
3191
2644
|
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
@@ -3232,10 +2685,7 @@ __export(exports_index_node, {
|
|
|
3232
2685
|
detectRemoteProvider: () => detectRemoteProvider,
|
|
3233
2686
|
createAiRunner: () => createAiRunner,
|
|
3234
2687
|
c: () => c,
|
|
3235
|
-
WorktreeManager: () => WorktreeManager,
|
|
3236
2688
|
WorkspacesModule: () => WorkspacesModule,
|
|
3237
|
-
WORKTREE_ROOT_DIR: () => WORKTREE_ROOT_DIR,
|
|
3238
|
-
WORKTREE_BRANCH_PREFIX: () => WORKTREE_BRANCH_PREFIX,
|
|
3239
2689
|
TasksModule: () => TasksModule,
|
|
3240
2690
|
TaskExecutor: () => TaskExecutor,
|
|
3241
2691
|
SprintsModule: () => SprintsModule,
|
|
@@ -3263,7 +2713,6 @@ __export(exports_index_node, {
|
|
|
3263
2713
|
ExecEventEmitter: () => ExecEventEmitter,
|
|
3264
2714
|
DocumentFetcher: () => DocumentFetcher,
|
|
3265
2715
|
DocsModule: () => DocsModule,
|
|
3266
|
-
DEFAULT_WORKTREE_CONFIG: () => DEFAULT_WORKTREE_CONFIG,
|
|
3267
2716
|
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
3268
2717
|
ContextTracker: () => ContextTracker,
|
|
3269
2718
|
CodexRunner: () => CodexRunner,
|
|
@@ -3279,8 +2728,8 @@ module.exports = __toCommonJS(exports_index_node);
|
|
|
3279
2728
|
|
|
3280
2729
|
// src/core/indexer.ts
|
|
3281
2730
|
var import_node_crypto2 = require("node:crypto");
|
|
3282
|
-
var
|
|
3283
|
-
var
|
|
2731
|
+
var import_node_fs5 = require("node:fs");
|
|
2732
|
+
var import_node_path7 = require("node:path");
|
|
3284
2733
|
var import_globby = require("globby");
|
|
3285
2734
|
|
|
3286
2735
|
class CodebaseIndexer {
|
|
@@ -3289,7 +2738,7 @@ class CodebaseIndexer {
|
|
|
3289
2738
|
fullReindexRatioThreshold = 0.2;
|
|
3290
2739
|
constructor(projectPath) {
|
|
3291
2740
|
this.projectPath = projectPath;
|
|
3292
|
-
this.indexPath =
|
|
2741
|
+
this.indexPath = import_node_path7.join(projectPath, ".locus", "codebase-index.json");
|
|
3293
2742
|
}
|
|
3294
2743
|
async index(onProgress, treeSummarizer, force = false) {
|
|
3295
2744
|
if (!treeSummarizer) {
|
|
@@ -3345,11 +2794,11 @@ class CodebaseIndexer {
|
|
|
3345
2794
|
}
|
|
3346
2795
|
}
|
|
3347
2796
|
async getFileTree() {
|
|
3348
|
-
const gitmodulesPath =
|
|
2797
|
+
const gitmodulesPath = import_node_path7.join(this.projectPath, ".gitmodules");
|
|
3349
2798
|
const submoduleIgnores = [];
|
|
3350
|
-
if (
|
|
2799
|
+
if (import_node_fs5.existsSync(gitmodulesPath)) {
|
|
3351
2800
|
try {
|
|
3352
|
-
const content =
|
|
2801
|
+
const content = import_node_fs5.readFileSync(gitmodulesPath, "utf-8");
|
|
3353
2802
|
const lines = content.split(`
|
|
3354
2803
|
`);
|
|
3355
2804
|
for (const line of lines) {
|
|
@@ -3405,9 +2854,9 @@ class CodebaseIndexer {
|
|
|
3405
2854
|
});
|
|
3406
2855
|
}
|
|
3407
2856
|
loadIndex() {
|
|
3408
|
-
if (
|
|
2857
|
+
if (import_node_fs5.existsSync(this.indexPath)) {
|
|
3409
2858
|
try {
|
|
3410
|
-
return JSON.parse(
|
|
2859
|
+
return JSON.parse(import_node_fs5.readFileSync(this.indexPath, "utf-8"));
|
|
3411
2860
|
} catch {
|
|
3412
2861
|
return null;
|
|
3413
2862
|
}
|
|
@@ -3415,11 +2864,11 @@ class CodebaseIndexer {
|
|
|
3415
2864
|
return null;
|
|
3416
2865
|
}
|
|
3417
2866
|
saveIndex(index) {
|
|
3418
|
-
const dir =
|
|
3419
|
-
if (!
|
|
3420
|
-
|
|
2867
|
+
const dir = import_node_path7.dirname(this.indexPath);
|
|
2868
|
+
if (!import_node_fs5.existsSync(dir)) {
|
|
2869
|
+
import_node_fs5.mkdirSync(dir, { recursive: true });
|
|
3421
2870
|
}
|
|
3422
|
-
|
|
2871
|
+
import_node_fs5.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
3423
2872
|
}
|
|
3424
2873
|
cloneIndex(index) {
|
|
3425
2874
|
return JSON.parse(JSON.stringify(index));
|
|
@@ -3435,7 +2884,7 @@ class CodebaseIndexer {
|
|
|
3435
2884
|
}
|
|
3436
2885
|
hashFile(filePath) {
|
|
3437
2886
|
try {
|
|
3438
|
-
const content =
|
|
2887
|
+
const content = import_node_fs5.readFileSync(import_node_path7.join(this.projectPath, filePath), "utf-8");
|
|
3439
2888
|
return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
3440
2889
|
} catch {
|
|
3441
2890
|
return null;
|
|
@@ -3586,8 +3035,8 @@ Return ONLY valid JSON, no markdown formatting.`;
|
|
|
3586
3035
|
}
|
|
3587
3036
|
// src/agent/document-fetcher.ts
|
|
3588
3037
|
init_config();
|
|
3589
|
-
var
|
|
3590
|
-
var
|
|
3038
|
+
var import_node_fs6 = require("node:fs");
|
|
3039
|
+
var import_node_path8 = require("node:path");
|
|
3591
3040
|
|
|
3592
3041
|
class DocumentFetcher {
|
|
3593
3042
|
deps;
|
|
@@ -3596,8 +3045,8 @@ class DocumentFetcher {
|
|
|
3596
3045
|
}
|
|
3597
3046
|
async fetch() {
|
|
3598
3047
|
const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
|
|
3599
|
-
if (!
|
|
3600
|
-
|
|
3048
|
+
if (!import_node_fs6.existsSync(documentsDir)) {
|
|
3049
|
+
import_node_fs6.mkdirSync(documentsDir, { recursive: true });
|
|
3601
3050
|
}
|
|
3602
3051
|
try {
|
|
3603
3052
|
const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
|
|
@@ -3610,14 +3059,14 @@ class DocumentFetcher {
|
|
|
3610
3059
|
continue;
|
|
3611
3060
|
}
|
|
3612
3061
|
const groupName = groupMap.get(doc.groupId || "") || "General";
|
|
3613
|
-
const groupDir =
|
|
3614
|
-
if (!
|
|
3615
|
-
|
|
3062
|
+
const groupDir = import_node_path8.join(documentsDir, groupName);
|
|
3063
|
+
if (!import_node_fs6.existsSync(groupDir)) {
|
|
3064
|
+
import_node_fs6.mkdirSync(groupDir, { recursive: true });
|
|
3616
3065
|
}
|
|
3617
3066
|
const fileName = `${doc.title}.md`;
|
|
3618
|
-
const filePath =
|
|
3619
|
-
if (!
|
|
3620
|
-
|
|
3067
|
+
const filePath = import_node_path8.join(groupDir, fileName);
|
|
3068
|
+
if (!import_node_fs6.existsSync(filePath) || import_node_fs6.readFileSync(filePath, "utf-8") !== doc.content) {
|
|
3069
|
+
import_node_fs6.writeFileSync(filePath, doc.content || "");
|
|
3621
3070
|
fetchedCount++;
|
|
3622
3071
|
}
|
|
3623
3072
|
}
|
|
@@ -3635,7 +3084,7 @@ class DocumentFetcher {
|
|
|
3635
3084
|
init_git_workflow();
|
|
3636
3085
|
|
|
3637
3086
|
// src/agent/review-service.ts
|
|
3638
|
-
var
|
|
3087
|
+
var import_node_child_process5 = require("node:child_process");
|
|
3639
3088
|
|
|
3640
3089
|
class ReviewService {
|
|
3641
3090
|
deps;
|
|
@@ -3645,7 +3094,7 @@ class ReviewService {
|
|
|
3645
3094
|
async reviewStagedChanges(sprint) {
|
|
3646
3095
|
const { projectPath, log } = this.deps;
|
|
3647
3096
|
try {
|
|
3648
|
-
|
|
3097
|
+
import_node_child_process5.execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
|
|
3649
3098
|
log("Staged all changes for review.", "info");
|
|
3650
3099
|
} catch (err) {
|
|
3651
3100
|
log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
@@ -3653,7 +3102,7 @@ class ReviewService {
|
|
|
3653
3102
|
}
|
|
3654
3103
|
let diff;
|
|
3655
3104
|
try {
|
|
3656
|
-
diff =
|
|
3105
|
+
diff = import_node_child_process5.execSync("git diff --cached --stat && echo '---' && git diff --cached", {
|
|
3657
3106
|
cwd: projectPath,
|
|
3658
3107
|
maxBuffer: 10 * 1024 * 1024
|
|
3659
3108
|
}).toString();
|
|
@@ -3696,9 +3145,244 @@ Keep the review concise but thorough. Focus on substance over style.`;
|
|
|
3696
3145
|
init_factory();
|
|
3697
3146
|
init_config();
|
|
3698
3147
|
init_git_utils();
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3148
|
+
|
|
3149
|
+
// src/git/pr-service.ts
|
|
3150
|
+
init_git_utils();
|
|
3151
|
+
var import_node_child_process6 = require("node:child_process");
|
|
3152
|
+
|
|
3153
|
+
class PrService {
|
|
3154
|
+
projectPath;
|
|
3155
|
+
log;
|
|
3156
|
+
constructor(projectPath, log) {
|
|
3157
|
+
this.projectPath = projectPath;
|
|
3158
|
+
this.log = log;
|
|
3159
|
+
}
|
|
3160
|
+
createPr(options) {
|
|
3161
|
+
const {
|
|
3162
|
+
task,
|
|
3163
|
+
branch,
|
|
3164
|
+
baseBranch: requestedBaseBranch,
|
|
3165
|
+
agentId,
|
|
3166
|
+
summary
|
|
3167
|
+
} = options;
|
|
3168
|
+
const provider = detectRemoteProvider(this.projectPath);
|
|
3169
|
+
if (provider !== "github") {
|
|
3170
|
+
throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
|
|
3171
|
+
}
|
|
3172
|
+
if (!isGhAvailable(this.projectPath)) {
|
|
3173
|
+
throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
|
|
3174
|
+
}
|
|
3175
|
+
const title = `[Locus] ${task.title}`;
|
|
3176
|
+
const body = this.buildPrBody(task, agentId, summary);
|
|
3177
|
+
const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
|
|
3178
|
+
this.validateCreatePrInputs(baseBranch, branch);
|
|
3179
|
+
this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
|
|
3180
|
+
const output = import_node_child_process6.execFileSync("gh", [
|
|
3181
|
+
"pr",
|
|
3182
|
+
"create",
|
|
3183
|
+
"--title",
|
|
3184
|
+
title,
|
|
3185
|
+
"--body",
|
|
3186
|
+
body,
|
|
3187
|
+
"--base",
|
|
3188
|
+
baseBranch,
|
|
3189
|
+
"--head",
|
|
3190
|
+
branch
|
|
3191
|
+
], {
|
|
3192
|
+
cwd: this.projectPath,
|
|
3193
|
+
encoding: "utf-8",
|
|
3194
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3195
|
+
}).trim();
|
|
3196
|
+
const url = output;
|
|
3197
|
+
const prNumber = this.extractPrNumber(url);
|
|
3198
|
+
this.log(`PR created: ${url}`, "success");
|
|
3199
|
+
return { url, number: prNumber };
|
|
3200
|
+
}
|
|
3201
|
+
validateCreatePrInputs(baseBranch, headBranch) {
|
|
3202
|
+
if (!this.hasRemoteBranch(baseBranch)) {
|
|
3203
|
+
throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
|
|
3204
|
+
}
|
|
3205
|
+
if (!this.hasRemoteBranch(headBranch)) {
|
|
3206
|
+
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
3207
|
+
}
|
|
3208
|
+
const baseRef = this.resolveBranchRef(baseBranch);
|
|
3209
|
+
const headRef = this.resolveBranchRef(headBranch);
|
|
3210
|
+
if (!baseRef) {
|
|
3211
|
+
throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
|
|
3212
|
+
}
|
|
3213
|
+
if (!headRef) {
|
|
3214
|
+
throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
|
|
3215
|
+
}
|
|
3216
|
+
const commitsAhead = this.countCommitsAhead(baseRef, headRef);
|
|
3217
|
+
if (commitsAhead <= 0) {
|
|
3218
|
+
throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
countCommitsAhead(baseRef, headRef) {
|
|
3222
|
+
const output = import_node_child_process6.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
3223
|
+
cwd: this.projectPath,
|
|
3224
|
+
encoding: "utf-8",
|
|
3225
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3226
|
+
}).trim();
|
|
3227
|
+
const value = Number.parseInt(output || "0", 10);
|
|
3228
|
+
return Number.isNaN(value) ? 0 : value;
|
|
3229
|
+
}
|
|
3230
|
+
resolveBranchRef(branch) {
|
|
3231
|
+
if (this.hasLocalBranch(branch)) {
|
|
3232
|
+
return branch;
|
|
3233
|
+
}
|
|
3234
|
+
if (this.hasRemoteTrackingBranch(branch)) {
|
|
3235
|
+
return `origin/${branch}`;
|
|
3236
|
+
}
|
|
3237
|
+
return null;
|
|
3238
|
+
}
|
|
3239
|
+
hasLocalBranch(branch) {
|
|
3240
|
+
try {
|
|
3241
|
+
import_node_child_process6.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
3242
|
+
cwd: this.projectPath,
|
|
3243
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3244
|
+
});
|
|
3245
|
+
return true;
|
|
3246
|
+
} catch {
|
|
3247
|
+
return false;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
hasRemoteTrackingBranch(branch) {
|
|
3251
|
+
try {
|
|
3252
|
+
import_node_child_process6.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], {
|
|
3253
|
+
cwd: this.projectPath,
|
|
3254
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3255
|
+
});
|
|
3256
|
+
return true;
|
|
3257
|
+
} catch {
|
|
3258
|
+
return false;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
hasRemoteBranch(branch) {
|
|
3262
|
+
try {
|
|
3263
|
+
import_node_child_process6.execFileSync("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
|
|
3264
|
+
cwd: this.projectPath,
|
|
3265
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3266
|
+
});
|
|
3267
|
+
return true;
|
|
3268
|
+
} catch {
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
getPrDiff(branch) {
|
|
3273
|
+
return import_node_child_process6.execFileSync("gh", ["pr", "diff", branch], {
|
|
3274
|
+
cwd: this.projectPath,
|
|
3275
|
+
encoding: "utf-8",
|
|
3276
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3277
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
submitReview(prIdentifier, body, event) {
|
|
3281
|
+
try {
|
|
3282
|
+
import_node_child_process6.execFileSync("gh", [
|
|
3283
|
+
"pr",
|
|
3284
|
+
"review",
|
|
3285
|
+
prIdentifier,
|
|
3286
|
+
"--body",
|
|
3287
|
+
body,
|
|
3288
|
+
`--${event.toLowerCase().replace("_", "-")}`
|
|
3289
|
+
], {
|
|
3290
|
+
cwd: this.projectPath,
|
|
3291
|
+
encoding: "utf-8",
|
|
3292
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3293
|
+
});
|
|
3294
|
+
} catch (err) {
|
|
3295
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3296
|
+
if (event === "REQUEST_CHANGES" && msg.includes("own pull request")) {
|
|
3297
|
+
import_node_child_process6.execFileSync("gh", ["pr", "review", prIdentifier, "--body", body, "--comment"], {
|
|
3298
|
+
cwd: this.projectPath,
|
|
3299
|
+
encoding: "utf-8",
|
|
3300
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3301
|
+
});
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
throw err;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
listLocusPrs() {
|
|
3308
|
+
try {
|
|
3309
|
+
const output = import_node_child_process6.execFileSync("gh", [
|
|
3310
|
+
"pr",
|
|
3311
|
+
"list",
|
|
3312
|
+
"--search",
|
|
3313
|
+
"[Locus] in:title",
|
|
3314
|
+
"--state",
|
|
3315
|
+
"open",
|
|
3316
|
+
"--json",
|
|
3317
|
+
"number,title,url,headRefName"
|
|
3318
|
+
], {
|
|
3319
|
+
cwd: this.projectPath,
|
|
3320
|
+
encoding: "utf-8",
|
|
3321
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3322
|
+
}).trim();
|
|
3323
|
+
const prs = JSON.parse(output || "[]");
|
|
3324
|
+
return prs.map((pr) => ({
|
|
3325
|
+
number: pr.number,
|
|
3326
|
+
title: pr.title,
|
|
3327
|
+
url: pr.url,
|
|
3328
|
+
branch: pr.headRefName
|
|
3329
|
+
}));
|
|
3330
|
+
} catch {
|
|
3331
|
+
this.log("Failed to list Locus PRs", "warn");
|
|
3332
|
+
return [];
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
hasLocusReview(prNumber) {
|
|
3336
|
+
try {
|
|
3337
|
+
const output = import_node_child_process6.execFileSync("gh", ["pr", "view", prNumber, "--json", "reviews"], {
|
|
3338
|
+
cwd: this.projectPath,
|
|
3339
|
+
encoding: "utf-8",
|
|
3340
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3341
|
+
}).trim();
|
|
3342
|
+
const data = JSON.parse(output || "{}");
|
|
3343
|
+
return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
|
|
3344
|
+
} catch {
|
|
3345
|
+
return false;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
listUnreviewedLocusPrs() {
|
|
3349
|
+
const allPrs = this.listLocusPrs();
|
|
3350
|
+
return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
|
|
3351
|
+
}
|
|
3352
|
+
buildPrBody(task, agentId, summary) {
|
|
3353
|
+
const sections = [];
|
|
3354
|
+
sections.push(`## Task: ${task.title}`);
|
|
3355
|
+
sections.push("");
|
|
3356
|
+
if (task.description) {
|
|
3357
|
+
sections.push(task.description);
|
|
3358
|
+
sections.push("");
|
|
3359
|
+
}
|
|
3360
|
+
if (task.acceptanceChecklist?.length > 0) {
|
|
3361
|
+
sections.push("## Acceptance Criteria");
|
|
3362
|
+
for (const item of task.acceptanceChecklist) {
|
|
3363
|
+
sections.push(`- [ ] ${item.text}`);
|
|
3364
|
+
}
|
|
3365
|
+
sections.push("");
|
|
3366
|
+
}
|
|
3367
|
+
if (summary) {
|
|
3368
|
+
sections.push("## Agent Summary");
|
|
3369
|
+
sections.push(summary);
|
|
3370
|
+
sections.push("");
|
|
3371
|
+
}
|
|
3372
|
+
sections.push("---");
|
|
3373
|
+
sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}\`* | Task ID: \`${task.id}\``);
|
|
3374
|
+
return sections.join(`
|
|
3375
|
+
`);
|
|
3376
|
+
}
|
|
3377
|
+
extractPrNumber(url) {
|
|
3378
|
+
const match = url.match(/\/pull\/(\d+)/);
|
|
3379
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
// src/agent/reviewer-worker.ts
|
|
3384
|
+
init_src();
|
|
3385
|
+
init_knowledge_base();
|
|
3702
3386
|
init_colors();
|
|
3703
3387
|
function resolveProvider2(value) {
|
|
3704
3388
|
if (!value || value.startsWith("--"))
|
|
@@ -4344,8 +4028,8 @@ class ExecEventEmitter {
|
|
|
4344
4028
|
}
|
|
4345
4029
|
// src/exec/history-manager.ts
|
|
4346
4030
|
init_config();
|
|
4347
|
-
var
|
|
4348
|
-
var
|
|
4031
|
+
var import_node_fs7 = require("node:fs");
|
|
4032
|
+
var import_node_path9 = require("node:path");
|
|
4349
4033
|
var DEFAULT_MAX_SESSIONS = 30;
|
|
4350
4034
|
function generateSessionId2() {
|
|
4351
4035
|
const timestamp = Date.now().toString(36);
|
|
@@ -4357,30 +4041,30 @@ class HistoryManager {
|
|
|
4357
4041
|
historyDir;
|
|
4358
4042
|
maxSessions;
|
|
4359
4043
|
constructor(projectPath, options) {
|
|
4360
|
-
this.historyDir = options?.historyDir ??
|
|
4044
|
+
this.historyDir = options?.historyDir ?? import_node_path9.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
|
|
4361
4045
|
this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
4362
4046
|
this.ensureHistoryDir();
|
|
4363
4047
|
}
|
|
4364
4048
|
ensureHistoryDir() {
|
|
4365
|
-
if (!
|
|
4366
|
-
|
|
4049
|
+
if (!import_node_fs7.existsSync(this.historyDir)) {
|
|
4050
|
+
import_node_fs7.mkdirSync(this.historyDir, { recursive: true });
|
|
4367
4051
|
}
|
|
4368
4052
|
}
|
|
4369
4053
|
getSessionPath(sessionId) {
|
|
4370
|
-
return
|
|
4054
|
+
return import_node_path9.join(this.historyDir, `${sessionId}.json`);
|
|
4371
4055
|
}
|
|
4372
4056
|
saveSession(session) {
|
|
4373
4057
|
const filePath = this.getSessionPath(session.id);
|
|
4374
4058
|
session.updatedAt = Date.now();
|
|
4375
|
-
|
|
4059
|
+
import_node_fs7.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
4376
4060
|
}
|
|
4377
4061
|
loadSession(sessionId) {
|
|
4378
4062
|
const filePath = this.getSessionPath(sessionId);
|
|
4379
|
-
if (!
|
|
4063
|
+
if (!import_node_fs7.existsSync(filePath)) {
|
|
4380
4064
|
return null;
|
|
4381
4065
|
}
|
|
4382
4066
|
try {
|
|
4383
|
-
const content =
|
|
4067
|
+
const content = import_node_fs7.readFileSync(filePath, "utf-8");
|
|
4384
4068
|
return JSON.parse(content);
|
|
4385
4069
|
} catch {
|
|
4386
4070
|
return null;
|
|
@@ -4388,18 +4072,18 @@ class HistoryManager {
|
|
|
4388
4072
|
}
|
|
4389
4073
|
deleteSession(sessionId) {
|
|
4390
4074
|
const filePath = this.getSessionPath(sessionId);
|
|
4391
|
-
if (!
|
|
4075
|
+
if (!import_node_fs7.existsSync(filePath)) {
|
|
4392
4076
|
return false;
|
|
4393
4077
|
}
|
|
4394
4078
|
try {
|
|
4395
|
-
|
|
4079
|
+
import_node_fs7.rmSync(filePath);
|
|
4396
4080
|
return true;
|
|
4397
4081
|
} catch {
|
|
4398
4082
|
return false;
|
|
4399
4083
|
}
|
|
4400
4084
|
}
|
|
4401
4085
|
listSessions(options) {
|
|
4402
|
-
const files =
|
|
4086
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
4403
4087
|
let sessions = [];
|
|
4404
4088
|
for (const file of files) {
|
|
4405
4089
|
if (file.endsWith(".json")) {
|
|
@@ -4472,11 +4156,11 @@ class HistoryManager {
|
|
|
4472
4156
|
return deleted;
|
|
4473
4157
|
}
|
|
4474
4158
|
getSessionCount() {
|
|
4475
|
-
const files =
|
|
4159
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
4476
4160
|
return files.filter((f) => f.endsWith(".json")).length;
|
|
4477
4161
|
}
|
|
4478
4162
|
sessionExists(sessionId) {
|
|
4479
|
-
return
|
|
4163
|
+
return import_node_fs7.existsSync(this.getSessionPath(sessionId));
|
|
4480
4164
|
}
|
|
4481
4165
|
findSessionByPartialId(partialId) {
|
|
4482
4166
|
const sessions = this.listSessions();
|
|
@@ -4490,12 +4174,12 @@ class HistoryManager {
|
|
|
4490
4174
|
return this.historyDir;
|
|
4491
4175
|
}
|
|
4492
4176
|
clearAllSessions() {
|
|
4493
|
-
const files =
|
|
4177
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
4494
4178
|
let deleted = 0;
|
|
4495
4179
|
for (const file of files) {
|
|
4496
4180
|
if (file.endsWith(".json")) {
|
|
4497
4181
|
try {
|
|
4498
|
-
|
|
4182
|
+
import_node_fs7.rmSync(import_node_path9.join(this.historyDir, file));
|
|
4499
4183
|
deleted++;
|
|
4500
4184
|
} catch {}
|
|
4501
4185
|
}
|
|
@@ -4761,7 +4445,6 @@ ${currentPrompt}`);
|
|
|
4761
4445
|
}
|
|
4762
4446
|
// src/git/index.ts
|
|
4763
4447
|
init_git_utils();
|
|
4764
|
-
init_pr_service();
|
|
4765
4448
|
|
|
4766
4449
|
// src/index-node.ts
|
|
4767
4450
|
init_src();
|
|
@@ -4770,59 +4453,128 @@ init_src();
|
|
|
4770
4453
|
init_git_utils();
|
|
4771
4454
|
init_src();
|
|
4772
4455
|
init_colors();
|
|
4773
|
-
init_worktree_manager();
|
|
4774
|
-
var import_shared6 = require("@locusai/shared");
|
|
4775
|
-
var import_events5 = require("events");
|
|
4776
|
-
|
|
4777
|
-
// src/orchestrator/agent-pool.ts
|
|
4778
|
-
init_colors();
|
|
4779
4456
|
init_resolve_bin();
|
|
4780
4457
|
var import_node_child_process7 = require("node:child_process");
|
|
4781
|
-
var
|
|
4782
|
-
var
|
|
4458
|
+
var import_node_fs8 = require("node:fs");
|
|
4459
|
+
var import_node_path10 = require("node:path");
|
|
4783
4460
|
var import_node_url = require("node:url");
|
|
4784
4461
|
var import_shared4 = require("@locusai/shared");
|
|
4785
4462
|
var import_events4 = require("events");
|
|
4786
|
-
var MAX_AGENTS = 5;
|
|
4787
4463
|
|
|
4788
|
-
class
|
|
4464
|
+
class AgentOrchestrator extends import_events4.EventEmitter {
|
|
4465
|
+
client;
|
|
4789
4466
|
config;
|
|
4790
|
-
|
|
4467
|
+
isRunning = false;
|
|
4468
|
+
processedTasks = new Set;
|
|
4469
|
+
resolvedSprintId = null;
|
|
4470
|
+
agentState = null;
|
|
4791
4471
|
heartbeatInterval = null;
|
|
4792
4472
|
constructor(config) {
|
|
4793
4473
|
super();
|
|
4794
4474
|
this.config = config;
|
|
4475
|
+
this.client = new LocusClient({
|
|
4476
|
+
baseUrl: config.apiBase,
|
|
4477
|
+
token: config.apiKey
|
|
4478
|
+
});
|
|
4795
4479
|
}
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4480
|
+
async resolveSprintId() {
|
|
4481
|
+
if (this.config.sprintId) {
|
|
4482
|
+
return this.config.sprintId;
|
|
4483
|
+
}
|
|
4484
|
+
try {
|
|
4485
|
+
const sprint = await this.client.sprints.getActive(this.config.workspaceId);
|
|
4486
|
+
if (sprint?.id) {
|
|
4487
|
+
console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint.name}`));
|
|
4488
|
+
return sprint.id;
|
|
4489
|
+
}
|
|
4490
|
+
} catch {}
|
|
4491
|
+
console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
|
|
4492
|
+
return "";
|
|
4493
|
+
}
|
|
4494
|
+
async start() {
|
|
4495
|
+
if (this.isRunning) {
|
|
4496
|
+
throw new Error("Orchestrator is already running");
|
|
4497
|
+
}
|
|
4498
|
+
this.isRunning = true;
|
|
4499
|
+
this.processedTasks.clear();
|
|
4500
|
+
try {
|
|
4501
|
+
await this.orchestrationLoop();
|
|
4502
|
+
} catch (error) {
|
|
4503
|
+
this.emit("error", error);
|
|
4504
|
+
throw error;
|
|
4505
|
+
} finally {
|
|
4506
|
+
await this.cleanup();
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
async orchestrationLoop() {
|
|
4510
|
+
this.resolvedSprintId = await this.resolveSprintId();
|
|
4511
|
+
this.emit("started", {
|
|
4512
|
+
timestamp: new Date,
|
|
4513
|
+
config: this.config,
|
|
4514
|
+
sprintId: this.resolvedSprintId
|
|
4515
|
+
});
|
|
4516
|
+
this.printBanner();
|
|
4517
|
+
const tasks2 = await this.getAvailableTasks();
|
|
4518
|
+
if (tasks2.length === 0) {
|
|
4519
|
+
console.log(c.dim("ℹ No available tasks found in the backlog."));
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
if (!this.preflightChecks())
|
|
4523
|
+
return;
|
|
4524
|
+
this.startHeartbeatMonitor();
|
|
4525
|
+
await this.spawnAgent();
|
|
4526
|
+
await this.waitForAgent();
|
|
4527
|
+
console.log(`
|
|
4528
|
+
${c.success("✅ Orchestrator finished")}`);
|
|
4529
|
+
}
|
|
4530
|
+
printBanner() {
|
|
4531
|
+
console.log(`
|
|
4532
|
+
${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
4533
|
+
console.log(c.dim("----------------------------------------------"));
|
|
4534
|
+
console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
|
|
4535
|
+
if (this.resolvedSprintId) {
|
|
4536
|
+
console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
|
|
4537
|
+
}
|
|
4538
|
+
console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
|
|
4539
|
+
console.log(c.dim(`----------------------------------------------
|
|
4540
|
+
`));
|
|
4541
|
+
}
|
|
4542
|
+
preflightChecks() {
|
|
4543
|
+
if (!isGitAvailable()) {
|
|
4544
|
+
console.log(c.error("git is not installed. Install from https://git-scm.com/"));
|
|
4545
|
+
return false;
|
|
4546
|
+
}
|
|
4547
|
+
if (!isGhAvailable(this.config.projectPath)) {
|
|
4548
|
+
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/"));
|
|
4549
|
+
}
|
|
4550
|
+
return true;
|
|
4551
|
+
}
|
|
4552
|
+
async getAvailableTasks() {
|
|
4553
|
+
try {
|
|
4554
|
+
const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
|
|
4555
|
+
return tasks2.filter((task) => !this.processedTasks.has(task.id));
|
|
4556
|
+
} catch (error) {
|
|
4557
|
+
this.emit("error", error);
|
|
4558
|
+
return [];
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
async spawnAgent() {
|
|
4562
|
+
const agentId = `agent-0-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
4563
|
+
this.agentState = {
|
|
4564
|
+
id: agentId,
|
|
4565
|
+
status: "IDLE",
|
|
4566
|
+
currentTaskId: null,
|
|
4567
|
+
tasksCompleted: 0,
|
|
4568
|
+
tasksFailed: 0,
|
|
4569
|
+
lastHeartbeat: new Date
|
|
4817
4570
|
};
|
|
4818
|
-
this.agents.set(agentId, agentState);
|
|
4819
4571
|
console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
|
|
4820
4572
|
`);
|
|
4821
4573
|
const workerPath = this.resolveWorkerPath();
|
|
4822
4574
|
if (!workerPath) {
|
|
4823
4575
|
throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
|
|
4824
4576
|
}
|
|
4825
|
-
const workerArgs = this.buildWorkerArgs(agentId
|
|
4577
|
+
const workerArgs = this.buildWorkerArgs(agentId);
|
|
4826
4578
|
const agentProcess = import_node_child_process7.spawn(process.execPath, [workerPath, ...workerArgs], {
|
|
4827
4579
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4828
4580
|
detached: true,
|
|
@@ -4833,60 +4585,64 @@ class AgentPool extends import_events4.EventEmitter {
|
|
|
4833
4585
|
LOCUS_WORKSPACE: this.config.workspaceId
|
|
4834
4586
|
})
|
|
4835
4587
|
});
|
|
4836
|
-
agentState.process = agentProcess;
|
|
4837
|
-
this.attachProcessHandlers(agentId, agentState, agentProcess);
|
|
4588
|
+
this.agentState.process = agentProcess;
|
|
4589
|
+
this.attachProcessHandlers(agentId, this.agentState, agentProcess);
|
|
4838
4590
|
this.emit("agent:spawned", { agentId });
|
|
4839
4591
|
}
|
|
4840
|
-
async
|
|
4841
|
-
while (this.
|
|
4592
|
+
async waitForAgent() {
|
|
4593
|
+
while (this.agentState && this.isRunning) {
|
|
4842
4594
|
await sleep(2000);
|
|
4843
4595
|
}
|
|
4844
4596
|
}
|
|
4845
4597
|
startHeartbeatMonitor() {
|
|
4846
4598
|
this.heartbeatInterval = setInterval(() => {
|
|
4599
|
+
if (!this.agentState)
|
|
4600
|
+
return;
|
|
4847
4601
|
const now = Date.now();
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
killProcessTree(agent.process);
|
|
4853
|
-
}
|
|
4854
|
-
this.emit("agent:stale", { agentId });
|
|
4602
|
+
if (this.agentState.status === "WORKING" && now - this.agentState.lastHeartbeat.getTime() > import_shared4.STALE_AGENT_TIMEOUT_MS) {
|
|
4603
|
+
console.log(c.error(`Agent ${this.agentState.id} is stale (no heartbeat for 10 minutes). Killing.`));
|
|
4604
|
+
if (this.agentState.process && !this.agentState.process.killed) {
|
|
4605
|
+
killProcessTree(this.agentState.process);
|
|
4855
4606
|
}
|
|
4607
|
+
this.emit("agent:stale", { agentId: this.agentState.id });
|
|
4856
4608
|
}
|
|
4857
4609
|
}, 60000);
|
|
4858
4610
|
}
|
|
4611
|
+
async stop() {
|
|
4612
|
+
this.isRunning = false;
|
|
4613
|
+
await this.cleanup();
|
|
4614
|
+
this.emit("stopped", { timestamp: new Date });
|
|
4615
|
+
}
|
|
4859
4616
|
stopAgent(agentId) {
|
|
4860
|
-
|
|
4861
|
-
if (!agent)
|
|
4617
|
+
if (!this.agentState || this.agentState.id !== agentId)
|
|
4862
4618
|
return false;
|
|
4863
|
-
if (
|
|
4864
|
-
killProcessTree(
|
|
4619
|
+
if (this.agentState.process && !this.agentState.process.killed) {
|
|
4620
|
+
killProcessTree(this.agentState.process);
|
|
4865
4621
|
}
|
|
4866
4622
|
return true;
|
|
4867
4623
|
}
|
|
4868
|
-
|
|
4624
|
+
async cleanup() {
|
|
4869
4625
|
if (this.heartbeatInterval) {
|
|
4870
4626
|
clearInterval(this.heartbeatInterval);
|
|
4871
4627
|
this.heartbeatInterval = null;
|
|
4872
4628
|
}
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
killProcessTree(agent.process);
|
|
4877
|
-
}
|
|
4629
|
+
if (this.agentState?.process && !this.agentState.process.killed) {
|
|
4630
|
+
console.log(`Killing agent: ${this.agentState.id}`);
|
|
4631
|
+
killProcessTree(this.agentState.process);
|
|
4878
4632
|
}
|
|
4879
|
-
this.agents.clear();
|
|
4880
4633
|
}
|
|
4881
4634
|
getStats() {
|
|
4882
4635
|
return {
|
|
4883
|
-
activeAgents: this.
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4636
|
+
activeAgents: this.agentState ? 1 : 0,
|
|
4637
|
+
totalTasksCompleted: this.agentState?.tasksCompleted ?? 0,
|
|
4638
|
+
totalTasksFailed: this.agentState?.tasksFailed ?? 0,
|
|
4639
|
+
processedTasks: this.processedTasks.size
|
|
4887
4640
|
};
|
|
4888
4641
|
}
|
|
4889
|
-
|
|
4642
|
+
getAgentStates() {
|
|
4643
|
+
return this.agentState ? [this.agentState] : [];
|
|
4644
|
+
}
|
|
4645
|
+
buildWorkerArgs(agentId) {
|
|
4890
4646
|
const args = [
|
|
4891
4647
|
"--agent-id",
|
|
4892
4648
|
agentId,
|
|
@@ -4905,17 +4661,8 @@ class AgentPool extends import_events4.EventEmitter {
|
|
|
4905
4661
|
if (this.config.provider) {
|
|
4906
4662
|
args.push("--provider", this.config.provider);
|
|
4907
4663
|
}
|
|
4908
|
-
if (resolvedSprintId) {
|
|
4909
|
-
args.push("--sprint-id", resolvedSprintId);
|
|
4910
|
-
}
|
|
4911
|
-
if (this.config.useWorktrees ?? true) {
|
|
4912
|
-
args.push("--use-worktrees");
|
|
4913
|
-
}
|
|
4914
|
-
if (this.config.autoPush) {
|
|
4915
|
-
args.push("--auto-push");
|
|
4916
|
-
}
|
|
4917
|
-
if (baseBranch) {
|
|
4918
|
-
args.push("--base-branch", baseBranch);
|
|
4664
|
+
if (this.resolvedSprintId) {
|
|
4665
|
+
args.push("--sprint-id", this.resolvedSprintId);
|
|
4919
4666
|
}
|
|
4920
4667
|
return args;
|
|
4921
4668
|
}
|
|
@@ -4930,37 +4677,58 @@ class AgentPool extends import_events4.EventEmitter {
|
|
|
4930
4677
|
}
|
|
4931
4678
|
});
|
|
4932
4679
|
proc.stdout?.on("data", (data) => {
|
|
4933
|
-
|
|
4680
|
+
const text = data.toString();
|
|
4681
|
+
for (const line of text.split(`
|
|
4682
|
+
`)) {
|
|
4683
|
+
const trimmed = line.trim();
|
|
4684
|
+
if (!trimmed)
|
|
4685
|
+
continue;
|
|
4686
|
+
if (/[✓✗⚠]/.test(trimmed) || /\b(Claimed|Completed|Failed|error|PR created)\b/i.test(trimmed)) {
|
|
4687
|
+
process.stdout.write(`${line}
|
|
4688
|
+
`);
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4934
4691
|
});
|
|
4935
4692
|
proc.stderr?.on("data", (data) => {
|
|
4936
|
-
|
|
4693
|
+
const text = data.toString();
|
|
4694
|
+
for (const line of text.split(`
|
|
4695
|
+
`)) {
|
|
4696
|
+
const trimmed = line.trim();
|
|
4697
|
+
if (!trimmed)
|
|
4698
|
+
continue;
|
|
4699
|
+
if (/^\[\d{2}:\d{2}:\d{2}\]/.test(trimmed))
|
|
4700
|
+
continue;
|
|
4701
|
+
if (trimmed.length < 20)
|
|
4702
|
+
continue;
|
|
4703
|
+
process.stderr.write(`${line}
|
|
4704
|
+
`);
|
|
4705
|
+
}
|
|
4937
4706
|
});
|
|
4938
4707
|
proc.on("exit", (code) => {
|
|
4939
4708
|
console.log(`
|
|
4940
4709
|
${agentId} finished (exit code: ${code})`);
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
agent.status = code === 0 ? "COMPLETED" : "FAILED";
|
|
4710
|
+
if (this.agentState) {
|
|
4711
|
+
this.agentState.status = code === 0 ? "COMPLETED" : "FAILED";
|
|
4944
4712
|
this.emit("agent:completed", {
|
|
4945
4713
|
agentId,
|
|
4946
|
-
status:
|
|
4947
|
-
tasksCompleted:
|
|
4948
|
-
tasksFailed:
|
|
4714
|
+
status: this.agentState.status,
|
|
4715
|
+
tasksCompleted: this.agentState.tasksCompleted,
|
|
4716
|
+
tasksFailed: this.agentState.tasksFailed
|
|
4949
4717
|
});
|
|
4950
|
-
this.
|
|
4718
|
+
this.agentState = null;
|
|
4951
4719
|
}
|
|
4952
4720
|
});
|
|
4953
4721
|
}
|
|
4954
4722
|
resolveWorkerPath() {
|
|
4955
|
-
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator/
|
|
4956
|
-
const currentModuleDir =
|
|
4723
|
+
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator/index.ts");
|
|
4724
|
+
const currentModuleDir = import_node_path10.dirname(currentModulePath);
|
|
4957
4725
|
const potentialPaths = [
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4726
|
+
import_node_path10.join(currentModuleDir, "..", "agent", "worker.js"),
|
|
4727
|
+
import_node_path10.join(currentModuleDir, "agent", "worker.js"),
|
|
4728
|
+
import_node_path10.join(currentModuleDir, "worker.js"),
|
|
4729
|
+
import_node_path10.join(currentModuleDir, "..", "agent", "worker.ts")
|
|
4962
4730
|
];
|
|
4963
|
-
return potentialPaths.find((p) =>
|
|
4731
|
+
return potentialPaths.find((p) => import_node_fs8.existsSync(p));
|
|
4964
4732
|
}
|
|
4965
4733
|
}
|
|
4966
4734
|
function killProcessTree(proc) {
|
|
@@ -4975,460 +4743,16 @@ function killProcessTree(proc) {
|
|
|
4975
4743
|
}
|
|
4976
4744
|
}
|
|
4977
4745
|
function sleep(ms) {
|
|
4978
|
-
return new Promise((
|
|
4979
|
-
}
|
|
4980
|
-
|
|
4981
|
-
// src/orchestrator/execution.ts
|
|
4982
|
-
init_git_utils();
|
|
4983
|
-
init_colors();
|
|
4984
|
-
var import_shared5 = require("@locusai/shared");
|
|
4985
|
-
var SPAWN_DELAY_MS = 5000;
|
|
4986
|
-
|
|
4987
|
-
class ExecutionStrategy {
|
|
4988
|
-
config;
|
|
4989
|
-
pool;
|
|
4990
|
-
tierMerge;
|
|
4991
|
-
resolvedSprintId;
|
|
4992
|
-
isRunning;
|
|
4993
|
-
constructor(config, pool, tierMerge, resolvedSprintId, isRunning) {
|
|
4994
|
-
this.config = config;
|
|
4995
|
-
this.pool = pool;
|
|
4996
|
-
this.tierMerge = tierMerge;
|
|
4997
|
-
this.resolvedSprintId = resolvedSprintId;
|
|
4998
|
-
this.isRunning = isRunning;
|
|
4999
|
-
}
|
|
5000
|
-
async execute(tasks2) {
|
|
5001
|
-
const hasTiers = tasks2.some((t) => t.tier !== null && t.tier !== undefined);
|
|
5002
|
-
const useWorktrees = this.config.useWorktrees ?? true;
|
|
5003
|
-
if (hasTiers && useWorktrees) {
|
|
5004
|
-
await this.tierBasedExecution(tasks2);
|
|
5005
|
-
} else {
|
|
5006
|
-
await this.legacyExecution(tasks2);
|
|
5007
|
-
}
|
|
5008
|
-
}
|
|
5009
|
-
async tierBasedExecution(allTasks) {
|
|
5010
|
-
const tierMap = groupByTier(allTasks);
|
|
5011
|
-
const tiers = Array.from(tierMap.keys()).sort((a, b) => a - b);
|
|
5012
|
-
const defaultBranch = getDefaultBranch(this.config.projectPath);
|
|
5013
|
-
console.log(c.primary(`\uD83D\uDCCA Tier-based execution: ${tiers.length} tier(s) detected [${tiers.join(", ")}]`));
|
|
5014
|
-
let currentBaseBranch = defaultBranch;
|
|
5015
|
-
for (const tier of tiers) {
|
|
5016
|
-
if (!this.isRunning())
|
|
5017
|
-
break;
|
|
5018
|
-
const tierTasks = tierMap.get(tier) ?? [];
|
|
5019
|
-
const dispatchable = tierTasks.filter(isDispatchable);
|
|
5020
|
-
if (dispatchable.length === 0) {
|
|
5021
|
-
console.log(c.dim(`ℹ Tier ${tier}: all ${tierTasks.length} task(s) already completed, skipping`));
|
|
5022
|
-
const tierBranch = this.tierMerge.tierBranchName(tier);
|
|
5023
|
-
if (this.tierMerge.remoteBranchExists(tierBranch)) {
|
|
5024
|
-
currentBaseBranch = tierBranch;
|
|
5025
|
-
}
|
|
5026
|
-
continue;
|
|
5027
|
-
}
|
|
5028
|
-
console.log(`
|
|
5029
|
-
${c.primary(`\uD83C\uDFD7️ Tier ${tier}:`)} ${dispatchable.length} task(s) | base: ${c.bold(currentBaseBranch)}`);
|
|
5030
|
-
await this.spawnAgentsForTasks(dispatchable.length, currentBaseBranch);
|
|
5031
|
-
await this.pool.waitForAll(this.isRunning);
|
|
5032
|
-
console.log(c.success(`✓ Tier ${tier} complete`));
|
|
5033
|
-
if (this.config.autoPush && tiers.indexOf(tier) < tiers.length - 1) {
|
|
5034
|
-
const mergeBranch = this.tierMerge.createMergeBranch(tier, currentBaseBranch);
|
|
5035
|
-
if (mergeBranch) {
|
|
5036
|
-
currentBaseBranch = mergeBranch;
|
|
5037
|
-
console.log(c.success(`\uD83D\uDCCC Created merge branch: ${mergeBranch} (base for tier ${tier + 1})`));
|
|
5038
|
-
}
|
|
5039
|
-
}
|
|
5040
|
-
}
|
|
5041
|
-
}
|
|
5042
|
-
async legacyExecution(tasks2) {
|
|
5043
|
-
const defaultBranch = getDefaultBranch(this.config.projectPath);
|
|
5044
|
-
await this.spawnAgentsForTasks(tasks2.length, defaultBranch);
|
|
5045
|
-
await this.pool.waitForAll(this.isRunning);
|
|
5046
|
-
}
|
|
5047
|
-
async spawnAgentsForTasks(taskCount, baseBranch) {
|
|
5048
|
-
const agentsToSpawn = Math.min(this.pool.effectiveAgentCount, taskCount);
|
|
5049
|
-
const spawnPromises = [];
|
|
5050
|
-
for (let i = 0;i < agentsToSpawn; i++) {
|
|
5051
|
-
if (i > 0) {
|
|
5052
|
-
await sleep2(SPAWN_DELAY_MS);
|
|
5053
|
-
}
|
|
5054
|
-
spawnPromises.push(this.pool.spawn(i, this.resolvedSprintId, baseBranch));
|
|
5055
|
-
}
|
|
5056
|
-
await Promise.all(spawnPromises);
|
|
5057
|
-
}
|
|
5058
|
-
}
|
|
5059
|
-
function groupByTier(tasks2) {
|
|
5060
|
-
const tierMap = new Map;
|
|
5061
|
-
for (const task of tasks2) {
|
|
5062
|
-
const tier = task.tier ?? 0;
|
|
5063
|
-
const existing = tierMap.get(tier);
|
|
5064
|
-
if (existing) {
|
|
5065
|
-
existing.push(task);
|
|
5066
|
-
} else {
|
|
5067
|
-
tierMap.set(tier, [task]);
|
|
5068
|
-
}
|
|
5069
|
-
}
|
|
5070
|
-
return tierMap;
|
|
5071
|
-
}
|
|
5072
|
-
function isDispatchable(task) {
|
|
5073
|
-
return task.status === import_shared5.TaskStatus.BACKLOG || task.status === import_shared5.TaskStatus.IN_PROGRESS && !task.assignedTo;
|
|
5074
|
-
}
|
|
5075
|
-
function sleep2(ms) {
|
|
5076
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5077
|
-
}
|
|
5078
|
-
|
|
5079
|
-
// src/orchestrator/tier-merge.ts
|
|
5080
|
-
init_colors();
|
|
5081
|
-
var import_node_child_process8 = require("node:child_process");
|
|
5082
|
-
var TIER_BRANCH_PREFIX = "locus/tier";
|
|
5083
|
-
|
|
5084
|
-
class TierMergeService {
|
|
5085
|
-
projectPath;
|
|
5086
|
-
sprintId;
|
|
5087
|
-
tierTaskIds = new Map;
|
|
5088
|
-
constructor(projectPath, sprintId) {
|
|
5089
|
-
this.projectPath = projectPath;
|
|
5090
|
-
this.sprintId = sprintId;
|
|
5091
|
-
}
|
|
5092
|
-
registerTierTasks(tasks2) {
|
|
5093
|
-
for (const task of tasks2) {
|
|
5094
|
-
const tier = task.tier ?? 0;
|
|
5095
|
-
const existing = this.tierTaskIds.get(tier);
|
|
5096
|
-
if (existing) {
|
|
5097
|
-
existing.push(task.id);
|
|
5098
|
-
} else {
|
|
5099
|
-
this.tierTaskIds.set(tier, [task.id]);
|
|
5100
|
-
}
|
|
5101
|
-
}
|
|
5102
|
-
}
|
|
5103
|
-
tierBranchName(tier) {
|
|
5104
|
-
const suffix = this.sprintId ? `-${this.sprintId.slice(0, 8)}` : "";
|
|
5105
|
-
return `${TIER_BRANCH_PREFIX}-${tier}${suffix}`;
|
|
5106
|
-
}
|
|
5107
|
-
remoteBranchExists(branch) {
|
|
5108
|
-
try {
|
|
5109
|
-
import_node_child_process8.execFileSync("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
|
|
5110
|
-
cwd: this.projectPath,
|
|
5111
|
-
encoding: "utf-8",
|
|
5112
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5113
|
-
});
|
|
5114
|
-
return true;
|
|
5115
|
-
} catch {
|
|
5116
|
-
return false;
|
|
5117
|
-
}
|
|
5118
|
-
}
|
|
5119
|
-
createMergeBranch(tier, baseBranch) {
|
|
5120
|
-
const mergeBranchName = this.tierBranchName(tier);
|
|
5121
|
-
try {
|
|
5122
|
-
this.gitExec(["fetch", "origin"]);
|
|
5123
|
-
const tierTaskBranches = this.findTierTaskBranches(tier);
|
|
5124
|
-
if (tierTaskBranches.length === 0) {
|
|
5125
|
-
console.log(c.dim(` Tier ${tier}: no pushed task branches found, skipping merge branch creation`));
|
|
5126
|
-
return null;
|
|
5127
|
-
}
|
|
5128
|
-
console.log(c.dim(` Merging ${tierTaskBranches.length} branch(es) into ${mergeBranchName}: ${tierTaskBranches.join(", ")}`));
|
|
5129
|
-
try {
|
|
5130
|
-
this.gitExec(["branch", "-D", mergeBranchName]);
|
|
5131
|
-
} catch {}
|
|
5132
|
-
this.gitExec(["checkout", "-b", mergeBranchName, `origin/${baseBranch}`]);
|
|
5133
|
-
for (const branch of tierTaskBranches) {
|
|
5134
|
-
try {
|
|
5135
|
-
this.gitExec(["merge", `origin/${branch}`, "--no-edit"]);
|
|
5136
|
-
} catch (err) {
|
|
5137
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5138
|
-
console.log(c.error(` Merge conflict merging ${branch} into ${mergeBranchName}: ${msg}`));
|
|
5139
|
-
try {
|
|
5140
|
-
this.gitExec(["merge", "--abort"]);
|
|
5141
|
-
} catch {}
|
|
5142
|
-
}
|
|
5143
|
-
}
|
|
5144
|
-
this.gitExec(["push", "-u", "origin", mergeBranchName, "--force"]);
|
|
5145
|
-
this.gitExec(["checkout", baseBranch]);
|
|
5146
|
-
return mergeBranchName;
|
|
5147
|
-
} catch (err) {
|
|
5148
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5149
|
-
console.log(c.error(`Failed to create tier merge branch: ${msg}`));
|
|
5150
|
-
try {
|
|
5151
|
-
this.gitExec(["checkout", baseBranch]);
|
|
5152
|
-
} catch {}
|
|
5153
|
-
return null;
|
|
5154
|
-
}
|
|
5155
|
-
}
|
|
5156
|
-
findTierTaskBranches(tier) {
|
|
5157
|
-
const tierTaskIds = this.tierTaskIds.get(tier);
|
|
5158
|
-
if (!tierTaskIds || tierTaskIds.length === 0)
|
|
5159
|
-
return [];
|
|
5160
|
-
try {
|
|
5161
|
-
const output = import_node_child_process8.execSync('git branch -r --list "origin/agent/*" --format="%(refname:short)"', { cwd: this.projectPath, encoding: "utf-8" }).trim();
|
|
5162
|
-
if (!output)
|
|
5163
|
-
return [];
|
|
5164
|
-
const remoteBranches = output.split(`
|
|
5165
|
-
`).map((b) => b.replace("origin/", ""));
|
|
5166
|
-
return remoteBranches.filter((branch) => {
|
|
5167
|
-
const branchSuffix = branch.replace(/^agent\//, "");
|
|
5168
|
-
if (!branchSuffix)
|
|
5169
|
-
return false;
|
|
5170
|
-
return tierTaskIds.some((id) => branchSuffix.startsWith(`${id}-`) || branchSuffix === id || branchSuffix.startsWith(id));
|
|
5171
|
-
});
|
|
5172
|
-
} catch (err) {
|
|
5173
|
-
console.log(c.dim(` Could not list remote branches for tier ${tier}: ${err instanceof Error ? err.message : String(err)}`));
|
|
5174
|
-
return [];
|
|
5175
|
-
}
|
|
5176
|
-
}
|
|
5177
|
-
gitExec(args) {
|
|
5178
|
-
return import_node_child_process8.execFileSync("git", args, {
|
|
5179
|
-
cwd: this.projectPath,
|
|
5180
|
-
encoding: "utf-8",
|
|
5181
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5182
|
-
});
|
|
5183
|
-
}
|
|
5184
|
-
}
|
|
5185
|
-
|
|
5186
|
-
// src/orchestrator/index.ts
|
|
5187
|
-
class AgentOrchestrator extends import_events5.EventEmitter {
|
|
5188
|
-
client;
|
|
5189
|
-
config;
|
|
5190
|
-
pool;
|
|
5191
|
-
isRunning = false;
|
|
5192
|
-
processedTasks = new Set;
|
|
5193
|
-
resolvedSprintId = null;
|
|
5194
|
-
worktreeManager = null;
|
|
5195
|
-
constructor(config) {
|
|
5196
|
-
super();
|
|
5197
|
-
this.config = config;
|
|
5198
|
-
this.client = new LocusClient({
|
|
5199
|
-
baseUrl: config.apiBase,
|
|
5200
|
-
token: config.apiKey
|
|
5201
|
-
});
|
|
5202
|
-
this.pool = new AgentPool(config);
|
|
5203
|
-
this.pool.on("agent:spawned", (data) => this.emit("agent:spawned", data));
|
|
5204
|
-
this.pool.on("agent:completed", (data) => this.emit("agent:completed", data));
|
|
5205
|
-
this.pool.on("agent:stale", (data) => this.emit("agent:stale", data));
|
|
5206
|
-
}
|
|
5207
|
-
get useWorktrees() {
|
|
5208
|
-
return this.config.useWorktrees ?? true;
|
|
5209
|
-
}
|
|
5210
|
-
get worktreeCleanupPolicy() {
|
|
5211
|
-
return this.config.worktreeCleanupPolicy ?? "retain-on-failure";
|
|
5212
|
-
}
|
|
5213
|
-
async resolveSprintId() {
|
|
5214
|
-
if (this.config.sprintId) {
|
|
5215
|
-
return this.config.sprintId;
|
|
5216
|
-
}
|
|
5217
|
-
try {
|
|
5218
|
-
const sprint = await this.client.sprints.getActive(this.config.workspaceId);
|
|
5219
|
-
if (sprint?.id) {
|
|
5220
|
-
console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint.name}`));
|
|
5221
|
-
return sprint.id;
|
|
5222
|
-
}
|
|
5223
|
-
} catch {}
|
|
5224
|
-
console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
|
|
5225
|
-
return "";
|
|
5226
|
-
}
|
|
5227
|
-
async start() {
|
|
5228
|
-
if (this.isRunning) {
|
|
5229
|
-
throw new Error("Orchestrator is already running");
|
|
5230
|
-
}
|
|
5231
|
-
this.isRunning = true;
|
|
5232
|
-
this.processedTasks.clear();
|
|
5233
|
-
try {
|
|
5234
|
-
await this.orchestrationLoop();
|
|
5235
|
-
} catch (error) {
|
|
5236
|
-
this.emit("error", error);
|
|
5237
|
-
throw error;
|
|
5238
|
-
} finally {
|
|
5239
|
-
await this.cleanup();
|
|
5240
|
-
}
|
|
5241
|
-
}
|
|
5242
|
-
async orchestrationLoop() {
|
|
5243
|
-
this.resolvedSprintId = await this.resolveSprintId();
|
|
5244
|
-
this.emit("started", {
|
|
5245
|
-
timestamp: new Date,
|
|
5246
|
-
config: this.config,
|
|
5247
|
-
sprintId: this.resolvedSprintId
|
|
5248
|
-
});
|
|
5249
|
-
this.printBanner();
|
|
5250
|
-
const tasks2 = await this.getAvailableTasks();
|
|
5251
|
-
if (tasks2.length === 0) {
|
|
5252
|
-
console.log(c.dim("ℹ No available tasks found in the backlog."));
|
|
5253
|
-
return;
|
|
5254
|
-
}
|
|
5255
|
-
if (!this.preflightChecks(tasks2))
|
|
5256
|
-
return;
|
|
5257
|
-
if (this.useWorktrees) {
|
|
5258
|
-
this.worktreeManager = new WorktreeManager(this.config.projectPath, {
|
|
5259
|
-
cleanupPolicy: this.worktreeCleanupPolicy
|
|
5260
|
-
});
|
|
5261
|
-
}
|
|
5262
|
-
this.pool.startHeartbeatMonitor();
|
|
5263
|
-
const tierMerge = new TierMergeService(this.config.projectPath, this.resolvedSprintId);
|
|
5264
|
-
tierMerge.registerTierTasks(tasks2);
|
|
5265
|
-
const execution = new ExecutionStrategy(this.config, this.pool, tierMerge, this.resolvedSprintId, () => this.isRunning);
|
|
5266
|
-
await execution.execute(tasks2);
|
|
5267
|
-
console.log(`
|
|
5268
|
-
${c.success("✅ Orchestrator finished")}`);
|
|
5269
|
-
}
|
|
5270
|
-
printBanner() {
|
|
5271
|
-
console.log(`
|
|
5272
|
-
${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
5273
|
-
console.log(c.dim("----------------------------------------------"));
|
|
5274
|
-
console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
|
|
5275
|
-
if (this.resolvedSprintId) {
|
|
5276
|
-
console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
|
|
5277
|
-
}
|
|
5278
|
-
console.log(`${c.bold("Agents:")} ${this.pool.effectiveAgentCount}`);
|
|
5279
|
-
console.log(`${c.bold("Worktrees:")} ${this.useWorktrees ? "enabled" : "disabled"}`);
|
|
5280
|
-
if (this.useWorktrees) {
|
|
5281
|
-
console.log(`${c.bold("Cleanup policy:")} ${this.worktreeCleanupPolicy}`);
|
|
5282
|
-
console.log(`${c.bold("Auto-push:")} ${this.config.autoPush ? "enabled" : "disabled"}`);
|
|
5283
|
-
}
|
|
5284
|
-
console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
|
|
5285
|
-
console.log(c.dim(`----------------------------------------------
|
|
5286
|
-
`));
|
|
5287
|
-
}
|
|
5288
|
-
preflightChecks(_tasks) {
|
|
5289
|
-
if (this.useWorktrees && !isGitAvailable()) {
|
|
5290
|
-
console.log(c.error("git is not installed. Worktree isolation requires git. Install from https://git-scm.com/"));
|
|
5291
|
-
return false;
|
|
5292
|
-
}
|
|
5293
|
-
if (this.config.autoPush && !isGhAvailable(this.config.projectPath)) {
|
|
5294
|
-
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/"));
|
|
5295
|
-
}
|
|
5296
|
-
return true;
|
|
5297
|
-
}
|
|
5298
|
-
async getAvailableTasks() {
|
|
5299
|
-
try {
|
|
5300
|
-
const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
|
|
5301
|
-
return tasks2.filter((task) => !this.processedTasks.has(task.id));
|
|
5302
|
-
} catch (error) {
|
|
5303
|
-
this.emit("error", error);
|
|
5304
|
-
return [];
|
|
5305
|
-
}
|
|
5306
|
-
}
|
|
5307
|
-
async assignTaskToAgent(agentId) {
|
|
5308
|
-
const agent = this.pool.get(agentId);
|
|
5309
|
-
if (!agent)
|
|
5310
|
-
return null;
|
|
5311
|
-
try {
|
|
5312
|
-
const tasks2 = await this.getAvailableTasks();
|
|
5313
|
-
const priorityOrder = [
|
|
5314
|
-
import_shared6.TaskPriority.CRITICAL,
|
|
5315
|
-
import_shared6.TaskPriority.HIGH,
|
|
5316
|
-
import_shared6.TaskPriority.MEDIUM,
|
|
5317
|
-
import_shared6.TaskPriority.LOW
|
|
5318
|
-
];
|
|
5319
|
-
let task = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
|
|
5320
|
-
if (!task && tasks2.length > 0) {
|
|
5321
|
-
task = tasks2[0];
|
|
5322
|
-
}
|
|
5323
|
-
if (!task)
|
|
5324
|
-
return null;
|
|
5325
|
-
agent.currentTaskId = task.id;
|
|
5326
|
-
agent.status = "WORKING";
|
|
5327
|
-
this.emit("task:assigned", {
|
|
5328
|
-
agentId,
|
|
5329
|
-
taskId: task.id,
|
|
5330
|
-
title: task.title
|
|
5331
|
-
});
|
|
5332
|
-
return task;
|
|
5333
|
-
} catch (error) {
|
|
5334
|
-
this.emit("error", error);
|
|
5335
|
-
return null;
|
|
5336
|
-
}
|
|
5337
|
-
}
|
|
5338
|
-
async completeTask(taskId, agentId, summary) {
|
|
5339
|
-
try {
|
|
5340
|
-
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
5341
|
-
status: import_shared6.TaskStatus.IN_REVIEW
|
|
5342
|
-
});
|
|
5343
|
-
if (summary) {
|
|
5344
|
-
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
5345
|
-
author: agentId,
|
|
5346
|
-
text: `✅ Task completed
|
|
5347
|
-
|
|
5348
|
-
${summary}`
|
|
5349
|
-
});
|
|
5350
|
-
}
|
|
5351
|
-
this.processedTasks.add(taskId);
|
|
5352
|
-
const agent = this.pool.get(agentId);
|
|
5353
|
-
if (agent) {
|
|
5354
|
-
agent.tasksCompleted += 1;
|
|
5355
|
-
agent.currentTaskId = null;
|
|
5356
|
-
agent.status = "IDLE";
|
|
5357
|
-
}
|
|
5358
|
-
this.emit("task:completed", { agentId, taskId });
|
|
5359
|
-
} catch (error) {
|
|
5360
|
-
this.emit("error", error);
|
|
5361
|
-
}
|
|
5362
|
-
}
|
|
5363
|
-
async failTask(taskId, agentId, error) {
|
|
5364
|
-
try {
|
|
5365
|
-
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
5366
|
-
status: import_shared6.TaskStatus.BACKLOG,
|
|
5367
|
-
assignedTo: null
|
|
5368
|
-
});
|
|
5369
|
-
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
5370
|
-
author: agentId,
|
|
5371
|
-
text: `❌ Agent failed: ${error}`
|
|
5372
|
-
});
|
|
5373
|
-
const agent = this.pool.get(agentId);
|
|
5374
|
-
if (agent) {
|
|
5375
|
-
agent.tasksFailed += 1;
|
|
5376
|
-
agent.currentTaskId = null;
|
|
5377
|
-
agent.status = "IDLE";
|
|
5378
|
-
}
|
|
5379
|
-
this.emit("task:failed", { agentId, taskId, error });
|
|
5380
|
-
} catch (error2) {
|
|
5381
|
-
this.emit("error", error2);
|
|
5382
|
-
}
|
|
5383
|
-
}
|
|
5384
|
-
async stop() {
|
|
5385
|
-
this.isRunning = false;
|
|
5386
|
-
await this.cleanup();
|
|
5387
|
-
this.emit("stopped", { timestamp: new Date });
|
|
5388
|
-
}
|
|
5389
|
-
stopAgent(agentId) {
|
|
5390
|
-
return this.pool.stopAgent(agentId);
|
|
5391
|
-
}
|
|
5392
|
-
async cleanup() {
|
|
5393
|
-
this.pool.shutdown();
|
|
5394
|
-
if (this.worktreeManager) {
|
|
5395
|
-
try {
|
|
5396
|
-
if (this.worktreeCleanupPolicy === "auto") {
|
|
5397
|
-
const removed = this.worktreeManager.removeAll();
|
|
5398
|
-
if (removed > 0) {
|
|
5399
|
-
console.log(c.dim(`Cleaned up ${removed} worktree(s)`));
|
|
5400
|
-
}
|
|
5401
|
-
} else if (this.worktreeCleanupPolicy === "retain-on-failure") {
|
|
5402
|
-
this.worktreeManager.prune();
|
|
5403
|
-
console.log(c.dim("Retaining worktrees for failure analysis (cleanup policy: retain-on-failure)"));
|
|
5404
|
-
} else {
|
|
5405
|
-
console.log(c.dim("Skipping worktree cleanup (cleanup policy: manual)"));
|
|
5406
|
-
}
|
|
5407
|
-
} catch {
|
|
5408
|
-
console.log(c.dim("Could not clean up some worktrees"));
|
|
5409
|
-
}
|
|
5410
|
-
}
|
|
5411
|
-
}
|
|
5412
|
-
getStats() {
|
|
5413
|
-
const poolStats = this.pool.getStats();
|
|
5414
|
-
return {
|
|
5415
|
-
...poolStats,
|
|
5416
|
-
useWorktrees: this.useWorktrees,
|
|
5417
|
-
processedTasks: this.processedTasks.size
|
|
5418
|
-
};
|
|
5419
|
-
}
|
|
5420
|
-
getAgentStates() {
|
|
5421
|
-
return this.pool.getAll();
|
|
5422
|
-
}
|
|
4746
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
5423
4747
|
}
|
|
5424
4748
|
// src/planning/plan-manager.ts
|
|
5425
4749
|
init_config();
|
|
5426
4750
|
init_knowledge_base();
|
|
5427
|
-
var
|
|
5428
|
-
var
|
|
4751
|
+
var import_node_fs9 = require("node:fs");
|
|
4752
|
+
var import_node_path11 = require("node:path");
|
|
5429
4753
|
|
|
5430
4754
|
// src/planning/sprint-plan.ts
|
|
5431
|
-
var
|
|
4755
|
+
var import_shared5 = require("@locusai/shared");
|
|
5432
4756
|
function sprintPlanToMarkdown(plan) {
|
|
5433
4757
|
const lines = [];
|
|
5434
4758
|
lines.push(`# Sprint Plan: ${plan.name}`);
|
|
@@ -5450,32 +4774,25 @@ function sprintPlanToMarkdown(plan) {
|
|
|
5450
4774
|
}
|
|
5451
4775
|
lines.push(`## Tasks (${plan.tasks.length})`);
|
|
5452
4776
|
lines.push("");
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
lines.push(
|
|
5459
|
-
lines.push(
|
|
4777
|
+
lines.push("_Tasks are executed sequentially in the order listed below._");
|
|
4778
|
+
lines.push("");
|
|
4779
|
+
for (const task of plan.tasks) {
|
|
4780
|
+
lines.push(`### ${task.index}. ${task.title}`);
|
|
4781
|
+
lines.push(`- **Role:** ${task.assigneeRole}`);
|
|
4782
|
+
lines.push(`- **Priority:** ${task.priority}`);
|
|
4783
|
+
lines.push(`- **Complexity:** ${"█".repeat(task.complexity)}${"░".repeat(5 - task.complexity)} (${task.complexity}/5)`);
|
|
4784
|
+
if (task.labels.length > 0) {
|
|
4785
|
+
lines.push(`- **Labels:** ${task.labels.join(", ")}`);
|
|
4786
|
+
}
|
|
4787
|
+
lines.push("");
|
|
4788
|
+
lines.push(task.description);
|
|
5460
4789
|
lines.push("");
|
|
5461
|
-
|
|
5462
|
-
lines.push(
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
lines.push(`- **Complexity:** ${"█".repeat(task.complexity)}${"░".repeat(5 - task.complexity)} (${task.complexity}/5)`);
|
|
5466
|
-
if (task.labels.length > 0) {
|
|
5467
|
-
lines.push(`- **Labels:** ${task.labels.join(", ")}`);
|
|
4790
|
+
if (task.acceptanceCriteria.length > 0) {
|
|
4791
|
+
lines.push(`**Acceptance Criteria:**`);
|
|
4792
|
+
for (const ac of task.acceptanceCriteria) {
|
|
4793
|
+
lines.push(`- [ ] ${ac}`);
|
|
5468
4794
|
}
|
|
5469
4795
|
lines.push("");
|
|
5470
|
-
lines.push(task.description);
|
|
5471
|
-
lines.push("");
|
|
5472
|
-
if (task.acceptanceCriteria.length > 0) {
|
|
5473
|
-
lines.push(`**Acceptance Criteria:**`);
|
|
5474
|
-
for (const ac of task.acceptanceCriteria) {
|
|
5475
|
-
lines.push(`- [ ] ${ac}`);
|
|
5476
|
-
}
|
|
5477
|
-
lines.push("");
|
|
5478
|
-
}
|
|
5479
4796
|
}
|
|
5480
4797
|
}
|
|
5481
4798
|
if (plan.risks.length > 0) {
|
|
@@ -5496,13 +4813,12 @@ function plannedTasksToCreatePayloads(plan, sprintId) {
|
|
|
5496
4813
|
return plan.tasks.map((task) => ({
|
|
5497
4814
|
title: task.title,
|
|
5498
4815
|
description: task.description,
|
|
5499
|
-
status:
|
|
4816
|
+
status: import_shared5.TaskStatus.BACKLOG,
|
|
5500
4817
|
assigneeRole: task.assigneeRole,
|
|
5501
4818
|
priority: task.priority,
|
|
5502
4819
|
labels: task.labels,
|
|
5503
4820
|
sprintId,
|
|
5504
4821
|
order: task.index * 10,
|
|
5505
|
-
tier: task.tier,
|
|
5506
4822
|
acceptanceChecklist: task.acceptanceCriteria.map((text, i) => ({
|
|
5507
4823
|
id: `ac-${i + 1}`,
|
|
5508
4824
|
text,
|
|
@@ -5526,8 +4842,7 @@ function parseSprintPlanFromAI(raw, directive) {
|
|
|
5526
4842
|
priority: t.priority || "MEDIUM",
|
|
5527
4843
|
complexity: t.complexity || 3,
|
|
5528
4844
|
acceptanceCriteria: t.acceptanceCriteria || [],
|
|
5529
|
-
labels: t.labels || []
|
|
5530
|
-
tier: typeof t.tier === "number" ? t.tier : 0
|
|
4845
|
+
labels: t.labels || []
|
|
5531
4846
|
}));
|
|
5532
4847
|
return {
|
|
5533
4848
|
id,
|
|
@@ -5558,19 +4873,19 @@ class PlanManager {
|
|
|
5558
4873
|
save(plan) {
|
|
5559
4874
|
this.ensurePlansDir();
|
|
5560
4875
|
const slug = this.slugify(plan.name);
|
|
5561
|
-
const jsonPath =
|
|
5562
|
-
const mdPath =
|
|
5563
|
-
|
|
5564
|
-
|
|
4876
|
+
const jsonPath = import_node_path11.join(this.plansDir, `${slug}.json`);
|
|
4877
|
+
const mdPath = import_node_path11.join(this.plansDir, `sprint-${slug}.md`);
|
|
4878
|
+
import_node_fs9.writeFileSync(jsonPath, JSON.stringify(plan, null, 2), "utf-8");
|
|
4879
|
+
import_node_fs9.writeFileSync(mdPath, sprintPlanToMarkdown(plan), "utf-8");
|
|
5565
4880
|
return plan.id;
|
|
5566
4881
|
}
|
|
5567
4882
|
load(idOrSlug) {
|
|
5568
4883
|
this.ensurePlansDir();
|
|
5569
|
-
const files =
|
|
4884
|
+
const files = import_node_fs9.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
|
|
5570
4885
|
for (const file of files) {
|
|
5571
|
-
const filePath =
|
|
4886
|
+
const filePath = import_node_path11.join(this.plansDir, file);
|
|
5572
4887
|
try {
|
|
5573
|
-
const plan = JSON.parse(
|
|
4888
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(filePath, "utf-8"));
|
|
5574
4889
|
if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
|
|
5575
4890
|
return plan;
|
|
5576
4891
|
}
|
|
@@ -5580,11 +4895,11 @@ class PlanManager {
|
|
|
5580
4895
|
}
|
|
5581
4896
|
list(status) {
|
|
5582
4897
|
this.ensurePlansDir();
|
|
5583
|
-
const files =
|
|
4898
|
+
const files = import_node_fs9.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
|
|
5584
4899
|
const plans = [];
|
|
5585
4900
|
for (const file of files) {
|
|
5586
4901
|
try {
|
|
5587
|
-
const plan = JSON.parse(
|
|
4902
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(import_node_path11.join(this.plansDir, file), "utf-8"));
|
|
5588
4903
|
if (!status || plan.status === status) {
|
|
5589
4904
|
plans.push(plan);
|
|
5590
4905
|
}
|
|
@@ -5650,18 +4965,18 @@ class PlanManager {
|
|
|
5650
4965
|
}
|
|
5651
4966
|
delete(idOrSlug) {
|
|
5652
4967
|
this.ensurePlansDir();
|
|
5653
|
-
const files =
|
|
4968
|
+
const files = import_node_fs9.readdirSync(this.plansDir);
|
|
5654
4969
|
for (const file of files) {
|
|
5655
|
-
const filePath =
|
|
4970
|
+
const filePath = import_node_path11.join(this.plansDir, file);
|
|
5656
4971
|
if (!file.endsWith(".json"))
|
|
5657
4972
|
continue;
|
|
5658
4973
|
try {
|
|
5659
|
-
const plan = JSON.parse(
|
|
4974
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(filePath, "utf-8"));
|
|
5660
4975
|
if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
|
|
5661
|
-
|
|
5662
|
-
const mdPath =
|
|
5663
|
-
if (
|
|
5664
|
-
|
|
4976
|
+
import_node_fs9.unlinkSync(filePath);
|
|
4977
|
+
const mdPath = import_node_path11.join(this.plansDir, `sprint-${this.slugify(plan.name)}.md`);
|
|
4978
|
+
if (import_node_fs9.existsSync(mdPath)) {
|
|
4979
|
+
import_node_fs9.unlinkSync(mdPath);
|
|
5665
4980
|
}
|
|
5666
4981
|
return;
|
|
5667
4982
|
}
|
|
@@ -5675,8 +4990,8 @@ class PlanManager {
|
|
|
5675
4990
|
return sprintPlanToMarkdown(plan);
|
|
5676
4991
|
}
|
|
5677
4992
|
ensurePlansDir() {
|
|
5678
|
-
if (!
|
|
5679
|
-
|
|
4993
|
+
if (!import_node_fs9.existsSync(this.plansDir)) {
|
|
4994
|
+
import_node_fs9.mkdirSync(this.plansDir, { recursive: true });
|
|
5680
4995
|
}
|
|
5681
4996
|
}
|
|
5682
4997
|
slugify(name) {
|
|
@@ -5686,7 +5001,7 @@ class PlanManager {
|
|
|
5686
5001
|
// src/planning/planning-meeting.ts
|
|
5687
5002
|
init_config();
|
|
5688
5003
|
init_knowledge_base();
|
|
5689
|
-
var
|
|
5004
|
+
var import_node_fs10 = require("node:fs");
|
|
5690
5005
|
|
|
5691
5006
|
// src/planning/agents/architect.ts
|
|
5692
5007
|
function buildArchitectPrompt(input) {
|
|
@@ -5723,21 +5038,14 @@ Review and refine the Tech Lead's breakdown:
|
|
|
5723
5038
|
5. **Missing Tasks** — Add any tasks the Tech Lead missed (database migrations, configuration, testing, etc.).
|
|
5724
5039
|
6. **Description Quality** — Review and improve each task description to be a clear, actionable implementation guide. Each description must tell the executing agent exactly what to do, where to do it (specific files/modules), how to do it (patterns, utilities, data flow), and what is NOT in scope. Vague descriptions like "Add authentication" must be rewritten with specific file paths, implementation approach, and boundaries.
|
|
5725
5040
|
|
|
5726
|
-
## CRITICAL: Task
|
|
5727
|
-
|
|
5728
|
-
Tasks are executed by INDEPENDENT agents on SEPARATE git branches that get merged together. Each agent has NO knowledge of what other agents are doing. You MUST enforce these rules:
|
|
5729
|
-
|
|
5730
|
-
1. **Detect overlapping file modifications.** For each task, mentally list the files it will touch. If two tasks modify the same file (especially config files like app.module.ts, configuration.ts, package.json, or shared modules), they WILL cause merge conflicts. You must either:
|
|
5731
|
-
- **Merge them** into a single task, OR
|
|
5732
|
-
- **Move the shared file changes** into one foundational task that runs and merges first
|
|
5041
|
+
## CRITICAL: Task Ordering & Dependencies
|
|
5733
5042
|
|
|
5734
|
-
|
|
5043
|
+
Tasks are executed SEQUENTIALLY by a single agent on ONE branch. The agent works through tasks in array order. Each completed task's changes are available to subsequent tasks. You MUST enforce these rules:
|
|
5735
5044
|
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
5. **Flag high-conflict zones.** In your risk assessment, specifically call out any remaining cases where tasks might touch the same files, and explain why it's unavoidable and how merge conflicts can be minimized.
|
|
5045
|
+
1. **Order tasks by dependency.** Foundation tasks (schemas, config, shared code) must come first. Tasks that build on earlier work must appear later in the list.
|
|
5046
|
+
2. **Each task must be self-contained for its scope.** A task can depend on earlier tasks (they run sequentially), but must include all changes needed for its own goal.
|
|
5047
|
+
3. **Split tasks at logical boundaries.** Since tasks run sequentially on the same branch, splitting is safe — later tasks see earlier changes. Split when it improves clarity and reviewability.
|
|
5048
|
+
4. **Flag risks.** In your risk assessment, call out tasks that are complex or have unknowns.
|
|
5741
5049
|
|
|
5742
5050
|
## Output Format
|
|
5743
5051
|
|
|
@@ -5771,20 +5079,20 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5771
5079
|
function buildCrossTaskReviewerPrompt(input) {
|
|
5772
5080
|
let prompt = `# Role: Cross-Task Reviewer (Architect + Engineer + Planner)
|
|
5773
5081
|
|
|
5774
|
-
You are a combined Architect, Senior Engineer, and Sprint Planner performing a FINAL review of a sprint plan. Your
|
|
5082
|
+
You are a combined Architect, Senior Engineer, and Sprint Planner performing a FINAL review of a sprint plan. Your focus is ensuring that tasks are correctly ordered, well-scoped, and will execute successfully in sequence.
|
|
5775
5083
|
|
|
5776
5084
|
## Context
|
|
5777
5085
|
|
|
5778
|
-
In this system, tasks are
|
|
5779
|
-
-
|
|
5780
|
-
-
|
|
5781
|
-
-
|
|
5782
|
-
-
|
|
5086
|
+
In this system, tasks are executed SEQUENTIALLY by a single agent on ONE branch:
|
|
5087
|
+
- Tasks run one at a time, in the order they appear in the array
|
|
5088
|
+
- Each task's changes are committed before the next task starts
|
|
5089
|
+
- Later tasks can see and build on earlier tasks' work
|
|
5090
|
+
- The final result is a single branch with all changes, which becomes a pull request
|
|
5783
5091
|
|
|
5784
5092
|
This means:
|
|
5785
|
-
-
|
|
5786
|
-
-
|
|
5787
|
-
-
|
|
5093
|
+
- Task ordering is critical — a task must NOT depend on a later task's output
|
|
5094
|
+
- Foundation work (config, schemas, shared code) must come first
|
|
5095
|
+
- Each task should be a focused, logical unit of work
|
|
5788
5096
|
|
|
5789
5097
|
## CEO Directive
|
|
5790
5098
|
> ${input.directive}
|
|
@@ -5806,41 +5114,21 @@ ${input.sprintOrganizerOutput}
|
|
|
5806
5114
|
|
|
5807
5115
|
## Your Review Checklist
|
|
5808
5116
|
|
|
5809
|
-
Go through EACH
|
|
5810
|
-
|
|
5811
|
-
### 1. File Overlap Analysis (WITHIN the same tier)
|
|
5812
|
-
For each task, list the files it will likely modify. Then check:
|
|
5813
|
-
- Do any two tasks **in the same tier** modify the same file? (e.g., app.module.ts, configuration.ts, package.json, shared DTOs)
|
|
5814
|
-
- If yes: MERGE those tasks, move them to different tiers, or move shared changes to a foundational task in a lower tier
|
|
5815
|
-
- Note: tasks in different tiers are safe because higher tiers branch from merged lower-tier results
|
|
5117
|
+
Go through EACH task and check for:
|
|
5816
5118
|
|
|
5817
|
-
###
|
|
5818
|
-
|
|
5819
|
-
-
|
|
5820
|
-
-
|
|
5821
|
-
-
|
|
5822
|
-
- Create the same helper function, guard, interceptor, or middleware
|
|
5823
|
-
- Add the same import to a shared file
|
|
5824
|
-
If yes: consolidate into ONE task or move the shared work to a lower tier
|
|
5119
|
+
### 1. Ordering & Dependency Analysis
|
|
5120
|
+
For each task, verify:
|
|
5121
|
+
- Does it depend on any task that appears LATER in the list? If so, reorder.
|
|
5122
|
+
- Are foundational tasks (config, schemas, shared code) at the beginning?
|
|
5123
|
+
- Is the overall execution order logical?
|
|
5825
5124
|
|
|
5826
|
-
###
|
|
5125
|
+
### 2. Scope & Completeness
|
|
5827
5126
|
For each task, verify:
|
|
5828
|
-
-
|
|
5829
|
-
- Does it include ALL
|
|
5830
|
-
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
### 4. Tier Assignment Validation
|
|
5834
|
-
Verify tier assignments are correct:
|
|
5835
|
-
- Foundational tasks (schemas, config, shared code) MUST be in tier 0
|
|
5836
|
-
- Tasks that depend on another task's output must be in a HIGHER tier
|
|
5837
|
-
- Tasks in the same tier must be truly independent of each other
|
|
5838
|
-
- No circular dependencies between tiers
|
|
5839
|
-
|
|
5840
|
-
### 5. Merge Conflict Risk Zones
|
|
5841
|
-
Identify the highest-risk files (files that multiple same-tier tasks might touch) and ensure only ONE task per tier modifies each.
|
|
5842
|
-
|
|
5843
|
-
### 6. Description Quality Validation
|
|
5127
|
+
- Is the task well-scoped? Not too large, not too trivial?
|
|
5128
|
+
- Does it include ALL changes needed for its goal (given earlier tasks are done)?
|
|
5129
|
+
- Are there any missing tasks that should be added?
|
|
5130
|
+
|
|
5131
|
+
### 3. Description Quality Validation
|
|
5844
5132
|
For each task, verify the description is a clear, actionable implementation guide. Each description must specify:
|
|
5845
5133
|
- **What to do** — the specific goal and expected behavior/outcome
|
|
5846
5134
|
- **Where to do it** — specific files, modules, or directories to modify or create
|
|
@@ -5849,6 +5137,10 @@ For each task, verify the description is a clear, actionable implementation guid
|
|
|
5849
5137
|
|
|
5850
5138
|
If any description is vague (e.g., "Add authentication", "Update the API", "Fix the frontend"), rewrite it with concrete implementation details. The executing agent receives ONLY the task title, description, and acceptance criteria as its instructions.
|
|
5851
5139
|
|
|
5140
|
+
### 4. Risk Assessment
|
|
5141
|
+
- Are there tasks that might fail or have unknowns?
|
|
5142
|
+
- Is the sprint scope realistic for sequential execution?
|
|
5143
|
+
|
|
5852
5144
|
## Output Format
|
|
5853
5145
|
|
|
5854
5146
|
Your entire response must be a single JSON object — no text before it, no text after it, no markdown code blocks, no explanation. Start your response with the "{" character:
|
|
@@ -5857,10 +5149,10 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5857
5149
|
"hasIssues": true | false,
|
|
5858
5150
|
"issues": [
|
|
5859
5151
|
{
|
|
5860
|
-
"type": "
|
|
5152
|
+
"type": "wrong_order" | "missing_task" | "scope_issue" | "vague_description",
|
|
5861
5153
|
"description": "string describing the specific issue",
|
|
5862
5154
|
"affectedTasks": ["Task Title 1", "Task Title 2"],
|
|
5863
|
-
"resolution": "string describing how to fix it
|
|
5155
|
+
"resolution": "string describing how to fix it"
|
|
5864
5156
|
}
|
|
5865
5157
|
],
|
|
5866
5158
|
"revisedPlan": {
|
|
@@ -5875,8 +5167,7 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5875
5167
|
"priority": "CRITICAL | HIGH | MEDIUM | LOW",
|
|
5876
5168
|
"labels": ["string"],
|
|
5877
5169
|
"acceptanceCriteria": ["string"],
|
|
5878
|
-
"complexity": 3
|
|
5879
|
-
"tier": 0
|
|
5170
|
+
"complexity": 3
|
|
5880
5171
|
}
|
|
5881
5172
|
],
|
|
5882
5173
|
"risks": [
|
|
@@ -5890,15 +5181,11 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5890
5181
|
}
|
|
5891
5182
|
|
|
5892
5183
|
IMPORTANT:
|
|
5893
|
-
- If hasIssues is true, the revisedPlan MUST contain the corrected task list with issues resolved (
|
|
5184
|
+
- If hasIssues is true, the revisedPlan MUST contain the corrected task list with issues resolved (reordered, descriptions rewritten, missing tasks added, etc.)
|
|
5894
5185
|
- If hasIssues is false, the revisedPlan should be identical to the input plan (no changes needed)
|
|
5895
5186
|
- The revisedPlan is ALWAYS required — it becomes the final plan
|
|
5896
|
-
- When merging tasks, combine their acceptance criteria and update descriptions to cover all consolidated work
|
|
5897
5187
|
- Ensure every task description is a detailed implementation guide (what, where, how, boundaries) — rewrite vague descriptions
|
|
5898
|
-
-
|
|
5899
|
-
- Every task MUST have a "tier" field (integer >= 0)
|
|
5900
|
-
- tier 0 = foundational (runs first), tier 1 = depends on tier 0, tier 2 = depends on tier 1, etc.
|
|
5901
|
-
- Tasks within the same tier run in parallel — they MUST NOT conflict with each other`;
|
|
5188
|
+
- Tasks execute sequentially — the array order IS the execution order`;
|
|
5902
5189
|
return prompt;
|
|
5903
5190
|
}
|
|
5904
5191
|
|
|
@@ -5929,30 +5216,27 @@ Produce the final sprint plan:
|
|
|
5929
5216
|
|
|
5930
5217
|
1. **Sprint Name** — A concise, memorable name for this sprint (e.g., "User Authentication", "Payment Integration")
|
|
5931
5218
|
2. **Sprint Goal** — One paragraph describing what this sprint delivers
|
|
5932
|
-
3. **Task Ordering** — Final ordering so that foundational work comes first. The position in the array IS the execution order — task at index 0 runs first, index 1 runs second, etc.
|
|
5933
|
-
4. **
|
|
5934
|
-
5. **
|
|
5935
|
-
6. **
|
|
5936
|
-
7. **Description Quality Check** — Ensure every task description is a clear, actionable implementation guide. Each description must specify: what to do, where to do it (specific files/modules/directories), how to do it (implementation approach, patterns to follow, existing utilities to use), and what is NOT in scope. If any description is vague or generic, rewrite it with specifics. Remember: an independent agent will receive ONLY the task title, description, and acceptance criteria — the description is its primary instruction.
|
|
5219
|
+
3. **Task Ordering** — Final ordering so that foundational work comes first. The position in the array IS the execution order — task at index 0 runs first, index 1 runs second, etc. Tasks are executed SEQUENTIALLY by a single agent on one branch.
|
|
5220
|
+
4. **Duration Estimate** — How many days this sprint will take with a single agent working sequentially
|
|
5221
|
+
5. **Final Task List** — Each task with all fields filled in, ordered by execution priority
|
|
5222
|
+
6. **Description Quality Check** — Ensure every task description is a clear, actionable implementation guide. Each description must specify: what to do, where to do it (specific files/modules/directories), how to do it (implementation approach, patterns to follow, existing utilities to use), and what is NOT in scope. If any description is vague or generic, rewrite it with specifics. Remember: an independent agent will receive ONLY the task title, description, and acceptance criteria — the description is its primary instruction.
|
|
5937
5223
|
|
|
5938
5224
|
Guidelines:
|
|
5939
5225
|
- The order of tasks in the array determines execution order. Tasks are dispatched sequentially from first to last.
|
|
5940
|
-
- Foundation tasks (schemas, config, shared code) must appear before tasks that build on them
|
|
5941
|
-
-
|
|
5942
|
-
- Tasks that depend on outputs from other tasks must be in a higher tier than those dependencies
|
|
5943
|
-
- Group related independent tasks in the same tier for maximum parallelism
|
|
5226
|
+
- Foundation tasks (schemas, config, shared code) must appear before tasks that build on them
|
|
5227
|
+
- Since tasks execute sequentially on one branch, later tasks can depend on earlier tasks' outputs
|
|
5944
5228
|
- Ensure acceptance criteria are specific and testable
|
|
5945
5229
|
- Keep the sprint focused — if it's too large (>12 tasks), consider reducing scope
|
|
5946
5230
|
- Ensure every task description reads as a standalone implementation brief — not a summary
|
|
5947
5231
|
|
|
5948
|
-
## CRITICAL: Task
|
|
5232
|
+
## CRITICAL: Task Ordering Validation
|
|
5949
5233
|
|
|
5950
|
-
Before finalizing, validate that
|
|
5234
|
+
Before finalizing, validate that tasks are in the correct execution order:
|
|
5951
5235
|
|
|
5952
|
-
1. **No
|
|
5953
|
-
2. **
|
|
5954
|
-
3. **Each task is independently executable
|
|
5955
|
-
4. **Prefer
|
|
5236
|
+
1. **No forward dependencies.** A task must NOT depend on a task that appears later in the list.
|
|
5237
|
+
2. **Foundation first.** Config, schemas, and shared code must come before implementation tasks.
|
|
5238
|
+
3. **Each task is independently executable given prior tasks.** An agent working on task N must be able to complete it assuming tasks 1 through N-1 are already done.
|
|
5239
|
+
4. **Prefer focused, well-scoped tasks.** Each task should do one logical unit of work.
|
|
5956
5240
|
|
|
5957
5241
|
## Output Format
|
|
5958
5242
|
|
|
@@ -5970,8 +5254,7 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5970
5254
|
"priority": "CRITICAL | HIGH | MEDIUM | LOW",
|
|
5971
5255
|
"labels": ["string"],
|
|
5972
5256
|
"acceptanceCriteria": ["string"],
|
|
5973
|
-
"complexity": 3
|
|
5974
|
-
"tier": 0
|
|
5257
|
+
"complexity": 3
|
|
5975
5258
|
}
|
|
5976
5259
|
],
|
|
5977
5260
|
"risks": [
|
|
@@ -5983,12 +5266,7 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5983
5266
|
]
|
|
5984
5267
|
}
|
|
5985
5268
|
|
|
5986
|
-
IMPORTANT
|
|
5987
|
-
- tier 0 = foundational tasks (run first, merged before anything else)
|
|
5988
|
-
- tier 1 = tasks that depend on tier 0 outputs (run in parallel after tier 0 merges)
|
|
5989
|
-
- tier 2 = tasks that depend on tier 1 outputs (run in parallel after tier 1 merges)
|
|
5990
|
-
- Tasks within the same tier run in parallel on separate branches — they MUST NOT conflict
|
|
5991
|
-
- Every task MUST have a "tier" field (integer >= 0)`;
|
|
5269
|
+
IMPORTANT: Tasks are executed sequentially by a single agent. The array order IS the execution order — ensure foundational work comes first and dependent tasks come after their prerequisites.`;
|
|
5992
5270
|
return prompt;
|
|
5993
5271
|
}
|
|
5994
5272
|
|
|
@@ -6046,15 +5324,14 @@ Each description MUST include:
|
|
|
6046
5324
|
Bad example: "Add authentication to the API."
|
|
6047
5325
|
Good example: "Implement JWT-based authentication middleware in src/middleware/auth.ts. Create a verifyToken middleware that extracts the Bearer token from the Authorization header, validates it using the existing JWT_SECRET from env config, and attaches the decoded user payload to req.user. Apply this middleware to all routes in src/routes/protected/. Add a POST /auth/login endpoint in src/routes/auth.ts that accepts {email, password}, validates credentials against the users table, and returns a signed JWT. This task does NOT include user registration or password reset — those are handled separately."
|
|
6048
5326
|
|
|
6049
|
-
## CRITICAL: Task
|
|
5327
|
+
## CRITICAL: Task Ordering Rules
|
|
6050
5328
|
|
|
6051
|
-
Tasks
|
|
5329
|
+
Tasks are executed SEQUENTIALLY by a single agent on ONE branch. The agent works through tasks in the order they appear in the array. Therefore:
|
|
6052
5330
|
|
|
6053
|
-
1. **
|
|
6054
|
-
2. **Each task must be
|
|
6055
|
-
3. **
|
|
6056
|
-
4. **
|
|
6057
|
-
5. **Environment variables, configs, and shared modules are high-conflict zones.** If a task introduces a new env var, config schema field, or module import, NO other task should touch that same file unless absolutely necessary.
|
|
5331
|
+
1. **Foundation first.** Place foundational tasks (schemas, config, shared code) at the beginning of the list. Later tasks can build on earlier ones since they run in sequence on the same branch.
|
|
5332
|
+
2. **Each task must be self-contained.** A task must include ALL the code changes it needs to work — from config to implementation to tests. A task CAN depend on earlier tasks in the list since they will have already been completed.
|
|
5333
|
+
3. **Logical ordering matters.** Tasks are dispatched in the order they appear. Ensure dependent tasks come after their prerequisites.
|
|
5334
|
+
4. **Keep tasks focused.** Each task should do one logical unit of work. Since there are no parallel execution conflicts, tasks can be more granular — but avoid tasks that are too small or trivial.
|
|
6058
5335
|
|
|
6059
5336
|
## Output Format
|
|
6060
5337
|
|
|
@@ -6146,11 +5423,11 @@ class PlanningMeeting {
|
|
|
6146
5423
|
}
|
|
6147
5424
|
getCodebaseIndex() {
|
|
6148
5425
|
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
6149
|
-
if (!
|
|
5426
|
+
if (!import_node_fs10.existsSync(indexPath)) {
|
|
6150
5427
|
return "";
|
|
6151
5428
|
}
|
|
6152
5429
|
try {
|
|
6153
|
-
const raw =
|
|
5430
|
+
const raw = import_node_fs10.readFileSync(indexPath, "utf-8");
|
|
6154
5431
|
const index = JSON.parse(raw);
|
|
6155
5432
|
const parts = [];
|
|
6156
5433
|
if (index.responsibilities) {
|
|
@@ -6173,7 +5450,3 @@ class PlanningMeeting {
|
|
|
6173
5450
|
// src/index-node.ts
|
|
6174
5451
|
init_knowledge_base();
|
|
6175
5452
|
init_colors();
|
|
6176
|
-
|
|
6177
|
-
// src/worktree/index.ts
|
|
6178
|
-
init_worktree_config();
|
|
6179
|
-
init_worktree_manager();
|