@locusai/sdk 0.10.5 → 0.11.0
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/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 +255 -802
- 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 +783 -1532
- 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,7 +1330,7 @@ class CodexRunner {
|
|
|
1333
1330
|
type: "tool_use",
|
|
1334
1331
|
tool: line.replace(/^[→•✓]\s*/, "")
|
|
1335
1332
|
});
|
|
1336
|
-
} else
|
|
1333
|
+
} else {
|
|
1337
1334
|
enqueueChunk({ type: "text_delta", content: `${line}
|
|
1338
1335
|
` });
|
|
1339
1336
|
}
|
|
@@ -1473,18 +1470,11 @@ class CodexRunner {
|
|
|
1473
1470
|
}
|
|
1474
1471
|
buildArgs(outputPath) {
|
|
1475
1472
|
const args = [
|
|
1476
|
-
"--ask-for-approval",
|
|
1477
|
-
"never",
|
|
1478
1473
|
"exec",
|
|
1479
|
-
"--
|
|
1480
|
-
"workspace-write",
|
|
1474
|
+
"--full-auto",
|
|
1481
1475
|
"--skip-git-repo-check",
|
|
1482
1476
|
"--output-last-message",
|
|
1483
|
-
outputPath
|
|
1484
|
-
"-c",
|
|
1485
|
-
"sandbox_workspace_write.network_access=true",
|
|
1486
|
-
"-c",
|
|
1487
|
-
'sandbox.excludedCommands=["git", "gh"]'
|
|
1477
|
+
outputPath
|
|
1488
1478
|
];
|
|
1489
1479
|
if (this.model) {
|
|
1490
1480
|
args.push("--model", this.model);
|
|
@@ -1760,583 +1750,247 @@ var init_knowledge_base = __esm(() => {
|
|
|
1760
1750
|
import_node_path5 = require("node:path");
|
|
1761
1751
|
});
|
|
1762
1752
|
|
|
1763
|
-
// src/git
|
|
1764
|
-
class
|
|
1765
|
-
|
|
1753
|
+
// src/agent/git-workflow.ts
|
|
1754
|
+
class GitWorkflow {
|
|
1755
|
+
config;
|
|
1766
1756
|
log;
|
|
1767
|
-
|
|
1768
|
-
|
|
1757
|
+
projectPath;
|
|
1758
|
+
branchName = null;
|
|
1759
|
+
baseBranch = null;
|
|
1760
|
+
ghUsername;
|
|
1761
|
+
constructor(config, log) {
|
|
1762
|
+
this.config = config;
|
|
1769
1763
|
this.log = log;
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
branch,
|
|
1775
|
-
baseBranch: requestedBaseBranch,
|
|
1776
|
-
agentId,
|
|
1777
|
-
summary
|
|
1778
|
-
} = options;
|
|
1779
|
-
const provider = detectRemoteProvider(this.projectPath);
|
|
1780
|
-
if (provider !== "github") {
|
|
1781
|
-
throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
|
|
1782
|
-
}
|
|
1783
|
-
if (!isGhAvailable(this.projectPath)) {
|
|
1784
|
-
throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
|
|
1785
|
-
}
|
|
1786
|
-
const title = `[Locus] ${task.title}`;
|
|
1787
|
-
const body = this.buildPrBody(task, agentId, summary);
|
|
1788
|
-
const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
|
|
1789
|
-
this.validateCreatePrInputs(baseBranch, branch);
|
|
1790
|
-
this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
|
|
1791
|
-
const output = import_node_child_process4.execFileSync("gh", [
|
|
1792
|
-
"pr",
|
|
1793
|
-
"create",
|
|
1794
|
-
"--title",
|
|
1795
|
-
title,
|
|
1796
|
-
"--body",
|
|
1797
|
-
body,
|
|
1798
|
-
"--base",
|
|
1799
|
-
baseBranch,
|
|
1800
|
-
"--head",
|
|
1801
|
-
branch
|
|
1802
|
-
], {
|
|
1803
|
-
cwd: this.projectPath,
|
|
1804
|
-
encoding: "utf-8",
|
|
1805
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1806
|
-
}).trim();
|
|
1807
|
-
const url = output;
|
|
1808
|
-
const prNumber = this.extractPrNumber(url);
|
|
1809
|
-
this.log(`PR created: ${url}`, "success");
|
|
1810
|
-
return { url, number: prNumber };
|
|
1811
|
-
}
|
|
1812
|
-
validateCreatePrInputs(baseBranch, headBranch) {
|
|
1813
|
-
if (!this.hasRemoteBranch(baseBranch)) {
|
|
1814
|
-
throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
|
|
1815
|
-
}
|
|
1816
|
-
if (!this.hasRemoteBranch(headBranch)) {
|
|
1817
|
-
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
1818
|
-
}
|
|
1819
|
-
const baseRef = this.resolveBranchRef(baseBranch);
|
|
1820
|
-
const headRef = this.resolveBranchRef(headBranch);
|
|
1821
|
-
if (!baseRef) {
|
|
1822
|
-
throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
|
|
1823
|
-
}
|
|
1824
|
-
if (!headRef) {
|
|
1825
|
-
throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
|
|
1826
|
-
}
|
|
1827
|
-
const commitsAhead = this.countCommitsAhead(baseRef, headRef);
|
|
1828
|
-
if (commitsAhead <= 0) {
|
|
1829
|
-
throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
countCommitsAhead(baseRef, headRef) {
|
|
1833
|
-
const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
1834
|
-
cwd: this.projectPath,
|
|
1835
|
-
encoding: "utf-8",
|
|
1836
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1837
|
-
}).trim();
|
|
1838
|
-
const value = Number.parseInt(output || "0", 10);
|
|
1839
|
-
return Number.isNaN(value) ? 0 : value;
|
|
1840
|
-
}
|
|
1841
|
-
resolveBranchRef(branch) {
|
|
1842
|
-
if (this.hasLocalBranch(branch)) {
|
|
1843
|
-
return branch;
|
|
1844
|
-
}
|
|
1845
|
-
if (this.hasRemoteTrackingBranch(branch)) {
|
|
1846
|
-
return `origin/${branch}`;
|
|
1764
|
+
this.projectPath = config.projectPath || process.cwd();
|
|
1765
|
+
this.ghUsername = getGhUsername();
|
|
1766
|
+
if (this.ghUsername) {
|
|
1767
|
+
this.log(`GitHub user: ${this.ghUsername}`, "info");
|
|
1847
1768
|
}
|
|
1848
|
-
return null;
|
|
1849
1769
|
}
|
|
1850
|
-
|
|
1770
|
+
createBranch(sprintId) {
|
|
1771
|
+
const defaultBranch = getDefaultBranch(this.projectPath);
|
|
1772
|
+
this.baseBranch = defaultBranch;
|
|
1851
1773
|
try {
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1855
|
-
});
|
|
1856
|
-
return true;
|
|
1774
|
+
this.gitExec(["checkout", defaultBranch]);
|
|
1775
|
+
this.gitExec(["pull", "origin", defaultBranch]);
|
|
1857
1776
|
} catch {
|
|
1858
|
-
|
|
1777
|
+
this.log(`Could not pull latest from ${defaultBranch}, continuing with current state`, "warn");
|
|
1859
1778
|
}
|
|
1779
|
+
const suffix = sprintId ? sprintId.slice(0, 8) : Date.now().toString(36);
|
|
1780
|
+
this.branchName = `locus/${suffix}`;
|
|
1781
|
+
try {
|
|
1782
|
+
this.gitExec(["branch", "-D", this.branchName]);
|
|
1783
|
+
} catch {}
|
|
1784
|
+
this.gitExec(["checkout", "-b", this.branchName]);
|
|
1785
|
+
this.log(`Created branch: ${this.branchName} (from ${defaultBranch})`, "success");
|
|
1786
|
+
return this.branchName;
|
|
1860
1787
|
}
|
|
1861
|
-
|
|
1788
|
+
commitAndPush(task) {
|
|
1789
|
+
if (!this.branchName) {
|
|
1790
|
+
this.log("No branch created yet, skipping commit", "warn");
|
|
1791
|
+
return { branch: null, pushed: false, pushFailed: false };
|
|
1792
|
+
}
|
|
1862
1793
|
try {
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1794
|
+
const status = this.gitExec(["status", "--porcelain"]).trim();
|
|
1795
|
+
if (!status) {
|
|
1796
|
+
const baseBranchCommit = this.getBaseCommit();
|
|
1797
|
+
const headCommit = this.gitExec(["rev-parse", "HEAD"]).trim();
|
|
1798
|
+
if (baseBranchCommit && headCommit !== baseBranchCommit) {
|
|
1799
|
+
return this.pushBranch();
|
|
1800
|
+
}
|
|
1801
|
+
this.log("No changes to commit for this task", "info");
|
|
1802
|
+
return {
|
|
1803
|
+
branch: this.branchName,
|
|
1804
|
+
pushed: false,
|
|
1805
|
+
pushFailed: false,
|
|
1806
|
+
noChanges: true,
|
|
1807
|
+
skipReason: "No changes were made for this task."
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
this.gitExec(["add", "-A"]);
|
|
1811
|
+
const staged = this.gitExec(["diff", "--cached", "--name-only"]).trim();
|
|
1812
|
+
if (!staged) {
|
|
1813
|
+
this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
|
|
1814
|
+
return {
|
|
1815
|
+
branch: this.branchName,
|
|
1816
|
+
pushed: false,
|
|
1817
|
+
pushFailed: false,
|
|
1818
|
+
noChanges: true,
|
|
1819
|
+
skipReason: "All changes were ignored by .gitignore."
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
this.log(`Staging ${staged.split(`
|
|
1823
|
+
`).length} file(s) for commit`, "info");
|
|
1824
|
+
const trailers = [
|
|
1825
|
+
`Task-ID: ${task.id}`,
|
|
1826
|
+
`Agent: ${this.config.agentId}`,
|
|
1827
|
+
"Co-authored-by: LocusAI <agent@locusai.team>"
|
|
1828
|
+
];
|
|
1829
|
+
if (this.ghUsername) {
|
|
1830
|
+
trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
|
|
1831
|
+
}
|
|
1832
|
+
const commitMessage = `feat(agent): ${task.title}
|
|
1833
|
+
|
|
1834
|
+
${trailers.join(`
|
|
1835
|
+
`)}`;
|
|
1836
|
+
this.gitExec(["commit", "-m", commitMessage]);
|
|
1837
|
+
const hash = this.gitExec(["rev-parse", "HEAD"]).trim();
|
|
1838
|
+
this.log(`Committed: ${hash.slice(0, 8)}`, "success");
|
|
1839
|
+
return this.pushBranch();
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1842
|
+
this.log(`Git commit failed: ${errorMessage}`, "error");
|
|
1843
|
+
return {
|
|
1844
|
+
branch: this.branchName,
|
|
1845
|
+
pushed: false,
|
|
1846
|
+
pushFailed: true,
|
|
1847
|
+
pushError: `Git commit/push failed: ${errorMessage}`
|
|
1848
|
+
};
|
|
1870
1849
|
}
|
|
1871
1850
|
}
|
|
1872
|
-
|
|
1851
|
+
pushBranch() {
|
|
1852
|
+
if (!this.branchName) {
|
|
1853
|
+
return { branch: null, pushed: false, pushFailed: false };
|
|
1854
|
+
}
|
|
1873
1855
|
try {
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1856
|
+
this.gitExec(["push", "-u", "origin", this.branchName]);
|
|
1857
|
+
this.log(`Pushed ${this.branchName} to origin`, "success");
|
|
1858
|
+
return { branch: this.branchName, pushed: true, pushFailed: false };
|
|
1859
|
+
} catch (error) {
|
|
1860
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1861
|
+
if (msg.includes("non-fast-forward") || msg.includes("[rejected]") || msg.includes("fetch first")) {
|
|
1862
|
+
this.log(`Push rejected (non-fast-forward). Retrying with --force-with-lease.`, "warn");
|
|
1863
|
+
try {
|
|
1864
|
+
this.gitExec([
|
|
1865
|
+
"push",
|
|
1866
|
+
"--force-with-lease",
|
|
1867
|
+
"-u",
|
|
1868
|
+
"origin",
|
|
1869
|
+
this.branchName
|
|
1870
|
+
]);
|
|
1871
|
+
this.log(`Pushed ${this.branchName} to origin with --force-with-lease`, "success");
|
|
1872
|
+
return { branch: this.branchName, pushed: true, pushFailed: false };
|
|
1873
|
+
} catch (retryErr) {
|
|
1874
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
1875
|
+
this.log(`Git push retry failed: ${retryMsg}`, "error");
|
|
1876
|
+
return {
|
|
1877
|
+
branch: this.branchName,
|
|
1878
|
+
pushed: false,
|
|
1879
|
+
pushFailed: true,
|
|
1880
|
+
pushError: retryMsg
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
this.log(`Git push failed: ${msg}`, "error");
|
|
1885
|
+
return {
|
|
1886
|
+
branch: this.branchName,
|
|
1887
|
+
pushed: false,
|
|
1888
|
+
pushFailed: true,
|
|
1889
|
+
pushError: msg
|
|
1890
|
+
};
|
|
1881
1891
|
}
|
|
1882
1892
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1893
|
+
createPullRequest(completedTasks, summaries) {
|
|
1894
|
+
if (!this.branchName || !this.baseBranch) {
|
|
1895
|
+
return { url: null, error: "No branch or base branch available." };
|
|
1896
|
+
}
|
|
1897
|
+
const provider = detectRemoteProvider(this.projectPath);
|
|
1898
|
+
if (provider !== "github") {
|
|
1899
|
+
return {
|
|
1900
|
+
url: null,
|
|
1901
|
+
error: `PR creation is only supported for GitHub repositories (detected: ${provider})`
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
if (!isGhAvailable(this.projectPath)) {
|
|
1905
|
+
return {
|
|
1906
|
+
url: null,
|
|
1907
|
+
error: "GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/"
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
const title = `[Locus] Sprint tasks (${completedTasks.length} task${completedTasks.length !== 1 ? "s" : ""})`;
|
|
1911
|
+
const body = this.buildPrBody(completedTasks, summaries);
|
|
1912
|
+
this.log(`Creating PR: ${title} (${this.branchName} → ${this.baseBranch})`, "info");
|
|
1892
1913
|
try {
|
|
1893
|
-
import_node_child_process4.execFileSync("gh", [
|
|
1914
|
+
const output = import_node_child_process4.execFileSync("gh", [
|
|
1894
1915
|
"pr",
|
|
1895
|
-
"
|
|
1896
|
-
|
|
1916
|
+
"create",
|
|
1917
|
+
"--title",
|
|
1918
|
+
title,
|
|
1897
1919
|
"--body",
|
|
1898
1920
|
body,
|
|
1899
|
-
|
|
1921
|
+
"--base",
|
|
1922
|
+
this.baseBranch,
|
|
1923
|
+
"--head",
|
|
1924
|
+
this.branchName
|
|
1900
1925
|
], {
|
|
1901
1926
|
cwd: this.projectPath,
|
|
1902
1927
|
encoding: "utf-8",
|
|
1903
1928
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1904
|
-
});
|
|
1929
|
+
}).trim();
|
|
1930
|
+
this.log(`PR created: ${output}`, "success");
|
|
1931
|
+
return { url: output };
|
|
1905
1932
|
} catch (err) {
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
cwd: this.projectPath,
|
|
1910
|
-
encoding: "utf-8",
|
|
1911
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1912
|
-
});
|
|
1913
|
-
return;
|
|
1914
|
-
}
|
|
1915
|
-
throw err;
|
|
1933
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1934
|
+
this.log(`PR creation failed: ${errorMessage}`, "error");
|
|
1935
|
+
return { url: null, error: errorMessage };
|
|
1916
1936
|
}
|
|
1917
1937
|
}
|
|
1918
|
-
|
|
1938
|
+
checkoutBaseBranch() {
|
|
1939
|
+
if (!this.baseBranch)
|
|
1940
|
+
return;
|
|
1919
1941
|
try {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
"[Locus] in:title",
|
|
1925
|
-
"--state",
|
|
1926
|
-
"open",
|
|
1927
|
-
"--json",
|
|
1928
|
-
"number,title,url,headRefName"
|
|
1929
|
-
], {
|
|
1930
|
-
cwd: this.projectPath,
|
|
1931
|
-
encoding: "utf-8",
|
|
1932
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1933
|
-
}).trim();
|
|
1934
|
-
const prs = JSON.parse(output || "[]");
|
|
1935
|
-
return prs.map((pr) => ({
|
|
1936
|
-
number: pr.number,
|
|
1937
|
-
title: pr.title,
|
|
1938
|
-
url: pr.url,
|
|
1939
|
-
branch: pr.headRefName
|
|
1940
|
-
}));
|
|
1941
|
-
} catch {
|
|
1942
|
-
this.log("Failed to list Locus PRs", "warn");
|
|
1943
|
-
return [];
|
|
1942
|
+
this.gitExec(["checkout", this.baseBranch]);
|
|
1943
|
+
this.log(`Checked out base branch: ${this.baseBranch}`, "info");
|
|
1944
|
+
} catch (err) {
|
|
1945
|
+
this.log(`Could not checkout base branch: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
1944
1946
|
}
|
|
1945
1947
|
}
|
|
1946
|
-
|
|
1948
|
+
getBranchName() {
|
|
1949
|
+
return this.branchName;
|
|
1950
|
+
}
|
|
1951
|
+
getBaseBranch() {
|
|
1952
|
+
return this.baseBranch;
|
|
1953
|
+
}
|
|
1954
|
+
getBaseCommit() {
|
|
1955
|
+
if (!this.baseBranch)
|
|
1956
|
+
return null;
|
|
1947
1957
|
try {
|
|
1948
|
-
|
|
1949
|
-
cwd: this.projectPath,
|
|
1950
|
-
encoding: "utf-8",
|
|
1951
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1952
|
-
}).trim();
|
|
1953
|
-
const data = JSON.parse(output || "{}");
|
|
1954
|
-
return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
|
|
1958
|
+
return this.gitExec(["rev-parse", this.baseBranch]).trim();
|
|
1955
1959
|
} catch {
|
|
1956
|
-
return
|
|
1960
|
+
return null;
|
|
1957
1961
|
}
|
|
1958
1962
|
}
|
|
1959
|
-
|
|
1960
|
-
const allPrs = this.listLocusPrs();
|
|
1961
|
-
return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
|
|
1962
|
-
}
|
|
1963
|
-
buildPrBody(task, agentId, summary) {
|
|
1963
|
+
buildPrBody(completedTasks, summaries) {
|
|
1964
1964
|
const sections = [];
|
|
1965
|
-
sections.push(
|
|
1965
|
+
sections.push("## Completed Tasks");
|
|
1966
1966
|
sections.push("");
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
sections.push(
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
sections.push(`- [ ] ${item.text}`);
|
|
1967
|
+
for (let i = 0;i < completedTasks.length; i++) {
|
|
1968
|
+
const task = completedTasks[i];
|
|
1969
|
+
sections.push(`### ${i + 1}. ${task.title}`);
|
|
1970
|
+
sections.push(`Task ID: \`${task.id}\``);
|
|
1971
|
+
if (summaries[i]) {
|
|
1972
|
+
sections.push("");
|
|
1973
|
+
sections.push(summaries[i]);
|
|
1975
1974
|
}
|
|
1976
1975
|
sections.push("");
|
|
1977
1976
|
}
|
|
1978
|
-
if (summary) {
|
|
1979
|
-
sections.push("## Agent Summary");
|
|
1980
|
-
sections.push(summary);
|
|
1981
|
-
sections.push("");
|
|
1982
|
-
}
|
|
1983
1977
|
sections.push("---");
|
|
1984
|
-
sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}
|
|
1978
|
+
sections.push(`*Created by Locus Agent \`${this.config.agentId.slice(-8)}\`*`);
|
|
1985
1979
|
return sections.join(`
|
|
1986
1980
|
`);
|
|
1987
1981
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
var import_node_child_process4;
|
|
1994
|
-
var init_pr_service = __esm(() => {
|
|
1995
|
-
init_git_utils();
|
|
1996
|
-
import_node_child_process4 = require("node:child_process");
|
|
1997
|
-
});
|
|
1998
|
-
|
|
1999
|
-
// src/worktree/worktree-config.ts
|
|
2000
|
-
var WORKTREE_ROOT_DIR = ".locus-worktrees", WORKTREE_BRANCH_PREFIX = "agent", DEFAULT_WORKTREE_CONFIG;
|
|
2001
|
-
var init_worktree_config = __esm(() => {
|
|
2002
|
-
DEFAULT_WORKTREE_CONFIG = {
|
|
2003
|
-
rootDir: WORKTREE_ROOT_DIR,
|
|
2004
|
-
branchPrefix: WORKTREE_BRANCH_PREFIX,
|
|
2005
|
-
cleanupPolicy: "retain-on-failure"
|
|
2006
|
-
};
|
|
2007
|
-
});
|
|
2008
|
-
|
|
2009
|
-
// src/worktree/worktree-manager.ts
|
|
2010
|
-
class WorktreeManager {
|
|
2011
|
-
config;
|
|
2012
|
-
projectPath;
|
|
2013
|
-
log;
|
|
2014
|
-
constructor(projectPath, config, log) {
|
|
2015
|
-
this.projectPath = import_node_path6.resolve(projectPath);
|
|
2016
|
-
this.config = { ...DEFAULT_WORKTREE_CONFIG, ...config };
|
|
2017
|
-
this.log = log ?? ((_msg) => {
|
|
2018
|
-
return;
|
|
2019
|
-
});
|
|
2020
|
-
}
|
|
2021
|
-
get rootPath() {
|
|
2022
|
-
return import_node_path6.join(this.projectPath, this.config.rootDir);
|
|
2023
|
-
}
|
|
2024
|
-
buildBranchName(taskId, taskSlug) {
|
|
2025
|
-
const sanitized = taskSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
2026
|
-
return `${this.config.branchPrefix}/${taskId}-${sanitized}`;
|
|
2027
|
-
}
|
|
2028
|
-
create(options) {
|
|
2029
|
-
const branch = this.buildBranchName(options.taskId, options.taskSlug);
|
|
2030
|
-
const worktreeDir = `${options.agentId}-${options.taskId}`;
|
|
2031
|
-
const worktreePath = import_node_path6.join(this.rootPath, worktreeDir);
|
|
2032
|
-
this.ensureDirectory(this.rootPath, "Worktree root");
|
|
2033
|
-
const baseBranch = options.baseBranch ?? this.config.baseBranch ?? this.getCurrentBranch();
|
|
2034
|
-
if (!this.branchExists(baseBranch)) {
|
|
2035
|
-
this.log(`Base branch "${baseBranch}" not found locally, fetching from origin`, "info");
|
|
2036
|
-
try {
|
|
2037
|
-
this.gitExec(["fetch", "origin", baseBranch], this.projectPath);
|
|
2038
|
-
this.gitExec(["branch", baseBranch, `origin/${baseBranch}`], this.projectPath);
|
|
2039
|
-
} catch {
|
|
2040
|
-
this.log(`Could not fetch/create local branch for "${baseBranch}", falling back to current branch`, "warn");
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
this.log(`Creating worktree: ${worktreeDir} (branch: ${branch}, base: ${baseBranch})`, "info");
|
|
2044
|
-
if (import_node_fs4.existsSync(worktreePath)) {
|
|
2045
|
-
this.log(`Removing stale worktree directory: ${worktreePath}`, "warn");
|
|
2046
|
-
try {
|
|
2047
|
-
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
2048
|
-
} catch {
|
|
2049
|
-
import_node_fs4.rmSync(worktreePath, { recursive: true, force: true });
|
|
2050
|
-
this.git("worktree prune", this.projectPath);
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
if (this.branchExists(branch)) {
|
|
2054
|
-
this.log(`Deleting existing branch: ${branch}`, "warn");
|
|
2055
|
-
const branchWorktrees = this.list().filter((wt) => wt.branch === branch);
|
|
2056
|
-
for (const wt of branchWorktrees) {
|
|
2057
|
-
const worktreePath2 = import_node_path6.resolve(wt.path);
|
|
2058
|
-
if (wt.isMain || !this.isManagedWorktreePath(worktreePath2)) {
|
|
2059
|
-
throw new Error(`Branch "${branch}" is checked out at "${worktreePath2}". Remove or detach that worktree before retrying.`);
|
|
2060
|
-
}
|
|
2061
|
-
this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
|
|
2062
|
-
this.remove(worktreePath2, false);
|
|
2063
|
-
}
|
|
2064
|
-
try {
|
|
2065
|
-
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
2066
|
-
} catch {
|
|
2067
|
-
this.git("worktree prune", this.projectPath);
|
|
2068
|
-
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
const addWorktree = () => this.git(`worktree add "${worktreePath}" -b "${branch}" "${baseBranch}"`, this.projectPath);
|
|
2072
|
-
try {
|
|
2073
|
-
addWorktree();
|
|
2074
|
-
} catch (error) {
|
|
2075
|
-
if (!this.isMissingDirectoryError(error)) {
|
|
2076
|
-
throw error;
|
|
2077
|
-
}
|
|
2078
|
-
this.log(`Worktree creation failed due to missing directories. Retrying after cleanup: ${worktreePath}`, "warn");
|
|
2079
|
-
this.cleanupFailedWorktree(worktreePath, branch);
|
|
2080
|
-
this.ensureDirectory(this.rootPath, "Worktree root");
|
|
2081
|
-
addWorktree();
|
|
2082
|
-
}
|
|
2083
|
-
const baseCommitHash = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2084
|
-
this.log(`Worktree created at ${worktreePath} (base: ${baseCommitHash.slice(0, 8)})`, "success");
|
|
2085
|
-
return { worktreePath, branch, baseBranch, baseCommitHash };
|
|
2086
|
-
}
|
|
2087
|
-
list() {
|
|
2088
|
-
const output = this.git("worktree list --porcelain", this.projectPath);
|
|
2089
|
-
const worktrees = [];
|
|
2090
|
-
const blocks = output.trim().split(`
|
|
2091
|
-
|
|
2092
|
-
`);
|
|
2093
|
-
for (const block of blocks) {
|
|
2094
|
-
if (!block.trim())
|
|
2095
|
-
continue;
|
|
2096
|
-
const lines = block.trim().split(`
|
|
2097
|
-
`);
|
|
2098
|
-
let path = "";
|
|
2099
|
-
let head = "";
|
|
2100
|
-
let branch = "";
|
|
2101
|
-
let isMain = false;
|
|
2102
|
-
let isPrunable = false;
|
|
2103
|
-
for (const line of lines) {
|
|
2104
|
-
if (line.startsWith("worktree ")) {
|
|
2105
|
-
path = line.slice("worktree ".length);
|
|
2106
|
-
} else if (line.startsWith("HEAD ")) {
|
|
2107
|
-
head = line.slice("HEAD ".length);
|
|
2108
|
-
} else if (line.startsWith("branch ")) {
|
|
2109
|
-
branch = line.slice("branch ".length).replace("refs/heads/", "");
|
|
2110
|
-
} else if (line === "bare" || path === this.projectPath) {
|
|
2111
|
-
isMain = true;
|
|
2112
|
-
} else if (line === "prunable") {
|
|
2113
|
-
isPrunable = true;
|
|
2114
|
-
} else if (line === "detached") {
|
|
2115
|
-
branch = "(detached)";
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
if (import_node_path6.resolve(path) === this.projectPath) {
|
|
2119
|
-
isMain = true;
|
|
2120
|
-
}
|
|
2121
|
-
if (path) {
|
|
2122
|
-
worktrees.push({ path, branch, head, isMain, isPrunable });
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
return worktrees;
|
|
2126
|
-
}
|
|
2127
|
-
listAgentWorktrees() {
|
|
2128
|
-
return this.list().filter((wt) => !wt.isMain);
|
|
2129
|
-
}
|
|
2130
|
-
remove(worktreePath, deleteBranch = true) {
|
|
2131
|
-
const absolutePath = import_node_path6.resolve(worktreePath);
|
|
2132
|
-
const worktrees = this.list();
|
|
2133
|
-
const worktree = worktrees.find((wt) => import_node_path6.resolve(wt.path) === absolutePath);
|
|
2134
|
-
const branchToDelete = worktree?.branch;
|
|
2135
|
-
this.log(`Removing worktree: ${absolutePath}`, "info");
|
|
2136
|
-
try {
|
|
2137
|
-
this.git(`worktree remove "${absolutePath}" --force`, this.projectPath);
|
|
2138
|
-
} catch {
|
|
2139
|
-
if (import_node_fs4.existsSync(absolutePath)) {
|
|
2140
|
-
import_node_fs4.rmSync(absolutePath, { recursive: true, force: true });
|
|
2141
|
-
}
|
|
2142
|
-
this.git("worktree prune", this.projectPath);
|
|
2143
|
-
}
|
|
2144
|
-
if (deleteBranch && branchToDelete && !branchToDelete.startsWith("(")) {
|
|
2145
|
-
try {
|
|
2146
|
-
this.git(`branch -D "${branchToDelete}"`, this.projectPath);
|
|
2147
|
-
this.log(`Deleted branch: ${branchToDelete}`, "success");
|
|
2148
|
-
} catch {
|
|
2149
|
-
this.log(`Could not delete branch: ${branchToDelete} (may already be deleted)`, "warn");
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
this.log("Worktree removed", "success");
|
|
2153
|
-
}
|
|
2154
|
-
prune() {
|
|
2155
|
-
const before = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
2156
|
-
this.git("worktree prune", this.projectPath);
|
|
2157
|
-
const after = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
2158
|
-
const pruned = before - after;
|
|
2159
|
-
if (pruned > 0) {
|
|
2160
|
-
this.log(`Pruned ${pruned} stale worktree(s)`, "success");
|
|
2161
|
-
}
|
|
2162
|
-
return pruned;
|
|
2163
|
-
}
|
|
2164
|
-
removeAll() {
|
|
2165
|
-
const agentWorktrees = this.listAgentWorktrees();
|
|
2166
|
-
let removed = 0;
|
|
2167
|
-
for (const wt of agentWorktrees) {
|
|
2168
|
-
try {
|
|
2169
|
-
this.remove(wt.path, true);
|
|
2170
|
-
removed++;
|
|
2171
|
-
} catch {
|
|
2172
|
-
this.log(`Failed to remove worktree: ${wt.path}`, "warn");
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
if (import_node_fs4.existsSync(this.rootPath)) {
|
|
2176
|
-
try {
|
|
2177
|
-
import_node_fs4.rmSync(this.rootPath, { recursive: true, force: true });
|
|
2178
|
-
} catch {}
|
|
2179
|
-
}
|
|
2180
|
-
return removed;
|
|
2181
|
-
}
|
|
2182
|
-
hasChanges(worktreePath) {
|
|
2183
|
-
const status = this.git("status --porcelain", worktreePath).trim();
|
|
2184
|
-
return status.length > 0;
|
|
2185
|
-
}
|
|
2186
|
-
hasCommitsAhead(worktreePath, baseBranch) {
|
|
2187
|
-
try {
|
|
2188
|
-
const count = this.git(`rev-list --count "${baseBranch}..HEAD"`, worktreePath).trim();
|
|
2189
|
-
return Number.parseInt(count, 10) > 0;
|
|
2190
|
-
} catch (err) {
|
|
2191
|
-
this.log(`Could not compare HEAD against base branch "${baseBranch}": ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
2192
|
-
return false;
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
hasCommitsAheadOfHash(worktreePath, baseHash) {
|
|
2196
|
-
try {
|
|
2197
|
-
const headHash = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2198
|
-
return headHash !== baseHash;
|
|
2199
|
-
} catch {
|
|
2200
|
-
return false;
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
commitChanges(worktreePath, message, baseBranch, baseCommitHash) {
|
|
2204
|
-
const hasUncommittedChanges = this.hasChanges(worktreePath);
|
|
2205
|
-
if (hasUncommittedChanges) {
|
|
2206
|
-
const statusOutput = this.git("status --porcelain", worktreePath).trim();
|
|
2207
|
-
this.log(`Detected uncommitted changes:
|
|
2208
|
-
${statusOutput.split(`
|
|
2209
|
-
`).slice(0, 10).join(`
|
|
2210
|
-
`)}${statusOutput.split(`
|
|
2211
|
-
`).length > 10 ? `
|
|
2212
|
-
... and ${statusOutput.split(`
|
|
2213
|
-
`).length - 10} more` : ""}`, "info");
|
|
2214
|
-
}
|
|
2215
|
-
if (!hasUncommittedChanges) {
|
|
2216
|
-
if (baseBranch && this.hasCommitsAhead(worktreePath, baseBranch)) {
|
|
2217
|
-
const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2218
|
-
this.log(`Agent already committed changes (${hash2.slice(0, 8)}); skipping additional commit`, "info");
|
|
2219
|
-
return hash2;
|
|
2220
|
-
}
|
|
2221
|
-
if (baseCommitHash && this.hasCommitsAheadOfHash(worktreePath, baseCommitHash)) {
|
|
2222
|
-
const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2223
|
-
this.log(`Agent already committed changes (${hash2.slice(0, 8)}, detected via base commit hash); skipping additional commit`, "info");
|
|
2224
|
-
return hash2;
|
|
2225
|
-
}
|
|
2226
|
-
const branch = this.getBranch(worktreePath);
|
|
2227
|
-
this.log(`No changes detected in worktree (branch: ${branch}, baseBranch: ${baseBranch ?? "none"}, baseCommitHash: ${baseCommitHash?.slice(0, 8) ?? "none"})`, "warn");
|
|
2228
|
-
return null;
|
|
2229
|
-
}
|
|
2230
|
-
this.git("add -A", worktreePath);
|
|
2231
|
-
const staged = this.git("diff --cached --name-only", worktreePath).trim();
|
|
2232
|
-
if (!staged) {
|
|
2233
|
-
this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
|
|
2234
|
-
return null;
|
|
2235
|
-
}
|
|
2236
|
-
this.log(`Staging ${staged.split(`
|
|
2237
|
-
`).length} file(s) for commit`, "info");
|
|
2238
|
-
this.gitExec(["commit", "-m", message], worktreePath);
|
|
2239
|
-
const hash = this.git("rev-parse HEAD", worktreePath).trim();
|
|
2240
|
-
this.log(`Committed: ${hash.slice(0, 8)}`, "success");
|
|
2241
|
-
return hash;
|
|
2242
|
-
}
|
|
2243
|
-
pushBranch(worktreePath, remote = "origin") {
|
|
2244
|
-
const branch = this.getBranch(worktreePath);
|
|
2245
|
-
this.log(`Pushing branch ${branch} to ${remote}`, "info");
|
|
2246
|
-
try {
|
|
2247
|
-
this.gitExec(["push", "-u", remote, branch], worktreePath);
|
|
2248
|
-
this.log(`Pushed ${branch} to ${remote}`, "success");
|
|
2249
|
-
return branch;
|
|
2250
|
-
} catch (error) {
|
|
2251
|
-
if (!this.isNonFastForwardPushError(error)) {
|
|
2252
|
-
throw error;
|
|
2253
|
-
}
|
|
2254
|
-
this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
|
|
2255
|
-
try {
|
|
2256
|
-
this.gitExec(["fetch", remote, branch], worktreePath);
|
|
2257
|
-
} catch {}
|
|
2258
|
-
this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
|
|
2259
|
-
this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
|
|
2260
|
-
return branch;
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
getBranch(worktreePath) {
|
|
2264
|
-
return this.git("rev-parse --abbrev-ref HEAD", worktreePath).trim();
|
|
2265
|
-
}
|
|
2266
|
-
hasWorktreeForTask(taskId) {
|
|
2267
|
-
return this.listAgentWorktrees().some((wt) => wt.branch.includes(taskId) || wt.path.includes(taskId));
|
|
2268
|
-
}
|
|
2269
|
-
branchExists(branchName) {
|
|
2270
|
-
try {
|
|
2271
|
-
this.git(`rev-parse --verify "refs/heads/${branchName}"`, this.projectPath);
|
|
2272
|
-
return true;
|
|
2273
|
-
} catch {
|
|
2274
|
-
return false;
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
getCurrentBranch() {
|
|
2278
|
-
return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
|
|
2279
|
-
}
|
|
2280
|
-
isManagedWorktreePath(worktreePath) {
|
|
2281
|
-
const rootPath = import_node_path6.resolve(this.rootPath);
|
|
2282
|
-
const candidate = import_node_path6.resolve(worktreePath);
|
|
2283
|
-
const rootWithSep = rootPath.endsWith(import_node_path6.sep) ? rootPath : `${rootPath}${import_node_path6.sep}`;
|
|
2284
|
-
return candidate.startsWith(rootWithSep);
|
|
2285
|
-
}
|
|
2286
|
-
ensureDirectory(dirPath, label) {
|
|
2287
|
-
if (import_node_fs4.existsSync(dirPath)) {
|
|
2288
|
-
if (!import_node_fs4.statSync(dirPath).isDirectory()) {
|
|
2289
|
-
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
2290
|
-
}
|
|
2291
|
-
return;
|
|
2292
|
-
}
|
|
2293
|
-
import_node_fs4.mkdirSync(dirPath, { recursive: true });
|
|
2294
|
-
}
|
|
2295
|
-
isMissingDirectoryError(error) {
|
|
2296
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2297
|
-
return message.includes("cannot create directory") || message.includes("No such file or directory");
|
|
2298
|
-
}
|
|
2299
|
-
cleanupFailedWorktree(worktreePath, branch) {
|
|
2300
|
-
try {
|
|
2301
|
-
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
2302
|
-
} catch {}
|
|
2303
|
-
if (import_node_fs4.existsSync(worktreePath)) {
|
|
2304
|
-
import_node_fs4.rmSync(worktreePath, { recursive: true, force: true });
|
|
2305
|
-
}
|
|
2306
|
-
try {
|
|
2307
|
-
this.git("worktree prune", this.projectPath);
|
|
2308
|
-
} catch {}
|
|
2309
|
-
if (this.branchExists(branch)) {
|
|
2310
|
-
try {
|
|
2311
|
-
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
2312
|
-
} catch {}
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
isNonFastForwardPushError(error) {
|
|
2316
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2317
|
-
return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
|
|
2318
|
-
}
|
|
2319
|
-
git(args, cwd) {
|
|
2320
|
-
return import_node_child_process5.execSync(`git ${args}`, {
|
|
2321
|
-
cwd,
|
|
2322
|
-
encoding: "utf-8",
|
|
2323
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2324
|
-
});
|
|
2325
|
-
}
|
|
2326
|
-
gitExec(args, cwd) {
|
|
2327
|
-
return import_node_child_process5.execFileSync("git", args, {
|
|
2328
|
-
cwd,
|
|
1982
|
+
gitExec(args) {
|
|
1983
|
+
return import_node_child_process4.execFileSync("git", args, {
|
|
1984
|
+
cwd: this.projectPath,
|
|
2329
1985
|
encoding: "utf-8",
|
|
2330
1986
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2331
1987
|
});
|
|
2332
1988
|
}
|
|
2333
1989
|
}
|
|
2334
|
-
var
|
|
2335
|
-
var
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
import_node_fs4 = require("node:fs");
|
|
2339
|
-
import_node_path6 = require("node:path");
|
|
1990
|
+
var import_node_child_process4;
|
|
1991
|
+
var init_git_workflow = __esm(() => {
|
|
1992
|
+
init_git_utils();
|
|
1993
|
+
import_node_child_process4 = require("node:child_process");
|
|
2340
1994
|
});
|
|
2341
1995
|
|
|
2342
1996
|
// src/core/prompt-builder.ts
|
|
@@ -2380,9 +2034,9 @@ ${task.description || "No description provided."}
|
|
|
2380
2034
|
}
|
|
2381
2035
|
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
2382
2036
|
let hasLocalContext = false;
|
|
2383
|
-
if (
|
|
2037
|
+
if (import_node_fs4.existsSync(contextPath)) {
|
|
2384
2038
|
try {
|
|
2385
|
-
const context =
|
|
2039
|
+
const context = import_node_fs4.readFileSync(contextPath, "utf-8");
|
|
2386
2040
|
if (context.trim().length > 20) {
|
|
2387
2041
|
prompt += `## Project Context (Local)
|
|
2388
2042
|
${context}
|
|
@@ -2436,7 +2090,7 @@ ${serverContext.context}
|
|
|
2436
2090
|
|
|
2437
2091
|
`;
|
|
2438
2092
|
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
2439
|
-
if (
|
|
2093
|
+
if (import_node_fs4.existsSync(indexPath)) {
|
|
2440
2094
|
prompt += `## Codebase Overview
|
|
2441
2095
|
There is an index file in the .locus/codebase-index.json and if you need you can check it.
|
|
2442
2096
|
|
|
@@ -2513,9 +2167,9 @@ ${query}
|
|
|
2513
2167
|
}
|
|
2514
2168
|
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
2515
2169
|
let hasLocalContext = false;
|
|
2516
|
-
if (
|
|
2170
|
+
if (import_node_fs4.existsSync(contextPath)) {
|
|
2517
2171
|
try {
|
|
2518
|
-
const context =
|
|
2172
|
+
const context = import_node_fs4.readFileSync(contextPath, "utf-8");
|
|
2519
2173
|
if (context.trim().length > 20) {
|
|
2520
2174
|
prompt += `## Project Context (Local)
|
|
2521
2175
|
${context}
|
|
@@ -2549,7 +2203,7 @@ ${fallback}
|
|
|
2549
2203
|
|
|
2550
2204
|
`;
|
|
2551
2205
|
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
2552
|
-
if (
|
|
2206
|
+
if (import_node_fs4.existsSync(indexPath)) {
|
|
2553
2207
|
prompt += `## Codebase Overview
|
|
2554
2208
|
There is an index file in the .locus/codebase-index.json and if you need you can check it.
|
|
2555
2209
|
|
|
@@ -2564,9 +2218,9 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
2564
2218
|
}
|
|
2565
2219
|
getProjectConfig() {
|
|
2566
2220
|
const configPath = getLocusPath(this.projectPath, "configFile");
|
|
2567
|
-
if (
|
|
2221
|
+
if (import_node_fs4.existsSync(configPath)) {
|
|
2568
2222
|
try {
|
|
2569
|
-
return JSON.parse(
|
|
2223
|
+
return JSON.parse(import_node_fs4.readFileSync(configPath, "utf-8"));
|
|
2570
2224
|
} catch {
|
|
2571
2225
|
return null;
|
|
2572
2226
|
}
|
|
@@ -2574,10 +2228,10 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
2574
2228
|
return null;
|
|
2575
2229
|
}
|
|
2576
2230
|
getFallbackContext() {
|
|
2577
|
-
const readmePath =
|
|
2578
|
-
if (
|
|
2231
|
+
const readmePath = import_node_path6.join(this.projectPath, "README.md");
|
|
2232
|
+
if (import_node_fs4.existsSync(readmePath)) {
|
|
2579
2233
|
try {
|
|
2580
|
-
const content =
|
|
2234
|
+
const content = import_node_fs4.readFileSync(readmePath, "utf-8");
|
|
2581
2235
|
const limit = 1000;
|
|
2582
2236
|
return content.slice(0, limit) + (content.length > limit ? `
|
|
2583
2237
|
...(truncated)...` : "");
|
|
@@ -2589,12 +2243,12 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
2589
2243
|
}
|
|
2590
2244
|
getProjectStructure() {
|
|
2591
2245
|
try {
|
|
2592
|
-
const entries =
|
|
2246
|
+
const entries = import_node_fs4.readdirSync(this.projectPath);
|
|
2593
2247
|
const folders = entries.filter((e) => {
|
|
2594
2248
|
if (e.startsWith(".") || e === "node_modules")
|
|
2595
2249
|
return false;
|
|
2596
2250
|
try {
|
|
2597
|
-
return
|
|
2251
|
+
return import_node_fs4.statSync(import_node_path6.join(this.projectPath, e)).isDirectory();
|
|
2598
2252
|
} catch {
|
|
2599
2253
|
return false;
|
|
2600
2254
|
}
|
|
@@ -2635,11 +2289,11 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
2635
2289
|
}
|
|
2636
2290
|
}
|
|
2637
2291
|
}
|
|
2638
|
-
var
|
|
2292
|
+
var import_node_fs4, import_node_path6, import_shared2;
|
|
2639
2293
|
var init_prompt_builder = __esm(() => {
|
|
2640
2294
|
init_config();
|
|
2641
|
-
|
|
2642
|
-
|
|
2295
|
+
import_node_fs4 = require("node:fs");
|
|
2296
|
+
import_node_path6 = require("node:path");
|
|
2643
2297
|
import_shared2 = require("@locusai/shared");
|
|
2644
2298
|
});
|
|
2645
2299
|
|
|
@@ -2682,162 +2336,6 @@ var init_task_executor = __esm(() => {
|
|
|
2682
2336
|
init_prompt_builder();
|
|
2683
2337
|
});
|
|
2684
2338
|
|
|
2685
|
-
// src/agent/git-workflow.ts
|
|
2686
|
-
class GitWorkflow {
|
|
2687
|
-
config;
|
|
2688
|
-
log;
|
|
2689
|
-
ghUsername;
|
|
2690
|
-
worktreeManager;
|
|
2691
|
-
prService;
|
|
2692
|
-
constructor(config, log, ghUsername) {
|
|
2693
|
-
this.config = config;
|
|
2694
|
-
this.log = log;
|
|
2695
|
-
this.ghUsername = ghUsername;
|
|
2696
|
-
const projectPath = config.projectPath || process.cwd();
|
|
2697
|
-
this.worktreeManager = config.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }, log) : null;
|
|
2698
|
-
this.prService = config.autoPush ? new PrService(projectPath, log) : null;
|
|
2699
|
-
}
|
|
2700
|
-
createTaskWorktree(task, defaultExecutor) {
|
|
2701
|
-
if (!this.worktreeManager) {
|
|
2702
|
-
return {
|
|
2703
|
-
worktreePath: null,
|
|
2704
|
-
baseBranch: null,
|
|
2705
|
-
baseCommitHash: null,
|
|
2706
|
-
executor: defaultExecutor
|
|
2707
|
-
};
|
|
2708
|
-
}
|
|
2709
|
-
const slug = task.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
2710
|
-
const result = this.worktreeManager.create({
|
|
2711
|
-
taskId: task.id,
|
|
2712
|
-
taskSlug: slug,
|
|
2713
|
-
agentId: this.config.agentId,
|
|
2714
|
-
baseBranch: this.config.baseBranch
|
|
2715
|
-
});
|
|
2716
|
-
this.log(`Worktree created: ${result.worktreePath} (${result.branch})`, "info");
|
|
2717
|
-
const provider = this.config.provider ?? PROVIDER.CLAUDE;
|
|
2718
|
-
const taskAiRunner = createAiRunner(provider, {
|
|
2719
|
-
projectPath: result.worktreePath,
|
|
2720
|
-
model: this.config.model,
|
|
2721
|
-
log: this.log
|
|
2722
|
-
});
|
|
2723
|
-
const taskExecutor = new TaskExecutor({
|
|
2724
|
-
aiRunner: taskAiRunner,
|
|
2725
|
-
projectPath: result.worktreePath,
|
|
2726
|
-
log: this.log
|
|
2727
|
-
});
|
|
2728
|
-
return {
|
|
2729
|
-
worktreePath: result.worktreePath,
|
|
2730
|
-
baseBranch: result.baseBranch,
|
|
2731
|
-
baseCommitHash: result.baseCommitHash,
|
|
2732
|
-
executor: taskExecutor
|
|
2733
|
-
};
|
|
2734
|
-
}
|
|
2735
|
-
commitAndPush(worktreePath, task, baseBranch, baseCommitHash) {
|
|
2736
|
-
if (!this.worktreeManager) {
|
|
2737
|
-
return { branch: null, pushed: false, pushFailed: false };
|
|
2738
|
-
}
|
|
2739
|
-
try {
|
|
2740
|
-
const trailers = [
|
|
2741
|
-
`Task-ID: ${task.id}`,
|
|
2742
|
-
`Agent: ${this.config.agentId}`,
|
|
2743
|
-
"Co-authored-by: LocusAI <agent@locusai.team>"
|
|
2744
|
-
];
|
|
2745
|
-
if (this.ghUsername) {
|
|
2746
|
-
trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
|
|
2747
|
-
}
|
|
2748
|
-
const commitMessage = `feat(agent): ${task.title}
|
|
2749
|
-
|
|
2750
|
-
${trailers.join(`
|
|
2751
|
-
`)}`;
|
|
2752
|
-
const hash = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch, baseCommitHash);
|
|
2753
|
-
if (!hash) {
|
|
2754
|
-
this.log("No changes to commit for this task", "info");
|
|
2755
|
-
return {
|
|
2756
|
-
branch: null,
|
|
2757
|
-
pushed: false,
|
|
2758
|
-
pushFailed: false,
|
|
2759
|
-
noChanges: true,
|
|
2760
|
-
skipReason: "No changes were committed, so no branch was pushed."
|
|
2761
|
-
};
|
|
2762
|
-
}
|
|
2763
|
-
const localBranch = this.worktreeManager.getBranch(worktreePath);
|
|
2764
|
-
if (this.config.autoPush) {
|
|
2765
|
-
try {
|
|
2766
|
-
return {
|
|
2767
|
-
branch: this.worktreeManager.pushBranch(worktreePath),
|
|
2768
|
-
pushed: true,
|
|
2769
|
-
pushFailed: false
|
|
2770
|
-
};
|
|
2771
|
-
} catch (err) {
|
|
2772
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2773
|
-
this.log(`Git push failed: ${errorMessage}`, "error");
|
|
2774
|
-
return {
|
|
2775
|
-
branch: localBranch,
|
|
2776
|
-
pushed: false,
|
|
2777
|
-
pushFailed: true,
|
|
2778
|
-
pushError: errorMessage
|
|
2779
|
-
};
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
this.log("Auto-push disabled; skipping branch push", "info");
|
|
2783
|
-
return {
|
|
2784
|
-
branch: localBranch,
|
|
2785
|
-
pushed: false,
|
|
2786
|
-
pushFailed: false,
|
|
2787
|
-
skipReason: "Auto-push is disabled, so PR creation was skipped."
|
|
2788
|
-
};
|
|
2789
|
-
} catch (err) {
|
|
2790
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2791
|
-
this.log(`Git commit failed: ${errorMessage}`, "error");
|
|
2792
|
-
return {
|
|
2793
|
-
branch: null,
|
|
2794
|
-
pushed: false,
|
|
2795
|
-
pushFailed: true,
|
|
2796
|
-
pushError: `Git commit/push failed: ${errorMessage}`
|
|
2797
|
-
};
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
createPullRequest(task, branch, summary, baseBranch) {
|
|
2801
|
-
if (!this.prService) {
|
|
2802
|
-
const errorMessage = "PR service is not initialized. Enable auto-push to allow PR creation.";
|
|
2803
|
-
this.log(`PR creation skipped: ${errorMessage}`, "warn");
|
|
2804
|
-
return { url: null, error: errorMessage };
|
|
2805
|
-
}
|
|
2806
|
-
this.log(`Attempting PR creation from branch: ${branch}`, "info");
|
|
2807
|
-
try {
|
|
2808
|
-
const result = this.prService.createPr({
|
|
2809
|
-
task,
|
|
2810
|
-
branch,
|
|
2811
|
-
baseBranch,
|
|
2812
|
-
agentId: this.config.agentId,
|
|
2813
|
-
summary
|
|
2814
|
-
});
|
|
2815
|
-
return { url: result.url };
|
|
2816
|
-
} catch (err) {
|
|
2817
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2818
|
-
this.log(`PR creation failed: ${errorMessage}`, "error");
|
|
2819
|
-
return { url: null, error: errorMessage };
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
cleanupWorktree(worktreePath, keepBranch) {
|
|
2823
|
-
if (!this.worktreeManager || !worktreePath)
|
|
2824
|
-
return;
|
|
2825
|
-
try {
|
|
2826
|
-
this.worktreeManager.remove(worktreePath, !keepBranch);
|
|
2827
|
-
this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
|
|
2828
|
-
} catch {
|
|
2829
|
-
this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
var init_git_workflow = __esm(() => {
|
|
2834
|
-
init_factory();
|
|
2835
|
-
init_config();
|
|
2836
|
-
init_pr_service();
|
|
2837
|
-
init_worktree_manager();
|
|
2838
|
-
init_task_executor();
|
|
2839
|
-
});
|
|
2840
|
-
|
|
2841
2339
|
// src/agent/worker-cli.ts
|
|
2842
2340
|
var exports_worker_cli = {};
|
|
2843
2341
|
__export(exports_worker_cli, {
|
|
@@ -2870,16 +2368,8 @@ function parseWorkerArgs(argv) {
|
|
|
2870
2368
|
config.apiKey = args[++i];
|
|
2871
2369
|
else if (arg === "--project-path")
|
|
2872
2370
|
config.projectPath = args[++i];
|
|
2873
|
-
else if (arg === "--main-project-path")
|
|
2874
|
-
config.mainProjectPath = args[++i];
|
|
2875
2371
|
else if (arg === "--model")
|
|
2876
2372
|
config.model = args[++i];
|
|
2877
|
-
else if (arg === "--use-worktrees")
|
|
2878
|
-
config.useWorktrees = true;
|
|
2879
|
-
else if (arg === "--auto-push")
|
|
2880
|
-
config.autoPush = true;
|
|
2881
|
-
else if (arg === "--base-branch")
|
|
2882
|
-
config.baseBranch = args[++i];
|
|
2883
2373
|
else if (arg === "--provider") {
|
|
2884
2374
|
const value = args[i + 1];
|
|
2885
2375
|
if (value && !value.startsWith("--"))
|
|
@@ -2927,8 +2417,8 @@ class AgentWorker {
|
|
|
2927
2417
|
tasksCompleted = 0;
|
|
2928
2418
|
heartbeatInterval = null;
|
|
2929
2419
|
currentTaskId = null;
|
|
2930
|
-
|
|
2931
|
-
|
|
2420
|
+
completedTaskList = [];
|
|
2421
|
+
taskSummaries = [];
|
|
2932
2422
|
constructor(config) {
|
|
2933
2423
|
this.config = config;
|
|
2934
2424
|
const projectPath = config.projectPath || process.cwd();
|
|
@@ -2943,17 +2433,12 @@ class AgentWorker {
|
|
|
2943
2433
|
}
|
|
2944
2434
|
});
|
|
2945
2435
|
const log = this.log.bind(this);
|
|
2946
|
-
if (
|
|
2947
|
-
this.log("git is not installed —
|
|
2948
|
-
config.useWorktrees = false;
|
|
2436
|
+
if (!isGitAvailable()) {
|
|
2437
|
+
this.log("git is not installed — branch management will not work", "error");
|
|
2949
2438
|
}
|
|
2950
|
-
if (
|
|
2439
|
+
if (!isGhAvailable(projectPath)) {
|
|
2951
2440
|
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");
|
|
2952
2441
|
}
|
|
2953
|
-
const ghUsername = config.autoPush ? getGhUsername() : null;
|
|
2954
|
-
if (ghUsername) {
|
|
2955
|
-
this.log(`GitHub user: ${ghUsername}`, "info");
|
|
2956
|
-
}
|
|
2957
2442
|
const provider = config.provider ?? PROVIDER.CLAUDE;
|
|
2958
2443
|
this.aiRunner = createAiRunner(provider, {
|
|
2959
2444
|
projectPath,
|
|
@@ -2966,18 +2451,9 @@ class AgentWorker {
|
|
|
2966
2451
|
log
|
|
2967
2452
|
});
|
|
2968
2453
|
this.knowledgeBase = new KnowledgeBase(projectPath);
|
|
2969
|
-
this.gitWorkflow = new GitWorkflow(config, log
|
|
2454
|
+
this.gitWorkflow = new GitWorkflow(config, log);
|
|
2970
2455
|
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
2971
2456
|
this.log(`Using ${providerLabel} CLI for all phases`, "info");
|
|
2972
|
-
if (config.useWorktrees) {
|
|
2973
|
-
this.log("Per-task worktree isolation enabled", "info");
|
|
2974
|
-
if (config.baseBranch) {
|
|
2975
|
-
this.log(`Base branch for worktrees: ${config.baseBranch}`, "info");
|
|
2976
|
-
}
|
|
2977
|
-
if (config.autoPush) {
|
|
2978
|
-
this.log("Auto-push enabled: branches will be pushed to remote", "info");
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
2457
|
}
|
|
2982
2458
|
log(message, level = "info") {
|
|
2983
2459
|
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
@@ -3026,56 +2502,26 @@ class AgentWorker {
|
|
|
3026
2502
|
}
|
|
3027
2503
|
async executeTask(task) {
|
|
3028
2504
|
const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
|
|
3029
|
-
const { worktreePath, baseBranch, baseCommitHash, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
|
|
3030
|
-
this.currentWorktreePath = worktreePath;
|
|
3031
|
-
let branchPushed = false;
|
|
3032
|
-
let keepBranch = false;
|
|
3033
|
-
let preserveWorktree = false;
|
|
3034
2505
|
try {
|
|
3035
|
-
const result = await
|
|
3036
|
-
let taskBranch = null;
|
|
3037
|
-
let prUrl = null;
|
|
3038
|
-
let prError = null;
|
|
2506
|
+
const result = await this.taskExecutor.execute(fullTask);
|
|
3039
2507
|
let noChanges = false;
|
|
3040
|
-
|
|
3041
|
-
|
|
2508
|
+
let taskBranch = null;
|
|
2509
|
+
if (result.success) {
|
|
2510
|
+
const commitResult = this.gitWorkflow.commitAndPush(fullTask);
|
|
3042
2511
|
taskBranch = commitResult.branch;
|
|
3043
|
-
branchPushed = commitResult.pushed;
|
|
3044
|
-
keepBranch = taskBranch !== null;
|
|
3045
2512
|
noChanges = Boolean(commitResult.noChanges);
|
|
3046
|
-
if (commitResult.pushFailed) {
|
|
3047
|
-
preserveWorktree = true;
|
|
3048
|
-
prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
|
|
3049
|
-
this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
|
|
3050
|
-
}
|
|
3051
|
-
if (branchPushed && taskBranch) {
|
|
3052
|
-
const prResult = this.gitWorkflow.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
|
|
3053
|
-
prUrl = prResult.url;
|
|
3054
|
-
prError = prResult.error ?? null;
|
|
3055
|
-
if (!prUrl) {
|
|
3056
|
-
preserveWorktree = true;
|
|
3057
|
-
this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
|
|
3058
|
-
}
|
|
3059
|
-
} else if (commitResult.skipReason) {
|
|
3060
|
-
this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
|
|
3061
|
-
}
|
|
3062
|
-
} else if (result.success && !worktreePath) {
|
|
3063
|
-
this.log("Skipping commit/push/PR flow because no task worktree is active.", "warn");
|
|
3064
2513
|
}
|
|
3065
2514
|
return {
|
|
3066
2515
|
...result,
|
|
3067
2516
|
branch: taskBranch ?? undefined,
|
|
3068
|
-
prUrl: prUrl ?? undefined,
|
|
3069
|
-
prError: prError ?? undefined,
|
|
3070
2517
|
noChanges: noChanges || undefined
|
|
3071
2518
|
};
|
|
3072
|
-
}
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
}
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2521
|
+
return {
|
|
2522
|
+
success: false,
|
|
2523
|
+
summary: `Execution error: ${msg}`
|
|
2524
|
+
};
|
|
3079
2525
|
}
|
|
3080
2526
|
}
|
|
3081
2527
|
updateProgress(task, summary) {
|
|
@@ -3108,19 +2554,13 @@ class AgentWorker {
|
|
|
3108
2554
|
this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
3109
2555
|
});
|
|
3110
2556
|
}
|
|
3111
|
-
async delayAfterCleanup() {
|
|
3112
|
-
if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
|
|
3113
|
-
return;
|
|
3114
|
-
this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
|
|
3115
|
-
await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
|
|
3116
|
-
}
|
|
3117
2557
|
async run() {
|
|
3118
2558
|
this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
3119
2559
|
const handleShutdown = () => {
|
|
3120
2560
|
this.log("Received shutdown signal. Aborting...", "warn");
|
|
3121
2561
|
this.aiRunner.abort();
|
|
3122
2562
|
this.stopHeartbeat();
|
|
3123
|
-
this.gitWorkflow.
|
|
2563
|
+
this.gitWorkflow.checkoutBaseBranch();
|
|
3124
2564
|
process.exit(1);
|
|
3125
2565
|
};
|
|
3126
2566
|
process.on("SIGTERM", handleShutdown);
|
|
@@ -3132,10 +2572,12 @@ class AgentWorker {
|
|
|
3132
2572
|
} else {
|
|
3133
2573
|
this.log("No active sprint found.", "warn");
|
|
3134
2574
|
}
|
|
2575
|
+
const branchName = this.gitWorkflow.createBranch(this.config.sprintId);
|
|
2576
|
+
this.log(`Working on branch: ${branchName}`, "info");
|
|
3135
2577
|
while (this.tasksCompleted < this.maxTasks) {
|
|
3136
2578
|
const task = await this.getNextTask();
|
|
3137
2579
|
if (!task) {
|
|
3138
|
-
this.log("No more tasks to process.
|
|
2580
|
+
this.log("No more tasks to process.", "info");
|
|
3139
2581
|
break;
|
|
3140
2582
|
}
|
|
3141
2583
|
this.log(`Claimed: ${task.title}`, "success");
|
|
@@ -3151,7 +2593,7 @@ class AgentWorker {
|
|
|
3151
2593
|
});
|
|
3152
2594
|
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
3153
2595
|
author: this.config.agentId,
|
|
3154
|
-
text: `⚠️ Agent execution finished with no file changes, so no commit
|
|
2596
|
+
text: `⚠️ Agent execution finished with no file changes, so no commit was created.
|
|
3155
2597
|
|
|
3156
2598
|
${result.summary}`
|
|
3157
2599
|
});
|
|
@@ -3160,22 +2602,17 @@ ${result.summary}`
|
|
|
3160
2602
|
const updatePayload = {
|
|
3161
2603
|
status: import_shared3.TaskStatus.IN_REVIEW
|
|
3162
2604
|
};
|
|
3163
|
-
if (result.prUrl) {
|
|
3164
|
-
updatePayload.prUrl = result.prUrl;
|
|
3165
|
-
}
|
|
3166
2605
|
await this.client.tasks.update(task.id, this.config.workspaceId, updatePayload);
|
|
3167
2606
|
const branchInfo = result.branch ? `
|
|
3168
2607
|
|
|
3169
2608
|
Branch: \`${result.branch}\`` : "";
|
|
3170
|
-
const prInfo = result.prUrl ? `
|
|
3171
|
-
PR: ${result.prUrl}` : "";
|
|
3172
|
-
const prErrorInfo = result.prError ? `
|
|
3173
|
-
PR automation error: ${result.prError}` : "";
|
|
3174
2609
|
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
3175
2610
|
author: this.config.agentId,
|
|
3176
|
-
text: `✅ ${result.summary}${branchInfo}
|
|
2611
|
+
text: `✅ ${result.summary}${branchInfo}`
|
|
3177
2612
|
});
|
|
3178
2613
|
this.tasksCompleted++;
|
|
2614
|
+
this.completedTaskList.push({ title: task.title, id: task.id });
|
|
2615
|
+
this.taskSummaries.push(result.summary);
|
|
3179
2616
|
this.updateProgress(task, result.summary);
|
|
3180
2617
|
}
|
|
3181
2618
|
} else {
|
|
@@ -3191,8 +2628,24 @@ PR automation error: ${result.prError}` : "";
|
|
|
3191
2628
|
}
|
|
3192
2629
|
this.currentTaskId = null;
|
|
3193
2630
|
this.sendHeartbeat();
|
|
3194
|
-
await this.delayAfterCleanup();
|
|
3195
2631
|
}
|
|
2632
|
+
if (this.completedTaskList.length > 0) {
|
|
2633
|
+
this.log("All tasks done. Creating pull request...", "info");
|
|
2634
|
+
const prResult = this.gitWorkflow.createPullRequest(this.completedTaskList, this.taskSummaries);
|
|
2635
|
+
if (prResult.url) {
|
|
2636
|
+
this.log(`PR created: ${prResult.url}`, "success");
|
|
2637
|
+
for (const task of this.completedTaskList) {
|
|
2638
|
+
try {
|
|
2639
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
2640
|
+
prUrl: prResult.url
|
|
2641
|
+
});
|
|
2642
|
+
} catch {}
|
|
2643
|
+
}
|
|
2644
|
+
} else if (prResult.error) {
|
|
2645
|
+
this.log(`PR creation failed: ${prResult.error}`, "error");
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
this.gitWorkflow.checkoutBaseBranch();
|
|
3196
2649
|
this.currentTaskId = null;
|
|
3197
2650
|
this.stopHeartbeat();
|
|
3198
2651
|
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
@@ -3239,10 +2692,7 @@ __export(exports_index_node, {
|
|
|
3239
2692
|
detectRemoteProvider: () => detectRemoteProvider,
|
|
3240
2693
|
createAiRunner: () => createAiRunner,
|
|
3241
2694
|
c: () => c,
|
|
3242
|
-
WorktreeManager: () => WorktreeManager,
|
|
3243
2695
|
WorkspacesModule: () => WorkspacesModule,
|
|
3244
|
-
WORKTREE_ROOT_DIR: () => WORKTREE_ROOT_DIR,
|
|
3245
|
-
WORKTREE_BRANCH_PREFIX: () => WORKTREE_BRANCH_PREFIX,
|
|
3246
2696
|
TasksModule: () => TasksModule,
|
|
3247
2697
|
TaskExecutor: () => TaskExecutor,
|
|
3248
2698
|
SprintsModule: () => SprintsModule,
|
|
@@ -3270,7 +2720,6 @@ __export(exports_index_node, {
|
|
|
3270
2720
|
ExecEventEmitter: () => ExecEventEmitter,
|
|
3271
2721
|
DocumentFetcher: () => DocumentFetcher,
|
|
3272
2722
|
DocsModule: () => DocsModule,
|
|
3273
|
-
DEFAULT_WORKTREE_CONFIG: () => DEFAULT_WORKTREE_CONFIG,
|
|
3274
2723
|
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
3275
2724
|
ContextTracker: () => ContextTracker,
|
|
3276
2725
|
CodexRunner: () => CodexRunner,
|
|
@@ -3286,8 +2735,8 @@ module.exports = __toCommonJS(exports_index_node);
|
|
|
3286
2735
|
|
|
3287
2736
|
// src/core/indexer.ts
|
|
3288
2737
|
var import_node_crypto2 = require("node:crypto");
|
|
3289
|
-
var
|
|
3290
|
-
var
|
|
2738
|
+
var import_node_fs5 = require("node:fs");
|
|
2739
|
+
var import_node_path7 = require("node:path");
|
|
3291
2740
|
var import_globby = require("globby");
|
|
3292
2741
|
|
|
3293
2742
|
class CodebaseIndexer {
|
|
@@ -3296,7 +2745,7 @@ class CodebaseIndexer {
|
|
|
3296
2745
|
fullReindexRatioThreshold = 0.2;
|
|
3297
2746
|
constructor(projectPath) {
|
|
3298
2747
|
this.projectPath = projectPath;
|
|
3299
|
-
this.indexPath =
|
|
2748
|
+
this.indexPath = import_node_path7.join(projectPath, ".locus", "codebase-index.json");
|
|
3300
2749
|
}
|
|
3301
2750
|
async index(onProgress, treeSummarizer, force = false) {
|
|
3302
2751
|
if (!treeSummarizer) {
|
|
@@ -3352,11 +2801,11 @@ class CodebaseIndexer {
|
|
|
3352
2801
|
}
|
|
3353
2802
|
}
|
|
3354
2803
|
async getFileTree() {
|
|
3355
|
-
const gitmodulesPath =
|
|
2804
|
+
const gitmodulesPath = import_node_path7.join(this.projectPath, ".gitmodules");
|
|
3356
2805
|
const submoduleIgnores = [];
|
|
3357
|
-
if (
|
|
2806
|
+
if (import_node_fs5.existsSync(gitmodulesPath)) {
|
|
3358
2807
|
try {
|
|
3359
|
-
const content =
|
|
2808
|
+
const content = import_node_fs5.readFileSync(gitmodulesPath, "utf-8");
|
|
3360
2809
|
const lines = content.split(`
|
|
3361
2810
|
`);
|
|
3362
2811
|
for (const line of lines) {
|
|
@@ -3412,9 +2861,9 @@ class CodebaseIndexer {
|
|
|
3412
2861
|
});
|
|
3413
2862
|
}
|
|
3414
2863
|
loadIndex() {
|
|
3415
|
-
if (
|
|
2864
|
+
if (import_node_fs5.existsSync(this.indexPath)) {
|
|
3416
2865
|
try {
|
|
3417
|
-
return JSON.parse(
|
|
2866
|
+
return JSON.parse(import_node_fs5.readFileSync(this.indexPath, "utf-8"));
|
|
3418
2867
|
} catch {
|
|
3419
2868
|
return null;
|
|
3420
2869
|
}
|
|
@@ -3422,11 +2871,11 @@ class CodebaseIndexer {
|
|
|
3422
2871
|
return null;
|
|
3423
2872
|
}
|
|
3424
2873
|
saveIndex(index) {
|
|
3425
|
-
const dir =
|
|
3426
|
-
if (!
|
|
3427
|
-
|
|
2874
|
+
const dir = import_node_path7.dirname(this.indexPath);
|
|
2875
|
+
if (!import_node_fs5.existsSync(dir)) {
|
|
2876
|
+
import_node_fs5.mkdirSync(dir, { recursive: true });
|
|
3428
2877
|
}
|
|
3429
|
-
|
|
2878
|
+
import_node_fs5.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
3430
2879
|
}
|
|
3431
2880
|
cloneIndex(index) {
|
|
3432
2881
|
return JSON.parse(JSON.stringify(index));
|
|
@@ -3442,7 +2891,7 @@ class CodebaseIndexer {
|
|
|
3442
2891
|
}
|
|
3443
2892
|
hashFile(filePath) {
|
|
3444
2893
|
try {
|
|
3445
|
-
const content =
|
|
2894
|
+
const content = import_node_fs5.readFileSync(import_node_path7.join(this.projectPath, filePath), "utf-8");
|
|
3446
2895
|
return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
3447
2896
|
} catch {
|
|
3448
2897
|
return null;
|
|
@@ -3593,8 +3042,8 @@ Return ONLY valid JSON, no markdown formatting.`;
|
|
|
3593
3042
|
}
|
|
3594
3043
|
// src/agent/document-fetcher.ts
|
|
3595
3044
|
init_config();
|
|
3596
|
-
var
|
|
3597
|
-
var
|
|
3045
|
+
var import_node_fs6 = require("node:fs");
|
|
3046
|
+
var import_node_path8 = require("node:path");
|
|
3598
3047
|
|
|
3599
3048
|
class DocumentFetcher {
|
|
3600
3049
|
deps;
|
|
@@ -3603,8 +3052,8 @@ class DocumentFetcher {
|
|
|
3603
3052
|
}
|
|
3604
3053
|
async fetch() {
|
|
3605
3054
|
const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
|
|
3606
|
-
if (!
|
|
3607
|
-
|
|
3055
|
+
if (!import_node_fs6.existsSync(documentsDir)) {
|
|
3056
|
+
import_node_fs6.mkdirSync(documentsDir, { recursive: true });
|
|
3608
3057
|
}
|
|
3609
3058
|
try {
|
|
3610
3059
|
const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
|
|
@@ -3617,14 +3066,14 @@ class DocumentFetcher {
|
|
|
3617
3066
|
continue;
|
|
3618
3067
|
}
|
|
3619
3068
|
const groupName = groupMap.get(doc.groupId || "") || "General";
|
|
3620
|
-
const groupDir =
|
|
3621
|
-
if (!
|
|
3622
|
-
|
|
3069
|
+
const groupDir = import_node_path8.join(documentsDir, groupName);
|
|
3070
|
+
if (!import_node_fs6.existsSync(groupDir)) {
|
|
3071
|
+
import_node_fs6.mkdirSync(groupDir, { recursive: true });
|
|
3623
3072
|
}
|
|
3624
3073
|
const fileName = `${doc.title}.md`;
|
|
3625
|
-
const filePath =
|
|
3626
|
-
if (!
|
|
3627
|
-
|
|
3074
|
+
const filePath = import_node_path8.join(groupDir, fileName);
|
|
3075
|
+
if (!import_node_fs6.existsSync(filePath) || import_node_fs6.readFileSync(filePath, "utf-8") !== doc.content) {
|
|
3076
|
+
import_node_fs6.writeFileSync(filePath, doc.content || "");
|
|
3628
3077
|
fetchedCount++;
|
|
3629
3078
|
}
|
|
3630
3079
|
}
|
|
@@ -3642,7 +3091,7 @@ class DocumentFetcher {
|
|
|
3642
3091
|
init_git_workflow();
|
|
3643
3092
|
|
|
3644
3093
|
// src/agent/review-service.ts
|
|
3645
|
-
var
|
|
3094
|
+
var import_node_child_process5 = require("node:child_process");
|
|
3646
3095
|
|
|
3647
3096
|
class ReviewService {
|
|
3648
3097
|
deps;
|
|
@@ -3652,7 +3101,7 @@ class ReviewService {
|
|
|
3652
3101
|
async reviewStagedChanges(sprint) {
|
|
3653
3102
|
const { projectPath, log } = this.deps;
|
|
3654
3103
|
try {
|
|
3655
|
-
|
|
3104
|
+
import_node_child_process5.execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
|
|
3656
3105
|
log("Staged all changes for review.", "info");
|
|
3657
3106
|
} catch (err) {
|
|
3658
3107
|
log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
@@ -3660,7 +3109,7 @@ class ReviewService {
|
|
|
3660
3109
|
}
|
|
3661
3110
|
let diff;
|
|
3662
3111
|
try {
|
|
3663
|
-
diff =
|
|
3112
|
+
diff = import_node_child_process5.execSync("git diff --cached --stat && echo '---' && git diff --cached", {
|
|
3664
3113
|
cwd: projectPath,
|
|
3665
3114
|
maxBuffer: 10 * 1024 * 1024
|
|
3666
3115
|
}).toString();
|
|
@@ -3703,10 +3152,245 @@ Keep the review concise but thorough. Focus on substance over style.`;
|
|
|
3703
3152
|
init_factory();
|
|
3704
3153
|
init_config();
|
|
3705
3154
|
init_git_utils();
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3155
|
+
|
|
3156
|
+
// src/git/pr-service.ts
|
|
3157
|
+
init_git_utils();
|
|
3158
|
+
var import_node_child_process6 = require("node:child_process");
|
|
3159
|
+
|
|
3160
|
+
class PrService {
|
|
3161
|
+
projectPath;
|
|
3162
|
+
log;
|
|
3163
|
+
constructor(projectPath, log) {
|
|
3164
|
+
this.projectPath = projectPath;
|
|
3165
|
+
this.log = log;
|
|
3166
|
+
}
|
|
3167
|
+
createPr(options) {
|
|
3168
|
+
const {
|
|
3169
|
+
task,
|
|
3170
|
+
branch,
|
|
3171
|
+
baseBranch: requestedBaseBranch,
|
|
3172
|
+
agentId,
|
|
3173
|
+
summary
|
|
3174
|
+
} = options;
|
|
3175
|
+
const provider = detectRemoteProvider(this.projectPath);
|
|
3176
|
+
if (provider !== "github") {
|
|
3177
|
+
throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
|
|
3178
|
+
}
|
|
3179
|
+
if (!isGhAvailable(this.projectPath)) {
|
|
3180
|
+
throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
|
|
3181
|
+
}
|
|
3182
|
+
const title = `[Locus] ${task.title}`;
|
|
3183
|
+
const body = this.buildPrBody(task, agentId, summary);
|
|
3184
|
+
const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
|
|
3185
|
+
this.validateCreatePrInputs(baseBranch, branch);
|
|
3186
|
+
this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
|
|
3187
|
+
const output = import_node_child_process6.execFileSync("gh", [
|
|
3188
|
+
"pr",
|
|
3189
|
+
"create",
|
|
3190
|
+
"--title",
|
|
3191
|
+
title,
|
|
3192
|
+
"--body",
|
|
3193
|
+
body,
|
|
3194
|
+
"--base",
|
|
3195
|
+
baseBranch,
|
|
3196
|
+
"--head",
|
|
3197
|
+
branch
|
|
3198
|
+
], {
|
|
3199
|
+
cwd: this.projectPath,
|
|
3200
|
+
encoding: "utf-8",
|
|
3201
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3202
|
+
}).trim();
|
|
3203
|
+
const url = output;
|
|
3204
|
+
const prNumber = this.extractPrNumber(url);
|
|
3205
|
+
this.log(`PR created: ${url}`, "success");
|
|
3206
|
+
return { url, number: prNumber };
|
|
3207
|
+
}
|
|
3208
|
+
validateCreatePrInputs(baseBranch, headBranch) {
|
|
3209
|
+
if (!this.hasRemoteBranch(baseBranch)) {
|
|
3210
|
+
throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
|
|
3211
|
+
}
|
|
3212
|
+
if (!this.hasRemoteBranch(headBranch)) {
|
|
3213
|
+
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
3214
|
+
}
|
|
3215
|
+
const baseRef = this.resolveBranchRef(baseBranch);
|
|
3216
|
+
const headRef = this.resolveBranchRef(headBranch);
|
|
3217
|
+
if (!baseRef) {
|
|
3218
|
+
throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
|
|
3219
|
+
}
|
|
3220
|
+
if (!headRef) {
|
|
3221
|
+
throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
|
|
3222
|
+
}
|
|
3223
|
+
const commitsAhead = this.countCommitsAhead(baseRef, headRef);
|
|
3224
|
+
if (commitsAhead <= 0) {
|
|
3225
|
+
throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
countCommitsAhead(baseRef, headRef) {
|
|
3229
|
+
const output = import_node_child_process6.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
3230
|
+
cwd: this.projectPath,
|
|
3231
|
+
encoding: "utf-8",
|
|
3232
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3233
|
+
}).trim();
|
|
3234
|
+
const value = Number.parseInt(output || "0", 10);
|
|
3235
|
+
return Number.isNaN(value) ? 0 : value;
|
|
3236
|
+
}
|
|
3237
|
+
resolveBranchRef(branch) {
|
|
3238
|
+
if (this.hasLocalBranch(branch)) {
|
|
3239
|
+
return branch;
|
|
3240
|
+
}
|
|
3241
|
+
if (this.hasRemoteTrackingBranch(branch)) {
|
|
3242
|
+
return `origin/${branch}`;
|
|
3243
|
+
}
|
|
3244
|
+
return null;
|
|
3245
|
+
}
|
|
3246
|
+
hasLocalBranch(branch) {
|
|
3247
|
+
try {
|
|
3248
|
+
import_node_child_process6.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
3249
|
+
cwd: this.projectPath,
|
|
3250
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3251
|
+
});
|
|
3252
|
+
return true;
|
|
3253
|
+
} catch {
|
|
3254
|
+
return false;
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
hasRemoteTrackingBranch(branch) {
|
|
3258
|
+
try {
|
|
3259
|
+
import_node_child_process6.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], {
|
|
3260
|
+
cwd: this.projectPath,
|
|
3261
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3262
|
+
});
|
|
3263
|
+
return true;
|
|
3264
|
+
} catch {
|
|
3265
|
+
return false;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
hasRemoteBranch(branch) {
|
|
3269
|
+
try {
|
|
3270
|
+
import_node_child_process6.execFileSync("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
|
|
3271
|
+
cwd: this.projectPath,
|
|
3272
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3273
|
+
});
|
|
3274
|
+
return true;
|
|
3275
|
+
} catch {
|
|
3276
|
+
return false;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
getPrDiff(branch) {
|
|
3280
|
+
return import_node_child_process6.execFileSync("gh", ["pr", "diff", branch], {
|
|
3281
|
+
cwd: this.projectPath,
|
|
3282
|
+
encoding: "utf-8",
|
|
3283
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3284
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
submitReview(prIdentifier, body, event) {
|
|
3288
|
+
try {
|
|
3289
|
+
import_node_child_process6.execFileSync("gh", [
|
|
3290
|
+
"pr",
|
|
3291
|
+
"review",
|
|
3292
|
+
prIdentifier,
|
|
3293
|
+
"--body",
|
|
3294
|
+
body,
|
|
3295
|
+
`--${event.toLowerCase().replace("_", "-")}`
|
|
3296
|
+
], {
|
|
3297
|
+
cwd: this.projectPath,
|
|
3298
|
+
encoding: "utf-8",
|
|
3299
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3300
|
+
});
|
|
3301
|
+
} catch (err) {
|
|
3302
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3303
|
+
if (event === "REQUEST_CHANGES" && msg.includes("own pull request")) {
|
|
3304
|
+
import_node_child_process6.execFileSync("gh", ["pr", "review", prIdentifier, "--body", body, "--comment"], {
|
|
3305
|
+
cwd: this.projectPath,
|
|
3306
|
+
encoding: "utf-8",
|
|
3307
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3308
|
+
});
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
throw err;
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
listLocusPrs() {
|
|
3315
|
+
try {
|
|
3316
|
+
const output = import_node_child_process6.execFileSync("gh", [
|
|
3317
|
+
"pr",
|
|
3318
|
+
"list",
|
|
3319
|
+
"--search",
|
|
3320
|
+
"[Locus] in:title",
|
|
3321
|
+
"--state",
|
|
3322
|
+
"open",
|
|
3323
|
+
"--json",
|
|
3324
|
+
"number,title,url,headRefName"
|
|
3325
|
+
], {
|
|
3326
|
+
cwd: this.projectPath,
|
|
3327
|
+
encoding: "utf-8",
|
|
3328
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3329
|
+
}).trim();
|
|
3330
|
+
const prs = JSON.parse(output || "[]");
|
|
3331
|
+
return prs.map((pr) => ({
|
|
3332
|
+
number: pr.number,
|
|
3333
|
+
title: pr.title,
|
|
3334
|
+
url: pr.url,
|
|
3335
|
+
branch: pr.headRefName
|
|
3336
|
+
}));
|
|
3337
|
+
} catch {
|
|
3338
|
+
this.log("Failed to list Locus PRs", "warn");
|
|
3339
|
+
return [];
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
hasLocusReview(prNumber) {
|
|
3343
|
+
try {
|
|
3344
|
+
const output = import_node_child_process6.execFileSync("gh", ["pr", "view", prNumber, "--json", "reviews"], {
|
|
3345
|
+
cwd: this.projectPath,
|
|
3346
|
+
encoding: "utf-8",
|
|
3347
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3348
|
+
}).trim();
|
|
3349
|
+
const data = JSON.parse(output || "{}");
|
|
3350
|
+
return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
|
|
3351
|
+
} catch {
|
|
3352
|
+
return false;
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
listUnreviewedLocusPrs() {
|
|
3356
|
+
const allPrs = this.listLocusPrs();
|
|
3357
|
+
return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
|
|
3358
|
+
}
|
|
3359
|
+
buildPrBody(task, agentId, summary) {
|
|
3360
|
+
const sections = [];
|
|
3361
|
+
sections.push(`## Task: ${task.title}`);
|
|
3362
|
+
sections.push("");
|
|
3363
|
+
if (task.description) {
|
|
3364
|
+
sections.push(task.description);
|
|
3365
|
+
sections.push("");
|
|
3366
|
+
}
|
|
3367
|
+
if (task.acceptanceChecklist?.length > 0) {
|
|
3368
|
+
sections.push("## Acceptance Criteria");
|
|
3369
|
+
for (const item of task.acceptanceChecklist) {
|
|
3370
|
+
sections.push(`- [ ] ${item.text}`);
|
|
3371
|
+
}
|
|
3372
|
+
sections.push("");
|
|
3373
|
+
}
|
|
3374
|
+
if (summary) {
|
|
3375
|
+
sections.push("## Agent Summary");
|
|
3376
|
+
sections.push(summary);
|
|
3377
|
+
sections.push("");
|
|
3378
|
+
}
|
|
3379
|
+
sections.push("---");
|
|
3380
|
+
sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}\`* | Task ID: \`${task.id}\``);
|
|
3381
|
+
return sections.join(`
|
|
3382
|
+
`);
|
|
3383
|
+
}
|
|
3384
|
+
extractPrNumber(url) {
|
|
3385
|
+
const match = url.match(/\/pull\/(\d+)/);
|
|
3386
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
// src/agent/reviewer-worker.ts
|
|
3391
|
+
init_src();
|
|
3392
|
+
init_knowledge_base();
|
|
3393
|
+
init_colors();
|
|
3710
3394
|
function resolveProvider2(value) {
|
|
3711
3395
|
if (!value || value.startsWith("--"))
|
|
3712
3396
|
return PROVIDER.CLAUDE;
|
|
@@ -4351,8 +4035,8 @@ class ExecEventEmitter {
|
|
|
4351
4035
|
}
|
|
4352
4036
|
// src/exec/history-manager.ts
|
|
4353
4037
|
init_config();
|
|
4354
|
-
var
|
|
4355
|
-
var
|
|
4038
|
+
var import_node_fs7 = require("node:fs");
|
|
4039
|
+
var import_node_path9 = require("node:path");
|
|
4356
4040
|
var DEFAULT_MAX_SESSIONS = 30;
|
|
4357
4041
|
function generateSessionId2() {
|
|
4358
4042
|
const timestamp = Date.now().toString(36);
|
|
@@ -4364,30 +4048,30 @@ class HistoryManager {
|
|
|
4364
4048
|
historyDir;
|
|
4365
4049
|
maxSessions;
|
|
4366
4050
|
constructor(projectPath, options) {
|
|
4367
|
-
this.historyDir = options?.historyDir ??
|
|
4051
|
+
this.historyDir = options?.historyDir ?? import_node_path9.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
|
|
4368
4052
|
this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
4369
4053
|
this.ensureHistoryDir();
|
|
4370
4054
|
}
|
|
4371
4055
|
ensureHistoryDir() {
|
|
4372
|
-
if (!
|
|
4373
|
-
|
|
4056
|
+
if (!import_node_fs7.existsSync(this.historyDir)) {
|
|
4057
|
+
import_node_fs7.mkdirSync(this.historyDir, { recursive: true });
|
|
4374
4058
|
}
|
|
4375
4059
|
}
|
|
4376
4060
|
getSessionPath(sessionId) {
|
|
4377
|
-
return
|
|
4061
|
+
return import_node_path9.join(this.historyDir, `${sessionId}.json`);
|
|
4378
4062
|
}
|
|
4379
4063
|
saveSession(session) {
|
|
4380
4064
|
const filePath = this.getSessionPath(session.id);
|
|
4381
4065
|
session.updatedAt = Date.now();
|
|
4382
|
-
|
|
4066
|
+
import_node_fs7.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
4383
4067
|
}
|
|
4384
4068
|
loadSession(sessionId) {
|
|
4385
4069
|
const filePath = this.getSessionPath(sessionId);
|
|
4386
|
-
if (!
|
|
4070
|
+
if (!import_node_fs7.existsSync(filePath)) {
|
|
4387
4071
|
return null;
|
|
4388
4072
|
}
|
|
4389
4073
|
try {
|
|
4390
|
-
const content =
|
|
4074
|
+
const content = import_node_fs7.readFileSync(filePath, "utf-8");
|
|
4391
4075
|
return JSON.parse(content);
|
|
4392
4076
|
} catch {
|
|
4393
4077
|
return null;
|
|
@@ -4395,18 +4079,18 @@ class HistoryManager {
|
|
|
4395
4079
|
}
|
|
4396
4080
|
deleteSession(sessionId) {
|
|
4397
4081
|
const filePath = this.getSessionPath(sessionId);
|
|
4398
|
-
if (!
|
|
4082
|
+
if (!import_node_fs7.existsSync(filePath)) {
|
|
4399
4083
|
return false;
|
|
4400
4084
|
}
|
|
4401
4085
|
try {
|
|
4402
|
-
|
|
4086
|
+
import_node_fs7.rmSync(filePath);
|
|
4403
4087
|
return true;
|
|
4404
4088
|
} catch {
|
|
4405
4089
|
return false;
|
|
4406
4090
|
}
|
|
4407
4091
|
}
|
|
4408
4092
|
listSessions(options) {
|
|
4409
|
-
const files =
|
|
4093
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
4410
4094
|
let sessions = [];
|
|
4411
4095
|
for (const file of files) {
|
|
4412
4096
|
if (file.endsWith(".json")) {
|
|
@@ -4479,11 +4163,11 @@ class HistoryManager {
|
|
|
4479
4163
|
return deleted;
|
|
4480
4164
|
}
|
|
4481
4165
|
getSessionCount() {
|
|
4482
|
-
const files =
|
|
4166
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
4483
4167
|
return files.filter((f) => f.endsWith(".json")).length;
|
|
4484
4168
|
}
|
|
4485
4169
|
sessionExists(sessionId) {
|
|
4486
|
-
return
|
|
4170
|
+
return import_node_fs7.existsSync(this.getSessionPath(sessionId));
|
|
4487
4171
|
}
|
|
4488
4172
|
findSessionByPartialId(partialId) {
|
|
4489
4173
|
const sessions = this.listSessions();
|
|
@@ -4497,12 +4181,12 @@ class HistoryManager {
|
|
|
4497
4181
|
return this.historyDir;
|
|
4498
4182
|
}
|
|
4499
4183
|
clearAllSessions() {
|
|
4500
|
-
const files =
|
|
4184
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
4501
4185
|
let deleted = 0;
|
|
4502
4186
|
for (const file of files) {
|
|
4503
4187
|
if (file.endsWith(".json")) {
|
|
4504
4188
|
try {
|
|
4505
|
-
|
|
4189
|
+
import_node_fs7.rmSync(import_node_path9.join(this.historyDir, file));
|
|
4506
4190
|
deleted++;
|
|
4507
4191
|
} catch {}
|
|
4508
4192
|
}
|
|
@@ -4768,7 +4452,6 @@ ${currentPrompt}`);
|
|
|
4768
4452
|
}
|
|
4769
4453
|
// src/git/index.ts
|
|
4770
4454
|
init_git_utils();
|
|
4771
|
-
init_pr_service();
|
|
4772
4455
|
|
|
4773
4456
|
// src/index-node.ts
|
|
4774
4457
|
init_src();
|
|
@@ -4777,59 +4460,128 @@ init_src();
|
|
|
4777
4460
|
init_git_utils();
|
|
4778
4461
|
init_src();
|
|
4779
4462
|
init_colors();
|
|
4780
|
-
init_worktree_manager();
|
|
4781
|
-
var import_shared6 = require("@locusai/shared");
|
|
4782
|
-
var import_events5 = require("events");
|
|
4783
|
-
|
|
4784
|
-
// src/orchestrator/agent-pool.ts
|
|
4785
|
-
init_colors();
|
|
4786
4463
|
init_resolve_bin();
|
|
4787
4464
|
var import_node_child_process7 = require("node:child_process");
|
|
4788
|
-
var
|
|
4789
|
-
var
|
|
4465
|
+
var import_node_fs8 = require("node:fs");
|
|
4466
|
+
var import_node_path10 = require("node:path");
|
|
4790
4467
|
var import_node_url = require("node:url");
|
|
4791
4468
|
var import_shared4 = require("@locusai/shared");
|
|
4792
4469
|
var import_events4 = require("events");
|
|
4793
|
-
var MAX_AGENTS = 5;
|
|
4794
4470
|
|
|
4795
|
-
class
|
|
4471
|
+
class AgentOrchestrator extends import_events4.EventEmitter {
|
|
4472
|
+
client;
|
|
4796
4473
|
config;
|
|
4797
|
-
|
|
4474
|
+
isRunning = false;
|
|
4475
|
+
processedTasks = new Set;
|
|
4476
|
+
resolvedSprintId = null;
|
|
4477
|
+
agentState = null;
|
|
4798
4478
|
heartbeatInterval = null;
|
|
4799
4479
|
constructor(config) {
|
|
4800
4480
|
super();
|
|
4801
4481
|
this.config = config;
|
|
4482
|
+
this.client = new LocusClient({
|
|
4483
|
+
baseUrl: config.apiBase,
|
|
4484
|
+
token: config.apiKey
|
|
4485
|
+
});
|
|
4802
4486
|
}
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4487
|
+
async resolveSprintId() {
|
|
4488
|
+
if (this.config.sprintId) {
|
|
4489
|
+
return this.config.sprintId;
|
|
4490
|
+
}
|
|
4491
|
+
try {
|
|
4492
|
+
const sprint = await this.client.sprints.getActive(this.config.workspaceId);
|
|
4493
|
+
if (sprint?.id) {
|
|
4494
|
+
console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint.name}`));
|
|
4495
|
+
return sprint.id;
|
|
4496
|
+
}
|
|
4497
|
+
} catch {}
|
|
4498
|
+
console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
|
|
4499
|
+
return "";
|
|
4500
|
+
}
|
|
4501
|
+
async start() {
|
|
4502
|
+
if (this.isRunning) {
|
|
4503
|
+
throw new Error("Orchestrator is already running");
|
|
4504
|
+
}
|
|
4505
|
+
this.isRunning = true;
|
|
4506
|
+
this.processedTasks.clear();
|
|
4507
|
+
try {
|
|
4508
|
+
await this.orchestrationLoop();
|
|
4509
|
+
} catch (error) {
|
|
4510
|
+
this.emit("error", error);
|
|
4511
|
+
throw error;
|
|
4512
|
+
} finally {
|
|
4513
|
+
await this.cleanup();
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
async orchestrationLoop() {
|
|
4517
|
+
this.resolvedSprintId = await this.resolveSprintId();
|
|
4518
|
+
this.emit("started", {
|
|
4519
|
+
timestamp: new Date,
|
|
4520
|
+
config: this.config,
|
|
4521
|
+
sprintId: this.resolvedSprintId
|
|
4522
|
+
});
|
|
4523
|
+
this.printBanner();
|
|
4524
|
+
const tasks2 = await this.getAvailableTasks();
|
|
4525
|
+
if (tasks2.length === 0) {
|
|
4526
|
+
console.log(c.dim("ℹ No available tasks found in the backlog."));
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
if (!this.preflightChecks())
|
|
4530
|
+
return;
|
|
4531
|
+
this.startHeartbeatMonitor();
|
|
4532
|
+
await this.spawnAgent();
|
|
4533
|
+
await this.waitForAgent();
|
|
4534
|
+
console.log(`
|
|
4535
|
+
${c.success("✅ Orchestrator finished")}`);
|
|
4536
|
+
}
|
|
4537
|
+
printBanner() {
|
|
4538
|
+
console.log(`
|
|
4539
|
+
${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
4540
|
+
console.log(c.dim("----------------------------------------------"));
|
|
4541
|
+
console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
|
|
4542
|
+
if (this.resolvedSprintId) {
|
|
4543
|
+
console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
|
|
4544
|
+
}
|
|
4545
|
+
console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
|
|
4546
|
+
console.log(c.dim(`----------------------------------------------
|
|
4547
|
+
`));
|
|
4548
|
+
}
|
|
4549
|
+
preflightChecks() {
|
|
4550
|
+
if (!isGitAvailable()) {
|
|
4551
|
+
console.log(c.error("git is not installed. Install from https://git-scm.com/"));
|
|
4552
|
+
return false;
|
|
4553
|
+
}
|
|
4554
|
+
if (!isGhAvailable(this.config.projectPath)) {
|
|
4555
|
+
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/"));
|
|
4556
|
+
}
|
|
4557
|
+
return true;
|
|
4558
|
+
}
|
|
4559
|
+
async getAvailableTasks() {
|
|
4560
|
+
try {
|
|
4561
|
+
const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
|
|
4562
|
+
return tasks2.filter((task) => !this.processedTasks.has(task.id));
|
|
4563
|
+
} catch (error) {
|
|
4564
|
+
this.emit("error", error);
|
|
4565
|
+
return [];
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
async spawnAgent() {
|
|
4569
|
+
const agentId = `agent-0-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
4570
|
+
this.agentState = {
|
|
4571
|
+
id: agentId,
|
|
4572
|
+
status: "IDLE",
|
|
4573
|
+
currentTaskId: null,
|
|
4574
|
+
tasksCompleted: 0,
|
|
4575
|
+
tasksFailed: 0,
|
|
4823
4576
|
lastHeartbeat: new Date
|
|
4824
4577
|
};
|
|
4825
|
-
this.agents.set(agentId, agentState);
|
|
4826
4578
|
console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
|
|
4827
4579
|
`);
|
|
4828
4580
|
const workerPath = this.resolveWorkerPath();
|
|
4829
4581
|
if (!workerPath) {
|
|
4830
4582
|
throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
|
|
4831
4583
|
}
|
|
4832
|
-
const workerArgs = this.buildWorkerArgs(agentId
|
|
4584
|
+
const workerArgs = this.buildWorkerArgs(agentId);
|
|
4833
4585
|
const agentProcess = import_node_child_process7.spawn(process.execPath, [workerPath, ...workerArgs], {
|
|
4834
4586
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4835
4587
|
detached: true,
|
|
@@ -4840,60 +4592,64 @@ class AgentPool extends import_events4.EventEmitter {
|
|
|
4840
4592
|
LOCUS_WORKSPACE: this.config.workspaceId
|
|
4841
4593
|
})
|
|
4842
4594
|
});
|
|
4843
|
-
agentState.process = agentProcess;
|
|
4844
|
-
this.attachProcessHandlers(agentId, agentState, agentProcess);
|
|
4595
|
+
this.agentState.process = agentProcess;
|
|
4596
|
+
this.attachProcessHandlers(agentId, this.agentState, agentProcess);
|
|
4845
4597
|
this.emit("agent:spawned", { agentId });
|
|
4846
4598
|
}
|
|
4847
|
-
async
|
|
4848
|
-
while (this.
|
|
4599
|
+
async waitForAgent() {
|
|
4600
|
+
while (this.agentState && this.isRunning) {
|
|
4849
4601
|
await sleep(2000);
|
|
4850
4602
|
}
|
|
4851
4603
|
}
|
|
4852
4604
|
startHeartbeatMonitor() {
|
|
4853
4605
|
this.heartbeatInterval = setInterval(() => {
|
|
4606
|
+
if (!this.agentState)
|
|
4607
|
+
return;
|
|
4854
4608
|
const now = Date.now();
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
killProcessTree(agent.process);
|
|
4860
|
-
}
|
|
4861
|
-
this.emit("agent:stale", { agentId });
|
|
4609
|
+
if (this.agentState.status === "WORKING" && now - this.agentState.lastHeartbeat.getTime() > import_shared4.STALE_AGENT_TIMEOUT_MS) {
|
|
4610
|
+
console.log(c.error(`Agent ${this.agentState.id} is stale (no heartbeat for 10 minutes). Killing.`));
|
|
4611
|
+
if (this.agentState.process && !this.agentState.process.killed) {
|
|
4612
|
+
killProcessTree(this.agentState.process);
|
|
4862
4613
|
}
|
|
4614
|
+
this.emit("agent:stale", { agentId: this.agentState.id });
|
|
4863
4615
|
}
|
|
4864
4616
|
}, 60000);
|
|
4865
4617
|
}
|
|
4618
|
+
async stop() {
|
|
4619
|
+
this.isRunning = false;
|
|
4620
|
+
await this.cleanup();
|
|
4621
|
+
this.emit("stopped", { timestamp: new Date });
|
|
4622
|
+
}
|
|
4866
4623
|
stopAgent(agentId) {
|
|
4867
|
-
|
|
4868
|
-
if (!agent)
|
|
4624
|
+
if (!this.agentState || this.agentState.id !== agentId)
|
|
4869
4625
|
return false;
|
|
4870
|
-
if (
|
|
4871
|
-
killProcessTree(
|
|
4626
|
+
if (this.agentState.process && !this.agentState.process.killed) {
|
|
4627
|
+
killProcessTree(this.agentState.process);
|
|
4872
4628
|
}
|
|
4873
4629
|
return true;
|
|
4874
4630
|
}
|
|
4875
|
-
|
|
4631
|
+
async cleanup() {
|
|
4876
4632
|
if (this.heartbeatInterval) {
|
|
4877
4633
|
clearInterval(this.heartbeatInterval);
|
|
4878
4634
|
this.heartbeatInterval = null;
|
|
4879
4635
|
}
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
killProcessTree(agent.process);
|
|
4884
|
-
}
|
|
4636
|
+
if (this.agentState?.process && !this.agentState.process.killed) {
|
|
4637
|
+
console.log(`Killing agent: ${this.agentState.id}`);
|
|
4638
|
+
killProcessTree(this.agentState.process);
|
|
4885
4639
|
}
|
|
4886
|
-
this.agents.clear();
|
|
4887
4640
|
}
|
|
4888
4641
|
getStats() {
|
|
4889
4642
|
return {
|
|
4890
|
-
activeAgents: this.
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4643
|
+
activeAgents: this.agentState ? 1 : 0,
|
|
4644
|
+
totalTasksCompleted: this.agentState?.tasksCompleted ?? 0,
|
|
4645
|
+
totalTasksFailed: this.agentState?.tasksFailed ?? 0,
|
|
4646
|
+
processedTasks: this.processedTasks.size
|
|
4894
4647
|
};
|
|
4895
4648
|
}
|
|
4896
|
-
|
|
4649
|
+
getAgentStates() {
|
|
4650
|
+
return this.agentState ? [this.agentState] : [];
|
|
4651
|
+
}
|
|
4652
|
+
buildWorkerArgs(agentId) {
|
|
4897
4653
|
const args = [
|
|
4898
4654
|
"--agent-id",
|
|
4899
4655
|
agentId,
|
|
@@ -4912,17 +4668,8 @@ class AgentPool extends import_events4.EventEmitter {
|
|
|
4912
4668
|
if (this.config.provider) {
|
|
4913
4669
|
args.push("--provider", this.config.provider);
|
|
4914
4670
|
}
|
|
4915
|
-
if (resolvedSprintId) {
|
|
4916
|
-
args.push("--sprint-id", resolvedSprintId);
|
|
4917
|
-
}
|
|
4918
|
-
if (this.config.useWorktrees ?? true) {
|
|
4919
|
-
args.push("--use-worktrees");
|
|
4920
|
-
}
|
|
4921
|
-
if (this.config.autoPush) {
|
|
4922
|
-
args.push("--auto-push");
|
|
4923
|
-
}
|
|
4924
|
-
if (baseBranch) {
|
|
4925
|
-
args.push("--base-branch", baseBranch);
|
|
4671
|
+
if (this.resolvedSprintId) {
|
|
4672
|
+
args.push("--sprint-id", this.resolvedSprintId);
|
|
4926
4673
|
}
|
|
4927
4674
|
return args;
|
|
4928
4675
|
}
|
|
@@ -4945,29 +4692,28 @@ class AgentPool extends import_events4.EventEmitter {
|
|
|
4945
4692
|
proc.on("exit", (code) => {
|
|
4946
4693
|
console.log(`
|
|
4947
4694
|
${agentId} finished (exit code: ${code})`);
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
agent.status = code === 0 ? "COMPLETED" : "FAILED";
|
|
4695
|
+
if (this.agentState) {
|
|
4696
|
+
this.agentState.status = code === 0 ? "COMPLETED" : "FAILED";
|
|
4951
4697
|
this.emit("agent:completed", {
|
|
4952
4698
|
agentId,
|
|
4953
|
-
status:
|
|
4954
|
-
tasksCompleted:
|
|
4955
|
-
tasksFailed:
|
|
4699
|
+
status: this.agentState.status,
|
|
4700
|
+
tasksCompleted: this.agentState.tasksCompleted,
|
|
4701
|
+
tasksFailed: this.agentState.tasksFailed
|
|
4956
4702
|
});
|
|
4957
|
-
this.
|
|
4703
|
+
this.agentState = null;
|
|
4958
4704
|
}
|
|
4959
4705
|
});
|
|
4960
4706
|
}
|
|
4961
4707
|
resolveWorkerPath() {
|
|
4962
|
-
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator/
|
|
4963
|
-
const currentModuleDir =
|
|
4708
|
+
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator/index.ts");
|
|
4709
|
+
const currentModuleDir = import_node_path10.dirname(currentModulePath);
|
|
4964
4710
|
const potentialPaths = [
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4711
|
+
import_node_path10.join(currentModuleDir, "..", "agent", "worker.js"),
|
|
4712
|
+
import_node_path10.join(currentModuleDir, "agent", "worker.js"),
|
|
4713
|
+
import_node_path10.join(currentModuleDir, "worker.js"),
|
|
4714
|
+
import_node_path10.join(currentModuleDir, "..", "agent", "worker.ts")
|
|
4969
4715
|
];
|
|
4970
|
-
return potentialPaths.find((p) =>
|
|
4716
|
+
return potentialPaths.find((p) => import_node_fs8.existsSync(p));
|
|
4971
4717
|
}
|
|
4972
4718
|
}
|
|
4973
4719
|
function killProcessTree(proc) {
|
|
@@ -4982,460 +4728,16 @@ function killProcessTree(proc) {
|
|
|
4982
4728
|
}
|
|
4983
4729
|
}
|
|
4984
4730
|
function sleep(ms) {
|
|
4985
|
-
return new Promise((
|
|
4986
|
-
}
|
|
4987
|
-
|
|
4988
|
-
// src/orchestrator/execution.ts
|
|
4989
|
-
init_git_utils();
|
|
4990
|
-
init_colors();
|
|
4991
|
-
var import_shared5 = require("@locusai/shared");
|
|
4992
|
-
var SPAWN_DELAY_MS = 5000;
|
|
4993
|
-
|
|
4994
|
-
class ExecutionStrategy {
|
|
4995
|
-
config;
|
|
4996
|
-
pool;
|
|
4997
|
-
tierMerge;
|
|
4998
|
-
resolvedSprintId;
|
|
4999
|
-
isRunning;
|
|
5000
|
-
constructor(config, pool, tierMerge, resolvedSprintId, isRunning) {
|
|
5001
|
-
this.config = config;
|
|
5002
|
-
this.pool = pool;
|
|
5003
|
-
this.tierMerge = tierMerge;
|
|
5004
|
-
this.resolvedSprintId = resolvedSprintId;
|
|
5005
|
-
this.isRunning = isRunning;
|
|
5006
|
-
}
|
|
5007
|
-
async execute(tasks2) {
|
|
5008
|
-
const hasTiers = tasks2.some((t) => t.tier !== null && t.tier !== undefined);
|
|
5009
|
-
const useWorktrees = this.config.useWorktrees ?? true;
|
|
5010
|
-
if (hasTiers && useWorktrees) {
|
|
5011
|
-
await this.tierBasedExecution(tasks2);
|
|
5012
|
-
} else {
|
|
5013
|
-
await this.legacyExecution(tasks2);
|
|
5014
|
-
}
|
|
5015
|
-
}
|
|
5016
|
-
async tierBasedExecution(allTasks) {
|
|
5017
|
-
const tierMap = groupByTier(allTasks);
|
|
5018
|
-
const tiers = Array.from(tierMap.keys()).sort((a, b) => a - b);
|
|
5019
|
-
const defaultBranch = getDefaultBranch(this.config.projectPath);
|
|
5020
|
-
console.log(c.primary(`\uD83D\uDCCA Tier-based execution: ${tiers.length} tier(s) detected [${tiers.join(", ")}]`));
|
|
5021
|
-
let currentBaseBranch = defaultBranch;
|
|
5022
|
-
for (const tier of tiers) {
|
|
5023
|
-
if (!this.isRunning())
|
|
5024
|
-
break;
|
|
5025
|
-
const tierTasks = tierMap.get(tier) ?? [];
|
|
5026
|
-
const dispatchable = tierTasks.filter(isDispatchable);
|
|
5027
|
-
if (dispatchable.length === 0) {
|
|
5028
|
-
console.log(c.dim(`ℹ Tier ${tier}: all ${tierTasks.length} task(s) already completed, skipping`));
|
|
5029
|
-
const tierBranch = this.tierMerge.tierBranchName(tier);
|
|
5030
|
-
if (this.tierMerge.remoteBranchExists(tierBranch)) {
|
|
5031
|
-
currentBaseBranch = tierBranch;
|
|
5032
|
-
}
|
|
5033
|
-
continue;
|
|
5034
|
-
}
|
|
5035
|
-
console.log(`
|
|
5036
|
-
${c.primary(`\uD83C\uDFD7️ Tier ${tier}:`)} ${dispatchable.length} task(s) | base: ${c.bold(currentBaseBranch)}`);
|
|
5037
|
-
await this.spawnAgentsForTasks(dispatchable.length, currentBaseBranch);
|
|
5038
|
-
await this.pool.waitForAll(this.isRunning);
|
|
5039
|
-
console.log(c.success(`✓ Tier ${tier} complete`));
|
|
5040
|
-
if (this.config.autoPush && tiers.indexOf(tier) < tiers.length - 1) {
|
|
5041
|
-
const mergeBranch = this.tierMerge.createMergeBranch(tier, currentBaseBranch);
|
|
5042
|
-
if (mergeBranch) {
|
|
5043
|
-
currentBaseBranch = mergeBranch;
|
|
5044
|
-
console.log(c.success(`\uD83D\uDCCC Created merge branch: ${mergeBranch} (base for tier ${tier + 1})`));
|
|
5045
|
-
}
|
|
5046
|
-
}
|
|
5047
|
-
}
|
|
5048
|
-
}
|
|
5049
|
-
async legacyExecution(tasks2) {
|
|
5050
|
-
const defaultBranch = getDefaultBranch(this.config.projectPath);
|
|
5051
|
-
await this.spawnAgentsForTasks(tasks2.length, defaultBranch);
|
|
5052
|
-
await this.pool.waitForAll(this.isRunning);
|
|
5053
|
-
}
|
|
5054
|
-
async spawnAgentsForTasks(taskCount, baseBranch) {
|
|
5055
|
-
const agentsToSpawn = Math.min(this.pool.effectiveAgentCount, taskCount);
|
|
5056
|
-
const spawnPromises = [];
|
|
5057
|
-
for (let i = 0;i < agentsToSpawn; i++) {
|
|
5058
|
-
if (i > 0) {
|
|
5059
|
-
await sleep2(SPAWN_DELAY_MS);
|
|
5060
|
-
}
|
|
5061
|
-
spawnPromises.push(this.pool.spawn(i, this.resolvedSprintId, baseBranch));
|
|
5062
|
-
}
|
|
5063
|
-
await Promise.all(spawnPromises);
|
|
5064
|
-
}
|
|
5065
|
-
}
|
|
5066
|
-
function groupByTier(tasks2) {
|
|
5067
|
-
const tierMap = new Map;
|
|
5068
|
-
for (const task of tasks2) {
|
|
5069
|
-
const tier = task.tier ?? 0;
|
|
5070
|
-
const existing = tierMap.get(tier);
|
|
5071
|
-
if (existing) {
|
|
5072
|
-
existing.push(task);
|
|
5073
|
-
} else {
|
|
5074
|
-
tierMap.set(tier, [task]);
|
|
5075
|
-
}
|
|
5076
|
-
}
|
|
5077
|
-
return tierMap;
|
|
5078
|
-
}
|
|
5079
|
-
function isDispatchable(task) {
|
|
5080
|
-
return task.status === import_shared5.TaskStatus.BACKLOG || task.status === import_shared5.TaskStatus.IN_PROGRESS && !task.assignedTo;
|
|
5081
|
-
}
|
|
5082
|
-
function sleep2(ms) {
|
|
5083
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5084
|
-
}
|
|
5085
|
-
|
|
5086
|
-
// src/orchestrator/tier-merge.ts
|
|
5087
|
-
init_colors();
|
|
5088
|
-
var import_node_child_process8 = require("node:child_process");
|
|
5089
|
-
var TIER_BRANCH_PREFIX = "locus/tier";
|
|
5090
|
-
|
|
5091
|
-
class TierMergeService {
|
|
5092
|
-
projectPath;
|
|
5093
|
-
sprintId;
|
|
5094
|
-
tierTaskIds = new Map;
|
|
5095
|
-
constructor(projectPath, sprintId) {
|
|
5096
|
-
this.projectPath = projectPath;
|
|
5097
|
-
this.sprintId = sprintId;
|
|
5098
|
-
}
|
|
5099
|
-
registerTierTasks(tasks2) {
|
|
5100
|
-
for (const task of tasks2) {
|
|
5101
|
-
const tier = task.tier ?? 0;
|
|
5102
|
-
const existing = this.tierTaskIds.get(tier);
|
|
5103
|
-
if (existing) {
|
|
5104
|
-
existing.push(task.id);
|
|
5105
|
-
} else {
|
|
5106
|
-
this.tierTaskIds.set(tier, [task.id]);
|
|
5107
|
-
}
|
|
5108
|
-
}
|
|
5109
|
-
}
|
|
5110
|
-
tierBranchName(tier) {
|
|
5111
|
-
const suffix = this.sprintId ? `-${this.sprintId.slice(0, 8)}` : "";
|
|
5112
|
-
return `${TIER_BRANCH_PREFIX}-${tier}${suffix}`;
|
|
5113
|
-
}
|
|
5114
|
-
remoteBranchExists(branch) {
|
|
5115
|
-
try {
|
|
5116
|
-
import_node_child_process8.execFileSync("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
|
|
5117
|
-
cwd: this.projectPath,
|
|
5118
|
-
encoding: "utf-8",
|
|
5119
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5120
|
-
});
|
|
5121
|
-
return true;
|
|
5122
|
-
} catch {
|
|
5123
|
-
return false;
|
|
5124
|
-
}
|
|
5125
|
-
}
|
|
5126
|
-
createMergeBranch(tier, baseBranch) {
|
|
5127
|
-
const mergeBranchName = this.tierBranchName(tier);
|
|
5128
|
-
try {
|
|
5129
|
-
this.gitExec(["fetch", "origin"]);
|
|
5130
|
-
const tierTaskBranches = this.findTierTaskBranches(tier);
|
|
5131
|
-
if (tierTaskBranches.length === 0) {
|
|
5132
|
-
console.log(c.dim(` Tier ${tier}: no pushed task branches found, skipping merge branch creation`));
|
|
5133
|
-
return null;
|
|
5134
|
-
}
|
|
5135
|
-
console.log(c.dim(` Merging ${tierTaskBranches.length} branch(es) into ${mergeBranchName}: ${tierTaskBranches.join(", ")}`));
|
|
5136
|
-
try {
|
|
5137
|
-
this.gitExec(["branch", "-D", mergeBranchName]);
|
|
5138
|
-
} catch {}
|
|
5139
|
-
this.gitExec(["checkout", "-b", mergeBranchName, `origin/${baseBranch}`]);
|
|
5140
|
-
for (const branch of tierTaskBranches) {
|
|
5141
|
-
try {
|
|
5142
|
-
this.gitExec(["merge", `origin/${branch}`, "--no-edit"]);
|
|
5143
|
-
} catch (err) {
|
|
5144
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5145
|
-
console.log(c.error(` Merge conflict merging ${branch} into ${mergeBranchName}: ${msg}`));
|
|
5146
|
-
try {
|
|
5147
|
-
this.gitExec(["merge", "--abort"]);
|
|
5148
|
-
} catch {}
|
|
5149
|
-
}
|
|
5150
|
-
}
|
|
5151
|
-
this.gitExec(["push", "-u", "origin", mergeBranchName, "--force"]);
|
|
5152
|
-
this.gitExec(["checkout", baseBranch]);
|
|
5153
|
-
return mergeBranchName;
|
|
5154
|
-
} catch (err) {
|
|
5155
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5156
|
-
console.log(c.error(`Failed to create tier merge branch: ${msg}`));
|
|
5157
|
-
try {
|
|
5158
|
-
this.gitExec(["checkout", baseBranch]);
|
|
5159
|
-
} catch {}
|
|
5160
|
-
return null;
|
|
5161
|
-
}
|
|
5162
|
-
}
|
|
5163
|
-
findTierTaskBranches(tier) {
|
|
5164
|
-
const tierTaskIds = this.tierTaskIds.get(tier);
|
|
5165
|
-
if (!tierTaskIds || tierTaskIds.length === 0)
|
|
5166
|
-
return [];
|
|
5167
|
-
try {
|
|
5168
|
-
const output = import_node_child_process8.execSync('git branch -r --list "origin/agent/*" --format="%(refname:short)"', { cwd: this.projectPath, encoding: "utf-8" }).trim();
|
|
5169
|
-
if (!output)
|
|
5170
|
-
return [];
|
|
5171
|
-
const remoteBranches = output.split(`
|
|
5172
|
-
`).map((b) => b.replace("origin/", ""));
|
|
5173
|
-
return remoteBranches.filter((branch) => {
|
|
5174
|
-
const branchSuffix = branch.replace(/^agent\//, "");
|
|
5175
|
-
if (!branchSuffix)
|
|
5176
|
-
return false;
|
|
5177
|
-
return tierTaskIds.some((id) => branchSuffix.startsWith(`${id}-`) || branchSuffix === id || branchSuffix.startsWith(id));
|
|
5178
|
-
});
|
|
5179
|
-
} catch (err) {
|
|
5180
|
-
console.log(c.dim(` Could not list remote branches for tier ${tier}: ${err instanceof Error ? err.message : String(err)}`));
|
|
5181
|
-
return [];
|
|
5182
|
-
}
|
|
5183
|
-
}
|
|
5184
|
-
gitExec(args) {
|
|
5185
|
-
return import_node_child_process8.execFileSync("git", args, {
|
|
5186
|
-
cwd: this.projectPath,
|
|
5187
|
-
encoding: "utf-8",
|
|
5188
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5189
|
-
});
|
|
5190
|
-
}
|
|
5191
|
-
}
|
|
5192
|
-
|
|
5193
|
-
// src/orchestrator/index.ts
|
|
5194
|
-
class AgentOrchestrator extends import_events5.EventEmitter {
|
|
5195
|
-
client;
|
|
5196
|
-
config;
|
|
5197
|
-
pool;
|
|
5198
|
-
isRunning = false;
|
|
5199
|
-
processedTasks = new Set;
|
|
5200
|
-
resolvedSprintId = null;
|
|
5201
|
-
worktreeManager = null;
|
|
5202
|
-
constructor(config) {
|
|
5203
|
-
super();
|
|
5204
|
-
this.config = config;
|
|
5205
|
-
this.client = new LocusClient({
|
|
5206
|
-
baseUrl: config.apiBase,
|
|
5207
|
-
token: config.apiKey
|
|
5208
|
-
});
|
|
5209
|
-
this.pool = new AgentPool(config);
|
|
5210
|
-
this.pool.on("agent:spawned", (data) => this.emit("agent:spawned", data));
|
|
5211
|
-
this.pool.on("agent:completed", (data) => this.emit("agent:completed", data));
|
|
5212
|
-
this.pool.on("agent:stale", (data) => this.emit("agent:stale", data));
|
|
5213
|
-
}
|
|
5214
|
-
get useWorktrees() {
|
|
5215
|
-
return this.config.useWorktrees ?? true;
|
|
5216
|
-
}
|
|
5217
|
-
get worktreeCleanupPolicy() {
|
|
5218
|
-
return this.config.worktreeCleanupPolicy ?? "retain-on-failure";
|
|
5219
|
-
}
|
|
5220
|
-
async resolveSprintId() {
|
|
5221
|
-
if (this.config.sprintId) {
|
|
5222
|
-
return this.config.sprintId;
|
|
5223
|
-
}
|
|
5224
|
-
try {
|
|
5225
|
-
const sprint = await this.client.sprints.getActive(this.config.workspaceId);
|
|
5226
|
-
if (sprint?.id) {
|
|
5227
|
-
console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint.name}`));
|
|
5228
|
-
return sprint.id;
|
|
5229
|
-
}
|
|
5230
|
-
} catch {}
|
|
5231
|
-
console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
|
|
5232
|
-
return "";
|
|
5233
|
-
}
|
|
5234
|
-
async start() {
|
|
5235
|
-
if (this.isRunning) {
|
|
5236
|
-
throw new Error("Orchestrator is already running");
|
|
5237
|
-
}
|
|
5238
|
-
this.isRunning = true;
|
|
5239
|
-
this.processedTasks.clear();
|
|
5240
|
-
try {
|
|
5241
|
-
await this.orchestrationLoop();
|
|
5242
|
-
} catch (error) {
|
|
5243
|
-
this.emit("error", error);
|
|
5244
|
-
throw error;
|
|
5245
|
-
} finally {
|
|
5246
|
-
await this.cleanup();
|
|
5247
|
-
}
|
|
5248
|
-
}
|
|
5249
|
-
async orchestrationLoop() {
|
|
5250
|
-
this.resolvedSprintId = await this.resolveSprintId();
|
|
5251
|
-
this.emit("started", {
|
|
5252
|
-
timestamp: new Date,
|
|
5253
|
-
config: this.config,
|
|
5254
|
-
sprintId: this.resolvedSprintId
|
|
5255
|
-
});
|
|
5256
|
-
this.printBanner();
|
|
5257
|
-
const tasks2 = await this.getAvailableTasks();
|
|
5258
|
-
if (tasks2.length === 0) {
|
|
5259
|
-
console.log(c.dim("ℹ No available tasks found in the backlog."));
|
|
5260
|
-
return;
|
|
5261
|
-
}
|
|
5262
|
-
if (!this.preflightChecks(tasks2))
|
|
5263
|
-
return;
|
|
5264
|
-
if (this.useWorktrees) {
|
|
5265
|
-
this.worktreeManager = new WorktreeManager(this.config.projectPath, {
|
|
5266
|
-
cleanupPolicy: this.worktreeCleanupPolicy
|
|
5267
|
-
});
|
|
5268
|
-
}
|
|
5269
|
-
this.pool.startHeartbeatMonitor();
|
|
5270
|
-
const tierMerge = new TierMergeService(this.config.projectPath, this.resolvedSprintId);
|
|
5271
|
-
tierMerge.registerTierTasks(tasks2);
|
|
5272
|
-
const execution = new ExecutionStrategy(this.config, this.pool, tierMerge, this.resolvedSprintId, () => this.isRunning);
|
|
5273
|
-
await execution.execute(tasks2);
|
|
5274
|
-
console.log(`
|
|
5275
|
-
${c.success("✅ Orchestrator finished")}`);
|
|
5276
|
-
}
|
|
5277
|
-
printBanner() {
|
|
5278
|
-
console.log(`
|
|
5279
|
-
${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
5280
|
-
console.log(c.dim("----------------------------------------------"));
|
|
5281
|
-
console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
|
|
5282
|
-
if (this.resolvedSprintId) {
|
|
5283
|
-
console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
|
|
5284
|
-
}
|
|
5285
|
-
console.log(`${c.bold("Agents:")} ${this.pool.effectiveAgentCount}`);
|
|
5286
|
-
console.log(`${c.bold("Worktrees:")} ${this.useWorktrees ? "enabled" : "disabled"}`);
|
|
5287
|
-
if (this.useWorktrees) {
|
|
5288
|
-
console.log(`${c.bold("Cleanup policy:")} ${this.worktreeCleanupPolicy}`);
|
|
5289
|
-
console.log(`${c.bold("Auto-push:")} ${this.config.autoPush ? "enabled" : "disabled"}`);
|
|
5290
|
-
}
|
|
5291
|
-
console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
|
|
5292
|
-
console.log(c.dim(`----------------------------------------------
|
|
5293
|
-
`));
|
|
5294
|
-
}
|
|
5295
|
-
preflightChecks(_tasks) {
|
|
5296
|
-
if (this.useWorktrees && !isGitAvailable()) {
|
|
5297
|
-
console.log(c.error("git is not installed. Worktree isolation requires git. Install from https://git-scm.com/"));
|
|
5298
|
-
return false;
|
|
5299
|
-
}
|
|
5300
|
-
if (this.config.autoPush && !isGhAvailable(this.config.projectPath)) {
|
|
5301
|
-
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/"));
|
|
5302
|
-
}
|
|
5303
|
-
return true;
|
|
5304
|
-
}
|
|
5305
|
-
async getAvailableTasks() {
|
|
5306
|
-
try {
|
|
5307
|
-
const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
|
|
5308
|
-
return tasks2.filter((task) => !this.processedTasks.has(task.id));
|
|
5309
|
-
} catch (error) {
|
|
5310
|
-
this.emit("error", error);
|
|
5311
|
-
return [];
|
|
5312
|
-
}
|
|
5313
|
-
}
|
|
5314
|
-
async assignTaskToAgent(agentId) {
|
|
5315
|
-
const agent = this.pool.get(agentId);
|
|
5316
|
-
if (!agent)
|
|
5317
|
-
return null;
|
|
5318
|
-
try {
|
|
5319
|
-
const tasks2 = await this.getAvailableTasks();
|
|
5320
|
-
const priorityOrder = [
|
|
5321
|
-
import_shared6.TaskPriority.CRITICAL,
|
|
5322
|
-
import_shared6.TaskPriority.HIGH,
|
|
5323
|
-
import_shared6.TaskPriority.MEDIUM,
|
|
5324
|
-
import_shared6.TaskPriority.LOW
|
|
5325
|
-
];
|
|
5326
|
-
let task = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
|
|
5327
|
-
if (!task && tasks2.length > 0) {
|
|
5328
|
-
task = tasks2[0];
|
|
5329
|
-
}
|
|
5330
|
-
if (!task)
|
|
5331
|
-
return null;
|
|
5332
|
-
agent.currentTaskId = task.id;
|
|
5333
|
-
agent.status = "WORKING";
|
|
5334
|
-
this.emit("task:assigned", {
|
|
5335
|
-
agentId,
|
|
5336
|
-
taskId: task.id,
|
|
5337
|
-
title: task.title
|
|
5338
|
-
});
|
|
5339
|
-
return task;
|
|
5340
|
-
} catch (error) {
|
|
5341
|
-
this.emit("error", error);
|
|
5342
|
-
return null;
|
|
5343
|
-
}
|
|
5344
|
-
}
|
|
5345
|
-
async completeTask(taskId, agentId, summary) {
|
|
5346
|
-
try {
|
|
5347
|
-
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
5348
|
-
status: import_shared6.TaskStatus.IN_REVIEW
|
|
5349
|
-
});
|
|
5350
|
-
if (summary) {
|
|
5351
|
-
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
5352
|
-
author: agentId,
|
|
5353
|
-
text: `✅ Task completed
|
|
5354
|
-
|
|
5355
|
-
${summary}`
|
|
5356
|
-
});
|
|
5357
|
-
}
|
|
5358
|
-
this.processedTasks.add(taskId);
|
|
5359
|
-
const agent = this.pool.get(agentId);
|
|
5360
|
-
if (agent) {
|
|
5361
|
-
agent.tasksCompleted += 1;
|
|
5362
|
-
agent.currentTaskId = null;
|
|
5363
|
-
agent.status = "IDLE";
|
|
5364
|
-
}
|
|
5365
|
-
this.emit("task:completed", { agentId, taskId });
|
|
5366
|
-
} catch (error) {
|
|
5367
|
-
this.emit("error", error);
|
|
5368
|
-
}
|
|
5369
|
-
}
|
|
5370
|
-
async failTask(taskId, agentId, error) {
|
|
5371
|
-
try {
|
|
5372
|
-
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
5373
|
-
status: import_shared6.TaskStatus.BACKLOG,
|
|
5374
|
-
assignedTo: null
|
|
5375
|
-
});
|
|
5376
|
-
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
5377
|
-
author: agentId,
|
|
5378
|
-
text: `❌ Agent failed: ${error}`
|
|
5379
|
-
});
|
|
5380
|
-
const agent = this.pool.get(agentId);
|
|
5381
|
-
if (agent) {
|
|
5382
|
-
agent.tasksFailed += 1;
|
|
5383
|
-
agent.currentTaskId = null;
|
|
5384
|
-
agent.status = "IDLE";
|
|
5385
|
-
}
|
|
5386
|
-
this.emit("task:failed", { agentId, taskId, error });
|
|
5387
|
-
} catch (error2) {
|
|
5388
|
-
this.emit("error", error2);
|
|
5389
|
-
}
|
|
5390
|
-
}
|
|
5391
|
-
async stop() {
|
|
5392
|
-
this.isRunning = false;
|
|
5393
|
-
await this.cleanup();
|
|
5394
|
-
this.emit("stopped", { timestamp: new Date });
|
|
5395
|
-
}
|
|
5396
|
-
stopAgent(agentId) {
|
|
5397
|
-
return this.pool.stopAgent(agentId);
|
|
5398
|
-
}
|
|
5399
|
-
async cleanup() {
|
|
5400
|
-
this.pool.shutdown();
|
|
5401
|
-
if (this.worktreeManager) {
|
|
5402
|
-
try {
|
|
5403
|
-
if (this.worktreeCleanupPolicy === "auto") {
|
|
5404
|
-
const removed = this.worktreeManager.removeAll();
|
|
5405
|
-
if (removed > 0) {
|
|
5406
|
-
console.log(c.dim(`Cleaned up ${removed} worktree(s)`));
|
|
5407
|
-
}
|
|
5408
|
-
} else if (this.worktreeCleanupPolicy === "retain-on-failure") {
|
|
5409
|
-
this.worktreeManager.prune();
|
|
5410
|
-
console.log(c.dim("Retaining worktrees for failure analysis (cleanup policy: retain-on-failure)"));
|
|
5411
|
-
} else {
|
|
5412
|
-
console.log(c.dim("Skipping worktree cleanup (cleanup policy: manual)"));
|
|
5413
|
-
}
|
|
5414
|
-
} catch {
|
|
5415
|
-
console.log(c.dim("Could not clean up some worktrees"));
|
|
5416
|
-
}
|
|
5417
|
-
}
|
|
5418
|
-
}
|
|
5419
|
-
getStats() {
|
|
5420
|
-
const poolStats = this.pool.getStats();
|
|
5421
|
-
return {
|
|
5422
|
-
...poolStats,
|
|
5423
|
-
useWorktrees: this.useWorktrees,
|
|
5424
|
-
processedTasks: this.processedTasks.size
|
|
5425
|
-
};
|
|
5426
|
-
}
|
|
5427
|
-
getAgentStates() {
|
|
5428
|
-
return this.pool.getAll();
|
|
5429
|
-
}
|
|
4731
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
5430
4732
|
}
|
|
5431
4733
|
// src/planning/plan-manager.ts
|
|
5432
4734
|
init_config();
|
|
5433
4735
|
init_knowledge_base();
|
|
5434
|
-
var
|
|
5435
|
-
var
|
|
4736
|
+
var import_node_fs9 = require("node:fs");
|
|
4737
|
+
var import_node_path11 = require("node:path");
|
|
5436
4738
|
|
|
5437
4739
|
// src/planning/sprint-plan.ts
|
|
5438
|
-
var
|
|
4740
|
+
var import_shared5 = require("@locusai/shared");
|
|
5439
4741
|
function sprintPlanToMarkdown(plan) {
|
|
5440
4742
|
const lines = [];
|
|
5441
4743
|
lines.push(`# Sprint Plan: ${plan.name}`);
|
|
@@ -5457,32 +4759,25 @@ function sprintPlanToMarkdown(plan) {
|
|
|
5457
4759
|
}
|
|
5458
4760
|
lines.push(`## Tasks (${plan.tasks.length})`);
|
|
5459
4761
|
lines.push("");
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
lines.push(
|
|
5466
|
-
lines.push(
|
|
4762
|
+
lines.push("_Tasks are executed sequentially in the order listed below._");
|
|
4763
|
+
lines.push("");
|
|
4764
|
+
for (const task of plan.tasks) {
|
|
4765
|
+
lines.push(`### ${task.index}. ${task.title}`);
|
|
4766
|
+
lines.push(`- **Role:** ${task.assigneeRole}`);
|
|
4767
|
+
lines.push(`- **Priority:** ${task.priority}`);
|
|
4768
|
+
lines.push(`- **Complexity:** ${"█".repeat(task.complexity)}${"░".repeat(5 - task.complexity)} (${task.complexity}/5)`);
|
|
4769
|
+
if (task.labels.length > 0) {
|
|
4770
|
+
lines.push(`- **Labels:** ${task.labels.join(", ")}`);
|
|
4771
|
+
}
|
|
5467
4772
|
lines.push("");
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
lines.push(
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
lines.push(`- **Labels:** ${task.labels.join(", ")}`);
|
|
4773
|
+
lines.push(task.description);
|
|
4774
|
+
lines.push("");
|
|
4775
|
+
if (task.acceptanceCriteria.length > 0) {
|
|
4776
|
+
lines.push(`**Acceptance Criteria:**`);
|
|
4777
|
+
for (const ac of task.acceptanceCriteria) {
|
|
4778
|
+
lines.push(`- [ ] ${ac}`);
|
|
5475
4779
|
}
|
|
5476
4780
|
lines.push("");
|
|
5477
|
-
lines.push(task.description);
|
|
5478
|
-
lines.push("");
|
|
5479
|
-
if (task.acceptanceCriteria.length > 0) {
|
|
5480
|
-
lines.push(`**Acceptance Criteria:**`);
|
|
5481
|
-
for (const ac of task.acceptanceCriteria) {
|
|
5482
|
-
lines.push(`- [ ] ${ac}`);
|
|
5483
|
-
}
|
|
5484
|
-
lines.push("");
|
|
5485
|
-
}
|
|
5486
4781
|
}
|
|
5487
4782
|
}
|
|
5488
4783
|
if (plan.risks.length > 0) {
|
|
@@ -5503,13 +4798,12 @@ function plannedTasksToCreatePayloads(plan, sprintId) {
|
|
|
5503
4798
|
return plan.tasks.map((task) => ({
|
|
5504
4799
|
title: task.title,
|
|
5505
4800
|
description: task.description,
|
|
5506
|
-
status:
|
|
4801
|
+
status: import_shared5.TaskStatus.BACKLOG,
|
|
5507
4802
|
assigneeRole: task.assigneeRole,
|
|
5508
4803
|
priority: task.priority,
|
|
5509
4804
|
labels: task.labels,
|
|
5510
4805
|
sprintId,
|
|
5511
4806
|
order: task.index * 10,
|
|
5512
|
-
tier: task.tier,
|
|
5513
4807
|
acceptanceChecklist: task.acceptanceCriteria.map((text, i) => ({
|
|
5514
4808
|
id: `ac-${i + 1}`,
|
|
5515
4809
|
text,
|
|
@@ -5533,8 +4827,7 @@ function parseSprintPlanFromAI(raw, directive) {
|
|
|
5533
4827
|
priority: t.priority || "MEDIUM",
|
|
5534
4828
|
complexity: t.complexity || 3,
|
|
5535
4829
|
acceptanceCriteria: t.acceptanceCriteria || [],
|
|
5536
|
-
labels: t.labels || []
|
|
5537
|
-
tier: typeof t.tier === "number" ? t.tier : 0
|
|
4830
|
+
labels: t.labels || []
|
|
5538
4831
|
}));
|
|
5539
4832
|
return {
|
|
5540
4833
|
id,
|
|
@@ -5565,19 +4858,19 @@ class PlanManager {
|
|
|
5565
4858
|
save(plan) {
|
|
5566
4859
|
this.ensurePlansDir();
|
|
5567
4860
|
const slug = this.slugify(plan.name);
|
|
5568
|
-
const jsonPath =
|
|
5569
|
-
const mdPath =
|
|
5570
|
-
|
|
5571
|
-
|
|
4861
|
+
const jsonPath = import_node_path11.join(this.plansDir, `${slug}.json`);
|
|
4862
|
+
const mdPath = import_node_path11.join(this.plansDir, `sprint-${slug}.md`);
|
|
4863
|
+
import_node_fs9.writeFileSync(jsonPath, JSON.stringify(plan, null, 2), "utf-8");
|
|
4864
|
+
import_node_fs9.writeFileSync(mdPath, sprintPlanToMarkdown(plan), "utf-8");
|
|
5572
4865
|
return plan.id;
|
|
5573
4866
|
}
|
|
5574
4867
|
load(idOrSlug) {
|
|
5575
4868
|
this.ensurePlansDir();
|
|
5576
|
-
const files =
|
|
4869
|
+
const files = import_node_fs9.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
|
|
5577
4870
|
for (const file of files) {
|
|
5578
|
-
const filePath =
|
|
4871
|
+
const filePath = import_node_path11.join(this.plansDir, file);
|
|
5579
4872
|
try {
|
|
5580
|
-
const plan = JSON.parse(
|
|
4873
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(filePath, "utf-8"));
|
|
5581
4874
|
if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
|
|
5582
4875
|
return plan;
|
|
5583
4876
|
}
|
|
@@ -5587,11 +4880,11 @@ class PlanManager {
|
|
|
5587
4880
|
}
|
|
5588
4881
|
list(status) {
|
|
5589
4882
|
this.ensurePlansDir();
|
|
5590
|
-
const files =
|
|
4883
|
+
const files = import_node_fs9.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
|
|
5591
4884
|
const plans = [];
|
|
5592
4885
|
for (const file of files) {
|
|
5593
4886
|
try {
|
|
5594
|
-
const plan = JSON.parse(
|
|
4887
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(import_node_path11.join(this.plansDir, file), "utf-8"));
|
|
5595
4888
|
if (!status || plan.status === status) {
|
|
5596
4889
|
plans.push(plan);
|
|
5597
4890
|
}
|
|
@@ -5657,18 +4950,18 @@ class PlanManager {
|
|
|
5657
4950
|
}
|
|
5658
4951
|
delete(idOrSlug) {
|
|
5659
4952
|
this.ensurePlansDir();
|
|
5660
|
-
const files =
|
|
4953
|
+
const files = import_node_fs9.readdirSync(this.plansDir);
|
|
5661
4954
|
for (const file of files) {
|
|
5662
|
-
const filePath =
|
|
4955
|
+
const filePath = import_node_path11.join(this.plansDir, file);
|
|
5663
4956
|
if (!file.endsWith(".json"))
|
|
5664
4957
|
continue;
|
|
5665
4958
|
try {
|
|
5666
|
-
const plan = JSON.parse(
|
|
4959
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(filePath, "utf-8"));
|
|
5667
4960
|
if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
|
|
5668
|
-
|
|
5669
|
-
const mdPath =
|
|
5670
|
-
if (
|
|
5671
|
-
|
|
4961
|
+
import_node_fs9.unlinkSync(filePath);
|
|
4962
|
+
const mdPath = import_node_path11.join(this.plansDir, `sprint-${this.slugify(plan.name)}.md`);
|
|
4963
|
+
if (import_node_fs9.existsSync(mdPath)) {
|
|
4964
|
+
import_node_fs9.unlinkSync(mdPath);
|
|
5672
4965
|
}
|
|
5673
4966
|
return;
|
|
5674
4967
|
}
|
|
@@ -5682,8 +4975,8 @@ class PlanManager {
|
|
|
5682
4975
|
return sprintPlanToMarkdown(plan);
|
|
5683
4976
|
}
|
|
5684
4977
|
ensurePlansDir() {
|
|
5685
|
-
if (!
|
|
5686
|
-
|
|
4978
|
+
if (!import_node_fs9.existsSync(this.plansDir)) {
|
|
4979
|
+
import_node_fs9.mkdirSync(this.plansDir, { recursive: true });
|
|
5687
4980
|
}
|
|
5688
4981
|
}
|
|
5689
4982
|
slugify(name) {
|
|
@@ -5693,7 +4986,7 @@ class PlanManager {
|
|
|
5693
4986
|
// src/planning/planning-meeting.ts
|
|
5694
4987
|
init_config();
|
|
5695
4988
|
init_knowledge_base();
|
|
5696
|
-
var
|
|
4989
|
+
var import_node_fs10 = require("node:fs");
|
|
5697
4990
|
|
|
5698
4991
|
// src/planning/agents/architect.ts
|
|
5699
4992
|
function buildArchitectPrompt(input) {
|
|
@@ -5730,21 +5023,14 @@ Review and refine the Tech Lead's breakdown:
|
|
|
5730
5023
|
5. **Missing Tasks** — Add any tasks the Tech Lead missed (database migrations, configuration, testing, etc.).
|
|
5731
5024
|
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.
|
|
5732
5025
|
|
|
5733
|
-
## CRITICAL: Task
|
|
5026
|
+
## CRITICAL: Task Ordering & Dependencies
|
|
5734
5027
|
|
|
5735
|
-
Tasks are executed by
|
|
5028
|
+
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:
|
|
5736
5029
|
|
|
5737
|
-
1. **
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
2. **Detect duplicated work.** If two tasks both introduce the same environment variable, config field, dependency, helper function, or module registration — that is duplicated work. Consolidate it into ONE task.
|
|
5742
|
-
|
|
5743
|
-
3. **Do NOT split tasks that share code changes.** Even if a task is large, do NOT split it if the subtasks would both need to modify the same files. A single larger self-contained task is far better than two smaller conflicting tasks. Only split tasks when the parts are truly independent (touch completely different files and modules).
|
|
5744
|
-
|
|
5745
|
-
4. **Validate self-containment.** Each task must include ALL changes it needs to function: config, schema, module registration, implementation, and tests. A task must NOT assume another concurrent task will provide something it needs.
|
|
5746
|
-
|
|
5747
|
-
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.
|
|
5030
|
+
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.
|
|
5031
|
+
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.
|
|
5032
|
+
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.
|
|
5033
|
+
4. **Flag risks.** In your risk assessment, call out tasks that are complex or have unknowns.
|
|
5748
5034
|
|
|
5749
5035
|
## Output Format
|
|
5750
5036
|
|
|
@@ -5778,20 +5064,20 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5778
5064
|
function buildCrossTaskReviewerPrompt(input) {
|
|
5779
5065
|
let prompt = `# Role: Cross-Task Reviewer (Architect + Engineer + Planner)
|
|
5780
5066
|
|
|
5781
|
-
You are a combined Architect, Senior Engineer, and Sprint Planner performing a FINAL review of a sprint plan. Your
|
|
5067
|
+
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.
|
|
5782
5068
|
|
|
5783
5069
|
## Context
|
|
5784
5070
|
|
|
5785
|
-
In this system, tasks are
|
|
5786
|
-
-
|
|
5787
|
-
-
|
|
5788
|
-
-
|
|
5789
|
-
-
|
|
5071
|
+
In this system, tasks are executed SEQUENTIALLY by a single agent on ONE branch:
|
|
5072
|
+
- Tasks run one at a time, in the order they appear in the array
|
|
5073
|
+
- Each task's changes are committed before the next task starts
|
|
5074
|
+
- Later tasks can see and build on earlier tasks' work
|
|
5075
|
+
- The final result is a single branch with all changes, which becomes a pull request
|
|
5790
5076
|
|
|
5791
5077
|
This means:
|
|
5792
|
-
-
|
|
5793
|
-
-
|
|
5794
|
-
-
|
|
5078
|
+
- Task ordering is critical — a task must NOT depend on a later task's output
|
|
5079
|
+
- Foundation work (config, schemas, shared code) must come first
|
|
5080
|
+
- Each task should be a focused, logical unit of work
|
|
5795
5081
|
|
|
5796
5082
|
## CEO Directive
|
|
5797
5083
|
> ${input.directive}
|
|
@@ -5813,41 +5099,21 @@ ${input.sprintOrganizerOutput}
|
|
|
5813
5099
|
|
|
5814
5100
|
## Your Review Checklist
|
|
5815
5101
|
|
|
5816
|
-
Go through EACH
|
|
5817
|
-
|
|
5818
|
-
### 1. File Overlap Analysis (WITHIN the same tier)
|
|
5819
|
-
For each task, list the files it will likely modify. Then check:
|
|
5820
|
-
- Do any two tasks **in the same tier** modify the same file? (e.g., app.module.ts, configuration.ts, package.json, shared DTOs)
|
|
5821
|
-
- If yes: MERGE those tasks, move them to different tiers, or move shared changes to a foundational task in a lower tier
|
|
5822
|
-
- Note: tasks in different tiers are safe because higher tiers branch from merged lower-tier results
|
|
5102
|
+
Go through EACH task and check for:
|
|
5823
5103
|
|
|
5824
|
-
###
|
|
5825
|
-
|
|
5826
|
-
-
|
|
5827
|
-
-
|
|
5828
|
-
-
|
|
5829
|
-
- Create the same helper function, guard, interceptor, or middleware
|
|
5830
|
-
- Add the same import to a shared file
|
|
5831
|
-
If yes: consolidate into ONE task or move the shared work to a lower tier
|
|
5104
|
+
### 1. Ordering & Dependency Analysis
|
|
5105
|
+
For each task, verify:
|
|
5106
|
+
- Does it depend on any task that appears LATER in the list? If so, reorder.
|
|
5107
|
+
- Are foundational tasks (config, schemas, shared code) at the beginning?
|
|
5108
|
+
- Is the overall execution order logical?
|
|
5832
5109
|
|
|
5833
|
-
###
|
|
5110
|
+
### 2. Scope & Completeness
|
|
5834
5111
|
For each task, verify:
|
|
5835
|
-
-
|
|
5836
|
-
- Does it include ALL
|
|
5837
|
-
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
### 4. Tier Assignment Validation
|
|
5841
|
-
Verify tier assignments are correct:
|
|
5842
|
-
- Foundational tasks (schemas, config, shared code) MUST be in tier 0
|
|
5843
|
-
- Tasks that depend on another task's output must be in a HIGHER tier
|
|
5844
|
-
- Tasks in the same tier must be truly independent of each other
|
|
5845
|
-
- No circular dependencies between tiers
|
|
5846
|
-
|
|
5847
|
-
### 5. Merge Conflict Risk Zones
|
|
5848
|
-
Identify the highest-risk files (files that multiple same-tier tasks might touch) and ensure only ONE task per tier modifies each.
|
|
5849
|
-
|
|
5850
|
-
### 6. Description Quality Validation
|
|
5112
|
+
- Is the task well-scoped? Not too large, not too trivial?
|
|
5113
|
+
- Does it include ALL changes needed for its goal (given earlier tasks are done)?
|
|
5114
|
+
- Are there any missing tasks that should be added?
|
|
5115
|
+
|
|
5116
|
+
### 3. Description Quality Validation
|
|
5851
5117
|
For each task, verify the description is a clear, actionable implementation guide. Each description must specify:
|
|
5852
5118
|
- **What to do** — the specific goal and expected behavior/outcome
|
|
5853
5119
|
- **Where to do it** — specific files, modules, or directories to modify or create
|
|
@@ -5856,6 +5122,10 @@ For each task, verify the description is a clear, actionable implementation guid
|
|
|
5856
5122
|
|
|
5857
5123
|
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.
|
|
5858
5124
|
|
|
5125
|
+
### 4. Risk Assessment
|
|
5126
|
+
- Are there tasks that might fail or have unknowns?
|
|
5127
|
+
- Is the sprint scope realistic for sequential execution?
|
|
5128
|
+
|
|
5859
5129
|
## Output Format
|
|
5860
5130
|
|
|
5861
5131
|
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:
|
|
@@ -5864,10 +5134,10 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5864
5134
|
"hasIssues": true | false,
|
|
5865
5135
|
"issues": [
|
|
5866
5136
|
{
|
|
5867
|
-
"type": "
|
|
5137
|
+
"type": "wrong_order" | "missing_task" | "scope_issue" | "vague_description",
|
|
5868
5138
|
"description": "string describing the specific issue",
|
|
5869
5139
|
"affectedTasks": ["Task Title 1", "Task Title 2"],
|
|
5870
|
-
"resolution": "string describing how to fix it
|
|
5140
|
+
"resolution": "string describing how to fix it"
|
|
5871
5141
|
}
|
|
5872
5142
|
],
|
|
5873
5143
|
"revisedPlan": {
|
|
@@ -5882,8 +5152,7 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5882
5152
|
"priority": "CRITICAL | HIGH | MEDIUM | LOW",
|
|
5883
5153
|
"labels": ["string"],
|
|
5884
5154
|
"acceptanceCriteria": ["string"],
|
|
5885
|
-
"complexity": 3
|
|
5886
|
-
"tier": 0
|
|
5155
|
+
"complexity": 3
|
|
5887
5156
|
}
|
|
5888
5157
|
],
|
|
5889
5158
|
"risks": [
|
|
@@ -5897,15 +5166,11 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5897
5166
|
}
|
|
5898
5167
|
|
|
5899
5168
|
IMPORTANT:
|
|
5900
|
-
- If hasIssues is true, the revisedPlan MUST contain the corrected task list with issues resolved (
|
|
5169
|
+
- If hasIssues is true, the revisedPlan MUST contain the corrected task list with issues resolved (reordered, descriptions rewritten, missing tasks added, etc.)
|
|
5901
5170
|
- If hasIssues is false, the revisedPlan should be identical to the input plan (no changes needed)
|
|
5902
5171
|
- The revisedPlan is ALWAYS required — it becomes the final plan
|
|
5903
|
-
- When merging tasks, combine their acceptance criteria and update descriptions to cover all consolidated work
|
|
5904
5172
|
- Ensure every task description is a detailed implementation guide (what, where, how, boundaries) — rewrite vague descriptions
|
|
5905
|
-
-
|
|
5906
|
-
- Every task MUST have a "tier" field (integer >= 0)
|
|
5907
|
-
- tier 0 = foundational (runs first), tier 1 = depends on tier 0, tier 2 = depends on tier 1, etc.
|
|
5908
|
-
- Tasks within the same tier run in parallel — they MUST NOT conflict with each other`;
|
|
5173
|
+
- Tasks execute sequentially — the array order IS the execution order`;
|
|
5909
5174
|
return prompt;
|
|
5910
5175
|
}
|
|
5911
5176
|
|
|
@@ -5936,30 +5201,27 @@ Produce the final sprint plan:
|
|
|
5936
5201
|
|
|
5937
5202
|
1. **Sprint Name** — A concise, memorable name for this sprint (e.g., "User Authentication", "Payment Integration")
|
|
5938
5203
|
2. **Sprint Goal** — One paragraph describing what this sprint delivers
|
|
5939
|
-
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.
|
|
5940
|
-
4. **
|
|
5941
|
-
5. **
|
|
5942
|
-
6. **
|
|
5943
|
-
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.
|
|
5204
|
+
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.
|
|
5205
|
+
4. **Duration Estimate** — How many days this sprint will take with a single agent working sequentially
|
|
5206
|
+
5. **Final Task List** — Each task with all fields filled in, ordered by execution priority
|
|
5207
|
+
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.
|
|
5944
5208
|
|
|
5945
5209
|
Guidelines:
|
|
5946
5210
|
- The order of tasks in the array determines execution order. Tasks are dispatched sequentially from first to last.
|
|
5947
|
-
- Foundation tasks (schemas, config, shared code) must appear before tasks that build on them
|
|
5948
|
-
-
|
|
5949
|
-
- Tasks that depend on outputs from other tasks must be in a higher tier than those dependencies
|
|
5950
|
-
- Group related independent tasks in the same tier for maximum parallelism
|
|
5211
|
+
- Foundation tasks (schemas, config, shared code) must appear before tasks that build on them
|
|
5212
|
+
- Since tasks execute sequentially on one branch, later tasks can depend on earlier tasks' outputs
|
|
5951
5213
|
- Ensure acceptance criteria are specific and testable
|
|
5952
5214
|
- Keep the sprint focused — if it's too large (>12 tasks), consider reducing scope
|
|
5953
5215
|
- Ensure every task description reads as a standalone implementation brief — not a summary
|
|
5954
5216
|
|
|
5955
|
-
## CRITICAL: Task
|
|
5217
|
+
## CRITICAL: Task Ordering Validation
|
|
5956
5218
|
|
|
5957
|
-
Before finalizing, validate that
|
|
5219
|
+
Before finalizing, validate that tasks are in the correct execution order:
|
|
5958
5220
|
|
|
5959
|
-
1. **No
|
|
5960
|
-
2. **
|
|
5961
|
-
3. **Each task is independently executable
|
|
5962
|
-
4. **Prefer
|
|
5221
|
+
1. **No forward dependencies.** A task must NOT depend on a task that appears later in the list.
|
|
5222
|
+
2. **Foundation first.** Config, schemas, and shared code must come before implementation tasks.
|
|
5223
|
+
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.
|
|
5224
|
+
4. **Prefer focused, well-scoped tasks.** Each task should do one logical unit of work.
|
|
5963
5225
|
|
|
5964
5226
|
## Output Format
|
|
5965
5227
|
|
|
@@ -5977,8 +5239,7 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5977
5239
|
"priority": "CRITICAL | HIGH | MEDIUM | LOW",
|
|
5978
5240
|
"labels": ["string"],
|
|
5979
5241
|
"acceptanceCriteria": ["string"],
|
|
5980
|
-
"complexity": 3
|
|
5981
|
-
"tier": 0
|
|
5242
|
+
"complexity": 3
|
|
5982
5243
|
}
|
|
5983
5244
|
],
|
|
5984
5245
|
"risks": [
|
|
@@ -5990,12 +5251,7 @@ Your entire response must be a single JSON object — no text before it, no text
|
|
|
5990
5251
|
]
|
|
5991
5252
|
}
|
|
5992
5253
|
|
|
5993
|
-
IMPORTANT
|
|
5994
|
-
- tier 0 = foundational tasks (run first, merged before anything else)
|
|
5995
|
-
- tier 1 = tasks that depend on tier 0 outputs (run in parallel after tier 0 merges)
|
|
5996
|
-
- tier 2 = tasks that depend on tier 1 outputs (run in parallel after tier 1 merges)
|
|
5997
|
-
- Tasks within the same tier run in parallel on separate branches — they MUST NOT conflict
|
|
5998
|
-
- Every task MUST have a "tier" field (integer >= 0)`;
|
|
5254
|
+
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.`;
|
|
5999
5255
|
return prompt;
|
|
6000
5256
|
}
|
|
6001
5257
|
|
|
@@ -6053,15 +5309,14 @@ Each description MUST include:
|
|
|
6053
5309
|
Bad example: "Add authentication to the API."
|
|
6054
5310
|
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."
|
|
6055
5311
|
|
|
6056
|
-
## CRITICAL: Task
|
|
5312
|
+
## CRITICAL: Task Ordering Rules
|
|
6057
5313
|
|
|
6058
|
-
Tasks
|
|
5314
|
+
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:
|
|
6059
5315
|
|
|
6060
|
-
1. **
|
|
6061
|
-
2. **Each task must be
|
|
6062
|
-
3. **
|
|
6063
|
-
4. **
|
|
6064
|
-
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.
|
|
5316
|
+
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.
|
|
5317
|
+
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.
|
|
5318
|
+
3. **Logical ordering matters.** Tasks are dispatched in the order they appear. Ensure dependent tasks come after their prerequisites.
|
|
5319
|
+
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.
|
|
6065
5320
|
|
|
6066
5321
|
## Output Format
|
|
6067
5322
|
|
|
@@ -6153,11 +5408,11 @@ class PlanningMeeting {
|
|
|
6153
5408
|
}
|
|
6154
5409
|
getCodebaseIndex() {
|
|
6155
5410
|
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
6156
|
-
if (!
|
|
5411
|
+
if (!import_node_fs10.existsSync(indexPath)) {
|
|
6157
5412
|
return "";
|
|
6158
5413
|
}
|
|
6159
5414
|
try {
|
|
6160
|
-
const raw =
|
|
5415
|
+
const raw = import_node_fs10.readFileSync(indexPath, "utf-8");
|
|
6161
5416
|
const index = JSON.parse(raw);
|
|
6162
5417
|
const parts = [];
|
|
6163
5418
|
if (index.responsibilities) {
|
|
@@ -6180,7 +5435,3 @@ class PlanningMeeting {
|
|
|
6180
5435
|
// src/index-node.ts
|
|
6181
5436
|
init_knowledge_base();
|
|
6182
5437
|
init_colors();
|
|
6183
|
-
|
|
6184
|
-
// src/worktree/index.ts
|
|
6185
|
-
init_worktree_config();
|
|
6186
|
-
init_worktree_manager();
|