@locusai/cli 0.17.16 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/locus.js +2011 -567
- 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
|
|
@@ -1582,6 +1603,15 @@ ${bold("Initializing Locus...")}
|
|
|
1582
1603
|
`);
|
|
1583
1604
|
} else {
|
|
1584
1605
|
process.stderr.write(`${dim("○")} LEARNINGS.md already exists (preserved)
|
|
1606
|
+
`);
|
|
1607
|
+
}
|
|
1608
|
+
const sandboxIgnorePath = join5(cwd, ".sandboxignore");
|
|
1609
|
+
if (!existsSync5(sandboxIgnorePath)) {
|
|
1610
|
+
writeFileSync4(sandboxIgnorePath, SANDBOXIGNORE_TEMPLATE, "utf-8");
|
|
1611
|
+
process.stderr.write(`${green("✓")} Generated .sandboxignore
|
|
1612
|
+
`);
|
|
1613
|
+
} else {
|
|
1614
|
+
process.stderr.write(`${dim("○")} .sandboxignore already exists (preserved)
|
|
1585
1615
|
`);
|
|
1586
1616
|
}
|
|
1587
1617
|
process.stderr.write(`${cyan("●")} Creating GitHub labels...`);
|
|
@@ -1627,6 +1657,23 @@ ${bold(green("Locus initialized!"))}
|
|
|
1627
1657
|
process.stderr.write(` ${gray("4.")} Start coding: ${bold("locus exec")}
|
|
1628
1658
|
`);
|
|
1629
1659
|
process.stderr.write(`
|
|
1660
|
+
${bold("Sandbox mode")} ${dim("(recommended)")}
|
|
1661
|
+
`);
|
|
1662
|
+
process.stderr.write(` Run AI agents in an isolated Docker sandbox for safety.
|
|
1663
|
+
|
|
1664
|
+
`);
|
|
1665
|
+
process.stderr.write(` ${gray("1.")} ${cyan("locus sandbox")} ${dim("Create the sandbox environment")}
|
|
1666
|
+
`);
|
|
1667
|
+
process.stderr.write(` ${gray("2.")} ${cyan("locus sandbox claude")} ${dim("Login to Claude inside the sandbox")}
|
|
1668
|
+
`);
|
|
1669
|
+
process.stderr.write(` ${gray("3.")} ${cyan("locus exec")} ${dim("All commands now run sandboxed")}
|
|
1670
|
+
`);
|
|
1671
|
+
process.stderr.write(`
|
|
1672
|
+
${dim("Using Codex? Run")} ${cyan("locus sandbox codex")} ${dim("instead of step 2.")}
|
|
1673
|
+
`);
|
|
1674
|
+
process.stderr.write(` ${dim("Learn more:")} ${cyan("locus sandbox help")}
|
|
1675
|
+
`);
|
|
1676
|
+
process.stderr.write(`
|
|
1630
1677
|
`);
|
|
1631
1678
|
log.info("Locus initialized", {
|
|
1632
1679
|
owner: context.owner,
|
|
@@ -1743,6 +1790,31 @@ Read ".locus/LEARNINGS.md" **before starting any task** to avoid repeating mista
|
|
|
1743
1790
|
## Development Workflow
|
|
1744
1791
|
|
|
1745
1792
|
<!-- How to run, test, build, and deploy the project -->
|
|
1793
|
+
`, SANDBOXIGNORE_TEMPLATE = `# Files and directories to exclude from sandbox environments.
|
|
1794
|
+
# Patterns follow .gitignore syntax (one per line, # for comments).
|
|
1795
|
+
# These files will be removed from the sandbox after creation.
|
|
1796
|
+
|
|
1797
|
+
# Environment files
|
|
1798
|
+
.env
|
|
1799
|
+
.env.*
|
|
1800
|
+
!.env.example
|
|
1801
|
+
|
|
1802
|
+
# Secrets and credentials
|
|
1803
|
+
*.pem
|
|
1804
|
+
*.key
|
|
1805
|
+
*.p12
|
|
1806
|
+
*.pfx
|
|
1807
|
+
*.keystore
|
|
1808
|
+
credentials.json
|
|
1809
|
+
service-account*.json
|
|
1810
|
+
|
|
1811
|
+
# Cloud provider configs
|
|
1812
|
+
.aws/
|
|
1813
|
+
.gcp/
|
|
1814
|
+
.azure/
|
|
1815
|
+
|
|
1816
|
+
# Docker secrets
|
|
1817
|
+
docker-compose.override.yml
|
|
1746
1818
|
`, LEARNINGS_MD_TEMPLATE = `# Learnings
|
|
1747
1819
|
|
|
1748
1820
|
This file captures important lessons, decisions, and corrections made during development.
|
|
@@ -2611,24 +2683,33 @@ var init_status_indicator = __esm(() => {
|
|
|
2611
2683
|
startTime = 0;
|
|
2612
2684
|
activity = "";
|
|
2613
2685
|
frame = 0;
|
|
2686
|
+
message = "";
|
|
2614
2687
|
static BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2615
2688
|
static DIAMOND = "◆";
|
|
2616
2689
|
start(message, options) {
|
|
2617
2690
|
this.stop();
|
|
2618
2691
|
this.startTime = Date.now();
|
|
2619
2692
|
this.activity = options?.activity ?? "";
|
|
2693
|
+
this.message = message;
|
|
2620
2694
|
this.frame = 0;
|
|
2621
2695
|
if (process.stderr.isTTY) {
|
|
2622
2696
|
process.stderr.write("\x1B[?25l");
|
|
2623
2697
|
}
|
|
2698
|
+
this.render();
|
|
2699
|
+
this.frame++;
|
|
2624
2700
|
this.timer = setInterval(() => {
|
|
2625
|
-
this.render(
|
|
2701
|
+
this.render();
|
|
2626
2702
|
this.frame++;
|
|
2627
2703
|
}, 80);
|
|
2628
2704
|
}
|
|
2629
2705
|
setActivity(activity) {
|
|
2630
2706
|
this.activity = activity;
|
|
2631
2707
|
}
|
|
2708
|
+
setMessage(message) {
|
|
2709
|
+
this.message = message;
|
|
2710
|
+
if (this.timer)
|
|
2711
|
+
this.render();
|
|
2712
|
+
}
|
|
2632
2713
|
stop() {
|
|
2633
2714
|
if (this.timer) {
|
|
2634
2715
|
clearInterval(this.timer);
|
|
@@ -2641,7 +2722,8 @@ var init_status_indicator = __esm(() => {
|
|
|
2641
2722
|
isActive() {
|
|
2642
2723
|
return this.timer !== null;
|
|
2643
2724
|
}
|
|
2644
|
-
render(
|
|
2725
|
+
render() {
|
|
2726
|
+
const message = this.message;
|
|
2645
2727
|
const caps = getCapabilities();
|
|
2646
2728
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
2647
2729
|
const elapsedStr = `${elapsed}s`;
|
|
@@ -2663,7 +2745,7 @@ var init_status_indicator = __esm(() => {
|
|
|
2663
2745
|
}
|
|
2664
2746
|
if (!process.stderr.isTTY)
|
|
2665
2747
|
return;
|
|
2666
|
-
process.stderr.write(
|
|
2748
|
+
process.stderr.write(`\x1B[2K\r${line}`);
|
|
2667
2749
|
}
|
|
2668
2750
|
renderShimmer() {
|
|
2669
2751
|
const t = Date.now() / 1000;
|
|
@@ -3787,7 +3869,25 @@ var init_input_handler = __esm(() => {
|
|
|
3787
3869
|
});
|
|
3788
3870
|
|
|
3789
3871
|
// src/ai/claude.ts
|
|
3872
|
+
var exports_claude = {};
|
|
3873
|
+
__export(exports_claude, {
|
|
3874
|
+
buildClaudeArgs: () => buildClaudeArgs,
|
|
3875
|
+
ClaudeRunner: () => ClaudeRunner
|
|
3876
|
+
});
|
|
3790
3877
|
import { execSync as execSync4, spawn as spawn2 } from "node:child_process";
|
|
3878
|
+
function buildClaudeArgs(options) {
|
|
3879
|
+
const args = [
|
|
3880
|
+
"--dangerously-skip-permissions",
|
|
3881
|
+
"--no-session-persistence"
|
|
3882
|
+
];
|
|
3883
|
+
if (options.model) {
|
|
3884
|
+
args.push("--model", options.model);
|
|
3885
|
+
}
|
|
3886
|
+
if (options.verbose) {
|
|
3887
|
+
args.push("--verbose", "--output-format", "stream-json");
|
|
3888
|
+
}
|
|
3889
|
+
return args;
|
|
3890
|
+
}
|
|
3791
3891
|
|
|
3792
3892
|
class ClaudeRunner {
|
|
3793
3893
|
name = "claude";
|
|
@@ -3818,17 +3918,7 @@ class ClaudeRunner {
|
|
|
3818
3918
|
async execute(options) {
|
|
3819
3919
|
const log = getLogger();
|
|
3820
3920
|
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
|
-
}
|
|
3921
|
+
const args = ["--print", ...buildClaudeArgs(options)];
|
|
3832
3922
|
log.debug("Spawning claude", { args: args.join(" "), cwd: options.cwd });
|
|
3833
3923
|
return new Promise((resolve2) => {
|
|
3834
3924
|
let output = "";
|
|
@@ -3979,224 +4069,1207 @@ var init_claude = __esm(() => {
|
|
|
3979
4069
|
init_logger();
|
|
3980
4070
|
});
|
|
3981
4071
|
|
|
3982
|
-
// src/
|
|
3983
|
-
import {
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
4072
|
+
// src/core/sandbox-ignore.ts
|
|
4073
|
+
import { exec } from "node:child_process";
|
|
4074
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
|
|
4075
|
+
import { join as join10 } from "node:path";
|
|
4076
|
+
import { promisify } from "node:util";
|
|
4077
|
+
function parseIgnoreFile(filePath) {
|
|
4078
|
+
if (!existsSync11(filePath))
|
|
4079
|
+
return [];
|
|
4080
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
4081
|
+
const rules = [];
|
|
4082
|
+
for (const rawLine of content.split(`
|
|
4083
|
+
`)) {
|
|
4084
|
+
const line = rawLine.trim();
|
|
4085
|
+
if (!line || line.startsWith("#"))
|
|
4086
|
+
continue;
|
|
4087
|
+
const negated = line.startsWith("!");
|
|
4088
|
+
const raw = negated ? line.slice(1) : line;
|
|
4089
|
+
const isDirectory = raw.endsWith("/");
|
|
4090
|
+
const pattern = isDirectory ? raw.slice(0, -1) : raw;
|
|
4091
|
+
rules.push({ pattern, negated, isDirectory });
|
|
3988
4092
|
}
|
|
3989
|
-
|
|
3990
|
-
return args;
|
|
4093
|
+
return rules;
|
|
3991
4094
|
}
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
} catch {
|
|
4016
|
-
return "unknown";
|
|
4095
|
+
function shellEscape(s) {
|
|
4096
|
+
return s.replace(/'/g, "'\\''");
|
|
4097
|
+
}
|
|
4098
|
+
function buildCleanupScript(rules, workspacePath) {
|
|
4099
|
+
const positive = rules.filter((r) => !r.negated);
|
|
4100
|
+
const negated = rules.filter((r) => r.negated);
|
|
4101
|
+
if (positive.length === 0)
|
|
4102
|
+
return null;
|
|
4103
|
+
const exclusions = negated.map((r) => `! -name '${shellEscape(r.pattern)}'`).join(" ");
|
|
4104
|
+
const commands = [];
|
|
4105
|
+
for (const rule of positive) {
|
|
4106
|
+
const parts = ["find", `'${shellEscape(workspacePath)}'`];
|
|
4107
|
+
if (rule.isDirectory) {
|
|
4108
|
+
parts.push("-type d");
|
|
4109
|
+
}
|
|
4110
|
+
parts.push(`-name '${shellEscape(rule.pattern)}'`);
|
|
4111
|
+
if (exclusions) {
|
|
4112
|
+
parts.push(exclusions);
|
|
4113
|
+
}
|
|
4114
|
+
if (rule.isDirectory) {
|
|
4115
|
+
parts.push("-exec rm -rf {} +");
|
|
4116
|
+
} else {
|
|
4117
|
+
parts.push("-delete");
|
|
4017
4118
|
}
|
|
4119
|
+
commands.push(parts.join(" "));
|
|
4018
4120
|
}
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
let lineBuffer = "";
|
|
4042
|
-
this.process.stdout?.on("data", (chunk) => {
|
|
4043
|
-
lineBuffer += chunk.toString();
|
|
4044
|
-
const lines = lineBuffer.split(`
|
|
4045
|
-
`);
|
|
4046
|
-
lineBuffer = lines.pop() ?? "";
|
|
4047
|
-
for (const line of lines) {
|
|
4048
|
-
if (!line.trim())
|
|
4049
|
-
continue;
|
|
4050
|
-
rawOutput += `${line}
|
|
4051
|
-
`;
|
|
4052
|
-
log.debug("codex stdout line", { line });
|
|
4053
|
-
try {
|
|
4054
|
-
const event = JSON.parse(line);
|
|
4055
|
-
const { type, item } = event;
|
|
4056
|
-
if (type === "item.started" && item?.type === "command_execution") {
|
|
4057
|
-
const cmd = (item.command ?? "").split(`
|
|
4058
|
-
`)[0].slice(0, 80);
|
|
4059
|
-
options.onToolActivity?.(`running: ${cmd}`);
|
|
4060
|
-
} else if (type === "item.completed" && item?.type === "command_execution") {
|
|
4061
|
-
const code = item.exit_code;
|
|
4062
|
-
options.onToolActivity?.(code === 0 ? "done" : `exit ${code}`);
|
|
4063
|
-
} else if (type === "item.completed" && item?.type === "reasoning") {
|
|
4064
|
-
const text = (item.text ?? "").trim().replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
|
|
4065
|
-
if (text)
|
|
4066
|
-
options.onToolActivity?.(text);
|
|
4067
|
-
} else if (type === "item.completed" && item?.type === "agent_message") {
|
|
4068
|
-
const text = item.text ?? "";
|
|
4069
|
-
if (text) {
|
|
4070
|
-
agentMessages.push(text);
|
|
4071
|
-
options.onToolActivity?.(text.split(`
|
|
4072
|
-
`)[0].slice(0, 80));
|
|
4073
|
-
}
|
|
4074
|
-
} else if (type === "turn.completed") {
|
|
4075
|
-
flushAgentMessages();
|
|
4076
|
-
}
|
|
4077
|
-
} catch {
|
|
4078
|
-
const newLine = `${line}
|
|
4079
|
-
`;
|
|
4080
|
-
rawOutput += newLine;
|
|
4081
|
-
options.onOutput?.(newLine);
|
|
4082
|
-
}
|
|
4083
|
-
}
|
|
4084
|
-
});
|
|
4085
|
-
this.process.stderr?.on("data", (chunk) => {
|
|
4086
|
-
const text = chunk.toString();
|
|
4087
|
-
errorOutput += text;
|
|
4088
|
-
log.debug("codex stderr", { text: text.slice(0, 500) });
|
|
4089
|
-
});
|
|
4090
|
-
this.process.on("close", (code) => {
|
|
4091
|
-
this.process = null;
|
|
4092
|
-
flushAgentMessages();
|
|
4093
|
-
if (this.aborted) {
|
|
4094
|
-
resolve2({
|
|
4095
|
-
success: false,
|
|
4096
|
-
output: rawOutput,
|
|
4097
|
-
error: "Aborted by user",
|
|
4098
|
-
exitCode: code ?? 143
|
|
4099
|
-
});
|
|
4100
|
-
return;
|
|
4101
|
-
}
|
|
4102
|
-
if (code === 0) {
|
|
4103
|
-
resolve2({
|
|
4104
|
-
success: true,
|
|
4105
|
-
output: rawOutput,
|
|
4106
|
-
exitCode: 0
|
|
4107
|
-
});
|
|
4108
|
-
} else {
|
|
4109
|
-
resolve2({
|
|
4110
|
-
success: false,
|
|
4111
|
-
output: rawOutput,
|
|
4112
|
-
error: errorOutput || `codex exited with code ${code}`,
|
|
4113
|
-
exitCode: code ?? 1
|
|
4114
|
-
});
|
|
4115
|
-
}
|
|
4116
|
-
});
|
|
4117
|
-
this.process.on("error", (err) => {
|
|
4118
|
-
this.process = null;
|
|
4119
|
-
resolve2({
|
|
4120
|
-
success: false,
|
|
4121
|
-
output: rawOutput,
|
|
4122
|
-
error: `Failed to spawn codex: ${err.message}`,
|
|
4123
|
-
exitCode: 1
|
|
4124
|
-
});
|
|
4125
|
-
});
|
|
4126
|
-
if (options.signal) {
|
|
4127
|
-
options.signal.addEventListener("abort", () => {
|
|
4128
|
-
this.abort();
|
|
4129
|
-
});
|
|
4130
|
-
}
|
|
4131
|
-
this.process.stdin?.write(options.prompt);
|
|
4132
|
-
this.process.stdin?.end();
|
|
4121
|
+
return `${commands.join(" 2>/dev/null ; ")} 2>/dev/null`;
|
|
4122
|
+
}
|
|
4123
|
+
async function enforceSandboxIgnore(sandboxName, projectRoot) {
|
|
4124
|
+
const log = getLogger();
|
|
4125
|
+
const ignorePath = join10(projectRoot, ".sandboxignore");
|
|
4126
|
+
const rules = parseIgnoreFile(ignorePath);
|
|
4127
|
+
if (rules.length === 0)
|
|
4128
|
+
return;
|
|
4129
|
+
const script = buildCleanupScript(rules, projectRoot);
|
|
4130
|
+
if (!script)
|
|
4131
|
+
return;
|
|
4132
|
+
log.debug("Enforcing .sandboxignore", {
|
|
4133
|
+
sandboxName,
|
|
4134
|
+
ruleCount: rules.length
|
|
4135
|
+
});
|
|
4136
|
+
try {
|
|
4137
|
+
await execAsync(`docker sandbox exec ${sandboxName} sh -c ${JSON.stringify(script)}`, { timeout: 15000 });
|
|
4138
|
+
log.debug("sandbox-ignore enforcement complete", { sandboxName });
|
|
4139
|
+
} catch (err) {
|
|
4140
|
+
log.debug("sandbox-ignore enforcement failed (non-fatal)", {
|
|
4141
|
+
sandboxName,
|
|
4142
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4133
4143
|
});
|
|
4134
4144
|
}
|
|
4135
|
-
abort() {
|
|
4136
|
-
if (!this.process)
|
|
4137
|
-
return;
|
|
4138
|
-
this.aborted = true;
|
|
4139
|
-
const log = getLogger();
|
|
4140
|
-
log.debug("Aborting codex process");
|
|
4141
|
-
this.process.kill("SIGTERM");
|
|
4142
|
-
const forceKillTimer = setTimeout(() => {
|
|
4143
|
-
if (this.process) {
|
|
4144
|
-
log.debug("Force killing codex process");
|
|
4145
|
-
this.process.kill("SIGKILL");
|
|
4146
|
-
}
|
|
4147
|
-
}, 3000);
|
|
4148
|
-
if (forceKillTimer.unref) {
|
|
4149
|
-
forceKillTimer.unref();
|
|
4150
|
-
}
|
|
4151
|
-
}
|
|
4152
4145
|
}
|
|
4153
|
-
var
|
|
4146
|
+
var execAsync;
|
|
4147
|
+
var init_sandbox_ignore = __esm(() => {
|
|
4154
4148
|
init_logger();
|
|
4149
|
+
execAsync = promisify(exec);
|
|
4155
4150
|
});
|
|
4156
4151
|
|
|
4157
|
-
// src/
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4152
|
+
// src/core/run-state.ts
|
|
4153
|
+
import {
|
|
4154
|
+
existsSync as existsSync12,
|
|
4155
|
+
mkdirSync as mkdirSync8,
|
|
4156
|
+
readFileSync as readFileSync9,
|
|
4157
|
+
unlinkSync as unlinkSync3,
|
|
4158
|
+
writeFileSync as writeFileSync6
|
|
4159
|
+
} from "node:fs";
|
|
4160
|
+
import { dirname as dirname3, join as join11 } from "node:path";
|
|
4161
|
+
function getRunStatePath(projectRoot) {
|
|
4162
|
+
return join11(projectRoot, ".locus", "run-state.json");
|
|
4163
|
+
}
|
|
4164
|
+
function loadRunState(projectRoot) {
|
|
4165
|
+
const path = getRunStatePath(projectRoot);
|
|
4166
|
+
if (!existsSync12(path))
|
|
4167
|
+
return null;
|
|
4168
|
+
try {
|
|
4169
|
+
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
4170
|
+
} catch {
|
|
4171
|
+
getLogger().warn("Corrupted run-state.json, ignoring");
|
|
4172
|
+
return null;
|
|
4166
4173
|
}
|
|
4167
4174
|
}
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4175
|
+
function saveRunState(projectRoot, state) {
|
|
4176
|
+
const path = getRunStatePath(projectRoot);
|
|
4177
|
+
const dir = dirname3(path);
|
|
4178
|
+
if (!existsSync12(dir)) {
|
|
4179
|
+
mkdirSync8(dir, { recursive: true });
|
|
4180
|
+
}
|
|
4181
|
+
writeFileSync6(path, `${JSON.stringify(state, null, 2)}
|
|
4182
|
+
`, "utf-8");
|
|
4183
|
+
}
|
|
4184
|
+
function clearRunState(projectRoot) {
|
|
4185
|
+
const path = getRunStatePath(projectRoot);
|
|
4186
|
+
if (existsSync12(path)) {
|
|
4187
|
+
unlinkSync3(path);
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
function createSprintRunState(sprint, branch, issues) {
|
|
4191
|
+
return {
|
|
4192
|
+
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
4193
|
+
type: "sprint",
|
|
4194
|
+
sprint,
|
|
4195
|
+
branch,
|
|
4196
|
+
startedAt: new Date().toISOString(),
|
|
4197
|
+
tasks: issues.map(({ number, order }) => ({
|
|
4198
|
+
issue: number,
|
|
4199
|
+
order,
|
|
4200
|
+
status: "pending"
|
|
4201
|
+
}))
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
4204
|
+
function createParallelRunState(issueNumbers) {
|
|
4205
|
+
return {
|
|
4206
|
+
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
4207
|
+
type: "parallel",
|
|
4208
|
+
startedAt: new Date().toISOString(),
|
|
4209
|
+
tasks: issueNumbers.map((issue, i) => ({
|
|
4210
|
+
issue,
|
|
4211
|
+
order: i + 1,
|
|
4212
|
+
status: "pending"
|
|
4213
|
+
}))
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
function markTaskInProgress(state, issueNumber) {
|
|
4217
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
4218
|
+
if (task) {
|
|
4219
|
+
task.status = "in_progress";
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
function markTaskDone(state, issueNumber, prNumber) {
|
|
4223
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
4224
|
+
if (task) {
|
|
4225
|
+
task.status = "done";
|
|
4226
|
+
task.completedAt = new Date().toISOString();
|
|
4227
|
+
if (prNumber)
|
|
4228
|
+
task.pr = prNumber;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
function markTaskFailed(state, issueNumber, error) {
|
|
4232
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
4233
|
+
if (task) {
|
|
4234
|
+
task.status = "failed";
|
|
4235
|
+
task.failedAt = new Date().toISOString();
|
|
4236
|
+
task.error = error;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
function getRunStats(state) {
|
|
4240
|
+
const tasks = state.tasks;
|
|
4241
|
+
return {
|
|
4242
|
+
total: tasks.length,
|
|
4243
|
+
done: tasks.filter((t) => t.status === "done").length,
|
|
4244
|
+
failed: tasks.filter((t) => t.status === "failed").length,
|
|
4245
|
+
pending: tasks.filter((t) => t.status === "pending").length,
|
|
4246
|
+
inProgress: tasks.filter((t) => t.status === "in_progress").length
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
function getNextTask(state) {
|
|
4250
|
+
const failed = state.tasks.find((t) => t.status === "failed");
|
|
4251
|
+
if (failed)
|
|
4252
|
+
return failed;
|
|
4253
|
+
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
4254
|
+
}
|
|
4255
|
+
var init_run_state = __esm(() => {
|
|
4256
|
+
init_logger();
|
|
4257
|
+
});
|
|
4258
|
+
|
|
4259
|
+
// src/core/shutdown.ts
|
|
4260
|
+
import { execSync as execSync5 } from "node:child_process";
|
|
4261
|
+
function registerActiveSandbox(name) {
|
|
4262
|
+
activeSandboxes.add(name);
|
|
4263
|
+
}
|
|
4264
|
+
function unregisterActiveSandbox(name) {
|
|
4265
|
+
activeSandboxes.delete(name);
|
|
4266
|
+
}
|
|
4267
|
+
function cleanupActiveSandboxes() {
|
|
4268
|
+
for (const name of activeSandboxes) {
|
|
4269
|
+
try {
|
|
4270
|
+
execSync5(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
4271
|
+
} catch {}
|
|
4272
|
+
}
|
|
4273
|
+
activeSandboxes.clear();
|
|
4274
|
+
}
|
|
4275
|
+
function registerShutdownHandlers(ctx) {
|
|
4276
|
+
shutdownContext = ctx;
|
|
4277
|
+
interruptCount = 0;
|
|
4278
|
+
const handler = () => {
|
|
4279
|
+
interruptCount++;
|
|
4280
|
+
if (interruptCount >= 2) {
|
|
4281
|
+
process.stderr.write(`
|
|
4282
|
+
Force exit.
|
|
4283
|
+
`);
|
|
4284
|
+
process.exit(1);
|
|
4285
|
+
}
|
|
4286
|
+
process.stderr.write(`
|
|
4287
|
+
|
|
4288
|
+
Interrupted. Saving state...
|
|
4289
|
+
`);
|
|
4290
|
+
const state = shutdownContext?.getRunState?.();
|
|
4291
|
+
if (state && shutdownContext) {
|
|
4292
|
+
for (const task of state.tasks) {
|
|
4293
|
+
if (task.status === "in_progress") {
|
|
4294
|
+
task.status = "failed";
|
|
4295
|
+
task.failedAt = new Date().toISOString();
|
|
4296
|
+
task.error = "Interrupted by user";
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
try {
|
|
4300
|
+
saveRunState(shutdownContext.projectRoot, state);
|
|
4301
|
+
process.stderr.write(`State saved. Resume with: locus run --resume
|
|
4302
|
+
`);
|
|
4303
|
+
} catch {
|
|
4304
|
+
process.stderr.write(`Warning: Could not save run state.
|
|
4305
|
+
`);
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
cleanupActiveSandboxes();
|
|
4309
|
+
shutdownContext?.onShutdown?.();
|
|
4310
|
+
if (interruptTimer)
|
|
4311
|
+
clearTimeout(interruptTimer);
|
|
4312
|
+
interruptTimer = setTimeout(() => {
|
|
4313
|
+
interruptCount = 0;
|
|
4314
|
+
}, 2000);
|
|
4315
|
+
setTimeout(() => {
|
|
4316
|
+
process.exit(130);
|
|
4317
|
+
}, 100);
|
|
4318
|
+
};
|
|
4319
|
+
if (!shutdownRegistered) {
|
|
4320
|
+
process.on("SIGINT", handler);
|
|
4321
|
+
process.on("SIGTERM", handler);
|
|
4322
|
+
shutdownRegistered = true;
|
|
4323
|
+
}
|
|
4324
|
+
return () => {
|
|
4325
|
+
process.removeListener("SIGINT", handler);
|
|
4326
|
+
process.removeListener("SIGTERM", handler);
|
|
4327
|
+
shutdownRegistered = false;
|
|
4328
|
+
shutdownContext = null;
|
|
4329
|
+
interruptCount = 0;
|
|
4330
|
+
if (interruptTimer) {
|
|
4331
|
+
clearTimeout(interruptTimer);
|
|
4332
|
+
interruptTimer = null;
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
}
|
|
4336
|
+
var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
|
|
4337
|
+
var init_shutdown = __esm(() => {
|
|
4338
|
+
init_run_state();
|
|
4339
|
+
activeSandboxes = new Set;
|
|
4340
|
+
});
|
|
4341
|
+
|
|
4342
|
+
// src/ai/claude-sandbox.ts
|
|
4343
|
+
import { execSync as execSync6, spawn as spawn3 } from "node:child_process";
|
|
4344
|
+
|
|
4345
|
+
class SandboxedClaudeRunner {
|
|
4346
|
+
name = "claude-sandboxed";
|
|
4347
|
+
process = null;
|
|
4348
|
+
aborted = false;
|
|
4349
|
+
sandboxName = null;
|
|
4350
|
+
persistent;
|
|
4351
|
+
sandboxCreated = false;
|
|
4352
|
+
userManaged = false;
|
|
4353
|
+
constructor(persistentName, userManaged = false) {
|
|
4354
|
+
if (persistentName) {
|
|
4355
|
+
this.persistent = true;
|
|
4356
|
+
this.sandboxName = persistentName;
|
|
4357
|
+
this.userManaged = userManaged;
|
|
4358
|
+
if (userManaged) {
|
|
4359
|
+
this.sandboxCreated = true;
|
|
4360
|
+
}
|
|
4361
|
+
} else {
|
|
4362
|
+
this.persistent = false;
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
async isAvailable() {
|
|
4366
|
+
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4367
|
+
const delegate = new ClaudeRunner2;
|
|
4368
|
+
return delegate.isAvailable();
|
|
4369
|
+
}
|
|
4370
|
+
async getVersion() {
|
|
4371
|
+
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4372
|
+
const delegate = new ClaudeRunner2;
|
|
4373
|
+
return delegate.getVersion();
|
|
4374
|
+
}
|
|
4375
|
+
async execute(options) {
|
|
4376
|
+
const log = getLogger();
|
|
4377
|
+
this.aborted = false;
|
|
4378
|
+
const claudeArgs = ["-p", options.prompt, ...buildClaudeArgs(options)];
|
|
4379
|
+
let dockerArgs;
|
|
4380
|
+
if (this.persistent && !this.sandboxName) {
|
|
4381
|
+
throw new Error("Sandbox name is required");
|
|
4382
|
+
}
|
|
4383
|
+
if (this.persistent && this.sandboxCreated && await this.isSandboxRunning()) {
|
|
4384
|
+
const name = this.sandboxName;
|
|
4385
|
+
if (!name) {
|
|
4386
|
+
throw new Error("Sandbox name is required");
|
|
4387
|
+
}
|
|
4388
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
4389
|
+
await enforceSandboxIgnore(name, options.cwd);
|
|
4390
|
+
options.onStatusChange?.("Thinking...");
|
|
4391
|
+
dockerArgs = [
|
|
4392
|
+
"sandbox",
|
|
4393
|
+
"exec",
|
|
4394
|
+
"-w",
|
|
4395
|
+
options.cwd,
|
|
4396
|
+
name,
|
|
4397
|
+
"claude",
|
|
4398
|
+
...claudeArgs
|
|
4399
|
+
];
|
|
4400
|
+
} else {
|
|
4401
|
+
if (!this.persistent) {
|
|
4402
|
+
this.sandboxName = buildSandboxName(options);
|
|
4403
|
+
}
|
|
4404
|
+
const name = this.sandboxName;
|
|
4405
|
+
if (!name) {
|
|
4406
|
+
throw new Error("Sandbox name is required");
|
|
4407
|
+
}
|
|
4408
|
+
registerActiveSandbox(name);
|
|
4409
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
4410
|
+
dockerArgs = [
|
|
4411
|
+
"sandbox",
|
|
4412
|
+
"run",
|
|
4413
|
+
"--name",
|
|
4414
|
+
name,
|
|
4415
|
+
"claude",
|
|
4416
|
+
options.cwd,
|
|
4417
|
+
"--",
|
|
4418
|
+
...claudeArgs
|
|
4419
|
+
];
|
|
4420
|
+
}
|
|
4421
|
+
log.debug("Spawning sandboxed claude", {
|
|
4422
|
+
sandboxName: this.sandboxName,
|
|
4423
|
+
persistent: this.persistent,
|
|
4424
|
+
reusing: this.persistent && this.sandboxCreated,
|
|
4425
|
+
args: dockerArgs.join(" "),
|
|
4426
|
+
cwd: options.cwd
|
|
4427
|
+
});
|
|
4428
|
+
try {
|
|
4429
|
+
return await new Promise((resolve2) => {
|
|
4430
|
+
let output = "";
|
|
4431
|
+
let errorOutput = "";
|
|
4432
|
+
this.process = spawn3("docker", dockerArgs, {
|
|
4433
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4434
|
+
env: process.env
|
|
4435
|
+
});
|
|
4436
|
+
if (this.persistent && !this.sandboxCreated) {
|
|
4437
|
+
this.process.on("spawn", () => {
|
|
4438
|
+
this.sandboxCreated = true;
|
|
4439
|
+
});
|
|
4440
|
+
}
|
|
4441
|
+
if (options.verbose) {
|
|
4442
|
+
let lineBuffer = "";
|
|
4443
|
+
const seenToolIds = new Set;
|
|
4444
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4445
|
+
lineBuffer += chunk.toString();
|
|
4446
|
+
const lines = lineBuffer.split(`
|
|
4447
|
+
`);
|
|
4448
|
+
lineBuffer = lines.pop() ?? "";
|
|
4449
|
+
for (const line of lines) {
|
|
4450
|
+
if (!line.trim())
|
|
4451
|
+
continue;
|
|
4452
|
+
try {
|
|
4453
|
+
const event = JSON.parse(line);
|
|
4454
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
4455
|
+
for (const item of event.message.content) {
|
|
4456
|
+
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
4457
|
+
seenToolIds.add(item.id);
|
|
4458
|
+
options.onToolActivity?.(formatToolCall2(item.name ?? "", item.input ?? {}));
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
} else if (event.type === "result") {
|
|
4462
|
+
const text = event.result ?? "";
|
|
4463
|
+
output = text;
|
|
4464
|
+
options.onOutput?.(text);
|
|
4465
|
+
}
|
|
4466
|
+
} catch {
|
|
4467
|
+
const newLine = `${line}
|
|
4468
|
+
`;
|
|
4469
|
+
output += newLine;
|
|
4470
|
+
options.onOutput?.(newLine);
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
});
|
|
4474
|
+
} else {
|
|
4475
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4476
|
+
const text = chunk.toString();
|
|
4477
|
+
output += text;
|
|
4478
|
+
options.onOutput?.(text);
|
|
4479
|
+
});
|
|
4480
|
+
}
|
|
4481
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
4482
|
+
const text = chunk.toString();
|
|
4483
|
+
errorOutput += text;
|
|
4484
|
+
log.debug("sandboxed claude stderr", { text: text.slice(0, 500) });
|
|
4485
|
+
options.onOutput?.(text);
|
|
4486
|
+
});
|
|
4487
|
+
this.process.on("close", (code) => {
|
|
4488
|
+
this.process = null;
|
|
4489
|
+
if (this.aborted) {
|
|
4490
|
+
resolve2({
|
|
4491
|
+
success: false,
|
|
4492
|
+
output,
|
|
4493
|
+
error: "Aborted by user",
|
|
4494
|
+
exitCode: code ?? 143
|
|
4495
|
+
});
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
if (code === 0) {
|
|
4499
|
+
resolve2({
|
|
4500
|
+
success: true,
|
|
4501
|
+
output,
|
|
4502
|
+
exitCode: 0
|
|
4503
|
+
});
|
|
4504
|
+
} else {
|
|
4505
|
+
resolve2({
|
|
4506
|
+
success: false,
|
|
4507
|
+
output,
|
|
4508
|
+
error: errorOutput || `sandboxed claude exited with code ${code}`,
|
|
4509
|
+
exitCode: code ?? 1
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
});
|
|
4513
|
+
this.process.on("error", (err) => {
|
|
4514
|
+
this.process = null;
|
|
4515
|
+
if (this.persistent && !this.sandboxCreated) {}
|
|
4516
|
+
resolve2({
|
|
4517
|
+
success: false,
|
|
4518
|
+
output,
|
|
4519
|
+
error: `Failed to spawn docker sandbox: ${err.message}`,
|
|
4520
|
+
exitCode: 1
|
|
4521
|
+
});
|
|
4522
|
+
});
|
|
4523
|
+
if (options.signal) {
|
|
4524
|
+
options.signal.addEventListener("abort", () => {
|
|
4525
|
+
this.abort();
|
|
4526
|
+
});
|
|
4527
|
+
}
|
|
4528
|
+
});
|
|
4529
|
+
} finally {
|
|
4530
|
+
if (!this.persistent) {
|
|
4531
|
+
this.cleanupSandbox();
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
abort() {
|
|
4536
|
+
this.aborted = true;
|
|
4537
|
+
const log = getLogger();
|
|
4538
|
+
if (this.persistent) {
|
|
4539
|
+
log.debug("Aborting sandboxed claude (persistent — keeping sandbox)", {
|
|
4540
|
+
sandboxName: this.sandboxName
|
|
4541
|
+
});
|
|
4542
|
+
if (this.process) {
|
|
4543
|
+
this.process.kill("SIGTERM");
|
|
4544
|
+
const timer = setTimeout(() => {
|
|
4545
|
+
if (this.process) {
|
|
4546
|
+
this.process.kill("SIGKILL");
|
|
4547
|
+
}
|
|
4548
|
+
}, 3000);
|
|
4549
|
+
if (timer.unref)
|
|
4550
|
+
timer.unref();
|
|
4551
|
+
}
|
|
4552
|
+
} else {
|
|
4553
|
+
if (!this.sandboxName)
|
|
4554
|
+
return;
|
|
4555
|
+
log.debug("Aborting sandboxed claude (ephemeral — removing sandbox)", {
|
|
4556
|
+
sandboxName: this.sandboxName
|
|
4557
|
+
});
|
|
4558
|
+
try {
|
|
4559
|
+
execSync6(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4560
|
+
} catch {}
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
destroy() {
|
|
4564
|
+
if (!this.sandboxName)
|
|
4565
|
+
return;
|
|
4566
|
+
if (this.userManaged) {
|
|
4567
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
4568
|
+
return;
|
|
4569
|
+
}
|
|
4570
|
+
const log = getLogger();
|
|
4571
|
+
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
4572
|
+
try {
|
|
4573
|
+
execSync6(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4574
|
+
} catch {}
|
|
4575
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
4576
|
+
this.sandboxName = null;
|
|
4577
|
+
this.sandboxCreated = false;
|
|
4578
|
+
}
|
|
4579
|
+
cleanupSandbox() {
|
|
4580
|
+
if (!this.sandboxName)
|
|
4581
|
+
return;
|
|
4582
|
+
const log = getLogger();
|
|
4583
|
+
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
4584
|
+
try {
|
|
4585
|
+
execSync6(`docker sandbox rm ${this.sandboxName}`, {
|
|
4586
|
+
timeout: 60000
|
|
4587
|
+
});
|
|
4588
|
+
} catch {}
|
|
4589
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
4590
|
+
this.sandboxName = null;
|
|
4591
|
+
}
|
|
4592
|
+
async isSandboxRunning() {
|
|
4593
|
+
if (!this.sandboxName)
|
|
4594
|
+
return false;
|
|
4595
|
+
try {
|
|
4596
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
4597
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
4598
|
+
const execAsync2 = promisify2(exec2);
|
|
4599
|
+
const { stdout } = await execAsync2("docker sandbox ls", {
|
|
4600
|
+
timeout: 5000
|
|
4601
|
+
});
|
|
4602
|
+
return stdout.includes(this.sandboxName);
|
|
4603
|
+
} catch {
|
|
4604
|
+
return false;
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
getSandboxName() {
|
|
4608
|
+
return this.sandboxName;
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
function buildSandboxName(options) {
|
|
4612
|
+
const ts = Date.now();
|
|
4613
|
+
if (options.activity) {
|
|
4614
|
+
const match = options.activity.match(/issue\s*#(\d+)/i);
|
|
4615
|
+
if (match) {
|
|
4616
|
+
return `locus-issue-${match[1]}-${ts}`;
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
const segment = options.cwd.split("/").pop() ?? "run";
|
|
4620
|
+
return `locus-${segment}-${ts}`;
|
|
4621
|
+
}
|
|
4622
|
+
function buildPersistentSandboxName(cwd) {
|
|
4623
|
+
const segment = cwd.split("/").pop() ?? "repl";
|
|
4624
|
+
return `locus-${segment}-${Date.now()}`;
|
|
4625
|
+
}
|
|
4626
|
+
function formatToolCall2(name, input) {
|
|
4627
|
+
switch (name) {
|
|
4628
|
+
case "Read":
|
|
4629
|
+
return `reading ${input.file_path ?? ""}`;
|
|
4630
|
+
case "Write":
|
|
4631
|
+
return `writing ${input.file_path ?? ""}`;
|
|
4632
|
+
case "Edit":
|
|
4633
|
+
case "MultiEdit":
|
|
4634
|
+
return `editing ${input.file_path ?? ""}`;
|
|
4635
|
+
case "Bash":
|
|
4636
|
+
return `running: ${String(input.command ?? "").slice(0, 60)}`;
|
|
4637
|
+
case "Glob":
|
|
4638
|
+
return `glob ${input.pattern ?? ""}`;
|
|
4639
|
+
case "Grep":
|
|
4640
|
+
return `grep ${input.pattern ?? ""}`;
|
|
4641
|
+
case "LS":
|
|
4642
|
+
return `ls ${input.path ?? ""}`;
|
|
4643
|
+
case "WebFetch":
|
|
4644
|
+
return `fetching ${String(input.url ?? "").slice(0, 50)}`;
|
|
4645
|
+
case "WebSearch":
|
|
4646
|
+
return `searching: ${input.query ?? ""}`;
|
|
4647
|
+
case "Task":
|
|
4648
|
+
return `spawning agent`;
|
|
4649
|
+
default:
|
|
4650
|
+
return name;
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
var init_claude_sandbox = __esm(() => {
|
|
4654
|
+
init_logger();
|
|
4655
|
+
init_sandbox_ignore();
|
|
4656
|
+
init_shutdown();
|
|
4657
|
+
init_claude();
|
|
4658
|
+
});
|
|
4659
|
+
|
|
4660
|
+
// src/ai/codex.ts
|
|
4661
|
+
import { execSync as execSync7, spawn as spawn4 } from "node:child_process";
|
|
4662
|
+
function buildCodexArgs(model) {
|
|
4663
|
+
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
|
|
4664
|
+
if (model) {
|
|
4665
|
+
args.push("--model", model);
|
|
4666
|
+
}
|
|
4667
|
+
args.push("-");
|
|
4668
|
+
return args;
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
class CodexRunner {
|
|
4672
|
+
name = "codex";
|
|
4673
|
+
process = null;
|
|
4674
|
+
aborted = false;
|
|
4675
|
+
async isAvailable() {
|
|
4676
|
+
try {
|
|
4677
|
+
execSync7("codex --version", {
|
|
4678
|
+
encoding: "utf-8",
|
|
4679
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4680
|
+
});
|
|
4681
|
+
return true;
|
|
4682
|
+
} catch {
|
|
4683
|
+
return false;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
async getVersion() {
|
|
4687
|
+
try {
|
|
4688
|
+
const output = execSync7("codex --version", {
|
|
4689
|
+
encoding: "utf-8",
|
|
4690
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4691
|
+
}).trim();
|
|
4692
|
+
return output.replace(/^codex\s*/i, "");
|
|
4693
|
+
} catch {
|
|
4694
|
+
return "unknown";
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
async execute(options) {
|
|
4698
|
+
const log = getLogger();
|
|
4699
|
+
this.aborted = false;
|
|
4700
|
+
const args = buildCodexArgs(options.model);
|
|
4701
|
+
log.debug("Spawning codex", { args: args.join(" "), cwd: options.cwd });
|
|
4702
|
+
return new Promise((resolve2) => {
|
|
4703
|
+
let rawOutput = "";
|
|
4704
|
+
let errorOutput = "";
|
|
4705
|
+
this.process = spawn4("codex", args, {
|
|
4706
|
+
cwd: options.cwd,
|
|
4707
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4708
|
+
env: { ...process.env }
|
|
4709
|
+
});
|
|
4710
|
+
let agentMessages = [];
|
|
4711
|
+
const flushAgentMessages = () => {
|
|
4712
|
+
if (agentMessages.length > 0) {
|
|
4713
|
+
options.onOutput?.(agentMessages.join(`
|
|
4714
|
+
|
|
4715
|
+
`));
|
|
4716
|
+
agentMessages = [];
|
|
4717
|
+
}
|
|
4718
|
+
};
|
|
4719
|
+
let lineBuffer = "";
|
|
4720
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4721
|
+
lineBuffer += chunk.toString();
|
|
4722
|
+
const lines = lineBuffer.split(`
|
|
4723
|
+
`);
|
|
4724
|
+
lineBuffer = lines.pop() ?? "";
|
|
4725
|
+
for (const line of lines) {
|
|
4726
|
+
if (!line.trim())
|
|
4727
|
+
continue;
|
|
4728
|
+
rawOutput += `${line}
|
|
4729
|
+
`;
|
|
4730
|
+
log.debug("codex stdout line", { line });
|
|
4731
|
+
try {
|
|
4732
|
+
const event = JSON.parse(line);
|
|
4733
|
+
const { type, item } = event;
|
|
4734
|
+
if (type === "item.started" && item?.type === "command_execution") {
|
|
4735
|
+
const cmd = (item.command ?? "").split(`
|
|
4736
|
+
`)[0].slice(0, 80);
|
|
4737
|
+
options.onToolActivity?.(`running: ${cmd}`);
|
|
4738
|
+
} else if (type === "item.completed" && item?.type === "command_execution") {
|
|
4739
|
+
const code = item.exit_code;
|
|
4740
|
+
options.onToolActivity?.(code === 0 ? "done" : `exit ${code}`);
|
|
4741
|
+
} else if (type === "item.completed" && item?.type === "reasoning") {
|
|
4742
|
+
const text = (item.text ?? "").trim().replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
|
|
4743
|
+
if (text)
|
|
4744
|
+
options.onToolActivity?.(text);
|
|
4745
|
+
} else if (type === "item.completed" && item?.type === "agent_message") {
|
|
4746
|
+
const text = item.text ?? "";
|
|
4747
|
+
if (text) {
|
|
4748
|
+
agentMessages.push(text);
|
|
4749
|
+
options.onToolActivity?.(text.split(`
|
|
4750
|
+
`)[0].slice(0, 80));
|
|
4751
|
+
}
|
|
4752
|
+
} else if (type === "turn.completed") {
|
|
4753
|
+
flushAgentMessages();
|
|
4754
|
+
}
|
|
4755
|
+
} catch {
|
|
4756
|
+
const newLine = `${line}
|
|
4757
|
+
`;
|
|
4758
|
+
rawOutput += newLine;
|
|
4759
|
+
options.onOutput?.(newLine);
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
});
|
|
4763
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
4764
|
+
const text = chunk.toString();
|
|
4765
|
+
errorOutput += text;
|
|
4766
|
+
log.debug("codex stderr", { text: text.slice(0, 500) });
|
|
4767
|
+
});
|
|
4768
|
+
this.process.on("close", (code) => {
|
|
4769
|
+
this.process = null;
|
|
4770
|
+
flushAgentMessages();
|
|
4771
|
+
if (this.aborted) {
|
|
4772
|
+
resolve2({
|
|
4773
|
+
success: false,
|
|
4774
|
+
output: rawOutput,
|
|
4775
|
+
error: "Aborted by user",
|
|
4776
|
+
exitCode: code ?? 143
|
|
4777
|
+
});
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
if (code === 0) {
|
|
4781
|
+
resolve2({
|
|
4782
|
+
success: true,
|
|
4783
|
+
output: rawOutput,
|
|
4784
|
+
exitCode: 0
|
|
4785
|
+
});
|
|
4786
|
+
} else {
|
|
4787
|
+
resolve2({
|
|
4788
|
+
success: false,
|
|
4789
|
+
output: rawOutput,
|
|
4790
|
+
error: errorOutput || `codex exited with code ${code}`,
|
|
4791
|
+
exitCode: code ?? 1
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
});
|
|
4795
|
+
this.process.on("error", (err) => {
|
|
4796
|
+
this.process = null;
|
|
4797
|
+
resolve2({
|
|
4798
|
+
success: false,
|
|
4799
|
+
output: rawOutput,
|
|
4800
|
+
error: `Failed to spawn codex: ${err.message}`,
|
|
4801
|
+
exitCode: 1
|
|
4802
|
+
});
|
|
4803
|
+
});
|
|
4804
|
+
if (options.signal) {
|
|
4805
|
+
options.signal.addEventListener("abort", () => {
|
|
4806
|
+
this.abort();
|
|
4807
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
this.process.stdin?.write(options.prompt);
|
|
4810
|
+
this.process.stdin?.end();
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
abort() {
|
|
4814
|
+
if (!this.process)
|
|
4815
|
+
return;
|
|
4816
|
+
this.aborted = true;
|
|
4817
|
+
const log = getLogger();
|
|
4818
|
+
log.debug("Aborting codex process");
|
|
4819
|
+
this.process.kill("SIGTERM");
|
|
4820
|
+
const forceKillTimer = setTimeout(() => {
|
|
4821
|
+
if (this.process) {
|
|
4822
|
+
log.debug("Force killing codex process");
|
|
4823
|
+
this.process.kill("SIGKILL");
|
|
4824
|
+
}
|
|
4825
|
+
}, 3000);
|
|
4826
|
+
if (forceKillTimer.unref) {
|
|
4827
|
+
forceKillTimer.unref();
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
var init_codex = __esm(() => {
|
|
4832
|
+
init_logger();
|
|
4833
|
+
});
|
|
4834
|
+
|
|
4835
|
+
// src/ai/codex-sandbox.ts
|
|
4836
|
+
import { execSync as execSync8, spawn as spawn5 } from "node:child_process";
|
|
4837
|
+
|
|
4838
|
+
class SandboxedCodexRunner {
|
|
4839
|
+
name = "codex-sandboxed";
|
|
4840
|
+
process = null;
|
|
4841
|
+
aborted = false;
|
|
4842
|
+
sandboxName = null;
|
|
4843
|
+
persistent;
|
|
4844
|
+
sandboxCreated = false;
|
|
4845
|
+
userManaged = false;
|
|
4846
|
+
codexInstalled = false;
|
|
4847
|
+
constructor(persistentName, userManaged = false) {
|
|
4848
|
+
if (persistentName) {
|
|
4849
|
+
this.persistent = true;
|
|
4850
|
+
this.sandboxName = persistentName;
|
|
4851
|
+
this.userManaged = userManaged;
|
|
4852
|
+
if (userManaged) {
|
|
4853
|
+
this.sandboxCreated = true;
|
|
4854
|
+
}
|
|
4855
|
+
} else {
|
|
4856
|
+
this.persistent = false;
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
async isAvailable() {
|
|
4860
|
+
const delegate = new CodexRunner;
|
|
4861
|
+
return delegate.isAvailable();
|
|
4862
|
+
}
|
|
4863
|
+
async getVersion() {
|
|
4864
|
+
const delegate = new CodexRunner;
|
|
4865
|
+
return delegate.getVersion();
|
|
4866
|
+
}
|
|
4867
|
+
async execute(options) {
|
|
4868
|
+
const log = getLogger();
|
|
4869
|
+
this.aborted = false;
|
|
4870
|
+
const codexArgs = buildCodexArgs(options.model);
|
|
4871
|
+
let dockerArgs;
|
|
4872
|
+
if (this.persistent && !this.sandboxName) {
|
|
4873
|
+
throw new Error("Sandbox name is required");
|
|
4874
|
+
}
|
|
4875
|
+
if (this.persistent && this.sandboxCreated && await this.isSandboxRunning()) {
|
|
4876
|
+
const name = this.sandboxName;
|
|
4877
|
+
if (!name) {
|
|
4878
|
+
throw new Error("Sandbox name is required");
|
|
4879
|
+
}
|
|
4880
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
4881
|
+
await enforceSandboxIgnore(name, options.cwd);
|
|
4882
|
+
if (!this.codexInstalled) {
|
|
4883
|
+
options.onStatusChange?.("Checking codex...");
|
|
4884
|
+
await this.ensureCodexInstalled(name);
|
|
4885
|
+
this.codexInstalled = true;
|
|
4886
|
+
}
|
|
4887
|
+
options.onStatusChange?.("Thinking...");
|
|
4888
|
+
dockerArgs = [
|
|
4889
|
+
"sandbox",
|
|
4890
|
+
"exec",
|
|
4891
|
+
"-i",
|
|
4892
|
+
"-w",
|
|
4893
|
+
options.cwd,
|
|
4894
|
+
name,
|
|
4895
|
+
"codex",
|
|
4896
|
+
...codexArgs
|
|
4897
|
+
];
|
|
4898
|
+
} else {
|
|
4899
|
+
if (!this.persistent) {
|
|
4900
|
+
this.sandboxName = buildSandboxName2(options);
|
|
4901
|
+
}
|
|
4902
|
+
const name = this.sandboxName;
|
|
4903
|
+
if (!name) {
|
|
4904
|
+
throw new Error("Sandbox name is required");
|
|
4905
|
+
}
|
|
4906
|
+
registerActiveSandbox(name);
|
|
4907
|
+
options.onStatusChange?.("Creating sandbox...");
|
|
4908
|
+
await this.createSandboxWithClaude(name, options.cwd);
|
|
4909
|
+
options.onStatusChange?.("Installing codex...");
|
|
4910
|
+
await this.ensureCodexInstalled(name);
|
|
4911
|
+
this.codexInstalled = true;
|
|
4912
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
4913
|
+
await enforceSandboxIgnore(name, options.cwd);
|
|
4914
|
+
options.onStatusChange?.("Thinking...");
|
|
4915
|
+
dockerArgs = [
|
|
4916
|
+
"sandbox",
|
|
4917
|
+
"exec",
|
|
4918
|
+
"-i",
|
|
4919
|
+
"-w",
|
|
4920
|
+
options.cwd,
|
|
4921
|
+
name,
|
|
4922
|
+
"codex",
|
|
4923
|
+
...codexArgs
|
|
4924
|
+
];
|
|
4925
|
+
}
|
|
4926
|
+
log.debug("Spawning sandboxed codex", {
|
|
4927
|
+
sandboxName: this.sandboxName,
|
|
4928
|
+
persistent: this.persistent,
|
|
4929
|
+
reusing: this.persistent && this.sandboxCreated,
|
|
4930
|
+
args: dockerArgs.join(" "),
|
|
4931
|
+
cwd: options.cwd
|
|
4932
|
+
});
|
|
4933
|
+
try {
|
|
4934
|
+
return await new Promise((resolve2) => {
|
|
4935
|
+
let rawOutput = "";
|
|
4936
|
+
let errorOutput = "";
|
|
4937
|
+
this.process = spawn5("docker", dockerArgs, {
|
|
4938
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4939
|
+
env: process.env
|
|
4940
|
+
});
|
|
4941
|
+
if (this.persistent && !this.sandboxCreated) {
|
|
4942
|
+
this.process.on("spawn", () => {
|
|
4943
|
+
this.sandboxCreated = true;
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
let agentMessages = [];
|
|
4947
|
+
const flushAgentMessages = () => {
|
|
4948
|
+
if (agentMessages.length > 0) {
|
|
4949
|
+
options.onOutput?.(agentMessages.join(`
|
|
4950
|
+
|
|
4951
|
+
`));
|
|
4952
|
+
agentMessages = [];
|
|
4953
|
+
}
|
|
4954
|
+
};
|
|
4955
|
+
let lineBuffer = "";
|
|
4956
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4957
|
+
lineBuffer += chunk.toString();
|
|
4958
|
+
const lines = lineBuffer.split(`
|
|
4959
|
+
`);
|
|
4960
|
+
lineBuffer = lines.pop() ?? "";
|
|
4961
|
+
for (const line of lines) {
|
|
4962
|
+
if (!line.trim())
|
|
4963
|
+
continue;
|
|
4964
|
+
rawOutput += `${line}
|
|
4965
|
+
`;
|
|
4966
|
+
log.debug("sandboxed codex stdout line", { line });
|
|
4967
|
+
try {
|
|
4968
|
+
const event = JSON.parse(line);
|
|
4969
|
+
const { type, item } = event;
|
|
4970
|
+
if (type === "item.started" && item?.type === "command_execution") {
|
|
4971
|
+
const cmd = (item.command ?? "").split(`
|
|
4972
|
+
`)[0].slice(0, 80);
|
|
4973
|
+
options.onToolActivity?.(`running: ${cmd}`);
|
|
4974
|
+
} else if (type === "item.completed" && item?.type === "command_execution") {
|
|
4975
|
+
const code = item.exit_code;
|
|
4976
|
+
options.onToolActivity?.(code === 0 ? "done" : `exit ${code}`);
|
|
4977
|
+
} else if (type === "item.completed" && item?.type === "reasoning") {
|
|
4978
|
+
const text = (item.text ?? "").trim().replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
|
|
4979
|
+
if (text)
|
|
4980
|
+
options.onToolActivity?.(text);
|
|
4981
|
+
} else if (type === "item.completed" && item?.type === "agent_message") {
|
|
4982
|
+
const text = item.text ?? "";
|
|
4983
|
+
if (text) {
|
|
4984
|
+
agentMessages.push(text);
|
|
4985
|
+
options.onToolActivity?.(text.split(`
|
|
4986
|
+
`)[0].slice(0, 80));
|
|
4987
|
+
}
|
|
4988
|
+
} else if (type === "turn.completed") {
|
|
4989
|
+
flushAgentMessages();
|
|
4990
|
+
}
|
|
4991
|
+
} catch {
|
|
4992
|
+
const newLine = `${line}
|
|
4993
|
+
`;
|
|
4994
|
+
rawOutput += newLine;
|
|
4995
|
+
options.onOutput?.(newLine);
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
});
|
|
4999
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
5000
|
+
const text = chunk.toString();
|
|
5001
|
+
errorOutput += text;
|
|
5002
|
+
log.debug("sandboxed codex stderr", { text: text.slice(0, 500) });
|
|
5003
|
+
});
|
|
5004
|
+
this.process.on("close", (code) => {
|
|
5005
|
+
this.process = null;
|
|
5006
|
+
flushAgentMessages();
|
|
5007
|
+
if (this.aborted) {
|
|
5008
|
+
resolve2({
|
|
5009
|
+
success: false,
|
|
5010
|
+
output: rawOutput,
|
|
5011
|
+
error: "Aborted by user",
|
|
5012
|
+
exitCode: code ?? 143
|
|
5013
|
+
});
|
|
5014
|
+
return;
|
|
5015
|
+
}
|
|
5016
|
+
if (code === 0) {
|
|
5017
|
+
resolve2({
|
|
5018
|
+
success: true,
|
|
5019
|
+
output: rawOutput,
|
|
5020
|
+
exitCode: 0
|
|
5021
|
+
});
|
|
5022
|
+
} else {
|
|
5023
|
+
resolve2({
|
|
5024
|
+
success: false,
|
|
5025
|
+
output: rawOutput,
|
|
5026
|
+
error: errorOutput || `sandboxed codex exited with code ${code}`,
|
|
5027
|
+
exitCode: code ?? 1
|
|
5028
|
+
});
|
|
5029
|
+
}
|
|
5030
|
+
});
|
|
5031
|
+
this.process.on("error", (err) => {
|
|
5032
|
+
this.process = null;
|
|
5033
|
+
if (this.persistent && !this.sandboxCreated) {}
|
|
5034
|
+
resolve2({
|
|
5035
|
+
success: false,
|
|
5036
|
+
output: rawOutput,
|
|
5037
|
+
error: `Failed to spawn docker sandbox: ${err.message}`,
|
|
5038
|
+
exitCode: 1
|
|
5039
|
+
});
|
|
5040
|
+
});
|
|
5041
|
+
if (options.signal) {
|
|
5042
|
+
options.signal.addEventListener("abort", () => {
|
|
5043
|
+
this.abort();
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
this.process.stdin?.write(options.prompt);
|
|
5047
|
+
this.process.stdin?.end();
|
|
5048
|
+
});
|
|
5049
|
+
} finally {
|
|
5050
|
+
if (!this.persistent) {
|
|
5051
|
+
this.cleanupSandbox();
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
abort() {
|
|
5056
|
+
this.aborted = true;
|
|
5057
|
+
const log = getLogger();
|
|
5058
|
+
if (this.persistent) {
|
|
5059
|
+
log.debug("Aborting sandboxed codex (persistent — keeping sandbox)", {
|
|
5060
|
+
sandboxName: this.sandboxName
|
|
5061
|
+
});
|
|
5062
|
+
if (this.process) {
|
|
5063
|
+
this.process.kill("SIGTERM");
|
|
5064
|
+
const timer = setTimeout(() => {
|
|
5065
|
+
if (this.process) {
|
|
5066
|
+
this.process.kill("SIGKILL");
|
|
5067
|
+
}
|
|
5068
|
+
}, 3000);
|
|
5069
|
+
if (timer.unref)
|
|
5070
|
+
timer.unref();
|
|
5071
|
+
}
|
|
5072
|
+
} else {
|
|
5073
|
+
if (!this.sandboxName)
|
|
5074
|
+
return;
|
|
5075
|
+
log.debug("Aborting sandboxed codex (ephemeral — removing sandbox)", {
|
|
5076
|
+
sandboxName: this.sandboxName
|
|
5077
|
+
});
|
|
5078
|
+
try {
|
|
5079
|
+
execSync8(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5080
|
+
} catch {}
|
|
5081
|
+
}
|
|
5082
|
+
}
|
|
5083
|
+
destroy() {
|
|
5084
|
+
if (!this.sandboxName)
|
|
5085
|
+
return;
|
|
5086
|
+
if (this.userManaged) {
|
|
5087
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
5088
|
+
return;
|
|
5089
|
+
}
|
|
5090
|
+
const log = getLogger();
|
|
5091
|
+
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
5092
|
+
try {
|
|
5093
|
+
execSync8(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5094
|
+
} catch {}
|
|
5095
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
5096
|
+
this.sandboxName = null;
|
|
5097
|
+
this.sandboxCreated = false;
|
|
5098
|
+
}
|
|
5099
|
+
cleanupSandbox() {
|
|
5100
|
+
if (!this.sandboxName)
|
|
5101
|
+
return;
|
|
5102
|
+
const log = getLogger();
|
|
5103
|
+
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
5104
|
+
try {
|
|
5105
|
+
execSync8(`docker sandbox rm ${this.sandboxName}`, {
|
|
5106
|
+
timeout: 60000
|
|
5107
|
+
});
|
|
5108
|
+
} catch {}
|
|
5109
|
+
unregisterActiveSandbox(this.sandboxName);
|
|
5110
|
+
this.sandboxName = null;
|
|
5111
|
+
}
|
|
5112
|
+
async isSandboxRunning() {
|
|
5113
|
+
if (!this.sandboxName)
|
|
5114
|
+
return false;
|
|
5115
|
+
try {
|
|
5116
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5117
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
5118
|
+
const execAsync2 = promisify2(exec2);
|
|
5119
|
+
const { stdout } = await execAsync2("docker sandbox ls", {
|
|
5120
|
+
timeout: 5000
|
|
5121
|
+
});
|
|
5122
|
+
return stdout.includes(this.sandboxName);
|
|
5123
|
+
} catch {
|
|
5124
|
+
return false;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
async createSandboxWithClaude(name, cwd) {
|
|
5128
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5129
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
5130
|
+
const execAsync2 = promisify2(exec2);
|
|
5131
|
+
try {
|
|
5132
|
+
await execAsync2(`docker sandbox run --name ${name} claude ${cwd} -- --version`, { timeout: 120000 });
|
|
5133
|
+
} catch {}
|
|
5134
|
+
}
|
|
5135
|
+
async ensureCodexInstalled(name) {
|
|
5136
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5137
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
5138
|
+
const execAsync2 = promisify2(exec2);
|
|
5139
|
+
try {
|
|
5140
|
+
await execAsync2(`docker sandbox exec ${name} which codex`, {
|
|
5141
|
+
timeout: 5000
|
|
5142
|
+
});
|
|
5143
|
+
} catch {
|
|
5144
|
+
await execAsync2(`docker sandbox exec ${name} npm install -g @openai/codex`, { timeout: 120000 });
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
getSandboxName() {
|
|
5148
|
+
return this.sandboxName;
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
function buildSandboxName2(options) {
|
|
5152
|
+
const ts = Date.now();
|
|
5153
|
+
if (options.activity) {
|
|
5154
|
+
const match = options.activity.match(/issue\s*#(\d+)/i);
|
|
5155
|
+
if (match) {
|
|
5156
|
+
return `locus-codex-issue-${match[1]}-${ts}`;
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
const segment = options.cwd.split("/").pop() ?? "run";
|
|
5160
|
+
return `locus-codex-${segment}-${ts}`;
|
|
5161
|
+
}
|
|
5162
|
+
var init_codex_sandbox = __esm(() => {
|
|
5163
|
+
init_logger();
|
|
5164
|
+
init_sandbox_ignore();
|
|
5165
|
+
init_shutdown();
|
|
5166
|
+
init_codex();
|
|
5167
|
+
});
|
|
5168
|
+
|
|
5169
|
+
// src/ai/runner.ts
|
|
5170
|
+
async function createRunnerAsync(provider, sandboxed) {
|
|
5171
|
+
switch (provider) {
|
|
5172
|
+
case "claude":
|
|
5173
|
+
return sandboxed ? new SandboxedClaudeRunner : new ClaudeRunner;
|
|
5174
|
+
case "codex":
|
|
5175
|
+
return sandboxed ? new SandboxedCodexRunner : new CodexRunner;
|
|
5176
|
+
default:
|
|
5177
|
+
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
function createUserManagedSandboxRunner(provider, sandboxName) {
|
|
5181
|
+
switch (provider) {
|
|
5182
|
+
case "claude":
|
|
5183
|
+
return new SandboxedClaudeRunner(sandboxName, true);
|
|
5184
|
+
case "codex":
|
|
5185
|
+
return new SandboxedCodexRunner(sandboxName, true);
|
|
5186
|
+
default:
|
|
5187
|
+
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
var init_runner = __esm(() => {
|
|
5191
|
+
init_claude();
|
|
5192
|
+
init_claude_sandbox();
|
|
5193
|
+
init_codex();
|
|
5194
|
+
init_codex_sandbox();
|
|
5195
|
+
});
|
|
5196
|
+
|
|
5197
|
+
// src/ai/run-ai.ts
|
|
5198
|
+
var exports_run_ai = {};
|
|
5199
|
+
__export(exports_run_ai, {
|
|
5200
|
+
runAI: () => runAI
|
|
5201
|
+
});
|
|
5202
|
+
function normalizeErrorMessage(error) {
|
|
5203
|
+
if (!error)
|
|
5204
|
+
return;
|
|
5205
|
+
const trimmed = error.trim();
|
|
5206
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
5207
|
+
}
|
|
5208
|
+
function stripAnsi2(text) {
|
|
5209
|
+
return text.replace(/\u001B\[[0-9;]*[A-Za-z]/g, "");
|
|
5210
|
+
}
|
|
5211
|
+
function extractErrorFromStructuredLine(line) {
|
|
5212
|
+
try {
|
|
5213
|
+
const parsed = JSON.parse(line);
|
|
5214
|
+
const candidateValues = [
|
|
5215
|
+
parsed.error,
|
|
5216
|
+
parsed.message,
|
|
5217
|
+
parsed.text,
|
|
5218
|
+
typeof parsed.item === "object" && parsed.item ? parsed.item.error : undefined,
|
|
5219
|
+
typeof parsed.item === "object" && parsed.item ? parsed.item.message : undefined,
|
|
5220
|
+
typeof parsed.item === "object" && parsed.item ? parsed.item.text : undefined
|
|
5221
|
+
];
|
|
5222
|
+
for (const value of candidateValues) {
|
|
5223
|
+
if (typeof value !== "string")
|
|
5224
|
+
continue;
|
|
5225
|
+
const normalized = normalizeErrorMessage(stripAnsi2(value));
|
|
5226
|
+
if (normalized)
|
|
5227
|
+
return normalized;
|
|
5228
|
+
}
|
|
5229
|
+
return;
|
|
5230
|
+
} catch {
|
|
5231
|
+
return;
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
function extractErrorFromOutput(output) {
|
|
5235
|
+
if (!output)
|
|
5236
|
+
return;
|
|
5237
|
+
const lines = output.split(`
|
|
5238
|
+
`);
|
|
5239
|
+
for (let index = lines.length - 1;index >= 0; index--) {
|
|
5240
|
+
const rawLine = lines[index] ?? "";
|
|
5241
|
+
const line = normalizeErrorMessage(stripAnsi2(rawLine));
|
|
5242
|
+
if (!line)
|
|
5243
|
+
continue;
|
|
5244
|
+
const structured = extractErrorFromStructuredLine(line);
|
|
5245
|
+
if (structured)
|
|
5246
|
+
return structured.slice(0, 500);
|
|
5247
|
+
return line.slice(0, 500);
|
|
5248
|
+
}
|
|
5249
|
+
return;
|
|
5250
|
+
}
|
|
5251
|
+
async function runAI(options) {
|
|
5252
|
+
const indicator = getStatusIndicator();
|
|
5253
|
+
const renderer = options.silent ? null : new StreamRenderer;
|
|
5254
|
+
let output = "";
|
|
5255
|
+
let wasAborted = false;
|
|
5256
|
+
let runner = null;
|
|
5257
|
+
const resolvedProvider = inferProviderFromModel(options.model) || options.provider;
|
|
5258
|
+
const abortController = new AbortController;
|
|
5259
|
+
const cleanupInterrupt = options.noInterrupt ? () => {} : listenForInterrupt(() => {
|
|
5260
|
+
if (wasAborted)
|
|
5261
|
+
return;
|
|
5262
|
+
wasAborted = true;
|
|
5263
|
+
indicator.stop();
|
|
5264
|
+
renderer?.stop();
|
|
5265
|
+
process.stderr.write(`\r
|
|
5266
|
+
${yellow("⚡")} ${dim("Interrupting...")}\r
|
|
5267
|
+
`);
|
|
5268
|
+
abortController.abort();
|
|
5269
|
+
if (runner)
|
|
5270
|
+
runner.abort();
|
|
5271
|
+
}, () => {
|
|
5272
|
+
indicator.stop();
|
|
4200
5273
|
renderer?.stop();
|
|
4201
5274
|
process.stderr.write(`\r
|
|
4202
5275
|
${red("✗")} ${dim("Force exit.")}\r
|
|
@@ -4207,7 +5280,13 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
4207
5280
|
indicator.start("Thinking...", {
|
|
4208
5281
|
activity: options.activity
|
|
4209
5282
|
});
|
|
4210
|
-
|
|
5283
|
+
if (options.runner) {
|
|
5284
|
+
runner = options.runner;
|
|
5285
|
+
} else if (options.sandboxName) {
|
|
5286
|
+
runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName);
|
|
5287
|
+
} else {
|
|
5288
|
+
runner = await createRunnerAsync(resolvedProvider, options.sandboxed ?? true);
|
|
5289
|
+
}
|
|
4211
5290
|
const available = await runner.isAvailable();
|
|
4212
5291
|
if (!available) {
|
|
4213
5292
|
indicator.stop();
|
|
@@ -4227,6 +5306,7 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
4227
5306
|
cwd: options.cwd,
|
|
4228
5307
|
signal: abortController.signal,
|
|
4229
5308
|
verbose: options.verbose,
|
|
5309
|
+
activity: options.activity,
|
|
4230
5310
|
onOutput: (chunk) => {
|
|
4231
5311
|
if (wasAborted)
|
|
4232
5312
|
return;
|
|
@@ -4237,6 +5317,9 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
4237
5317
|
renderer?.push(chunk);
|
|
4238
5318
|
output += chunk;
|
|
4239
5319
|
},
|
|
5320
|
+
onStatusChange: (message) => {
|
|
5321
|
+
indicator.setMessage(message);
|
|
5322
|
+
},
|
|
4240
5323
|
onToolActivity: (() => {
|
|
4241
5324
|
let lastActivityTime = 0;
|
|
4242
5325
|
return (summary) => {
|
|
@@ -4261,20 +5344,25 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
4261
5344
|
exitCode: result.exitCode
|
|
4262
5345
|
};
|
|
4263
5346
|
}
|
|
5347
|
+
const normalizedRunnerError = normalizeErrorMessage(result.error);
|
|
5348
|
+
const extractedOutputError = extractErrorFromOutput(result.output);
|
|
5349
|
+
const fallbackError = `${runner.name} failed with exit code ${result.exitCode}.`;
|
|
4264
5350
|
return {
|
|
4265
5351
|
success: result.success,
|
|
4266
5352
|
output,
|
|
4267
|
-
error: result.
|
|
5353
|
+
error: result.success ? undefined : normalizedRunnerError ?? extractedOutputError ?? fallbackError,
|
|
4268
5354
|
interrupted: false,
|
|
4269
5355
|
exitCode: result.exitCode
|
|
4270
5356
|
};
|
|
4271
5357
|
} catch (e) {
|
|
4272
5358
|
indicator.stop();
|
|
4273
5359
|
renderer?.stop();
|
|
5360
|
+
const normalizedCaughtError = normalizeErrorMessage(e instanceof Error ? e.message : String(e));
|
|
5361
|
+
const fallbackError = `${resolvedProvider} runner failed unexpectedly.`;
|
|
4274
5362
|
return {
|
|
4275
5363
|
success: false,
|
|
4276
5364
|
output,
|
|
4277
|
-
error:
|
|
5365
|
+
error: normalizedCaughtError ?? fallbackError,
|
|
4278
5366
|
interrupted: wasAborted,
|
|
4279
5367
|
exitCode: 1
|
|
4280
5368
|
};
|
|
@@ -4517,7 +5605,9 @@ async function issueCreate(projectRoot, parsed) {
|
|
|
4517
5605
|
model: config.ai.model,
|
|
4518
5606
|
cwd: projectRoot,
|
|
4519
5607
|
silent: true,
|
|
4520
|
-
activity: "generating issue"
|
|
5608
|
+
activity: "generating issue",
|
|
5609
|
+
sandboxed: config.sandbox.enabled,
|
|
5610
|
+
sandboxName: config.sandbox.name
|
|
4521
5611
|
});
|
|
4522
5612
|
if (!aiResult.success && !aiResult.interrupted) {
|
|
4523
5613
|
process.stderr.write(`${red("✗")} Failed to generate issue: ${aiResult.error}
|
|
@@ -5675,9 +6765,9 @@ var init_sprint = __esm(() => {
|
|
|
5675
6765
|
});
|
|
5676
6766
|
|
|
5677
6767
|
// src/core/prompt-builder.ts
|
|
5678
|
-
import { execSync as
|
|
5679
|
-
import { existsSync as
|
|
5680
|
-
import { join as
|
|
6768
|
+
import { execSync as execSync9 } from "node:child_process";
|
|
6769
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "node:fs";
|
|
6770
|
+
import { join as join12 } from "node:path";
|
|
5681
6771
|
function buildExecutionPrompt(ctx) {
|
|
5682
6772
|
const sections = [];
|
|
5683
6773
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -5707,13 +6797,13 @@ function buildFeedbackPrompt(ctx) {
|
|
|
5707
6797
|
}
|
|
5708
6798
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
5709
6799
|
const sections = [];
|
|
5710
|
-
const locusmd = readFileSafe(
|
|
6800
|
+
const locusmd = readFileSafe(join12(projectRoot, "LOCUS.md"));
|
|
5711
6801
|
if (locusmd) {
|
|
5712
6802
|
sections.push(`# Project Instructions
|
|
5713
6803
|
|
|
5714
6804
|
${locusmd}`);
|
|
5715
6805
|
}
|
|
5716
|
-
const learnings = readFileSafe(
|
|
6806
|
+
const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
|
|
5717
6807
|
if (learnings) {
|
|
5718
6808
|
sections.push(`# Past Learnings
|
|
5719
6809
|
|
|
@@ -5739,24 +6829,24 @@ ${userMessage}`);
|
|
|
5739
6829
|
}
|
|
5740
6830
|
function buildSystemContext(projectRoot) {
|
|
5741
6831
|
const parts = ["# System Context"];
|
|
5742
|
-
const locusmd = readFileSafe(
|
|
6832
|
+
const locusmd = readFileSafe(join12(projectRoot, "LOCUS.md"));
|
|
5743
6833
|
if (locusmd) {
|
|
5744
6834
|
parts.push(`## Project Instructions (LOCUS.md)
|
|
5745
6835
|
|
|
5746
6836
|
${locusmd}`);
|
|
5747
6837
|
}
|
|
5748
|
-
const learnings = readFileSafe(
|
|
6838
|
+
const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
|
|
5749
6839
|
if (learnings) {
|
|
5750
6840
|
parts.push(`## Past Learnings
|
|
5751
6841
|
|
|
5752
6842
|
${learnings}`);
|
|
5753
6843
|
}
|
|
5754
|
-
const discussionsDir =
|
|
5755
|
-
if (
|
|
6844
|
+
const discussionsDir = join12(projectRoot, ".locus", "discussions");
|
|
6845
|
+
if (existsSync13(discussionsDir)) {
|
|
5756
6846
|
try {
|
|
5757
6847
|
const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
5758
6848
|
for (const file of files) {
|
|
5759
|
-
const content = readFileSafe(
|
|
6849
|
+
const content = readFileSafe(join12(discussionsDir, file));
|
|
5760
6850
|
if (content) {
|
|
5761
6851
|
parts.push(`## Discussion: ${file.replace(".md", "")}
|
|
5762
6852
|
|
|
@@ -5819,7 +6909,7 @@ ${diffSummary}
|
|
|
5819
6909
|
function buildRepoContext(projectRoot) {
|
|
5820
6910
|
const parts = ["# Repository Context"];
|
|
5821
6911
|
try {
|
|
5822
|
-
const tree =
|
|
6912
|
+
const tree = execSync9("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
6913
|
if (tree) {
|
|
5824
6914
|
parts.push(`## File Tree
|
|
5825
6915
|
|
|
@@ -5829,7 +6919,7 @@ ${tree}
|
|
|
5829
6919
|
}
|
|
5830
6920
|
} catch {}
|
|
5831
6921
|
try {
|
|
5832
|
-
const gitLog =
|
|
6922
|
+
const gitLog = execSync9("git log --oneline -10", {
|
|
5833
6923
|
cwd: projectRoot,
|
|
5834
6924
|
encoding: "utf-8",
|
|
5835
6925
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5843,7 +6933,7 @@ ${gitLog}
|
|
|
5843
6933
|
}
|
|
5844
6934
|
} catch {}
|
|
5845
6935
|
try {
|
|
5846
|
-
const branch =
|
|
6936
|
+
const branch = execSync9("git rev-parse --abbrev-ref HEAD", {
|
|
5847
6937
|
cwd: projectRoot,
|
|
5848
6938
|
encoding: "utf-8",
|
|
5849
6939
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5902,9 +6992,9 @@ function buildFeedbackInstructions() {
|
|
|
5902
6992
|
}
|
|
5903
6993
|
function readFileSafe(path) {
|
|
5904
6994
|
try {
|
|
5905
|
-
if (!
|
|
6995
|
+
if (!existsSync13(path))
|
|
5906
6996
|
return null;
|
|
5907
|
-
return
|
|
6997
|
+
return readFileSync10(path, "utf-8");
|
|
5908
6998
|
} catch {
|
|
5909
6999
|
return null;
|
|
5910
7000
|
}
|
|
@@ -6096,7 +7186,7 @@ var init_diff_renderer = __esm(() => {
|
|
|
6096
7186
|
});
|
|
6097
7187
|
|
|
6098
7188
|
// src/repl/commands.ts
|
|
6099
|
-
import { execSync as
|
|
7189
|
+
import { execSync as execSync10 } from "node:child_process";
|
|
6100
7190
|
function getSlashCommands() {
|
|
6101
7191
|
return [
|
|
6102
7192
|
{
|
|
@@ -6288,7 +7378,7 @@ function cmdModel(args, ctx) {
|
|
|
6288
7378
|
}
|
|
6289
7379
|
function cmdDiff(_args, ctx) {
|
|
6290
7380
|
try {
|
|
6291
|
-
const diff =
|
|
7381
|
+
const diff = execSync10("git diff", {
|
|
6292
7382
|
cwd: ctx.projectRoot,
|
|
6293
7383
|
encoding: "utf-8",
|
|
6294
7384
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6324,7 +7414,7 @@ function cmdDiff(_args, ctx) {
|
|
|
6324
7414
|
}
|
|
6325
7415
|
function cmdUndo(_args, ctx) {
|
|
6326
7416
|
try {
|
|
6327
|
-
const status =
|
|
7417
|
+
const status = execSync10("git status --porcelain", {
|
|
6328
7418
|
cwd: ctx.projectRoot,
|
|
6329
7419
|
encoding: "utf-8",
|
|
6330
7420
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6334,7 +7424,7 @@ function cmdUndo(_args, ctx) {
|
|
|
6334
7424
|
`);
|
|
6335
7425
|
return;
|
|
6336
7426
|
}
|
|
6337
|
-
|
|
7427
|
+
execSync10("git checkout .", {
|
|
6338
7428
|
cwd: ctx.projectRoot,
|
|
6339
7429
|
encoding: "utf-8",
|
|
6340
7430
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6368,7 +7458,7 @@ var init_commands = __esm(() => {
|
|
|
6368
7458
|
|
|
6369
7459
|
// src/repl/completions.ts
|
|
6370
7460
|
import { readdirSync as readdirSync4 } from "node:fs";
|
|
6371
|
-
import { basename as basename2, dirname as
|
|
7461
|
+
import { basename as basename2, dirname as dirname4, join as join13 } from "node:path";
|
|
6372
7462
|
|
|
6373
7463
|
class SlashCommandCompletion {
|
|
6374
7464
|
commands;
|
|
@@ -6423,7 +7513,7 @@ class FilePathCompletion {
|
|
|
6423
7513
|
}
|
|
6424
7514
|
findMatches(partial) {
|
|
6425
7515
|
try {
|
|
6426
|
-
const dir = partial.includes("/") ?
|
|
7516
|
+
const dir = partial.includes("/") ? join13(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
6427
7517
|
const prefix = basename2(partial);
|
|
6428
7518
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
6429
7519
|
return entries.filter((e) => {
|
|
@@ -6434,7 +7524,7 @@ class FilePathCompletion {
|
|
|
6434
7524
|
return e.name.startsWith(prefix);
|
|
6435
7525
|
}).map((e) => {
|
|
6436
7526
|
const name = e.isDirectory() ? `${e.name}/` : e.name;
|
|
6437
|
-
return partial.includes("/") ? `${
|
|
7527
|
+
return partial.includes("/") ? `${dirname4(partial)}/${name}` : name;
|
|
6438
7528
|
}).slice(0, 20);
|
|
6439
7529
|
} catch {
|
|
6440
7530
|
return [];
|
|
@@ -6459,14 +7549,14 @@ class CombinedCompletion {
|
|
|
6459
7549
|
var init_completions = () => {};
|
|
6460
7550
|
|
|
6461
7551
|
// src/repl/input-history.ts
|
|
6462
|
-
import { existsSync as
|
|
6463
|
-
import { dirname as
|
|
7552
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
7553
|
+
import { dirname as dirname5, join as join14 } from "node:path";
|
|
6464
7554
|
|
|
6465
7555
|
class InputHistory {
|
|
6466
7556
|
entries = [];
|
|
6467
7557
|
filePath;
|
|
6468
7558
|
constructor(projectRoot) {
|
|
6469
|
-
this.filePath =
|
|
7559
|
+
this.filePath = join14(projectRoot, ".locus", "sessions", ".input-history");
|
|
6470
7560
|
this.load();
|
|
6471
7561
|
}
|
|
6472
7562
|
add(text) {
|
|
@@ -6505,22 +7595,22 @@ class InputHistory {
|
|
|
6505
7595
|
}
|
|
6506
7596
|
load() {
|
|
6507
7597
|
try {
|
|
6508
|
-
if (!
|
|
7598
|
+
if (!existsSync14(this.filePath))
|
|
6509
7599
|
return;
|
|
6510
|
-
const content =
|
|
7600
|
+
const content = readFileSync11(this.filePath, "utf-8");
|
|
6511
7601
|
this.entries = content.split(`
|
|
6512
7602
|
`).map((line) => this.unescape(line)).filter(Boolean);
|
|
6513
7603
|
} catch {}
|
|
6514
7604
|
}
|
|
6515
7605
|
save() {
|
|
6516
7606
|
try {
|
|
6517
|
-
const dir =
|
|
6518
|
-
if (!
|
|
6519
|
-
|
|
7607
|
+
const dir = dirname5(this.filePath);
|
|
7608
|
+
if (!existsSync14(dir)) {
|
|
7609
|
+
mkdirSync9(dir, { recursive: true });
|
|
6520
7610
|
}
|
|
6521
7611
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
6522
7612
|
`);
|
|
6523
|
-
|
|
7613
|
+
writeFileSync7(this.filePath, content, "utf-8");
|
|
6524
7614
|
} catch {}
|
|
6525
7615
|
}
|
|
6526
7616
|
escape(text) {
|
|
@@ -6545,23 +7635,22 @@ var init_model_config = __esm(() => {
|
|
|
6545
7635
|
});
|
|
6546
7636
|
|
|
6547
7637
|
// src/repl/session-manager.ts
|
|
6548
|
-
import { randomBytes } from "node:crypto";
|
|
6549
7638
|
import {
|
|
6550
|
-
existsSync as
|
|
6551
|
-
mkdirSync as
|
|
7639
|
+
existsSync as existsSync15,
|
|
7640
|
+
mkdirSync as mkdirSync10,
|
|
6552
7641
|
readdirSync as readdirSync5,
|
|
6553
|
-
readFileSync as
|
|
6554
|
-
unlinkSync as
|
|
6555
|
-
writeFileSync as
|
|
7642
|
+
readFileSync as readFileSync12,
|
|
7643
|
+
unlinkSync as unlinkSync4,
|
|
7644
|
+
writeFileSync as writeFileSync8
|
|
6556
7645
|
} from "node:fs";
|
|
6557
|
-
import { basename as basename3, join as
|
|
7646
|
+
import { basename as basename3, join as join15 } from "node:path";
|
|
6558
7647
|
|
|
6559
7648
|
class SessionManager {
|
|
6560
7649
|
sessionsDir;
|
|
6561
7650
|
constructor(projectRoot) {
|
|
6562
|
-
this.sessionsDir =
|
|
6563
|
-
if (!
|
|
6564
|
-
|
|
7651
|
+
this.sessionsDir = join15(projectRoot, ".locus", "sessions");
|
|
7652
|
+
if (!existsSync15(this.sessionsDir)) {
|
|
7653
|
+
mkdirSync10(this.sessionsDir, { recursive: true });
|
|
6565
7654
|
}
|
|
6566
7655
|
}
|
|
6567
7656
|
create(options) {
|
|
@@ -6586,14 +7675,14 @@ class SessionManager {
|
|
|
6586
7675
|
}
|
|
6587
7676
|
isPersisted(sessionOrId) {
|
|
6588
7677
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
6589
|
-
return
|
|
7678
|
+
return existsSync15(this.getSessionPath(sessionId));
|
|
6590
7679
|
}
|
|
6591
7680
|
load(idOrPrefix) {
|
|
6592
7681
|
const files = this.listSessionFiles();
|
|
6593
7682
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
6594
|
-
if (
|
|
7683
|
+
if (existsSync15(exactPath)) {
|
|
6595
7684
|
try {
|
|
6596
|
-
return JSON.parse(
|
|
7685
|
+
return JSON.parse(readFileSync12(exactPath, "utf-8"));
|
|
6597
7686
|
} catch {
|
|
6598
7687
|
return null;
|
|
6599
7688
|
}
|
|
@@ -6601,7 +7690,7 @@ class SessionManager {
|
|
|
6601
7690
|
const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
|
|
6602
7691
|
if (matches.length === 1) {
|
|
6603
7692
|
try {
|
|
6604
|
-
return JSON.parse(
|
|
7693
|
+
return JSON.parse(readFileSync12(matches[0], "utf-8"));
|
|
6605
7694
|
} catch {
|
|
6606
7695
|
return null;
|
|
6607
7696
|
}
|
|
@@ -6614,7 +7703,7 @@ class SessionManager {
|
|
|
6614
7703
|
save(session) {
|
|
6615
7704
|
session.updated = new Date().toISOString();
|
|
6616
7705
|
const path = this.getSessionPath(session.id);
|
|
6617
|
-
|
|
7706
|
+
writeFileSync8(path, `${JSON.stringify(session, null, 2)}
|
|
6618
7707
|
`, "utf-8");
|
|
6619
7708
|
}
|
|
6620
7709
|
addMessage(session, message) {
|
|
@@ -6626,7 +7715,7 @@ class SessionManager {
|
|
|
6626
7715
|
const sessions = [];
|
|
6627
7716
|
for (const file of files) {
|
|
6628
7717
|
try {
|
|
6629
|
-
const session = JSON.parse(
|
|
7718
|
+
const session = JSON.parse(readFileSync12(file, "utf-8"));
|
|
6630
7719
|
sessions.push({
|
|
6631
7720
|
id: session.id,
|
|
6632
7721
|
created: session.created,
|
|
@@ -6641,8 +7730,8 @@ class SessionManager {
|
|
|
6641
7730
|
}
|
|
6642
7731
|
delete(sessionId) {
|
|
6643
7732
|
const path = this.getSessionPath(sessionId);
|
|
6644
|
-
if (
|
|
6645
|
-
|
|
7733
|
+
if (existsSync15(path)) {
|
|
7734
|
+
unlinkSync4(path);
|
|
6646
7735
|
return true;
|
|
6647
7736
|
}
|
|
6648
7737
|
return false;
|
|
@@ -6653,7 +7742,7 @@ class SessionManager {
|
|
|
6653
7742
|
let pruned = 0;
|
|
6654
7743
|
const withStats = files.map((f) => {
|
|
6655
7744
|
try {
|
|
6656
|
-
const session = JSON.parse(
|
|
7745
|
+
const session = JSON.parse(readFileSync12(f, "utf-8"));
|
|
6657
7746
|
return { path: f, updated: new Date(session.updated).getTime() };
|
|
6658
7747
|
} catch {
|
|
6659
7748
|
return { path: f, updated: 0 };
|
|
@@ -6663,7 +7752,7 @@ class SessionManager {
|
|
|
6663
7752
|
for (const entry of withStats) {
|
|
6664
7753
|
if (now - entry.updated > SESSION_MAX_AGE_MS) {
|
|
6665
7754
|
try {
|
|
6666
|
-
|
|
7755
|
+
unlinkSync4(entry.path);
|
|
6667
7756
|
pruned++;
|
|
6668
7757
|
} catch {}
|
|
6669
7758
|
}
|
|
@@ -6671,10 +7760,10 @@ class SessionManager {
|
|
|
6671
7760
|
const remaining = withStats.length - pruned;
|
|
6672
7761
|
if (remaining > MAX_SESSIONS) {
|
|
6673
7762
|
const toRemove = remaining - MAX_SESSIONS;
|
|
6674
|
-
const alive = withStats.filter((e) =>
|
|
7763
|
+
const alive = withStats.filter((e) => existsSync15(e.path));
|
|
6675
7764
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
6676
7765
|
try {
|
|
6677
|
-
|
|
7766
|
+
unlinkSync4(alive[i].path);
|
|
6678
7767
|
pruned++;
|
|
6679
7768
|
} catch {}
|
|
6680
7769
|
}
|
|
@@ -6686,16 +7775,16 @@ class SessionManager {
|
|
|
6686
7775
|
}
|
|
6687
7776
|
listSessionFiles() {
|
|
6688
7777
|
try {
|
|
6689
|
-
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
7778
|
+
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join15(this.sessionsDir, f));
|
|
6690
7779
|
} catch {
|
|
6691
7780
|
return [];
|
|
6692
7781
|
}
|
|
6693
7782
|
}
|
|
6694
7783
|
generateId() {
|
|
6695
|
-
return
|
|
7784
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6696
7785
|
}
|
|
6697
7786
|
getSessionPath(sessionId) {
|
|
6698
|
-
return
|
|
7787
|
+
return join15(this.sessionsDir, `${sessionId}.json`);
|
|
6699
7788
|
}
|
|
6700
7789
|
}
|
|
6701
7790
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -6705,7 +7794,7 @@ var init_session_manager = __esm(() => {
|
|
|
6705
7794
|
});
|
|
6706
7795
|
|
|
6707
7796
|
// src/repl/repl.ts
|
|
6708
|
-
import { execSync as
|
|
7797
|
+
import { execSync as execSync11 } from "node:child_process";
|
|
6709
7798
|
async function startRepl(options) {
|
|
6710
7799
|
const { projectRoot, config } = options;
|
|
6711
7800
|
const sessionManager = new SessionManager(projectRoot);
|
|
@@ -6723,7 +7812,7 @@ async function startRepl(options) {
|
|
|
6723
7812
|
} else {
|
|
6724
7813
|
let branch = "main";
|
|
6725
7814
|
try {
|
|
6726
|
-
branch =
|
|
7815
|
+
branch = execSync11("git rev-parse --abbrev-ref HEAD", {
|
|
6727
7816
|
cwd: projectRoot,
|
|
6728
7817
|
encoding: "utf-8",
|
|
6729
7818
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6764,6 +7853,18 @@ async function executeOneShotPrompt(prompt, session, sessionManager, options) {
|
|
|
6764
7853
|
}
|
|
6765
7854
|
async function runInteractiveRepl(session, sessionManager, options) {
|
|
6766
7855
|
const { projectRoot, config } = options;
|
|
7856
|
+
let sandboxRunner = null;
|
|
7857
|
+
if (config.sandbox.enabled && config.sandbox.name) {
|
|
7858
|
+
const provider = inferProviderFromModel(config.ai.model) || config.ai.provider;
|
|
7859
|
+
sandboxRunner = createUserManagedSandboxRunner(provider, config.sandbox.name);
|
|
7860
|
+
process.stderr.write(`${dim("Using sandbox")} ${dim(config.sandbox.name)}
|
|
7861
|
+
`);
|
|
7862
|
+
} else if (config.sandbox.enabled) {
|
|
7863
|
+
const sandboxName = buildPersistentSandboxName(projectRoot);
|
|
7864
|
+
sandboxRunner = new SandboxedClaudeRunner(sandboxName);
|
|
7865
|
+
process.stderr.write(`${dim("Sandbox mode: prompts will share sandbox")} ${dim(sandboxName)}
|
|
7866
|
+
`);
|
|
7867
|
+
}
|
|
6767
7868
|
const history = new InputHistory(projectRoot);
|
|
6768
7869
|
const completion = new CombinedCompletion([
|
|
6769
7870
|
new SlashCommandCompletion(getAllCommandNames()),
|
|
@@ -6793,8 +7894,14 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
6793
7894
|
session.metadata.model = model;
|
|
6794
7895
|
const inferredProvider = inferProviderFromModel(model);
|
|
6795
7896
|
if (inferredProvider) {
|
|
7897
|
+
const providerChanged = inferredProvider !== currentProvider;
|
|
6796
7898
|
currentProvider = inferredProvider;
|
|
6797
7899
|
session.metadata.provider = inferredProvider;
|
|
7900
|
+
if (providerChanged && config.sandbox.enabled && config.sandbox.name) {
|
|
7901
|
+
sandboxRunner = createUserManagedSandboxRunner(inferredProvider, config.sandbox.name);
|
|
7902
|
+
process.stderr.write(`${dim("Switched sandbox agent to")} ${dim(inferredProvider)}
|
|
7903
|
+
`);
|
|
7904
|
+
}
|
|
6798
7905
|
}
|
|
6799
7906
|
persistReplModelSelection(projectRoot, config, model);
|
|
6800
7907
|
sessionManager.save(session);
|
|
@@ -6839,7 +7946,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
6839
7946
|
...config,
|
|
6840
7947
|
ai: { provider: currentProvider, model: currentModel }
|
|
6841
7948
|
}
|
|
6842
|
-
}, verbose);
|
|
7949
|
+
}, verbose, sandboxRunner ?? undefined);
|
|
6843
7950
|
sessionManager.addMessage(session, {
|
|
6844
7951
|
role: "assistant",
|
|
6845
7952
|
content: response,
|
|
@@ -6864,6 +7971,10 @@ ${red("✗")} ${msg}
|
|
|
6864
7971
|
break;
|
|
6865
7972
|
}
|
|
6866
7973
|
}
|
|
7974
|
+
if (sandboxRunner && "destroy" in sandboxRunner) {
|
|
7975
|
+
const runner = sandboxRunner;
|
|
7976
|
+
runner.destroy();
|
|
7977
|
+
}
|
|
6867
7978
|
const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
|
|
6868
7979
|
if (shouldPersistOnExit) {
|
|
6869
7980
|
sessionManager.save(session);
|
|
@@ -6879,14 +7990,17 @@ ${red("✗")} ${msg}
|
|
|
6879
7990
|
process.stdin.pause();
|
|
6880
7991
|
process.exit(0);
|
|
6881
7992
|
}
|
|
6882
|
-
async function executeAITurn(prompt, session, options, verbose = false) {
|
|
7993
|
+
async function executeAITurn(prompt, session, options, verbose = false, runner) {
|
|
6883
7994
|
const { config, projectRoot } = options;
|
|
6884
7995
|
const aiResult = await runAI({
|
|
6885
7996
|
prompt,
|
|
6886
7997
|
provider: config.ai.provider,
|
|
6887
7998
|
model: config.ai.model,
|
|
6888
7999
|
cwd: projectRoot,
|
|
6889
|
-
verbose
|
|
8000
|
+
verbose,
|
|
8001
|
+
sandboxed: config.sandbox.enabled,
|
|
8002
|
+
sandboxName: config.sandbox.name,
|
|
8003
|
+
runner
|
|
6890
8004
|
});
|
|
6891
8005
|
if (aiResult.interrupted) {
|
|
6892
8006
|
if (aiResult.output) {
|
|
@@ -6916,7 +8030,9 @@ function printWelcome(session) {
|
|
|
6916
8030
|
`);
|
|
6917
8031
|
}
|
|
6918
8032
|
var init_repl = __esm(() => {
|
|
8033
|
+
init_claude_sandbox();
|
|
6919
8034
|
init_run_ai();
|
|
8035
|
+
init_runner();
|
|
6920
8036
|
init_ai_models();
|
|
6921
8037
|
init_prompt_builder();
|
|
6922
8038
|
init_terminal();
|
|
@@ -7059,7 +8175,7 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
|
|
|
7059
8175
|
stream.emitStatus("thinking");
|
|
7060
8176
|
try {
|
|
7061
8177
|
const fullPrompt = buildReplPrompt(prompt, projectRoot, config);
|
|
7062
|
-
const runner = await createRunnerAsync(config.ai.provider);
|
|
8178
|
+
const runner = config.sandbox.name ? createUserManagedSandboxRunner(config.ai.provider, config.sandbox.name) : await createRunnerAsync(config.ai.provider, config.sandbox.enabled);
|
|
7063
8179
|
const available = await runner.isAvailable();
|
|
7064
8180
|
if (!available) {
|
|
7065
8181
|
stream.emitError(`${config.ai.provider} CLI not available`, false);
|
|
@@ -7107,7 +8223,7 @@ var init_exec = __esm(() => {
|
|
|
7107
8223
|
});
|
|
7108
8224
|
|
|
7109
8225
|
// src/core/agent.ts
|
|
7110
|
-
import { execSync as
|
|
8226
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
7111
8227
|
async function executeIssue(projectRoot, options) {
|
|
7112
8228
|
const log = getLogger();
|
|
7113
8229
|
const timer = createTimer();
|
|
@@ -7136,7 +8252,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
|
|
|
7136
8252
|
}
|
|
7137
8253
|
let issueComments = [];
|
|
7138
8254
|
try {
|
|
7139
|
-
const commentsRaw =
|
|
8255
|
+
const commentsRaw = execSync12(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7140
8256
|
if (commentsRaw) {
|
|
7141
8257
|
issueComments = commentsRaw.split(`
|
|
7142
8258
|
`).filter(Boolean);
|
|
@@ -7171,7 +8287,9 @@ ${yellow("⚠")} ${bold("Dry run")} — would execute with:
|
|
|
7171
8287
|
provider,
|
|
7172
8288
|
model,
|
|
7173
8289
|
cwd: options.worktreePath ?? projectRoot,
|
|
7174
|
-
activity: `issue #${issueNumber}
|
|
8290
|
+
activity: `issue #${issueNumber}`,
|
|
8291
|
+
sandboxed: options.sandboxed,
|
|
8292
|
+
sandboxName: options.sandboxName
|
|
7175
8293
|
});
|
|
7176
8294
|
const output = aiResult.output;
|
|
7177
8295
|
if (aiResult.interrupted) {
|
|
@@ -7275,7 +8393,9 @@ ${c.body}`),
|
|
|
7275
8393
|
provider: config.ai.provider,
|
|
7276
8394
|
model: config.ai.model,
|
|
7277
8395
|
cwd: projectRoot,
|
|
7278
|
-
activity: `iterating on PR #${prNumber}
|
|
8396
|
+
activity: `iterating on PR #${prNumber}`,
|
|
8397
|
+
sandboxed: config.sandbox.enabled,
|
|
8398
|
+
sandboxName: config.sandbox.name
|
|
7279
8399
|
});
|
|
7280
8400
|
if (aiResult.interrupted) {
|
|
7281
8401
|
process.stderr.write(`
|
|
@@ -7296,12 +8416,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
|
|
|
7296
8416
|
}
|
|
7297
8417
|
async function createIssuePR(projectRoot, config, issue) {
|
|
7298
8418
|
try {
|
|
7299
|
-
const currentBranch =
|
|
8419
|
+
const currentBranch = execSync12("git rev-parse --abbrev-ref HEAD", {
|
|
7300
8420
|
cwd: projectRoot,
|
|
7301
8421
|
encoding: "utf-8",
|
|
7302
8422
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7303
8423
|
}).trim();
|
|
7304
|
-
const diff =
|
|
8424
|
+
const diff = execSync12(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
7305
8425
|
cwd: projectRoot,
|
|
7306
8426
|
encoding: "utf-8",
|
|
7307
8427
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7310,7 +8430,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
7310
8430
|
getLogger().verbose("No changes to create PR for");
|
|
7311
8431
|
return;
|
|
7312
8432
|
}
|
|
7313
|
-
|
|
8433
|
+
execSync12(`git push -u origin ${currentBranch}`, {
|
|
7314
8434
|
cwd: projectRoot,
|
|
7315
8435
|
encoding: "utf-8",
|
|
7316
8436
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7356,9 +8476,9 @@ var init_agent = __esm(() => {
|
|
|
7356
8476
|
});
|
|
7357
8477
|
|
|
7358
8478
|
// src/core/conflict.ts
|
|
7359
|
-
import { execSync as
|
|
8479
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
7360
8480
|
function git2(args, cwd) {
|
|
7361
|
-
return
|
|
8481
|
+
return execSync13(`git ${args}`, {
|
|
7362
8482
|
cwd,
|
|
7363
8483
|
encoding: "utf-8",
|
|
7364
8484
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7483,185 +8603,144 @@ var init_conflict = __esm(() => {
|
|
|
7483
8603
|
init_logger();
|
|
7484
8604
|
});
|
|
7485
8605
|
|
|
7486
|
-
// src/core/
|
|
7487
|
-
import {
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
}
|
|
7498
|
-
|
|
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;
|
|
8606
|
+
// src/core/sandbox.ts
|
|
8607
|
+
import { execFile } from "node:child_process";
|
|
8608
|
+
async function detectSandboxSupport() {
|
|
8609
|
+
if (cachedStatus)
|
|
8610
|
+
return cachedStatus;
|
|
8611
|
+
const log = getLogger();
|
|
8612
|
+
log.debug("Detecting Docker sandbox support...");
|
|
8613
|
+
const status = await runDetection();
|
|
8614
|
+
cachedStatus = status;
|
|
8615
|
+
if (status.available) {
|
|
8616
|
+
log.verbose("Docker sandbox support detected");
|
|
8617
|
+
} else {
|
|
8618
|
+
log.verbose(`Docker sandbox not available: ${status.reason}`);
|
|
7571
8619
|
}
|
|
8620
|
+
return status;
|
|
7572
8621
|
}
|
|
7573
|
-
function
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
}
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
8622
|
+
function runDetection() {
|
|
8623
|
+
return new Promise((resolve2) => {
|
|
8624
|
+
let settled = false;
|
|
8625
|
+
const child = execFile("docker", ["sandbox", "ls"], { timeout: TIMEOUT_MS }, (error, _stdout, stderr) => {
|
|
8626
|
+
if (settled)
|
|
8627
|
+
return;
|
|
8628
|
+
settled = true;
|
|
8629
|
+
if (!error) {
|
|
8630
|
+
resolve2({ available: true });
|
|
8631
|
+
return;
|
|
8632
|
+
}
|
|
8633
|
+
const code = error.code;
|
|
8634
|
+
if (code === "ENOENT") {
|
|
8635
|
+
resolve2({ available: false, reason: "Docker is not installed" });
|
|
8636
|
+
return;
|
|
8637
|
+
}
|
|
8638
|
+
if (error.killed) {
|
|
8639
|
+
resolve2({ available: false, reason: "Docker is not responding" });
|
|
8640
|
+
return;
|
|
8641
|
+
}
|
|
8642
|
+
const stderrStr = (stderr ?? "").toLowerCase();
|
|
8643
|
+
if (stderrStr.includes("unknown") || stderrStr.includes("not a docker command") || stderrStr.includes("is not a docker command")) {
|
|
8644
|
+
resolve2({
|
|
8645
|
+
available: false,
|
|
8646
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
8647
|
+
});
|
|
8648
|
+
return;
|
|
8649
|
+
}
|
|
8650
|
+
resolve2({
|
|
8651
|
+
available: false,
|
|
8652
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
8653
|
+
});
|
|
8654
|
+
});
|
|
8655
|
+
child.on?.("error", (err) => {
|
|
8656
|
+
if (settled)
|
|
8657
|
+
return;
|
|
8658
|
+
settled = true;
|
|
8659
|
+
if (err.code === "ENOENT") {
|
|
8660
|
+
resolve2({ available: false, reason: "Docker is not installed" });
|
|
8661
|
+
} else {
|
|
8662
|
+
resolve2({
|
|
8663
|
+
available: false,
|
|
8664
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
8665
|
+
});
|
|
8666
|
+
}
|
|
8667
|
+
});
|
|
8668
|
+
});
|
|
8669
|
+
}
|
|
8670
|
+
async function cleanupStaleSandboxes() {
|
|
8671
|
+
const log = getLogger();
|
|
8672
|
+
try {
|
|
8673
|
+
const { stdout } = await execFileAsync("docker", ["sandbox", "ls"], {
|
|
8674
|
+
timeout: TIMEOUT_MS
|
|
8675
|
+
});
|
|
8676
|
+
const lines = stdout.trim().split(`
|
|
7608
8677
|
`);
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
}
|
|
8678
|
+
if (lines.length <= 1)
|
|
8679
|
+
return 0;
|
|
8680
|
+
const staleNames = [];
|
|
8681
|
+
for (const line of lines.slice(1)) {
|
|
8682
|
+
const name = line.trim().split(/\s+/)[0];
|
|
8683
|
+
if (name?.startsWith("locus-")) {
|
|
8684
|
+
staleNames.push(name);
|
|
7617
8685
|
}
|
|
8686
|
+
}
|
|
8687
|
+
if (staleNames.length === 0)
|
|
8688
|
+
return 0;
|
|
8689
|
+
log.verbose(`Found ${staleNames.length} stale sandbox(es) to clean up`);
|
|
8690
|
+
let cleaned = 0;
|
|
8691
|
+
for (const name of staleNames) {
|
|
7618
8692
|
try {
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
8693
|
+
await execFileAsync("docker", ["sandbox", "rm", name], {
|
|
8694
|
+
timeout: 1e4
|
|
8695
|
+
});
|
|
8696
|
+
log.debug(`Removed stale sandbox: ${name}`);
|
|
8697
|
+
cleaned++;
|
|
7622
8698
|
} catch {
|
|
7623
|
-
|
|
7624
|
-
`);
|
|
8699
|
+
log.debug(`Failed to remove stale sandbox: ${name}`);
|
|
7625
8700
|
}
|
|
7626
8701
|
}
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
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;
|
|
8702
|
+
return cleaned;
|
|
8703
|
+
} catch {
|
|
8704
|
+
return 0;
|
|
7641
8705
|
}
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
8706
|
+
}
|
|
8707
|
+
function execFileAsync(file, args, options) {
|
|
8708
|
+
return new Promise((resolve2, reject) => {
|
|
8709
|
+
execFile(file, args, options, (error, stdout, stderr) => {
|
|
8710
|
+
if (error)
|
|
8711
|
+
reject(error);
|
|
8712
|
+
else
|
|
8713
|
+
resolve2({ stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
8714
|
+
});
|
|
8715
|
+
});
|
|
8716
|
+
}
|
|
8717
|
+
function resolveSandboxMode(config, flags) {
|
|
8718
|
+
if (flags.noSandbox) {
|
|
8719
|
+
return "disabled";
|
|
8720
|
+
}
|
|
8721
|
+
if (flags.sandbox !== undefined) {
|
|
8722
|
+
if (flags.sandbox === "require") {
|
|
8723
|
+
return "required";
|
|
7651
8724
|
}
|
|
7652
|
-
|
|
8725
|
+
throw new Error(`Invalid --sandbox value: "${flags.sandbox}". Valid values: require`);
|
|
8726
|
+
}
|
|
8727
|
+
if (!config.enabled) {
|
|
8728
|
+
return "disabled";
|
|
8729
|
+
}
|
|
8730
|
+
return "auto";
|
|
7653
8731
|
}
|
|
7654
|
-
var
|
|
7655
|
-
var
|
|
7656
|
-
|
|
8732
|
+
var TIMEOUT_MS = 5000, cachedStatus = null;
|
|
8733
|
+
var init_sandbox = __esm(() => {
|
|
8734
|
+
init_terminal();
|
|
8735
|
+
init_logger();
|
|
7657
8736
|
});
|
|
7658
8737
|
|
|
7659
8738
|
// src/core/worktree.ts
|
|
7660
|
-
import { execSync as
|
|
7661
|
-
import { existsSync as
|
|
7662
|
-
import { join as
|
|
8739
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
8740
|
+
import { existsSync as existsSync16, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
|
|
8741
|
+
import { join as join16 } from "node:path";
|
|
7663
8742
|
function git3(args, cwd) {
|
|
7664
|
-
return
|
|
8743
|
+
return execSync14(`git ${args}`, {
|
|
7665
8744
|
cwd,
|
|
7666
8745
|
encoding: "utf-8",
|
|
7667
8746
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7675,10 +8754,10 @@ function gitSafe2(args, cwd) {
|
|
|
7675
8754
|
}
|
|
7676
8755
|
}
|
|
7677
8756
|
function getWorktreeDir(projectRoot) {
|
|
7678
|
-
return
|
|
8757
|
+
return join16(projectRoot, ".locus", "worktrees");
|
|
7679
8758
|
}
|
|
7680
8759
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
7681
|
-
return
|
|
8760
|
+
return join16(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
7682
8761
|
}
|
|
7683
8762
|
function generateBranchName(issueNumber) {
|
|
7684
8763
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -7686,7 +8765,7 @@ function generateBranchName(issueNumber) {
|
|
|
7686
8765
|
}
|
|
7687
8766
|
function getWorktreeBranch(worktreePath) {
|
|
7688
8767
|
try {
|
|
7689
|
-
return
|
|
8768
|
+
return execSync14("git branch --show-current", {
|
|
7690
8769
|
cwd: worktreePath,
|
|
7691
8770
|
encoding: "utf-8",
|
|
7692
8771
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7698,7 +8777,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
7698
8777
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
7699
8778
|
const log = getLogger();
|
|
7700
8779
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
7701
|
-
if (
|
|
8780
|
+
if (existsSync16(worktreePath)) {
|
|
7702
8781
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
7703
8782
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
7704
8783
|
return {
|
|
@@ -7725,7 +8804,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
7725
8804
|
function removeWorktree(projectRoot, issueNumber) {
|
|
7726
8805
|
const log = getLogger();
|
|
7727
8806
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
7728
|
-
if (!
|
|
8807
|
+
if (!existsSync16(worktreePath)) {
|
|
7729
8808
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
7730
8809
|
return;
|
|
7731
8810
|
}
|
|
@@ -7744,7 +8823,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
7744
8823
|
function listWorktrees(projectRoot) {
|
|
7745
8824
|
const log = getLogger();
|
|
7746
8825
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
7747
|
-
if (!
|
|
8826
|
+
if (!existsSync16(worktreeDir)) {
|
|
7748
8827
|
return [];
|
|
7749
8828
|
}
|
|
7750
8829
|
const entries = readdirSync6(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -7764,7 +8843,7 @@ function listWorktrees(projectRoot) {
|
|
|
7764
8843
|
if (!match)
|
|
7765
8844
|
continue;
|
|
7766
8845
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
7767
|
-
const path =
|
|
8846
|
+
const path = join16(worktreeDir, entry);
|
|
7768
8847
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
7769
8848
|
let resolvedPath;
|
|
7770
8849
|
try {
|
|
@@ -7811,8 +8890,44 @@ var exports_run = {};
|
|
|
7811
8890
|
__export(exports_run, {
|
|
7812
8891
|
runCommand: () => runCommand
|
|
7813
8892
|
});
|
|
7814
|
-
import { execSync as
|
|
8893
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
8894
|
+
function printRunHelp() {
|
|
8895
|
+
process.stderr.write(`
|
|
8896
|
+
${bold("locus run")} — Execute issues using AI agents
|
|
8897
|
+
|
|
8898
|
+
${bold("Usage:")}
|
|
8899
|
+
locus run ${dim("# Run active sprint (sequential)")}
|
|
8900
|
+
locus run <issue> ${dim("# Run single issue (worktree)")}
|
|
8901
|
+
locus run <issue> <issue> ... ${dim("# Run multiple issues (parallel)")}
|
|
8902
|
+
locus run --resume ${dim("# Resume interrupted run")}
|
|
8903
|
+
|
|
8904
|
+
${bold("Options:")}
|
|
8905
|
+
--resume Resume a previously interrupted run
|
|
8906
|
+
--dry-run Show what would happen without executing
|
|
8907
|
+
--model <name> Override the AI model for this run
|
|
8908
|
+
--no-sandbox Disable Docker sandbox isolation
|
|
8909
|
+
--sandbox=require Require Docker sandbox (fail if unavailable)
|
|
8910
|
+
|
|
8911
|
+
${bold("Sandbox:")}
|
|
8912
|
+
By default, agents run inside Docker Desktop sandboxes (4.58+) for
|
|
8913
|
+
hypervisor-level isolation. If Docker is not available, agents run
|
|
8914
|
+
unsandboxed with a warning.
|
|
8915
|
+
|
|
8916
|
+
${bold("Examples:")}
|
|
8917
|
+
locus run ${dim("# Execute active sprint")}
|
|
8918
|
+
locus run 42 ${dim("# Run single issue")}
|
|
8919
|
+
locus run 42 43 44 ${dim("# Run issues in parallel")}
|
|
8920
|
+
locus run --resume ${dim("# Resume after failure")}
|
|
8921
|
+
locus run 42 --no-sandbox ${dim("# Run without sandbox")}
|
|
8922
|
+
locus run 42 --sandbox=require ${dim("# Require sandbox")}
|
|
8923
|
+
|
|
8924
|
+
`);
|
|
8925
|
+
}
|
|
7815
8926
|
async function runCommand(projectRoot, args, flags = {}) {
|
|
8927
|
+
if (args[0] === "help") {
|
|
8928
|
+
printRunHelp();
|
|
8929
|
+
return;
|
|
8930
|
+
}
|
|
7816
8931
|
const config = loadConfig(projectRoot);
|
|
7817
8932
|
const _log = getLogger();
|
|
7818
8933
|
const cleanupShutdown = registerShutdownHandlers({
|
|
@@ -7820,22 +8935,50 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
7820
8935
|
getRunState: () => loadRunState(projectRoot)
|
|
7821
8936
|
});
|
|
7822
8937
|
try {
|
|
8938
|
+
const sandboxMode = resolveSandboxMode(config.sandbox, flags);
|
|
8939
|
+
let sandboxed = false;
|
|
8940
|
+
if (sandboxMode !== "disabled") {
|
|
8941
|
+
const status = await detectSandboxSupport();
|
|
8942
|
+
if (!status.available) {
|
|
8943
|
+
if (sandboxMode === "required") {
|
|
8944
|
+
process.stderr.write(`${red("✗")} Docker sandbox required but not available: ${status.reason}
|
|
8945
|
+
`);
|
|
8946
|
+
process.stderr.write(` Install Docker Desktop 4.58+ or remove --sandbox=require to continue.
|
|
8947
|
+
`);
|
|
8948
|
+
process.exit(1);
|
|
8949
|
+
}
|
|
8950
|
+
process.stderr.write(`${yellow("⚠")} Docker sandbox not available: ${status.reason}. Running unsandboxed.
|
|
8951
|
+
`);
|
|
8952
|
+
} else {
|
|
8953
|
+
sandboxed = true;
|
|
8954
|
+
}
|
|
8955
|
+
} else if (flags.noSandbox) {
|
|
8956
|
+
process.stderr.write(`${yellow("⚠")} Running without sandbox. The AI agent will have unrestricted access to your filesystem, network, and environment variables.
|
|
8957
|
+
`);
|
|
8958
|
+
}
|
|
8959
|
+
if (sandboxed) {
|
|
8960
|
+
const staleCleaned = await cleanupStaleSandboxes();
|
|
8961
|
+
if (staleCleaned > 0) {
|
|
8962
|
+
process.stderr.write(` ${dim(`Cleaned up ${staleCleaned} stale sandbox${staleCleaned === 1 ? "" : "es"}.`)}
|
|
8963
|
+
`);
|
|
8964
|
+
}
|
|
8965
|
+
}
|
|
7823
8966
|
if (flags.resume) {
|
|
7824
|
-
return handleResume(projectRoot, config);
|
|
8967
|
+
return handleResume(projectRoot, config, sandboxed);
|
|
7825
8968
|
}
|
|
7826
8969
|
const issueNumbers = args.filter((a) => /^\d+$/.test(a)).map(Number);
|
|
7827
8970
|
if (issueNumbers.length === 0) {
|
|
7828
|
-
return handleSprintRun(projectRoot, config, flags);
|
|
8971
|
+
return handleSprintRun(projectRoot, config, flags, sandboxed);
|
|
7829
8972
|
}
|
|
7830
8973
|
if (issueNumbers.length === 1) {
|
|
7831
|
-
return handleSingleIssue(projectRoot, config, issueNumbers[0], flags);
|
|
8974
|
+
return handleSingleIssue(projectRoot, config, issueNumbers[0], flags, sandboxed);
|
|
7832
8975
|
}
|
|
7833
|
-
return handleParallelRun(projectRoot, config, issueNumbers, flags);
|
|
8976
|
+
return handleParallelRun(projectRoot, config, issueNumbers, flags, sandboxed);
|
|
7834
8977
|
} finally {
|
|
7835
8978
|
cleanupShutdown();
|
|
7836
8979
|
}
|
|
7837
8980
|
}
|
|
7838
|
-
async function handleSprintRun(projectRoot, config, flags) {
|
|
8981
|
+
async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
7839
8982
|
const log = getLogger();
|
|
7840
8983
|
if (!config.sprint.active) {
|
|
7841
8984
|
process.stderr.write(`${red("✗")} No active sprint. Set one with: ${bold("locus sprint active <name>")}
|
|
@@ -7898,7 +9041,7 @@ ${yellow("⚠")} A sprint run is already in progress.
|
|
|
7898
9041
|
}
|
|
7899
9042
|
if (!flags.dryRun) {
|
|
7900
9043
|
try {
|
|
7901
|
-
|
|
9044
|
+
execSync15(`git checkout -B ${branchName}`, {
|
|
7902
9045
|
cwd: projectRoot,
|
|
7903
9046
|
encoding: "utf-8",
|
|
7904
9047
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7948,7 +9091,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
|
|
|
7948
9091
|
let sprintContext;
|
|
7949
9092
|
if (i > 0 && !flags.dryRun) {
|
|
7950
9093
|
try {
|
|
7951
|
-
sprintContext =
|
|
9094
|
+
sprintContext = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
7952
9095
|
cwd: projectRoot,
|
|
7953
9096
|
encoding: "utf-8",
|
|
7954
9097
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7967,7 +9110,9 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
7967
9110
|
model: flags.model ?? config.ai.model,
|
|
7968
9111
|
dryRun: flags.dryRun,
|
|
7969
9112
|
sprintContext,
|
|
7970
|
-
skipPR: true
|
|
9113
|
+
skipPR: true,
|
|
9114
|
+
sandboxed,
|
|
9115
|
+
sandboxName: config.sandbox.name
|
|
7971
9116
|
});
|
|
7972
9117
|
if (result.success) {
|
|
7973
9118
|
if (!flags.dryRun) {
|
|
@@ -8007,7 +9152,7 @@ ${bold("Summary:")}
|
|
|
8007
9152
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
8008
9153
|
if (prNumber !== undefined) {
|
|
8009
9154
|
try {
|
|
8010
|
-
|
|
9155
|
+
execSync15(`git checkout ${config.agent.baseBranch}`, {
|
|
8011
9156
|
cwd: projectRoot,
|
|
8012
9157
|
encoding: "utf-8",
|
|
8013
9158
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8021,7 +9166,7 @@ ${bold("Summary:")}
|
|
|
8021
9166
|
clearRunState(projectRoot);
|
|
8022
9167
|
}
|
|
8023
9168
|
}
|
|
8024
|
-
async function handleSingleIssue(projectRoot, config, issueNumber, flags) {
|
|
9169
|
+
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
8025
9170
|
let isSprintIssue = false;
|
|
8026
9171
|
try {
|
|
8027
9172
|
const issue = getIssue(issueNumber, { cwd: projectRoot });
|
|
@@ -8036,7 +9181,9 @@ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, n
|
|
|
8036
9181
|
issueNumber,
|
|
8037
9182
|
provider: config.ai.provider,
|
|
8038
9183
|
model: flags.model ?? config.ai.model,
|
|
8039
|
-
dryRun: flags.dryRun
|
|
9184
|
+
dryRun: flags.dryRun,
|
|
9185
|
+
sandboxed,
|
|
9186
|
+
sandboxName: config.sandbox.name
|
|
8040
9187
|
});
|
|
8041
9188
|
return;
|
|
8042
9189
|
}
|
|
@@ -8065,7 +9212,9 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
8065
9212
|
worktreePath,
|
|
8066
9213
|
provider: config.ai.provider,
|
|
8067
9214
|
model: flags.model ?? config.ai.model,
|
|
8068
|
-
dryRun: flags.dryRun
|
|
9215
|
+
dryRun: flags.dryRun,
|
|
9216
|
+
sandboxed,
|
|
9217
|
+
sandboxName: config.sandbox.name
|
|
8069
9218
|
});
|
|
8070
9219
|
if (worktreePath && !flags.dryRun) {
|
|
8071
9220
|
if (result.success) {
|
|
@@ -8078,7 +9227,7 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
8078
9227
|
}
|
|
8079
9228
|
}
|
|
8080
9229
|
}
|
|
8081
|
-
async function handleParallelRun(projectRoot, config, issueNumbers, flags) {
|
|
9230
|
+
async function handleParallelRun(projectRoot, config, issueNumbers, flags, sandboxed) {
|
|
8082
9231
|
const log = getLogger();
|
|
8083
9232
|
const maxConcurrent = config.agent.maxParallel;
|
|
8084
9233
|
process.stderr.write(`
|
|
@@ -8133,7 +9282,9 @@ ${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxCon
|
|
|
8133
9282
|
worktreePath,
|
|
8134
9283
|
provider: config.ai.provider,
|
|
8135
9284
|
model: flags.model ?? config.ai.model,
|
|
8136
|
-
dryRun: flags.dryRun
|
|
9285
|
+
dryRun: flags.dryRun,
|
|
9286
|
+
sandboxed,
|
|
9287
|
+
sandboxName: config.sandbox.name
|
|
8137
9288
|
});
|
|
8138
9289
|
if (result.success) {
|
|
8139
9290
|
markTaskDone(state, issueNumber, result.prNumber);
|
|
@@ -8145,9 +9296,19 @@ ${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxCon
|
|
|
8145
9296
|
markTaskFailed(state, issueNumber, result.error ?? "Unknown error");
|
|
8146
9297
|
}
|
|
8147
9298
|
saveRunState(projectRoot, state);
|
|
8148
|
-
|
|
9299
|
+
return { issue: issueNumber, success: result.success };
|
|
8149
9300
|
});
|
|
8150
|
-
await Promise.
|
|
9301
|
+
const settled = await Promise.allSettled(promises);
|
|
9302
|
+
for (const outcome of settled) {
|
|
9303
|
+
if (outcome.status === "fulfilled") {
|
|
9304
|
+
results.push(outcome.value);
|
|
9305
|
+
} else {
|
|
9306
|
+
const idx = settled.indexOf(outcome);
|
|
9307
|
+
const issueNumber = batch[idx];
|
|
9308
|
+
log.warn(`Parallel task #${issueNumber} threw: ${outcome.reason}`);
|
|
9309
|
+
results.push({ issue: issueNumber, success: false });
|
|
9310
|
+
}
|
|
9311
|
+
}
|
|
8151
9312
|
}
|
|
8152
9313
|
const succeeded = results.filter((r) => r.success).length;
|
|
8153
9314
|
const failed = results.filter((r) => !r.success).length;
|
|
@@ -8172,7 +9333,7 @@ ${yellow("⚠")} Failed worktrees preserved for debugging:
|
|
|
8172
9333
|
clearRunState(projectRoot);
|
|
8173
9334
|
}
|
|
8174
9335
|
}
|
|
8175
|
-
async function handleResume(projectRoot, config) {
|
|
9336
|
+
async function handleResume(projectRoot, config, sandboxed) {
|
|
8176
9337
|
const state = loadRunState(projectRoot);
|
|
8177
9338
|
if (!state) {
|
|
8178
9339
|
process.stderr.write(`${red("✗")} No run state found. Nothing to resume.
|
|
@@ -8188,13 +9349,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
8188
9349
|
`);
|
|
8189
9350
|
if (state.type === "sprint" && state.branch) {
|
|
8190
9351
|
try {
|
|
8191
|
-
const currentBranch =
|
|
9352
|
+
const currentBranch = execSync15("git rev-parse --abbrev-ref HEAD", {
|
|
8192
9353
|
cwd: projectRoot,
|
|
8193
9354
|
encoding: "utf-8",
|
|
8194
9355
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8195
9356
|
}).trim();
|
|
8196
9357
|
if (currentBranch !== state.branch) {
|
|
8197
|
-
|
|
9358
|
+
execSync15(`git checkout ${state.branch}`, {
|
|
8198
9359
|
cwd: projectRoot,
|
|
8199
9360
|
encoding: "utf-8",
|
|
8200
9361
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8220,7 +9381,9 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
8220
9381
|
issueNumber: task.issue,
|
|
8221
9382
|
provider: config.ai.provider,
|
|
8222
9383
|
model: config.ai.model,
|
|
8223
|
-
skipPR: isSprintRun
|
|
9384
|
+
skipPR: isSprintRun,
|
|
9385
|
+
sandboxed,
|
|
9386
|
+
sandboxName: config.sandbox.name
|
|
8224
9387
|
});
|
|
8225
9388
|
if (result.success) {
|
|
8226
9389
|
if (isSprintRun) {
|
|
@@ -8255,7 +9418,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
|
|
|
8255
9418
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
8256
9419
|
if (prNumber !== undefined) {
|
|
8257
9420
|
try {
|
|
8258
|
-
|
|
9421
|
+
execSync15(`git checkout ${config.agent.baseBranch}`, {
|
|
8259
9422
|
cwd: projectRoot,
|
|
8260
9423
|
encoding: "utf-8",
|
|
8261
9424
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8286,14 +9449,14 @@ function getOrder2(issue) {
|
|
|
8286
9449
|
}
|
|
8287
9450
|
function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
8288
9451
|
try {
|
|
8289
|
-
const status =
|
|
9452
|
+
const status = execSync15("git status --porcelain", {
|
|
8290
9453
|
cwd: projectRoot,
|
|
8291
9454
|
encoding: "utf-8",
|
|
8292
9455
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8293
9456
|
}).trim();
|
|
8294
9457
|
if (!status)
|
|
8295
9458
|
return;
|
|
8296
|
-
|
|
9459
|
+
execSync15("git add -A", {
|
|
8297
9460
|
cwd: projectRoot,
|
|
8298
9461
|
encoding: "utf-8",
|
|
8299
9462
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8301,7 +9464,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
8301
9464
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
8302
9465
|
|
|
8303
9466
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
8304
|
-
|
|
9467
|
+
execSync15(`git commit -F -`, {
|
|
8305
9468
|
input: message,
|
|
8306
9469
|
cwd: projectRoot,
|
|
8307
9470
|
encoding: "utf-8",
|
|
@@ -8315,7 +9478,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
8315
9478
|
if (!config.agent.autoPR)
|
|
8316
9479
|
return;
|
|
8317
9480
|
try {
|
|
8318
|
-
const diff =
|
|
9481
|
+
const diff = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8319
9482
|
cwd: projectRoot,
|
|
8320
9483
|
encoding: "utf-8",
|
|
8321
9484
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8325,7 +9488,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
8325
9488
|
`);
|
|
8326
9489
|
return;
|
|
8327
9490
|
}
|
|
8328
|
-
|
|
9491
|
+
execSync15(`git push -u origin ${branchName}`, {
|
|
8329
9492
|
cwd: projectRoot,
|
|
8330
9493
|
encoding: "utf-8",
|
|
8331
9494
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8358,6 +9521,7 @@ var init_run = __esm(() => {
|
|
|
8358
9521
|
init_logger();
|
|
8359
9522
|
init_rate_limiter();
|
|
8360
9523
|
init_run_state();
|
|
9524
|
+
init_sandbox();
|
|
8361
9525
|
init_shutdown();
|
|
8362
9526
|
init_worktree();
|
|
8363
9527
|
init_progress();
|
|
@@ -8470,13 +9634,13 @@ __export(exports_plan, {
|
|
|
8470
9634
|
parsePlanArgs: () => parsePlanArgs
|
|
8471
9635
|
});
|
|
8472
9636
|
import {
|
|
8473
|
-
existsSync as
|
|
9637
|
+
existsSync as existsSync17,
|
|
8474
9638
|
mkdirSync as mkdirSync11,
|
|
8475
9639
|
readdirSync as readdirSync7,
|
|
8476
|
-
readFileSync as
|
|
9640
|
+
readFileSync as readFileSync13,
|
|
8477
9641
|
writeFileSync as writeFileSync9
|
|
8478
9642
|
} from "node:fs";
|
|
8479
|
-
import { join as
|
|
9643
|
+
import { join as join17 } from "node:path";
|
|
8480
9644
|
function printHelp() {
|
|
8481
9645
|
process.stderr.write(`
|
|
8482
9646
|
${bold("locus plan")} — AI-powered sprint planning
|
|
@@ -8507,28 +9671,28 @@ function normalizeSprintName(name) {
|
|
|
8507
9671
|
return name.trim().toLowerCase();
|
|
8508
9672
|
}
|
|
8509
9673
|
function getPlansDir(projectRoot) {
|
|
8510
|
-
return
|
|
9674
|
+
return join17(projectRoot, ".locus", "plans");
|
|
8511
9675
|
}
|
|
8512
9676
|
function ensurePlansDir(projectRoot) {
|
|
8513
9677
|
const dir = getPlansDir(projectRoot);
|
|
8514
|
-
if (!
|
|
9678
|
+
if (!existsSync17(dir)) {
|
|
8515
9679
|
mkdirSync11(dir, { recursive: true });
|
|
8516
9680
|
}
|
|
8517
9681
|
return dir;
|
|
8518
9682
|
}
|
|
8519
9683
|
function generateId() {
|
|
8520
|
-
return `${Math.random().toString(36).slice(2, 8)}`;
|
|
9684
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8521
9685
|
}
|
|
8522
9686
|
function loadPlanFile(projectRoot, id) {
|
|
8523
9687
|
const dir = getPlansDir(projectRoot);
|
|
8524
|
-
if (!
|
|
9688
|
+
if (!existsSync17(dir))
|
|
8525
9689
|
return null;
|
|
8526
9690
|
const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
|
|
8527
9691
|
const match = files.find((f) => f.startsWith(id));
|
|
8528
9692
|
if (!match)
|
|
8529
9693
|
return null;
|
|
8530
9694
|
try {
|
|
8531
|
-
const content =
|
|
9695
|
+
const content = readFileSync13(join17(dir, match), "utf-8");
|
|
8532
9696
|
return JSON.parse(content);
|
|
8533
9697
|
} catch {
|
|
8534
9698
|
return null;
|
|
@@ -8574,7 +9738,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
8574
9738
|
}
|
|
8575
9739
|
function handleListPlans(projectRoot) {
|
|
8576
9740
|
const dir = getPlansDir(projectRoot);
|
|
8577
|
-
if (!
|
|
9741
|
+
if (!existsSync17(dir)) {
|
|
8578
9742
|
process.stderr.write(`${dim("No saved plans yet.")}
|
|
8579
9743
|
`);
|
|
8580
9744
|
return;
|
|
@@ -8592,7 +9756,7 @@ ${bold("Saved Plans:")}
|
|
|
8592
9756
|
for (const file of files) {
|
|
8593
9757
|
const id = file.replace(".json", "");
|
|
8594
9758
|
try {
|
|
8595
|
-
const content =
|
|
9759
|
+
const content = readFileSync13(join17(dir, file), "utf-8");
|
|
8596
9760
|
const plan = JSON.parse(content);
|
|
8597
9761
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
8598
9762
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -8703,7 +9867,7 @@ ${bold("Approving plan:")}
|
|
|
8703
9867
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
8704
9868
|
const id = generateId();
|
|
8705
9869
|
const plansDir = ensurePlansDir(projectRoot);
|
|
8706
|
-
const planPath =
|
|
9870
|
+
const planPath = join17(plansDir, `${id}.json`);
|
|
8707
9871
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
8708
9872
|
const displayDirective = directive;
|
|
8709
9873
|
process.stderr.write(`
|
|
@@ -8721,7 +9885,9 @@ ${bold("Planning:")} ${cyan(displayDirective)}
|
|
|
8721
9885
|
provider: config.ai.provider,
|
|
8722
9886
|
model: flags.model ?? config.ai.model,
|
|
8723
9887
|
cwd: projectRoot,
|
|
8724
|
-
activity: "planning"
|
|
9888
|
+
activity: "planning",
|
|
9889
|
+
sandboxed: config.sandbox.enabled,
|
|
9890
|
+
sandboxName: config.sandbox.name
|
|
8725
9891
|
});
|
|
8726
9892
|
if (aiResult.interrupted) {
|
|
8727
9893
|
process.stderr.write(`
|
|
@@ -8735,7 +9901,7 @@ ${red("✗")} Planning failed: ${aiResult.error}
|
|
|
8735
9901
|
`);
|
|
8736
9902
|
return;
|
|
8737
9903
|
}
|
|
8738
|
-
if (!
|
|
9904
|
+
if (!existsSync17(planPath)) {
|
|
8739
9905
|
process.stderr.write(`
|
|
8740
9906
|
${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
|
|
8741
9907
|
`);
|
|
@@ -8745,7 +9911,7 @@ ${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
|
|
|
8745
9911
|
}
|
|
8746
9912
|
let plan;
|
|
8747
9913
|
try {
|
|
8748
|
-
const content =
|
|
9914
|
+
const content = readFileSync13(planPath, "utf-8");
|
|
8749
9915
|
plan = JSON.parse(content);
|
|
8750
9916
|
} catch {
|
|
8751
9917
|
process.stderr.write(`
|
|
@@ -8829,7 +9995,9 @@ Start with foundational/setup tasks, then core features, then integration/testin
|
|
|
8829
9995
|
model: flags.model ?? config.ai.model,
|
|
8830
9996
|
cwd: projectRoot,
|
|
8831
9997
|
activity: "issue ordering",
|
|
8832
|
-
silent: true
|
|
9998
|
+
silent: true,
|
|
9999
|
+
sandboxed: config.sandbox.enabled,
|
|
10000
|
+
sandboxName: config.sandbox.name
|
|
8833
10001
|
});
|
|
8834
10002
|
if (aiResult.interrupted) {
|
|
8835
10003
|
process.stderr.write(`
|
|
@@ -8900,16 +10068,16 @@ function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, pla
|
|
|
8900
10068
|
parts.push(`SPRINT: ${sprintName}`);
|
|
8901
10069
|
}
|
|
8902
10070
|
parts.push("");
|
|
8903
|
-
const locusPath =
|
|
8904
|
-
if (
|
|
8905
|
-
const content =
|
|
10071
|
+
const locusPath = join17(projectRoot, "LOCUS.md");
|
|
10072
|
+
if (existsSync17(locusPath)) {
|
|
10073
|
+
const content = readFileSync13(locusPath, "utf-8");
|
|
8906
10074
|
parts.push("PROJECT CONTEXT (LOCUS.md):");
|
|
8907
10075
|
parts.push(content.slice(0, 3000));
|
|
8908
10076
|
parts.push("");
|
|
8909
10077
|
}
|
|
8910
|
-
const learningsPath =
|
|
8911
|
-
if (
|
|
8912
|
-
const content =
|
|
10078
|
+
const learningsPath = join17(projectRoot, ".locus", "LEARNINGS.md");
|
|
10079
|
+
if (existsSync17(learningsPath)) {
|
|
10080
|
+
const content = readFileSync13(learningsPath, "utf-8");
|
|
8913
10081
|
parts.push("PAST LEARNINGS:");
|
|
8914
10082
|
parts.push(content.slice(0, 2000));
|
|
8915
10083
|
parts.push("");
|
|
@@ -9085,9 +10253,9 @@ var exports_review = {};
|
|
|
9085
10253
|
__export(exports_review, {
|
|
9086
10254
|
reviewCommand: () => reviewCommand
|
|
9087
10255
|
});
|
|
9088
|
-
import { execSync as
|
|
9089
|
-
import { existsSync as
|
|
9090
|
-
import { join as
|
|
10256
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
10257
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
|
|
10258
|
+
import { join as join18 } from "node:path";
|
|
9091
10259
|
function printHelp2() {
|
|
9092
10260
|
process.stderr.write(`
|
|
9093
10261
|
${bold("locus review")} — AI-powered code review
|
|
@@ -9163,7 +10331,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
|
|
|
9163
10331
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
9164
10332
|
let prInfo;
|
|
9165
10333
|
try {
|
|
9166
|
-
const result =
|
|
10334
|
+
const result = execSync16(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
9167
10335
|
const raw = JSON.parse(result);
|
|
9168
10336
|
prInfo = {
|
|
9169
10337
|
number: raw.number,
|
|
@@ -9206,7 +10374,9 @@ async function reviewPR(projectRoot, config, pr, focus, flags) {
|
|
|
9206
10374
|
provider: config.ai.provider,
|
|
9207
10375
|
model: flags.model ?? config.ai.model,
|
|
9208
10376
|
cwd: projectRoot,
|
|
9209
|
-
activity: `PR #${pr.number}
|
|
10377
|
+
activity: `PR #${pr.number}`,
|
|
10378
|
+
sandboxed: config.sandbox.enabled,
|
|
10379
|
+
sandboxName: config.sandbox.name
|
|
9210
10380
|
});
|
|
9211
10381
|
if (aiResult.interrupted) {
|
|
9212
10382
|
process.stderr.write(` ${yellow("⚡")} Review interrupted.
|
|
@@ -9227,7 +10397,7 @@ ${output.slice(0, 60000)}
|
|
|
9227
10397
|
|
|
9228
10398
|
---
|
|
9229
10399
|
_Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
|
|
9230
|
-
|
|
10400
|
+
execSync16(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
9231
10401
|
process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
|
|
9232
10402
|
`);
|
|
9233
10403
|
} catch (e) {
|
|
@@ -9244,9 +10414,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
9244
10414
|
const parts = [];
|
|
9245
10415
|
parts.push(`You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.`);
|
|
9246
10416
|
parts.push("");
|
|
9247
|
-
const locusPath =
|
|
9248
|
-
if (
|
|
9249
|
-
const content =
|
|
10417
|
+
const locusPath = join18(projectRoot, "LOCUS.md");
|
|
10418
|
+
if (existsSync18(locusPath)) {
|
|
10419
|
+
const content = readFileSync14(locusPath, "utf-8");
|
|
9250
10420
|
parts.push("PROJECT CONTEXT:");
|
|
9251
10421
|
parts.push(content.slice(0, 2000));
|
|
9252
10422
|
parts.push("");
|
|
@@ -9298,7 +10468,7 @@ var exports_iterate = {};
|
|
|
9298
10468
|
__export(exports_iterate, {
|
|
9299
10469
|
iterateCommand: () => iterateCommand
|
|
9300
10470
|
});
|
|
9301
|
-
import { execSync as
|
|
10471
|
+
import { execSync as execSync17 } from "node:child_process";
|
|
9302
10472
|
function printHelp3() {
|
|
9303
10473
|
process.stderr.write(`
|
|
9304
10474
|
${bold("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -9508,12 +10678,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
|
|
|
9508
10678
|
}
|
|
9509
10679
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
9510
10680
|
try {
|
|
9511
|
-
const result =
|
|
10681
|
+
const result = execSync17(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
9512
10682
|
const parsed = JSON.parse(result);
|
|
9513
10683
|
if (parsed.length > 0) {
|
|
9514
10684
|
return parsed[0].number;
|
|
9515
10685
|
}
|
|
9516
|
-
const branchResult =
|
|
10686
|
+
const branchResult = execSync17(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
9517
10687
|
const branchParsed = JSON.parse(branchResult);
|
|
9518
10688
|
if (branchParsed.length > 0) {
|
|
9519
10689
|
return branchParsed[0].number;
|
|
@@ -9548,14 +10718,14 @@ __export(exports_discuss, {
|
|
|
9548
10718
|
discussCommand: () => discussCommand
|
|
9549
10719
|
});
|
|
9550
10720
|
import {
|
|
9551
|
-
existsSync as
|
|
10721
|
+
existsSync as existsSync19,
|
|
9552
10722
|
mkdirSync as mkdirSync12,
|
|
9553
10723
|
readdirSync as readdirSync8,
|
|
9554
|
-
readFileSync as
|
|
10724
|
+
readFileSync as readFileSync15,
|
|
9555
10725
|
unlinkSync as unlinkSync5,
|
|
9556
10726
|
writeFileSync as writeFileSync10
|
|
9557
10727
|
} from "node:fs";
|
|
9558
|
-
import { join as
|
|
10728
|
+
import { join as join19 } from "node:path";
|
|
9559
10729
|
function printHelp4() {
|
|
9560
10730
|
process.stderr.write(`
|
|
9561
10731
|
${bold("locus discuss")} — AI-powered architectural discussions
|
|
@@ -9577,11 +10747,11 @@ ${bold("Examples:")}
|
|
|
9577
10747
|
`);
|
|
9578
10748
|
}
|
|
9579
10749
|
function getDiscussionsDir(projectRoot) {
|
|
9580
|
-
return
|
|
10750
|
+
return join19(projectRoot, ".locus", "discussions");
|
|
9581
10751
|
}
|
|
9582
10752
|
function ensureDiscussionsDir(projectRoot) {
|
|
9583
10753
|
const dir = getDiscussionsDir(projectRoot);
|
|
9584
|
-
if (!
|
|
10754
|
+
if (!existsSync19(dir)) {
|
|
9585
10755
|
mkdirSync12(dir, { recursive: true });
|
|
9586
10756
|
}
|
|
9587
10757
|
return dir;
|
|
@@ -9616,7 +10786,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
9616
10786
|
}
|
|
9617
10787
|
function listDiscussions(projectRoot) {
|
|
9618
10788
|
const dir = getDiscussionsDir(projectRoot);
|
|
9619
|
-
if (!
|
|
10789
|
+
if (!existsSync19(dir)) {
|
|
9620
10790
|
process.stderr.write(`${dim("No discussions yet.")}
|
|
9621
10791
|
`);
|
|
9622
10792
|
return;
|
|
@@ -9633,7 +10803,7 @@ ${bold("Discussions:")}
|
|
|
9633
10803
|
`);
|
|
9634
10804
|
for (const file of files) {
|
|
9635
10805
|
const id = file.replace(".md", "");
|
|
9636
|
-
const content =
|
|
10806
|
+
const content = readFileSync15(join19(dir, file), "utf-8");
|
|
9637
10807
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
9638
10808
|
const title = titleMatch ? titleMatch[1] : id;
|
|
9639
10809
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -9651,7 +10821,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
9651
10821
|
return;
|
|
9652
10822
|
}
|
|
9653
10823
|
const dir = getDiscussionsDir(projectRoot);
|
|
9654
|
-
if (!
|
|
10824
|
+
if (!existsSync19(dir)) {
|
|
9655
10825
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
9656
10826
|
`);
|
|
9657
10827
|
return;
|
|
@@ -9663,7 +10833,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
9663
10833
|
`);
|
|
9664
10834
|
return;
|
|
9665
10835
|
}
|
|
9666
|
-
const content =
|
|
10836
|
+
const content = readFileSync15(join19(dir, match), "utf-8");
|
|
9667
10837
|
process.stdout.write(`${content}
|
|
9668
10838
|
`);
|
|
9669
10839
|
}
|
|
@@ -9674,7 +10844,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
9674
10844
|
return;
|
|
9675
10845
|
}
|
|
9676
10846
|
const dir = getDiscussionsDir(projectRoot);
|
|
9677
|
-
if (!
|
|
10847
|
+
if (!existsSync19(dir)) {
|
|
9678
10848
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
9679
10849
|
`);
|
|
9680
10850
|
return;
|
|
@@ -9686,7 +10856,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
9686
10856
|
`);
|
|
9687
10857
|
return;
|
|
9688
10858
|
}
|
|
9689
|
-
unlinkSync5(
|
|
10859
|
+
unlinkSync5(join19(dir, match));
|
|
9690
10860
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
9691
10861
|
`);
|
|
9692
10862
|
}
|
|
@@ -9699,7 +10869,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
9699
10869
|
return;
|
|
9700
10870
|
}
|
|
9701
10871
|
const dir = getDiscussionsDir(projectRoot);
|
|
9702
|
-
if (!
|
|
10872
|
+
if (!existsSync19(dir)) {
|
|
9703
10873
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
9704
10874
|
`);
|
|
9705
10875
|
return;
|
|
@@ -9711,7 +10881,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
9711
10881
|
`);
|
|
9712
10882
|
return;
|
|
9713
10883
|
}
|
|
9714
|
-
const content =
|
|
10884
|
+
const content = readFileSync15(join19(dir, match), "utf-8");
|
|
9715
10885
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
9716
10886
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
9717
10887
|
await planCommand(projectRoot, [
|
|
@@ -9756,7 +10926,9 @@ ${bold("Discussion:")} ${cyan(topic)}
|
|
|
9756
10926
|
provider: config.ai.provider,
|
|
9757
10927
|
model: flags.model ?? config.ai.model,
|
|
9758
10928
|
cwd: projectRoot,
|
|
9759
|
-
activity: "discussion"
|
|
10929
|
+
activity: "discussion",
|
|
10930
|
+
sandboxed: config.sandbox.enabled,
|
|
10931
|
+
sandboxName: config.sandbox.name
|
|
9760
10932
|
});
|
|
9761
10933
|
if (aiResult.interrupted) {
|
|
9762
10934
|
process.stderr.write(`
|
|
@@ -9823,7 +10995,7 @@ ${turn.content}`;
|
|
|
9823
10995
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
9824
10996
|
].join(`
|
|
9825
10997
|
`);
|
|
9826
|
-
writeFileSync10(
|
|
10998
|
+
writeFileSync10(join19(dir, `${id}.md`), markdown, "utf-8");
|
|
9827
10999
|
process.stderr.write(`
|
|
9828
11000
|
${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
|
|
9829
11001
|
`);
|
|
@@ -9837,16 +11009,16 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
9837
11009
|
const parts = [];
|
|
9838
11010
|
parts.push(`You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.`);
|
|
9839
11011
|
parts.push("");
|
|
9840
|
-
const locusPath =
|
|
9841
|
-
if (
|
|
9842
|
-
const content =
|
|
11012
|
+
const locusPath = join19(projectRoot, "LOCUS.md");
|
|
11013
|
+
if (existsSync19(locusPath)) {
|
|
11014
|
+
const content = readFileSync15(locusPath, "utf-8");
|
|
9843
11015
|
parts.push("PROJECT CONTEXT:");
|
|
9844
11016
|
parts.push(content.slice(0, 3000));
|
|
9845
11017
|
parts.push("");
|
|
9846
11018
|
}
|
|
9847
|
-
const learningsPath =
|
|
9848
|
-
if (
|
|
9849
|
-
const content =
|
|
11019
|
+
const learningsPath = join19(projectRoot, ".locus", "LEARNINGS.md");
|
|
11020
|
+
if (existsSync19(learningsPath)) {
|
|
11021
|
+
const content = readFileSync15(learningsPath, "utf-8");
|
|
9850
11022
|
parts.push("PAST LEARNINGS:");
|
|
9851
11023
|
parts.push(content.slice(0, 2000));
|
|
9852
11024
|
parts.push("");
|
|
@@ -9905,8 +11077,8 @@ __export(exports_artifacts, {
|
|
|
9905
11077
|
formatDate: () => formatDate2,
|
|
9906
11078
|
artifactsCommand: () => artifactsCommand
|
|
9907
11079
|
});
|
|
9908
|
-
import { existsSync as
|
|
9909
|
-
import { join as
|
|
11080
|
+
import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
|
|
11081
|
+
import { join as join20 } from "node:path";
|
|
9910
11082
|
function printHelp5() {
|
|
9911
11083
|
process.stderr.write(`
|
|
9912
11084
|
${bold("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -9926,14 +11098,14 @@ ${dim("Artifact names support partial matching.")}
|
|
|
9926
11098
|
`);
|
|
9927
11099
|
}
|
|
9928
11100
|
function getArtifactsDir(projectRoot) {
|
|
9929
|
-
return
|
|
11101
|
+
return join20(projectRoot, ".locus", "artifacts");
|
|
9930
11102
|
}
|
|
9931
11103
|
function listArtifacts(projectRoot) {
|
|
9932
11104
|
const dir = getArtifactsDir(projectRoot);
|
|
9933
|
-
if (!
|
|
11105
|
+
if (!existsSync20(dir))
|
|
9934
11106
|
return [];
|
|
9935
11107
|
return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
9936
|
-
const filePath =
|
|
11108
|
+
const filePath = join20(dir, fileName);
|
|
9937
11109
|
const stat = statSync4(filePath);
|
|
9938
11110
|
return {
|
|
9939
11111
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -9946,12 +11118,12 @@ function listArtifacts(projectRoot) {
|
|
|
9946
11118
|
function readArtifact(projectRoot, name) {
|
|
9947
11119
|
const dir = getArtifactsDir(projectRoot);
|
|
9948
11120
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
9949
|
-
const filePath =
|
|
9950
|
-
if (!
|
|
11121
|
+
const filePath = join20(dir, fileName);
|
|
11122
|
+
if (!existsSync20(filePath))
|
|
9951
11123
|
return null;
|
|
9952
11124
|
const stat = statSync4(filePath);
|
|
9953
11125
|
return {
|
|
9954
|
-
content:
|
|
11126
|
+
content: readFileSync16(filePath, "utf-8"),
|
|
9955
11127
|
info: {
|
|
9956
11128
|
name: fileName.replace(/\.md$/, ""),
|
|
9957
11129
|
fileName,
|
|
@@ -10109,23 +11281,276 @@ var init_artifacts = __esm(() => {
|
|
|
10109
11281
|
init_terminal();
|
|
10110
11282
|
});
|
|
10111
11283
|
|
|
11284
|
+
// src/commands/sandbox.ts
|
|
11285
|
+
var exports_sandbox = {};
|
|
11286
|
+
__export(exports_sandbox, {
|
|
11287
|
+
sandboxCommand: () => sandboxCommand
|
|
11288
|
+
});
|
|
11289
|
+
import { execSync as execSync18, spawn as spawn6 } from "node:child_process";
|
|
11290
|
+
function printSandboxHelp() {
|
|
11291
|
+
process.stderr.write(`
|
|
11292
|
+
${bold("locus sandbox")} — Manage Docker sandbox lifecycle
|
|
11293
|
+
|
|
11294
|
+
${bold("Usage:")}
|
|
11295
|
+
locus sandbox ${dim("# Create sandbox and enable sandbox mode")}
|
|
11296
|
+
locus sandbox claude ${dim("# Run claude interactively (for login)")}
|
|
11297
|
+
locus sandbox codex ${dim("# Run codex interactively (for login)")}
|
|
11298
|
+
locus sandbox rm ${dim("# Destroy sandbox and disable sandbox mode")}
|
|
11299
|
+
locus sandbox status ${dim("# Show current sandbox state")}
|
|
11300
|
+
|
|
11301
|
+
${bold("Flow:")}
|
|
11302
|
+
1. ${cyan("locus sandbox")} Create the sandbox environment
|
|
11303
|
+
2. ${cyan("locus sandbox claude")} Login to Claude inside the sandbox
|
|
11304
|
+
3. ${cyan("locus exec")} All commands now run inside the sandbox
|
|
11305
|
+
|
|
11306
|
+
`);
|
|
11307
|
+
}
|
|
11308
|
+
async function sandboxCommand(projectRoot, args) {
|
|
11309
|
+
const subcommand = args[0] ?? "";
|
|
11310
|
+
switch (subcommand) {
|
|
11311
|
+
case "help":
|
|
11312
|
+
printSandboxHelp();
|
|
11313
|
+
return;
|
|
11314
|
+
case "claude":
|
|
11315
|
+
case "codex":
|
|
11316
|
+
return handleAgentLogin(projectRoot, subcommand);
|
|
11317
|
+
case "rm":
|
|
11318
|
+
return handleRemove(projectRoot);
|
|
11319
|
+
case "status":
|
|
11320
|
+
return handleStatus(projectRoot);
|
|
11321
|
+
case "":
|
|
11322
|
+
return handleCreate(projectRoot);
|
|
11323
|
+
default:
|
|
11324
|
+
process.stderr.write(`${red("✗")} Unknown sandbox subcommand: ${bold(subcommand)}
|
|
11325
|
+
`);
|
|
11326
|
+
process.stderr.write(` Available: ${cyan("claude")}, ${cyan("codex")}, ${cyan("rm")}, ${cyan("status")}
|
|
11327
|
+
`);
|
|
11328
|
+
}
|
|
11329
|
+
}
|
|
11330
|
+
async function handleCreate(projectRoot) {
|
|
11331
|
+
const config = loadConfig(projectRoot);
|
|
11332
|
+
if (config.sandbox.name) {
|
|
11333
|
+
const alive = isSandboxAlive(config.sandbox.name);
|
|
11334
|
+
if (alive) {
|
|
11335
|
+
process.stderr.write(`${green("✓")} Sandbox already exists: ${bold(config.sandbox.name)}
|
|
11336
|
+
`);
|
|
11337
|
+
process.stderr.write(` Run ${cyan("locus sandbox claude")} or ${cyan("locus sandbox codex")} to login.
|
|
11338
|
+
`);
|
|
11339
|
+
return;
|
|
11340
|
+
}
|
|
11341
|
+
process.stderr.write(`${yellow("⚠")} Previous sandbox ${dim(config.sandbox.name)} is no longer running. Creating a new one.
|
|
11342
|
+
`);
|
|
11343
|
+
}
|
|
11344
|
+
const status = await detectSandboxSupport();
|
|
11345
|
+
if (!status.available) {
|
|
11346
|
+
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
11347
|
+
`);
|
|
11348
|
+
process.stderr.write(` Install Docker Desktop 4.58+ with sandbox support.
|
|
11349
|
+
`);
|
|
11350
|
+
return;
|
|
11351
|
+
}
|
|
11352
|
+
const segment = projectRoot.split("/").pop() ?? "sandbox";
|
|
11353
|
+
const sandboxName = `locus-${segment}-${Date.now()}`;
|
|
11354
|
+
config.sandbox.enabled = true;
|
|
11355
|
+
config.sandbox.name = sandboxName;
|
|
11356
|
+
saveConfig(projectRoot, config);
|
|
11357
|
+
process.stderr.write(`${green("✓")} Sandbox name reserved: ${bold(sandboxName)}
|
|
11358
|
+
`);
|
|
11359
|
+
process.stderr.write(` Next: run ${cyan("locus sandbox claude")} or ${cyan("locus sandbox codex")} to create the sandbox and login.
|
|
11360
|
+
`);
|
|
11361
|
+
}
|
|
11362
|
+
async function handleAgentLogin(projectRoot, agent) {
|
|
11363
|
+
const config = loadConfig(projectRoot);
|
|
11364
|
+
if (!config.sandbox.name) {
|
|
11365
|
+
const status = await detectSandboxSupport();
|
|
11366
|
+
if (!status.available) {
|
|
11367
|
+
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
11368
|
+
`);
|
|
11369
|
+
process.stderr.write(` Install Docker Desktop 4.58+ with sandbox support.
|
|
11370
|
+
`);
|
|
11371
|
+
return;
|
|
11372
|
+
}
|
|
11373
|
+
const segment = projectRoot.split("/").pop() ?? "sandbox";
|
|
11374
|
+
config.sandbox.name = `locus-${segment}-${Date.now()}`;
|
|
11375
|
+
config.sandbox.enabled = true;
|
|
11376
|
+
saveConfig(projectRoot, config);
|
|
11377
|
+
}
|
|
11378
|
+
const sandboxName = config.sandbox.name;
|
|
11379
|
+
const alive = isSandboxAlive(sandboxName);
|
|
11380
|
+
let dockerArgs;
|
|
11381
|
+
if (alive) {
|
|
11382
|
+
if (agent === "codex") {
|
|
11383
|
+
await ensureCodexInSandbox(sandboxName);
|
|
11384
|
+
}
|
|
11385
|
+
process.stderr.write(`Connecting to sandbox ${dim(sandboxName)}...
|
|
11386
|
+
`);
|
|
11387
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11388
|
+
|
|
11389
|
+
`);
|
|
11390
|
+
dockerArgs = [
|
|
11391
|
+
"sandbox",
|
|
11392
|
+
"exec",
|
|
11393
|
+
"-it",
|
|
11394
|
+
"-w",
|
|
11395
|
+
projectRoot,
|
|
11396
|
+
sandboxName,
|
|
11397
|
+
agent
|
|
11398
|
+
];
|
|
11399
|
+
} else if (agent === "codex") {
|
|
11400
|
+
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11401
|
+
`);
|
|
11402
|
+
try {
|
|
11403
|
+
execSync18(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
11404
|
+
} catch {}
|
|
11405
|
+
if (!isSandboxAlive(sandboxName)) {
|
|
11406
|
+
process.stderr.write(`${red("✗")} Failed to create sandbox.
|
|
11407
|
+
`);
|
|
11408
|
+
return;
|
|
11409
|
+
}
|
|
11410
|
+
await ensureCodexInSandbox(sandboxName);
|
|
11411
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11412
|
+
|
|
11413
|
+
`);
|
|
11414
|
+
dockerArgs = [
|
|
11415
|
+
"sandbox",
|
|
11416
|
+
"exec",
|
|
11417
|
+
"-it",
|
|
11418
|
+
"-w",
|
|
11419
|
+
projectRoot,
|
|
11420
|
+
sandboxName,
|
|
11421
|
+
"codex"
|
|
11422
|
+
];
|
|
11423
|
+
} else {
|
|
11424
|
+
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11425
|
+
`);
|
|
11426
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11427
|
+
|
|
11428
|
+
`);
|
|
11429
|
+
dockerArgs = ["sandbox", "run", "--name", sandboxName, agent, projectRoot];
|
|
11430
|
+
}
|
|
11431
|
+
const child = spawn6("docker", dockerArgs, {
|
|
11432
|
+
stdio: "inherit"
|
|
11433
|
+
});
|
|
11434
|
+
await new Promise((resolve2) => {
|
|
11435
|
+
child.on("close", async (code) => {
|
|
11436
|
+
await enforceSandboxIgnore(sandboxName, projectRoot);
|
|
11437
|
+
if (code === 0) {
|
|
11438
|
+
process.stderr.write(`
|
|
11439
|
+
${green("✓")} ${agent} session ended. Auth should now be persisted in the sandbox.
|
|
11440
|
+
`);
|
|
11441
|
+
} else {
|
|
11442
|
+
process.stderr.write(`
|
|
11443
|
+
${yellow("⚠")} ${agent} exited with code ${code}.
|
|
11444
|
+
`);
|
|
11445
|
+
}
|
|
11446
|
+
resolve2();
|
|
11447
|
+
});
|
|
11448
|
+
child.on("error", (err) => {
|
|
11449
|
+
process.stderr.write(`${red("✗")} Failed to start ${agent}: ${err.message}
|
|
11450
|
+
`);
|
|
11451
|
+
resolve2();
|
|
11452
|
+
});
|
|
11453
|
+
});
|
|
11454
|
+
}
|
|
11455
|
+
function handleRemove(projectRoot) {
|
|
11456
|
+
const config = loadConfig(projectRoot);
|
|
11457
|
+
if (!config.sandbox.name) {
|
|
11458
|
+
process.stderr.write(`${dim("No sandbox to remove.")}
|
|
11459
|
+
`);
|
|
11460
|
+
return;
|
|
11461
|
+
}
|
|
11462
|
+
const sandboxName = config.sandbox.name;
|
|
11463
|
+
process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
|
|
11464
|
+
`);
|
|
11465
|
+
try {
|
|
11466
|
+
execSync18(`docker sandbox rm ${sandboxName}`, {
|
|
11467
|
+
encoding: "utf-8",
|
|
11468
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11469
|
+
timeout: 15000
|
|
11470
|
+
});
|
|
11471
|
+
} catch {}
|
|
11472
|
+
config.sandbox.name = undefined;
|
|
11473
|
+
config.sandbox.enabled = false;
|
|
11474
|
+
saveConfig(projectRoot, config);
|
|
11475
|
+
process.stderr.write(`${green("✓")} Sandbox removed. Sandbox mode disabled.
|
|
11476
|
+
`);
|
|
11477
|
+
}
|
|
11478
|
+
function handleStatus(projectRoot) {
|
|
11479
|
+
const config = loadConfig(projectRoot);
|
|
11480
|
+
process.stderr.write(`
|
|
11481
|
+
${bold("Sandbox Status")}
|
|
11482
|
+
|
|
11483
|
+
`);
|
|
11484
|
+
process.stderr.write(` ${dim("Enabled:")} ${config.sandbox.enabled ? green("yes") : red("no")}
|
|
11485
|
+
`);
|
|
11486
|
+
process.stderr.write(` ${dim("Name:")} ${config.sandbox.name ? bold(config.sandbox.name) : dim("(none)")}
|
|
11487
|
+
`);
|
|
11488
|
+
if (config.sandbox.name) {
|
|
11489
|
+
const alive = isSandboxAlive(config.sandbox.name);
|
|
11490
|
+
process.stderr.write(` ${dim("Running:")} ${alive ? green("yes") : red("no")}
|
|
11491
|
+
`);
|
|
11492
|
+
if (!alive) {
|
|
11493
|
+
process.stderr.write(`
|
|
11494
|
+
${yellow("⚠")} Sandbox is not running. Run ${bold("locus sandbox")} to create a new one.
|
|
11495
|
+
`);
|
|
11496
|
+
}
|
|
11497
|
+
}
|
|
11498
|
+
process.stderr.write(`
|
|
11499
|
+
`);
|
|
11500
|
+
}
|
|
11501
|
+
async function ensureCodexInSandbox(sandboxName) {
|
|
11502
|
+
try {
|
|
11503
|
+
execSync18(`docker sandbox exec ${sandboxName} which codex`, {
|
|
11504
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11505
|
+
timeout: 5000
|
|
11506
|
+
});
|
|
11507
|
+
} catch {
|
|
11508
|
+
process.stderr.write(`Installing codex in sandbox...
|
|
11509
|
+
`);
|
|
11510
|
+
try {
|
|
11511
|
+
execSync18(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
11512
|
+
} catch {
|
|
11513
|
+
process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
|
|
11514
|
+
`);
|
|
11515
|
+
}
|
|
11516
|
+
}
|
|
11517
|
+
}
|
|
11518
|
+
function isSandboxAlive(name) {
|
|
11519
|
+
try {
|
|
11520
|
+
const output = execSync18("docker sandbox ls", {
|
|
11521
|
+
encoding: "utf-8",
|
|
11522
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11523
|
+
timeout: 5000
|
|
11524
|
+
});
|
|
11525
|
+
return output.includes(name);
|
|
11526
|
+
} catch {
|
|
11527
|
+
return false;
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11530
|
+
var init_sandbox2 = __esm(() => {
|
|
11531
|
+
init_config();
|
|
11532
|
+
init_sandbox();
|
|
11533
|
+
init_sandbox_ignore();
|
|
11534
|
+
init_terminal();
|
|
11535
|
+
});
|
|
11536
|
+
|
|
10112
11537
|
// src/cli.ts
|
|
10113
11538
|
init_config();
|
|
10114
11539
|
init_context();
|
|
10115
11540
|
init_logger();
|
|
10116
11541
|
init_rate_limiter();
|
|
10117
11542
|
init_terminal();
|
|
10118
|
-
import { existsSync as
|
|
10119
|
-
import { join as
|
|
11543
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17 } from "node:fs";
|
|
11544
|
+
import { join as join21 } from "node:path";
|
|
10120
11545
|
import { fileURLToPath } from "node:url";
|
|
10121
11546
|
function getCliVersion() {
|
|
10122
11547
|
const fallbackVersion = "0.0.0";
|
|
10123
|
-
const packageJsonPath =
|
|
10124
|
-
if (!
|
|
11548
|
+
const packageJsonPath = join21(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
11549
|
+
if (!existsSync21(packageJsonPath)) {
|
|
10125
11550
|
return fallbackVersion;
|
|
10126
11551
|
}
|
|
10127
11552
|
try {
|
|
10128
|
-
const parsed = JSON.parse(
|
|
11553
|
+
const parsed = JSON.parse(readFileSync17(packageJsonPath, "utf-8"));
|
|
10129
11554
|
return parsed.version ?? fallbackVersion;
|
|
10130
11555
|
} catch {
|
|
10131
11556
|
return fallbackVersion;
|
|
@@ -10145,7 +11570,8 @@ function parseArgs(argv) {
|
|
|
10145
11570
|
dryRun: false,
|
|
10146
11571
|
check: false,
|
|
10147
11572
|
upgrade: false,
|
|
10148
|
-
list: false
|
|
11573
|
+
list: false,
|
|
11574
|
+
noSandbox: false
|
|
10149
11575
|
};
|
|
10150
11576
|
const positional = [];
|
|
10151
11577
|
let i = 0;
|
|
@@ -10222,7 +11648,14 @@ function parseArgs(argv) {
|
|
|
10222
11648
|
case "--target-version":
|
|
10223
11649
|
flags.targetVersion = rawArgs[++i];
|
|
10224
11650
|
break;
|
|
11651
|
+
case "--no-sandbox":
|
|
11652
|
+
flags.noSandbox = true;
|
|
11653
|
+
break;
|
|
10225
11654
|
default:
|
|
11655
|
+
if (arg.startsWith("--sandbox=")) {
|
|
11656
|
+
flags.sandbox = arg.slice("--sandbox=".length);
|
|
11657
|
+
break;
|
|
11658
|
+
}
|
|
10226
11659
|
positional.push(arg);
|
|
10227
11660
|
}
|
|
10228
11661
|
i++;
|
|
@@ -10256,6 +11689,7 @@ ${bold("Commands:")}
|
|
|
10256
11689
|
${cyan("uninstall")} Remove an installed package
|
|
10257
11690
|
${cyan("packages")} Manage installed packages (list, outdated)
|
|
10258
11691
|
${cyan("pkg")} ${dim("<name> [cmd]")} Run a command from an installed package
|
|
11692
|
+
${cyan("sandbox")} Manage Docker sandbox lifecycle
|
|
10259
11693
|
${cyan("upgrade")} Check for and install updates
|
|
10260
11694
|
|
|
10261
11695
|
${bold("Options:")}
|
|
@@ -10271,6 +11705,10 @@ ${bold("Examples:")}
|
|
|
10271
11705
|
locus plan approve <id> ${dim("# Create issues from saved plan")}
|
|
10272
11706
|
locus run ${dim("# Execute active sprint")}
|
|
10273
11707
|
locus run 42 43 ${dim("# Run issues in parallel")}
|
|
11708
|
+
locus run 42 --no-sandbox ${dim("# Run without sandbox")}
|
|
11709
|
+
locus run 42 --sandbox=require ${dim("# Require Docker sandbox")}
|
|
11710
|
+
locus sandbox ${dim("# Create Docker sandbox")}
|
|
11711
|
+
locus sandbox claude ${dim("# Login to Claude in sandbox")}
|
|
10274
11712
|
|
|
10275
11713
|
`);
|
|
10276
11714
|
}
|
|
@@ -10301,7 +11739,7 @@ async function main() {
|
|
|
10301
11739
|
try {
|
|
10302
11740
|
const root = getGitRoot(cwd);
|
|
10303
11741
|
if (isInitialized(root)) {
|
|
10304
|
-
logDir =
|
|
11742
|
+
logDir = join21(root, ".locus", "logs");
|
|
10305
11743
|
getRateLimiter(root);
|
|
10306
11744
|
}
|
|
10307
11745
|
} catch {}
|
|
@@ -10375,7 +11813,6 @@ async function main() {
|
|
|
10375
11813
|
process.stderr.write(`${red("✗")} Not inside a git repository.
|
|
10376
11814
|
`);
|
|
10377
11815
|
process.exit(1);
|
|
10378
|
-
return;
|
|
10379
11816
|
}
|
|
10380
11817
|
if (!isInitialized(projectRoot)) {
|
|
10381
11818
|
process.stderr.write(`${red("✗")} Locus is not initialized in this project.
|
|
@@ -10383,7 +11820,6 @@ async function main() {
|
|
|
10383
11820
|
process.stderr.write(` Run: ${bold("locus init")}
|
|
10384
11821
|
`);
|
|
10385
11822
|
process.exit(1);
|
|
10386
|
-
return;
|
|
10387
11823
|
}
|
|
10388
11824
|
switch (command) {
|
|
10389
11825
|
case "config": {
|
|
@@ -10428,7 +11864,9 @@ async function main() {
|
|
|
10428
11864
|
await runCommand2(projectRoot, runArgs, {
|
|
10429
11865
|
resume: parsed.flags.resume,
|
|
10430
11866
|
dryRun: parsed.flags.dryRun,
|
|
10431
|
-
model: parsed.flags.model
|
|
11867
|
+
model: parsed.flags.model,
|
|
11868
|
+
sandbox: parsed.flags.sandbox,
|
|
11869
|
+
noSandbox: parsed.flags.noSandbox
|
|
10432
11870
|
});
|
|
10433
11871
|
break;
|
|
10434
11872
|
}
|
|
@@ -10478,6 +11916,12 @@ async function main() {
|
|
|
10478
11916
|
await artifactsCommand2(projectRoot, artifactsArgs);
|
|
10479
11917
|
break;
|
|
10480
11918
|
}
|
|
11919
|
+
case "sandbox": {
|
|
11920
|
+
const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(), exports_sandbox));
|
|
11921
|
+
const sandboxArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
11922
|
+
await sandboxCommand2(projectRoot, sandboxArgs);
|
|
11923
|
+
break;
|
|
11924
|
+
}
|
|
10481
11925
|
case "upgrade": {
|
|
10482
11926
|
const { upgradeCommand: upgradeCommand2 } = await Promise.resolve().then(() => (init_upgrade(), exports_upgrade));
|
|
10483
11927
|
await upgradeCommand2(projectRoot, parsed.args, {
|