@locusai/cli 0.17.16 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/locus.js +2148 -535
- package/package.json +1 -1
package/bin/locus.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __returnValue = (v) => v;
|
|
4
5
|
function __exportSetter(name, newValue) {
|
|
@@ -14,6 +15,7 @@ var __export = (target, all) => {
|
|
|
14
15
|
});
|
|
15
16
|
};
|
|
16
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
17
19
|
|
|
18
20
|
// src/core/ai-models.ts
|
|
19
21
|
function inferProviderFromModel(model) {
|
|
@@ -520,7 +522,7 @@ var init_config = __esm(() => {
|
|
|
520
522
|
init_ai_models();
|
|
521
523
|
init_logger();
|
|
522
524
|
DEFAULT_CONFIG = {
|
|
523
|
-
version: "3.
|
|
525
|
+
version: "3.1.0",
|
|
524
526
|
github: {
|
|
525
527
|
owner: "",
|
|
526
528
|
repo: "",
|
|
@@ -545,9 +547,28 @@ var init_config = __esm(() => {
|
|
|
545
547
|
level: "normal",
|
|
546
548
|
maxFiles: 20,
|
|
547
549
|
maxTotalSizeMB: 50
|
|
550
|
+
},
|
|
551
|
+
sandbox: {
|
|
552
|
+
enabled: true,
|
|
553
|
+
extraWorkspaces: [],
|
|
554
|
+
readOnlyPaths: []
|
|
548
555
|
}
|
|
549
556
|
};
|
|
550
|
-
migrations = [
|
|
557
|
+
migrations = [
|
|
558
|
+
{
|
|
559
|
+
from: "3.0",
|
|
560
|
+
to: "3.1.0",
|
|
561
|
+
migrate: (config) => {
|
|
562
|
+
config.sandbox ??= {
|
|
563
|
+
enabled: true,
|
|
564
|
+
extraWorkspaces: [],
|
|
565
|
+
readOnlyPaths: []
|
|
566
|
+
};
|
|
567
|
+
config.version = "3.1.0";
|
|
568
|
+
return config;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
];
|
|
551
572
|
});
|
|
552
573
|
|
|
553
574
|
// src/core/context.ts
|
|
@@ -786,6 +807,14 @@ var init_rate_limiter = __esm(() => {
|
|
|
786
807
|
});
|
|
787
808
|
|
|
788
809
|
// src/display/progress.ts
|
|
810
|
+
var exports_progress = {};
|
|
811
|
+
__export(exports_progress, {
|
|
812
|
+
renderTaskStatus: () => renderTaskStatus,
|
|
813
|
+
progressBar: () => progressBar,
|
|
814
|
+
formatDuration: () => formatDuration,
|
|
815
|
+
createTimer: () => createTimer,
|
|
816
|
+
Spinner: () => Spinner
|
|
817
|
+
});
|
|
789
818
|
function progressBar(current, total, options = {}) {
|
|
790
819
|
const { width = 30, showPercent = true, showCount = true, label } = options;
|
|
791
820
|
const percent = total > 0 ? current / total : 0;
|
|
@@ -882,6 +911,149 @@ var init_progress = __esm(() => {
|
|
|
882
911
|
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
883
912
|
});
|
|
884
913
|
|
|
914
|
+
// src/core/sandbox.ts
|
|
915
|
+
var exports_sandbox = {};
|
|
916
|
+
__export(exports_sandbox, {
|
|
917
|
+
resolveSandboxMode: () => resolveSandboxMode,
|
|
918
|
+
displaySandboxWarning: () => displaySandboxWarning,
|
|
919
|
+
detectSandboxSupport: () => detectSandboxSupport
|
|
920
|
+
});
|
|
921
|
+
import { execFile } from "node:child_process";
|
|
922
|
+
import { createInterface } from "node:readline";
|
|
923
|
+
async function detectSandboxSupport() {
|
|
924
|
+
if (cachedStatus)
|
|
925
|
+
return cachedStatus;
|
|
926
|
+
const log = getLogger();
|
|
927
|
+
log.debug("Detecting Docker sandbox support...");
|
|
928
|
+
const status = await runDetection();
|
|
929
|
+
cachedStatus = status;
|
|
930
|
+
if (status.available) {
|
|
931
|
+
log.verbose("Docker sandbox support detected");
|
|
932
|
+
} else {
|
|
933
|
+
log.verbose(`Docker sandbox not available: ${status.reason}`);
|
|
934
|
+
}
|
|
935
|
+
return status;
|
|
936
|
+
}
|
|
937
|
+
function runDetection() {
|
|
938
|
+
return new Promise((resolve) => {
|
|
939
|
+
let settled = false;
|
|
940
|
+
const child = execFile("docker", ["sandbox", "ls"], { timeout: TIMEOUT_MS }, (error, _stdout, stderr) => {
|
|
941
|
+
if (settled)
|
|
942
|
+
return;
|
|
943
|
+
settled = true;
|
|
944
|
+
if (!error) {
|
|
945
|
+
resolve({ available: true });
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const code = error.code;
|
|
949
|
+
if (code === "ENOENT") {
|
|
950
|
+
resolve({ available: false, reason: "Docker is not installed" });
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (error.killed) {
|
|
954
|
+
resolve({ available: false, reason: "Docker is not responding" });
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const stderrStr = (stderr ?? "").toLowerCase();
|
|
958
|
+
if (stderrStr.includes("unknown") || stderrStr.includes("not a docker command") || stderrStr.includes("is not a docker command")) {
|
|
959
|
+
resolve({
|
|
960
|
+
available: false,
|
|
961
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
resolve({
|
|
966
|
+
available: false,
|
|
967
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
child.on?.("error", (err) => {
|
|
971
|
+
if (settled)
|
|
972
|
+
return;
|
|
973
|
+
settled = true;
|
|
974
|
+
if (err.code === "ENOENT") {
|
|
975
|
+
resolve({ available: false, reason: "Docker is not installed" });
|
|
976
|
+
} else {
|
|
977
|
+
resolve({
|
|
978
|
+
available: false,
|
|
979
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
function resolveSandboxMode(config, flags) {
|
|
986
|
+
if (flags.noSandbox) {
|
|
987
|
+
return "disabled";
|
|
988
|
+
}
|
|
989
|
+
if (flags.sandbox !== undefined) {
|
|
990
|
+
if (flags.sandbox === "require") {
|
|
991
|
+
return "required";
|
|
992
|
+
}
|
|
993
|
+
throw new Error(`Invalid --sandbox value: "${flags.sandbox}". Valid values: require`);
|
|
994
|
+
}
|
|
995
|
+
if (!config.enabled) {
|
|
996
|
+
return "disabled";
|
|
997
|
+
}
|
|
998
|
+
return "auto";
|
|
999
|
+
}
|
|
1000
|
+
async function displaySandboxWarning(mode, status) {
|
|
1001
|
+
if (mode === "required" && !status.available) {
|
|
1002
|
+
process.stderr.write(`
|
|
1003
|
+
${red("✖")} Docker sandbox required but not available: ${bold(status.reason ?? "Docker Desktop 4.58+ with sandbox support required")}
|
|
1004
|
+
`);
|
|
1005
|
+
process.stderr.write(` Install Docker Desktop 4.58+ or remove --sandbox=require to continue.
|
|
1006
|
+
|
|
1007
|
+
`);
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
if (mode === "disabled") {
|
|
1011
|
+
process.stderr.write(`
|
|
1012
|
+
${yellow("⚠")} ${bold("WARNING:")} Running without sandbox. The AI agent will have unrestricted
|
|
1013
|
+
`);
|
|
1014
|
+
process.stderr.write(` access to your filesystem, network, and environment variables.
|
|
1015
|
+
`);
|
|
1016
|
+
if (process.stdin.isTTY) {
|
|
1017
|
+
process.stderr.write(` Press ${bold("Enter")} to continue or ${bold("Ctrl+C")} to abort.
|
|
1018
|
+
`);
|
|
1019
|
+
await waitForEnter();
|
|
1020
|
+
}
|
|
1021
|
+
process.stderr.write(`
|
|
1022
|
+
`);
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
if (mode === "auto" && !status.available) {
|
|
1026
|
+
process.stderr.write(`
|
|
1027
|
+
${yellow("⚠")} Docker sandbox not available. Install Docker Desktop 4.58+ for secure execution.
|
|
1028
|
+
`);
|
|
1029
|
+
process.stderr.write(` Running without sandbox. Use ${dim("--no-sandbox")} to suppress this warning.
|
|
1030
|
+
|
|
1031
|
+
`);
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
function waitForEnter() {
|
|
1037
|
+
return new Promise((resolve) => {
|
|
1038
|
+
const rl = createInterface({
|
|
1039
|
+
input: process.stdin,
|
|
1040
|
+
output: process.stderr
|
|
1041
|
+
});
|
|
1042
|
+
rl.once("line", () => {
|
|
1043
|
+
rl.close();
|
|
1044
|
+
resolve();
|
|
1045
|
+
});
|
|
1046
|
+
rl.once("close", () => {
|
|
1047
|
+
resolve();
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
var TIMEOUT_MS = 5000, cachedStatus = null;
|
|
1052
|
+
var init_sandbox = __esm(() => {
|
|
1053
|
+
init_terminal();
|
|
1054
|
+
init_logger();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
885
1057
|
// src/commands/upgrade.ts
|
|
886
1058
|
var exports_upgrade = {};
|
|
887
1059
|
__export(exports_upgrade, {
|
|
@@ -1582,6 +1754,15 @@ ${bold("Initializing Locus...")}
|
|
|
1582
1754
|
`);
|
|
1583
1755
|
} else {
|
|
1584
1756
|
process.stderr.write(`${dim("○")} LEARNINGS.md already exists (preserved)
|
|
1757
|
+
`);
|
|
1758
|
+
}
|
|
1759
|
+
const sandboxIgnorePath = join5(cwd, ".sandboxignore");
|
|
1760
|
+
if (!existsSync5(sandboxIgnorePath)) {
|
|
1761
|
+
writeFileSync4(sandboxIgnorePath, SANDBOXIGNORE_TEMPLATE, "utf-8");
|
|
1762
|
+
process.stderr.write(`${green("✓")} Generated .sandboxignore
|
|
1763
|
+
`);
|
|
1764
|
+
} else {
|
|
1765
|
+
process.stderr.write(`${dim("○")} .sandboxignore already exists (preserved)
|
|
1585
1766
|
`);
|
|
1586
1767
|
}
|
|
1587
1768
|
process.stderr.write(`${cyan("●")} Creating GitHub labels...`);
|
|
@@ -1627,6 +1808,23 @@ ${bold(green("Locus initialized!"))}
|
|
|
1627
1808
|
process.stderr.write(` ${gray("4.")} Start coding: ${bold("locus exec")}
|
|
1628
1809
|
`);
|
|
1629
1810
|
process.stderr.write(`
|
|
1811
|
+
${bold("Sandbox mode")} ${dim("(recommended)")}
|
|
1812
|
+
`);
|
|
1813
|
+
process.stderr.write(` Run AI agents in an isolated Docker sandbox for safety.
|
|
1814
|
+
|
|
1815
|
+
`);
|
|
1816
|
+
process.stderr.write(` ${gray("1.")} ${cyan("locus sandbox")} ${dim("Create the sandbox environment")}
|
|
1817
|
+
`);
|
|
1818
|
+
process.stderr.write(` ${gray("2.")} ${cyan("locus sandbox claude")} ${dim("Login to Claude inside the sandbox")}
|
|
1819
|
+
`);
|
|
1820
|
+
process.stderr.write(` ${gray("3.")} ${cyan("locus exec")} ${dim("All commands now run sandboxed")}
|
|
1821
|
+
`);
|
|
1822
|
+
process.stderr.write(`
|
|
1823
|
+
${dim("Using Codex? Run")} ${cyan("locus sandbox codex")} ${dim("instead of step 2.")}
|
|
1824
|
+
`);
|
|
1825
|
+
process.stderr.write(` ${dim("Learn more:")} ${cyan("locus sandbox help")}
|
|
1826
|
+
`);
|
|
1827
|
+
process.stderr.write(`
|
|
1630
1828
|
`);
|
|
1631
1829
|
log.info("Locus initialized", {
|
|
1632
1830
|
owner: context.owner,
|
|
@@ -1743,6 +1941,31 @@ Read ".locus/LEARNINGS.md" **before starting any task** to avoid repeating mista
|
|
|
1743
1941
|
## Development Workflow
|
|
1744
1942
|
|
|
1745
1943
|
<!-- How to run, test, build, and deploy the project -->
|
|
1944
|
+
`, SANDBOXIGNORE_TEMPLATE = `# Files and directories to exclude from sandbox environments.
|
|
1945
|
+
# Patterns follow .gitignore syntax (one per line, # for comments).
|
|
1946
|
+
# These files will be removed from the sandbox after creation.
|
|
1947
|
+
|
|
1948
|
+
# Environment files
|
|
1949
|
+
.env
|
|
1950
|
+
.env.*
|
|
1951
|
+
!.env.example
|
|
1952
|
+
|
|
1953
|
+
# Secrets and credentials
|
|
1954
|
+
*.pem
|
|
1955
|
+
*.key
|
|
1956
|
+
*.p12
|
|
1957
|
+
*.pfx
|
|
1958
|
+
*.keystore
|
|
1959
|
+
credentials.json
|
|
1960
|
+
service-account*.json
|
|
1961
|
+
|
|
1962
|
+
# Cloud provider configs
|
|
1963
|
+
.aws/
|
|
1964
|
+
.gcp/
|
|
1965
|
+
.azure/
|
|
1966
|
+
|
|
1967
|
+
# Docker secrets
|
|
1968
|
+
docker-compose.override.yml
|
|
1746
1969
|
`, LEARNINGS_MD_TEMPLATE = `# Learnings
|
|
1747
1970
|
|
|
1748
1971
|
This file captures important lessons, decisions, and corrections made during development.
|
|
@@ -1768,7 +1991,8 @@ var init_init = __esm(() => {
|
|
|
1768
1991
|
".locus/logs/",
|
|
1769
1992
|
".locus/worktrees/",
|
|
1770
1993
|
".locus/artifacts/",
|
|
1771
|
-
".locus/discussions/"
|
|
1994
|
+
".locus/discussions/",
|
|
1995
|
+
".locus/tmp/"
|
|
1772
1996
|
];
|
|
1773
1997
|
});
|
|
1774
1998
|
|
|
@@ -2611,24 +2835,33 @@ var init_status_indicator = __esm(() => {
|
|
|
2611
2835
|
startTime = 0;
|
|
2612
2836
|
activity = "";
|
|
2613
2837
|
frame = 0;
|
|
2838
|
+
message = "";
|
|
2614
2839
|
static BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2615
2840
|
static DIAMOND = "◆";
|
|
2616
2841
|
start(message, options) {
|
|
2617
2842
|
this.stop();
|
|
2618
2843
|
this.startTime = Date.now();
|
|
2619
2844
|
this.activity = options?.activity ?? "";
|
|
2845
|
+
this.message = message;
|
|
2620
2846
|
this.frame = 0;
|
|
2621
2847
|
if (process.stderr.isTTY) {
|
|
2622
2848
|
process.stderr.write("\x1B[?25l");
|
|
2623
2849
|
}
|
|
2850
|
+
this.render();
|
|
2851
|
+
this.frame++;
|
|
2624
2852
|
this.timer = setInterval(() => {
|
|
2625
|
-
this.render(
|
|
2853
|
+
this.render();
|
|
2626
2854
|
this.frame++;
|
|
2627
2855
|
}, 80);
|
|
2628
2856
|
}
|
|
2629
2857
|
setActivity(activity) {
|
|
2630
2858
|
this.activity = activity;
|
|
2631
2859
|
}
|
|
2860
|
+
setMessage(message) {
|
|
2861
|
+
this.message = message;
|
|
2862
|
+
if (this.timer)
|
|
2863
|
+
this.render();
|
|
2864
|
+
}
|
|
2632
2865
|
stop() {
|
|
2633
2866
|
if (this.timer) {
|
|
2634
2867
|
clearInterval(this.timer);
|
|
@@ -2641,7 +2874,8 @@ var init_status_indicator = __esm(() => {
|
|
|
2641
2874
|
isActive() {
|
|
2642
2875
|
return this.timer !== null;
|
|
2643
2876
|
}
|
|
2644
|
-
render(
|
|
2877
|
+
render() {
|
|
2878
|
+
const message = this.message;
|
|
2645
2879
|
const caps = getCapabilities();
|
|
2646
2880
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
2647
2881
|
const elapsedStr = `${elapsed}s`;
|
|
@@ -2663,7 +2897,7 @@ var init_status_indicator = __esm(() => {
|
|
|
2663
2897
|
}
|
|
2664
2898
|
if (!process.stderr.isTTY)
|
|
2665
2899
|
return;
|
|
2666
|
-
process.stderr.write(
|
|
2900
|
+
process.stderr.write(`\x1B[2K\r${line}`);
|
|
2667
2901
|
}
|
|
2668
2902
|
renderShimmer() {
|
|
2669
2903
|
const t = Date.now() / 1000;
|
|
@@ -2829,14 +3063,87 @@ var init_stream_renderer = __esm(() => {
|
|
|
2829
3063
|
init_terminal();
|
|
2830
3064
|
});
|
|
2831
3065
|
|
|
3066
|
+
// src/repl/clipboard.ts
|
|
3067
|
+
import { execSync as execSync4 } from "node:child_process";
|
|
3068
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
|
|
3069
|
+
import { tmpdir } from "node:os";
|
|
3070
|
+
import { join as join9 } from "node:path";
|
|
3071
|
+
function readClipboardImage() {
|
|
3072
|
+
if (process.platform === "darwin") {
|
|
3073
|
+
return readMacOSClipboardImage();
|
|
3074
|
+
}
|
|
3075
|
+
if (process.platform === "linux") {
|
|
3076
|
+
return readLinuxClipboardImage();
|
|
3077
|
+
}
|
|
3078
|
+
return null;
|
|
3079
|
+
}
|
|
3080
|
+
function ensureStableDir() {
|
|
3081
|
+
if (!existsSync10(STABLE_DIR)) {
|
|
3082
|
+
mkdirSync7(STABLE_DIR, { recursive: true });
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
function readMacOSClipboardImage() {
|
|
3086
|
+
try {
|
|
3087
|
+
ensureStableDir();
|
|
3088
|
+
const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3089
|
+
const script = [
|
|
3090
|
+
`set destPath to POSIX file "${destPath}"`,
|
|
3091
|
+
"try",
|
|
3092
|
+
` set imgData to the clipboard as «class PNGf»`,
|
|
3093
|
+
"on error",
|
|
3094
|
+
" try",
|
|
3095
|
+
` set imgData to the clipboard as «class TIFF»`,
|
|
3096
|
+
" on error",
|
|
3097
|
+
` return "no-image"`,
|
|
3098
|
+
" end try",
|
|
3099
|
+
"end try",
|
|
3100
|
+
"set fRef to open for access destPath with write permission",
|
|
3101
|
+
"write imgData to fRef",
|
|
3102
|
+
"close access fRef",
|
|
3103
|
+
`return "ok"`
|
|
3104
|
+
].join(`
|
|
3105
|
+
`);
|
|
3106
|
+
const result = execSync4("osascript", {
|
|
3107
|
+
input: script,
|
|
3108
|
+
encoding: "utf-8",
|
|
3109
|
+
timeout: 5000,
|
|
3110
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3111
|
+
}).trim();
|
|
3112
|
+
if (result === "ok" && existsSync10(destPath)) {
|
|
3113
|
+
return destPath;
|
|
3114
|
+
}
|
|
3115
|
+
} catch {}
|
|
3116
|
+
return null;
|
|
3117
|
+
}
|
|
3118
|
+
function readLinuxClipboardImage() {
|
|
3119
|
+
try {
|
|
3120
|
+
const targets = execSync4("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
|
|
3121
|
+
if (!targets.includes("image/png")) {
|
|
3122
|
+
return null;
|
|
3123
|
+
}
|
|
3124
|
+
ensureStableDir();
|
|
3125
|
+
const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3126
|
+
execSync4(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
3127
|
+
if (existsSync10(destPath)) {
|
|
3128
|
+
return destPath;
|
|
3129
|
+
}
|
|
3130
|
+
} catch {}
|
|
3131
|
+
return null;
|
|
3132
|
+
}
|
|
3133
|
+
var STABLE_DIR;
|
|
3134
|
+
var init_clipboard = __esm(() => {
|
|
3135
|
+
STABLE_DIR = join9(tmpdir(), "locus-images");
|
|
3136
|
+
});
|
|
3137
|
+
|
|
2832
3138
|
// src/repl/image-detect.ts
|
|
2833
|
-
import { copyFileSync, existsSync as
|
|
2834
|
-
import { homedir as homedir3, tmpdir } from "node:os";
|
|
2835
|
-
import { basename, extname, join as
|
|
3139
|
+
import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
|
|
3140
|
+
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
3141
|
+
import { basename, extname, join as join10, resolve } from "node:path";
|
|
2836
3142
|
function detectImages(input) {
|
|
2837
3143
|
const detected = [];
|
|
2838
3144
|
const byResolved = new Map;
|
|
2839
|
-
|
|
3145
|
+
const sanitized = input.replace(/!\[Screenshot:[^\]]*\]\(locus:\/\/screenshot-\d+\)/g, "");
|
|
3146
|
+
for (const line of sanitized.split(`
|
|
2840
3147
|
`)) {
|
|
2841
3148
|
const trimmed = line.trim();
|
|
2842
3149
|
if (!trimmed)
|
|
@@ -2847,20 +3154,20 @@ function detectImages(input) {
|
|
|
2847
3154
|
}
|
|
2848
3155
|
}
|
|
2849
3156
|
const quotedPattern = /["']([^"']+\.(?:png|jpg|jpeg|gif|webp|bmp|svg))["']/gi;
|
|
2850
|
-
for (const match of
|
|
3157
|
+
for (const match of sanitized.matchAll(quotedPattern)) {
|
|
2851
3158
|
if (!match[0] || !match[1])
|
|
2852
3159
|
continue;
|
|
2853
3160
|
addIfImage(match[1], match[0], detected, byResolved);
|
|
2854
3161
|
}
|
|
2855
3162
|
const escapedPattern = /(?:\/|~\/|\.\/)?(?:[^\s"'\\]|\\ )+\.(?:png|jpg|jpeg|gif|webp|bmp|svg|tiff?)/gi;
|
|
2856
|
-
for (const match of
|
|
3163
|
+
for (const match of sanitized.matchAll(escapedPattern)) {
|
|
2857
3164
|
if (!match[0])
|
|
2858
3165
|
continue;
|
|
2859
3166
|
const path = match[0].replace(/\\ /g, " ");
|
|
2860
3167
|
addIfImage(path, match[0], detected, byResolved);
|
|
2861
3168
|
}
|
|
2862
3169
|
const plainPattern = /(?:\/|~\/|\.\/)[^\s"']+\.(?:png|jpg|jpeg|gif|webp|bmp|svg|tiff?)/gi;
|
|
2863
|
-
for (const match of
|
|
3170
|
+
for (const match of sanitized.matchAll(plainPattern)) {
|
|
2864
3171
|
if (!match[0])
|
|
2865
3172
|
continue;
|
|
2866
3173
|
addIfImage(match[0], match[0], detected, byResolved);
|
|
@@ -2924,13 +3231,28 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
2924
3231
|
const selected = attachments.filter((attachment) => ids.has(attachment.id));
|
|
2925
3232
|
return dedupeByResolvedPath(selected);
|
|
2926
3233
|
}
|
|
3234
|
+
function relocateImages(images, projectRoot) {
|
|
3235
|
+
const targetDir = join10(projectRoot, ".locus", "tmp", "images");
|
|
3236
|
+
for (const img of images) {
|
|
3237
|
+
if (!img.exists)
|
|
3238
|
+
continue;
|
|
3239
|
+
try {
|
|
3240
|
+
if (!existsSync11(targetDir)) {
|
|
3241
|
+
mkdirSync8(targetDir, { recursive: true });
|
|
3242
|
+
}
|
|
3243
|
+
const dest = join10(targetDir, basename(img.stablePath));
|
|
3244
|
+
copyFileSync(img.stablePath, dest);
|
|
3245
|
+
img.stablePath = dest;
|
|
3246
|
+
} catch {}
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
2927
3249
|
function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
2928
3250
|
const ext = extname(rawPath).toLowerCase();
|
|
2929
3251
|
if (!IMAGE_EXTENSIONS.has(ext))
|
|
2930
3252
|
return;
|
|
2931
3253
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
2932
3254
|
if (resolved.startsWith("~/")) {
|
|
2933
|
-
resolved =
|
|
3255
|
+
resolved = join10(homedir3(), resolved.slice(2));
|
|
2934
3256
|
}
|
|
2935
3257
|
resolved = resolve(resolved);
|
|
2936
3258
|
const existing = byResolved.get(resolved);
|
|
@@ -2943,7 +3265,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
2943
3265
|
]);
|
|
2944
3266
|
return;
|
|
2945
3267
|
}
|
|
2946
|
-
const exists =
|
|
3268
|
+
const exists = existsSync11(resolved);
|
|
2947
3269
|
let stablePath = resolved;
|
|
2948
3270
|
if (exists) {
|
|
2949
3271
|
stablePath = copyToStable(resolved);
|
|
@@ -2997,17 +3319,17 @@ function dedupeByResolvedPath(images) {
|
|
|
2997
3319
|
}
|
|
2998
3320
|
function copyToStable(sourcePath) {
|
|
2999
3321
|
try {
|
|
3000
|
-
if (!
|
|
3001
|
-
|
|
3322
|
+
if (!existsSync11(STABLE_DIR2)) {
|
|
3323
|
+
mkdirSync8(STABLE_DIR2, { recursive: true });
|
|
3002
3324
|
}
|
|
3003
|
-
const dest =
|
|
3325
|
+
const dest = join10(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
3004
3326
|
copyFileSync(sourcePath, dest);
|
|
3005
3327
|
return dest;
|
|
3006
3328
|
} catch {
|
|
3007
3329
|
return sourcePath;
|
|
3008
3330
|
}
|
|
3009
3331
|
}
|
|
3010
|
-
var IMAGE_EXTENSIONS,
|
|
3332
|
+
var IMAGE_EXTENSIONS, STABLE_DIR2, PLACEHOLDER_SCHEME = "locus://screenshot-", PLACEHOLDER_ID_PATTERN;
|
|
3011
3333
|
var init_image_detect = __esm(() => {
|
|
3012
3334
|
IMAGE_EXTENSIONS = new Set([
|
|
3013
3335
|
".png",
|
|
@@ -3020,7 +3342,7 @@ var init_image_detect = __esm(() => {
|
|
|
3020
3342
|
".tif",
|
|
3021
3343
|
".tiff"
|
|
3022
3344
|
]);
|
|
3023
|
-
|
|
3345
|
+
STABLE_DIR2 = join10(tmpdir2(), "locus-images");
|
|
3024
3346
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
3025
3347
|
});
|
|
3026
3348
|
|
|
@@ -3439,6 +3761,15 @@ ${dim("Press Ctrl+C again to exit")}\r
|
|
|
3439
3761
|
pasteBuffer += pending.slice(0, endIdx);
|
|
3440
3762
|
pending = pending.slice(endIdx + PASTE_END.length);
|
|
3441
3763
|
isPasting = false;
|
|
3764
|
+
if (pasteBuffer.trim() === "") {
|
|
3765
|
+
const clipboardImagePath = readClipboardImage();
|
|
3766
|
+
if (clipboardImagePath) {
|
|
3767
|
+
insertText(clipboardImagePath);
|
|
3768
|
+
pasteBuffer = "";
|
|
3769
|
+
render();
|
|
3770
|
+
continue;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3442
3773
|
insertText(normalizeLineEndings(pasteBuffer));
|
|
3443
3774
|
pasteBuffer = "";
|
|
3444
3775
|
render();
|
|
@@ -3730,6 +4061,7 @@ var CSI = "\x1B[", SAVE_CURSOR = "\x1B7", RESTORE_CURSOR = "\x1B8", ENABLE_BRACK
|
|
|
3730
4061
|
`, ESC = "\x1B", BACKSPACE = "", SEQ_LEFT, SEQ_RIGHT, SEQ_UP, SEQ_DOWN, SEQ_HOME, SEQ_END, SEQ_HOME_1, SEQ_END_4, SEQ_HOME_O = "\x1BOH", SEQ_END_O = "\x1BOF", SEQ_DELETE, SEQ_WORD_LEFT, SEQ_WORD_RIGHT, SEQ_SHIFT_LEFT, SEQ_SHIFT_RIGHT, SEQ_META_LEFT, SEQ_META_RIGHT, SEQ_META_SHIFT_LEFT, SEQ_META_SHIFT_RIGHT, SEQ_SHIFT_ENTER_CSI_U, SEQ_SHIFT_ENTER_MODIFY, SEQ_SHIFT_ENTER_TILDE, SEQ_ALT_ENTER, CONTROL_SEQUENCES;
|
|
3731
4062
|
var init_input_handler = __esm(() => {
|
|
3732
4063
|
init_terminal();
|
|
4064
|
+
init_clipboard();
|
|
3733
4065
|
init_image_detect();
|
|
3734
4066
|
ENABLE_BRACKETED_PASTE = `${CSI}?2004h`;
|
|
3735
4067
|
DISABLE_BRACKETED_PASTE = `${CSI}?2004l`;
|
|
@@ -3787,7 +4119,25 @@ var init_input_handler = __esm(() => {
|
|
|
3787
4119
|
});
|
|
3788
4120
|
|
|
3789
4121
|
// src/ai/claude.ts
|
|
3790
|
-
|
|
4122
|
+
var exports_claude = {};
|
|
4123
|
+
__export(exports_claude, {
|
|
4124
|
+
buildClaudeArgs: () => buildClaudeArgs,
|
|
4125
|
+
ClaudeRunner: () => ClaudeRunner
|
|
4126
|
+
});
|
|
4127
|
+
import { execSync as execSync5, spawn as spawn2 } from "node:child_process";
|
|
4128
|
+
function buildClaudeArgs(options) {
|
|
4129
|
+
const args = [
|
|
4130
|
+
"--dangerously-skip-permissions",
|
|
4131
|
+
"--no-session-persistence"
|
|
4132
|
+
];
|
|
4133
|
+
if (options.model) {
|
|
4134
|
+
args.push("--model", options.model);
|
|
4135
|
+
}
|
|
4136
|
+
if (options.verbose) {
|
|
4137
|
+
args.push("--verbose", "--output-format", "stream-json");
|
|
4138
|
+
}
|
|
4139
|
+
return args;
|
|
4140
|
+
}
|
|
3791
4141
|
|
|
3792
4142
|
class ClaudeRunner {
|
|
3793
4143
|
name = "claude";
|
|
@@ -3795,7 +4145,7 @@ class ClaudeRunner {
|
|
|
3795
4145
|
aborted = false;
|
|
3796
4146
|
async isAvailable() {
|
|
3797
4147
|
try {
|
|
3798
|
-
|
|
4148
|
+
execSync5("claude --version", {
|
|
3799
4149
|
encoding: "utf-8",
|
|
3800
4150
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3801
4151
|
});
|
|
@@ -3806,7 +4156,7 @@ class ClaudeRunner {
|
|
|
3806
4156
|
}
|
|
3807
4157
|
async getVersion() {
|
|
3808
4158
|
try {
|
|
3809
|
-
const output =
|
|
4159
|
+
const output = execSync5("claude --version", {
|
|
3810
4160
|
encoding: "utf-8",
|
|
3811
4161
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3812
4162
|
}).trim();
|
|
@@ -3818,17 +4168,7 @@ class ClaudeRunner {
|
|
|
3818
4168
|
async execute(options) {
|
|
3819
4169
|
const log = getLogger();
|
|
3820
4170
|
this.aborted = false;
|
|
3821
|
-
const args = [
|
|
3822
|
-
"--print",
|
|
3823
|
-
"--dangerously-skip-permissions",
|
|
3824
|
-
"--no-session-persistence"
|
|
3825
|
-
];
|
|
3826
|
-
if (options.model) {
|
|
3827
|
-
args.push("--model", options.model);
|
|
3828
|
-
}
|
|
3829
|
-
if (options.verbose) {
|
|
3830
|
-
args.push("--verbose", "--output-format", "stream-json");
|
|
3831
|
-
}
|
|
4171
|
+
const args = ["--print", ...buildClaudeArgs(options)];
|
|
3832
4172
|
log.debug("Spawning claude", { args: args.join(" "), cwd: options.cwd });
|
|
3833
4173
|
return new Promise((resolve2) => {
|
|
3834
4174
|
let output = "";
|
|
@@ -3979,121 +4319,709 @@ var init_claude = __esm(() => {
|
|
|
3979
4319
|
init_logger();
|
|
3980
4320
|
});
|
|
3981
4321
|
|
|
3982
|
-
// src/
|
|
3983
|
-
import {
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
4322
|
+
// src/core/sandbox-ignore.ts
|
|
4323
|
+
import { exec } from "node:child_process";
|
|
4324
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
|
|
4325
|
+
import { join as join11 } from "node:path";
|
|
4326
|
+
import { promisify } from "node:util";
|
|
4327
|
+
function parseIgnoreFile(filePath) {
|
|
4328
|
+
if (!existsSync12(filePath))
|
|
4329
|
+
return [];
|
|
4330
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
4331
|
+
const rules = [];
|
|
4332
|
+
for (const rawLine of content.split(`
|
|
4333
|
+
`)) {
|
|
4334
|
+
const line = rawLine.trim();
|
|
4335
|
+
if (!line || line.startsWith("#"))
|
|
4336
|
+
continue;
|
|
4337
|
+
const negated = line.startsWith("!");
|
|
4338
|
+
const raw = negated ? line.slice(1) : line;
|
|
4339
|
+
const isDirectory = raw.endsWith("/");
|
|
4340
|
+
const pattern = isDirectory ? raw.slice(0, -1) : raw;
|
|
4341
|
+
rules.push({ pattern, negated, isDirectory });
|
|
3988
4342
|
}
|
|
3989
|
-
|
|
3990
|
-
return args;
|
|
4343
|
+
return rules;
|
|
3991
4344
|
}
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4345
|
+
function shellEscape(s) {
|
|
4346
|
+
return s.replace(/'/g, "'\\''");
|
|
4347
|
+
}
|
|
4348
|
+
function buildCleanupScript(rules, workspacePath) {
|
|
4349
|
+
const positive = rules.filter((r) => !r.negated);
|
|
4350
|
+
const negated = rules.filter((r) => r.negated);
|
|
4351
|
+
if (positive.length === 0)
|
|
4352
|
+
return null;
|
|
4353
|
+
const exclusions = negated.map((r) => `! -name '${shellEscape(r.pattern)}'`).join(" ");
|
|
4354
|
+
const commands = [];
|
|
4355
|
+
for (const rule of positive) {
|
|
4356
|
+
const parts = ["find", `'${shellEscape(workspacePath)}'`];
|
|
4357
|
+
if (rule.isDirectory) {
|
|
4358
|
+
parts.push("-type d");
|
|
4359
|
+
}
|
|
4360
|
+
parts.push(`-name '${shellEscape(rule.pattern)}'`);
|
|
4361
|
+
if (exclusions) {
|
|
4362
|
+
parts.push(exclusions);
|
|
4363
|
+
}
|
|
4364
|
+
if (rule.isDirectory) {
|
|
4365
|
+
parts.push("-exec rm -rf {} +");
|
|
4366
|
+
} else {
|
|
4367
|
+
parts.push("-delete");
|
|
4006
4368
|
}
|
|
4369
|
+
commands.push(parts.join(" "));
|
|
4007
4370
|
}
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4371
|
+
return `${commands.join(" 2>/dev/null ; ")} 2>/dev/null`;
|
|
4372
|
+
}
|
|
4373
|
+
async function enforceSandboxIgnore(sandboxName, projectRoot) {
|
|
4374
|
+
const log = getLogger();
|
|
4375
|
+
const ignorePath = join11(projectRoot, ".sandboxignore");
|
|
4376
|
+
const rules = parseIgnoreFile(ignorePath);
|
|
4377
|
+
if (rules.length === 0)
|
|
4378
|
+
return;
|
|
4379
|
+
const script = buildCleanupScript(rules, projectRoot);
|
|
4380
|
+
if (!script)
|
|
4381
|
+
return;
|
|
4382
|
+
log.debug("Enforcing .sandboxignore", {
|
|
4383
|
+
sandboxName,
|
|
4384
|
+
ruleCount: rules.length
|
|
4385
|
+
});
|
|
4386
|
+
try {
|
|
4387
|
+
await execAsync(`docker sandbox exec ${sandboxName} sh -c ${JSON.stringify(script)}`, { timeout: 15000 });
|
|
4388
|
+
log.debug("sandbox-ignore enforcement complete", { sandboxName });
|
|
4389
|
+
} catch (err) {
|
|
4390
|
+
log.debug("sandbox-ignore enforcement failed (non-fatal)", {
|
|
4391
|
+
sandboxName,
|
|
4392
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4393
|
+
});
|
|
4018
4394
|
}
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
let rawOutput = "";
|
|
4026
|
-
let errorOutput = "";
|
|
4027
|
-
this.process = spawn3("codex", args, {
|
|
4028
|
-
cwd: options.cwd,
|
|
4029
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
4030
|
-
env: { ...process.env }
|
|
4031
|
-
});
|
|
4032
|
-
let agentMessages = [];
|
|
4033
|
-
const flushAgentMessages = () => {
|
|
4034
|
-
if (agentMessages.length > 0) {
|
|
4035
|
-
options.onOutput?.(agentMessages.join(`
|
|
4395
|
+
}
|
|
4396
|
+
var execAsync;
|
|
4397
|
+
var init_sandbox_ignore = __esm(() => {
|
|
4398
|
+
init_logger();
|
|
4399
|
+
execAsync = promisify(exec);
|
|
4400
|
+
});
|
|
4036
4401
|
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4402
|
+
// src/core/run-state.ts
|
|
4403
|
+
import {
|
|
4404
|
+
existsSync as existsSync13,
|
|
4405
|
+
mkdirSync as mkdirSync9,
|
|
4406
|
+
readFileSync as readFileSync9,
|
|
4407
|
+
unlinkSync as unlinkSync3,
|
|
4408
|
+
writeFileSync as writeFileSync6
|
|
4409
|
+
} from "node:fs";
|
|
4410
|
+
import { dirname as dirname3, join as join12 } from "node:path";
|
|
4411
|
+
function getRunStatePath(projectRoot) {
|
|
4412
|
+
return join12(projectRoot, ".locus", "run-state.json");
|
|
4413
|
+
}
|
|
4414
|
+
function loadRunState(projectRoot) {
|
|
4415
|
+
const path = getRunStatePath(projectRoot);
|
|
4416
|
+
if (!existsSync13(path))
|
|
4417
|
+
return null;
|
|
4418
|
+
try {
|
|
4419
|
+
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
4420
|
+
} catch {
|
|
4421
|
+
getLogger().warn("Corrupted run-state.json, ignoring");
|
|
4422
|
+
return null;
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
function saveRunState(projectRoot, state) {
|
|
4426
|
+
const path = getRunStatePath(projectRoot);
|
|
4427
|
+
const dir = dirname3(path);
|
|
4428
|
+
if (!existsSync13(dir)) {
|
|
4429
|
+
mkdirSync9(dir, { recursive: true });
|
|
4430
|
+
}
|
|
4431
|
+
writeFileSync6(path, `${JSON.stringify(state, null, 2)}
|
|
4432
|
+
`, "utf-8");
|
|
4433
|
+
}
|
|
4434
|
+
function clearRunState(projectRoot) {
|
|
4435
|
+
const path = getRunStatePath(projectRoot);
|
|
4436
|
+
if (existsSync13(path)) {
|
|
4437
|
+
unlinkSync3(path);
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
function createSprintRunState(sprint, branch, issues) {
|
|
4441
|
+
return {
|
|
4442
|
+
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
4443
|
+
type: "sprint",
|
|
4444
|
+
sprint,
|
|
4445
|
+
branch,
|
|
4446
|
+
startedAt: new Date().toISOString(),
|
|
4447
|
+
tasks: issues.map(({ number, order }) => ({
|
|
4448
|
+
issue: number,
|
|
4449
|
+
order,
|
|
4450
|
+
status: "pending"
|
|
4451
|
+
}))
|
|
4452
|
+
};
|
|
4453
|
+
}
|
|
4454
|
+
function createParallelRunState(issueNumbers) {
|
|
4455
|
+
return {
|
|
4456
|
+
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
4457
|
+
type: "parallel",
|
|
4458
|
+
startedAt: new Date().toISOString(),
|
|
4459
|
+
tasks: issueNumbers.map((issue, i) => ({
|
|
4460
|
+
issue,
|
|
4461
|
+
order: i + 1,
|
|
4462
|
+
status: "pending"
|
|
4463
|
+
}))
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
function markTaskInProgress(state, issueNumber) {
|
|
4467
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
4468
|
+
if (task) {
|
|
4469
|
+
task.status = "in_progress";
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
function markTaskDone(state, issueNumber, prNumber) {
|
|
4473
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
4474
|
+
if (task) {
|
|
4475
|
+
task.status = "done";
|
|
4476
|
+
task.completedAt = new Date().toISOString();
|
|
4477
|
+
if (prNumber)
|
|
4478
|
+
task.pr = prNumber;
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
function markTaskFailed(state, issueNumber, error) {
|
|
4482
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
4483
|
+
if (task) {
|
|
4484
|
+
task.status = "failed";
|
|
4485
|
+
task.failedAt = new Date().toISOString();
|
|
4486
|
+
task.error = error;
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
function getRunStats(state) {
|
|
4490
|
+
const tasks = state.tasks;
|
|
4491
|
+
return {
|
|
4492
|
+
total: tasks.length,
|
|
4493
|
+
done: tasks.filter((t) => t.status === "done").length,
|
|
4494
|
+
failed: tasks.filter((t) => t.status === "failed").length,
|
|
4495
|
+
pending: tasks.filter((t) => t.status === "pending").length,
|
|
4496
|
+
inProgress: tasks.filter((t) => t.status === "in_progress").length
|
|
4497
|
+
};
|
|
4498
|
+
}
|
|
4499
|
+
function getNextTask(state) {
|
|
4500
|
+
const failed = state.tasks.find((t) => t.status === "failed");
|
|
4501
|
+
if (failed)
|
|
4502
|
+
return failed;
|
|
4503
|
+
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
4504
|
+
}
|
|
4505
|
+
var init_run_state = __esm(() => {
|
|
4506
|
+
init_logger();
|
|
4507
|
+
});
|
|
4508
|
+
|
|
4509
|
+
// src/core/shutdown.ts
|
|
4510
|
+
import { execSync as execSync6 } from "node:child_process";
|
|
4511
|
+
function registerActiveSandbox(name) {
|
|
4512
|
+
activeSandboxes.add(name);
|
|
4513
|
+
}
|
|
4514
|
+
function unregisterActiveSandbox(name) {
|
|
4515
|
+
activeSandboxes.delete(name);
|
|
4516
|
+
}
|
|
4517
|
+
function cleanupActiveSandboxes() {
|
|
4518
|
+
for (const name of activeSandboxes) {
|
|
4519
|
+
try {
|
|
4520
|
+
execSync6(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
4521
|
+
} catch {}
|
|
4522
|
+
}
|
|
4523
|
+
activeSandboxes.clear();
|
|
4524
|
+
}
|
|
4525
|
+
function registerShutdownHandlers(ctx) {
|
|
4526
|
+
shutdownContext = ctx;
|
|
4527
|
+
interruptCount = 0;
|
|
4528
|
+
const handler = () => {
|
|
4529
|
+
interruptCount++;
|
|
4530
|
+
if (interruptCount >= 2) {
|
|
4531
|
+
process.stderr.write(`
|
|
4532
|
+
Force exit.
|
|
4533
|
+
`);
|
|
4534
|
+
process.exit(1);
|
|
4535
|
+
}
|
|
4536
|
+
process.stderr.write(`
|
|
4537
|
+
|
|
4538
|
+
Interrupted. Saving state...
|
|
4539
|
+
`);
|
|
4540
|
+
const state = shutdownContext?.getRunState?.();
|
|
4541
|
+
if (state && shutdownContext) {
|
|
4542
|
+
for (const task of state.tasks) {
|
|
4543
|
+
if (task.status === "in_progress") {
|
|
4544
|
+
task.status = "failed";
|
|
4545
|
+
task.failedAt = new Date().toISOString();
|
|
4546
|
+
task.error = "Interrupted by user";
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
try {
|
|
4550
|
+
saveRunState(shutdownContext.projectRoot, state);
|
|
4551
|
+
process.stderr.write(`State saved. Resume with: locus run --resume
|
|
4552
|
+
`);
|
|
4553
|
+
} catch {
|
|
4554
|
+
process.stderr.write(`Warning: Could not save run state.
|
|
4555
|
+
`);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
cleanupActiveSandboxes();
|
|
4559
|
+
shutdownContext?.onShutdown?.();
|
|
4560
|
+
if (interruptTimer)
|
|
4561
|
+
clearTimeout(interruptTimer);
|
|
4562
|
+
interruptTimer = setTimeout(() => {
|
|
4563
|
+
interruptCount = 0;
|
|
4564
|
+
}, 2000);
|
|
4565
|
+
setTimeout(() => {
|
|
4566
|
+
process.exit(130);
|
|
4567
|
+
}, 100);
|
|
4568
|
+
};
|
|
4569
|
+
if (!shutdownRegistered) {
|
|
4570
|
+
process.on("SIGINT", handler);
|
|
4571
|
+
process.on("SIGTERM", handler);
|
|
4572
|
+
shutdownRegistered = true;
|
|
4573
|
+
}
|
|
4574
|
+
return () => {
|
|
4575
|
+
process.removeListener("SIGINT", handler);
|
|
4576
|
+
process.removeListener("SIGTERM", handler);
|
|
4577
|
+
shutdownRegistered = false;
|
|
4578
|
+
shutdownContext = null;
|
|
4579
|
+
interruptCount = 0;
|
|
4580
|
+
if (interruptTimer) {
|
|
4581
|
+
clearTimeout(interruptTimer);
|
|
4582
|
+
interruptTimer = null;
|
|
4583
|
+
}
|
|
4584
|
+
};
|
|
4585
|
+
}
|
|
4586
|
+
var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
|
|
4587
|
+
var init_shutdown = __esm(() => {
|
|
4588
|
+
init_run_state();
|
|
4589
|
+
activeSandboxes = new Set;
|
|
4590
|
+
});
|
|
4591
|
+
|
|
4592
|
+
// src/ai/claude-sandbox.ts
|
|
4593
|
+
import { execSync as execSync7, spawn as spawn3 } from "node:child_process";
|
|
4594
|
+
|
|
4595
|
+
class SandboxedClaudeRunner {
|
|
4596
|
+
name = "claude-sandboxed";
|
|
4597
|
+
process = null;
|
|
4598
|
+
aborted = false;
|
|
4599
|
+
sandboxName = null;
|
|
4600
|
+
persistent;
|
|
4601
|
+
sandboxCreated = false;
|
|
4602
|
+
userManaged = false;
|
|
4603
|
+
constructor(persistentName, userManaged = false) {
|
|
4604
|
+
if (persistentName) {
|
|
4605
|
+
this.persistent = true;
|
|
4606
|
+
this.sandboxName = persistentName;
|
|
4607
|
+
this.userManaged = userManaged;
|
|
4608
|
+
if (userManaged) {
|
|
4609
|
+
this.sandboxCreated = true;
|
|
4610
|
+
}
|
|
4611
|
+
} else {
|
|
4612
|
+
this.persistent = false;
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
async isAvailable() {
|
|
4616
|
+
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4617
|
+
const delegate = new ClaudeRunner2;
|
|
4618
|
+
return delegate.isAvailable();
|
|
4619
|
+
}
|
|
4620
|
+
async getVersion() {
|
|
4621
|
+
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4622
|
+
const delegate = new ClaudeRunner2;
|
|
4623
|
+
return delegate.getVersion();
|
|
4624
|
+
}
|
|
4625
|
+
async execute(options) {
|
|
4626
|
+
const log = getLogger();
|
|
4627
|
+
this.aborted = false;
|
|
4628
|
+
const claudeArgs = ["-p", options.prompt, ...buildClaudeArgs(options)];
|
|
4629
|
+
let dockerArgs;
|
|
4630
|
+
if (this.persistent && !this.sandboxName) {
|
|
4631
|
+
throw new Error("Sandbox name is required");
|
|
4632
|
+
}
|
|
4633
|
+
if (this.persistent && this.sandboxCreated && await this.isSandboxRunning()) {
|
|
4634
|
+
const name = this.sandboxName;
|
|
4635
|
+
if (!name) {
|
|
4636
|
+
throw new Error("Sandbox name is required");
|
|
4637
|
+
}
|
|
4638
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
4639
|
+
await enforceSandboxIgnore(name, options.cwd);
|
|
4640
|
+
options.onStatusChange?.("Thinking...");
|
|
4641
|
+
dockerArgs = [
|
|
4642
|
+
"sandbox",
|
|
4643
|
+
"exec",
|
|
4644
|
+
"-w",
|
|
4645
|
+
options.cwd,
|
|
4646
|
+
name,
|
|
4647
|
+
"claude",
|
|
4648
|
+
...claudeArgs
|
|
4649
|
+
];
|
|
4650
|
+
} else {
|
|
4651
|
+
if (!this.persistent) {
|
|
4652
|
+
this.sandboxName = buildSandboxName(options);
|
|
4653
|
+
}
|
|
4654
|
+
const name = this.sandboxName;
|
|
4655
|
+
if (!name) {
|
|
4656
|
+
throw new Error("Sandbox name is required");
|
|
4657
|
+
}
|
|
4658
|
+
registerActiveSandbox(name);
|
|
4659
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
4660
|
+
dockerArgs = [
|
|
4661
|
+
"sandbox",
|
|
4662
|
+
"run",
|
|
4663
|
+
"--name",
|
|
4664
|
+
name,
|
|
4665
|
+
"claude",
|
|
4666
|
+
options.cwd,
|
|
4667
|
+
"--",
|
|
4668
|
+
...claudeArgs
|
|
4669
|
+
];
|
|
4670
|
+
}
|
|
4671
|
+
log.debug("Spawning sandboxed claude", {
|
|
4672
|
+
sandboxName: this.sandboxName,
|
|
4673
|
+
persistent: this.persistent,
|
|
4674
|
+
reusing: this.persistent && this.sandboxCreated,
|
|
4675
|
+
args: dockerArgs.join(" "),
|
|
4676
|
+
cwd: options.cwd
|
|
4677
|
+
});
|
|
4678
|
+
try {
|
|
4679
|
+
return await new Promise((resolve2) => {
|
|
4680
|
+
let output = "";
|
|
4681
|
+
let errorOutput = "";
|
|
4682
|
+
this.process = spawn3("docker", dockerArgs, {
|
|
4683
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4684
|
+
env: process.env
|
|
4685
|
+
});
|
|
4686
|
+
if (this.persistent && !this.sandboxCreated) {
|
|
4687
|
+
this.process.on("spawn", () => {
|
|
4688
|
+
this.sandboxCreated = true;
|
|
4689
|
+
});
|
|
4690
|
+
}
|
|
4691
|
+
if (options.verbose) {
|
|
4692
|
+
let lineBuffer = "";
|
|
4693
|
+
const seenToolIds = new Set;
|
|
4694
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4695
|
+
lineBuffer += chunk.toString();
|
|
4696
|
+
const lines = lineBuffer.split(`
|
|
4697
|
+
`);
|
|
4698
|
+
lineBuffer = lines.pop() ?? "";
|
|
4699
|
+
for (const line of lines) {
|
|
4700
|
+
if (!line.trim())
|
|
4701
|
+
continue;
|
|
4702
|
+
try {
|
|
4703
|
+
const event = JSON.parse(line);
|
|
4704
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
4705
|
+
for (const item of event.message.content) {
|
|
4706
|
+
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
4707
|
+
seenToolIds.add(item.id);
|
|
4708
|
+
options.onToolActivity?.(formatToolCall2(item.name ?? "", item.input ?? {}));
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
} else if (event.type === "result") {
|
|
4712
|
+
const text = event.result ?? "";
|
|
4713
|
+
output = text;
|
|
4714
|
+
options.onOutput?.(text);
|
|
4715
|
+
}
|
|
4716
|
+
} catch {
|
|
4717
|
+
const newLine = `${line}
|
|
4718
|
+
`;
|
|
4719
|
+
output += newLine;
|
|
4720
|
+
options.onOutput?.(newLine);
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
});
|
|
4724
|
+
} else {
|
|
4725
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4726
|
+
const text = chunk.toString();
|
|
4727
|
+
output += text;
|
|
4728
|
+
options.onOutput?.(text);
|
|
4729
|
+
});
|
|
4730
|
+
}
|
|
4731
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
4732
|
+
const text = chunk.toString();
|
|
4733
|
+
errorOutput += text;
|
|
4734
|
+
log.debug("sandboxed claude stderr", { text: text.slice(0, 500) });
|
|
4735
|
+
options.onOutput?.(text);
|
|
4736
|
+
});
|
|
4737
|
+
this.process.on("close", (code) => {
|
|
4738
|
+
this.process = null;
|
|
4739
|
+
if (this.aborted) {
|
|
4740
|
+
resolve2({
|
|
4741
|
+
success: false,
|
|
4742
|
+
output,
|
|
4743
|
+
error: "Aborted by user",
|
|
4744
|
+
exitCode: code ?? 143
|
|
4745
|
+
});
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
if (code === 0) {
|
|
4749
|
+
resolve2({
|
|
4750
|
+
success: true,
|
|
4751
|
+
output,
|
|
4752
|
+
exitCode: 0
|
|
4753
|
+
});
|
|
4754
|
+
} else {
|
|
4755
|
+
resolve2({
|
|
4756
|
+
success: false,
|
|
4757
|
+
output,
|
|
4758
|
+
error: errorOutput || `sandboxed claude exited with code ${code}`,
|
|
4759
|
+
exitCode: code ?? 1
|
|
4760
|
+
});
|
|
4761
|
+
}
|
|
4762
|
+
});
|
|
4763
|
+
this.process.on("error", (err) => {
|
|
4764
|
+
this.process = null;
|
|
4765
|
+
if (this.persistent && !this.sandboxCreated) {}
|
|
4766
|
+
resolve2({
|
|
4767
|
+
success: false,
|
|
4768
|
+
output,
|
|
4769
|
+
error: `Failed to spawn docker sandbox: ${err.message}`,
|
|
4770
|
+
exitCode: 1
|
|
4771
|
+
});
|
|
4772
|
+
});
|
|
4773
|
+
if (options.signal) {
|
|
4774
|
+
options.signal.addEventListener("abort", () => {
|
|
4775
|
+
this.abort();
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
});
|
|
4779
|
+
} finally {
|
|
4780
|
+
if (!this.persistent) {
|
|
4781
|
+
this.cleanupSandbox();
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
abort() {
|
|
4786
|
+
this.aborted = true;
|
|
4787
|
+
const log = getLogger();
|
|
4788
|
+
if (this.persistent) {
|
|
4789
|
+
log.debug("Aborting sandboxed claude (persistent — keeping sandbox)", {
|
|
4790
|
+
sandboxName: this.sandboxName
|
|
4791
|
+
});
|
|
4792
|
+
if (this.process) {
|
|
4793
|
+
this.process.kill("SIGTERM");
|
|
4794
|
+
const timer = setTimeout(() => {
|
|
4795
|
+
if (this.process) {
|
|
4796
|
+
this.process.kill("SIGKILL");
|
|
4797
|
+
}
|
|
4798
|
+
}, 3000);
|
|
4799
|
+
if (timer.unref)
|
|
4800
|
+
timer.unref();
|
|
4801
|
+
}
|
|
4802
|
+
} else {
|
|
4803
|
+
if (!this.sandboxName)
|
|
4804
|
+
return;
|
|
4805
|
+
log.debug("Aborting sandboxed claude (ephemeral — removing sandbox)", {
|
|
4806
|
+
sandboxName: this.sandboxName
|
|
4807
|
+
});
|
|
4808
|
+
try {
|
|
4809
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4810
|
+
} catch {}
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
destroy() {
|
|
4814
|
+
if (!this.sandboxName)
|
|
4815
|
+
return;
|
|
4816
|
+
if (this.userManaged) {
|
|
4817
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
4818
|
+
return;
|
|
4819
|
+
}
|
|
4820
|
+
const log = getLogger();
|
|
4821
|
+
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
4822
|
+
try {
|
|
4823
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4824
|
+
} catch {}
|
|
4825
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
4826
|
+
this.sandboxName = null;
|
|
4827
|
+
this.sandboxCreated = false;
|
|
4828
|
+
}
|
|
4829
|
+
cleanupSandbox() {
|
|
4830
|
+
if (!this.sandboxName)
|
|
4831
|
+
return;
|
|
4832
|
+
const log = getLogger();
|
|
4833
|
+
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
4834
|
+
try {
|
|
4835
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, {
|
|
4836
|
+
timeout: 60000
|
|
4837
|
+
});
|
|
4838
|
+
} catch {}
|
|
4839
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
4840
|
+
this.sandboxName = null;
|
|
4841
|
+
}
|
|
4842
|
+
async isSandboxRunning() {
|
|
4843
|
+
if (!this.sandboxName)
|
|
4844
|
+
return false;
|
|
4845
|
+
try {
|
|
4846
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
4847
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
4848
|
+
const execAsync2 = promisify2(exec2);
|
|
4849
|
+
const { stdout } = await execAsync2("docker sandbox ls", {
|
|
4850
|
+
timeout: 5000
|
|
4851
|
+
});
|
|
4852
|
+
return stdout.includes(this.sandboxName);
|
|
4853
|
+
} catch {
|
|
4854
|
+
return false;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
getSandboxName() {
|
|
4858
|
+
return this.sandboxName;
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
function buildSandboxName(options) {
|
|
4862
|
+
const ts = Date.now();
|
|
4863
|
+
if (options.activity) {
|
|
4864
|
+
const match = options.activity.match(/issue\s*#(\d+)/i);
|
|
4865
|
+
if (match) {
|
|
4866
|
+
return `locus-issue-${match[1]}-${ts}`;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
const segment = options.cwd.split("/").pop() ?? "run";
|
|
4870
|
+
return `locus-${segment}-${ts}`;
|
|
4871
|
+
}
|
|
4872
|
+
function buildPersistentSandboxName(cwd) {
|
|
4873
|
+
const segment = cwd.split("/").pop() ?? "repl";
|
|
4874
|
+
return `locus-${segment}-${Date.now()}`;
|
|
4875
|
+
}
|
|
4876
|
+
function formatToolCall2(name, input) {
|
|
4877
|
+
switch (name) {
|
|
4878
|
+
case "Read":
|
|
4879
|
+
return `reading ${input.file_path ?? ""}`;
|
|
4880
|
+
case "Write":
|
|
4881
|
+
return `writing ${input.file_path ?? ""}`;
|
|
4882
|
+
case "Edit":
|
|
4883
|
+
case "MultiEdit":
|
|
4884
|
+
return `editing ${input.file_path ?? ""}`;
|
|
4885
|
+
case "Bash":
|
|
4886
|
+
return `running: ${String(input.command ?? "").slice(0, 60)}`;
|
|
4887
|
+
case "Glob":
|
|
4888
|
+
return `glob ${input.pattern ?? ""}`;
|
|
4889
|
+
case "Grep":
|
|
4890
|
+
return `grep ${input.pattern ?? ""}`;
|
|
4891
|
+
case "LS":
|
|
4892
|
+
return `ls ${input.path ?? ""}`;
|
|
4893
|
+
case "WebFetch":
|
|
4894
|
+
return `fetching ${String(input.url ?? "").slice(0, 50)}`;
|
|
4895
|
+
case "WebSearch":
|
|
4896
|
+
return `searching: ${input.query ?? ""}`;
|
|
4897
|
+
case "Task":
|
|
4898
|
+
return `spawning agent`;
|
|
4899
|
+
default:
|
|
4900
|
+
return name;
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
var init_claude_sandbox = __esm(() => {
|
|
4904
|
+
init_logger();
|
|
4905
|
+
init_sandbox_ignore();
|
|
4906
|
+
init_shutdown();
|
|
4907
|
+
init_claude();
|
|
4908
|
+
});
|
|
4909
|
+
|
|
4910
|
+
// src/ai/codex.ts
|
|
4911
|
+
import { execSync as execSync8, spawn as spawn4 } from "node:child_process";
|
|
4912
|
+
function buildCodexArgs(model) {
|
|
4913
|
+
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
|
|
4914
|
+
if (model) {
|
|
4915
|
+
args.push("--model", model);
|
|
4916
|
+
}
|
|
4917
|
+
args.push("-");
|
|
4918
|
+
return args;
|
|
4919
|
+
}
|
|
4920
|
+
|
|
4921
|
+
class CodexRunner {
|
|
4922
|
+
name = "codex";
|
|
4923
|
+
process = null;
|
|
4924
|
+
aborted = false;
|
|
4925
|
+
async isAvailable() {
|
|
4926
|
+
try {
|
|
4927
|
+
execSync8("codex --version", {
|
|
4928
|
+
encoding: "utf-8",
|
|
4929
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4930
|
+
});
|
|
4931
|
+
return true;
|
|
4932
|
+
} catch {
|
|
4933
|
+
return false;
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
async getVersion() {
|
|
4937
|
+
try {
|
|
4938
|
+
const output = execSync8("codex --version", {
|
|
4939
|
+
encoding: "utf-8",
|
|
4940
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4941
|
+
}).trim();
|
|
4942
|
+
return output.replace(/^codex\s*/i, "");
|
|
4943
|
+
} catch {
|
|
4944
|
+
return "unknown";
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
async execute(options) {
|
|
4948
|
+
const log = getLogger();
|
|
4949
|
+
this.aborted = false;
|
|
4950
|
+
const args = buildCodexArgs(options.model);
|
|
4951
|
+
log.debug("Spawning codex", { args: args.join(" "), cwd: options.cwd });
|
|
4952
|
+
return new Promise((resolve2) => {
|
|
4953
|
+
let rawOutput = "";
|
|
4954
|
+
let errorOutput = "";
|
|
4955
|
+
this.process = spawn4("codex", args, {
|
|
4956
|
+
cwd: options.cwd,
|
|
4957
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4958
|
+
env: { ...process.env }
|
|
4959
|
+
});
|
|
4960
|
+
let agentMessages = [];
|
|
4961
|
+
const flushAgentMessages = () => {
|
|
4962
|
+
if (agentMessages.length > 0) {
|
|
4963
|
+
options.onOutput?.(agentMessages.join(`
|
|
4964
|
+
|
|
4965
|
+
`));
|
|
4966
|
+
agentMessages = [];
|
|
4967
|
+
}
|
|
4968
|
+
};
|
|
4969
|
+
let lineBuffer = "";
|
|
4970
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4971
|
+
lineBuffer += chunk.toString();
|
|
4972
|
+
const lines = lineBuffer.split(`
|
|
4973
|
+
`);
|
|
4974
|
+
lineBuffer = lines.pop() ?? "";
|
|
4975
|
+
for (const line of lines) {
|
|
4976
|
+
if (!line.trim())
|
|
4977
|
+
continue;
|
|
4978
|
+
rawOutput += `${line}
|
|
4979
|
+
`;
|
|
4980
|
+
log.debug("codex stdout line", { line });
|
|
4981
|
+
try {
|
|
4982
|
+
const event = JSON.parse(line);
|
|
4983
|
+
const { type, item } = event;
|
|
4984
|
+
if (type === "item.started" && item?.type === "command_execution") {
|
|
4985
|
+
const cmd = (item.command ?? "").split(`
|
|
4986
|
+
`)[0].slice(0, 80);
|
|
4987
|
+
options.onToolActivity?.(`running: ${cmd}`);
|
|
4988
|
+
} else if (type === "item.completed" && item?.type === "command_execution") {
|
|
4989
|
+
const code = item.exit_code;
|
|
4990
|
+
options.onToolActivity?.(code === 0 ? "done" : `exit ${code}`);
|
|
4991
|
+
} else if (type === "item.completed" && item?.type === "reasoning") {
|
|
4992
|
+
const text = (item.text ?? "").trim().replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
|
|
4993
|
+
if (text)
|
|
4994
|
+
options.onToolActivity?.(text);
|
|
4995
|
+
} else if (type === "item.completed" && item?.type === "agent_message") {
|
|
4996
|
+
const text = item.text ?? "";
|
|
4997
|
+
if (text) {
|
|
4998
|
+
agentMessages.push(text);
|
|
4999
|
+
options.onToolActivity?.(text.split(`
|
|
5000
|
+
`)[0].slice(0, 80));
|
|
5001
|
+
}
|
|
5002
|
+
} else if (type === "turn.completed") {
|
|
5003
|
+
flushAgentMessages();
|
|
5004
|
+
}
|
|
5005
|
+
} catch {
|
|
5006
|
+
const newLine = `${line}
|
|
5007
|
+
`;
|
|
5008
|
+
rawOutput += newLine;
|
|
5009
|
+
options.onOutput?.(newLine);
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
});
|
|
5013
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
5014
|
+
const text = chunk.toString();
|
|
5015
|
+
errorOutput += text;
|
|
5016
|
+
log.debug("codex stderr", { text: text.slice(0, 500) });
|
|
5017
|
+
});
|
|
5018
|
+
this.process.on("close", (code) => {
|
|
5019
|
+
this.process = null;
|
|
5020
|
+
flushAgentMessages();
|
|
5021
|
+
if (this.aborted) {
|
|
5022
|
+
resolve2({
|
|
5023
|
+
success: false,
|
|
5024
|
+
output: rawOutput,
|
|
4097
5025
|
error: "Aborted by user",
|
|
4098
5026
|
exitCode: code ?? 143
|
|
4099
5027
|
});
|
|
@@ -4122,52 +5050,398 @@ class CodexRunner {
|
|
|
4122
5050
|
error: `Failed to spawn codex: ${err.message}`,
|
|
4123
5051
|
exitCode: 1
|
|
4124
5052
|
});
|
|
4125
|
-
});
|
|
4126
|
-
if (options.signal) {
|
|
4127
|
-
options.signal.addEventListener("abort", () => {
|
|
4128
|
-
this.abort();
|
|
5053
|
+
});
|
|
5054
|
+
if (options.signal) {
|
|
5055
|
+
options.signal.addEventListener("abort", () => {
|
|
5056
|
+
this.abort();
|
|
5057
|
+
});
|
|
5058
|
+
}
|
|
5059
|
+
this.process.stdin?.write(options.prompt);
|
|
5060
|
+
this.process.stdin?.end();
|
|
5061
|
+
});
|
|
5062
|
+
}
|
|
5063
|
+
abort() {
|
|
5064
|
+
if (!this.process)
|
|
5065
|
+
return;
|
|
5066
|
+
this.aborted = true;
|
|
5067
|
+
const log = getLogger();
|
|
5068
|
+
log.debug("Aborting codex process");
|
|
5069
|
+
this.process.kill("SIGTERM");
|
|
5070
|
+
const forceKillTimer = setTimeout(() => {
|
|
5071
|
+
if (this.process) {
|
|
5072
|
+
log.debug("Force killing codex process");
|
|
5073
|
+
this.process.kill("SIGKILL");
|
|
5074
|
+
}
|
|
5075
|
+
}, 3000);
|
|
5076
|
+
if (forceKillTimer.unref) {
|
|
5077
|
+
forceKillTimer.unref();
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
var init_codex = __esm(() => {
|
|
5082
|
+
init_logger();
|
|
5083
|
+
});
|
|
5084
|
+
|
|
5085
|
+
// src/ai/codex-sandbox.ts
|
|
5086
|
+
import { execSync as execSync9, spawn as spawn5 } from "node:child_process";
|
|
5087
|
+
|
|
5088
|
+
class SandboxedCodexRunner {
|
|
5089
|
+
name = "codex-sandboxed";
|
|
5090
|
+
process = null;
|
|
5091
|
+
aborted = false;
|
|
5092
|
+
sandboxName = null;
|
|
5093
|
+
persistent;
|
|
5094
|
+
sandboxCreated = false;
|
|
5095
|
+
userManaged = false;
|
|
5096
|
+
codexInstalled = false;
|
|
5097
|
+
constructor(persistentName, userManaged = false) {
|
|
5098
|
+
if (persistentName) {
|
|
5099
|
+
this.persistent = true;
|
|
5100
|
+
this.sandboxName = persistentName;
|
|
5101
|
+
this.userManaged = userManaged;
|
|
5102
|
+
if (userManaged) {
|
|
5103
|
+
this.sandboxCreated = true;
|
|
5104
|
+
}
|
|
5105
|
+
} else {
|
|
5106
|
+
this.persistent = false;
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
async isAvailable() {
|
|
5110
|
+
const delegate = new CodexRunner;
|
|
5111
|
+
return delegate.isAvailable();
|
|
5112
|
+
}
|
|
5113
|
+
async getVersion() {
|
|
5114
|
+
const delegate = new CodexRunner;
|
|
5115
|
+
return delegate.getVersion();
|
|
5116
|
+
}
|
|
5117
|
+
async execute(options) {
|
|
5118
|
+
const log = getLogger();
|
|
5119
|
+
this.aborted = false;
|
|
5120
|
+
const codexArgs = buildCodexArgs(options.model);
|
|
5121
|
+
let dockerArgs;
|
|
5122
|
+
if (this.persistent && !this.sandboxName) {
|
|
5123
|
+
throw new Error("Sandbox name is required");
|
|
5124
|
+
}
|
|
5125
|
+
if (this.persistent && this.sandboxCreated && await this.isSandboxRunning()) {
|
|
5126
|
+
const name = this.sandboxName;
|
|
5127
|
+
if (!name) {
|
|
5128
|
+
throw new Error("Sandbox name is required");
|
|
5129
|
+
}
|
|
5130
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
5131
|
+
await enforceSandboxIgnore(name, options.cwd);
|
|
5132
|
+
if (!this.codexInstalled) {
|
|
5133
|
+
options.onStatusChange?.("Checking codex...");
|
|
5134
|
+
await this.ensureCodexInstalled(name);
|
|
5135
|
+
this.codexInstalled = true;
|
|
5136
|
+
}
|
|
5137
|
+
options.onStatusChange?.("Thinking...");
|
|
5138
|
+
dockerArgs = [
|
|
5139
|
+
"sandbox",
|
|
5140
|
+
"exec",
|
|
5141
|
+
"-i",
|
|
5142
|
+
"-w",
|
|
5143
|
+
options.cwd,
|
|
5144
|
+
name,
|
|
5145
|
+
"codex",
|
|
5146
|
+
...codexArgs
|
|
5147
|
+
];
|
|
5148
|
+
} else {
|
|
5149
|
+
if (!this.persistent) {
|
|
5150
|
+
this.sandboxName = buildSandboxName2(options);
|
|
5151
|
+
}
|
|
5152
|
+
const name = this.sandboxName;
|
|
5153
|
+
if (!name) {
|
|
5154
|
+
throw new Error("Sandbox name is required");
|
|
5155
|
+
}
|
|
5156
|
+
registerActiveSandbox(name);
|
|
5157
|
+
options.onStatusChange?.("Creating sandbox...");
|
|
5158
|
+
await this.createSandboxWithClaude(name, options.cwd);
|
|
5159
|
+
options.onStatusChange?.("Installing codex...");
|
|
5160
|
+
await this.ensureCodexInstalled(name);
|
|
5161
|
+
this.codexInstalled = true;
|
|
5162
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
5163
|
+
await enforceSandboxIgnore(name, options.cwd);
|
|
5164
|
+
options.onStatusChange?.("Thinking...");
|
|
5165
|
+
dockerArgs = [
|
|
5166
|
+
"sandbox",
|
|
5167
|
+
"exec",
|
|
5168
|
+
"-i",
|
|
5169
|
+
"-w",
|
|
5170
|
+
options.cwd,
|
|
5171
|
+
name,
|
|
5172
|
+
"codex",
|
|
5173
|
+
...codexArgs
|
|
5174
|
+
];
|
|
5175
|
+
}
|
|
5176
|
+
log.debug("Spawning sandboxed codex", {
|
|
5177
|
+
sandboxName: this.sandboxName,
|
|
5178
|
+
persistent: this.persistent,
|
|
5179
|
+
reusing: this.persistent && this.sandboxCreated,
|
|
5180
|
+
args: dockerArgs.join(" "),
|
|
5181
|
+
cwd: options.cwd
|
|
5182
|
+
});
|
|
5183
|
+
try {
|
|
5184
|
+
return await new Promise((resolve2) => {
|
|
5185
|
+
let rawOutput = "";
|
|
5186
|
+
let errorOutput = "";
|
|
5187
|
+
this.process = spawn5("docker", dockerArgs, {
|
|
5188
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5189
|
+
env: process.env
|
|
5190
|
+
});
|
|
5191
|
+
if (this.persistent && !this.sandboxCreated) {
|
|
5192
|
+
this.process.on("spawn", () => {
|
|
5193
|
+
this.sandboxCreated = true;
|
|
5194
|
+
});
|
|
5195
|
+
}
|
|
5196
|
+
let agentMessages = [];
|
|
5197
|
+
const flushAgentMessages = () => {
|
|
5198
|
+
if (agentMessages.length > 0) {
|
|
5199
|
+
options.onOutput?.(agentMessages.join(`
|
|
5200
|
+
|
|
5201
|
+
`));
|
|
5202
|
+
agentMessages = [];
|
|
5203
|
+
}
|
|
5204
|
+
};
|
|
5205
|
+
let lineBuffer = "";
|
|
5206
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
5207
|
+
lineBuffer += chunk.toString();
|
|
5208
|
+
const lines = lineBuffer.split(`
|
|
5209
|
+
`);
|
|
5210
|
+
lineBuffer = lines.pop() ?? "";
|
|
5211
|
+
for (const line of lines) {
|
|
5212
|
+
if (!line.trim())
|
|
5213
|
+
continue;
|
|
5214
|
+
rawOutput += `${line}
|
|
5215
|
+
`;
|
|
5216
|
+
log.debug("sandboxed codex stdout line", { line });
|
|
5217
|
+
try {
|
|
5218
|
+
const event = JSON.parse(line);
|
|
5219
|
+
const { type, item } = event;
|
|
5220
|
+
if (type === "item.started" && item?.type === "command_execution") {
|
|
5221
|
+
const cmd = (item.command ?? "").split(`
|
|
5222
|
+
`)[0].slice(0, 80);
|
|
5223
|
+
options.onToolActivity?.(`running: ${cmd}`);
|
|
5224
|
+
} else if (type === "item.completed" && item?.type === "command_execution") {
|
|
5225
|
+
const code = item.exit_code;
|
|
5226
|
+
options.onToolActivity?.(code === 0 ? "done" : `exit ${code}`);
|
|
5227
|
+
} else if (type === "item.completed" && item?.type === "reasoning") {
|
|
5228
|
+
const text = (item.text ?? "").trim().replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
|
|
5229
|
+
if (text)
|
|
5230
|
+
options.onToolActivity?.(text);
|
|
5231
|
+
} else if (type === "item.completed" && item?.type === "agent_message") {
|
|
5232
|
+
const text = item.text ?? "";
|
|
5233
|
+
if (text) {
|
|
5234
|
+
agentMessages.push(text);
|
|
5235
|
+
options.onToolActivity?.(text.split(`
|
|
5236
|
+
`)[0].slice(0, 80));
|
|
5237
|
+
}
|
|
5238
|
+
} else if (type === "turn.completed") {
|
|
5239
|
+
flushAgentMessages();
|
|
5240
|
+
}
|
|
5241
|
+
} catch {
|
|
5242
|
+
const newLine = `${line}
|
|
5243
|
+
`;
|
|
5244
|
+
rawOutput += newLine;
|
|
5245
|
+
options.onOutput?.(newLine);
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
});
|
|
5249
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
5250
|
+
const text = chunk.toString();
|
|
5251
|
+
errorOutput += text;
|
|
5252
|
+
log.debug("sandboxed codex stderr", { text: text.slice(0, 500) });
|
|
5253
|
+
});
|
|
5254
|
+
this.process.on("close", (code) => {
|
|
5255
|
+
this.process = null;
|
|
5256
|
+
flushAgentMessages();
|
|
5257
|
+
if (this.aborted) {
|
|
5258
|
+
resolve2({
|
|
5259
|
+
success: false,
|
|
5260
|
+
output: rawOutput,
|
|
5261
|
+
error: "Aborted by user",
|
|
5262
|
+
exitCode: code ?? 143
|
|
5263
|
+
});
|
|
5264
|
+
return;
|
|
5265
|
+
}
|
|
5266
|
+
if (code === 0) {
|
|
5267
|
+
resolve2({
|
|
5268
|
+
success: true,
|
|
5269
|
+
output: rawOutput,
|
|
5270
|
+
exitCode: 0
|
|
5271
|
+
});
|
|
5272
|
+
} else {
|
|
5273
|
+
resolve2({
|
|
5274
|
+
success: false,
|
|
5275
|
+
output: rawOutput,
|
|
5276
|
+
error: errorOutput || `sandboxed codex exited with code ${code}`,
|
|
5277
|
+
exitCode: code ?? 1
|
|
5278
|
+
});
|
|
5279
|
+
}
|
|
5280
|
+
});
|
|
5281
|
+
this.process.on("error", (err) => {
|
|
5282
|
+
this.process = null;
|
|
5283
|
+
if (this.persistent && !this.sandboxCreated) {}
|
|
5284
|
+
resolve2({
|
|
5285
|
+
success: false,
|
|
5286
|
+
output: rawOutput,
|
|
5287
|
+
error: `Failed to spawn docker sandbox: ${err.message}`,
|
|
5288
|
+
exitCode: 1
|
|
5289
|
+
});
|
|
4129
5290
|
});
|
|
5291
|
+
if (options.signal) {
|
|
5292
|
+
options.signal.addEventListener("abort", () => {
|
|
5293
|
+
this.abort();
|
|
5294
|
+
});
|
|
5295
|
+
}
|
|
5296
|
+
this.process.stdin?.write(options.prompt);
|
|
5297
|
+
this.process.stdin?.end();
|
|
5298
|
+
});
|
|
5299
|
+
} finally {
|
|
5300
|
+
if (!this.persistent) {
|
|
5301
|
+
this.cleanupSandbox();
|
|
4130
5302
|
}
|
|
4131
|
-
|
|
4132
|
-
this.process.stdin?.end();
|
|
4133
|
-
});
|
|
5303
|
+
}
|
|
4134
5304
|
}
|
|
4135
5305
|
abort() {
|
|
4136
|
-
if (!this.process)
|
|
4137
|
-
return;
|
|
4138
5306
|
this.aborted = true;
|
|
4139
5307
|
const log = getLogger();
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
5308
|
+
if (this.persistent) {
|
|
5309
|
+
log.debug("Aborting sandboxed codex (persistent — keeping sandbox)", {
|
|
5310
|
+
sandboxName: this.sandboxName
|
|
5311
|
+
});
|
|
4143
5312
|
if (this.process) {
|
|
4144
|
-
|
|
4145
|
-
|
|
5313
|
+
this.process.kill("SIGTERM");
|
|
5314
|
+
const timer = setTimeout(() => {
|
|
5315
|
+
if (this.process) {
|
|
5316
|
+
this.process.kill("SIGKILL");
|
|
5317
|
+
}
|
|
5318
|
+
}, 3000);
|
|
5319
|
+
if (timer.unref)
|
|
5320
|
+
timer.unref();
|
|
4146
5321
|
}
|
|
4147
|
-
}
|
|
4148
|
-
|
|
4149
|
-
|
|
5322
|
+
} else {
|
|
5323
|
+
if (!this.sandboxName)
|
|
5324
|
+
return;
|
|
5325
|
+
log.debug("Aborting sandboxed codex (ephemeral — removing sandbox)", {
|
|
5326
|
+
sandboxName: this.sandboxName
|
|
5327
|
+
});
|
|
5328
|
+
try {
|
|
5329
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5330
|
+
} catch {}
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
destroy() {
|
|
5334
|
+
if (!this.sandboxName)
|
|
5335
|
+
return;
|
|
5336
|
+
if (this.userManaged) {
|
|
5337
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
const log = getLogger();
|
|
5341
|
+
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
5342
|
+
try {
|
|
5343
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5344
|
+
} catch {}
|
|
5345
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
5346
|
+
this.sandboxName = null;
|
|
5347
|
+
this.sandboxCreated = false;
|
|
5348
|
+
}
|
|
5349
|
+
cleanupSandbox() {
|
|
5350
|
+
if (!this.sandboxName)
|
|
5351
|
+
return;
|
|
5352
|
+
const log = getLogger();
|
|
5353
|
+
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
5354
|
+
try {
|
|
5355
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, {
|
|
5356
|
+
timeout: 60000
|
|
5357
|
+
});
|
|
5358
|
+
} catch {}
|
|
5359
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
5360
|
+
this.sandboxName = null;
|
|
5361
|
+
}
|
|
5362
|
+
async isSandboxRunning() {
|
|
5363
|
+
if (!this.sandboxName)
|
|
5364
|
+
return false;
|
|
5365
|
+
try {
|
|
5366
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5367
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
5368
|
+
const execAsync2 = promisify2(exec2);
|
|
5369
|
+
const { stdout } = await execAsync2("docker sandbox ls", {
|
|
5370
|
+
timeout: 5000
|
|
5371
|
+
});
|
|
5372
|
+
return stdout.includes(this.sandboxName);
|
|
5373
|
+
} catch {
|
|
5374
|
+
return false;
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
async createSandboxWithClaude(name, cwd) {
|
|
5378
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5379
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
5380
|
+
const execAsync2 = promisify2(exec2);
|
|
5381
|
+
try {
|
|
5382
|
+
await execAsync2(`docker sandbox run --name ${name} claude ${cwd} -- --version`, { timeout: 120000 });
|
|
5383
|
+
} catch {}
|
|
5384
|
+
}
|
|
5385
|
+
async ensureCodexInstalled(name) {
|
|
5386
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5387
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
5388
|
+
const execAsync2 = promisify2(exec2);
|
|
5389
|
+
try {
|
|
5390
|
+
await execAsync2(`docker sandbox exec ${name} which codex`, {
|
|
5391
|
+
timeout: 5000
|
|
5392
|
+
});
|
|
5393
|
+
} catch {
|
|
5394
|
+
await execAsync2(`docker sandbox exec ${name} npm install -g @openai/codex`, { timeout: 120000 });
|
|
4150
5395
|
}
|
|
4151
5396
|
}
|
|
5397
|
+
getSandboxName() {
|
|
5398
|
+
return this.sandboxName;
|
|
5399
|
+
}
|
|
4152
5400
|
}
|
|
4153
|
-
|
|
5401
|
+
function buildSandboxName2(options) {
|
|
5402
|
+
const ts = Date.now();
|
|
5403
|
+
if (options.activity) {
|
|
5404
|
+
const match = options.activity.match(/issue\s*#(\d+)/i);
|
|
5405
|
+
if (match) {
|
|
5406
|
+
return `locus-codex-issue-${match[1]}-${ts}`;
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
const segment = options.cwd.split("/").pop() ?? "run";
|
|
5410
|
+
return `locus-codex-${segment}-${ts}`;
|
|
5411
|
+
}
|
|
5412
|
+
var init_codex_sandbox = __esm(() => {
|
|
4154
5413
|
init_logger();
|
|
5414
|
+
init_sandbox_ignore();
|
|
5415
|
+
init_shutdown();
|
|
5416
|
+
init_codex();
|
|
4155
5417
|
});
|
|
4156
5418
|
|
|
4157
5419
|
// src/ai/runner.ts
|
|
4158
|
-
async function createRunnerAsync(provider) {
|
|
5420
|
+
async function createRunnerAsync(provider, sandboxed) {
|
|
5421
|
+
switch (provider) {
|
|
5422
|
+
case "claude":
|
|
5423
|
+
return sandboxed ? new SandboxedClaudeRunner : new ClaudeRunner;
|
|
5424
|
+
case "codex":
|
|
5425
|
+
return sandboxed ? new SandboxedCodexRunner : new CodexRunner;
|
|
5426
|
+
default:
|
|
5427
|
+
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
function createUserManagedSandboxRunner(provider, sandboxName) {
|
|
4159
5431
|
switch (provider) {
|
|
4160
5432
|
case "claude":
|
|
4161
|
-
return new
|
|
5433
|
+
return new SandboxedClaudeRunner(sandboxName, true);
|
|
4162
5434
|
case "codex":
|
|
4163
|
-
return new
|
|
5435
|
+
return new SandboxedCodexRunner(sandboxName, true);
|
|
4164
5436
|
default:
|
|
4165
5437
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
4166
5438
|
}
|
|
4167
5439
|
}
|
|
4168
5440
|
var init_runner = __esm(() => {
|
|
4169
5441
|
init_claude();
|
|
5442
|
+
init_claude_sandbox();
|
|
4170
5443
|
init_codex();
|
|
5444
|
+
init_codex_sandbox();
|
|
4171
5445
|
});
|
|
4172
5446
|
|
|
4173
5447
|
// src/ai/run-ai.ts
|
|
@@ -4175,6 +5449,55 @@ var exports_run_ai = {};
|
|
|
4175
5449
|
__export(exports_run_ai, {
|
|
4176
5450
|
runAI: () => runAI
|
|
4177
5451
|
});
|
|
5452
|
+
function normalizeErrorMessage(error) {
|
|
5453
|
+
if (!error)
|
|
5454
|
+
return;
|
|
5455
|
+
const trimmed = error.trim();
|
|
5456
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
5457
|
+
}
|
|
5458
|
+
function stripAnsi2(text) {
|
|
5459
|
+
return text.replace(/\u001B\[[0-9;]*[A-Za-z]/g, "");
|
|
5460
|
+
}
|
|
5461
|
+
function extractErrorFromStructuredLine(line) {
|
|
5462
|
+
try {
|
|
5463
|
+
const parsed = JSON.parse(line);
|
|
5464
|
+
const candidateValues = [
|
|
5465
|
+
parsed.error,
|
|
5466
|
+
parsed.message,
|
|
5467
|
+
parsed.text,
|
|
5468
|
+
typeof parsed.item === "object" && parsed.item ? parsed.item.error : undefined,
|
|
5469
|
+
typeof parsed.item === "object" && parsed.item ? parsed.item.message : undefined,
|
|
5470
|
+
typeof parsed.item === "object" && parsed.item ? parsed.item.text : undefined
|
|
5471
|
+
];
|
|
5472
|
+
for (const value of candidateValues) {
|
|
5473
|
+
if (typeof value !== "string")
|
|
5474
|
+
continue;
|
|
5475
|
+
const normalized = normalizeErrorMessage(stripAnsi2(value));
|
|
5476
|
+
if (normalized)
|
|
5477
|
+
return normalized;
|
|
5478
|
+
}
|
|
5479
|
+
return;
|
|
5480
|
+
} catch {
|
|
5481
|
+
return;
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
function extractErrorFromOutput(output) {
|
|
5485
|
+
if (!output)
|
|
5486
|
+
return;
|
|
5487
|
+
const lines = output.split(`
|
|
5488
|
+
`);
|
|
5489
|
+
for (let index = lines.length - 1;index >= 0; index--) {
|
|
5490
|
+
const rawLine = lines[index] ?? "";
|
|
5491
|
+
const line = normalizeErrorMessage(stripAnsi2(rawLine));
|
|
5492
|
+
if (!line)
|
|
5493
|
+
continue;
|
|
5494
|
+
const structured = extractErrorFromStructuredLine(line);
|
|
5495
|
+
if (structured)
|
|
5496
|
+
return structured.slice(0, 500);
|
|
5497
|
+
return line.slice(0, 500);
|
|
5498
|
+
}
|
|
5499
|
+
return;
|
|
5500
|
+
}
|
|
4178
5501
|
async function runAI(options) {
|
|
4179
5502
|
const indicator = getStatusIndicator();
|
|
4180
5503
|
const renderer = options.silent ? null : new StreamRenderer;
|
|
@@ -4207,7 +5530,13 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
4207
5530
|
indicator.start("Thinking...", {
|
|
4208
5531
|
activity: options.activity
|
|
4209
5532
|
});
|
|
4210
|
-
|
|
5533
|
+
if (options.runner) {
|
|
5534
|
+
runner = options.runner;
|
|
5535
|
+
} else if (options.sandboxName) {
|
|
5536
|
+
runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName);
|
|
5537
|
+
} else {
|
|
5538
|
+
runner = await createRunnerAsync(resolvedProvider, options.sandboxed ?? true);
|
|
5539
|
+
}
|
|
4211
5540
|
const available = await runner.isAvailable();
|
|
4212
5541
|
if (!available) {
|
|
4213
5542
|
indicator.stop();
|
|
@@ -4227,6 +5556,7 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
4227
5556
|
cwd: options.cwd,
|
|
4228
5557
|
signal: abortController.signal,
|
|
4229
5558
|
verbose: options.verbose,
|
|
5559
|
+
activity: options.activity,
|
|
4230
5560
|
onOutput: (chunk) => {
|
|
4231
5561
|
if (wasAborted)
|
|
4232
5562
|
return;
|
|
@@ -4237,6 +5567,9 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
4237
5567
|
renderer?.push(chunk);
|
|
4238
5568
|
output += chunk;
|
|
4239
5569
|
},
|
|
5570
|
+
onStatusChange: (message) => {
|
|
5571
|
+
indicator.setMessage(message);
|
|
5572
|
+
},
|
|
4240
5573
|
onToolActivity: (() => {
|
|
4241
5574
|
let lastActivityTime = 0;
|
|
4242
5575
|
return (summary) => {
|
|
@@ -4261,20 +5594,25 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
4261
5594
|
exitCode: result.exitCode
|
|
4262
5595
|
};
|
|
4263
5596
|
}
|
|
5597
|
+
const normalizedRunnerError = normalizeErrorMessage(result.error);
|
|
5598
|
+
const extractedOutputError = extractErrorFromOutput(result.output);
|
|
5599
|
+
const fallbackError = `${runner.name} failed with exit code ${result.exitCode}.`;
|
|
4264
5600
|
return {
|
|
4265
5601
|
success: result.success,
|
|
4266
5602
|
output,
|
|
4267
|
-
error: result.
|
|
5603
|
+
error: result.success ? undefined : normalizedRunnerError ?? extractedOutputError ?? fallbackError,
|
|
4268
5604
|
interrupted: false,
|
|
4269
5605
|
exitCode: result.exitCode
|
|
4270
5606
|
};
|
|
4271
5607
|
} catch (e) {
|
|
4272
5608
|
indicator.stop();
|
|
4273
5609
|
renderer?.stop();
|
|
5610
|
+
const normalizedCaughtError = normalizeErrorMessage(e instanceof Error ? e.message : String(e));
|
|
5611
|
+
const fallbackError = `${resolvedProvider} runner failed unexpectedly.`;
|
|
4274
5612
|
return {
|
|
4275
5613
|
success: false,
|
|
4276
5614
|
output,
|
|
4277
|
-
error:
|
|
5615
|
+
error: normalizedCaughtError ?? fallbackError,
|
|
4278
5616
|
interrupted: wasAborted,
|
|
4279
5617
|
exitCode: 1
|
|
4280
5618
|
};
|
|
@@ -4405,7 +5743,7 @@ var exports_issue = {};
|
|
|
4405
5743
|
__export(exports_issue, {
|
|
4406
5744
|
issueCommand: () => issueCommand
|
|
4407
5745
|
});
|
|
4408
|
-
import { createInterface } from "node:readline";
|
|
5746
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
4409
5747
|
function parseIssueArgs(args) {
|
|
4410
5748
|
const flags = {};
|
|
4411
5749
|
const positional = [];
|
|
@@ -4517,7 +5855,9 @@ async function issueCreate(projectRoot, parsed) {
|
|
|
4517
5855
|
model: config.ai.model,
|
|
4518
5856
|
cwd: projectRoot,
|
|
4519
5857
|
silent: true,
|
|
4520
|
-
activity: "generating issue"
|
|
5858
|
+
activity: "generating issue",
|
|
5859
|
+
sandboxed: config.sandbox.enabled,
|
|
5860
|
+
sandboxName: config.sandbox.name
|
|
4521
5861
|
});
|
|
4522
5862
|
if (!aiResult.success && !aiResult.interrupted) {
|
|
4523
5863
|
process.stderr.write(`${red("✗")} Failed to generate issue: ${aiResult.error}
|
|
@@ -4636,7 +5976,7 @@ function extractJSON(text) {
|
|
|
4636
5976
|
}
|
|
4637
5977
|
function askQuestion(question) {
|
|
4638
5978
|
return new Promise((resolve2) => {
|
|
4639
|
-
const rl =
|
|
5979
|
+
const rl = createInterface2({
|
|
4640
5980
|
input: process.stdin,
|
|
4641
5981
|
output: process.stderr
|
|
4642
5982
|
});
|
|
@@ -5675,9 +7015,9 @@ var init_sprint = __esm(() => {
|
|
|
5675
7015
|
});
|
|
5676
7016
|
|
|
5677
7017
|
// src/core/prompt-builder.ts
|
|
5678
|
-
import { execSync as
|
|
5679
|
-
import { existsSync as
|
|
5680
|
-
import { join as
|
|
7018
|
+
import { execSync as execSync10 } from "node:child_process";
|
|
7019
|
+
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "node:fs";
|
|
7020
|
+
import { join as join13 } from "node:path";
|
|
5681
7021
|
function buildExecutionPrompt(ctx) {
|
|
5682
7022
|
const sections = [];
|
|
5683
7023
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -5707,13 +7047,13 @@ function buildFeedbackPrompt(ctx) {
|
|
|
5707
7047
|
}
|
|
5708
7048
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
5709
7049
|
const sections = [];
|
|
5710
|
-
const locusmd = readFileSafe(
|
|
7050
|
+
const locusmd = readFileSafe(join13(projectRoot, "LOCUS.md"));
|
|
5711
7051
|
if (locusmd) {
|
|
5712
7052
|
sections.push(`# Project Instructions
|
|
5713
7053
|
|
|
5714
7054
|
${locusmd}`);
|
|
5715
7055
|
}
|
|
5716
|
-
const learnings = readFileSafe(
|
|
7056
|
+
const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
|
|
5717
7057
|
if (learnings) {
|
|
5718
7058
|
sections.push(`# Past Learnings
|
|
5719
7059
|
|
|
@@ -5739,24 +7079,24 @@ ${userMessage}`);
|
|
|
5739
7079
|
}
|
|
5740
7080
|
function buildSystemContext(projectRoot) {
|
|
5741
7081
|
const parts = ["# System Context"];
|
|
5742
|
-
const locusmd = readFileSafe(
|
|
7082
|
+
const locusmd = readFileSafe(join13(projectRoot, "LOCUS.md"));
|
|
5743
7083
|
if (locusmd) {
|
|
5744
7084
|
parts.push(`## Project Instructions (LOCUS.md)
|
|
5745
7085
|
|
|
5746
7086
|
${locusmd}`);
|
|
5747
7087
|
}
|
|
5748
|
-
const learnings = readFileSafe(
|
|
7088
|
+
const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
|
|
5749
7089
|
if (learnings) {
|
|
5750
7090
|
parts.push(`## Past Learnings
|
|
5751
7091
|
|
|
5752
7092
|
${learnings}`);
|
|
5753
7093
|
}
|
|
5754
|
-
const discussionsDir =
|
|
5755
|
-
if (
|
|
7094
|
+
const discussionsDir = join13(projectRoot, ".locus", "discussions");
|
|
7095
|
+
if (existsSync14(discussionsDir)) {
|
|
5756
7096
|
try {
|
|
5757
7097
|
const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
5758
7098
|
for (const file of files) {
|
|
5759
|
-
const content = readFileSafe(
|
|
7099
|
+
const content = readFileSafe(join13(discussionsDir, file));
|
|
5760
7100
|
if (content) {
|
|
5761
7101
|
parts.push(`## Discussion: ${file.replace(".md", "")}
|
|
5762
7102
|
|
|
@@ -5819,7 +7159,7 @@ ${diffSummary}
|
|
|
5819
7159
|
function buildRepoContext(projectRoot) {
|
|
5820
7160
|
const parts = ["# Repository Context"];
|
|
5821
7161
|
try {
|
|
5822
|
-
const tree =
|
|
7162
|
+
const tree = execSync10("find . -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.locus/*' -not -path '*/dist/*' -not -path '*/build/*' | head -80", { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
5823
7163
|
if (tree) {
|
|
5824
7164
|
parts.push(`## File Tree
|
|
5825
7165
|
|
|
@@ -5829,7 +7169,7 @@ ${tree}
|
|
|
5829
7169
|
}
|
|
5830
7170
|
} catch {}
|
|
5831
7171
|
try {
|
|
5832
|
-
const gitLog =
|
|
7172
|
+
const gitLog = execSync10("git log --oneline -10", {
|
|
5833
7173
|
cwd: projectRoot,
|
|
5834
7174
|
encoding: "utf-8",
|
|
5835
7175
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5843,7 +7183,7 @@ ${gitLog}
|
|
|
5843
7183
|
}
|
|
5844
7184
|
} catch {}
|
|
5845
7185
|
try {
|
|
5846
|
-
const branch =
|
|
7186
|
+
const branch = execSync10("git rev-parse --abbrev-ref HEAD", {
|
|
5847
7187
|
cwd: projectRoot,
|
|
5848
7188
|
encoding: "utf-8",
|
|
5849
7189
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5902,9 +7242,9 @@ function buildFeedbackInstructions() {
|
|
|
5902
7242
|
}
|
|
5903
7243
|
function readFileSafe(path) {
|
|
5904
7244
|
try {
|
|
5905
|
-
if (!
|
|
7245
|
+
if (!existsSync14(path))
|
|
5906
7246
|
return null;
|
|
5907
|
-
return
|
|
7247
|
+
return readFileSync10(path, "utf-8");
|
|
5908
7248
|
} catch {
|
|
5909
7249
|
return null;
|
|
5910
7250
|
}
|
|
@@ -6096,7 +7436,7 @@ var init_diff_renderer = __esm(() => {
|
|
|
6096
7436
|
});
|
|
6097
7437
|
|
|
6098
7438
|
// src/repl/commands.ts
|
|
6099
|
-
import { execSync as
|
|
7439
|
+
import { execSync as execSync11 } from "node:child_process";
|
|
6100
7440
|
function getSlashCommands() {
|
|
6101
7441
|
return [
|
|
6102
7442
|
{
|
|
@@ -6288,7 +7628,7 @@ function cmdModel(args, ctx) {
|
|
|
6288
7628
|
}
|
|
6289
7629
|
function cmdDiff(_args, ctx) {
|
|
6290
7630
|
try {
|
|
6291
|
-
const diff =
|
|
7631
|
+
const diff = execSync11("git diff", {
|
|
6292
7632
|
cwd: ctx.projectRoot,
|
|
6293
7633
|
encoding: "utf-8",
|
|
6294
7634
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6324,7 +7664,7 @@ function cmdDiff(_args, ctx) {
|
|
|
6324
7664
|
}
|
|
6325
7665
|
function cmdUndo(_args, ctx) {
|
|
6326
7666
|
try {
|
|
6327
|
-
const status =
|
|
7667
|
+
const status = execSync11("git status --porcelain", {
|
|
6328
7668
|
cwd: ctx.projectRoot,
|
|
6329
7669
|
encoding: "utf-8",
|
|
6330
7670
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6334,7 +7674,7 @@ function cmdUndo(_args, ctx) {
|
|
|
6334
7674
|
`);
|
|
6335
7675
|
return;
|
|
6336
7676
|
}
|
|
6337
|
-
|
|
7677
|
+
execSync11("git checkout .", {
|
|
6338
7678
|
cwd: ctx.projectRoot,
|
|
6339
7679
|
encoding: "utf-8",
|
|
6340
7680
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6368,7 +7708,7 @@ var init_commands = __esm(() => {
|
|
|
6368
7708
|
|
|
6369
7709
|
// src/repl/completions.ts
|
|
6370
7710
|
import { readdirSync as readdirSync4 } from "node:fs";
|
|
6371
|
-
import { basename as basename2, dirname as
|
|
7711
|
+
import { basename as basename2, dirname as dirname4, join as join14 } from "node:path";
|
|
6372
7712
|
|
|
6373
7713
|
class SlashCommandCompletion {
|
|
6374
7714
|
commands;
|
|
@@ -6423,7 +7763,7 @@ class FilePathCompletion {
|
|
|
6423
7763
|
}
|
|
6424
7764
|
findMatches(partial) {
|
|
6425
7765
|
try {
|
|
6426
|
-
const dir = partial.includes("/") ?
|
|
7766
|
+
const dir = partial.includes("/") ? join14(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
6427
7767
|
const prefix = basename2(partial);
|
|
6428
7768
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
6429
7769
|
return entries.filter((e) => {
|
|
@@ -6434,7 +7774,7 @@ class FilePathCompletion {
|
|
|
6434
7774
|
return e.name.startsWith(prefix);
|
|
6435
7775
|
}).map((e) => {
|
|
6436
7776
|
const name = e.isDirectory() ? `${e.name}/` : e.name;
|
|
6437
|
-
return partial.includes("/") ? `${
|
|
7777
|
+
return partial.includes("/") ? `${dirname4(partial)}/${name}` : name;
|
|
6438
7778
|
}).slice(0, 20);
|
|
6439
7779
|
} catch {
|
|
6440
7780
|
return [];
|
|
@@ -6459,14 +7799,14 @@ class CombinedCompletion {
|
|
|
6459
7799
|
var init_completions = () => {};
|
|
6460
7800
|
|
|
6461
7801
|
// src/repl/input-history.ts
|
|
6462
|
-
import { existsSync as
|
|
6463
|
-
import { dirname as
|
|
7802
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
7803
|
+
import { dirname as dirname5, join as join15 } from "node:path";
|
|
6464
7804
|
|
|
6465
7805
|
class InputHistory {
|
|
6466
7806
|
entries = [];
|
|
6467
7807
|
filePath;
|
|
6468
7808
|
constructor(projectRoot) {
|
|
6469
|
-
this.filePath =
|
|
7809
|
+
this.filePath = join15(projectRoot, ".locus", "sessions", ".input-history");
|
|
6470
7810
|
this.load();
|
|
6471
7811
|
}
|
|
6472
7812
|
add(text) {
|
|
@@ -6505,22 +7845,22 @@ class InputHistory {
|
|
|
6505
7845
|
}
|
|
6506
7846
|
load() {
|
|
6507
7847
|
try {
|
|
6508
|
-
if (!
|
|
7848
|
+
if (!existsSync15(this.filePath))
|
|
6509
7849
|
return;
|
|
6510
|
-
const content =
|
|
7850
|
+
const content = readFileSync11(this.filePath, "utf-8");
|
|
6511
7851
|
this.entries = content.split(`
|
|
6512
7852
|
`).map((line) => this.unescape(line)).filter(Boolean);
|
|
6513
7853
|
} catch {}
|
|
6514
7854
|
}
|
|
6515
7855
|
save() {
|
|
6516
7856
|
try {
|
|
6517
|
-
const dir =
|
|
6518
|
-
if (!
|
|
6519
|
-
|
|
7857
|
+
const dir = dirname5(this.filePath);
|
|
7858
|
+
if (!existsSync15(dir)) {
|
|
7859
|
+
mkdirSync10(dir, { recursive: true });
|
|
6520
7860
|
}
|
|
6521
7861
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
6522
7862
|
`);
|
|
6523
|
-
|
|
7863
|
+
writeFileSync7(this.filePath, content, "utf-8");
|
|
6524
7864
|
} catch {}
|
|
6525
7865
|
}
|
|
6526
7866
|
escape(text) {
|
|
@@ -6545,23 +7885,22 @@ var init_model_config = __esm(() => {
|
|
|
6545
7885
|
});
|
|
6546
7886
|
|
|
6547
7887
|
// src/repl/session-manager.ts
|
|
6548
|
-
import { randomBytes } from "node:crypto";
|
|
6549
7888
|
import {
|
|
6550
|
-
existsSync as
|
|
6551
|
-
mkdirSync as
|
|
7889
|
+
existsSync as existsSync16,
|
|
7890
|
+
mkdirSync as mkdirSync11,
|
|
6552
7891
|
readdirSync as readdirSync5,
|
|
6553
|
-
readFileSync as
|
|
6554
|
-
unlinkSync as
|
|
6555
|
-
writeFileSync as
|
|
7892
|
+
readFileSync as readFileSync12,
|
|
7893
|
+
unlinkSync as unlinkSync4,
|
|
7894
|
+
writeFileSync as writeFileSync8
|
|
6556
7895
|
} from "node:fs";
|
|
6557
|
-
import { basename as basename3, join as
|
|
7896
|
+
import { basename as basename3, join as join16 } from "node:path";
|
|
6558
7897
|
|
|
6559
7898
|
class SessionManager {
|
|
6560
7899
|
sessionsDir;
|
|
6561
7900
|
constructor(projectRoot) {
|
|
6562
|
-
this.sessionsDir =
|
|
6563
|
-
if (!
|
|
6564
|
-
|
|
7901
|
+
this.sessionsDir = join16(projectRoot, ".locus", "sessions");
|
|
7902
|
+
if (!existsSync16(this.sessionsDir)) {
|
|
7903
|
+
mkdirSync11(this.sessionsDir, { recursive: true });
|
|
6565
7904
|
}
|
|
6566
7905
|
}
|
|
6567
7906
|
create(options) {
|
|
@@ -6586,14 +7925,14 @@ class SessionManager {
|
|
|
6586
7925
|
}
|
|
6587
7926
|
isPersisted(sessionOrId) {
|
|
6588
7927
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
6589
|
-
return
|
|
7928
|
+
return existsSync16(this.getSessionPath(sessionId));
|
|
6590
7929
|
}
|
|
6591
7930
|
load(idOrPrefix) {
|
|
6592
7931
|
const files = this.listSessionFiles();
|
|
6593
7932
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
6594
|
-
if (
|
|
7933
|
+
if (existsSync16(exactPath)) {
|
|
6595
7934
|
try {
|
|
6596
|
-
return JSON.parse(
|
|
7935
|
+
return JSON.parse(readFileSync12(exactPath, "utf-8"));
|
|
6597
7936
|
} catch {
|
|
6598
7937
|
return null;
|
|
6599
7938
|
}
|
|
@@ -6601,7 +7940,7 @@ class SessionManager {
|
|
|
6601
7940
|
const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
|
|
6602
7941
|
if (matches.length === 1) {
|
|
6603
7942
|
try {
|
|
6604
|
-
return JSON.parse(
|
|
7943
|
+
return JSON.parse(readFileSync12(matches[0], "utf-8"));
|
|
6605
7944
|
} catch {
|
|
6606
7945
|
return null;
|
|
6607
7946
|
}
|
|
@@ -6614,7 +7953,7 @@ class SessionManager {
|
|
|
6614
7953
|
save(session) {
|
|
6615
7954
|
session.updated = new Date().toISOString();
|
|
6616
7955
|
const path = this.getSessionPath(session.id);
|
|
6617
|
-
|
|
7956
|
+
writeFileSync8(path, `${JSON.stringify(session, null, 2)}
|
|
6618
7957
|
`, "utf-8");
|
|
6619
7958
|
}
|
|
6620
7959
|
addMessage(session, message) {
|
|
@@ -6626,7 +7965,7 @@ class SessionManager {
|
|
|
6626
7965
|
const sessions = [];
|
|
6627
7966
|
for (const file of files) {
|
|
6628
7967
|
try {
|
|
6629
|
-
const session = JSON.parse(
|
|
7968
|
+
const session = JSON.parse(readFileSync12(file, "utf-8"));
|
|
6630
7969
|
sessions.push({
|
|
6631
7970
|
id: session.id,
|
|
6632
7971
|
created: session.created,
|
|
@@ -6641,8 +7980,8 @@ class SessionManager {
|
|
|
6641
7980
|
}
|
|
6642
7981
|
delete(sessionId) {
|
|
6643
7982
|
const path = this.getSessionPath(sessionId);
|
|
6644
|
-
if (
|
|
6645
|
-
|
|
7983
|
+
if (existsSync16(path)) {
|
|
7984
|
+
unlinkSync4(path);
|
|
6646
7985
|
return true;
|
|
6647
7986
|
}
|
|
6648
7987
|
return false;
|
|
@@ -6653,7 +7992,7 @@ class SessionManager {
|
|
|
6653
7992
|
let pruned = 0;
|
|
6654
7993
|
const withStats = files.map((f) => {
|
|
6655
7994
|
try {
|
|
6656
|
-
const session = JSON.parse(
|
|
7995
|
+
const session = JSON.parse(readFileSync12(f, "utf-8"));
|
|
6657
7996
|
return { path: f, updated: new Date(session.updated).getTime() };
|
|
6658
7997
|
} catch {
|
|
6659
7998
|
return { path: f, updated: 0 };
|
|
@@ -6663,7 +8002,7 @@ class SessionManager {
|
|
|
6663
8002
|
for (const entry of withStats) {
|
|
6664
8003
|
if (now - entry.updated > SESSION_MAX_AGE_MS) {
|
|
6665
8004
|
try {
|
|
6666
|
-
|
|
8005
|
+
unlinkSync4(entry.path);
|
|
6667
8006
|
pruned++;
|
|
6668
8007
|
} catch {}
|
|
6669
8008
|
}
|
|
@@ -6671,10 +8010,10 @@ class SessionManager {
|
|
|
6671
8010
|
const remaining = withStats.length - pruned;
|
|
6672
8011
|
if (remaining > MAX_SESSIONS) {
|
|
6673
8012
|
const toRemove = remaining - MAX_SESSIONS;
|
|
6674
|
-
const alive = withStats.filter((e) =>
|
|
8013
|
+
const alive = withStats.filter((e) => existsSync16(e.path));
|
|
6675
8014
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
6676
8015
|
try {
|
|
6677
|
-
|
|
8016
|
+
unlinkSync4(alive[i].path);
|
|
6678
8017
|
pruned++;
|
|
6679
8018
|
} catch {}
|
|
6680
8019
|
}
|
|
@@ -6686,16 +8025,16 @@ class SessionManager {
|
|
|
6686
8025
|
}
|
|
6687
8026
|
listSessionFiles() {
|
|
6688
8027
|
try {
|
|
6689
|
-
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
8028
|
+
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join16(this.sessionsDir, f));
|
|
6690
8029
|
} catch {
|
|
6691
8030
|
return [];
|
|
6692
8031
|
}
|
|
6693
8032
|
}
|
|
6694
8033
|
generateId() {
|
|
6695
|
-
return
|
|
8034
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6696
8035
|
}
|
|
6697
8036
|
getSessionPath(sessionId) {
|
|
6698
|
-
return
|
|
8037
|
+
return join16(this.sessionsDir, `${sessionId}.json`);
|
|
6699
8038
|
}
|
|
6700
8039
|
}
|
|
6701
8040
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -6705,7 +8044,7 @@ var init_session_manager = __esm(() => {
|
|
|
6705
8044
|
});
|
|
6706
8045
|
|
|
6707
8046
|
// src/repl/repl.ts
|
|
6708
|
-
import { execSync as
|
|
8047
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
6709
8048
|
async function startRepl(options) {
|
|
6710
8049
|
const { projectRoot, config } = options;
|
|
6711
8050
|
const sessionManager = new SessionManager(projectRoot);
|
|
@@ -6723,7 +8062,7 @@ async function startRepl(options) {
|
|
|
6723
8062
|
} else {
|
|
6724
8063
|
let branch = "main";
|
|
6725
8064
|
try {
|
|
6726
|
-
branch =
|
|
8065
|
+
branch = execSync12("git rev-parse --abbrev-ref HEAD", {
|
|
6727
8066
|
cwd: projectRoot,
|
|
6728
8067
|
encoding: "utf-8",
|
|
6729
8068
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6748,6 +8087,7 @@ async function executeOneShotPrompt(prompt, session, sessionManager, options) {
|
|
|
6748
8087
|
const normalized = normalizeImagePlaceholders(prompt);
|
|
6749
8088
|
const text = normalized.text;
|
|
6750
8089
|
const images = collectReferencedAttachments(text, normalized.attachments);
|
|
8090
|
+
relocateImages(images, projectRoot);
|
|
6751
8091
|
const imageContext = buildImageContext(images);
|
|
6752
8092
|
const fullPrompt = buildReplPrompt(text + imageContext, projectRoot, config, session.messages);
|
|
6753
8093
|
sessionManager.addMessage(session, {
|
|
@@ -6764,6 +8104,18 @@ async function executeOneShotPrompt(prompt, session, sessionManager, options) {
|
|
|
6764
8104
|
}
|
|
6765
8105
|
async function runInteractiveRepl(session, sessionManager, options) {
|
|
6766
8106
|
const { projectRoot, config } = options;
|
|
8107
|
+
let sandboxRunner = null;
|
|
8108
|
+
if (config.sandbox.enabled && config.sandbox.name) {
|
|
8109
|
+
const provider = inferProviderFromModel(config.ai.model) || config.ai.provider;
|
|
8110
|
+
sandboxRunner = createUserManagedSandboxRunner(provider, config.sandbox.name);
|
|
8111
|
+
process.stderr.write(`${dim("Using sandbox")} ${dim(config.sandbox.name)}
|
|
8112
|
+
`);
|
|
8113
|
+
} else if (config.sandbox.enabled) {
|
|
8114
|
+
const sandboxName = buildPersistentSandboxName(projectRoot);
|
|
8115
|
+
sandboxRunner = new SandboxedClaudeRunner(sandboxName);
|
|
8116
|
+
process.stderr.write(`${dim("Sandbox mode: prompts will share sandbox")} ${dim(sandboxName)}
|
|
8117
|
+
`);
|
|
8118
|
+
}
|
|
6767
8119
|
const history = new InputHistory(projectRoot);
|
|
6768
8120
|
const completion = new CombinedCompletion([
|
|
6769
8121
|
new SlashCommandCompletion(getAllCommandNames()),
|
|
@@ -6793,8 +8145,14 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
6793
8145
|
session.metadata.model = model;
|
|
6794
8146
|
const inferredProvider = inferProviderFromModel(model);
|
|
6795
8147
|
if (inferredProvider) {
|
|
8148
|
+
const providerChanged = inferredProvider !== currentProvider;
|
|
6796
8149
|
currentProvider = inferredProvider;
|
|
6797
8150
|
session.metadata.provider = inferredProvider;
|
|
8151
|
+
if (providerChanged && config.sandbox.enabled && config.sandbox.name) {
|
|
8152
|
+
sandboxRunner = createUserManagedSandboxRunner(inferredProvider, config.sandbox.name);
|
|
8153
|
+
process.stderr.write(`${dim("Switched sandbox agent to")} ${dim(inferredProvider)}
|
|
8154
|
+
`);
|
|
8155
|
+
}
|
|
6798
8156
|
}
|
|
6799
8157
|
persistReplModelSelection(projectRoot, config, model);
|
|
6800
8158
|
sessionManager.save(session);
|
|
@@ -6824,6 +8182,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
6824
8182
|
continue;
|
|
6825
8183
|
}
|
|
6826
8184
|
history.add(text);
|
|
8185
|
+
relocateImages(result.images, projectRoot);
|
|
6827
8186
|
const imageContext = buildImageContext(result.images);
|
|
6828
8187
|
const fullPrompt = buildReplPrompt(text + imageContext, projectRoot, { ...config, ai: { provider: currentProvider, model: currentModel } }, session.messages);
|
|
6829
8188
|
sessionManager.addMessage(session, {
|
|
@@ -6839,7 +8198,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
6839
8198
|
...config,
|
|
6840
8199
|
ai: { provider: currentProvider, model: currentModel }
|
|
6841
8200
|
}
|
|
6842
|
-
}, verbose);
|
|
8201
|
+
}, verbose, sandboxRunner ?? undefined);
|
|
6843
8202
|
sessionManager.addMessage(session, {
|
|
6844
8203
|
role: "assistant",
|
|
6845
8204
|
content: response,
|
|
@@ -6864,6 +8223,10 @@ ${red("✗")} ${msg}
|
|
|
6864
8223
|
break;
|
|
6865
8224
|
}
|
|
6866
8225
|
}
|
|
8226
|
+
if (sandboxRunner && "destroy" in sandboxRunner) {
|
|
8227
|
+
const runner = sandboxRunner;
|
|
8228
|
+
runner.destroy();
|
|
8229
|
+
}
|
|
6867
8230
|
const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
|
|
6868
8231
|
if (shouldPersistOnExit) {
|
|
6869
8232
|
sessionManager.save(session);
|
|
@@ -6879,14 +8242,17 @@ ${red("✗")} ${msg}
|
|
|
6879
8242
|
process.stdin.pause();
|
|
6880
8243
|
process.exit(0);
|
|
6881
8244
|
}
|
|
6882
|
-
async function executeAITurn(prompt, session, options, verbose = false) {
|
|
8245
|
+
async function executeAITurn(prompt, session, options, verbose = false, runner) {
|
|
6883
8246
|
const { config, projectRoot } = options;
|
|
6884
8247
|
const aiResult = await runAI({
|
|
6885
8248
|
prompt,
|
|
6886
8249
|
provider: config.ai.provider,
|
|
6887
8250
|
model: config.ai.model,
|
|
6888
8251
|
cwd: projectRoot,
|
|
6889
|
-
verbose
|
|
8252
|
+
verbose,
|
|
8253
|
+
sandboxed: config.sandbox.enabled,
|
|
8254
|
+
sandboxName: config.sandbox.name,
|
|
8255
|
+
runner
|
|
6890
8256
|
});
|
|
6891
8257
|
if (aiResult.interrupted) {
|
|
6892
8258
|
if (aiResult.output) {
|
|
@@ -6916,7 +8282,9 @@ function printWelcome(session) {
|
|
|
6916
8282
|
`);
|
|
6917
8283
|
}
|
|
6918
8284
|
var init_repl = __esm(() => {
|
|
8285
|
+
init_claude_sandbox();
|
|
6919
8286
|
init_run_ai();
|
|
8287
|
+
init_runner();
|
|
6920
8288
|
init_ai_models();
|
|
6921
8289
|
init_prompt_builder();
|
|
6922
8290
|
init_terminal();
|
|
@@ -7059,7 +8427,7 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
|
|
|
7059
8427
|
stream.emitStatus("thinking");
|
|
7060
8428
|
try {
|
|
7061
8429
|
const fullPrompt = buildReplPrompt(prompt, projectRoot, config);
|
|
7062
|
-
const runner = await createRunnerAsync(config.ai.provider);
|
|
8430
|
+
const runner = config.sandbox.name ? createUserManagedSandboxRunner(config.ai.provider, config.sandbox.name) : await createRunnerAsync(config.ai.provider, config.sandbox.enabled);
|
|
7063
8431
|
const available = await runner.isAvailable();
|
|
7064
8432
|
if (!available) {
|
|
7065
8433
|
stream.emitError(`${config.ai.provider} CLI not available`, false);
|
|
@@ -7107,7 +8475,7 @@ var init_exec = __esm(() => {
|
|
|
7107
8475
|
});
|
|
7108
8476
|
|
|
7109
8477
|
// src/core/agent.ts
|
|
7110
|
-
import { execSync as
|
|
8478
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
7111
8479
|
async function executeIssue(projectRoot, options) {
|
|
7112
8480
|
const log = getLogger();
|
|
7113
8481
|
const timer = createTimer();
|
|
@@ -7136,7 +8504,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
|
|
|
7136
8504
|
}
|
|
7137
8505
|
let issueComments = [];
|
|
7138
8506
|
try {
|
|
7139
|
-
const commentsRaw =
|
|
8507
|
+
const commentsRaw = execSync13(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7140
8508
|
if (commentsRaw) {
|
|
7141
8509
|
issueComments = commentsRaw.split(`
|
|
7142
8510
|
`).filter(Boolean);
|
|
@@ -7171,7 +8539,9 @@ ${yellow("⚠")} ${bold("Dry run")} — would execute with:
|
|
|
7171
8539
|
provider,
|
|
7172
8540
|
model,
|
|
7173
8541
|
cwd: options.worktreePath ?? projectRoot,
|
|
7174
|
-
activity: `issue #${issueNumber}
|
|
8542
|
+
activity: `issue #${issueNumber}`,
|
|
8543
|
+
sandboxed: options.sandboxed,
|
|
8544
|
+
sandboxName: options.sandboxName
|
|
7175
8545
|
});
|
|
7176
8546
|
const output = aiResult.output;
|
|
7177
8547
|
if (aiResult.interrupted) {
|
|
@@ -7275,7 +8645,9 @@ ${c.body}`),
|
|
|
7275
8645
|
provider: config.ai.provider,
|
|
7276
8646
|
model: config.ai.model,
|
|
7277
8647
|
cwd: projectRoot,
|
|
7278
|
-
activity: `iterating on PR #${prNumber}
|
|
8648
|
+
activity: `iterating on PR #${prNumber}`,
|
|
8649
|
+
sandboxed: config.sandbox.enabled,
|
|
8650
|
+
sandboxName: config.sandbox.name
|
|
7279
8651
|
});
|
|
7280
8652
|
if (aiResult.interrupted) {
|
|
7281
8653
|
process.stderr.write(`
|
|
@@ -7296,12 +8668,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
|
|
|
7296
8668
|
}
|
|
7297
8669
|
async function createIssuePR(projectRoot, config, issue) {
|
|
7298
8670
|
try {
|
|
7299
|
-
const currentBranch =
|
|
8671
|
+
const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
|
|
7300
8672
|
cwd: projectRoot,
|
|
7301
8673
|
encoding: "utf-8",
|
|
7302
8674
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7303
8675
|
}).trim();
|
|
7304
|
-
const diff =
|
|
8676
|
+
const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
7305
8677
|
cwd: projectRoot,
|
|
7306
8678
|
encoding: "utf-8",
|
|
7307
8679
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7310,7 +8682,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
7310
8682
|
getLogger().verbose("No changes to create PR for");
|
|
7311
8683
|
return;
|
|
7312
8684
|
}
|
|
7313
|
-
|
|
8685
|
+
execSync13(`git push -u origin ${currentBranch}`, {
|
|
7314
8686
|
cwd: projectRoot,
|
|
7315
8687
|
encoding: "utf-8",
|
|
7316
8688
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7356,9 +8728,9 @@ var init_agent = __esm(() => {
|
|
|
7356
8728
|
});
|
|
7357
8729
|
|
|
7358
8730
|
// src/core/conflict.ts
|
|
7359
|
-
import { execSync as
|
|
8731
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
7360
8732
|
function git2(args, cwd) {
|
|
7361
|
-
return
|
|
8733
|
+
return execSync14(`git ${args}`, {
|
|
7362
8734
|
cwd,
|
|
7363
8735
|
encoding: "utf-8",
|
|
7364
8736
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7470,198 +8842,25 @@ ${bold(red("✗"))} ${bold("Merge conflict detected")}
|
|
|
7470
8842
|
process.stderr.write(` 3. ${dim("git rebase --continue")}
|
|
7471
8843
|
`);
|
|
7472
8844
|
process.stderr.write(` 4. ${dim("locus run --resume")} to continue the sprint
|
|
7473
|
-
|
|
7474
|
-
`);
|
|
7475
|
-
} else if (result.newCommits > 0) {
|
|
7476
|
-
process.stderr.write(`
|
|
7477
|
-
${bold(yellow("⚠"))} Base branch has ${result.newCommits} new commit${result.newCommits === 1 ? "" : "s"} — auto-rebasing...
|
|
7478
|
-
`);
|
|
7479
|
-
}
|
|
7480
|
-
}
|
|
7481
|
-
var init_conflict = __esm(() => {
|
|
7482
|
-
init_terminal();
|
|
7483
|
-
init_logger();
|
|
7484
|
-
});
|
|
7485
|
-
|
|
7486
|
-
// src/core/run-state.ts
|
|
7487
|
-
import {
|
|
7488
|
-
existsSync as existsSync14,
|
|
7489
|
-
mkdirSync as mkdirSync10,
|
|
7490
|
-
readFileSync as readFileSync11,
|
|
7491
|
-
unlinkSync as unlinkSync4,
|
|
7492
|
-
writeFileSync as writeFileSync8
|
|
7493
|
-
} from "node:fs";
|
|
7494
|
-
import { dirname as dirname5, join as join14 } from "node:path";
|
|
7495
|
-
function getRunStatePath(projectRoot) {
|
|
7496
|
-
return join14(projectRoot, ".locus", "run-state.json");
|
|
7497
|
-
}
|
|
7498
|
-
function loadRunState(projectRoot) {
|
|
7499
|
-
const path = getRunStatePath(projectRoot);
|
|
7500
|
-
if (!existsSync14(path))
|
|
7501
|
-
return null;
|
|
7502
|
-
try {
|
|
7503
|
-
return JSON.parse(readFileSync11(path, "utf-8"));
|
|
7504
|
-
} catch {
|
|
7505
|
-
getLogger().warn("Corrupted run-state.json, ignoring");
|
|
7506
|
-
return null;
|
|
7507
|
-
}
|
|
7508
|
-
}
|
|
7509
|
-
function saveRunState(projectRoot, state) {
|
|
7510
|
-
const path = getRunStatePath(projectRoot);
|
|
7511
|
-
const dir = dirname5(path);
|
|
7512
|
-
if (!existsSync14(dir)) {
|
|
7513
|
-
mkdirSync10(dir, { recursive: true });
|
|
7514
|
-
}
|
|
7515
|
-
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
7516
|
-
`, "utf-8");
|
|
7517
|
-
}
|
|
7518
|
-
function clearRunState(projectRoot) {
|
|
7519
|
-
const path = getRunStatePath(projectRoot);
|
|
7520
|
-
if (existsSync14(path)) {
|
|
7521
|
-
unlinkSync4(path);
|
|
7522
|
-
}
|
|
7523
|
-
}
|
|
7524
|
-
function createSprintRunState(sprint, branch, issues) {
|
|
7525
|
-
return {
|
|
7526
|
-
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
7527
|
-
type: "sprint",
|
|
7528
|
-
sprint,
|
|
7529
|
-
branch,
|
|
7530
|
-
startedAt: new Date().toISOString(),
|
|
7531
|
-
tasks: issues.map(({ number, order }) => ({
|
|
7532
|
-
issue: number,
|
|
7533
|
-
order,
|
|
7534
|
-
status: "pending"
|
|
7535
|
-
}))
|
|
7536
|
-
};
|
|
7537
|
-
}
|
|
7538
|
-
function createParallelRunState(issueNumbers) {
|
|
7539
|
-
return {
|
|
7540
|
-
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
7541
|
-
type: "parallel",
|
|
7542
|
-
startedAt: new Date().toISOString(),
|
|
7543
|
-
tasks: issueNumbers.map((issue, i) => ({
|
|
7544
|
-
issue,
|
|
7545
|
-
order: i + 1,
|
|
7546
|
-
status: "pending"
|
|
7547
|
-
}))
|
|
7548
|
-
};
|
|
7549
|
-
}
|
|
7550
|
-
function markTaskInProgress(state, issueNumber) {
|
|
7551
|
-
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
7552
|
-
if (task) {
|
|
7553
|
-
task.status = "in_progress";
|
|
7554
|
-
}
|
|
7555
|
-
}
|
|
7556
|
-
function markTaskDone(state, issueNumber, prNumber) {
|
|
7557
|
-
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
7558
|
-
if (task) {
|
|
7559
|
-
task.status = "done";
|
|
7560
|
-
task.completedAt = new Date().toISOString();
|
|
7561
|
-
if (prNumber)
|
|
7562
|
-
task.pr = prNumber;
|
|
7563
|
-
}
|
|
7564
|
-
}
|
|
7565
|
-
function markTaskFailed(state, issueNumber, error) {
|
|
7566
|
-
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
7567
|
-
if (task) {
|
|
7568
|
-
task.status = "failed";
|
|
7569
|
-
task.failedAt = new Date().toISOString();
|
|
7570
|
-
task.error = error;
|
|
7571
|
-
}
|
|
7572
|
-
}
|
|
7573
|
-
function getRunStats(state) {
|
|
7574
|
-
const tasks = state.tasks;
|
|
7575
|
-
return {
|
|
7576
|
-
total: tasks.length,
|
|
7577
|
-
done: tasks.filter((t) => t.status === "done").length,
|
|
7578
|
-
failed: tasks.filter((t) => t.status === "failed").length,
|
|
7579
|
-
pending: tasks.filter((t) => t.status === "pending").length,
|
|
7580
|
-
inProgress: tasks.filter((t) => t.status === "in_progress").length
|
|
7581
|
-
};
|
|
7582
|
-
}
|
|
7583
|
-
function getNextTask(state) {
|
|
7584
|
-
const failed = state.tasks.find((t) => t.status === "failed");
|
|
7585
|
-
if (failed)
|
|
7586
|
-
return failed;
|
|
7587
|
-
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
7588
|
-
}
|
|
7589
|
-
var init_run_state = __esm(() => {
|
|
7590
|
-
init_logger();
|
|
7591
|
-
});
|
|
7592
|
-
|
|
7593
|
-
// src/core/shutdown.ts
|
|
7594
|
-
function registerShutdownHandlers(ctx) {
|
|
7595
|
-
shutdownContext = ctx;
|
|
7596
|
-
interruptCount = 0;
|
|
7597
|
-
const handler = () => {
|
|
7598
|
-
interruptCount++;
|
|
7599
|
-
if (interruptCount >= 2) {
|
|
7600
|
-
process.stderr.write(`
|
|
7601
|
-
Force exit.
|
|
7602
|
-
`);
|
|
7603
|
-
process.exit(1);
|
|
7604
|
-
}
|
|
7605
|
-
process.stderr.write(`
|
|
7606
|
-
|
|
7607
|
-
Interrupted. Saving state...
|
|
7608
|
-
`);
|
|
7609
|
-
const state = shutdownContext?.getRunState?.();
|
|
7610
|
-
if (state && shutdownContext) {
|
|
7611
|
-
for (const task of state.tasks) {
|
|
7612
|
-
if (task.status === "in_progress") {
|
|
7613
|
-
task.status = "failed";
|
|
7614
|
-
task.failedAt = new Date().toISOString();
|
|
7615
|
-
task.error = "Interrupted by user";
|
|
7616
|
-
}
|
|
7617
|
-
}
|
|
7618
|
-
try {
|
|
7619
|
-
saveRunState(shutdownContext.projectRoot, state);
|
|
7620
|
-
process.stderr.write(`State saved. Resume with: locus run --resume
|
|
8845
|
+
|
|
7621
8846
|
`);
|
|
7622
|
-
|
|
7623
|
-
|
|
8847
|
+
} else if (result.newCommits > 0) {
|
|
8848
|
+
process.stderr.write(`
|
|
8849
|
+
${bold(yellow("⚠"))} Base branch has ${result.newCommits} new commit${result.newCommits === 1 ? "" : "s"} — auto-rebasing...
|
|
7624
8850
|
`);
|
|
7625
|
-
}
|
|
7626
|
-
}
|
|
7627
|
-
shutdownContext?.onShutdown?.();
|
|
7628
|
-
if (interruptTimer)
|
|
7629
|
-
clearTimeout(interruptTimer);
|
|
7630
|
-
interruptTimer = setTimeout(() => {
|
|
7631
|
-
interruptCount = 0;
|
|
7632
|
-
}, 2000);
|
|
7633
|
-
setTimeout(() => {
|
|
7634
|
-
process.exit(130);
|
|
7635
|
-
}, 100);
|
|
7636
|
-
};
|
|
7637
|
-
if (!shutdownRegistered) {
|
|
7638
|
-
process.on("SIGINT", handler);
|
|
7639
|
-
process.on("SIGTERM", handler);
|
|
7640
|
-
shutdownRegistered = true;
|
|
7641
8851
|
}
|
|
7642
|
-
return () => {
|
|
7643
|
-
process.removeListener("SIGINT", handler);
|
|
7644
|
-
process.removeListener("SIGTERM", handler);
|
|
7645
|
-
shutdownRegistered = false;
|
|
7646
|
-
shutdownContext = null;
|
|
7647
|
-
interruptCount = 0;
|
|
7648
|
-
if (interruptTimer) {
|
|
7649
|
-
clearTimeout(interruptTimer);
|
|
7650
|
-
interruptTimer = null;
|
|
7651
|
-
}
|
|
7652
|
-
};
|
|
7653
8852
|
}
|
|
7654
|
-
var
|
|
7655
|
-
|
|
7656
|
-
|
|
8853
|
+
var init_conflict = __esm(() => {
|
|
8854
|
+
init_terminal();
|
|
8855
|
+
init_logger();
|
|
7657
8856
|
});
|
|
7658
8857
|
|
|
7659
8858
|
// src/core/worktree.ts
|
|
7660
|
-
import { execSync as
|
|
7661
|
-
import { existsSync as
|
|
7662
|
-
import { join as
|
|
8859
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
8860
|
+
import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
|
|
8861
|
+
import { join as join17 } from "node:path";
|
|
7663
8862
|
function git3(args, cwd) {
|
|
7664
|
-
return
|
|
8863
|
+
return execSync15(`git ${args}`, {
|
|
7665
8864
|
cwd,
|
|
7666
8865
|
encoding: "utf-8",
|
|
7667
8866
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7675,10 +8874,10 @@ function gitSafe2(args, cwd) {
|
|
|
7675
8874
|
}
|
|
7676
8875
|
}
|
|
7677
8876
|
function getWorktreeDir(projectRoot) {
|
|
7678
|
-
return
|
|
8877
|
+
return join17(projectRoot, ".locus", "worktrees");
|
|
7679
8878
|
}
|
|
7680
8879
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
7681
|
-
return
|
|
8880
|
+
return join17(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
7682
8881
|
}
|
|
7683
8882
|
function generateBranchName(issueNumber) {
|
|
7684
8883
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -7686,7 +8885,7 @@ function generateBranchName(issueNumber) {
|
|
|
7686
8885
|
}
|
|
7687
8886
|
function getWorktreeBranch(worktreePath) {
|
|
7688
8887
|
try {
|
|
7689
|
-
return
|
|
8888
|
+
return execSync15("git branch --show-current", {
|
|
7690
8889
|
cwd: worktreePath,
|
|
7691
8890
|
encoding: "utf-8",
|
|
7692
8891
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7698,7 +8897,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
7698
8897
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
7699
8898
|
const log = getLogger();
|
|
7700
8899
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
7701
|
-
if (
|
|
8900
|
+
if (existsSync17(worktreePath)) {
|
|
7702
8901
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
7703
8902
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
7704
8903
|
return {
|
|
@@ -7725,7 +8924,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
7725
8924
|
function removeWorktree(projectRoot, issueNumber) {
|
|
7726
8925
|
const log = getLogger();
|
|
7727
8926
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
7728
|
-
if (!
|
|
8927
|
+
if (!existsSync17(worktreePath)) {
|
|
7729
8928
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
7730
8929
|
return;
|
|
7731
8930
|
}
|
|
@@ -7744,7 +8943,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
7744
8943
|
function listWorktrees(projectRoot) {
|
|
7745
8944
|
const log = getLogger();
|
|
7746
8945
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
7747
|
-
if (!
|
|
8946
|
+
if (!existsSync17(worktreeDir)) {
|
|
7748
8947
|
return [];
|
|
7749
8948
|
}
|
|
7750
8949
|
const entries = readdirSync6(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -7764,7 +8963,7 @@ function listWorktrees(projectRoot) {
|
|
|
7764
8963
|
if (!match)
|
|
7765
8964
|
continue;
|
|
7766
8965
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
7767
|
-
const path =
|
|
8966
|
+
const path = join17(worktreeDir, entry);
|
|
7768
8967
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
7769
8968
|
let resolvedPath;
|
|
7770
8969
|
try {
|
|
@@ -7811,8 +9010,44 @@ var exports_run = {};
|
|
|
7811
9010
|
__export(exports_run, {
|
|
7812
9011
|
runCommand: () => runCommand
|
|
7813
9012
|
});
|
|
7814
|
-
import { execSync as
|
|
9013
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
9014
|
+
function printRunHelp() {
|
|
9015
|
+
process.stderr.write(`
|
|
9016
|
+
${bold("locus run")} — Execute issues using AI agents
|
|
9017
|
+
|
|
9018
|
+
${bold("Usage:")}
|
|
9019
|
+
locus run ${dim("# Run active sprint (sequential)")}
|
|
9020
|
+
locus run <issue> ${dim("# Run single issue (worktree)")}
|
|
9021
|
+
locus run <issue> <issue> ... ${dim("# Run multiple issues (parallel)")}
|
|
9022
|
+
locus run --resume ${dim("# Resume interrupted run")}
|
|
9023
|
+
|
|
9024
|
+
${bold("Options:")}
|
|
9025
|
+
--resume Resume a previously interrupted run
|
|
9026
|
+
--dry-run Show what would happen without executing
|
|
9027
|
+
--model <name> Override the AI model for this run
|
|
9028
|
+
--no-sandbox Disable Docker sandbox isolation
|
|
9029
|
+
--sandbox=require Require Docker sandbox (fail if unavailable)
|
|
9030
|
+
|
|
9031
|
+
${bold("Sandbox:")}
|
|
9032
|
+
By default, agents run inside Docker Desktop sandboxes (4.58+) for
|
|
9033
|
+
hypervisor-level isolation. If Docker is not available, agents run
|
|
9034
|
+
unsandboxed with a warning.
|
|
9035
|
+
|
|
9036
|
+
${bold("Examples:")}
|
|
9037
|
+
locus run ${dim("# Execute active sprint")}
|
|
9038
|
+
locus run 42 ${dim("# Run single issue")}
|
|
9039
|
+
locus run 42 43 44 ${dim("# Run issues in parallel")}
|
|
9040
|
+
locus run --resume ${dim("# Resume after failure")}
|
|
9041
|
+
locus run 42 --no-sandbox ${dim("# Run without sandbox")}
|
|
9042
|
+
locus run 42 --sandbox=require ${dim("# Require sandbox")}
|
|
9043
|
+
|
|
9044
|
+
`);
|
|
9045
|
+
}
|
|
7815
9046
|
async function runCommand(projectRoot, args, flags = {}) {
|
|
9047
|
+
if (args[0] === "help") {
|
|
9048
|
+
printRunHelp();
|
|
9049
|
+
return;
|
|
9050
|
+
}
|
|
7816
9051
|
const config = loadConfig(projectRoot);
|
|
7817
9052
|
const _log = getLogger();
|
|
7818
9053
|
const cleanupShutdown = registerShutdownHandlers({
|
|
@@ -7820,22 +9055,43 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
7820
9055
|
getRunState: () => loadRunState(projectRoot)
|
|
7821
9056
|
});
|
|
7822
9057
|
try {
|
|
9058
|
+
const sandboxMode = resolveSandboxMode(config.sandbox, flags);
|
|
9059
|
+
let sandboxed = false;
|
|
9060
|
+
if (sandboxMode !== "disabled") {
|
|
9061
|
+
const status = await detectSandboxSupport();
|
|
9062
|
+
if (!status.available) {
|
|
9063
|
+
if (sandboxMode === "required") {
|
|
9064
|
+
process.stderr.write(`${red("✗")} Docker sandbox required but not available: ${status.reason}
|
|
9065
|
+
`);
|
|
9066
|
+
process.stderr.write(` Install Docker Desktop 4.58+ or remove --sandbox=require to continue.
|
|
9067
|
+
`);
|
|
9068
|
+
process.exit(1);
|
|
9069
|
+
}
|
|
9070
|
+
process.stderr.write(`${yellow("⚠")} Docker sandbox not available: ${status.reason}. Running unsandboxed.
|
|
9071
|
+
`);
|
|
9072
|
+
} else {
|
|
9073
|
+
sandboxed = true;
|
|
9074
|
+
}
|
|
9075
|
+
} else if (flags.noSandbox) {
|
|
9076
|
+
process.stderr.write(`${yellow("⚠")} Running without sandbox. The AI agent will have unrestricted access to your filesystem, network, and environment variables.
|
|
9077
|
+
`);
|
|
9078
|
+
}
|
|
7823
9079
|
if (flags.resume) {
|
|
7824
|
-
return handleResume(projectRoot, config);
|
|
9080
|
+
return handleResume(projectRoot, config, sandboxed);
|
|
7825
9081
|
}
|
|
7826
9082
|
const issueNumbers = args.filter((a) => /^\d+$/.test(a)).map(Number);
|
|
7827
9083
|
if (issueNumbers.length === 0) {
|
|
7828
|
-
return handleSprintRun(projectRoot, config, flags);
|
|
9084
|
+
return handleSprintRun(projectRoot, config, flags, sandboxed);
|
|
7829
9085
|
}
|
|
7830
9086
|
if (issueNumbers.length === 1) {
|
|
7831
|
-
return handleSingleIssue(projectRoot, config, issueNumbers[0], flags);
|
|
9087
|
+
return handleSingleIssue(projectRoot, config, issueNumbers[0], flags, sandboxed);
|
|
7832
9088
|
}
|
|
7833
|
-
return handleParallelRun(projectRoot, config, issueNumbers, flags);
|
|
9089
|
+
return handleParallelRun(projectRoot, config, issueNumbers, flags, sandboxed);
|
|
7834
9090
|
} finally {
|
|
7835
9091
|
cleanupShutdown();
|
|
7836
9092
|
}
|
|
7837
9093
|
}
|
|
7838
|
-
async function handleSprintRun(projectRoot, config, flags) {
|
|
9094
|
+
async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
7839
9095
|
const log = getLogger();
|
|
7840
9096
|
if (!config.sprint.active) {
|
|
7841
9097
|
process.stderr.write(`${red("✗")} No active sprint. Set one with: ${bold("locus sprint active <name>")}
|
|
@@ -7898,7 +9154,7 @@ ${yellow("⚠")} A sprint run is already in progress.
|
|
|
7898
9154
|
}
|
|
7899
9155
|
if (!flags.dryRun) {
|
|
7900
9156
|
try {
|
|
7901
|
-
|
|
9157
|
+
execSync16(`git checkout -B ${branchName}`, {
|
|
7902
9158
|
cwd: projectRoot,
|
|
7903
9159
|
encoding: "utf-8",
|
|
7904
9160
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7948,7 +9204,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
|
|
|
7948
9204
|
let sprintContext;
|
|
7949
9205
|
if (i > 0 && !flags.dryRun) {
|
|
7950
9206
|
try {
|
|
7951
|
-
sprintContext =
|
|
9207
|
+
sprintContext = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
7952
9208
|
cwd: projectRoot,
|
|
7953
9209
|
encoding: "utf-8",
|
|
7954
9210
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7967,12 +9223,17 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
7967
9223
|
model: flags.model ?? config.ai.model,
|
|
7968
9224
|
dryRun: flags.dryRun,
|
|
7969
9225
|
sprintContext,
|
|
7970
|
-
skipPR: true
|
|
9226
|
+
skipPR: true,
|
|
9227
|
+
sandboxed
|
|
7971
9228
|
});
|
|
7972
9229
|
if (result.success) {
|
|
7973
9230
|
if (!flags.dryRun) {
|
|
7974
9231
|
const issueTitle = issue?.title ?? "";
|
|
7975
9232
|
ensureTaskCommit(projectRoot, task.issue, issueTitle);
|
|
9233
|
+
if (sandboxed && i < state.tasks.length - 1) {
|
|
9234
|
+
process.stderr.write(` ${dim("↻ Sandbox will resync on next task")}
|
|
9235
|
+
`);
|
|
9236
|
+
}
|
|
7976
9237
|
}
|
|
7977
9238
|
markTaskDone(state, task.issue, result.prNumber);
|
|
7978
9239
|
} else {
|
|
@@ -8007,7 +9268,7 @@ ${bold("Summary:")}
|
|
|
8007
9268
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
8008
9269
|
if (prNumber !== undefined) {
|
|
8009
9270
|
try {
|
|
8010
|
-
|
|
9271
|
+
execSync16(`git checkout ${config.agent.baseBranch}`, {
|
|
8011
9272
|
cwd: projectRoot,
|
|
8012
9273
|
encoding: "utf-8",
|
|
8013
9274
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8021,7 +9282,7 @@ ${bold("Summary:")}
|
|
|
8021
9282
|
clearRunState(projectRoot);
|
|
8022
9283
|
}
|
|
8023
9284
|
}
|
|
8024
|
-
async function handleSingleIssue(projectRoot, config, issueNumber, flags) {
|
|
9285
|
+
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
8025
9286
|
let isSprintIssue = false;
|
|
8026
9287
|
try {
|
|
8027
9288
|
const issue = getIssue(issueNumber, { cwd: projectRoot });
|
|
@@ -8036,7 +9297,9 @@ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, n
|
|
|
8036
9297
|
issueNumber,
|
|
8037
9298
|
provider: config.ai.provider,
|
|
8038
9299
|
model: flags.model ?? config.ai.model,
|
|
8039
|
-
dryRun: flags.dryRun
|
|
9300
|
+
dryRun: flags.dryRun,
|
|
9301
|
+
sandboxed,
|
|
9302
|
+
sandboxName: config.sandbox.name
|
|
8040
9303
|
});
|
|
8041
9304
|
return;
|
|
8042
9305
|
}
|
|
@@ -8065,7 +9328,9 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
8065
9328
|
worktreePath,
|
|
8066
9329
|
provider: config.ai.provider,
|
|
8067
9330
|
model: flags.model ?? config.ai.model,
|
|
8068
|
-
dryRun: flags.dryRun
|
|
9331
|
+
dryRun: flags.dryRun,
|
|
9332
|
+
sandboxed,
|
|
9333
|
+
sandboxName: config.sandbox.name
|
|
8069
9334
|
});
|
|
8070
9335
|
if (worktreePath && !flags.dryRun) {
|
|
8071
9336
|
if (result.success) {
|
|
@@ -8078,7 +9343,7 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
8078
9343
|
}
|
|
8079
9344
|
}
|
|
8080
9345
|
}
|
|
8081
|
-
async function handleParallelRun(projectRoot, config, issueNumbers, flags) {
|
|
9346
|
+
async function handleParallelRun(projectRoot, config, issueNumbers, flags, sandboxed) {
|
|
8082
9347
|
const log = getLogger();
|
|
8083
9348
|
const maxConcurrent = config.agent.maxParallel;
|
|
8084
9349
|
process.stderr.write(`
|
|
@@ -8133,7 +9398,9 @@ ${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxCon
|
|
|
8133
9398
|
worktreePath,
|
|
8134
9399
|
provider: config.ai.provider,
|
|
8135
9400
|
model: flags.model ?? config.ai.model,
|
|
8136
|
-
dryRun: flags.dryRun
|
|
9401
|
+
dryRun: flags.dryRun,
|
|
9402
|
+
sandboxed,
|
|
9403
|
+
sandboxName: config.sandbox.name
|
|
8137
9404
|
});
|
|
8138
9405
|
if (result.success) {
|
|
8139
9406
|
markTaskDone(state, issueNumber, result.prNumber);
|
|
@@ -8145,9 +9412,19 @@ ${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxCon
|
|
|
8145
9412
|
markTaskFailed(state, issueNumber, result.error ?? "Unknown error");
|
|
8146
9413
|
}
|
|
8147
9414
|
saveRunState(projectRoot, state);
|
|
8148
|
-
|
|
9415
|
+
return { issue: issueNumber, success: result.success };
|
|
8149
9416
|
});
|
|
8150
|
-
await Promise.
|
|
9417
|
+
const settled = await Promise.allSettled(promises);
|
|
9418
|
+
for (const outcome of settled) {
|
|
9419
|
+
if (outcome.status === "fulfilled") {
|
|
9420
|
+
results.push(outcome.value);
|
|
9421
|
+
} else {
|
|
9422
|
+
const idx = settled.indexOf(outcome);
|
|
9423
|
+
const issueNumber = batch[idx];
|
|
9424
|
+
log.warn(`Parallel task #${issueNumber} threw: ${outcome.reason}`);
|
|
9425
|
+
results.push({ issue: issueNumber, success: false });
|
|
9426
|
+
}
|
|
9427
|
+
}
|
|
8151
9428
|
}
|
|
8152
9429
|
const succeeded = results.filter((r) => r.success).length;
|
|
8153
9430
|
const failed = results.filter((r) => !r.success).length;
|
|
@@ -8172,7 +9449,7 @@ ${yellow("⚠")} Failed worktrees preserved for debugging:
|
|
|
8172
9449
|
clearRunState(projectRoot);
|
|
8173
9450
|
}
|
|
8174
9451
|
}
|
|
8175
|
-
async function handleResume(projectRoot, config) {
|
|
9452
|
+
async function handleResume(projectRoot, config, sandboxed) {
|
|
8176
9453
|
const state = loadRunState(projectRoot);
|
|
8177
9454
|
if (!state) {
|
|
8178
9455
|
process.stderr.write(`${red("✗")} No run state found. Nothing to resume.
|
|
@@ -8188,13 +9465,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
8188
9465
|
`);
|
|
8189
9466
|
if (state.type === "sprint" && state.branch) {
|
|
8190
9467
|
try {
|
|
8191
|
-
const currentBranch =
|
|
9468
|
+
const currentBranch = execSync16("git rev-parse --abbrev-ref HEAD", {
|
|
8192
9469
|
cwd: projectRoot,
|
|
8193
9470
|
encoding: "utf-8",
|
|
8194
9471
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8195
9472
|
}).trim();
|
|
8196
9473
|
if (currentBranch !== state.branch) {
|
|
8197
|
-
|
|
9474
|
+
execSync16(`git checkout ${state.branch}`, {
|
|
8198
9475
|
cwd: projectRoot,
|
|
8199
9476
|
encoding: "utf-8",
|
|
8200
9477
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8220,7 +9497,9 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
8220
9497
|
issueNumber: task.issue,
|
|
8221
9498
|
provider: config.ai.provider,
|
|
8222
9499
|
model: config.ai.model,
|
|
8223
|
-
skipPR: isSprintRun
|
|
9500
|
+
skipPR: isSprintRun,
|
|
9501
|
+
sandboxed,
|
|
9502
|
+
sandboxName: isSprintRun ? undefined : config.sandbox.name
|
|
8224
9503
|
});
|
|
8225
9504
|
if (result.success) {
|
|
8226
9505
|
if (isSprintRun) {
|
|
@@ -8230,6 +9509,10 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
8230
9509
|
issueTitle = iss.title;
|
|
8231
9510
|
} catch {}
|
|
8232
9511
|
ensureTaskCommit(projectRoot, task.issue, issueTitle);
|
|
9512
|
+
if (sandboxed) {
|
|
9513
|
+
process.stderr.write(` ${dim("↻ Sandbox will resync on next task")}
|
|
9514
|
+
`);
|
|
9515
|
+
}
|
|
8233
9516
|
}
|
|
8234
9517
|
markTaskDone(state, task.issue, result.prNumber);
|
|
8235
9518
|
} else {
|
|
@@ -8255,7 +9538,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
|
|
|
8255
9538
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
8256
9539
|
if (prNumber !== undefined) {
|
|
8257
9540
|
try {
|
|
8258
|
-
|
|
9541
|
+
execSync16(`git checkout ${config.agent.baseBranch}`, {
|
|
8259
9542
|
cwd: projectRoot,
|
|
8260
9543
|
encoding: "utf-8",
|
|
8261
9544
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8286,14 +9569,14 @@ function getOrder2(issue) {
|
|
|
8286
9569
|
}
|
|
8287
9570
|
function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
8288
9571
|
try {
|
|
8289
|
-
const status =
|
|
9572
|
+
const status = execSync16("git status --porcelain", {
|
|
8290
9573
|
cwd: projectRoot,
|
|
8291
9574
|
encoding: "utf-8",
|
|
8292
9575
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8293
9576
|
}).trim();
|
|
8294
9577
|
if (!status)
|
|
8295
9578
|
return;
|
|
8296
|
-
|
|
9579
|
+
execSync16("git add -A", {
|
|
8297
9580
|
cwd: projectRoot,
|
|
8298
9581
|
encoding: "utf-8",
|
|
8299
9582
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8301,7 +9584,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
8301
9584
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
8302
9585
|
|
|
8303
9586
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
8304
|
-
|
|
9587
|
+
execSync16(`git commit -F -`, {
|
|
8305
9588
|
input: message,
|
|
8306
9589
|
cwd: projectRoot,
|
|
8307
9590
|
encoding: "utf-8",
|
|
@@ -8315,7 +9598,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
8315
9598
|
if (!config.agent.autoPR)
|
|
8316
9599
|
return;
|
|
8317
9600
|
try {
|
|
8318
|
-
const diff =
|
|
9601
|
+
const diff = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8319
9602
|
cwd: projectRoot,
|
|
8320
9603
|
encoding: "utf-8",
|
|
8321
9604
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8325,7 +9608,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
8325
9608
|
`);
|
|
8326
9609
|
return;
|
|
8327
9610
|
}
|
|
8328
|
-
|
|
9611
|
+
execSync16(`git push -u origin ${branchName}`, {
|
|
8329
9612
|
cwd: projectRoot,
|
|
8330
9613
|
encoding: "utf-8",
|
|
8331
9614
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8358,6 +9641,7 @@ var init_run = __esm(() => {
|
|
|
8358
9641
|
init_logger();
|
|
8359
9642
|
init_rate_limiter();
|
|
8360
9643
|
init_run_state();
|
|
9644
|
+
init_sandbox();
|
|
8361
9645
|
init_shutdown();
|
|
8362
9646
|
init_worktree();
|
|
8363
9647
|
init_progress();
|
|
@@ -8371,6 +9655,8 @@ __export(exports_status, {
|
|
|
8371
9655
|
});
|
|
8372
9656
|
async function statusCommand(projectRoot) {
|
|
8373
9657
|
const config = loadConfig(projectRoot);
|
|
9658
|
+
const spinner = new Spinner;
|
|
9659
|
+
spinner.start("Fetching project status...");
|
|
8374
9660
|
const lines = [];
|
|
8375
9661
|
lines.push(` ${dim("Repo:")} ${cyan(`${config.github.owner}/${config.github.repo}`)}`);
|
|
8376
9662
|
lines.push(` ${dim("Provider:")} ${config.ai.provider} / ${config.ai.model}`);
|
|
@@ -8447,6 +9733,7 @@ async function statusCommand(projectRoot) {
|
|
|
8447
9733
|
}
|
|
8448
9734
|
}
|
|
8449
9735
|
} catch {}
|
|
9736
|
+
spinner.stop();
|
|
8450
9737
|
lines.push("");
|
|
8451
9738
|
process.stderr.write(`
|
|
8452
9739
|
${drawBox(lines, { title: "Locus Status" })}
|
|
@@ -8470,13 +9757,13 @@ __export(exports_plan, {
|
|
|
8470
9757
|
parsePlanArgs: () => parsePlanArgs
|
|
8471
9758
|
});
|
|
8472
9759
|
import {
|
|
8473
|
-
existsSync as
|
|
8474
|
-
mkdirSync as
|
|
9760
|
+
existsSync as existsSync18,
|
|
9761
|
+
mkdirSync as mkdirSync12,
|
|
8475
9762
|
readdirSync as readdirSync7,
|
|
8476
|
-
readFileSync as
|
|
9763
|
+
readFileSync as readFileSync13,
|
|
8477
9764
|
writeFileSync as writeFileSync9
|
|
8478
9765
|
} from "node:fs";
|
|
8479
|
-
import { join as
|
|
9766
|
+
import { join as join18 } from "node:path";
|
|
8480
9767
|
function printHelp() {
|
|
8481
9768
|
process.stderr.write(`
|
|
8482
9769
|
${bold("locus plan")} — AI-powered sprint planning
|
|
@@ -8507,28 +9794,28 @@ function normalizeSprintName(name) {
|
|
|
8507
9794
|
return name.trim().toLowerCase();
|
|
8508
9795
|
}
|
|
8509
9796
|
function getPlansDir(projectRoot) {
|
|
8510
|
-
return
|
|
9797
|
+
return join18(projectRoot, ".locus", "plans");
|
|
8511
9798
|
}
|
|
8512
9799
|
function ensurePlansDir(projectRoot) {
|
|
8513
9800
|
const dir = getPlansDir(projectRoot);
|
|
8514
|
-
if (!
|
|
8515
|
-
|
|
9801
|
+
if (!existsSync18(dir)) {
|
|
9802
|
+
mkdirSync12(dir, { recursive: true });
|
|
8516
9803
|
}
|
|
8517
9804
|
return dir;
|
|
8518
9805
|
}
|
|
8519
9806
|
function generateId() {
|
|
8520
|
-
return `${Math.random().toString(36).slice(2, 8)}`;
|
|
9807
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8521
9808
|
}
|
|
8522
9809
|
function loadPlanFile(projectRoot, id) {
|
|
8523
9810
|
const dir = getPlansDir(projectRoot);
|
|
8524
|
-
if (!
|
|
9811
|
+
if (!existsSync18(dir))
|
|
8525
9812
|
return null;
|
|
8526
9813
|
const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
|
|
8527
9814
|
const match = files.find((f) => f.startsWith(id));
|
|
8528
9815
|
if (!match)
|
|
8529
9816
|
return null;
|
|
8530
9817
|
try {
|
|
8531
|
-
const content =
|
|
9818
|
+
const content = readFileSync13(join18(dir, match), "utf-8");
|
|
8532
9819
|
return JSON.parse(content);
|
|
8533
9820
|
} catch {
|
|
8534
9821
|
return null;
|
|
@@ -8574,7 +9861,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
8574
9861
|
}
|
|
8575
9862
|
function handleListPlans(projectRoot) {
|
|
8576
9863
|
const dir = getPlansDir(projectRoot);
|
|
8577
|
-
if (!
|
|
9864
|
+
if (!existsSync18(dir)) {
|
|
8578
9865
|
process.stderr.write(`${dim("No saved plans yet.")}
|
|
8579
9866
|
`);
|
|
8580
9867
|
return;
|
|
@@ -8592,7 +9879,7 @@ ${bold("Saved Plans:")}
|
|
|
8592
9879
|
for (const file of files) {
|
|
8593
9880
|
const id = file.replace(".json", "");
|
|
8594
9881
|
try {
|
|
8595
|
-
const content =
|
|
9882
|
+
const content = readFileSync13(join18(dir, file), "utf-8");
|
|
8596
9883
|
const plan = JSON.parse(content);
|
|
8597
9884
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
8598
9885
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -8703,7 +9990,7 @@ ${bold("Approving plan:")}
|
|
|
8703
9990
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
8704
9991
|
const id = generateId();
|
|
8705
9992
|
const plansDir = ensurePlansDir(projectRoot);
|
|
8706
|
-
const planPath =
|
|
9993
|
+
const planPath = join18(plansDir, `${id}.json`);
|
|
8707
9994
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
8708
9995
|
const displayDirective = directive;
|
|
8709
9996
|
process.stderr.write(`
|
|
@@ -8721,7 +10008,9 @@ ${bold("Planning:")} ${cyan(displayDirective)}
|
|
|
8721
10008
|
provider: config.ai.provider,
|
|
8722
10009
|
model: flags.model ?? config.ai.model,
|
|
8723
10010
|
cwd: projectRoot,
|
|
8724
|
-
activity: "planning"
|
|
10011
|
+
activity: "planning",
|
|
10012
|
+
sandboxed: config.sandbox.enabled,
|
|
10013
|
+
sandboxName: config.sandbox.name
|
|
8725
10014
|
});
|
|
8726
10015
|
if (aiResult.interrupted) {
|
|
8727
10016
|
process.stderr.write(`
|
|
@@ -8735,7 +10024,7 @@ ${red("✗")} Planning failed: ${aiResult.error}
|
|
|
8735
10024
|
`);
|
|
8736
10025
|
return;
|
|
8737
10026
|
}
|
|
8738
|
-
if (!
|
|
10027
|
+
if (!existsSync18(planPath)) {
|
|
8739
10028
|
process.stderr.write(`
|
|
8740
10029
|
${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
|
|
8741
10030
|
`);
|
|
@@ -8745,7 +10034,7 @@ ${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
|
|
|
8745
10034
|
}
|
|
8746
10035
|
let plan;
|
|
8747
10036
|
try {
|
|
8748
|
-
const content =
|
|
10037
|
+
const content = readFileSync13(planPath, "utf-8");
|
|
8749
10038
|
plan = JSON.parse(content);
|
|
8750
10039
|
} catch {
|
|
8751
10040
|
process.stderr.write(`
|
|
@@ -8829,7 +10118,9 @@ Start with foundational/setup tasks, then core features, then integration/testin
|
|
|
8829
10118
|
model: flags.model ?? config.ai.model,
|
|
8830
10119
|
cwd: projectRoot,
|
|
8831
10120
|
activity: "issue ordering",
|
|
8832
|
-
silent: true
|
|
10121
|
+
silent: true,
|
|
10122
|
+
sandboxed: config.sandbox.enabled,
|
|
10123
|
+
sandboxName: config.sandbox.name
|
|
8833
10124
|
});
|
|
8834
10125
|
if (aiResult.interrupted) {
|
|
8835
10126
|
process.stderr.write(`
|
|
@@ -8900,16 +10191,16 @@ function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, pla
|
|
|
8900
10191
|
parts.push(`SPRINT: ${sprintName}`);
|
|
8901
10192
|
}
|
|
8902
10193
|
parts.push("");
|
|
8903
|
-
const locusPath =
|
|
8904
|
-
if (
|
|
8905
|
-
const content =
|
|
10194
|
+
const locusPath = join18(projectRoot, "LOCUS.md");
|
|
10195
|
+
if (existsSync18(locusPath)) {
|
|
10196
|
+
const content = readFileSync13(locusPath, "utf-8");
|
|
8906
10197
|
parts.push("PROJECT CONTEXT (LOCUS.md):");
|
|
8907
10198
|
parts.push(content.slice(0, 3000));
|
|
8908
10199
|
parts.push("");
|
|
8909
10200
|
}
|
|
8910
|
-
const learningsPath =
|
|
8911
|
-
if (
|
|
8912
|
-
const content =
|
|
10201
|
+
const learningsPath = join18(projectRoot, ".locus", "LEARNINGS.md");
|
|
10202
|
+
if (existsSync18(learningsPath)) {
|
|
10203
|
+
const content = readFileSync13(learningsPath, "utf-8");
|
|
8913
10204
|
parts.push("PAST LEARNINGS:");
|
|
8914
10205
|
parts.push(content.slice(0, 2000));
|
|
8915
10206
|
parts.push("");
|
|
@@ -9085,9 +10376,9 @@ var exports_review = {};
|
|
|
9085
10376
|
__export(exports_review, {
|
|
9086
10377
|
reviewCommand: () => reviewCommand
|
|
9087
10378
|
});
|
|
9088
|
-
import { execSync as
|
|
9089
|
-
import { existsSync as
|
|
9090
|
-
import { join as
|
|
10379
|
+
import { execSync as execSync17 } from "node:child_process";
|
|
10380
|
+
import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
|
|
10381
|
+
import { join as join19 } from "node:path";
|
|
9091
10382
|
function printHelp2() {
|
|
9092
10383
|
process.stderr.write(`
|
|
9093
10384
|
${bold("locus review")} — AI-powered code review
|
|
@@ -9163,7 +10454,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
|
|
|
9163
10454
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
9164
10455
|
let prInfo;
|
|
9165
10456
|
try {
|
|
9166
|
-
const result =
|
|
10457
|
+
const result = execSync17(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
9167
10458
|
const raw = JSON.parse(result);
|
|
9168
10459
|
prInfo = {
|
|
9169
10460
|
number: raw.number,
|
|
@@ -9206,7 +10497,9 @@ async function reviewPR(projectRoot, config, pr, focus, flags) {
|
|
|
9206
10497
|
provider: config.ai.provider,
|
|
9207
10498
|
model: flags.model ?? config.ai.model,
|
|
9208
10499
|
cwd: projectRoot,
|
|
9209
|
-
activity: `PR #${pr.number}
|
|
10500
|
+
activity: `PR #${pr.number}`,
|
|
10501
|
+
sandboxed: config.sandbox.enabled,
|
|
10502
|
+
sandboxName: config.sandbox.name
|
|
9210
10503
|
});
|
|
9211
10504
|
if (aiResult.interrupted) {
|
|
9212
10505
|
process.stderr.write(` ${yellow("⚡")} Review interrupted.
|
|
@@ -9227,7 +10520,7 @@ ${output.slice(0, 60000)}
|
|
|
9227
10520
|
|
|
9228
10521
|
---
|
|
9229
10522
|
_Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
|
|
9230
|
-
|
|
10523
|
+
execSync17(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
9231
10524
|
process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
|
|
9232
10525
|
`);
|
|
9233
10526
|
} catch (e) {
|
|
@@ -9244,9 +10537,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
9244
10537
|
const parts = [];
|
|
9245
10538
|
parts.push(`You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.`);
|
|
9246
10539
|
parts.push("");
|
|
9247
|
-
const locusPath =
|
|
9248
|
-
if (
|
|
9249
|
-
const content =
|
|
10540
|
+
const locusPath = join19(projectRoot, "LOCUS.md");
|
|
10541
|
+
if (existsSync19(locusPath)) {
|
|
10542
|
+
const content = readFileSync14(locusPath, "utf-8");
|
|
9250
10543
|
parts.push("PROJECT CONTEXT:");
|
|
9251
10544
|
parts.push(content.slice(0, 2000));
|
|
9252
10545
|
parts.push("");
|
|
@@ -9298,7 +10591,7 @@ var exports_iterate = {};
|
|
|
9298
10591
|
__export(exports_iterate, {
|
|
9299
10592
|
iterateCommand: () => iterateCommand
|
|
9300
10593
|
});
|
|
9301
|
-
import { execSync as
|
|
10594
|
+
import { execSync as execSync18 } from "node:child_process";
|
|
9302
10595
|
function printHelp3() {
|
|
9303
10596
|
process.stderr.write(`
|
|
9304
10597
|
${bold("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -9508,12 +10801,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
|
|
|
9508
10801
|
}
|
|
9509
10802
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
9510
10803
|
try {
|
|
9511
|
-
const result =
|
|
10804
|
+
const result = execSync18(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
9512
10805
|
const parsed = JSON.parse(result);
|
|
9513
10806
|
if (parsed.length > 0) {
|
|
9514
10807
|
return parsed[0].number;
|
|
9515
10808
|
}
|
|
9516
|
-
const branchResult =
|
|
10809
|
+
const branchResult = execSync18(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
9517
10810
|
const branchParsed = JSON.parse(branchResult);
|
|
9518
10811
|
if (branchParsed.length > 0) {
|
|
9519
10812
|
return branchParsed[0].number;
|
|
@@ -9548,14 +10841,14 @@ __export(exports_discuss, {
|
|
|
9548
10841
|
discussCommand: () => discussCommand
|
|
9549
10842
|
});
|
|
9550
10843
|
import {
|
|
9551
|
-
existsSync as
|
|
9552
|
-
mkdirSync as
|
|
10844
|
+
existsSync as existsSync20,
|
|
10845
|
+
mkdirSync as mkdirSync13,
|
|
9553
10846
|
readdirSync as readdirSync8,
|
|
9554
|
-
readFileSync as
|
|
10847
|
+
readFileSync as readFileSync15,
|
|
9555
10848
|
unlinkSync as unlinkSync5,
|
|
9556
10849
|
writeFileSync as writeFileSync10
|
|
9557
10850
|
} from "node:fs";
|
|
9558
|
-
import { join as
|
|
10851
|
+
import { join as join20 } from "node:path";
|
|
9559
10852
|
function printHelp4() {
|
|
9560
10853
|
process.stderr.write(`
|
|
9561
10854
|
${bold("locus discuss")} — AI-powered architectural discussions
|
|
@@ -9577,12 +10870,12 @@ ${bold("Examples:")}
|
|
|
9577
10870
|
`);
|
|
9578
10871
|
}
|
|
9579
10872
|
function getDiscussionsDir(projectRoot) {
|
|
9580
|
-
return
|
|
10873
|
+
return join20(projectRoot, ".locus", "discussions");
|
|
9581
10874
|
}
|
|
9582
10875
|
function ensureDiscussionsDir(projectRoot) {
|
|
9583
10876
|
const dir = getDiscussionsDir(projectRoot);
|
|
9584
|
-
if (!
|
|
9585
|
-
|
|
10877
|
+
if (!existsSync20(dir)) {
|
|
10878
|
+
mkdirSync13(dir, { recursive: true });
|
|
9586
10879
|
}
|
|
9587
10880
|
return dir;
|
|
9588
10881
|
}
|
|
@@ -9616,7 +10909,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
9616
10909
|
}
|
|
9617
10910
|
function listDiscussions(projectRoot) {
|
|
9618
10911
|
const dir = getDiscussionsDir(projectRoot);
|
|
9619
|
-
if (!
|
|
10912
|
+
if (!existsSync20(dir)) {
|
|
9620
10913
|
process.stderr.write(`${dim("No discussions yet.")}
|
|
9621
10914
|
`);
|
|
9622
10915
|
return;
|
|
@@ -9633,7 +10926,7 @@ ${bold("Discussions:")}
|
|
|
9633
10926
|
`);
|
|
9634
10927
|
for (const file of files) {
|
|
9635
10928
|
const id = file.replace(".md", "");
|
|
9636
|
-
const content =
|
|
10929
|
+
const content = readFileSync15(join20(dir, file), "utf-8");
|
|
9637
10930
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
9638
10931
|
const title = titleMatch ? titleMatch[1] : id;
|
|
9639
10932
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -9651,7 +10944,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
9651
10944
|
return;
|
|
9652
10945
|
}
|
|
9653
10946
|
const dir = getDiscussionsDir(projectRoot);
|
|
9654
|
-
if (!
|
|
10947
|
+
if (!existsSync20(dir)) {
|
|
9655
10948
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
9656
10949
|
`);
|
|
9657
10950
|
return;
|
|
@@ -9663,7 +10956,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
9663
10956
|
`);
|
|
9664
10957
|
return;
|
|
9665
10958
|
}
|
|
9666
|
-
const content =
|
|
10959
|
+
const content = readFileSync15(join20(dir, match), "utf-8");
|
|
9667
10960
|
process.stdout.write(`${content}
|
|
9668
10961
|
`);
|
|
9669
10962
|
}
|
|
@@ -9674,7 +10967,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
9674
10967
|
return;
|
|
9675
10968
|
}
|
|
9676
10969
|
const dir = getDiscussionsDir(projectRoot);
|
|
9677
|
-
if (!
|
|
10970
|
+
if (!existsSync20(dir)) {
|
|
9678
10971
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
9679
10972
|
`);
|
|
9680
10973
|
return;
|
|
@@ -9686,7 +10979,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
9686
10979
|
`);
|
|
9687
10980
|
return;
|
|
9688
10981
|
}
|
|
9689
|
-
unlinkSync5(
|
|
10982
|
+
unlinkSync5(join20(dir, match));
|
|
9690
10983
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
9691
10984
|
`);
|
|
9692
10985
|
}
|
|
@@ -9699,7 +10992,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
9699
10992
|
return;
|
|
9700
10993
|
}
|
|
9701
10994
|
const dir = getDiscussionsDir(projectRoot);
|
|
9702
|
-
if (!
|
|
10995
|
+
if (!existsSync20(dir)) {
|
|
9703
10996
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
9704
10997
|
`);
|
|
9705
10998
|
return;
|
|
@@ -9711,7 +11004,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
9711
11004
|
`);
|
|
9712
11005
|
return;
|
|
9713
11006
|
}
|
|
9714
|
-
const content =
|
|
11007
|
+
const content = readFileSync15(join20(dir, match), "utf-8");
|
|
9715
11008
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
9716
11009
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
9717
11010
|
await planCommand(projectRoot, [
|
|
@@ -9756,7 +11049,9 @@ ${bold("Discussion:")} ${cyan(topic)}
|
|
|
9756
11049
|
provider: config.ai.provider,
|
|
9757
11050
|
model: flags.model ?? config.ai.model,
|
|
9758
11051
|
cwd: projectRoot,
|
|
9759
|
-
activity: "discussion"
|
|
11052
|
+
activity: "discussion",
|
|
11053
|
+
sandboxed: config.sandbox.enabled,
|
|
11054
|
+
sandboxName: config.sandbox.name
|
|
9760
11055
|
});
|
|
9761
11056
|
if (aiResult.interrupted) {
|
|
9762
11057
|
process.stderr.write(`
|
|
@@ -9823,7 +11118,7 @@ ${turn.content}`;
|
|
|
9823
11118
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
9824
11119
|
].join(`
|
|
9825
11120
|
`);
|
|
9826
|
-
writeFileSync10(
|
|
11121
|
+
writeFileSync10(join20(dir, `${id}.md`), markdown, "utf-8");
|
|
9827
11122
|
process.stderr.write(`
|
|
9828
11123
|
${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
|
|
9829
11124
|
`);
|
|
@@ -9837,16 +11132,16 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
9837
11132
|
const parts = [];
|
|
9838
11133
|
parts.push(`You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.`);
|
|
9839
11134
|
parts.push("");
|
|
9840
|
-
const locusPath =
|
|
9841
|
-
if (
|
|
9842
|
-
const content =
|
|
11135
|
+
const locusPath = join20(projectRoot, "LOCUS.md");
|
|
11136
|
+
if (existsSync20(locusPath)) {
|
|
11137
|
+
const content = readFileSync15(locusPath, "utf-8");
|
|
9843
11138
|
parts.push("PROJECT CONTEXT:");
|
|
9844
11139
|
parts.push(content.slice(0, 3000));
|
|
9845
11140
|
parts.push("");
|
|
9846
11141
|
}
|
|
9847
|
-
const learningsPath =
|
|
9848
|
-
if (
|
|
9849
|
-
const content =
|
|
11142
|
+
const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
|
|
11143
|
+
if (existsSync20(learningsPath)) {
|
|
11144
|
+
const content = readFileSync15(learningsPath, "utf-8");
|
|
9850
11145
|
parts.push("PAST LEARNINGS:");
|
|
9851
11146
|
parts.push(content.slice(0, 2000));
|
|
9852
11147
|
parts.push("");
|
|
@@ -9905,8 +11200,8 @@ __export(exports_artifacts, {
|
|
|
9905
11200
|
formatDate: () => formatDate2,
|
|
9906
11201
|
artifactsCommand: () => artifactsCommand
|
|
9907
11202
|
});
|
|
9908
|
-
import { existsSync as
|
|
9909
|
-
import { join as
|
|
11203
|
+
import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
|
|
11204
|
+
import { join as join21 } from "node:path";
|
|
9910
11205
|
function printHelp5() {
|
|
9911
11206
|
process.stderr.write(`
|
|
9912
11207
|
${bold("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -9926,14 +11221,14 @@ ${dim("Artifact names support partial matching.")}
|
|
|
9926
11221
|
`);
|
|
9927
11222
|
}
|
|
9928
11223
|
function getArtifactsDir(projectRoot) {
|
|
9929
|
-
return
|
|
11224
|
+
return join21(projectRoot, ".locus", "artifacts");
|
|
9930
11225
|
}
|
|
9931
11226
|
function listArtifacts(projectRoot) {
|
|
9932
11227
|
const dir = getArtifactsDir(projectRoot);
|
|
9933
|
-
if (!
|
|
11228
|
+
if (!existsSync21(dir))
|
|
9934
11229
|
return [];
|
|
9935
11230
|
return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
9936
|
-
const filePath =
|
|
11231
|
+
const filePath = join21(dir, fileName);
|
|
9937
11232
|
const stat = statSync4(filePath);
|
|
9938
11233
|
return {
|
|
9939
11234
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -9946,12 +11241,12 @@ function listArtifacts(projectRoot) {
|
|
|
9946
11241
|
function readArtifact(projectRoot, name) {
|
|
9947
11242
|
const dir = getArtifactsDir(projectRoot);
|
|
9948
11243
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
9949
|
-
const filePath =
|
|
9950
|
-
if (!
|
|
11244
|
+
const filePath = join21(dir, fileName);
|
|
11245
|
+
if (!existsSync21(filePath))
|
|
9951
11246
|
return null;
|
|
9952
11247
|
const stat = statSync4(filePath);
|
|
9953
11248
|
return {
|
|
9954
|
-
content:
|
|
11249
|
+
content: readFileSync16(filePath, "utf-8"),
|
|
9955
11250
|
info: {
|
|
9956
11251
|
name: fileName.replace(/\.md$/, ""),
|
|
9957
11252
|
fileName,
|
|
@@ -10109,23 +11404,276 @@ var init_artifacts = __esm(() => {
|
|
|
10109
11404
|
init_terminal();
|
|
10110
11405
|
});
|
|
10111
11406
|
|
|
11407
|
+
// src/commands/sandbox.ts
|
|
11408
|
+
var exports_sandbox2 = {};
|
|
11409
|
+
__export(exports_sandbox2, {
|
|
11410
|
+
sandboxCommand: () => sandboxCommand
|
|
11411
|
+
});
|
|
11412
|
+
import { execSync as execSync19, spawn as spawn6 } from "node:child_process";
|
|
11413
|
+
function printSandboxHelp() {
|
|
11414
|
+
process.stderr.write(`
|
|
11415
|
+
${bold("locus sandbox")} — Manage Docker sandbox lifecycle
|
|
11416
|
+
|
|
11417
|
+
${bold("Usage:")}
|
|
11418
|
+
locus sandbox ${dim("# Create sandbox and enable sandbox mode")}
|
|
11419
|
+
locus sandbox claude ${dim("# Run claude interactively (for login)")}
|
|
11420
|
+
locus sandbox codex ${dim("# Run codex interactively (for login)")}
|
|
11421
|
+
locus sandbox rm ${dim("# Destroy sandbox and disable sandbox mode")}
|
|
11422
|
+
locus sandbox status ${dim("# Show current sandbox state")}
|
|
11423
|
+
|
|
11424
|
+
${bold("Flow:")}
|
|
11425
|
+
1. ${cyan("locus sandbox")} Create the sandbox environment
|
|
11426
|
+
2. ${cyan("locus sandbox claude")} Login to Claude inside the sandbox
|
|
11427
|
+
3. ${cyan("locus exec")} All commands now run inside the sandbox
|
|
11428
|
+
|
|
11429
|
+
`);
|
|
11430
|
+
}
|
|
11431
|
+
async function sandboxCommand(projectRoot, args) {
|
|
11432
|
+
const subcommand = args[0] ?? "";
|
|
11433
|
+
switch (subcommand) {
|
|
11434
|
+
case "help":
|
|
11435
|
+
printSandboxHelp();
|
|
11436
|
+
return;
|
|
11437
|
+
case "claude":
|
|
11438
|
+
case "codex":
|
|
11439
|
+
return handleAgentLogin(projectRoot, subcommand);
|
|
11440
|
+
case "rm":
|
|
11441
|
+
return handleRemove(projectRoot);
|
|
11442
|
+
case "status":
|
|
11443
|
+
return handleStatus(projectRoot);
|
|
11444
|
+
case "":
|
|
11445
|
+
return handleCreate(projectRoot);
|
|
11446
|
+
default:
|
|
11447
|
+
process.stderr.write(`${red("✗")} Unknown sandbox subcommand: ${bold(subcommand)}
|
|
11448
|
+
`);
|
|
11449
|
+
process.stderr.write(` Available: ${cyan("claude")}, ${cyan("codex")}, ${cyan("rm")}, ${cyan("status")}
|
|
11450
|
+
`);
|
|
11451
|
+
}
|
|
11452
|
+
}
|
|
11453
|
+
async function handleCreate(projectRoot) {
|
|
11454
|
+
const config = loadConfig(projectRoot);
|
|
11455
|
+
if (config.sandbox.name) {
|
|
11456
|
+
const alive = isSandboxAlive(config.sandbox.name);
|
|
11457
|
+
if (alive) {
|
|
11458
|
+
process.stderr.write(`${green("✓")} Sandbox already exists: ${bold(config.sandbox.name)}
|
|
11459
|
+
`);
|
|
11460
|
+
process.stderr.write(` Run ${cyan("locus sandbox claude")} or ${cyan("locus sandbox codex")} to login.
|
|
11461
|
+
`);
|
|
11462
|
+
return;
|
|
11463
|
+
}
|
|
11464
|
+
process.stderr.write(`${yellow("⚠")} Previous sandbox ${dim(config.sandbox.name)} is no longer running. Creating a new one.
|
|
11465
|
+
`);
|
|
11466
|
+
}
|
|
11467
|
+
const status = await detectSandboxSupport();
|
|
11468
|
+
if (!status.available) {
|
|
11469
|
+
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
11470
|
+
`);
|
|
11471
|
+
process.stderr.write(` Install Docker Desktop 4.58+ with sandbox support.
|
|
11472
|
+
`);
|
|
11473
|
+
return;
|
|
11474
|
+
}
|
|
11475
|
+
const segment = projectRoot.split("/").pop() ?? "sandbox";
|
|
11476
|
+
const sandboxName = `locus-${segment}-${Date.now()}`;
|
|
11477
|
+
config.sandbox.enabled = true;
|
|
11478
|
+
config.sandbox.name = sandboxName;
|
|
11479
|
+
saveConfig(projectRoot, config);
|
|
11480
|
+
process.stderr.write(`${green("✓")} Sandbox name reserved: ${bold(sandboxName)}
|
|
11481
|
+
`);
|
|
11482
|
+
process.stderr.write(` Next: run ${cyan("locus sandbox claude")} or ${cyan("locus sandbox codex")} to create the sandbox and login.
|
|
11483
|
+
`);
|
|
11484
|
+
}
|
|
11485
|
+
async function handleAgentLogin(projectRoot, agent) {
|
|
11486
|
+
const config = loadConfig(projectRoot);
|
|
11487
|
+
if (!config.sandbox.name) {
|
|
11488
|
+
const status = await detectSandboxSupport();
|
|
11489
|
+
if (!status.available) {
|
|
11490
|
+
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
11491
|
+
`);
|
|
11492
|
+
process.stderr.write(` Install Docker Desktop 4.58+ with sandbox support.
|
|
11493
|
+
`);
|
|
11494
|
+
return;
|
|
11495
|
+
}
|
|
11496
|
+
const segment = projectRoot.split("/").pop() ?? "sandbox";
|
|
11497
|
+
config.sandbox.name = `locus-${segment}-${Date.now()}`;
|
|
11498
|
+
config.sandbox.enabled = true;
|
|
11499
|
+
saveConfig(projectRoot, config);
|
|
11500
|
+
}
|
|
11501
|
+
const sandboxName = config.sandbox.name;
|
|
11502
|
+
const alive = isSandboxAlive(sandboxName);
|
|
11503
|
+
let dockerArgs;
|
|
11504
|
+
if (alive) {
|
|
11505
|
+
if (agent === "codex") {
|
|
11506
|
+
await ensureCodexInSandbox(sandboxName);
|
|
11507
|
+
}
|
|
11508
|
+
process.stderr.write(`Connecting to sandbox ${dim(sandboxName)}...
|
|
11509
|
+
`);
|
|
11510
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11511
|
+
|
|
11512
|
+
`);
|
|
11513
|
+
dockerArgs = [
|
|
11514
|
+
"sandbox",
|
|
11515
|
+
"exec",
|
|
11516
|
+
"-it",
|
|
11517
|
+
"-w",
|
|
11518
|
+
projectRoot,
|
|
11519
|
+
sandboxName,
|
|
11520
|
+
agent
|
|
11521
|
+
];
|
|
11522
|
+
} else if (agent === "codex") {
|
|
11523
|
+
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11524
|
+
`);
|
|
11525
|
+
try {
|
|
11526
|
+
execSync19(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
11527
|
+
} catch {}
|
|
11528
|
+
if (!isSandboxAlive(sandboxName)) {
|
|
11529
|
+
process.stderr.write(`${red("✗")} Failed to create sandbox.
|
|
11530
|
+
`);
|
|
11531
|
+
return;
|
|
11532
|
+
}
|
|
11533
|
+
await ensureCodexInSandbox(sandboxName);
|
|
11534
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11535
|
+
|
|
11536
|
+
`);
|
|
11537
|
+
dockerArgs = [
|
|
11538
|
+
"sandbox",
|
|
11539
|
+
"exec",
|
|
11540
|
+
"-it",
|
|
11541
|
+
"-w",
|
|
11542
|
+
projectRoot,
|
|
11543
|
+
sandboxName,
|
|
11544
|
+
"codex"
|
|
11545
|
+
];
|
|
11546
|
+
} else {
|
|
11547
|
+
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11548
|
+
`);
|
|
11549
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11550
|
+
|
|
11551
|
+
`);
|
|
11552
|
+
dockerArgs = ["sandbox", "run", "--name", sandboxName, agent, projectRoot];
|
|
11553
|
+
}
|
|
11554
|
+
const child = spawn6("docker", dockerArgs, {
|
|
11555
|
+
stdio: "inherit"
|
|
11556
|
+
});
|
|
11557
|
+
await new Promise((resolve2) => {
|
|
11558
|
+
child.on("close", async (code) => {
|
|
11559
|
+
await enforceSandboxIgnore(sandboxName, projectRoot);
|
|
11560
|
+
if (code === 0) {
|
|
11561
|
+
process.stderr.write(`
|
|
11562
|
+
${green("✓")} ${agent} session ended. Auth should now be persisted in the sandbox.
|
|
11563
|
+
`);
|
|
11564
|
+
} else {
|
|
11565
|
+
process.stderr.write(`
|
|
11566
|
+
${yellow("⚠")} ${agent} exited with code ${code}.
|
|
11567
|
+
`);
|
|
11568
|
+
}
|
|
11569
|
+
resolve2();
|
|
11570
|
+
});
|
|
11571
|
+
child.on("error", (err) => {
|
|
11572
|
+
process.stderr.write(`${red("✗")} Failed to start ${agent}: ${err.message}
|
|
11573
|
+
`);
|
|
11574
|
+
resolve2();
|
|
11575
|
+
});
|
|
11576
|
+
});
|
|
11577
|
+
}
|
|
11578
|
+
function handleRemove(projectRoot) {
|
|
11579
|
+
const config = loadConfig(projectRoot);
|
|
11580
|
+
if (!config.sandbox.name) {
|
|
11581
|
+
process.stderr.write(`${dim("No sandbox to remove.")}
|
|
11582
|
+
`);
|
|
11583
|
+
return;
|
|
11584
|
+
}
|
|
11585
|
+
const sandboxName = config.sandbox.name;
|
|
11586
|
+
process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
|
|
11587
|
+
`);
|
|
11588
|
+
try {
|
|
11589
|
+
execSync19(`docker sandbox rm ${sandboxName}`, {
|
|
11590
|
+
encoding: "utf-8",
|
|
11591
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11592
|
+
timeout: 15000
|
|
11593
|
+
});
|
|
11594
|
+
} catch {}
|
|
11595
|
+
config.sandbox.name = undefined;
|
|
11596
|
+
config.sandbox.enabled = false;
|
|
11597
|
+
saveConfig(projectRoot, config);
|
|
11598
|
+
process.stderr.write(`${green("✓")} Sandbox removed. Sandbox mode disabled.
|
|
11599
|
+
`);
|
|
11600
|
+
}
|
|
11601
|
+
function handleStatus(projectRoot) {
|
|
11602
|
+
const config = loadConfig(projectRoot);
|
|
11603
|
+
process.stderr.write(`
|
|
11604
|
+
${bold("Sandbox Status")}
|
|
11605
|
+
|
|
11606
|
+
`);
|
|
11607
|
+
process.stderr.write(` ${dim("Enabled:")} ${config.sandbox.enabled ? green("yes") : red("no")}
|
|
11608
|
+
`);
|
|
11609
|
+
process.stderr.write(` ${dim("Name:")} ${config.sandbox.name ? bold(config.sandbox.name) : dim("(none)")}
|
|
11610
|
+
`);
|
|
11611
|
+
if (config.sandbox.name) {
|
|
11612
|
+
const alive = isSandboxAlive(config.sandbox.name);
|
|
11613
|
+
process.stderr.write(` ${dim("Running:")} ${alive ? green("yes") : red("no")}
|
|
11614
|
+
`);
|
|
11615
|
+
if (!alive) {
|
|
11616
|
+
process.stderr.write(`
|
|
11617
|
+
${yellow("⚠")} Sandbox is not running. Run ${bold("locus sandbox")} to create a new one.
|
|
11618
|
+
`);
|
|
11619
|
+
}
|
|
11620
|
+
}
|
|
11621
|
+
process.stderr.write(`
|
|
11622
|
+
`);
|
|
11623
|
+
}
|
|
11624
|
+
async function ensureCodexInSandbox(sandboxName) {
|
|
11625
|
+
try {
|
|
11626
|
+
execSync19(`docker sandbox exec ${sandboxName} which codex`, {
|
|
11627
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11628
|
+
timeout: 5000
|
|
11629
|
+
});
|
|
11630
|
+
} catch {
|
|
11631
|
+
process.stderr.write(`Installing codex in sandbox...
|
|
11632
|
+
`);
|
|
11633
|
+
try {
|
|
11634
|
+
execSync19(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
11635
|
+
} catch {
|
|
11636
|
+
process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
|
|
11637
|
+
`);
|
|
11638
|
+
}
|
|
11639
|
+
}
|
|
11640
|
+
}
|
|
11641
|
+
function isSandboxAlive(name) {
|
|
11642
|
+
try {
|
|
11643
|
+
const output = execSync19("docker sandbox ls", {
|
|
11644
|
+
encoding: "utf-8",
|
|
11645
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11646
|
+
timeout: 5000
|
|
11647
|
+
});
|
|
11648
|
+
return output.includes(name);
|
|
11649
|
+
} catch {
|
|
11650
|
+
return false;
|
|
11651
|
+
}
|
|
11652
|
+
}
|
|
11653
|
+
var init_sandbox2 = __esm(() => {
|
|
11654
|
+
init_config();
|
|
11655
|
+
init_sandbox();
|
|
11656
|
+
init_sandbox_ignore();
|
|
11657
|
+
init_terminal();
|
|
11658
|
+
});
|
|
11659
|
+
|
|
10112
11660
|
// src/cli.ts
|
|
10113
11661
|
init_config();
|
|
10114
11662
|
init_context();
|
|
10115
11663
|
init_logger();
|
|
10116
11664
|
init_rate_limiter();
|
|
10117
11665
|
init_terminal();
|
|
10118
|
-
import { existsSync as
|
|
10119
|
-
import { join as
|
|
11666
|
+
import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
|
|
11667
|
+
import { join as join22 } from "node:path";
|
|
10120
11668
|
import { fileURLToPath } from "node:url";
|
|
10121
11669
|
function getCliVersion() {
|
|
10122
11670
|
const fallbackVersion = "0.0.0";
|
|
10123
|
-
const packageJsonPath =
|
|
10124
|
-
if (!
|
|
11671
|
+
const packageJsonPath = join22(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
11672
|
+
if (!existsSync22(packageJsonPath)) {
|
|
10125
11673
|
return fallbackVersion;
|
|
10126
11674
|
}
|
|
10127
11675
|
try {
|
|
10128
|
-
const parsed = JSON.parse(
|
|
11676
|
+
const parsed = JSON.parse(readFileSync17(packageJsonPath, "utf-8"));
|
|
10129
11677
|
return parsed.version ?? fallbackVersion;
|
|
10130
11678
|
} catch {
|
|
10131
11679
|
return fallbackVersion;
|
|
@@ -10145,7 +11693,8 @@ function parseArgs(argv) {
|
|
|
10145
11693
|
dryRun: false,
|
|
10146
11694
|
check: false,
|
|
10147
11695
|
upgrade: false,
|
|
10148
|
-
list: false
|
|
11696
|
+
list: false,
|
|
11697
|
+
noSandbox: false
|
|
10149
11698
|
};
|
|
10150
11699
|
const positional = [];
|
|
10151
11700
|
let i = 0;
|
|
@@ -10222,7 +11771,14 @@ function parseArgs(argv) {
|
|
|
10222
11771
|
case "--target-version":
|
|
10223
11772
|
flags.targetVersion = rawArgs[++i];
|
|
10224
11773
|
break;
|
|
11774
|
+
case "--no-sandbox":
|
|
11775
|
+
flags.noSandbox = true;
|
|
11776
|
+
break;
|
|
10225
11777
|
default:
|
|
11778
|
+
if (arg.startsWith("--sandbox=")) {
|
|
11779
|
+
flags.sandbox = arg.slice("--sandbox=".length);
|
|
11780
|
+
break;
|
|
11781
|
+
}
|
|
10226
11782
|
positional.push(arg);
|
|
10227
11783
|
}
|
|
10228
11784
|
i++;
|
|
@@ -10256,6 +11812,7 @@ ${bold("Commands:")}
|
|
|
10256
11812
|
${cyan("uninstall")} Remove an installed package
|
|
10257
11813
|
${cyan("packages")} Manage installed packages (list, outdated)
|
|
10258
11814
|
${cyan("pkg")} ${dim("<name> [cmd]")} Run a command from an installed package
|
|
11815
|
+
${cyan("sandbox")} Manage Docker sandbox lifecycle
|
|
10259
11816
|
${cyan("upgrade")} Check for and install updates
|
|
10260
11817
|
|
|
10261
11818
|
${bold("Options:")}
|
|
@@ -10271,6 +11828,10 @@ ${bold("Examples:")}
|
|
|
10271
11828
|
locus plan approve <id> ${dim("# Create issues from saved plan")}
|
|
10272
11829
|
locus run ${dim("# Execute active sprint")}
|
|
10273
11830
|
locus run 42 43 ${dim("# Run issues in parallel")}
|
|
11831
|
+
locus run 42 --no-sandbox ${dim("# Run without sandbox")}
|
|
11832
|
+
locus run 42 --sandbox=require ${dim("# Require Docker sandbox")}
|
|
11833
|
+
locus sandbox ${dim("# Create Docker sandbox")}
|
|
11834
|
+
locus sandbox claude ${dim("# Login to Claude in sandbox")}
|
|
10274
11835
|
|
|
10275
11836
|
`);
|
|
10276
11837
|
}
|
|
@@ -10282,6 +11843,46 @@ function resolveAlias(command) {
|
|
|
10282
11843
|
};
|
|
10283
11844
|
return aliases[command] ?? command;
|
|
10284
11845
|
}
|
|
11846
|
+
function requiresSandboxSync(command, args, flags) {
|
|
11847
|
+
if (flags.noSandbox)
|
|
11848
|
+
return false;
|
|
11849
|
+
if (flags.help)
|
|
11850
|
+
return false;
|
|
11851
|
+
switch (command) {
|
|
11852
|
+
case "run":
|
|
11853
|
+
case "review":
|
|
11854
|
+
case "iterate":
|
|
11855
|
+
return true;
|
|
11856
|
+
case "exec":
|
|
11857
|
+
return args[0] !== "sessions" && args[0] !== "help";
|
|
11858
|
+
case "issue":
|
|
11859
|
+
return args[0] === "create";
|
|
11860
|
+
case "plan":
|
|
11861
|
+
if (args.length === 0)
|
|
11862
|
+
return false;
|
|
11863
|
+
return !["list", "show", "approve", "help"].includes(args[0]);
|
|
11864
|
+
case "discuss":
|
|
11865
|
+
if (args.length === 0)
|
|
11866
|
+
return false;
|
|
11867
|
+
return !["list", "show", "delete", "help"].includes(args[0]);
|
|
11868
|
+
case "config":
|
|
11869
|
+
return args[0] === "set";
|
|
11870
|
+
default:
|
|
11871
|
+
return false;
|
|
11872
|
+
}
|
|
11873
|
+
}
|
|
11874
|
+
async function prepareSandbox() {
|
|
11875
|
+
const { Spinner: Spinner2 } = await Promise.resolve().then(() => (init_progress(), exports_progress));
|
|
11876
|
+
const { detectSandboxSupport: detectSandboxSupport2 } = await Promise.resolve().then(() => (init_sandbox(), exports_sandbox));
|
|
11877
|
+
const spinner = new Spinner2;
|
|
11878
|
+
spinner.start("Preparing sandbox...");
|
|
11879
|
+
const status = await detectSandboxSupport2();
|
|
11880
|
+
if (status.available) {
|
|
11881
|
+
spinner.succeed("Sandbox ready");
|
|
11882
|
+
} else {
|
|
11883
|
+
spinner.warn(`Sandbox not available: ${status.reason}`);
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
10285
11886
|
async function main() {
|
|
10286
11887
|
const parsed = parseArgs(process.argv);
|
|
10287
11888
|
if (parsed.flags.version) {
|
|
@@ -10301,7 +11902,7 @@ async function main() {
|
|
|
10301
11902
|
try {
|
|
10302
11903
|
const root = getGitRoot(cwd);
|
|
10303
11904
|
if (isInitialized(root)) {
|
|
10304
|
-
logDir =
|
|
11905
|
+
logDir = join22(root, ".locus", "logs");
|
|
10305
11906
|
getRateLimiter(root);
|
|
10306
11907
|
}
|
|
10307
11908
|
} catch {}
|
|
@@ -10375,7 +11976,6 @@ async function main() {
|
|
|
10375
11976
|
process.stderr.write(`${red("✗")} Not inside a git repository.
|
|
10376
11977
|
`);
|
|
10377
11978
|
process.exit(1);
|
|
10378
|
-
return;
|
|
10379
11979
|
}
|
|
10380
11980
|
if (!isInitialized(projectRoot)) {
|
|
10381
11981
|
process.stderr.write(`${red("✗")} Locus is not initialized in this project.
|
|
@@ -10383,7 +11983,12 @@ async function main() {
|
|
|
10383
11983
|
process.stderr.write(` Run: ${bold("locus init")}
|
|
10384
11984
|
`);
|
|
10385
11985
|
process.exit(1);
|
|
10386
|
-
|
|
11986
|
+
}
|
|
11987
|
+
if (requiresSandboxSync(command, parsed.args, parsed.flags)) {
|
|
11988
|
+
const config = loadConfig(projectRoot);
|
|
11989
|
+
if (config.sandbox.enabled) {
|
|
11990
|
+
await prepareSandbox();
|
|
11991
|
+
}
|
|
10387
11992
|
}
|
|
10388
11993
|
switch (command) {
|
|
10389
11994
|
case "config": {
|
|
@@ -10428,7 +12033,9 @@ async function main() {
|
|
|
10428
12033
|
await runCommand2(projectRoot, runArgs, {
|
|
10429
12034
|
resume: parsed.flags.resume,
|
|
10430
12035
|
dryRun: parsed.flags.dryRun,
|
|
10431
|
-
model: parsed.flags.model
|
|
12036
|
+
model: parsed.flags.model,
|
|
12037
|
+
sandbox: parsed.flags.sandbox,
|
|
12038
|
+
noSandbox: parsed.flags.noSandbox
|
|
10432
12039
|
});
|
|
10433
12040
|
break;
|
|
10434
12041
|
}
|
|
@@ -10478,6 +12085,12 @@ async function main() {
|
|
|
10478
12085
|
await artifactsCommand2(projectRoot, artifactsArgs);
|
|
10479
12086
|
break;
|
|
10480
12087
|
}
|
|
12088
|
+
case "sandbox": {
|
|
12089
|
+
const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(), exports_sandbox2));
|
|
12090
|
+
const sandboxArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
12091
|
+
await sandboxCommand2(projectRoot, sandboxArgs);
|
|
12092
|
+
break;
|
|
12093
|
+
}
|
|
10481
12094
|
case "upgrade": {
|
|
10482
12095
|
const { upgradeCommand: upgradeCommand2 } = await Promise.resolve().then(() => (init_upgrade(), exports_upgrade));
|
|
10483
12096
|
await upgradeCommand2(projectRoot, parsed.args, {
|