@locusai/cli 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/locus.js +492 -323
- package/package.json +1 -1
package/bin/locus.js
CHANGED
|
@@ -807,6 +807,14 @@ var init_rate_limiter = __esm(() => {
|
|
|
807
807
|
});
|
|
808
808
|
|
|
809
809
|
// src/display/progress.ts
|
|
810
|
+
var exports_progress = {};
|
|
811
|
+
__export(exports_progress, {
|
|
812
|
+
renderTaskStatus: () => renderTaskStatus,
|
|
813
|
+
progressBar: () => progressBar,
|
|
814
|
+
formatDuration: () => formatDuration,
|
|
815
|
+
createTimer: () => createTimer,
|
|
816
|
+
Spinner: () => Spinner
|
|
817
|
+
});
|
|
810
818
|
function progressBar(current, total, options = {}) {
|
|
811
819
|
const { width = 30, showPercent = true, showCount = true, label } = options;
|
|
812
820
|
const percent = total > 0 ? current / total : 0;
|
|
@@ -903,6 +911,149 @@ var init_progress = __esm(() => {
|
|
|
903
911
|
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
904
912
|
});
|
|
905
913
|
|
|
914
|
+
// src/core/sandbox.ts
|
|
915
|
+
var exports_sandbox = {};
|
|
916
|
+
__export(exports_sandbox, {
|
|
917
|
+
resolveSandboxMode: () => resolveSandboxMode,
|
|
918
|
+
displaySandboxWarning: () => displaySandboxWarning,
|
|
919
|
+
detectSandboxSupport: () => detectSandboxSupport
|
|
920
|
+
});
|
|
921
|
+
import { execFile } from "node:child_process";
|
|
922
|
+
import { createInterface } from "node:readline";
|
|
923
|
+
async function detectSandboxSupport() {
|
|
924
|
+
if (cachedStatus)
|
|
925
|
+
return cachedStatus;
|
|
926
|
+
const log = getLogger();
|
|
927
|
+
log.debug("Detecting Docker sandbox support...");
|
|
928
|
+
const status = await runDetection();
|
|
929
|
+
cachedStatus = status;
|
|
930
|
+
if (status.available) {
|
|
931
|
+
log.verbose("Docker sandbox support detected");
|
|
932
|
+
} else {
|
|
933
|
+
log.verbose(`Docker sandbox not available: ${status.reason}`);
|
|
934
|
+
}
|
|
935
|
+
return status;
|
|
936
|
+
}
|
|
937
|
+
function runDetection() {
|
|
938
|
+
return new Promise((resolve) => {
|
|
939
|
+
let settled = false;
|
|
940
|
+
const child = execFile("docker", ["sandbox", "ls"], { timeout: TIMEOUT_MS }, (error, _stdout, stderr) => {
|
|
941
|
+
if (settled)
|
|
942
|
+
return;
|
|
943
|
+
settled = true;
|
|
944
|
+
if (!error) {
|
|
945
|
+
resolve({ available: true });
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const code = error.code;
|
|
949
|
+
if (code === "ENOENT") {
|
|
950
|
+
resolve({ available: false, reason: "Docker is not installed" });
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (error.killed) {
|
|
954
|
+
resolve({ available: false, reason: "Docker is not responding" });
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const stderrStr = (stderr ?? "").toLowerCase();
|
|
958
|
+
if (stderrStr.includes("unknown") || stderrStr.includes("not a docker command") || stderrStr.includes("is not a docker command")) {
|
|
959
|
+
resolve({
|
|
960
|
+
available: false,
|
|
961
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
resolve({
|
|
966
|
+
available: false,
|
|
967
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
child.on?.("error", (err) => {
|
|
971
|
+
if (settled)
|
|
972
|
+
return;
|
|
973
|
+
settled = true;
|
|
974
|
+
if (err.code === "ENOENT") {
|
|
975
|
+
resolve({ available: false, reason: "Docker is not installed" });
|
|
976
|
+
} else {
|
|
977
|
+
resolve({
|
|
978
|
+
available: false,
|
|
979
|
+
reason: "Docker Desktop 4.58+ with sandbox support required"
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
function resolveSandboxMode(config, flags) {
|
|
986
|
+
if (flags.noSandbox) {
|
|
987
|
+
return "disabled";
|
|
988
|
+
}
|
|
989
|
+
if (flags.sandbox !== undefined) {
|
|
990
|
+
if (flags.sandbox === "require") {
|
|
991
|
+
return "required";
|
|
992
|
+
}
|
|
993
|
+
throw new Error(`Invalid --sandbox value: "${flags.sandbox}". Valid values: require`);
|
|
994
|
+
}
|
|
995
|
+
if (!config.enabled) {
|
|
996
|
+
return "disabled";
|
|
997
|
+
}
|
|
998
|
+
return "auto";
|
|
999
|
+
}
|
|
1000
|
+
async function displaySandboxWarning(mode, status) {
|
|
1001
|
+
if (mode === "required" && !status.available) {
|
|
1002
|
+
process.stderr.write(`
|
|
1003
|
+
${red("✖")} Docker sandbox required but not available: ${bold(status.reason ?? "Docker Desktop 4.58+ with sandbox support required")}
|
|
1004
|
+
`);
|
|
1005
|
+
process.stderr.write(` Install Docker Desktop 4.58+ or remove --sandbox=require to continue.
|
|
1006
|
+
|
|
1007
|
+
`);
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
if (mode === "disabled") {
|
|
1011
|
+
process.stderr.write(`
|
|
1012
|
+
${yellow("⚠")} ${bold("WARNING:")} Running without sandbox. The AI agent will have unrestricted
|
|
1013
|
+
`);
|
|
1014
|
+
process.stderr.write(` access to your filesystem, network, and environment variables.
|
|
1015
|
+
`);
|
|
1016
|
+
if (process.stdin.isTTY) {
|
|
1017
|
+
process.stderr.write(` Press ${bold("Enter")} to continue or ${bold("Ctrl+C")} to abort.
|
|
1018
|
+
`);
|
|
1019
|
+
await waitForEnter();
|
|
1020
|
+
}
|
|
1021
|
+
process.stderr.write(`
|
|
1022
|
+
`);
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
if (mode === "auto" && !status.available) {
|
|
1026
|
+
process.stderr.write(`
|
|
1027
|
+
${yellow("⚠")} Docker sandbox not available. Install Docker Desktop 4.58+ for secure execution.
|
|
1028
|
+
`);
|
|
1029
|
+
process.stderr.write(` Running without sandbox. Use ${dim("--no-sandbox")} to suppress this warning.
|
|
1030
|
+
|
|
1031
|
+
`);
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
function waitForEnter() {
|
|
1037
|
+
return new Promise((resolve) => {
|
|
1038
|
+
const rl = createInterface({
|
|
1039
|
+
input: process.stdin,
|
|
1040
|
+
output: process.stderr
|
|
1041
|
+
});
|
|
1042
|
+
rl.once("line", () => {
|
|
1043
|
+
rl.close();
|
|
1044
|
+
resolve();
|
|
1045
|
+
});
|
|
1046
|
+
rl.once("close", () => {
|
|
1047
|
+
resolve();
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
var TIMEOUT_MS = 5000, cachedStatus = null;
|
|
1052
|
+
var init_sandbox = __esm(() => {
|
|
1053
|
+
init_terminal();
|
|
1054
|
+
init_logger();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
906
1057
|
// src/commands/upgrade.ts
|
|
907
1058
|
var exports_upgrade = {};
|
|
908
1059
|
__export(exports_upgrade, {
|
|
@@ -1840,7 +1991,8 @@ var init_init = __esm(() => {
|
|
|
1840
1991
|
".locus/logs/",
|
|
1841
1992
|
".locus/worktrees/",
|
|
1842
1993
|
".locus/artifacts/",
|
|
1843
|
-
".locus/discussions/"
|
|
1994
|
+
".locus/discussions/",
|
|
1995
|
+
".locus/tmp/"
|
|
1844
1996
|
];
|
|
1845
1997
|
});
|
|
1846
1998
|
|
|
@@ -2911,14 +3063,87 @@ var init_stream_renderer = __esm(() => {
|
|
|
2911
3063
|
init_terminal();
|
|
2912
3064
|
});
|
|
2913
3065
|
|
|
3066
|
+
// src/repl/clipboard.ts
|
|
3067
|
+
import { execSync as execSync4 } from "node:child_process";
|
|
3068
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
|
|
3069
|
+
import { tmpdir } from "node:os";
|
|
3070
|
+
import { join as join9 } from "node:path";
|
|
3071
|
+
function readClipboardImage() {
|
|
3072
|
+
if (process.platform === "darwin") {
|
|
3073
|
+
return readMacOSClipboardImage();
|
|
3074
|
+
}
|
|
3075
|
+
if (process.platform === "linux") {
|
|
3076
|
+
return readLinuxClipboardImage();
|
|
3077
|
+
}
|
|
3078
|
+
return null;
|
|
3079
|
+
}
|
|
3080
|
+
function ensureStableDir() {
|
|
3081
|
+
if (!existsSync10(STABLE_DIR)) {
|
|
3082
|
+
mkdirSync7(STABLE_DIR, { recursive: true });
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
function readMacOSClipboardImage() {
|
|
3086
|
+
try {
|
|
3087
|
+
ensureStableDir();
|
|
3088
|
+
const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3089
|
+
const script = [
|
|
3090
|
+
`set destPath to POSIX file "${destPath}"`,
|
|
3091
|
+
"try",
|
|
3092
|
+
` set imgData to the clipboard as «class PNGf»`,
|
|
3093
|
+
"on error",
|
|
3094
|
+
" try",
|
|
3095
|
+
` set imgData to the clipboard as «class TIFF»`,
|
|
3096
|
+
" on error",
|
|
3097
|
+
` return "no-image"`,
|
|
3098
|
+
" end try",
|
|
3099
|
+
"end try",
|
|
3100
|
+
"set fRef to open for access destPath with write permission",
|
|
3101
|
+
"write imgData to fRef",
|
|
3102
|
+
"close access fRef",
|
|
3103
|
+
`return "ok"`
|
|
3104
|
+
].join(`
|
|
3105
|
+
`);
|
|
3106
|
+
const result = execSync4("osascript", {
|
|
3107
|
+
input: script,
|
|
3108
|
+
encoding: "utf-8",
|
|
3109
|
+
timeout: 5000,
|
|
3110
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3111
|
+
}).trim();
|
|
3112
|
+
if (result === "ok" && existsSync10(destPath)) {
|
|
3113
|
+
return destPath;
|
|
3114
|
+
}
|
|
3115
|
+
} catch {}
|
|
3116
|
+
return null;
|
|
3117
|
+
}
|
|
3118
|
+
function readLinuxClipboardImage() {
|
|
3119
|
+
try {
|
|
3120
|
+
const targets = execSync4("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
|
|
3121
|
+
if (!targets.includes("image/png")) {
|
|
3122
|
+
return null;
|
|
3123
|
+
}
|
|
3124
|
+
ensureStableDir();
|
|
3125
|
+
const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3126
|
+
execSync4(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
3127
|
+
if (existsSync10(destPath)) {
|
|
3128
|
+
return destPath;
|
|
3129
|
+
}
|
|
3130
|
+
} catch {}
|
|
3131
|
+
return null;
|
|
3132
|
+
}
|
|
3133
|
+
var STABLE_DIR;
|
|
3134
|
+
var init_clipboard = __esm(() => {
|
|
3135
|
+
STABLE_DIR = join9(tmpdir(), "locus-images");
|
|
3136
|
+
});
|
|
3137
|
+
|
|
2914
3138
|
// src/repl/image-detect.ts
|
|
2915
|
-
import { copyFileSync, existsSync as
|
|
2916
|
-
import { homedir as homedir3, tmpdir } from "node:os";
|
|
2917
|
-
import { basename, extname, join as
|
|
3139
|
+
import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
|
|
3140
|
+
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
3141
|
+
import { basename, extname, join as join10, resolve } from "node:path";
|
|
2918
3142
|
function detectImages(input) {
|
|
2919
3143
|
const detected = [];
|
|
2920
3144
|
const byResolved = new Map;
|
|
2921
|
-
|
|
3145
|
+
const sanitized = input.replace(/!\[Screenshot:[^\]]*\]\(locus:\/\/screenshot-\d+\)/g, "");
|
|
3146
|
+
for (const line of sanitized.split(`
|
|
2922
3147
|
`)) {
|
|
2923
3148
|
const trimmed = line.trim();
|
|
2924
3149
|
if (!trimmed)
|
|
@@ -2929,20 +3154,20 @@ function detectImages(input) {
|
|
|
2929
3154
|
}
|
|
2930
3155
|
}
|
|
2931
3156
|
const quotedPattern = /["']([^"']+\.(?:png|jpg|jpeg|gif|webp|bmp|svg))["']/gi;
|
|
2932
|
-
for (const match of
|
|
3157
|
+
for (const match of sanitized.matchAll(quotedPattern)) {
|
|
2933
3158
|
if (!match[0] || !match[1])
|
|
2934
3159
|
continue;
|
|
2935
3160
|
addIfImage(match[1], match[0], detected, byResolved);
|
|
2936
3161
|
}
|
|
2937
3162
|
const escapedPattern = /(?:\/|~\/|\.\/)?(?:[^\s"'\\]|\\ )+\.(?:png|jpg|jpeg|gif|webp|bmp|svg|tiff?)/gi;
|
|
2938
|
-
for (const match of
|
|
3163
|
+
for (const match of sanitized.matchAll(escapedPattern)) {
|
|
2939
3164
|
if (!match[0])
|
|
2940
3165
|
continue;
|
|
2941
3166
|
const path = match[0].replace(/\\ /g, " ");
|
|
2942
3167
|
addIfImage(path, match[0], detected, byResolved);
|
|
2943
3168
|
}
|
|
2944
3169
|
const plainPattern = /(?:\/|~\/|\.\/)[^\s"']+\.(?:png|jpg|jpeg|gif|webp|bmp|svg|tiff?)/gi;
|
|
2945
|
-
for (const match of
|
|
3170
|
+
for (const match of sanitized.matchAll(plainPattern)) {
|
|
2946
3171
|
if (!match[0])
|
|
2947
3172
|
continue;
|
|
2948
3173
|
addIfImage(match[0], match[0], detected, byResolved);
|
|
@@ -3006,13 +3231,28 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
3006
3231
|
const selected = attachments.filter((attachment) => ids.has(attachment.id));
|
|
3007
3232
|
return dedupeByResolvedPath(selected);
|
|
3008
3233
|
}
|
|
3234
|
+
function relocateImages(images, projectRoot) {
|
|
3235
|
+
const targetDir = join10(projectRoot, ".locus", "tmp", "images");
|
|
3236
|
+
for (const img of images) {
|
|
3237
|
+
if (!img.exists)
|
|
3238
|
+
continue;
|
|
3239
|
+
try {
|
|
3240
|
+
if (!existsSync11(targetDir)) {
|
|
3241
|
+
mkdirSync8(targetDir, { recursive: true });
|
|
3242
|
+
}
|
|
3243
|
+
const dest = join10(targetDir, basename(img.stablePath));
|
|
3244
|
+
copyFileSync(img.stablePath, dest);
|
|
3245
|
+
img.stablePath = dest;
|
|
3246
|
+
} catch {}
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3009
3249
|
function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
3010
3250
|
const ext = extname(rawPath).toLowerCase();
|
|
3011
3251
|
if (!IMAGE_EXTENSIONS.has(ext))
|
|
3012
3252
|
return;
|
|
3013
3253
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
3014
3254
|
if (resolved.startsWith("~/")) {
|
|
3015
|
-
resolved =
|
|
3255
|
+
resolved = join10(homedir3(), resolved.slice(2));
|
|
3016
3256
|
}
|
|
3017
3257
|
resolved = resolve(resolved);
|
|
3018
3258
|
const existing = byResolved.get(resolved);
|
|
@@ -3025,7 +3265,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
3025
3265
|
]);
|
|
3026
3266
|
return;
|
|
3027
3267
|
}
|
|
3028
|
-
const exists =
|
|
3268
|
+
const exists = existsSync11(resolved);
|
|
3029
3269
|
let stablePath = resolved;
|
|
3030
3270
|
if (exists) {
|
|
3031
3271
|
stablePath = copyToStable(resolved);
|
|
@@ -3079,17 +3319,17 @@ function dedupeByResolvedPath(images) {
|
|
|
3079
3319
|
}
|
|
3080
3320
|
function copyToStable(sourcePath) {
|
|
3081
3321
|
try {
|
|
3082
|
-
if (!
|
|
3083
|
-
|
|
3322
|
+
if (!existsSync11(STABLE_DIR2)) {
|
|
3323
|
+
mkdirSync8(STABLE_DIR2, { recursive: true });
|
|
3084
3324
|
}
|
|
3085
|
-
const dest =
|
|
3325
|
+
const dest = join10(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
3086
3326
|
copyFileSync(sourcePath, dest);
|
|
3087
3327
|
return dest;
|
|
3088
3328
|
} catch {
|
|
3089
3329
|
return sourcePath;
|
|
3090
3330
|
}
|
|
3091
3331
|
}
|
|
3092
|
-
var IMAGE_EXTENSIONS,
|
|
3332
|
+
var IMAGE_EXTENSIONS, STABLE_DIR2, PLACEHOLDER_SCHEME = "locus://screenshot-", PLACEHOLDER_ID_PATTERN;
|
|
3093
3333
|
var init_image_detect = __esm(() => {
|
|
3094
3334
|
IMAGE_EXTENSIONS = new Set([
|
|
3095
3335
|
".png",
|
|
@@ -3102,7 +3342,7 @@ var init_image_detect = __esm(() => {
|
|
|
3102
3342
|
".tif",
|
|
3103
3343
|
".tiff"
|
|
3104
3344
|
]);
|
|
3105
|
-
|
|
3345
|
+
STABLE_DIR2 = join10(tmpdir2(), "locus-images");
|
|
3106
3346
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
3107
3347
|
});
|
|
3108
3348
|
|
|
@@ -3521,6 +3761,15 @@ ${dim("Press Ctrl+C again to exit")}\r
|
|
|
3521
3761
|
pasteBuffer += pending.slice(0, endIdx);
|
|
3522
3762
|
pending = pending.slice(endIdx + PASTE_END.length);
|
|
3523
3763
|
isPasting = false;
|
|
3764
|
+
if (pasteBuffer.trim() === "") {
|
|
3765
|
+
const clipboardImagePath = readClipboardImage();
|
|
3766
|
+
if (clipboardImagePath) {
|
|
3767
|
+
insertText(clipboardImagePath);
|
|
3768
|
+
pasteBuffer = "";
|
|
3769
|
+
render();
|
|
3770
|
+
continue;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3524
3773
|
insertText(normalizeLineEndings(pasteBuffer));
|
|
3525
3774
|
pasteBuffer = "";
|
|
3526
3775
|
render();
|
|
@@ -3812,6 +4061,7 @@ var CSI = "\x1B[", SAVE_CURSOR = "\x1B7", RESTORE_CURSOR = "\x1B8", ENABLE_BRACK
|
|
|
3812
4061
|
`, ESC = "\x1B", BACKSPACE = "", SEQ_LEFT, SEQ_RIGHT, SEQ_UP, SEQ_DOWN, SEQ_HOME, SEQ_END, SEQ_HOME_1, SEQ_END_4, SEQ_HOME_O = "\x1BOH", SEQ_END_O = "\x1BOF", SEQ_DELETE, SEQ_WORD_LEFT, SEQ_WORD_RIGHT, SEQ_SHIFT_LEFT, SEQ_SHIFT_RIGHT, SEQ_META_LEFT, SEQ_META_RIGHT, SEQ_META_SHIFT_LEFT, SEQ_META_SHIFT_RIGHT, SEQ_SHIFT_ENTER_CSI_U, SEQ_SHIFT_ENTER_MODIFY, SEQ_SHIFT_ENTER_TILDE, SEQ_ALT_ENTER, CONTROL_SEQUENCES;
|
|
3813
4062
|
var init_input_handler = __esm(() => {
|
|
3814
4063
|
init_terminal();
|
|
4064
|
+
init_clipboard();
|
|
3815
4065
|
init_image_detect();
|
|
3816
4066
|
ENABLE_BRACKETED_PASTE = `${CSI}?2004h`;
|
|
3817
4067
|
DISABLE_BRACKETED_PASTE = `${CSI}?2004l`;
|
|
@@ -3874,7 +4124,7 @@ __export(exports_claude, {
|
|
|
3874
4124
|
buildClaudeArgs: () => buildClaudeArgs,
|
|
3875
4125
|
ClaudeRunner: () => ClaudeRunner
|
|
3876
4126
|
});
|
|
3877
|
-
import { execSync as
|
|
4127
|
+
import { execSync as execSync5, spawn as spawn2 } from "node:child_process";
|
|
3878
4128
|
function buildClaudeArgs(options) {
|
|
3879
4129
|
const args = [
|
|
3880
4130
|
"--dangerously-skip-permissions",
|
|
@@ -3895,7 +4145,7 @@ class ClaudeRunner {
|
|
|
3895
4145
|
aborted = false;
|
|
3896
4146
|
async isAvailable() {
|
|
3897
4147
|
try {
|
|
3898
|
-
|
|
4148
|
+
execSync5("claude --version", {
|
|
3899
4149
|
encoding: "utf-8",
|
|
3900
4150
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3901
4151
|
});
|
|
@@ -3906,7 +4156,7 @@ class ClaudeRunner {
|
|
|
3906
4156
|
}
|
|
3907
4157
|
async getVersion() {
|
|
3908
4158
|
try {
|
|
3909
|
-
const output =
|
|
4159
|
+
const output = execSync5("claude --version", {
|
|
3910
4160
|
encoding: "utf-8",
|
|
3911
4161
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3912
4162
|
}).trim();
|
|
@@ -4071,11 +4321,11 @@ var init_claude = __esm(() => {
|
|
|
4071
4321
|
|
|
4072
4322
|
// src/core/sandbox-ignore.ts
|
|
4073
4323
|
import { exec } from "node:child_process";
|
|
4074
|
-
import { existsSync as
|
|
4075
|
-
import { join as
|
|
4324
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
|
|
4325
|
+
import { join as join11 } from "node:path";
|
|
4076
4326
|
import { promisify } from "node:util";
|
|
4077
4327
|
function parseIgnoreFile(filePath) {
|
|
4078
|
-
if (!
|
|
4328
|
+
if (!existsSync12(filePath))
|
|
4079
4329
|
return [];
|
|
4080
4330
|
const content = readFileSync8(filePath, "utf-8");
|
|
4081
4331
|
const rules = [];
|
|
@@ -4122,7 +4372,7 @@ function buildCleanupScript(rules, workspacePath) {
|
|
|
4122
4372
|
}
|
|
4123
4373
|
async function enforceSandboxIgnore(sandboxName, projectRoot) {
|
|
4124
4374
|
const log = getLogger();
|
|
4125
|
-
const ignorePath =
|
|
4375
|
+
const ignorePath = join11(projectRoot, ".sandboxignore");
|
|
4126
4376
|
const rules = parseIgnoreFile(ignorePath);
|
|
4127
4377
|
if (rules.length === 0)
|
|
4128
4378
|
return;
|
|
@@ -4151,19 +4401,19 @@ var init_sandbox_ignore = __esm(() => {
|
|
|
4151
4401
|
|
|
4152
4402
|
// src/core/run-state.ts
|
|
4153
4403
|
import {
|
|
4154
|
-
existsSync as
|
|
4155
|
-
mkdirSync as
|
|
4404
|
+
existsSync as existsSync13,
|
|
4405
|
+
mkdirSync as mkdirSync9,
|
|
4156
4406
|
readFileSync as readFileSync9,
|
|
4157
4407
|
unlinkSync as unlinkSync3,
|
|
4158
4408
|
writeFileSync as writeFileSync6
|
|
4159
4409
|
} from "node:fs";
|
|
4160
|
-
import { dirname as dirname3, join as
|
|
4410
|
+
import { dirname as dirname3, join as join12 } from "node:path";
|
|
4161
4411
|
function getRunStatePath(projectRoot) {
|
|
4162
|
-
return
|
|
4412
|
+
return join12(projectRoot, ".locus", "run-state.json");
|
|
4163
4413
|
}
|
|
4164
4414
|
function loadRunState(projectRoot) {
|
|
4165
4415
|
const path = getRunStatePath(projectRoot);
|
|
4166
|
-
if (!
|
|
4416
|
+
if (!existsSync13(path))
|
|
4167
4417
|
return null;
|
|
4168
4418
|
try {
|
|
4169
4419
|
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
@@ -4175,15 +4425,15 @@ function loadRunState(projectRoot) {
|
|
|
4175
4425
|
function saveRunState(projectRoot, state) {
|
|
4176
4426
|
const path = getRunStatePath(projectRoot);
|
|
4177
4427
|
const dir = dirname3(path);
|
|
4178
|
-
if (!
|
|
4179
|
-
|
|
4428
|
+
if (!existsSync13(dir)) {
|
|
4429
|
+
mkdirSync9(dir, { recursive: true });
|
|
4180
4430
|
}
|
|
4181
4431
|
writeFileSync6(path, `${JSON.stringify(state, null, 2)}
|
|
4182
4432
|
`, "utf-8");
|
|
4183
4433
|
}
|
|
4184
4434
|
function clearRunState(projectRoot) {
|
|
4185
4435
|
const path = getRunStatePath(projectRoot);
|
|
4186
|
-
if (
|
|
4436
|
+
if (existsSync13(path)) {
|
|
4187
4437
|
unlinkSync3(path);
|
|
4188
4438
|
}
|
|
4189
4439
|
}
|
|
@@ -4257,7 +4507,7 @@ var init_run_state = __esm(() => {
|
|
|
4257
4507
|
});
|
|
4258
4508
|
|
|
4259
4509
|
// src/core/shutdown.ts
|
|
4260
|
-
import { execSync as
|
|
4510
|
+
import { execSync as execSync6 } from "node:child_process";
|
|
4261
4511
|
function registerActiveSandbox(name) {
|
|
4262
4512
|
activeSandboxes.add(name);
|
|
4263
4513
|
}
|
|
@@ -4267,7 +4517,7 @@ function unregisterActiveSandbox(name) {
|
|
|
4267
4517
|
function cleanupActiveSandboxes() {
|
|
4268
4518
|
for (const name of activeSandboxes) {
|
|
4269
4519
|
try {
|
|
4270
|
-
|
|
4520
|
+
execSync6(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
4271
4521
|
} catch {}
|
|
4272
4522
|
}
|
|
4273
4523
|
activeSandboxes.clear();
|
|
@@ -4340,7 +4590,7 @@ var init_shutdown = __esm(() => {
|
|
|
4340
4590
|
});
|
|
4341
4591
|
|
|
4342
4592
|
// src/ai/claude-sandbox.ts
|
|
4343
|
-
import { execSync as
|
|
4593
|
+
import { execSync as execSync7, spawn as spawn3 } from "node:child_process";
|
|
4344
4594
|
|
|
4345
4595
|
class SandboxedClaudeRunner {
|
|
4346
4596
|
name = "claude-sandboxed";
|
|
@@ -4556,7 +4806,7 @@ class SandboxedClaudeRunner {
|
|
|
4556
4806
|
sandboxName: this.sandboxName
|
|
4557
4807
|
});
|
|
4558
4808
|
try {
|
|
4559
|
-
|
|
4809
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4560
4810
|
} catch {}
|
|
4561
4811
|
}
|
|
4562
4812
|
}
|
|
@@ -4570,7 +4820,7 @@ class SandboxedClaudeRunner {
|
|
|
4570
4820
|
const log = getLogger();
|
|
4571
4821
|
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
4572
4822
|
try {
|
|
4573
|
-
|
|
4823
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4574
4824
|
} catch {}
|
|
4575
4825
|
unregisterActiveSandbox(this.sandboxName);
|
|
4576
4826
|
this.sandboxName = null;
|
|
@@ -4582,7 +4832,7 @@ class SandboxedClaudeRunner {
|
|
|
4582
4832
|
const log = getLogger();
|
|
4583
4833
|
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
4584
4834
|
try {
|
|
4585
|
-
|
|
4835
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, {
|
|
4586
4836
|
timeout: 60000
|
|
4587
4837
|
});
|
|
4588
4838
|
} catch {}
|
|
@@ -4658,7 +4908,7 @@ var init_claude_sandbox = __esm(() => {
|
|
|
4658
4908
|
});
|
|
4659
4909
|
|
|
4660
4910
|
// src/ai/codex.ts
|
|
4661
|
-
import { execSync as
|
|
4911
|
+
import { execSync as execSync8, spawn as spawn4 } from "node:child_process";
|
|
4662
4912
|
function buildCodexArgs(model) {
|
|
4663
4913
|
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
|
|
4664
4914
|
if (model) {
|
|
@@ -4674,7 +4924,7 @@ class CodexRunner {
|
|
|
4674
4924
|
aborted = false;
|
|
4675
4925
|
async isAvailable() {
|
|
4676
4926
|
try {
|
|
4677
|
-
|
|
4927
|
+
execSync8("codex --version", {
|
|
4678
4928
|
encoding: "utf-8",
|
|
4679
4929
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4680
4930
|
});
|
|
@@ -4685,7 +4935,7 @@ class CodexRunner {
|
|
|
4685
4935
|
}
|
|
4686
4936
|
async getVersion() {
|
|
4687
4937
|
try {
|
|
4688
|
-
const output =
|
|
4938
|
+
const output = execSync8("codex --version", {
|
|
4689
4939
|
encoding: "utf-8",
|
|
4690
4940
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4691
4941
|
}).trim();
|
|
@@ -4833,7 +5083,7 @@ var init_codex = __esm(() => {
|
|
|
4833
5083
|
});
|
|
4834
5084
|
|
|
4835
5085
|
// src/ai/codex-sandbox.ts
|
|
4836
|
-
import { execSync as
|
|
5086
|
+
import { execSync as execSync9, spawn as spawn5 } from "node:child_process";
|
|
4837
5087
|
|
|
4838
5088
|
class SandboxedCodexRunner {
|
|
4839
5089
|
name = "codex-sandboxed";
|
|
@@ -5076,7 +5326,7 @@ class SandboxedCodexRunner {
|
|
|
5076
5326
|
sandboxName: this.sandboxName
|
|
5077
5327
|
});
|
|
5078
5328
|
try {
|
|
5079
|
-
|
|
5329
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5080
5330
|
} catch {}
|
|
5081
5331
|
}
|
|
5082
5332
|
}
|
|
@@ -5090,7 +5340,7 @@ class SandboxedCodexRunner {
|
|
|
5090
5340
|
const log = getLogger();
|
|
5091
5341
|
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
5092
5342
|
try {
|
|
5093
|
-
|
|
5343
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5094
5344
|
} catch {}
|
|
5095
5345
|
unregisterActiveSandbox(this.sandboxName);
|
|
5096
5346
|
this.sandboxName = null;
|
|
@@ -5102,7 +5352,7 @@ class SandboxedCodexRunner {
|
|
|
5102
5352
|
const log = getLogger();
|
|
5103
5353
|
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
5104
5354
|
try {
|
|
5105
|
-
|
|
5355
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, {
|
|
5106
5356
|
timeout: 60000
|
|
5107
5357
|
});
|
|
5108
5358
|
} catch {}
|
|
@@ -5493,7 +5743,7 @@ var exports_issue = {};
|
|
|
5493
5743
|
__export(exports_issue, {
|
|
5494
5744
|
issueCommand: () => issueCommand
|
|
5495
5745
|
});
|
|
5496
|
-
import { createInterface } from "node:readline";
|
|
5746
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
5497
5747
|
function parseIssueArgs(args) {
|
|
5498
5748
|
const flags = {};
|
|
5499
5749
|
const positional = [];
|
|
@@ -5726,7 +5976,7 @@ function extractJSON(text) {
|
|
|
5726
5976
|
}
|
|
5727
5977
|
function askQuestion(question) {
|
|
5728
5978
|
return new Promise((resolve2) => {
|
|
5729
|
-
const rl =
|
|
5979
|
+
const rl = createInterface2({
|
|
5730
5980
|
input: process.stdin,
|
|
5731
5981
|
output: process.stderr
|
|
5732
5982
|
});
|
|
@@ -6765,9 +7015,9 @@ var init_sprint = __esm(() => {
|
|
|
6765
7015
|
});
|
|
6766
7016
|
|
|
6767
7017
|
// src/core/prompt-builder.ts
|
|
6768
|
-
import { execSync as
|
|
6769
|
-
import { existsSync as
|
|
6770
|
-
import { join as
|
|
7018
|
+
import { execSync as execSync10 } from "node:child_process";
|
|
7019
|
+
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "node:fs";
|
|
7020
|
+
import { join as join13 } from "node:path";
|
|
6771
7021
|
function buildExecutionPrompt(ctx) {
|
|
6772
7022
|
const sections = [];
|
|
6773
7023
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -6797,13 +7047,13 @@ function buildFeedbackPrompt(ctx) {
|
|
|
6797
7047
|
}
|
|
6798
7048
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
6799
7049
|
const sections = [];
|
|
6800
|
-
const locusmd = readFileSafe(
|
|
7050
|
+
const locusmd = readFileSafe(join13(projectRoot, "LOCUS.md"));
|
|
6801
7051
|
if (locusmd) {
|
|
6802
7052
|
sections.push(`# Project Instructions
|
|
6803
7053
|
|
|
6804
7054
|
${locusmd}`);
|
|
6805
7055
|
}
|
|
6806
|
-
const learnings = readFileSafe(
|
|
7056
|
+
const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
|
|
6807
7057
|
if (learnings) {
|
|
6808
7058
|
sections.push(`# Past Learnings
|
|
6809
7059
|
|
|
@@ -6829,24 +7079,24 @@ ${userMessage}`);
|
|
|
6829
7079
|
}
|
|
6830
7080
|
function buildSystemContext(projectRoot) {
|
|
6831
7081
|
const parts = ["# System Context"];
|
|
6832
|
-
const locusmd = readFileSafe(
|
|
7082
|
+
const locusmd = readFileSafe(join13(projectRoot, "LOCUS.md"));
|
|
6833
7083
|
if (locusmd) {
|
|
6834
7084
|
parts.push(`## Project Instructions (LOCUS.md)
|
|
6835
7085
|
|
|
6836
7086
|
${locusmd}`);
|
|
6837
7087
|
}
|
|
6838
|
-
const learnings = readFileSafe(
|
|
7088
|
+
const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
|
|
6839
7089
|
if (learnings) {
|
|
6840
7090
|
parts.push(`## Past Learnings
|
|
6841
7091
|
|
|
6842
7092
|
${learnings}`);
|
|
6843
7093
|
}
|
|
6844
|
-
const discussionsDir =
|
|
6845
|
-
if (
|
|
7094
|
+
const discussionsDir = join13(projectRoot, ".locus", "discussions");
|
|
7095
|
+
if (existsSync14(discussionsDir)) {
|
|
6846
7096
|
try {
|
|
6847
7097
|
const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
6848
7098
|
for (const file of files) {
|
|
6849
|
-
const content = readFileSafe(
|
|
7099
|
+
const content = readFileSafe(join13(discussionsDir, file));
|
|
6850
7100
|
if (content) {
|
|
6851
7101
|
parts.push(`## Discussion: ${file.replace(".md", "")}
|
|
6852
7102
|
|
|
@@ -6909,7 +7159,7 @@ ${diffSummary}
|
|
|
6909
7159
|
function buildRepoContext(projectRoot) {
|
|
6910
7160
|
const parts = ["# Repository Context"];
|
|
6911
7161
|
try {
|
|
6912
|
-
const tree =
|
|
7162
|
+
const tree = execSync10("find . -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.locus/*' -not -path '*/dist/*' -not -path '*/build/*' | head -80", { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
6913
7163
|
if (tree) {
|
|
6914
7164
|
parts.push(`## File Tree
|
|
6915
7165
|
|
|
@@ -6919,7 +7169,7 @@ ${tree}
|
|
|
6919
7169
|
}
|
|
6920
7170
|
} catch {}
|
|
6921
7171
|
try {
|
|
6922
|
-
const gitLog =
|
|
7172
|
+
const gitLog = execSync10("git log --oneline -10", {
|
|
6923
7173
|
cwd: projectRoot,
|
|
6924
7174
|
encoding: "utf-8",
|
|
6925
7175
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6933,7 +7183,7 @@ ${gitLog}
|
|
|
6933
7183
|
}
|
|
6934
7184
|
} catch {}
|
|
6935
7185
|
try {
|
|
6936
|
-
const branch =
|
|
7186
|
+
const branch = execSync10("git rev-parse --abbrev-ref HEAD", {
|
|
6937
7187
|
cwd: projectRoot,
|
|
6938
7188
|
encoding: "utf-8",
|
|
6939
7189
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6992,7 +7242,7 @@ function buildFeedbackInstructions() {
|
|
|
6992
7242
|
}
|
|
6993
7243
|
function readFileSafe(path) {
|
|
6994
7244
|
try {
|
|
6995
|
-
if (!
|
|
7245
|
+
if (!existsSync14(path))
|
|
6996
7246
|
return null;
|
|
6997
7247
|
return readFileSync10(path, "utf-8");
|
|
6998
7248
|
} catch {
|
|
@@ -7186,7 +7436,7 @@ var init_diff_renderer = __esm(() => {
|
|
|
7186
7436
|
});
|
|
7187
7437
|
|
|
7188
7438
|
// src/repl/commands.ts
|
|
7189
|
-
import { execSync as
|
|
7439
|
+
import { execSync as execSync11 } from "node:child_process";
|
|
7190
7440
|
function getSlashCommands() {
|
|
7191
7441
|
return [
|
|
7192
7442
|
{
|
|
@@ -7378,7 +7628,7 @@ function cmdModel(args, ctx) {
|
|
|
7378
7628
|
}
|
|
7379
7629
|
function cmdDiff(_args, ctx) {
|
|
7380
7630
|
try {
|
|
7381
|
-
const diff =
|
|
7631
|
+
const diff = execSync11("git diff", {
|
|
7382
7632
|
cwd: ctx.projectRoot,
|
|
7383
7633
|
encoding: "utf-8",
|
|
7384
7634
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7414,7 +7664,7 @@ function cmdDiff(_args, ctx) {
|
|
|
7414
7664
|
}
|
|
7415
7665
|
function cmdUndo(_args, ctx) {
|
|
7416
7666
|
try {
|
|
7417
|
-
const status =
|
|
7667
|
+
const status = execSync11("git status --porcelain", {
|
|
7418
7668
|
cwd: ctx.projectRoot,
|
|
7419
7669
|
encoding: "utf-8",
|
|
7420
7670
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7424,7 +7674,7 @@ function cmdUndo(_args, ctx) {
|
|
|
7424
7674
|
`);
|
|
7425
7675
|
return;
|
|
7426
7676
|
}
|
|
7427
|
-
|
|
7677
|
+
execSync11("git checkout .", {
|
|
7428
7678
|
cwd: ctx.projectRoot,
|
|
7429
7679
|
encoding: "utf-8",
|
|
7430
7680
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7458,7 +7708,7 @@ var init_commands = __esm(() => {
|
|
|
7458
7708
|
|
|
7459
7709
|
// src/repl/completions.ts
|
|
7460
7710
|
import { readdirSync as readdirSync4 } from "node:fs";
|
|
7461
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
7711
|
+
import { basename as basename2, dirname as dirname4, join as join14 } from "node:path";
|
|
7462
7712
|
|
|
7463
7713
|
class SlashCommandCompletion {
|
|
7464
7714
|
commands;
|
|
@@ -7513,7 +7763,7 @@ class FilePathCompletion {
|
|
|
7513
7763
|
}
|
|
7514
7764
|
findMatches(partial) {
|
|
7515
7765
|
try {
|
|
7516
|
-
const dir = partial.includes("/") ?
|
|
7766
|
+
const dir = partial.includes("/") ? join14(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
7517
7767
|
const prefix = basename2(partial);
|
|
7518
7768
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7519
7769
|
return entries.filter((e) => {
|
|
@@ -7549,14 +7799,14 @@ class CombinedCompletion {
|
|
|
7549
7799
|
var init_completions = () => {};
|
|
7550
7800
|
|
|
7551
7801
|
// src/repl/input-history.ts
|
|
7552
|
-
import { existsSync as
|
|
7553
|
-
import { dirname as dirname5, join as
|
|
7802
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
7803
|
+
import { dirname as dirname5, join as join15 } from "node:path";
|
|
7554
7804
|
|
|
7555
7805
|
class InputHistory {
|
|
7556
7806
|
entries = [];
|
|
7557
7807
|
filePath;
|
|
7558
7808
|
constructor(projectRoot) {
|
|
7559
|
-
this.filePath =
|
|
7809
|
+
this.filePath = join15(projectRoot, ".locus", "sessions", ".input-history");
|
|
7560
7810
|
this.load();
|
|
7561
7811
|
}
|
|
7562
7812
|
add(text) {
|
|
@@ -7595,7 +7845,7 @@ class InputHistory {
|
|
|
7595
7845
|
}
|
|
7596
7846
|
load() {
|
|
7597
7847
|
try {
|
|
7598
|
-
if (!
|
|
7848
|
+
if (!existsSync15(this.filePath))
|
|
7599
7849
|
return;
|
|
7600
7850
|
const content = readFileSync11(this.filePath, "utf-8");
|
|
7601
7851
|
this.entries = content.split(`
|
|
@@ -7605,8 +7855,8 @@ class InputHistory {
|
|
|
7605
7855
|
save() {
|
|
7606
7856
|
try {
|
|
7607
7857
|
const dir = dirname5(this.filePath);
|
|
7608
|
-
if (!
|
|
7609
|
-
|
|
7858
|
+
if (!existsSync15(dir)) {
|
|
7859
|
+
mkdirSync10(dir, { recursive: true });
|
|
7610
7860
|
}
|
|
7611
7861
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
7612
7862
|
`);
|
|
@@ -7636,21 +7886,21 @@ var init_model_config = __esm(() => {
|
|
|
7636
7886
|
|
|
7637
7887
|
// src/repl/session-manager.ts
|
|
7638
7888
|
import {
|
|
7639
|
-
existsSync as
|
|
7640
|
-
mkdirSync as
|
|
7889
|
+
existsSync as existsSync16,
|
|
7890
|
+
mkdirSync as mkdirSync11,
|
|
7641
7891
|
readdirSync as readdirSync5,
|
|
7642
7892
|
readFileSync as readFileSync12,
|
|
7643
7893
|
unlinkSync as unlinkSync4,
|
|
7644
7894
|
writeFileSync as writeFileSync8
|
|
7645
7895
|
} from "node:fs";
|
|
7646
|
-
import { basename as basename3, join as
|
|
7896
|
+
import { basename as basename3, join as join16 } from "node:path";
|
|
7647
7897
|
|
|
7648
7898
|
class SessionManager {
|
|
7649
7899
|
sessionsDir;
|
|
7650
7900
|
constructor(projectRoot) {
|
|
7651
|
-
this.sessionsDir =
|
|
7652
|
-
if (!
|
|
7653
|
-
|
|
7901
|
+
this.sessionsDir = join16(projectRoot, ".locus", "sessions");
|
|
7902
|
+
if (!existsSync16(this.sessionsDir)) {
|
|
7903
|
+
mkdirSync11(this.sessionsDir, { recursive: true });
|
|
7654
7904
|
}
|
|
7655
7905
|
}
|
|
7656
7906
|
create(options) {
|
|
@@ -7675,12 +7925,12 @@ class SessionManager {
|
|
|
7675
7925
|
}
|
|
7676
7926
|
isPersisted(sessionOrId) {
|
|
7677
7927
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
7678
|
-
return
|
|
7928
|
+
return existsSync16(this.getSessionPath(sessionId));
|
|
7679
7929
|
}
|
|
7680
7930
|
load(idOrPrefix) {
|
|
7681
7931
|
const files = this.listSessionFiles();
|
|
7682
7932
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
7683
|
-
if (
|
|
7933
|
+
if (existsSync16(exactPath)) {
|
|
7684
7934
|
try {
|
|
7685
7935
|
return JSON.parse(readFileSync12(exactPath, "utf-8"));
|
|
7686
7936
|
} catch {
|
|
@@ -7730,7 +7980,7 @@ class SessionManager {
|
|
|
7730
7980
|
}
|
|
7731
7981
|
delete(sessionId) {
|
|
7732
7982
|
const path = this.getSessionPath(sessionId);
|
|
7733
|
-
if (
|
|
7983
|
+
if (existsSync16(path)) {
|
|
7734
7984
|
unlinkSync4(path);
|
|
7735
7985
|
return true;
|
|
7736
7986
|
}
|
|
@@ -7760,7 +8010,7 @@ class SessionManager {
|
|
|
7760
8010
|
const remaining = withStats.length - pruned;
|
|
7761
8011
|
if (remaining > MAX_SESSIONS) {
|
|
7762
8012
|
const toRemove = remaining - MAX_SESSIONS;
|
|
7763
|
-
const alive = withStats.filter((e) =>
|
|
8013
|
+
const alive = withStats.filter((e) => existsSync16(e.path));
|
|
7764
8014
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
7765
8015
|
try {
|
|
7766
8016
|
unlinkSync4(alive[i].path);
|
|
@@ -7775,7 +8025,7 @@ class SessionManager {
|
|
|
7775
8025
|
}
|
|
7776
8026
|
listSessionFiles() {
|
|
7777
8027
|
try {
|
|
7778
|
-
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
8028
|
+
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join16(this.sessionsDir, f));
|
|
7779
8029
|
} catch {
|
|
7780
8030
|
return [];
|
|
7781
8031
|
}
|
|
@@ -7784,7 +8034,7 @@ class SessionManager {
|
|
|
7784
8034
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
7785
8035
|
}
|
|
7786
8036
|
getSessionPath(sessionId) {
|
|
7787
|
-
return
|
|
8037
|
+
return join16(this.sessionsDir, `${sessionId}.json`);
|
|
7788
8038
|
}
|
|
7789
8039
|
}
|
|
7790
8040
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -7794,7 +8044,7 @@ var init_session_manager = __esm(() => {
|
|
|
7794
8044
|
});
|
|
7795
8045
|
|
|
7796
8046
|
// src/repl/repl.ts
|
|
7797
|
-
import { execSync as
|
|
8047
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
7798
8048
|
async function startRepl(options) {
|
|
7799
8049
|
const { projectRoot, config } = options;
|
|
7800
8050
|
const sessionManager = new SessionManager(projectRoot);
|
|
@@ -7812,7 +8062,7 @@ async function startRepl(options) {
|
|
|
7812
8062
|
} else {
|
|
7813
8063
|
let branch = "main";
|
|
7814
8064
|
try {
|
|
7815
|
-
branch =
|
|
8065
|
+
branch = execSync12("git rev-parse --abbrev-ref HEAD", {
|
|
7816
8066
|
cwd: projectRoot,
|
|
7817
8067
|
encoding: "utf-8",
|
|
7818
8068
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7837,6 +8087,7 @@ async function executeOneShotPrompt(prompt, session, sessionManager, options) {
|
|
|
7837
8087
|
const normalized = normalizeImagePlaceholders(prompt);
|
|
7838
8088
|
const text = normalized.text;
|
|
7839
8089
|
const images = collectReferencedAttachments(text, normalized.attachments);
|
|
8090
|
+
relocateImages(images, projectRoot);
|
|
7840
8091
|
const imageContext = buildImageContext(images);
|
|
7841
8092
|
const fullPrompt = buildReplPrompt(text + imageContext, projectRoot, config, session.messages);
|
|
7842
8093
|
sessionManager.addMessage(session, {
|
|
@@ -7931,6 +8182,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
7931
8182
|
continue;
|
|
7932
8183
|
}
|
|
7933
8184
|
history.add(text);
|
|
8185
|
+
relocateImages(result.images, projectRoot);
|
|
7934
8186
|
const imageContext = buildImageContext(result.images);
|
|
7935
8187
|
const fullPrompt = buildReplPrompt(text + imageContext, projectRoot, { ...config, ai: { provider: currentProvider, model: currentModel } }, session.messages);
|
|
7936
8188
|
sessionManager.addMessage(session, {
|
|
@@ -8223,7 +8475,7 @@ var init_exec = __esm(() => {
|
|
|
8223
8475
|
});
|
|
8224
8476
|
|
|
8225
8477
|
// src/core/agent.ts
|
|
8226
|
-
import { execSync as
|
|
8478
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
8227
8479
|
async function executeIssue(projectRoot, options) {
|
|
8228
8480
|
const log = getLogger();
|
|
8229
8481
|
const timer = createTimer();
|
|
@@ -8252,7 +8504,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
|
|
|
8252
8504
|
}
|
|
8253
8505
|
let issueComments = [];
|
|
8254
8506
|
try {
|
|
8255
|
-
const commentsRaw =
|
|
8507
|
+
const commentsRaw = execSync13(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8256
8508
|
if (commentsRaw) {
|
|
8257
8509
|
issueComments = commentsRaw.split(`
|
|
8258
8510
|
`).filter(Boolean);
|
|
@@ -8416,12 +8668,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
|
|
|
8416
8668
|
}
|
|
8417
8669
|
async function createIssuePR(projectRoot, config, issue) {
|
|
8418
8670
|
try {
|
|
8419
|
-
const currentBranch =
|
|
8671
|
+
const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
|
|
8420
8672
|
cwd: projectRoot,
|
|
8421
8673
|
encoding: "utf-8",
|
|
8422
8674
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8423
8675
|
}).trim();
|
|
8424
|
-
const diff =
|
|
8676
|
+
const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8425
8677
|
cwd: projectRoot,
|
|
8426
8678
|
encoding: "utf-8",
|
|
8427
8679
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8430,7 +8682,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
8430
8682
|
getLogger().verbose("No changes to create PR for");
|
|
8431
8683
|
return;
|
|
8432
8684
|
}
|
|
8433
|
-
|
|
8685
|
+
execSync13(`git push -u origin ${currentBranch}`, {
|
|
8434
8686
|
cwd: projectRoot,
|
|
8435
8687
|
encoding: "utf-8",
|
|
8436
8688
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8476,9 +8728,9 @@ var init_agent = __esm(() => {
|
|
|
8476
8728
|
});
|
|
8477
8729
|
|
|
8478
8730
|
// src/core/conflict.ts
|
|
8479
|
-
import { execSync as
|
|
8731
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
8480
8732
|
function git2(args, cwd) {
|
|
8481
|
-
return
|
|
8733
|
+
return execSync14(`git ${args}`, {
|
|
8482
8734
|
cwd,
|
|
8483
8735
|
encoding: "utf-8",
|
|
8484
8736
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8603,144 +8855,12 @@ var init_conflict = __esm(() => {
|
|
|
8603
8855
|
init_logger();
|
|
8604
8856
|
});
|
|
8605
8857
|
|
|
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}`);
|
|
8619
|
-
}
|
|
8620
|
-
return status;
|
|
8621
|
-
}
|
|
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(`
|
|
8677
|
-
`);
|
|
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);
|
|
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) {
|
|
8692
|
-
try {
|
|
8693
|
-
await execFileAsync("docker", ["sandbox", "rm", name], {
|
|
8694
|
-
timeout: 1e4
|
|
8695
|
-
});
|
|
8696
|
-
log.debug(`Removed stale sandbox: ${name}`);
|
|
8697
|
-
cleaned++;
|
|
8698
|
-
} catch {
|
|
8699
|
-
log.debug(`Failed to remove stale sandbox: ${name}`);
|
|
8700
|
-
}
|
|
8701
|
-
}
|
|
8702
|
-
return cleaned;
|
|
8703
|
-
} catch {
|
|
8704
|
-
return 0;
|
|
8705
|
-
}
|
|
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";
|
|
8724
|
-
}
|
|
8725
|
-
throw new Error(`Invalid --sandbox value: "${flags.sandbox}". Valid values: require`);
|
|
8726
|
-
}
|
|
8727
|
-
if (!config.enabled) {
|
|
8728
|
-
return "disabled";
|
|
8729
|
-
}
|
|
8730
|
-
return "auto";
|
|
8731
|
-
}
|
|
8732
|
-
var TIMEOUT_MS = 5000, cachedStatus = null;
|
|
8733
|
-
var init_sandbox = __esm(() => {
|
|
8734
|
-
init_terminal();
|
|
8735
|
-
init_logger();
|
|
8736
|
-
});
|
|
8737
|
-
|
|
8738
8858
|
// src/core/worktree.ts
|
|
8739
|
-
import { execSync as
|
|
8740
|
-
import { existsSync as
|
|
8741
|
-
import { join as
|
|
8859
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
8860
|
+
import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
|
|
8861
|
+
import { join as join17 } from "node:path";
|
|
8742
8862
|
function git3(args, cwd) {
|
|
8743
|
-
return
|
|
8863
|
+
return execSync15(`git ${args}`, {
|
|
8744
8864
|
cwd,
|
|
8745
8865
|
encoding: "utf-8",
|
|
8746
8866
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8754,10 +8874,10 @@ function gitSafe2(args, cwd) {
|
|
|
8754
8874
|
}
|
|
8755
8875
|
}
|
|
8756
8876
|
function getWorktreeDir(projectRoot) {
|
|
8757
|
-
return
|
|
8877
|
+
return join17(projectRoot, ".locus", "worktrees");
|
|
8758
8878
|
}
|
|
8759
8879
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
8760
|
-
return
|
|
8880
|
+
return join17(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
8761
8881
|
}
|
|
8762
8882
|
function generateBranchName(issueNumber) {
|
|
8763
8883
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -8765,7 +8885,7 @@ function generateBranchName(issueNumber) {
|
|
|
8765
8885
|
}
|
|
8766
8886
|
function getWorktreeBranch(worktreePath) {
|
|
8767
8887
|
try {
|
|
8768
|
-
return
|
|
8888
|
+
return execSync15("git branch --show-current", {
|
|
8769
8889
|
cwd: worktreePath,
|
|
8770
8890
|
encoding: "utf-8",
|
|
8771
8891
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8777,7 +8897,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
8777
8897
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
8778
8898
|
const log = getLogger();
|
|
8779
8899
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
8780
|
-
if (
|
|
8900
|
+
if (existsSync17(worktreePath)) {
|
|
8781
8901
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
8782
8902
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
8783
8903
|
return {
|
|
@@ -8804,7 +8924,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
8804
8924
|
function removeWorktree(projectRoot, issueNumber) {
|
|
8805
8925
|
const log = getLogger();
|
|
8806
8926
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
8807
|
-
if (!
|
|
8927
|
+
if (!existsSync17(worktreePath)) {
|
|
8808
8928
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
8809
8929
|
return;
|
|
8810
8930
|
}
|
|
@@ -8823,7 +8943,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
8823
8943
|
function listWorktrees(projectRoot) {
|
|
8824
8944
|
const log = getLogger();
|
|
8825
8945
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
8826
|
-
if (!
|
|
8946
|
+
if (!existsSync17(worktreeDir)) {
|
|
8827
8947
|
return [];
|
|
8828
8948
|
}
|
|
8829
8949
|
const entries = readdirSync6(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -8843,7 +8963,7 @@ function listWorktrees(projectRoot) {
|
|
|
8843
8963
|
if (!match)
|
|
8844
8964
|
continue;
|
|
8845
8965
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
8846
|
-
const path =
|
|
8966
|
+
const path = join17(worktreeDir, entry);
|
|
8847
8967
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
8848
8968
|
let resolvedPath;
|
|
8849
8969
|
try {
|
|
@@ -8890,7 +9010,7 @@ var exports_run = {};
|
|
|
8890
9010
|
__export(exports_run, {
|
|
8891
9011
|
runCommand: () => runCommand
|
|
8892
9012
|
});
|
|
8893
|
-
import { execSync as
|
|
9013
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
8894
9014
|
function printRunHelp() {
|
|
8895
9015
|
process.stderr.write(`
|
|
8896
9016
|
${bold("locus run")} — Execute issues using AI agents
|
|
@@ -8956,13 +9076,6 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
8956
9076
|
process.stderr.write(`${yellow("⚠")} Running without sandbox. The AI agent will have unrestricted access to your filesystem, network, and environment variables.
|
|
8957
9077
|
`);
|
|
8958
9078
|
}
|
|
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
|
-
}
|
|
8966
9079
|
if (flags.resume) {
|
|
8967
9080
|
return handleResume(projectRoot, config, sandboxed);
|
|
8968
9081
|
}
|
|
@@ -9041,7 +9154,7 @@ ${yellow("⚠")} A sprint run is already in progress.
|
|
|
9041
9154
|
}
|
|
9042
9155
|
if (!flags.dryRun) {
|
|
9043
9156
|
try {
|
|
9044
|
-
|
|
9157
|
+
execSync16(`git checkout -B ${branchName}`, {
|
|
9045
9158
|
cwd: projectRoot,
|
|
9046
9159
|
encoding: "utf-8",
|
|
9047
9160
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9091,7 +9204,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
|
|
|
9091
9204
|
let sprintContext;
|
|
9092
9205
|
if (i > 0 && !flags.dryRun) {
|
|
9093
9206
|
try {
|
|
9094
|
-
sprintContext =
|
|
9207
|
+
sprintContext = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
9095
9208
|
cwd: projectRoot,
|
|
9096
9209
|
encoding: "utf-8",
|
|
9097
9210
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9111,13 +9224,16 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
9111
9224
|
dryRun: flags.dryRun,
|
|
9112
9225
|
sprintContext,
|
|
9113
9226
|
skipPR: true,
|
|
9114
|
-
sandboxed
|
|
9115
|
-
sandboxName: config.sandbox.name
|
|
9227
|
+
sandboxed
|
|
9116
9228
|
});
|
|
9117
9229
|
if (result.success) {
|
|
9118
9230
|
if (!flags.dryRun) {
|
|
9119
9231
|
const issueTitle = issue?.title ?? "";
|
|
9120
9232
|
ensureTaskCommit(projectRoot, task.issue, issueTitle);
|
|
9233
|
+
if (sandboxed && i < state.tasks.length - 1) {
|
|
9234
|
+
process.stderr.write(` ${dim("↻ Sandbox will resync on next task")}
|
|
9235
|
+
`);
|
|
9236
|
+
}
|
|
9121
9237
|
}
|
|
9122
9238
|
markTaskDone(state, task.issue, result.prNumber);
|
|
9123
9239
|
} else {
|
|
@@ -9152,7 +9268,7 @@ ${bold("Summary:")}
|
|
|
9152
9268
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
9153
9269
|
if (prNumber !== undefined) {
|
|
9154
9270
|
try {
|
|
9155
|
-
|
|
9271
|
+
execSync16(`git checkout ${config.agent.baseBranch}`, {
|
|
9156
9272
|
cwd: projectRoot,
|
|
9157
9273
|
encoding: "utf-8",
|
|
9158
9274
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9349,13 +9465,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9349
9465
|
`);
|
|
9350
9466
|
if (state.type === "sprint" && state.branch) {
|
|
9351
9467
|
try {
|
|
9352
|
-
const currentBranch =
|
|
9468
|
+
const currentBranch = execSync16("git rev-parse --abbrev-ref HEAD", {
|
|
9353
9469
|
cwd: projectRoot,
|
|
9354
9470
|
encoding: "utf-8",
|
|
9355
9471
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9356
9472
|
}).trim();
|
|
9357
9473
|
if (currentBranch !== state.branch) {
|
|
9358
|
-
|
|
9474
|
+
execSync16(`git checkout ${state.branch}`, {
|
|
9359
9475
|
cwd: projectRoot,
|
|
9360
9476
|
encoding: "utf-8",
|
|
9361
9477
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9383,7 +9499,7 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9383
9499
|
model: config.ai.model,
|
|
9384
9500
|
skipPR: isSprintRun,
|
|
9385
9501
|
sandboxed,
|
|
9386
|
-
sandboxName: config.sandbox.name
|
|
9502
|
+
sandboxName: isSprintRun ? undefined : config.sandbox.name
|
|
9387
9503
|
});
|
|
9388
9504
|
if (result.success) {
|
|
9389
9505
|
if (isSprintRun) {
|
|
@@ -9393,6 +9509,10 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9393
9509
|
issueTitle = iss.title;
|
|
9394
9510
|
} catch {}
|
|
9395
9511
|
ensureTaskCommit(projectRoot, task.issue, issueTitle);
|
|
9512
|
+
if (sandboxed) {
|
|
9513
|
+
process.stderr.write(` ${dim("↻ Sandbox will resync on next task")}
|
|
9514
|
+
`);
|
|
9515
|
+
}
|
|
9396
9516
|
}
|
|
9397
9517
|
markTaskDone(state, task.issue, result.prNumber);
|
|
9398
9518
|
} else {
|
|
@@ -9418,7 +9538,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
|
|
|
9418
9538
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
9419
9539
|
if (prNumber !== undefined) {
|
|
9420
9540
|
try {
|
|
9421
|
-
|
|
9541
|
+
execSync16(`git checkout ${config.agent.baseBranch}`, {
|
|
9422
9542
|
cwd: projectRoot,
|
|
9423
9543
|
encoding: "utf-8",
|
|
9424
9544
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9449,14 +9569,14 @@ function getOrder2(issue) {
|
|
|
9449
9569
|
}
|
|
9450
9570
|
function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
9451
9571
|
try {
|
|
9452
|
-
const status =
|
|
9572
|
+
const status = execSync16("git status --porcelain", {
|
|
9453
9573
|
cwd: projectRoot,
|
|
9454
9574
|
encoding: "utf-8",
|
|
9455
9575
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9456
9576
|
}).trim();
|
|
9457
9577
|
if (!status)
|
|
9458
9578
|
return;
|
|
9459
|
-
|
|
9579
|
+
execSync16("git add -A", {
|
|
9460
9580
|
cwd: projectRoot,
|
|
9461
9581
|
encoding: "utf-8",
|
|
9462
9582
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9464,7 +9584,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
9464
9584
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
9465
9585
|
|
|
9466
9586
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
9467
|
-
|
|
9587
|
+
execSync16(`git commit -F -`, {
|
|
9468
9588
|
input: message,
|
|
9469
9589
|
cwd: projectRoot,
|
|
9470
9590
|
encoding: "utf-8",
|
|
@@ -9478,7 +9598,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9478
9598
|
if (!config.agent.autoPR)
|
|
9479
9599
|
return;
|
|
9480
9600
|
try {
|
|
9481
|
-
const diff =
|
|
9601
|
+
const diff = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
9482
9602
|
cwd: projectRoot,
|
|
9483
9603
|
encoding: "utf-8",
|
|
9484
9604
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9488,7 +9608,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9488
9608
|
`);
|
|
9489
9609
|
return;
|
|
9490
9610
|
}
|
|
9491
|
-
|
|
9611
|
+
execSync16(`git push -u origin ${branchName}`, {
|
|
9492
9612
|
cwd: projectRoot,
|
|
9493
9613
|
encoding: "utf-8",
|
|
9494
9614
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9535,6 +9655,8 @@ __export(exports_status, {
|
|
|
9535
9655
|
});
|
|
9536
9656
|
async function statusCommand(projectRoot) {
|
|
9537
9657
|
const config = loadConfig(projectRoot);
|
|
9658
|
+
const spinner = new Spinner;
|
|
9659
|
+
spinner.start("Fetching project status...");
|
|
9538
9660
|
const lines = [];
|
|
9539
9661
|
lines.push(` ${dim("Repo:")} ${cyan(`${config.github.owner}/${config.github.repo}`)}`);
|
|
9540
9662
|
lines.push(` ${dim("Provider:")} ${config.ai.provider} / ${config.ai.model}`);
|
|
@@ -9611,6 +9733,7 @@ async function statusCommand(projectRoot) {
|
|
|
9611
9733
|
}
|
|
9612
9734
|
}
|
|
9613
9735
|
} catch {}
|
|
9736
|
+
spinner.stop();
|
|
9614
9737
|
lines.push("");
|
|
9615
9738
|
process.stderr.write(`
|
|
9616
9739
|
${drawBox(lines, { title: "Locus Status" })}
|
|
@@ -9634,13 +9757,13 @@ __export(exports_plan, {
|
|
|
9634
9757
|
parsePlanArgs: () => parsePlanArgs
|
|
9635
9758
|
});
|
|
9636
9759
|
import {
|
|
9637
|
-
existsSync as
|
|
9638
|
-
mkdirSync as
|
|
9760
|
+
existsSync as existsSync18,
|
|
9761
|
+
mkdirSync as mkdirSync12,
|
|
9639
9762
|
readdirSync as readdirSync7,
|
|
9640
9763
|
readFileSync as readFileSync13,
|
|
9641
9764
|
writeFileSync as writeFileSync9
|
|
9642
9765
|
} from "node:fs";
|
|
9643
|
-
import { join as
|
|
9766
|
+
import { join as join18 } from "node:path";
|
|
9644
9767
|
function printHelp() {
|
|
9645
9768
|
process.stderr.write(`
|
|
9646
9769
|
${bold("locus plan")} — AI-powered sprint planning
|
|
@@ -9671,12 +9794,12 @@ function normalizeSprintName(name) {
|
|
|
9671
9794
|
return name.trim().toLowerCase();
|
|
9672
9795
|
}
|
|
9673
9796
|
function getPlansDir(projectRoot) {
|
|
9674
|
-
return
|
|
9797
|
+
return join18(projectRoot, ".locus", "plans");
|
|
9675
9798
|
}
|
|
9676
9799
|
function ensurePlansDir(projectRoot) {
|
|
9677
9800
|
const dir = getPlansDir(projectRoot);
|
|
9678
|
-
if (!
|
|
9679
|
-
|
|
9801
|
+
if (!existsSync18(dir)) {
|
|
9802
|
+
mkdirSync12(dir, { recursive: true });
|
|
9680
9803
|
}
|
|
9681
9804
|
return dir;
|
|
9682
9805
|
}
|
|
@@ -9685,14 +9808,14 @@ function generateId() {
|
|
|
9685
9808
|
}
|
|
9686
9809
|
function loadPlanFile(projectRoot, id) {
|
|
9687
9810
|
const dir = getPlansDir(projectRoot);
|
|
9688
|
-
if (!
|
|
9811
|
+
if (!existsSync18(dir))
|
|
9689
9812
|
return null;
|
|
9690
9813
|
const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
|
|
9691
9814
|
const match = files.find((f) => f.startsWith(id));
|
|
9692
9815
|
if (!match)
|
|
9693
9816
|
return null;
|
|
9694
9817
|
try {
|
|
9695
|
-
const content = readFileSync13(
|
|
9818
|
+
const content = readFileSync13(join18(dir, match), "utf-8");
|
|
9696
9819
|
return JSON.parse(content);
|
|
9697
9820
|
} catch {
|
|
9698
9821
|
return null;
|
|
@@ -9738,7 +9861,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
9738
9861
|
}
|
|
9739
9862
|
function handleListPlans(projectRoot) {
|
|
9740
9863
|
const dir = getPlansDir(projectRoot);
|
|
9741
|
-
if (!
|
|
9864
|
+
if (!existsSync18(dir)) {
|
|
9742
9865
|
process.stderr.write(`${dim("No saved plans yet.")}
|
|
9743
9866
|
`);
|
|
9744
9867
|
return;
|
|
@@ -9756,7 +9879,7 @@ ${bold("Saved Plans:")}
|
|
|
9756
9879
|
for (const file of files) {
|
|
9757
9880
|
const id = file.replace(".json", "");
|
|
9758
9881
|
try {
|
|
9759
|
-
const content = readFileSync13(
|
|
9882
|
+
const content = readFileSync13(join18(dir, file), "utf-8");
|
|
9760
9883
|
const plan = JSON.parse(content);
|
|
9761
9884
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
9762
9885
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -9867,7 +9990,7 @@ ${bold("Approving plan:")}
|
|
|
9867
9990
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
9868
9991
|
const id = generateId();
|
|
9869
9992
|
const plansDir = ensurePlansDir(projectRoot);
|
|
9870
|
-
const planPath =
|
|
9993
|
+
const planPath = join18(plansDir, `${id}.json`);
|
|
9871
9994
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
9872
9995
|
const displayDirective = directive;
|
|
9873
9996
|
process.stderr.write(`
|
|
@@ -9901,7 +10024,7 @@ ${red("✗")} Planning failed: ${aiResult.error}
|
|
|
9901
10024
|
`);
|
|
9902
10025
|
return;
|
|
9903
10026
|
}
|
|
9904
|
-
if (!
|
|
10027
|
+
if (!existsSync18(planPath)) {
|
|
9905
10028
|
process.stderr.write(`
|
|
9906
10029
|
${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
|
|
9907
10030
|
`);
|
|
@@ -10068,15 +10191,15 @@ function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, pla
|
|
|
10068
10191
|
parts.push(`SPRINT: ${sprintName}`);
|
|
10069
10192
|
}
|
|
10070
10193
|
parts.push("");
|
|
10071
|
-
const locusPath =
|
|
10072
|
-
if (
|
|
10194
|
+
const locusPath = join18(projectRoot, "LOCUS.md");
|
|
10195
|
+
if (existsSync18(locusPath)) {
|
|
10073
10196
|
const content = readFileSync13(locusPath, "utf-8");
|
|
10074
10197
|
parts.push("PROJECT CONTEXT (LOCUS.md):");
|
|
10075
10198
|
parts.push(content.slice(0, 3000));
|
|
10076
10199
|
parts.push("");
|
|
10077
10200
|
}
|
|
10078
|
-
const learningsPath =
|
|
10079
|
-
if (
|
|
10201
|
+
const learningsPath = join18(projectRoot, ".locus", "LEARNINGS.md");
|
|
10202
|
+
if (existsSync18(learningsPath)) {
|
|
10080
10203
|
const content = readFileSync13(learningsPath, "utf-8");
|
|
10081
10204
|
parts.push("PAST LEARNINGS:");
|
|
10082
10205
|
parts.push(content.slice(0, 2000));
|
|
@@ -10253,9 +10376,9 @@ var exports_review = {};
|
|
|
10253
10376
|
__export(exports_review, {
|
|
10254
10377
|
reviewCommand: () => reviewCommand
|
|
10255
10378
|
});
|
|
10256
|
-
import { execSync as
|
|
10257
|
-
import { existsSync as
|
|
10258
|
-
import { join as
|
|
10379
|
+
import { execSync as execSync17 } from "node:child_process";
|
|
10380
|
+
import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
|
|
10381
|
+
import { join as join19 } from "node:path";
|
|
10259
10382
|
function printHelp2() {
|
|
10260
10383
|
process.stderr.write(`
|
|
10261
10384
|
${bold("locus review")} — AI-powered code review
|
|
@@ -10331,7 +10454,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
|
|
|
10331
10454
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
10332
10455
|
let prInfo;
|
|
10333
10456
|
try {
|
|
10334
|
-
const result =
|
|
10457
|
+
const result = execSync17(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10335
10458
|
const raw = JSON.parse(result);
|
|
10336
10459
|
prInfo = {
|
|
10337
10460
|
number: raw.number,
|
|
@@ -10397,7 +10520,7 @@ ${output.slice(0, 60000)}
|
|
|
10397
10520
|
|
|
10398
10521
|
---
|
|
10399
10522
|
_Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
|
|
10400
|
-
|
|
10523
|
+
execSync17(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10401
10524
|
process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
|
|
10402
10525
|
`);
|
|
10403
10526
|
} catch (e) {
|
|
@@ -10414,8 +10537,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
10414
10537
|
const parts = [];
|
|
10415
10538
|
parts.push(`You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.`);
|
|
10416
10539
|
parts.push("");
|
|
10417
|
-
const locusPath =
|
|
10418
|
-
if (
|
|
10540
|
+
const locusPath = join19(projectRoot, "LOCUS.md");
|
|
10541
|
+
if (existsSync19(locusPath)) {
|
|
10419
10542
|
const content = readFileSync14(locusPath, "utf-8");
|
|
10420
10543
|
parts.push("PROJECT CONTEXT:");
|
|
10421
10544
|
parts.push(content.slice(0, 2000));
|
|
@@ -10468,7 +10591,7 @@ var exports_iterate = {};
|
|
|
10468
10591
|
__export(exports_iterate, {
|
|
10469
10592
|
iterateCommand: () => iterateCommand
|
|
10470
10593
|
});
|
|
10471
|
-
import { execSync as
|
|
10594
|
+
import { execSync as execSync18 } from "node:child_process";
|
|
10472
10595
|
function printHelp3() {
|
|
10473
10596
|
process.stderr.write(`
|
|
10474
10597
|
${bold("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -10678,12 +10801,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
|
|
|
10678
10801
|
}
|
|
10679
10802
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
10680
10803
|
try {
|
|
10681
|
-
const result =
|
|
10804
|
+
const result = execSync18(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10682
10805
|
const parsed = JSON.parse(result);
|
|
10683
10806
|
if (parsed.length > 0) {
|
|
10684
10807
|
return parsed[0].number;
|
|
10685
10808
|
}
|
|
10686
|
-
const branchResult =
|
|
10809
|
+
const branchResult = execSync18(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10687
10810
|
const branchParsed = JSON.parse(branchResult);
|
|
10688
10811
|
if (branchParsed.length > 0) {
|
|
10689
10812
|
return branchParsed[0].number;
|
|
@@ -10718,14 +10841,14 @@ __export(exports_discuss, {
|
|
|
10718
10841
|
discussCommand: () => discussCommand
|
|
10719
10842
|
});
|
|
10720
10843
|
import {
|
|
10721
|
-
existsSync as
|
|
10722
|
-
mkdirSync as
|
|
10844
|
+
existsSync as existsSync20,
|
|
10845
|
+
mkdirSync as mkdirSync13,
|
|
10723
10846
|
readdirSync as readdirSync8,
|
|
10724
10847
|
readFileSync as readFileSync15,
|
|
10725
10848
|
unlinkSync as unlinkSync5,
|
|
10726
10849
|
writeFileSync as writeFileSync10
|
|
10727
10850
|
} from "node:fs";
|
|
10728
|
-
import { join as
|
|
10851
|
+
import { join as join20 } from "node:path";
|
|
10729
10852
|
function printHelp4() {
|
|
10730
10853
|
process.stderr.write(`
|
|
10731
10854
|
${bold("locus discuss")} — AI-powered architectural discussions
|
|
@@ -10747,12 +10870,12 @@ ${bold("Examples:")}
|
|
|
10747
10870
|
`);
|
|
10748
10871
|
}
|
|
10749
10872
|
function getDiscussionsDir(projectRoot) {
|
|
10750
|
-
return
|
|
10873
|
+
return join20(projectRoot, ".locus", "discussions");
|
|
10751
10874
|
}
|
|
10752
10875
|
function ensureDiscussionsDir(projectRoot) {
|
|
10753
10876
|
const dir = getDiscussionsDir(projectRoot);
|
|
10754
|
-
if (!
|
|
10755
|
-
|
|
10877
|
+
if (!existsSync20(dir)) {
|
|
10878
|
+
mkdirSync13(dir, { recursive: true });
|
|
10756
10879
|
}
|
|
10757
10880
|
return dir;
|
|
10758
10881
|
}
|
|
@@ -10786,7 +10909,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
10786
10909
|
}
|
|
10787
10910
|
function listDiscussions(projectRoot) {
|
|
10788
10911
|
const dir = getDiscussionsDir(projectRoot);
|
|
10789
|
-
if (!
|
|
10912
|
+
if (!existsSync20(dir)) {
|
|
10790
10913
|
process.stderr.write(`${dim("No discussions yet.")}
|
|
10791
10914
|
`);
|
|
10792
10915
|
return;
|
|
@@ -10803,7 +10926,7 @@ ${bold("Discussions:")}
|
|
|
10803
10926
|
`);
|
|
10804
10927
|
for (const file of files) {
|
|
10805
10928
|
const id = file.replace(".md", "");
|
|
10806
|
-
const content = readFileSync15(
|
|
10929
|
+
const content = readFileSync15(join20(dir, file), "utf-8");
|
|
10807
10930
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
10808
10931
|
const title = titleMatch ? titleMatch[1] : id;
|
|
10809
10932
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -10821,7 +10944,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
10821
10944
|
return;
|
|
10822
10945
|
}
|
|
10823
10946
|
const dir = getDiscussionsDir(projectRoot);
|
|
10824
|
-
if (!
|
|
10947
|
+
if (!existsSync20(dir)) {
|
|
10825
10948
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10826
10949
|
`);
|
|
10827
10950
|
return;
|
|
@@ -10833,7 +10956,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
10833
10956
|
`);
|
|
10834
10957
|
return;
|
|
10835
10958
|
}
|
|
10836
|
-
const content = readFileSync15(
|
|
10959
|
+
const content = readFileSync15(join20(dir, match), "utf-8");
|
|
10837
10960
|
process.stdout.write(`${content}
|
|
10838
10961
|
`);
|
|
10839
10962
|
}
|
|
@@ -10844,7 +10967,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
10844
10967
|
return;
|
|
10845
10968
|
}
|
|
10846
10969
|
const dir = getDiscussionsDir(projectRoot);
|
|
10847
|
-
if (!
|
|
10970
|
+
if (!existsSync20(dir)) {
|
|
10848
10971
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10849
10972
|
`);
|
|
10850
10973
|
return;
|
|
@@ -10856,7 +10979,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
10856
10979
|
`);
|
|
10857
10980
|
return;
|
|
10858
10981
|
}
|
|
10859
|
-
unlinkSync5(
|
|
10982
|
+
unlinkSync5(join20(dir, match));
|
|
10860
10983
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
10861
10984
|
`);
|
|
10862
10985
|
}
|
|
@@ -10869,7 +10992,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
10869
10992
|
return;
|
|
10870
10993
|
}
|
|
10871
10994
|
const dir = getDiscussionsDir(projectRoot);
|
|
10872
|
-
if (!
|
|
10995
|
+
if (!existsSync20(dir)) {
|
|
10873
10996
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10874
10997
|
`);
|
|
10875
10998
|
return;
|
|
@@ -10881,7 +11004,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
10881
11004
|
`);
|
|
10882
11005
|
return;
|
|
10883
11006
|
}
|
|
10884
|
-
const content = readFileSync15(
|
|
11007
|
+
const content = readFileSync15(join20(dir, match), "utf-8");
|
|
10885
11008
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
10886
11009
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
10887
11010
|
await planCommand(projectRoot, [
|
|
@@ -10995,7 +11118,7 @@ ${turn.content}`;
|
|
|
10995
11118
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
10996
11119
|
].join(`
|
|
10997
11120
|
`);
|
|
10998
|
-
writeFileSync10(
|
|
11121
|
+
writeFileSync10(join20(dir, `${id}.md`), markdown, "utf-8");
|
|
10999
11122
|
process.stderr.write(`
|
|
11000
11123
|
${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
|
|
11001
11124
|
`);
|
|
@@ -11009,15 +11132,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
11009
11132
|
const parts = [];
|
|
11010
11133
|
parts.push(`You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.`);
|
|
11011
11134
|
parts.push("");
|
|
11012
|
-
const locusPath =
|
|
11013
|
-
if (
|
|
11135
|
+
const locusPath = join20(projectRoot, "LOCUS.md");
|
|
11136
|
+
if (existsSync20(locusPath)) {
|
|
11014
11137
|
const content = readFileSync15(locusPath, "utf-8");
|
|
11015
11138
|
parts.push("PROJECT CONTEXT:");
|
|
11016
11139
|
parts.push(content.slice(0, 3000));
|
|
11017
11140
|
parts.push("");
|
|
11018
11141
|
}
|
|
11019
|
-
const learningsPath =
|
|
11020
|
-
if (
|
|
11142
|
+
const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
|
|
11143
|
+
if (existsSync20(learningsPath)) {
|
|
11021
11144
|
const content = readFileSync15(learningsPath, "utf-8");
|
|
11022
11145
|
parts.push("PAST LEARNINGS:");
|
|
11023
11146
|
parts.push(content.slice(0, 2000));
|
|
@@ -11077,8 +11200,8 @@ __export(exports_artifacts, {
|
|
|
11077
11200
|
formatDate: () => formatDate2,
|
|
11078
11201
|
artifactsCommand: () => artifactsCommand
|
|
11079
11202
|
});
|
|
11080
|
-
import { existsSync as
|
|
11081
|
-
import { join as
|
|
11203
|
+
import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
|
|
11204
|
+
import { join as join21 } from "node:path";
|
|
11082
11205
|
function printHelp5() {
|
|
11083
11206
|
process.stderr.write(`
|
|
11084
11207
|
${bold("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -11098,14 +11221,14 @@ ${dim("Artifact names support partial matching.")}
|
|
|
11098
11221
|
`);
|
|
11099
11222
|
}
|
|
11100
11223
|
function getArtifactsDir(projectRoot) {
|
|
11101
|
-
return
|
|
11224
|
+
return join21(projectRoot, ".locus", "artifacts");
|
|
11102
11225
|
}
|
|
11103
11226
|
function listArtifacts(projectRoot) {
|
|
11104
11227
|
const dir = getArtifactsDir(projectRoot);
|
|
11105
|
-
if (!
|
|
11228
|
+
if (!existsSync21(dir))
|
|
11106
11229
|
return [];
|
|
11107
11230
|
return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
11108
|
-
const filePath =
|
|
11231
|
+
const filePath = join21(dir, fileName);
|
|
11109
11232
|
const stat = statSync4(filePath);
|
|
11110
11233
|
return {
|
|
11111
11234
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -11118,8 +11241,8 @@ function listArtifacts(projectRoot) {
|
|
|
11118
11241
|
function readArtifact(projectRoot, name) {
|
|
11119
11242
|
const dir = getArtifactsDir(projectRoot);
|
|
11120
11243
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
11121
|
-
const filePath =
|
|
11122
|
-
if (!
|
|
11244
|
+
const filePath = join21(dir, fileName);
|
|
11245
|
+
if (!existsSync21(filePath))
|
|
11123
11246
|
return null;
|
|
11124
11247
|
const stat = statSync4(filePath);
|
|
11125
11248
|
return {
|
|
@@ -11282,11 +11405,11 @@ var init_artifacts = __esm(() => {
|
|
|
11282
11405
|
});
|
|
11283
11406
|
|
|
11284
11407
|
// src/commands/sandbox.ts
|
|
11285
|
-
var
|
|
11286
|
-
__export(
|
|
11408
|
+
var exports_sandbox2 = {};
|
|
11409
|
+
__export(exports_sandbox2, {
|
|
11287
11410
|
sandboxCommand: () => sandboxCommand
|
|
11288
11411
|
});
|
|
11289
|
-
import { execSync as
|
|
11412
|
+
import { execSync as execSync19, spawn as spawn6 } from "node:child_process";
|
|
11290
11413
|
function printSandboxHelp() {
|
|
11291
11414
|
process.stderr.write(`
|
|
11292
11415
|
${bold("locus sandbox")} — Manage Docker sandbox lifecycle
|
|
@@ -11400,7 +11523,7 @@ async function handleAgentLogin(projectRoot, agent) {
|
|
|
11400
11523
|
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11401
11524
|
`);
|
|
11402
11525
|
try {
|
|
11403
|
-
|
|
11526
|
+
execSync19(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
11404
11527
|
} catch {}
|
|
11405
11528
|
if (!isSandboxAlive(sandboxName)) {
|
|
11406
11529
|
process.stderr.write(`${red("✗")} Failed to create sandbox.
|
|
@@ -11463,7 +11586,7 @@ function handleRemove(projectRoot) {
|
|
|
11463
11586
|
process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
|
|
11464
11587
|
`);
|
|
11465
11588
|
try {
|
|
11466
|
-
|
|
11589
|
+
execSync19(`docker sandbox rm ${sandboxName}`, {
|
|
11467
11590
|
encoding: "utf-8",
|
|
11468
11591
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11469
11592
|
timeout: 15000
|
|
@@ -11500,7 +11623,7 @@ ${bold("Sandbox Status")}
|
|
|
11500
11623
|
}
|
|
11501
11624
|
async function ensureCodexInSandbox(sandboxName) {
|
|
11502
11625
|
try {
|
|
11503
|
-
|
|
11626
|
+
execSync19(`docker sandbox exec ${sandboxName} which codex`, {
|
|
11504
11627
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11505
11628
|
timeout: 5000
|
|
11506
11629
|
});
|
|
@@ -11508,7 +11631,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11508
11631
|
process.stderr.write(`Installing codex in sandbox...
|
|
11509
11632
|
`);
|
|
11510
11633
|
try {
|
|
11511
|
-
|
|
11634
|
+
execSync19(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
11512
11635
|
} catch {
|
|
11513
11636
|
process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
|
|
11514
11637
|
`);
|
|
@@ -11517,7 +11640,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11517
11640
|
}
|
|
11518
11641
|
function isSandboxAlive(name) {
|
|
11519
11642
|
try {
|
|
11520
|
-
const output =
|
|
11643
|
+
const output = execSync19("docker sandbox ls", {
|
|
11521
11644
|
encoding: "utf-8",
|
|
11522
11645
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11523
11646
|
timeout: 5000
|
|
@@ -11540,13 +11663,13 @@ init_context();
|
|
|
11540
11663
|
init_logger();
|
|
11541
11664
|
init_rate_limiter();
|
|
11542
11665
|
init_terminal();
|
|
11543
|
-
import { existsSync as
|
|
11544
|
-
import { join as
|
|
11666
|
+
import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
|
|
11667
|
+
import { join as join22 } from "node:path";
|
|
11545
11668
|
import { fileURLToPath } from "node:url";
|
|
11546
11669
|
function getCliVersion() {
|
|
11547
11670
|
const fallbackVersion = "0.0.0";
|
|
11548
|
-
const packageJsonPath =
|
|
11549
|
-
if (!
|
|
11671
|
+
const packageJsonPath = join22(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
11672
|
+
if (!existsSync22(packageJsonPath)) {
|
|
11550
11673
|
return fallbackVersion;
|
|
11551
11674
|
}
|
|
11552
11675
|
try {
|
|
@@ -11720,6 +11843,46 @@ function resolveAlias(command) {
|
|
|
11720
11843
|
};
|
|
11721
11844
|
return aliases[command] ?? command;
|
|
11722
11845
|
}
|
|
11846
|
+
function requiresSandboxSync(command, args, flags) {
|
|
11847
|
+
if (flags.noSandbox)
|
|
11848
|
+
return false;
|
|
11849
|
+
if (flags.help)
|
|
11850
|
+
return false;
|
|
11851
|
+
switch (command) {
|
|
11852
|
+
case "run":
|
|
11853
|
+
case "review":
|
|
11854
|
+
case "iterate":
|
|
11855
|
+
return true;
|
|
11856
|
+
case "exec":
|
|
11857
|
+
return args[0] !== "sessions" && args[0] !== "help";
|
|
11858
|
+
case "issue":
|
|
11859
|
+
return args[0] === "create";
|
|
11860
|
+
case "plan":
|
|
11861
|
+
if (args.length === 0)
|
|
11862
|
+
return false;
|
|
11863
|
+
return !["list", "show", "approve", "help"].includes(args[0]);
|
|
11864
|
+
case "discuss":
|
|
11865
|
+
if (args.length === 0)
|
|
11866
|
+
return false;
|
|
11867
|
+
return !["list", "show", "delete", "help"].includes(args[0]);
|
|
11868
|
+
case "config":
|
|
11869
|
+
return args[0] === "set";
|
|
11870
|
+
default:
|
|
11871
|
+
return false;
|
|
11872
|
+
}
|
|
11873
|
+
}
|
|
11874
|
+
async function prepareSandbox() {
|
|
11875
|
+
const { Spinner: Spinner2 } = await Promise.resolve().then(() => (init_progress(), exports_progress));
|
|
11876
|
+
const { detectSandboxSupport: detectSandboxSupport2 } = await Promise.resolve().then(() => (init_sandbox(), exports_sandbox));
|
|
11877
|
+
const spinner = new Spinner2;
|
|
11878
|
+
spinner.start("Preparing sandbox...");
|
|
11879
|
+
const status = await detectSandboxSupport2();
|
|
11880
|
+
if (status.available) {
|
|
11881
|
+
spinner.succeed("Sandbox ready");
|
|
11882
|
+
} else {
|
|
11883
|
+
spinner.warn(`Sandbox not available: ${status.reason}`);
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
11723
11886
|
async function main() {
|
|
11724
11887
|
const parsed = parseArgs(process.argv);
|
|
11725
11888
|
if (parsed.flags.version) {
|
|
@@ -11739,7 +11902,7 @@ async function main() {
|
|
|
11739
11902
|
try {
|
|
11740
11903
|
const root = getGitRoot(cwd);
|
|
11741
11904
|
if (isInitialized(root)) {
|
|
11742
|
-
logDir =
|
|
11905
|
+
logDir = join22(root, ".locus", "logs");
|
|
11743
11906
|
getRateLimiter(root);
|
|
11744
11907
|
}
|
|
11745
11908
|
} catch {}
|
|
@@ -11821,6 +11984,12 @@ async function main() {
|
|
|
11821
11984
|
`);
|
|
11822
11985
|
process.exit(1);
|
|
11823
11986
|
}
|
|
11987
|
+
if (requiresSandboxSync(command, parsed.args, parsed.flags)) {
|
|
11988
|
+
const config = loadConfig(projectRoot);
|
|
11989
|
+
if (config.sandbox.enabled) {
|
|
11990
|
+
await prepareSandbox();
|
|
11991
|
+
}
|
|
11992
|
+
}
|
|
11824
11993
|
switch (command) {
|
|
11825
11994
|
case "config": {
|
|
11826
11995
|
const { configCommand: configCommand2 } = await Promise.resolve().then(() => (init_config2(), exports_config));
|
|
@@ -11917,7 +12086,7 @@ async function main() {
|
|
|
11917
12086
|
break;
|
|
11918
12087
|
}
|
|
11919
12088
|
case "sandbox": {
|
|
11920
|
-
const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(),
|
|
12089
|
+
const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(), exports_sandbox2));
|
|
11921
12090
|
const sandboxArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
11922
12091
|
await sandboxCommand2(projectRoot, sandboxArgs);
|
|
11923
12092
|
break;
|