@locusai/cli 0.18.0 → 0.18.2
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 +779 -526
- 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, {
|
|
@@ -1289,6 +1440,9 @@ function updateIssueLabels(number, addLabels, removeLabels, options = {}) {
|
|
|
1289
1440
|
}
|
|
1290
1441
|
gh(args, options);
|
|
1291
1442
|
}
|
|
1443
|
+
function deleteIssue(number, options = {}) {
|
|
1444
|
+
gh(`issue delete ${number} --yes`, options);
|
|
1445
|
+
}
|
|
1292
1446
|
function addIssueComment(number, body, options = {}) {
|
|
1293
1447
|
const cwd = options.cwd ?? process.cwd();
|
|
1294
1448
|
execFileSync("gh", ["issue", "comment", String(number), "--body", body], {
|
|
@@ -1840,7 +1994,8 @@ var init_init = __esm(() => {
|
|
|
1840
1994
|
".locus/logs/",
|
|
1841
1995
|
".locus/worktrees/",
|
|
1842
1996
|
".locus/artifacts/",
|
|
1843
|
-
".locus/discussions/"
|
|
1997
|
+
".locus/discussions/",
|
|
1998
|
+
".locus/tmp/"
|
|
1844
1999
|
];
|
|
1845
2000
|
});
|
|
1846
2001
|
|
|
@@ -2911,14 +3066,87 @@ var init_stream_renderer = __esm(() => {
|
|
|
2911
3066
|
init_terminal();
|
|
2912
3067
|
});
|
|
2913
3068
|
|
|
3069
|
+
// src/repl/clipboard.ts
|
|
3070
|
+
import { execSync as execSync4 } from "node:child_process";
|
|
3071
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
|
|
3072
|
+
import { tmpdir } from "node:os";
|
|
3073
|
+
import { join as join9 } from "node:path";
|
|
3074
|
+
function readClipboardImage() {
|
|
3075
|
+
if (process.platform === "darwin") {
|
|
3076
|
+
return readMacOSClipboardImage();
|
|
3077
|
+
}
|
|
3078
|
+
if (process.platform === "linux") {
|
|
3079
|
+
return readLinuxClipboardImage();
|
|
3080
|
+
}
|
|
3081
|
+
return null;
|
|
3082
|
+
}
|
|
3083
|
+
function ensureStableDir() {
|
|
3084
|
+
if (!existsSync10(STABLE_DIR)) {
|
|
3085
|
+
mkdirSync7(STABLE_DIR, { recursive: true });
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
function readMacOSClipboardImage() {
|
|
3089
|
+
try {
|
|
3090
|
+
ensureStableDir();
|
|
3091
|
+
const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3092
|
+
const script = [
|
|
3093
|
+
`set destPath to POSIX file "${destPath}"`,
|
|
3094
|
+
"try",
|
|
3095
|
+
` set imgData to the clipboard as «class PNGf»`,
|
|
3096
|
+
"on error",
|
|
3097
|
+
" try",
|
|
3098
|
+
` set imgData to the clipboard as «class TIFF»`,
|
|
3099
|
+
" on error",
|
|
3100
|
+
` return "no-image"`,
|
|
3101
|
+
" end try",
|
|
3102
|
+
"end try",
|
|
3103
|
+
"set fRef to open for access destPath with write permission",
|
|
3104
|
+
"write imgData to fRef",
|
|
3105
|
+
"close access fRef",
|
|
3106
|
+
`return "ok"`
|
|
3107
|
+
].join(`
|
|
3108
|
+
`);
|
|
3109
|
+
const result = execSync4("osascript", {
|
|
3110
|
+
input: script,
|
|
3111
|
+
encoding: "utf-8",
|
|
3112
|
+
timeout: 5000,
|
|
3113
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3114
|
+
}).trim();
|
|
3115
|
+
if (result === "ok" && existsSync10(destPath)) {
|
|
3116
|
+
return destPath;
|
|
3117
|
+
}
|
|
3118
|
+
} catch {}
|
|
3119
|
+
return null;
|
|
3120
|
+
}
|
|
3121
|
+
function readLinuxClipboardImage() {
|
|
3122
|
+
try {
|
|
3123
|
+
const targets = execSync4("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
|
|
3124
|
+
if (!targets.includes("image/png")) {
|
|
3125
|
+
return null;
|
|
3126
|
+
}
|
|
3127
|
+
ensureStableDir();
|
|
3128
|
+
const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3129
|
+
execSync4(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
3130
|
+
if (existsSync10(destPath)) {
|
|
3131
|
+
return destPath;
|
|
3132
|
+
}
|
|
3133
|
+
} catch {}
|
|
3134
|
+
return null;
|
|
3135
|
+
}
|
|
3136
|
+
var STABLE_DIR;
|
|
3137
|
+
var init_clipboard = __esm(() => {
|
|
3138
|
+
STABLE_DIR = join9(tmpdir(), "locus-images");
|
|
3139
|
+
});
|
|
3140
|
+
|
|
2914
3141
|
// 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
|
|
3142
|
+
import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
|
|
3143
|
+
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
3144
|
+
import { basename, extname, join as join10, resolve } from "node:path";
|
|
2918
3145
|
function detectImages(input) {
|
|
2919
3146
|
const detected = [];
|
|
2920
3147
|
const byResolved = new Map;
|
|
2921
|
-
|
|
3148
|
+
const sanitized = input.replace(/!\[Screenshot:[^\]]*\]\(locus:\/\/screenshot-\d+\)/g, "");
|
|
3149
|
+
for (const line of sanitized.split(`
|
|
2922
3150
|
`)) {
|
|
2923
3151
|
const trimmed = line.trim();
|
|
2924
3152
|
if (!trimmed)
|
|
@@ -2929,20 +3157,20 @@ function detectImages(input) {
|
|
|
2929
3157
|
}
|
|
2930
3158
|
}
|
|
2931
3159
|
const quotedPattern = /["']([^"']+\.(?:png|jpg|jpeg|gif|webp|bmp|svg))["']/gi;
|
|
2932
|
-
for (const match of
|
|
3160
|
+
for (const match of sanitized.matchAll(quotedPattern)) {
|
|
2933
3161
|
if (!match[0] || !match[1])
|
|
2934
3162
|
continue;
|
|
2935
3163
|
addIfImage(match[1], match[0], detected, byResolved);
|
|
2936
3164
|
}
|
|
2937
3165
|
const escapedPattern = /(?:\/|~\/|\.\/)?(?:[^\s"'\\]|\\ )+\.(?:png|jpg|jpeg|gif|webp|bmp|svg|tiff?)/gi;
|
|
2938
|
-
for (const match of
|
|
3166
|
+
for (const match of sanitized.matchAll(escapedPattern)) {
|
|
2939
3167
|
if (!match[0])
|
|
2940
3168
|
continue;
|
|
2941
3169
|
const path = match[0].replace(/\\ /g, " ");
|
|
2942
3170
|
addIfImage(path, match[0], detected, byResolved);
|
|
2943
3171
|
}
|
|
2944
3172
|
const plainPattern = /(?:\/|~\/|\.\/)[^\s"']+\.(?:png|jpg|jpeg|gif|webp|bmp|svg|tiff?)/gi;
|
|
2945
|
-
for (const match of
|
|
3173
|
+
for (const match of sanitized.matchAll(plainPattern)) {
|
|
2946
3174
|
if (!match[0])
|
|
2947
3175
|
continue;
|
|
2948
3176
|
addIfImage(match[0], match[0], detected, byResolved);
|
|
@@ -3006,13 +3234,28 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
3006
3234
|
const selected = attachments.filter((attachment) => ids.has(attachment.id));
|
|
3007
3235
|
return dedupeByResolvedPath(selected);
|
|
3008
3236
|
}
|
|
3237
|
+
function relocateImages(images, projectRoot) {
|
|
3238
|
+
const targetDir = join10(projectRoot, ".locus", "tmp", "images");
|
|
3239
|
+
for (const img of images) {
|
|
3240
|
+
if (!img.exists)
|
|
3241
|
+
continue;
|
|
3242
|
+
try {
|
|
3243
|
+
if (!existsSync11(targetDir)) {
|
|
3244
|
+
mkdirSync8(targetDir, { recursive: true });
|
|
3245
|
+
}
|
|
3246
|
+
const dest = join10(targetDir, basename(img.stablePath));
|
|
3247
|
+
copyFileSync(img.stablePath, dest);
|
|
3248
|
+
img.stablePath = dest;
|
|
3249
|
+
} catch {}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3009
3252
|
function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
3010
3253
|
const ext = extname(rawPath).toLowerCase();
|
|
3011
3254
|
if (!IMAGE_EXTENSIONS.has(ext))
|
|
3012
3255
|
return;
|
|
3013
3256
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
3014
3257
|
if (resolved.startsWith("~/")) {
|
|
3015
|
-
resolved =
|
|
3258
|
+
resolved = join10(homedir3(), resolved.slice(2));
|
|
3016
3259
|
}
|
|
3017
3260
|
resolved = resolve(resolved);
|
|
3018
3261
|
const existing = byResolved.get(resolved);
|
|
@@ -3025,7 +3268,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
3025
3268
|
]);
|
|
3026
3269
|
return;
|
|
3027
3270
|
}
|
|
3028
|
-
const exists =
|
|
3271
|
+
const exists = existsSync11(resolved);
|
|
3029
3272
|
let stablePath = resolved;
|
|
3030
3273
|
if (exists) {
|
|
3031
3274
|
stablePath = copyToStable(resolved);
|
|
@@ -3079,17 +3322,17 @@ function dedupeByResolvedPath(images) {
|
|
|
3079
3322
|
}
|
|
3080
3323
|
function copyToStable(sourcePath) {
|
|
3081
3324
|
try {
|
|
3082
|
-
if (!
|
|
3083
|
-
|
|
3325
|
+
if (!existsSync11(STABLE_DIR2)) {
|
|
3326
|
+
mkdirSync8(STABLE_DIR2, { recursive: true });
|
|
3084
3327
|
}
|
|
3085
|
-
const dest =
|
|
3328
|
+
const dest = join10(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
3086
3329
|
copyFileSync(sourcePath, dest);
|
|
3087
3330
|
return dest;
|
|
3088
3331
|
} catch {
|
|
3089
3332
|
return sourcePath;
|
|
3090
3333
|
}
|
|
3091
3334
|
}
|
|
3092
|
-
var IMAGE_EXTENSIONS,
|
|
3335
|
+
var IMAGE_EXTENSIONS, STABLE_DIR2, PLACEHOLDER_SCHEME = "locus://screenshot-", PLACEHOLDER_ID_PATTERN;
|
|
3093
3336
|
var init_image_detect = __esm(() => {
|
|
3094
3337
|
IMAGE_EXTENSIONS = new Set([
|
|
3095
3338
|
".png",
|
|
@@ -3102,7 +3345,7 @@ var init_image_detect = __esm(() => {
|
|
|
3102
3345
|
".tif",
|
|
3103
3346
|
".tiff"
|
|
3104
3347
|
]);
|
|
3105
|
-
|
|
3348
|
+
STABLE_DIR2 = join10(tmpdir2(), "locus-images");
|
|
3106
3349
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
3107
3350
|
});
|
|
3108
3351
|
|
|
@@ -3521,6 +3764,15 @@ ${dim("Press Ctrl+C again to exit")}\r
|
|
|
3521
3764
|
pasteBuffer += pending.slice(0, endIdx);
|
|
3522
3765
|
pending = pending.slice(endIdx + PASTE_END.length);
|
|
3523
3766
|
isPasting = false;
|
|
3767
|
+
if (pasteBuffer.trim() === "") {
|
|
3768
|
+
const clipboardImagePath = readClipboardImage();
|
|
3769
|
+
if (clipboardImagePath) {
|
|
3770
|
+
insertText(clipboardImagePath);
|
|
3771
|
+
pasteBuffer = "";
|
|
3772
|
+
render();
|
|
3773
|
+
continue;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3524
3776
|
insertText(normalizeLineEndings(pasteBuffer));
|
|
3525
3777
|
pasteBuffer = "";
|
|
3526
3778
|
render();
|
|
@@ -3812,6 +4064,7 @@ var CSI = "\x1B[", SAVE_CURSOR = "\x1B7", RESTORE_CURSOR = "\x1B8", ENABLE_BRACK
|
|
|
3812
4064
|
`, 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
4065
|
var init_input_handler = __esm(() => {
|
|
3814
4066
|
init_terminal();
|
|
4067
|
+
init_clipboard();
|
|
3815
4068
|
init_image_detect();
|
|
3816
4069
|
ENABLE_BRACKETED_PASTE = `${CSI}?2004h`;
|
|
3817
4070
|
DISABLE_BRACKETED_PASTE = `${CSI}?2004l`;
|
|
@@ -3874,7 +4127,7 @@ __export(exports_claude, {
|
|
|
3874
4127
|
buildClaudeArgs: () => buildClaudeArgs,
|
|
3875
4128
|
ClaudeRunner: () => ClaudeRunner
|
|
3876
4129
|
});
|
|
3877
|
-
import { execSync as
|
|
4130
|
+
import { execSync as execSync5, spawn as spawn2 } from "node:child_process";
|
|
3878
4131
|
function buildClaudeArgs(options) {
|
|
3879
4132
|
const args = [
|
|
3880
4133
|
"--dangerously-skip-permissions",
|
|
@@ -3895,7 +4148,7 @@ class ClaudeRunner {
|
|
|
3895
4148
|
aborted = false;
|
|
3896
4149
|
async isAvailable() {
|
|
3897
4150
|
try {
|
|
3898
|
-
|
|
4151
|
+
execSync5("claude --version", {
|
|
3899
4152
|
encoding: "utf-8",
|
|
3900
4153
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3901
4154
|
});
|
|
@@ -3906,7 +4159,7 @@ class ClaudeRunner {
|
|
|
3906
4159
|
}
|
|
3907
4160
|
async getVersion() {
|
|
3908
4161
|
try {
|
|
3909
|
-
const output =
|
|
4162
|
+
const output = execSync5("claude --version", {
|
|
3910
4163
|
encoding: "utf-8",
|
|
3911
4164
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3912
4165
|
}).trim();
|
|
@@ -4071,11 +4324,11 @@ var init_claude = __esm(() => {
|
|
|
4071
4324
|
|
|
4072
4325
|
// src/core/sandbox-ignore.ts
|
|
4073
4326
|
import { exec } from "node:child_process";
|
|
4074
|
-
import { existsSync as
|
|
4075
|
-
import { join as
|
|
4327
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
|
|
4328
|
+
import { join as join11 } from "node:path";
|
|
4076
4329
|
import { promisify } from "node:util";
|
|
4077
4330
|
function parseIgnoreFile(filePath) {
|
|
4078
|
-
if (!
|
|
4331
|
+
if (!existsSync12(filePath))
|
|
4079
4332
|
return [];
|
|
4080
4333
|
const content = readFileSync8(filePath, "utf-8");
|
|
4081
4334
|
const rules = [];
|
|
@@ -4122,7 +4375,7 @@ function buildCleanupScript(rules, workspacePath) {
|
|
|
4122
4375
|
}
|
|
4123
4376
|
async function enforceSandboxIgnore(sandboxName, projectRoot) {
|
|
4124
4377
|
const log = getLogger();
|
|
4125
|
-
const ignorePath =
|
|
4378
|
+
const ignorePath = join11(projectRoot, ".sandboxignore");
|
|
4126
4379
|
const rules = parseIgnoreFile(ignorePath);
|
|
4127
4380
|
if (rules.length === 0)
|
|
4128
4381
|
return;
|
|
@@ -4151,19 +4404,19 @@ var init_sandbox_ignore = __esm(() => {
|
|
|
4151
4404
|
|
|
4152
4405
|
// src/core/run-state.ts
|
|
4153
4406
|
import {
|
|
4154
|
-
existsSync as
|
|
4155
|
-
mkdirSync as
|
|
4407
|
+
existsSync as existsSync13,
|
|
4408
|
+
mkdirSync as mkdirSync9,
|
|
4156
4409
|
readFileSync as readFileSync9,
|
|
4157
4410
|
unlinkSync as unlinkSync3,
|
|
4158
4411
|
writeFileSync as writeFileSync6
|
|
4159
4412
|
} from "node:fs";
|
|
4160
|
-
import { dirname as dirname3, join as
|
|
4413
|
+
import { dirname as dirname3, join as join12 } from "node:path";
|
|
4161
4414
|
function getRunStatePath(projectRoot) {
|
|
4162
|
-
return
|
|
4415
|
+
return join12(projectRoot, ".locus", "run-state.json");
|
|
4163
4416
|
}
|
|
4164
4417
|
function loadRunState(projectRoot) {
|
|
4165
4418
|
const path = getRunStatePath(projectRoot);
|
|
4166
|
-
if (!
|
|
4419
|
+
if (!existsSync13(path))
|
|
4167
4420
|
return null;
|
|
4168
4421
|
try {
|
|
4169
4422
|
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
@@ -4175,15 +4428,15 @@ function loadRunState(projectRoot) {
|
|
|
4175
4428
|
function saveRunState(projectRoot, state) {
|
|
4176
4429
|
const path = getRunStatePath(projectRoot);
|
|
4177
4430
|
const dir = dirname3(path);
|
|
4178
|
-
if (!
|
|
4179
|
-
|
|
4431
|
+
if (!existsSync13(dir)) {
|
|
4432
|
+
mkdirSync9(dir, { recursive: true });
|
|
4180
4433
|
}
|
|
4181
4434
|
writeFileSync6(path, `${JSON.stringify(state, null, 2)}
|
|
4182
4435
|
`, "utf-8");
|
|
4183
4436
|
}
|
|
4184
4437
|
function clearRunState(projectRoot) {
|
|
4185
4438
|
const path = getRunStatePath(projectRoot);
|
|
4186
|
-
if (
|
|
4439
|
+
if (existsSync13(path)) {
|
|
4187
4440
|
unlinkSync3(path);
|
|
4188
4441
|
}
|
|
4189
4442
|
}
|
|
@@ -4257,7 +4510,7 @@ var init_run_state = __esm(() => {
|
|
|
4257
4510
|
});
|
|
4258
4511
|
|
|
4259
4512
|
// src/core/shutdown.ts
|
|
4260
|
-
import { execSync as
|
|
4513
|
+
import { execSync as execSync6 } from "node:child_process";
|
|
4261
4514
|
function registerActiveSandbox(name) {
|
|
4262
4515
|
activeSandboxes.add(name);
|
|
4263
4516
|
}
|
|
@@ -4267,7 +4520,7 @@ function unregisterActiveSandbox(name) {
|
|
|
4267
4520
|
function cleanupActiveSandboxes() {
|
|
4268
4521
|
for (const name of activeSandboxes) {
|
|
4269
4522
|
try {
|
|
4270
|
-
|
|
4523
|
+
execSync6(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
4271
4524
|
} catch {}
|
|
4272
4525
|
}
|
|
4273
4526
|
activeSandboxes.clear();
|
|
@@ -4340,7 +4593,7 @@ var init_shutdown = __esm(() => {
|
|
|
4340
4593
|
});
|
|
4341
4594
|
|
|
4342
4595
|
// src/ai/claude-sandbox.ts
|
|
4343
|
-
import { execSync as
|
|
4596
|
+
import { execSync as execSync7, spawn as spawn3 } from "node:child_process";
|
|
4344
4597
|
|
|
4345
4598
|
class SandboxedClaudeRunner {
|
|
4346
4599
|
name = "claude-sandboxed";
|
|
@@ -4556,7 +4809,7 @@ class SandboxedClaudeRunner {
|
|
|
4556
4809
|
sandboxName: this.sandboxName
|
|
4557
4810
|
});
|
|
4558
4811
|
try {
|
|
4559
|
-
|
|
4812
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4560
4813
|
} catch {}
|
|
4561
4814
|
}
|
|
4562
4815
|
}
|
|
@@ -4570,7 +4823,7 @@ class SandboxedClaudeRunner {
|
|
|
4570
4823
|
const log = getLogger();
|
|
4571
4824
|
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
4572
4825
|
try {
|
|
4573
|
-
|
|
4826
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4574
4827
|
} catch {}
|
|
4575
4828
|
unregisterActiveSandbox(this.sandboxName);
|
|
4576
4829
|
this.sandboxName = null;
|
|
@@ -4582,7 +4835,7 @@ class SandboxedClaudeRunner {
|
|
|
4582
4835
|
const log = getLogger();
|
|
4583
4836
|
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
4584
4837
|
try {
|
|
4585
|
-
|
|
4838
|
+
execSync7(`docker sandbox rm ${this.sandboxName}`, {
|
|
4586
4839
|
timeout: 60000
|
|
4587
4840
|
});
|
|
4588
4841
|
} catch {}
|
|
@@ -4658,7 +4911,7 @@ var init_claude_sandbox = __esm(() => {
|
|
|
4658
4911
|
});
|
|
4659
4912
|
|
|
4660
4913
|
// src/ai/codex.ts
|
|
4661
|
-
import { execSync as
|
|
4914
|
+
import { execSync as execSync8, spawn as spawn4 } from "node:child_process";
|
|
4662
4915
|
function buildCodexArgs(model) {
|
|
4663
4916
|
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
|
|
4664
4917
|
if (model) {
|
|
@@ -4674,7 +4927,7 @@ class CodexRunner {
|
|
|
4674
4927
|
aborted = false;
|
|
4675
4928
|
async isAvailable() {
|
|
4676
4929
|
try {
|
|
4677
|
-
|
|
4930
|
+
execSync8("codex --version", {
|
|
4678
4931
|
encoding: "utf-8",
|
|
4679
4932
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4680
4933
|
});
|
|
@@ -4685,7 +4938,7 @@ class CodexRunner {
|
|
|
4685
4938
|
}
|
|
4686
4939
|
async getVersion() {
|
|
4687
4940
|
try {
|
|
4688
|
-
const output =
|
|
4941
|
+
const output = execSync8("codex --version", {
|
|
4689
4942
|
encoding: "utf-8",
|
|
4690
4943
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4691
4944
|
}).trim();
|
|
@@ -4833,7 +5086,7 @@ var init_codex = __esm(() => {
|
|
|
4833
5086
|
});
|
|
4834
5087
|
|
|
4835
5088
|
// src/ai/codex-sandbox.ts
|
|
4836
|
-
import { execSync as
|
|
5089
|
+
import { execSync as execSync9, spawn as spawn5 } from "node:child_process";
|
|
4837
5090
|
|
|
4838
5091
|
class SandboxedCodexRunner {
|
|
4839
5092
|
name = "codex-sandboxed";
|
|
@@ -5076,7 +5329,7 @@ class SandboxedCodexRunner {
|
|
|
5076
5329
|
sandboxName: this.sandboxName
|
|
5077
5330
|
});
|
|
5078
5331
|
try {
|
|
5079
|
-
|
|
5332
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5080
5333
|
} catch {}
|
|
5081
5334
|
}
|
|
5082
5335
|
}
|
|
@@ -5090,7 +5343,7 @@ class SandboxedCodexRunner {
|
|
|
5090
5343
|
const log = getLogger();
|
|
5091
5344
|
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
5092
5345
|
try {
|
|
5093
|
-
|
|
5346
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5094
5347
|
} catch {}
|
|
5095
5348
|
unregisterActiveSandbox(this.sandboxName);
|
|
5096
5349
|
this.sandboxName = null;
|
|
@@ -5102,7 +5355,7 @@ class SandboxedCodexRunner {
|
|
|
5102
5355
|
const log = getLogger();
|
|
5103
5356
|
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
5104
5357
|
try {
|
|
5105
|
-
|
|
5358
|
+
execSync9(`docker sandbox rm ${this.sandboxName}`, {
|
|
5106
5359
|
timeout: 60000
|
|
5107
5360
|
});
|
|
5108
5361
|
} catch {}
|
|
@@ -5493,7 +5746,7 @@ var exports_issue = {};
|
|
|
5493
5746
|
__export(exports_issue, {
|
|
5494
5747
|
issueCommand: () => issueCommand
|
|
5495
5748
|
});
|
|
5496
|
-
import { createInterface } from "node:readline";
|
|
5749
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
5497
5750
|
function parseIssueArgs(args) {
|
|
5498
5751
|
const flags = {};
|
|
5499
5752
|
const positional = [];
|
|
@@ -5572,6 +5825,10 @@ async function issueCommand(projectRoot, args) {
|
|
|
5572
5825
|
case "close":
|
|
5573
5826
|
await issueClose(projectRoot, parsed);
|
|
5574
5827
|
break;
|
|
5828
|
+
case "delete":
|
|
5829
|
+
case "rm":
|
|
5830
|
+
await issueDelete(projectRoot, parsed);
|
|
5831
|
+
break;
|
|
5575
5832
|
default:
|
|
5576
5833
|
if (/^\d+$/.test(parsed.subcommand)) {
|
|
5577
5834
|
parsed.positional.unshift(parsed.subcommand);
|
|
@@ -5699,21 +5956,26 @@ ${dim("────────────────────────
|
|
|
5699
5956
|
}
|
|
5700
5957
|
}
|
|
5701
5958
|
function buildIssueCreationPrompt(userRequest) {
|
|
5702
|
-
return
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5959
|
+
return `<role>
|
|
5960
|
+
You are a task planner for a software development team.
|
|
5961
|
+
Given a user request, create a well-structured GitHub issue.
|
|
5962
|
+
</role>
|
|
5963
|
+
|
|
5964
|
+
<output-format>
|
|
5965
|
+
Output ONLY a valid JSON object with exactly these fields:
|
|
5966
|
+
- "title": A concise, actionable issue title (max 80 characters)
|
|
5967
|
+
- "body": Detailed markdown description with context, acceptance criteria, and technical notes
|
|
5968
|
+
- "priority": One of: critical, high, medium, low
|
|
5969
|
+
- "type": One of: feature, bug, chore, refactor, docs
|
|
5970
|
+
</output-format>
|
|
5971
|
+
|
|
5972
|
+
<user-request>
|
|
5973
|
+
${userRequest}
|
|
5974
|
+
</user-request>
|
|
5975
|
+
|
|
5976
|
+
<constraints>
|
|
5977
|
+
Output ONLY the JSON object. No explanations, no code execution, no other text.
|
|
5978
|
+
</constraints>`;
|
|
5717
5979
|
}
|
|
5718
5980
|
function extractJSON(text) {
|
|
5719
5981
|
const codeBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
@@ -5726,7 +5988,7 @@ function extractJSON(text) {
|
|
|
5726
5988
|
}
|
|
5727
5989
|
function askQuestion(question) {
|
|
5728
5990
|
return new Promise((resolve2) => {
|
|
5729
|
-
const rl =
|
|
5991
|
+
const rl = createInterface2({
|
|
5730
5992
|
input: process.stdin,
|
|
5731
5993
|
output: process.stderr
|
|
5732
5994
|
});
|
|
@@ -5977,6 +6239,43 @@ async function issueClose(projectRoot, parsed) {
|
|
|
5977
6239
|
process.exit(1);
|
|
5978
6240
|
}
|
|
5979
6241
|
}
|
|
6242
|
+
async function issueDelete(projectRoot, parsed) {
|
|
6243
|
+
const issueNumbers = parsed.positional.filter((a) => /^\d+$/.test(a)).map(Number);
|
|
6244
|
+
if (issueNumbers.length === 0) {
|
|
6245
|
+
process.stderr.write(`${red("✗")} No issue numbers provided.
|
|
6246
|
+
`);
|
|
6247
|
+
process.stderr.write(` Usage: ${bold("locus issue delete <number...>")}
|
|
6248
|
+
`);
|
|
6249
|
+
process.exit(1);
|
|
6250
|
+
}
|
|
6251
|
+
const label = issueNumbers.length === 1 ? `issue #${issueNumbers[0]}` : `${issueNumbers.length} issues (#${issueNumbers.join(", #")})`;
|
|
6252
|
+
process.stderr.write(`${yellow("⚠")} This will ${bold("permanently delete")} ${label}.
|
|
6253
|
+
`);
|
|
6254
|
+
const answer = await askQuestion(`${cyan("?")} Are you sure? ${dim("[y/N]")} `);
|
|
6255
|
+
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
6256
|
+
process.stderr.write(`${yellow("○")} Cancelled.
|
|
6257
|
+
`);
|
|
6258
|
+
return;
|
|
6259
|
+
}
|
|
6260
|
+
let failed = 0;
|
|
6261
|
+
for (const num of issueNumbers) {
|
|
6262
|
+
process.stderr.write(`${cyan("●")} Deleting issue #${num}...`);
|
|
6263
|
+
try {
|
|
6264
|
+
deleteIssue(num, { cwd: projectRoot });
|
|
6265
|
+
process.stderr.write(`\r${green("✓")} Deleted issue #${num}
|
|
6266
|
+
`);
|
|
6267
|
+
} catch (e) {
|
|
6268
|
+
process.stderr.write(`\r${red("✗")} #${num}: ${e.message}
|
|
6269
|
+
`);
|
|
6270
|
+
failed++;
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
if (issueNumbers.length > 1 && failed === 0) {
|
|
6274
|
+
process.stderr.write(`
|
|
6275
|
+
${green("✓")} All ${issueNumbers.length} issues deleted.
|
|
6276
|
+
`);
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
5980
6279
|
function formatPriority(labels) {
|
|
5981
6280
|
for (const label of labels) {
|
|
5982
6281
|
if (label === "p:critical")
|
|
@@ -6079,6 +6378,7 @@ ${bold("Subcommands:")}
|
|
|
6079
6378
|
${cyan("show")} Show issue details
|
|
6080
6379
|
${cyan("label")} Bulk-update labels / sprint assignment
|
|
6081
6380
|
${cyan("close")} Close an issue
|
|
6381
|
+
${cyan("delete")} ${dim("(rm)")} Permanently delete issues (bulk)
|
|
6082
6382
|
|
|
6083
6383
|
${bold("Create options:")}
|
|
6084
6384
|
${dim("--sprint, -s")} Assign to sprint (milestone)
|
|
@@ -6100,6 +6400,7 @@ ${bold("Examples:")}
|
|
|
6100
6400
|
locus issue show 42
|
|
6101
6401
|
locus issue label 42 43 --sprint "Sprint 2"
|
|
6102
6402
|
locus issue close 42
|
|
6403
|
+
locus issue delete 42 43 44
|
|
6103
6404
|
|
|
6104
6405
|
`);
|
|
6105
6406
|
}
|
|
@@ -6765,9 +7066,9 @@ var init_sprint = __esm(() => {
|
|
|
6765
7066
|
});
|
|
6766
7067
|
|
|
6767
7068
|
// src/core/prompt-builder.ts
|
|
6768
|
-
import { execSync as
|
|
6769
|
-
import { existsSync as
|
|
6770
|
-
import { join as
|
|
7069
|
+
import { execSync as execSync10 } from "node:child_process";
|
|
7070
|
+
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "node:fs";
|
|
7071
|
+
import { join as join13 } from "node:path";
|
|
6771
7072
|
function buildExecutionPrompt(ctx) {
|
|
6772
7073
|
const sections = [];
|
|
6773
7074
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -6797,30 +7098,30 @@ function buildFeedbackPrompt(ctx) {
|
|
|
6797
7098
|
}
|
|
6798
7099
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
6799
7100
|
const sections = [];
|
|
6800
|
-
const locusmd = readFileSafe(
|
|
7101
|
+
const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
|
|
6801
7102
|
if (locusmd) {
|
|
6802
|
-
sections.push(
|
|
6803
|
-
|
|
6804
|
-
|
|
7103
|
+
sections.push(`<project-instructions>
|
|
7104
|
+
${locusmd}
|
|
7105
|
+
</project-instructions>`);
|
|
6805
7106
|
}
|
|
6806
|
-
const learnings = readFileSafe(
|
|
7107
|
+
const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
|
|
6807
7108
|
if (learnings) {
|
|
6808
|
-
sections.push(
|
|
6809
|
-
|
|
6810
|
-
|
|
7109
|
+
sections.push(`<past-learnings>
|
|
7110
|
+
${learnings}
|
|
7111
|
+
</past-learnings>`);
|
|
6811
7112
|
}
|
|
6812
7113
|
if (previousMessages && previousMessages.length > 0) {
|
|
6813
7114
|
const recent = previousMessages.slice(-10);
|
|
6814
7115
|
const historyLines = recent.map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`);
|
|
6815
|
-
sections.push(
|
|
6816
|
-
|
|
7116
|
+
sections.push(`<previous-conversation>
|
|
6817
7117
|
${historyLines.join(`
|
|
6818
7118
|
|
|
6819
|
-
`)}
|
|
7119
|
+
`)}
|
|
7120
|
+
</previous-conversation>`);
|
|
6820
7121
|
}
|
|
6821
|
-
sections.push(
|
|
6822
|
-
|
|
6823
|
-
|
|
7122
|
+
sections.push(`<current-request>
|
|
7123
|
+
${userMessage}
|
|
7124
|
+
</current-request>`);
|
|
6824
7125
|
return sections.join(`
|
|
6825
7126
|
|
|
6826
7127
|
---
|
|
@@ -6828,63 +7129,66 @@ ${userMessage}`);
|
|
|
6828
7129
|
`);
|
|
6829
7130
|
}
|
|
6830
7131
|
function buildSystemContext(projectRoot) {
|
|
6831
|
-
const parts = [
|
|
6832
|
-
const locusmd = readFileSafe(
|
|
7132
|
+
const parts = [];
|
|
7133
|
+
const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
|
|
6833
7134
|
if (locusmd) {
|
|
6834
|
-
parts.push(
|
|
6835
|
-
|
|
6836
|
-
|
|
7135
|
+
parts.push(`<project-instructions>
|
|
7136
|
+
${locusmd}
|
|
7137
|
+
</project-instructions>`);
|
|
6837
7138
|
}
|
|
6838
|
-
const learnings = readFileSafe(
|
|
7139
|
+
const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
|
|
6839
7140
|
if (learnings) {
|
|
6840
|
-
parts.push(
|
|
6841
|
-
|
|
6842
|
-
|
|
7141
|
+
parts.push(`<past-learnings>
|
|
7142
|
+
${learnings}
|
|
7143
|
+
</past-learnings>`);
|
|
6843
7144
|
}
|
|
6844
|
-
const discussionsDir =
|
|
6845
|
-
if (
|
|
7145
|
+
const discussionsDir = join13(projectRoot, ".locus", "discussions");
|
|
7146
|
+
if (existsSync14(discussionsDir)) {
|
|
6846
7147
|
try {
|
|
6847
7148
|
const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
6848
7149
|
for (const file of files) {
|
|
6849
|
-
const content = readFileSafe(
|
|
7150
|
+
const content = readFileSafe(join13(discussionsDir, file));
|
|
6850
7151
|
if (content) {
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
${content.slice(0, 2000)}
|
|
7152
|
+
const name = file.replace(".md", "");
|
|
7153
|
+
parts.push(`<discussion name="${name}">
|
|
7154
|
+
${content.slice(0, 2000)}
|
|
7155
|
+
</discussion>`);
|
|
6854
7156
|
}
|
|
6855
7157
|
}
|
|
6856
7158
|
} catch {}
|
|
6857
7159
|
}
|
|
6858
|
-
return
|
|
7160
|
+
return `<system-context>
|
|
7161
|
+
${parts.join(`
|
|
6859
7162
|
|
|
6860
|
-
`)
|
|
7163
|
+
`)}
|
|
7164
|
+
</system-context>`;
|
|
6861
7165
|
}
|
|
6862
7166
|
function buildTaskContext(issue, comments) {
|
|
6863
|
-
const parts = [
|
|
6864
|
-
|
|
6865
|
-
``,
|
|
6866
|
-
`## Issue #${issue.number}: ${issue.title}`,
|
|
6867
|
-
``,
|
|
6868
|
-
issue.body || "_No description provided._"
|
|
6869
|
-
];
|
|
7167
|
+
const parts = [];
|
|
7168
|
+
const issueParts = [issue.body || "_No description provided._"];
|
|
6870
7169
|
const labels = issue.labels.filter((l) => l.startsWith("p:") || l.startsWith("type:"));
|
|
6871
7170
|
if (labels.length > 0) {
|
|
6872
|
-
|
|
6873
|
-
**Labels:** ${labels.join(", ")}`);
|
|
7171
|
+
issueParts.push(`**Labels:** ${labels.join(", ")}`);
|
|
6874
7172
|
}
|
|
7173
|
+
parts.push(`<issue number="${issue.number}" title="${issue.title}">
|
|
7174
|
+
${issueParts.join(`
|
|
7175
|
+
|
|
7176
|
+
`)}
|
|
7177
|
+
</issue>`);
|
|
6875
7178
|
if (comments && comments.length > 0) {
|
|
6876
|
-
parts.push(
|
|
6877
|
-
|
|
6878
|
-
`)
|
|
6879
|
-
|
|
6880
|
-
parts.push(comment);
|
|
6881
|
-
}
|
|
7179
|
+
parts.push(`<issue-comments>
|
|
7180
|
+
${comments.join(`
|
|
7181
|
+
`)}
|
|
7182
|
+
</issue-comments>`);
|
|
6882
7183
|
}
|
|
6883
|
-
return
|
|
6884
|
-
`
|
|
7184
|
+
return `<task-context>
|
|
7185
|
+
${parts.join(`
|
|
7186
|
+
|
|
7187
|
+
`)}
|
|
7188
|
+
</task-context>`;
|
|
6885
7189
|
}
|
|
6886
7190
|
function buildSprintContext(sprintName, position, diffSummary) {
|
|
6887
|
-
const parts = [
|
|
7191
|
+
const parts = [];
|
|
6888
7192
|
if (sprintName) {
|
|
6889
7193
|
parts.push(`**Sprint:** ${sprintName}`);
|
|
6890
7194
|
}
|
|
@@ -6892,61 +7196,63 @@ function buildSprintContext(sprintName, position, diffSummary) {
|
|
|
6892
7196
|
parts.push(`**Position:** Task ${position}`);
|
|
6893
7197
|
}
|
|
6894
7198
|
if (diffSummary) {
|
|
6895
|
-
parts.push(
|
|
6896
|
-
## Changes from Previous Tasks
|
|
6897
|
-
|
|
7199
|
+
parts.push(`<previous-changes>
|
|
6898
7200
|
The following changes have already been made by earlier tasks in this sprint:
|
|
6899
7201
|
|
|
6900
7202
|
\`\`\`diff
|
|
6901
7203
|
${diffSummary}
|
|
6902
|
-
|
|
7204
|
+
\`\`\`
|
|
7205
|
+
</previous-changes>`);
|
|
6903
7206
|
}
|
|
6904
|
-
parts.push(
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
7207
|
+
parts.push(`**Important:** Build upon the changes from previous tasks. Do not revert or undo their work.`);
|
|
7208
|
+
return `<sprint-context>
|
|
7209
|
+
${parts.join(`
|
|
7210
|
+
|
|
7211
|
+
`)}
|
|
7212
|
+
</sprint-context>`;
|
|
6908
7213
|
}
|
|
6909
7214
|
function buildRepoContext(projectRoot) {
|
|
6910
|
-
const parts = [
|
|
7215
|
+
const parts = [];
|
|
6911
7216
|
try {
|
|
6912
|
-
const tree =
|
|
7217
|
+
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
7218
|
if (tree) {
|
|
6914
|
-
parts.push(
|
|
6915
|
-
|
|
7219
|
+
parts.push(`<file-tree>
|
|
6916
7220
|
\`\`\`
|
|
6917
7221
|
${tree}
|
|
6918
|
-
|
|
7222
|
+
\`\`\`
|
|
7223
|
+
</file-tree>`);
|
|
6919
7224
|
}
|
|
6920
7225
|
} catch {}
|
|
6921
7226
|
try {
|
|
6922
|
-
const gitLog =
|
|
7227
|
+
const gitLog = execSync10("git log --oneline -10", {
|
|
6923
7228
|
cwd: projectRoot,
|
|
6924
7229
|
encoding: "utf-8",
|
|
6925
7230
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6926
7231
|
}).trim();
|
|
6927
7232
|
if (gitLog) {
|
|
6928
|
-
parts.push(
|
|
6929
|
-
|
|
7233
|
+
parts.push(`<recent-commits>
|
|
6930
7234
|
\`\`\`
|
|
6931
7235
|
${gitLog}
|
|
6932
|
-
|
|
7236
|
+
\`\`\`
|
|
7237
|
+
</recent-commits>`);
|
|
6933
7238
|
}
|
|
6934
7239
|
} catch {}
|
|
6935
7240
|
try {
|
|
6936
|
-
const branch =
|
|
7241
|
+
const branch = execSync10("git rev-parse --abbrev-ref HEAD", {
|
|
6937
7242
|
cwd: projectRoot,
|
|
6938
7243
|
encoding: "utf-8",
|
|
6939
7244
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6940
7245
|
}).trim();
|
|
6941
7246
|
parts.push(`**Current branch:** ${branch}`);
|
|
6942
7247
|
} catch {}
|
|
6943
|
-
return
|
|
7248
|
+
return `<repository-context>
|
|
7249
|
+
${parts.join(`
|
|
6944
7250
|
|
|
6945
|
-
`)
|
|
7251
|
+
`)}
|
|
7252
|
+
</repository-context>`;
|
|
6946
7253
|
}
|
|
6947
7254
|
function buildExecutionRules(config) {
|
|
6948
|
-
return
|
|
6949
|
-
|
|
7255
|
+
return `<execution-rules>
|
|
6950
7256
|
1. **Commit format:** Use conventional commits: \`feat: <title> (#<issue>)\`, \`fix: ...\`, \`chore: ...\`. Every commit message MUST be multi-line: the first line is the title, then a blank line, then \`Co-Authored-By: LocusAgent <agent@locusai.team>\` as a Git trailer. Use \`git commit -m "<title>" -m "Co-Authored-By: LocusAgent <agent@locusai.team>"\` (two separate -m flags) to ensure the trailer is on its own line.
|
|
6951
7257
|
2. **Code quality:** Follow existing code style. Run linters/formatters if available.
|
|
6952
7258
|
3. **Testing:** If test files exist for modified code, update them accordingly.
|
|
@@ -6958,41 +7264,41 @@ function buildExecutionRules(config) {
|
|
|
6958
7264
|
5. **Base branch:** ${config.agent.baseBranch}
|
|
6959
7265
|
6. **Provider:** ${config.ai.provider} / ${config.ai.model}
|
|
6960
7266
|
|
|
6961
|
-
When you are done, provide a brief summary of what you changed and why
|
|
7267
|
+
When you are done, provide a brief summary of what you changed and why.
|
|
7268
|
+
</execution-rules>`;
|
|
6962
7269
|
}
|
|
6963
7270
|
function buildPRContext(prNumber, diff, comments) {
|
|
6964
7271
|
const parts = [
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
diff.slice(0, 1e4),
|
|
6971
|
-
"```"
|
|
7272
|
+
`<pr-diff>
|
|
7273
|
+
\`\`\`diff
|
|
7274
|
+
${diff.slice(0, 1e4)}
|
|
7275
|
+
\`\`\`
|
|
7276
|
+
</pr-diff>`
|
|
6972
7277
|
];
|
|
6973
7278
|
if (comments.length > 0) {
|
|
6974
|
-
parts.push(
|
|
6975
|
-
|
|
6976
|
-
`)
|
|
6977
|
-
|
|
6978
|
-
parts.push(comment);
|
|
6979
|
-
}
|
|
7279
|
+
parts.push(`<review-comments>
|
|
7280
|
+
${comments.join(`
|
|
7281
|
+
`)}
|
|
7282
|
+
</review-comments>`);
|
|
6980
7283
|
}
|
|
6981
|
-
return
|
|
6982
|
-
`
|
|
7284
|
+
return `<pr-context number="${prNumber}">
|
|
7285
|
+
${parts.join(`
|
|
7286
|
+
|
|
7287
|
+
`)}
|
|
7288
|
+
</pr-context>`;
|
|
6983
7289
|
}
|
|
6984
7290
|
function buildFeedbackInstructions() {
|
|
6985
|
-
return
|
|
6986
|
-
|
|
7291
|
+
return `<instructions>
|
|
6987
7292
|
1. Address ALL review feedback from the comments above.
|
|
6988
7293
|
2. Make targeted changes — do NOT rewrite code from scratch.
|
|
6989
7294
|
3. If a reviewer comment is unclear, make your best judgment and note your interpretation.
|
|
6990
7295
|
4. Push changes to the same branch — do NOT create a new PR.
|
|
6991
|
-
5. When done, summarize what you changed in response to each comment
|
|
7296
|
+
5. When done, summarize what you changed in response to each comment.
|
|
7297
|
+
</instructions>`;
|
|
6992
7298
|
}
|
|
6993
7299
|
function readFileSafe(path) {
|
|
6994
7300
|
try {
|
|
6995
|
-
if (!
|
|
7301
|
+
if (!existsSync14(path))
|
|
6996
7302
|
return null;
|
|
6997
7303
|
return readFileSync10(path, "utf-8");
|
|
6998
7304
|
} catch {
|
|
@@ -7186,7 +7492,7 @@ var init_diff_renderer = __esm(() => {
|
|
|
7186
7492
|
});
|
|
7187
7493
|
|
|
7188
7494
|
// src/repl/commands.ts
|
|
7189
|
-
import { execSync as
|
|
7495
|
+
import { execSync as execSync11 } from "node:child_process";
|
|
7190
7496
|
function getSlashCommands() {
|
|
7191
7497
|
return [
|
|
7192
7498
|
{
|
|
@@ -7378,7 +7684,7 @@ function cmdModel(args, ctx) {
|
|
|
7378
7684
|
}
|
|
7379
7685
|
function cmdDiff(_args, ctx) {
|
|
7380
7686
|
try {
|
|
7381
|
-
const diff =
|
|
7687
|
+
const diff = execSync11("git diff", {
|
|
7382
7688
|
cwd: ctx.projectRoot,
|
|
7383
7689
|
encoding: "utf-8",
|
|
7384
7690
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7414,7 +7720,7 @@ function cmdDiff(_args, ctx) {
|
|
|
7414
7720
|
}
|
|
7415
7721
|
function cmdUndo(_args, ctx) {
|
|
7416
7722
|
try {
|
|
7417
|
-
const status =
|
|
7723
|
+
const status = execSync11("git status --porcelain", {
|
|
7418
7724
|
cwd: ctx.projectRoot,
|
|
7419
7725
|
encoding: "utf-8",
|
|
7420
7726
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7424,7 +7730,7 @@ function cmdUndo(_args, ctx) {
|
|
|
7424
7730
|
`);
|
|
7425
7731
|
return;
|
|
7426
7732
|
}
|
|
7427
|
-
|
|
7733
|
+
execSync11("git checkout .", {
|
|
7428
7734
|
cwd: ctx.projectRoot,
|
|
7429
7735
|
encoding: "utf-8",
|
|
7430
7736
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7458,7 +7764,7 @@ var init_commands = __esm(() => {
|
|
|
7458
7764
|
|
|
7459
7765
|
// src/repl/completions.ts
|
|
7460
7766
|
import { readdirSync as readdirSync4 } from "node:fs";
|
|
7461
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
7767
|
+
import { basename as basename2, dirname as dirname4, join as join14 } from "node:path";
|
|
7462
7768
|
|
|
7463
7769
|
class SlashCommandCompletion {
|
|
7464
7770
|
commands;
|
|
@@ -7513,7 +7819,7 @@ class FilePathCompletion {
|
|
|
7513
7819
|
}
|
|
7514
7820
|
findMatches(partial) {
|
|
7515
7821
|
try {
|
|
7516
|
-
const dir = partial.includes("/") ?
|
|
7822
|
+
const dir = partial.includes("/") ? join14(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
7517
7823
|
const prefix = basename2(partial);
|
|
7518
7824
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7519
7825
|
return entries.filter((e) => {
|
|
@@ -7549,14 +7855,14 @@ class CombinedCompletion {
|
|
|
7549
7855
|
var init_completions = () => {};
|
|
7550
7856
|
|
|
7551
7857
|
// src/repl/input-history.ts
|
|
7552
|
-
import { existsSync as
|
|
7553
|
-
import { dirname as dirname5, join as
|
|
7858
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
7859
|
+
import { dirname as dirname5, join as join15 } from "node:path";
|
|
7554
7860
|
|
|
7555
7861
|
class InputHistory {
|
|
7556
7862
|
entries = [];
|
|
7557
7863
|
filePath;
|
|
7558
7864
|
constructor(projectRoot) {
|
|
7559
|
-
this.filePath =
|
|
7865
|
+
this.filePath = join15(projectRoot, ".locus", "sessions", ".input-history");
|
|
7560
7866
|
this.load();
|
|
7561
7867
|
}
|
|
7562
7868
|
add(text) {
|
|
@@ -7595,7 +7901,7 @@ class InputHistory {
|
|
|
7595
7901
|
}
|
|
7596
7902
|
load() {
|
|
7597
7903
|
try {
|
|
7598
|
-
if (!
|
|
7904
|
+
if (!existsSync15(this.filePath))
|
|
7599
7905
|
return;
|
|
7600
7906
|
const content = readFileSync11(this.filePath, "utf-8");
|
|
7601
7907
|
this.entries = content.split(`
|
|
@@ -7605,8 +7911,8 @@ class InputHistory {
|
|
|
7605
7911
|
save() {
|
|
7606
7912
|
try {
|
|
7607
7913
|
const dir = dirname5(this.filePath);
|
|
7608
|
-
if (!
|
|
7609
|
-
|
|
7914
|
+
if (!existsSync15(dir)) {
|
|
7915
|
+
mkdirSync10(dir, { recursive: true });
|
|
7610
7916
|
}
|
|
7611
7917
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
7612
7918
|
`);
|
|
@@ -7636,21 +7942,21 @@ var init_model_config = __esm(() => {
|
|
|
7636
7942
|
|
|
7637
7943
|
// src/repl/session-manager.ts
|
|
7638
7944
|
import {
|
|
7639
|
-
existsSync as
|
|
7640
|
-
mkdirSync as
|
|
7945
|
+
existsSync as existsSync16,
|
|
7946
|
+
mkdirSync as mkdirSync11,
|
|
7641
7947
|
readdirSync as readdirSync5,
|
|
7642
7948
|
readFileSync as readFileSync12,
|
|
7643
7949
|
unlinkSync as unlinkSync4,
|
|
7644
7950
|
writeFileSync as writeFileSync8
|
|
7645
7951
|
} from "node:fs";
|
|
7646
|
-
import { basename as basename3, join as
|
|
7952
|
+
import { basename as basename3, join as join16 } from "node:path";
|
|
7647
7953
|
|
|
7648
7954
|
class SessionManager {
|
|
7649
7955
|
sessionsDir;
|
|
7650
7956
|
constructor(projectRoot) {
|
|
7651
|
-
this.sessionsDir =
|
|
7652
|
-
if (!
|
|
7653
|
-
|
|
7957
|
+
this.sessionsDir = join16(projectRoot, ".locus", "sessions");
|
|
7958
|
+
if (!existsSync16(this.sessionsDir)) {
|
|
7959
|
+
mkdirSync11(this.sessionsDir, { recursive: true });
|
|
7654
7960
|
}
|
|
7655
7961
|
}
|
|
7656
7962
|
create(options) {
|
|
@@ -7675,12 +7981,12 @@ class SessionManager {
|
|
|
7675
7981
|
}
|
|
7676
7982
|
isPersisted(sessionOrId) {
|
|
7677
7983
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
7678
|
-
return
|
|
7984
|
+
return existsSync16(this.getSessionPath(sessionId));
|
|
7679
7985
|
}
|
|
7680
7986
|
load(idOrPrefix) {
|
|
7681
7987
|
const files = this.listSessionFiles();
|
|
7682
7988
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
7683
|
-
if (
|
|
7989
|
+
if (existsSync16(exactPath)) {
|
|
7684
7990
|
try {
|
|
7685
7991
|
return JSON.parse(readFileSync12(exactPath, "utf-8"));
|
|
7686
7992
|
} catch {
|
|
@@ -7730,7 +8036,7 @@ class SessionManager {
|
|
|
7730
8036
|
}
|
|
7731
8037
|
delete(sessionId) {
|
|
7732
8038
|
const path = this.getSessionPath(sessionId);
|
|
7733
|
-
if (
|
|
8039
|
+
if (existsSync16(path)) {
|
|
7734
8040
|
unlinkSync4(path);
|
|
7735
8041
|
return true;
|
|
7736
8042
|
}
|
|
@@ -7760,7 +8066,7 @@ class SessionManager {
|
|
|
7760
8066
|
const remaining = withStats.length - pruned;
|
|
7761
8067
|
if (remaining > MAX_SESSIONS) {
|
|
7762
8068
|
const toRemove = remaining - MAX_SESSIONS;
|
|
7763
|
-
const alive = withStats.filter((e) =>
|
|
8069
|
+
const alive = withStats.filter((e) => existsSync16(e.path));
|
|
7764
8070
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
7765
8071
|
try {
|
|
7766
8072
|
unlinkSync4(alive[i].path);
|
|
@@ -7775,7 +8081,7 @@ class SessionManager {
|
|
|
7775
8081
|
}
|
|
7776
8082
|
listSessionFiles() {
|
|
7777
8083
|
try {
|
|
7778
|
-
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
8084
|
+
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join16(this.sessionsDir, f));
|
|
7779
8085
|
} catch {
|
|
7780
8086
|
return [];
|
|
7781
8087
|
}
|
|
@@ -7784,7 +8090,7 @@ class SessionManager {
|
|
|
7784
8090
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
7785
8091
|
}
|
|
7786
8092
|
getSessionPath(sessionId) {
|
|
7787
|
-
return
|
|
8093
|
+
return join16(this.sessionsDir, `${sessionId}.json`);
|
|
7788
8094
|
}
|
|
7789
8095
|
}
|
|
7790
8096
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -7794,7 +8100,7 @@ var init_session_manager = __esm(() => {
|
|
|
7794
8100
|
});
|
|
7795
8101
|
|
|
7796
8102
|
// src/repl/repl.ts
|
|
7797
|
-
import { execSync as
|
|
8103
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
7798
8104
|
async function startRepl(options) {
|
|
7799
8105
|
const { projectRoot, config } = options;
|
|
7800
8106
|
const sessionManager = new SessionManager(projectRoot);
|
|
@@ -7812,7 +8118,7 @@ async function startRepl(options) {
|
|
|
7812
8118
|
} else {
|
|
7813
8119
|
let branch = "main";
|
|
7814
8120
|
try {
|
|
7815
|
-
branch =
|
|
8121
|
+
branch = execSync12("git rev-parse --abbrev-ref HEAD", {
|
|
7816
8122
|
cwd: projectRoot,
|
|
7817
8123
|
encoding: "utf-8",
|
|
7818
8124
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7837,6 +8143,7 @@ async function executeOneShotPrompt(prompt, session, sessionManager, options) {
|
|
|
7837
8143
|
const normalized = normalizeImagePlaceholders(prompt);
|
|
7838
8144
|
const text = normalized.text;
|
|
7839
8145
|
const images = collectReferencedAttachments(text, normalized.attachments);
|
|
8146
|
+
relocateImages(images, projectRoot);
|
|
7840
8147
|
const imageContext = buildImageContext(images);
|
|
7841
8148
|
const fullPrompt = buildReplPrompt(text + imageContext, projectRoot, config, session.messages);
|
|
7842
8149
|
sessionManager.addMessage(session, {
|
|
@@ -7931,6 +8238,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
7931
8238
|
continue;
|
|
7932
8239
|
}
|
|
7933
8240
|
history.add(text);
|
|
8241
|
+
relocateImages(result.images, projectRoot);
|
|
7934
8242
|
const imageContext = buildImageContext(result.images);
|
|
7935
8243
|
const fullPrompt = buildReplPrompt(text + imageContext, projectRoot, { ...config, ai: { provider: currentProvider, model: currentModel } }, session.messages);
|
|
7936
8244
|
sessionManager.addMessage(session, {
|
|
@@ -8223,7 +8531,7 @@ var init_exec = __esm(() => {
|
|
|
8223
8531
|
});
|
|
8224
8532
|
|
|
8225
8533
|
// src/core/agent.ts
|
|
8226
|
-
import { execSync as
|
|
8534
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
8227
8535
|
async function executeIssue(projectRoot, options) {
|
|
8228
8536
|
const log = getLogger();
|
|
8229
8537
|
const timer = createTimer();
|
|
@@ -8252,7 +8560,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
|
|
|
8252
8560
|
}
|
|
8253
8561
|
let issueComments = [];
|
|
8254
8562
|
try {
|
|
8255
|
-
const commentsRaw =
|
|
8563
|
+
const commentsRaw = execSync13(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8256
8564
|
if (commentsRaw) {
|
|
8257
8565
|
issueComments = commentsRaw.split(`
|
|
8258
8566
|
`).filter(Boolean);
|
|
@@ -8416,12 +8724,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
|
|
|
8416
8724
|
}
|
|
8417
8725
|
async function createIssuePR(projectRoot, config, issue) {
|
|
8418
8726
|
try {
|
|
8419
|
-
const currentBranch =
|
|
8727
|
+
const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
|
|
8420
8728
|
cwd: projectRoot,
|
|
8421
8729
|
encoding: "utf-8",
|
|
8422
8730
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8423
8731
|
}).trim();
|
|
8424
|
-
const diff =
|
|
8732
|
+
const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8425
8733
|
cwd: projectRoot,
|
|
8426
8734
|
encoding: "utf-8",
|
|
8427
8735
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8430,7 +8738,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
8430
8738
|
getLogger().verbose("No changes to create PR for");
|
|
8431
8739
|
return;
|
|
8432
8740
|
}
|
|
8433
|
-
|
|
8741
|
+
execSync13(`git push -u origin ${currentBranch}`, {
|
|
8434
8742
|
cwd: projectRoot,
|
|
8435
8743
|
encoding: "utf-8",
|
|
8436
8744
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8476,9 +8784,9 @@ var init_agent = __esm(() => {
|
|
|
8476
8784
|
});
|
|
8477
8785
|
|
|
8478
8786
|
// src/core/conflict.ts
|
|
8479
|
-
import { execSync as
|
|
8787
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
8480
8788
|
function git2(args, cwd) {
|
|
8481
|
-
return
|
|
8789
|
+
return execSync14(`git ${args}`, {
|
|
8482
8790
|
cwd,
|
|
8483
8791
|
encoding: "utf-8",
|
|
8484
8792
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8603,144 +8911,12 @@ var init_conflict = __esm(() => {
|
|
|
8603
8911
|
init_logger();
|
|
8604
8912
|
});
|
|
8605
8913
|
|
|
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
8914
|
// src/core/worktree.ts
|
|
8739
|
-
import { execSync as
|
|
8740
|
-
import { existsSync as
|
|
8741
|
-
import { join as
|
|
8915
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
8916
|
+
import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
|
|
8917
|
+
import { join as join17 } from "node:path";
|
|
8742
8918
|
function git3(args, cwd) {
|
|
8743
|
-
return
|
|
8919
|
+
return execSync15(`git ${args}`, {
|
|
8744
8920
|
cwd,
|
|
8745
8921
|
encoding: "utf-8",
|
|
8746
8922
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8754,10 +8930,10 @@ function gitSafe2(args, cwd) {
|
|
|
8754
8930
|
}
|
|
8755
8931
|
}
|
|
8756
8932
|
function getWorktreeDir(projectRoot) {
|
|
8757
|
-
return
|
|
8933
|
+
return join17(projectRoot, ".locus", "worktrees");
|
|
8758
8934
|
}
|
|
8759
8935
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
8760
|
-
return
|
|
8936
|
+
return join17(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
8761
8937
|
}
|
|
8762
8938
|
function generateBranchName(issueNumber) {
|
|
8763
8939
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -8765,7 +8941,7 @@ function generateBranchName(issueNumber) {
|
|
|
8765
8941
|
}
|
|
8766
8942
|
function getWorktreeBranch(worktreePath) {
|
|
8767
8943
|
try {
|
|
8768
|
-
return
|
|
8944
|
+
return execSync15("git branch --show-current", {
|
|
8769
8945
|
cwd: worktreePath,
|
|
8770
8946
|
encoding: "utf-8",
|
|
8771
8947
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8777,7 +8953,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
8777
8953
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
8778
8954
|
const log = getLogger();
|
|
8779
8955
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
8780
|
-
if (
|
|
8956
|
+
if (existsSync17(worktreePath)) {
|
|
8781
8957
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
8782
8958
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
8783
8959
|
return {
|
|
@@ -8804,7 +8980,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
8804
8980
|
function removeWorktree(projectRoot, issueNumber) {
|
|
8805
8981
|
const log = getLogger();
|
|
8806
8982
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
8807
|
-
if (!
|
|
8983
|
+
if (!existsSync17(worktreePath)) {
|
|
8808
8984
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
8809
8985
|
return;
|
|
8810
8986
|
}
|
|
@@ -8823,7 +8999,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
8823
8999
|
function listWorktrees(projectRoot) {
|
|
8824
9000
|
const log = getLogger();
|
|
8825
9001
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
8826
|
-
if (!
|
|
9002
|
+
if (!existsSync17(worktreeDir)) {
|
|
8827
9003
|
return [];
|
|
8828
9004
|
}
|
|
8829
9005
|
const entries = readdirSync6(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -8843,7 +9019,7 @@ function listWorktrees(projectRoot) {
|
|
|
8843
9019
|
if (!match)
|
|
8844
9020
|
continue;
|
|
8845
9021
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
8846
|
-
const path =
|
|
9022
|
+
const path = join17(worktreeDir, entry);
|
|
8847
9023
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
8848
9024
|
let resolvedPath;
|
|
8849
9025
|
try {
|
|
@@ -8890,7 +9066,7 @@ var exports_run = {};
|
|
|
8890
9066
|
__export(exports_run, {
|
|
8891
9067
|
runCommand: () => runCommand
|
|
8892
9068
|
});
|
|
8893
|
-
import { execSync as
|
|
9069
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
8894
9070
|
function printRunHelp() {
|
|
8895
9071
|
process.stderr.write(`
|
|
8896
9072
|
${bold("locus run")} — Execute issues using AI agents
|
|
@@ -8956,13 +9132,6 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
8956
9132
|
process.stderr.write(`${yellow("⚠")} Running without sandbox. The AI agent will have unrestricted access to your filesystem, network, and environment variables.
|
|
8957
9133
|
`);
|
|
8958
9134
|
}
|
|
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
9135
|
if (flags.resume) {
|
|
8967
9136
|
return handleResume(projectRoot, config, sandboxed);
|
|
8968
9137
|
}
|
|
@@ -9041,7 +9210,7 @@ ${yellow("⚠")} A sprint run is already in progress.
|
|
|
9041
9210
|
}
|
|
9042
9211
|
if (!flags.dryRun) {
|
|
9043
9212
|
try {
|
|
9044
|
-
|
|
9213
|
+
execSync16(`git checkout -B ${branchName}`, {
|
|
9045
9214
|
cwd: projectRoot,
|
|
9046
9215
|
encoding: "utf-8",
|
|
9047
9216
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9091,7 +9260,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
|
|
|
9091
9260
|
let sprintContext;
|
|
9092
9261
|
if (i > 0 && !flags.dryRun) {
|
|
9093
9262
|
try {
|
|
9094
|
-
sprintContext =
|
|
9263
|
+
sprintContext = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
9095
9264
|
cwd: projectRoot,
|
|
9096
9265
|
encoding: "utf-8",
|
|
9097
9266
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9111,13 +9280,16 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
9111
9280
|
dryRun: flags.dryRun,
|
|
9112
9281
|
sprintContext,
|
|
9113
9282
|
skipPR: true,
|
|
9114
|
-
sandboxed
|
|
9115
|
-
sandboxName: config.sandbox.name
|
|
9283
|
+
sandboxed
|
|
9116
9284
|
});
|
|
9117
9285
|
if (result.success) {
|
|
9118
9286
|
if (!flags.dryRun) {
|
|
9119
9287
|
const issueTitle = issue?.title ?? "";
|
|
9120
9288
|
ensureTaskCommit(projectRoot, task.issue, issueTitle);
|
|
9289
|
+
if (sandboxed && i < state.tasks.length - 1) {
|
|
9290
|
+
process.stderr.write(` ${dim("↻ Sandbox will resync on next task")}
|
|
9291
|
+
`);
|
|
9292
|
+
}
|
|
9121
9293
|
}
|
|
9122
9294
|
markTaskDone(state, task.issue, result.prNumber);
|
|
9123
9295
|
} else {
|
|
@@ -9152,7 +9324,7 @@ ${bold("Summary:")}
|
|
|
9152
9324
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
9153
9325
|
if (prNumber !== undefined) {
|
|
9154
9326
|
try {
|
|
9155
|
-
|
|
9327
|
+
execSync16(`git checkout ${config.agent.baseBranch}`, {
|
|
9156
9328
|
cwd: projectRoot,
|
|
9157
9329
|
encoding: "utf-8",
|
|
9158
9330
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9349,13 +9521,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9349
9521
|
`);
|
|
9350
9522
|
if (state.type === "sprint" && state.branch) {
|
|
9351
9523
|
try {
|
|
9352
|
-
const currentBranch =
|
|
9524
|
+
const currentBranch = execSync16("git rev-parse --abbrev-ref HEAD", {
|
|
9353
9525
|
cwd: projectRoot,
|
|
9354
9526
|
encoding: "utf-8",
|
|
9355
9527
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9356
9528
|
}).trim();
|
|
9357
9529
|
if (currentBranch !== state.branch) {
|
|
9358
|
-
|
|
9530
|
+
execSync16(`git checkout ${state.branch}`, {
|
|
9359
9531
|
cwd: projectRoot,
|
|
9360
9532
|
encoding: "utf-8",
|
|
9361
9533
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9383,7 +9555,7 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9383
9555
|
model: config.ai.model,
|
|
9384
9556
|
skipPR: isSprintRun,
|
|
9385
9557
|
sandboxed,
|
|
9386
|
-
sandboxName: config.sandbox.name
|
|
9558
|
+
sandboxName: isSprintRun ? undefined : config.sandbox.name
|
|
9387
9559
|
});
|
|
9388
9560
|
if (result.success) {
|
|
9389
9561
|
if (isSprintRun) {
|
|
@@ -9393,6 +9565,10 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9393
9565
|
issueTitle = iss.title;
|
|
9394
9566
|
} catch {}
|
|
9395
9567
|
ensureTaskCommit(projectRoot, task.issue, issueTitle);
|
|
9568
|
+
if (sandboxed) {
|
|
9569
|
+
process.stderr.write(` ${dim("↻ Sandbox will resync on next task")}
|
|
9570
|
+
`);
|
|
9571
|
+
}
|
|
9396
9572
|
}
|
|
9397
9573
|
markTaskDone(state, task.issue, result.prNumber);
|
|
9398
9574
|
} else {
|
|
@@ -9418,7 +9594,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
|
|
|
9418
9594
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
9419
9595
|
if (prNumber !== undefined) {
|
|
9420
9596
|
try {
|
|
9421
|
-
|
|
9597
|
+
execSync16(`git checkout ${config.agent.baseBranch}`, {
|
|
9422
9598
|
cwd: projectRoot,
|
|
9423
9599
|
encoding: "utf-8",
|
|
9424
9600
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9449,14 +9625,14 @@ function getOrder2(issue) {
|
|
|
9449
9625
|
}
|
|
9450
9626
|
function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
9451
9627
|
try {
|
|
9452
|
-
const status =
|
|
9628
|
+
const status = execSync16("git status --porcelain", {
|
|
9453
9629
|
cwd: projectRoot,
|
|
9454
9630
|
encoding: "utf-8",
|
|
9455
9631
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9456
9632
|
}).trim();
|
|
9457
9633
|
if (!status)
|
|
9458
9634
|
return;
|
|
9459
|
-
|
|
9635
|
+
execSync16("git add -A", {
|
|
9460
9636
|
cwd: projectRoot,
|
|
9461
9637
|
encoding: "utf-8",
|
|
9462
9638
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9464,7 +9640,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
9464
9640
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
9465
9641
|
|
|
9466
9642
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
9467
|
-
|
|
9643
|
+
execSync16(`git commit -F -`, {
|
|
9468
9644
|
input: message,
|
|
9469
9645
|
cwd: projectRoot,
|
|
9470
9646
|
encoding: "utf-8",
|
|
@@ -9478,7 +9654,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9478
9654
|
if (!config.agent.autoPR)
|
|
9479
9655
|
return;
|
|
9480
9656
|
try {
|
|
9481
|
-
const diff =
|
|
9657
|
+
const diff = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
9482
9658
|
cwd: projectRoot,
|
|
9483
9659
|
encoding: "utf-8",
|
|
9484
9660
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9488,7 +9664,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9488
9664
|
`);
|
|
9489
9665
|
return;
|
|
9490
9666
|
}
|
|
9491
|
-
|
|
9667
|
+
execSync16(`git push -u origin ${branchName}`, {
|
|
9492
9668
|
cwd: projectRoot,
|
|
9493
9669
|
encoding: "utf-8",
|
|
9494
9670
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9535,6 +9711,8 @@ __export(exports_status, {
|
|
|
9535
9711
|
});
|
|
9536
9712
|
async function statusCommand(projectRoot) {
|
|
9537
9713
|
const config = loadConfig(projectRoot);
|
|
9714
|
+
const spinner = new Spinner;
|
|
9715
|
+
spinner.start("Fetching project status...");
|
|
9538
9716
|
const lines = [];
|
|
9539
9717
|
lines.push(` ${dim("Repo:")} ${cyan(`${config.github.owner}/${config.github.repo}`)}`);
|
|
9540
9718
|
lines.push(` ${dim("Provider:")} ${config.ai.provider} / ${config.ai.model}`);
|
|
@@ -9611,6 +9789,7 @@ async function statusCommand(projectRoot) {
|
|
|
9611
9789
|
}
|
|
9612
9790
|
}
|
|
9613
9791
|
} catch {}
|
|
9792
|
+
spinner.stop();
|
|
9614
9793
|
lines.push("");
|
|
9615
9794
|
process.stderr.write(`
|
|
9616
9795
|
${drawBox(lines, { title: "Locus Status" })}
|
|
@@ -9634,13 +9813,13 @@ __export(exports_plan, {
|
|
|
9634
9813
|
parsePlanArgs: () => parsePlanArgs
|
|
9635
9814
|
});
|
|
9636
9815
|
import {
|
|
9637
|
-
existsSync as
|
|
9638
|
-
mkdirSync as
|
|
9816
|
+
existsSync as existsSync18,
|
|
9817
|
+
mkdirSync as mkdirSync12,
|
|
9639
9818
|
readdirSync as readdirSync7,
|
|
9640
9819
|
readFileSync as readFileSync13,
|
|
9641
9820
|
writeFileSync as writeFileSync9
|
|
9642
9821
|
} from "node:fs";
|
|
9643
|
-
import { join as
|
|
9822
|
+
import { join as join18 } from "node:path";
|
|
9644
9823
|
function printHelp() {
|
|
9645
9824
|
process.stderr.write(`
|
|
9646
9825
|
${bold("locus plan")} — AI-powered sprint planning
|
|
@@ -9671,12 +9850,12 @@ function normalizeSprintName(name) {
|
|
|
9671
9850
|
return name.trim().toLowerCase();
|
|
9672
9851
|
}
|
|
9673
9852
|
function getPlansDir(projectRoot) {
|
|
9674
|
-
return
|
|
9853
|
+
return join18(projectRoot, ".locus", "plans");
|
|
9675
9854
|
}
|
|
9676
9855
|
function ensurePlansDir(projectRoot) {
|
|
9677
9856
|
const dir = getPlansDir(projectRoot);
|
|
9678
|
-
if (!
|
|
9679
|
-
|
|
9857
|
+
if (!existsSync18(dir)) {
|
|
9858
|
+
mkdirSync12(dir, { recursive: true });
|
|
9680
9859
|
}
|
|
9681
9860
|
return dir;
|
|
9682
9861
|
}
|
|
@@ -9685,14 +9864,14 @@ function generateId() {
|
|
|
9685
9864
|
}
|
|
9686
9865
|
function loadPlanFile(projectRoot, id) {
|
|
9687
9866
|
const dir = getPlansDir(projectRoot);
|
|
9688
|
-
if (!
|
|
9867
|
+
if (!existsSync18(dir))
|
|
9689
9868
|
return null;
|
|
9690
9869
|
const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
|
|
9691
9870
|
const match = files.find((f) => f.startsWith(id));
|
|
9692
9871
|
if (!match)
|
|
9693
9872
|
return null;
|
|
9694
9873
|
try {
|
|
9695
|
-
const content = readFileSync13(
|
|
9874
|
+
const content = readFileSync13(join18(dir, match), "utf-8");
|
|
9696
9875
|
return JSON.parse(content);
|
|
9697
9876
|
} catch {
|
|
9698
9877
|
return null;
|
|
@@ -9738,7 +9917,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
9738
9917
|
}
|
|
9739
9918
|
function handleListPlans(projectRoot) {
|
|
9740
9919
|
const dir = getPlansDir(projectRoot);
|
|
9741
|
-
if (!
|
|
9920
|
+
if (!existsSync18(dir)) {
|
|
9742
9921
|
process.stderr.write(`${dim("No saved plans yet.")}
|
|
9743
9922
|
`);
|
|
9744
9923
|
return;
|
|
@@ -9756,7 +9935,7 @@ ${bold("Saved Plans:")}
|
|
|
9756
9935
|
for (const file of files) {
|
|
9757
9936
|
const id = file.replace(".json", "");
|
|
9758
9937
|
try {
|
|
9759
|
-
const content = readFileSync13(
|
|
9938
|
+
const content = readFileSync13(join18(dir, file), "utf-8");
|
|
9760
9939
|
const plan = JSON.parse(content);
|
|
9761
9940
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
9762
9941
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -9867,7 +10046,7 @@ ${bold("Approving plan:")}
|
|
|
9867
10046
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
9868
10047
|
const id = generateId();
|
|
9869
10048
|
const plansDir = ensurePlansDir(projectRoot);
|
|
9870
|
-
const planPath =
|
|
10049
|
+
const planPath = join18(plansDir, `${id}.json`);
|
|
9871
10050
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
9872
10051
|
const displayDirective = directive;
|
|
9873
10052
|
process.stderr.write(`
|
|
@@ -9901,7 +10080,7 @@ ${red("✗")} Planning failed: ${aiResult.error}
|
|
|
9901
10080
|
`);
|
|
9902
10081
|
return;
|
|
9903
10082
|
}
|
|
9904
|
-
if (!
|
|
10083
|
+
if (!existsSync18(planPath)) {
|
|
9905
10084
|
process.stderr.write(`
|
|
9906
10085
|
${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
|
|
9907
10086
|
`);
|
|
@@ -9979,16 +10158,21 @@ ${i.body?.slice(0, 300) ?? ""}`).join(`
|
|
|
9979
10158
|
|
|
9980
10159
|
`);
|
|
9981
10160
|
const { runAI: runAI2 } = await Promise.resolve().then(() => (init_run_ai(), exports_run_ai));
|
|
9982
|
-
const prompt =
|
|
10161
|
+
const prompt = `<role>
|
|
10162
|
+
You are organizing GitHub issues for a sprint. Analyze these issues and suggest the optimal execution order.
|
|
10163
|
+
</role>
|
|
9983
10164
|
|
|
9984
|
-
|
|
10165
|
+
<issues>
|
|
9985
10166
|
${issueDescriptions}
|
|
10167
|
+
</issues>
|
|
9986
10168
|
|
|
10169
|
+
<instructions>
|
|
9987
10170
|
For each issue, output a line in this format:
|
|
9988
10171
|
ORDER: #<number> <reason for this position>
|
|
9989
10172
|
|
|
9990
10173
|
Order them so that dependencies are respected (issues that produce code needed by later issues should come first).
|
|
9991
|
-
Start with foundational/setup tasks, then core features, then integration/testing
|
|
10174
|
+
Start with foundational/setup tasks, then core features, then integration/testing.
|
|
10175
|
+
</instructions>`;
|
|
9992
10176
|
const aiResult = await runAI2({
|
|
9993
10177
|
prompt,
|
|
9994
10178
|
provider: config.ai.provider,
|
|
@@ -10061,59 +10245,62 @@ ${bold("Suggested Order:")}
|
|
|
10061
10245
|
}
|
|
10062
10246
|
function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative) {
|
|
10063
10247
|
const parts = [];
|
|
10064
|
-
parts.push(
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10248
|
+
parts.push(`<role>
|
|
10249
|
+
You are a sprint planning assistant for the GitHub repository ${config.github.owner}/${config.github.repo}.
|
|
10250
|
+
</role>`);
|
|
10251
|
+
parts.push(`<directive>
|
|
10252
|
+
${directive}${sprintName ? `
|
|
10253
|
+
|
|
10254
|
+
**Sprint:** ${sprintName}` : ""}
|
|
10255
|
+
</directive>`);
|
|
10256
|
+
const locusPath = join18(projectRoot, ".locus", "LOCUS.md");
|
|
10257
|
+
if (existsSync18(locusPath)) {
|
|
10073
10258
|
const content = readFileSync13(locusPath, "utf-8");
|
|
10074
|
-
parts.push(
|
|
10075
|
-
|
|
10076
|
-
|
|
10259
|
+
parts.push(`<project-context>
|
|
10260
|
+
${content.slice(0, 3000)}
|
|
10261
|
+
</project-context>`);
|
|
10077
10262
|
}
|
|
10078
|
-
const learningsPath =
|
|
10079
|
-
if (
|
|
10263
|
+
const learningsPath = join18(projectRoot, ".locus", "LEARNINGS.md");
|
|
10264
|
+
if (existsSync18(learningsPath)) {
|
|
10080
10265
|
const content = readFileSync13(learningsPath, "utf-8");
|
|
10081
|
-
parts.push(
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
}
|
|
10085
|
-
parts.push(
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
parts.push(
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10266
|
+
parts.push(`<past-learnings>
|
|
10267
|
+
${content.slice(0, 2000)}
|
|
10268
|
+
</past-learnings>`);
|
|
10269
|
+
}
|
|
10270
|
+
parts.push(`<task>
|
|
10271
|
+
Break down the directive into specific, actionable GitHub issues and write them to the file: ${planPathRelative}
|
|
10272
|
+
|
|
10273
|
+
Write ONLY a valid JSON file to ${planPathRelative} with this exact structure:
|
|
10274
|
+
|
|
10275
|
+
\`\`\`json
|
|
10276
|
+
{
|
|
10277
|
+
"id": "${id}",
|
|
10278
|
+
"directive": ${JSON.stringify(directive)},
|
|
10279
|
+
"sprint": ${sprintName ? JSON.stringify(sprintName) : "null"},
|
|
10280
|
+
"createdAt": "${new Date().toISOString()}",
|
|
10281
|
+
"issues": [
|
|
10282
|
+
{
|
|
10283
|
+
"order": 1,
|
|
10284
|
+
"title": "concise issue title",
|
|
10285
|
+
"body": "detailed markdown body with acceptance criteria",
|
|
10286
|
+
"priority": "critical|high|medium|low",
|
|
10287
|
+
"type": "feature|bug|chore|refactor|docs",
|
|
10288
|
+
"dependsOn": "none or comma-separated order numbers"
|
|
10289
|
+
}
|
|
10290
|
+
]
|
|
10291
|
+
}
|
|
10292
|
+
\`\`\`
|
|
10293
|
+
</task>`);
|
|
10294
|
+
parts.push(`<requirements>
|
|
10295
|
+
- Break the directive into 3-10 specific, actionable issues
|
|
10296
|
+
- Each issue must be independently executable by an AI agent
|
|
10297
|
+
- Order them so dependencies are respected (foundational tasks first)
|
|
10298
|
+
- Write detailed issue bodies with clear acceptance criteria
|
|
10299
|
+
- Use valid GitHub Markdown only in issue bodies
|
|
10300
|
+
- Create the file using the Write tool — do not print the JSON to the terminal
|
|
10301
|
+
</requirements>`);
|
|
10116
10302
|
return parts.join(`
|
|
10303
|
+
|
|
10117
10304
|
`);
|
|
10118
10305
|
}
|
|
10119
10306
|
function sanitizePlanOutput(output) {
|
|
@@ -10253,9 +10440,9 @@ var exports_review = {};
|
|
|
10253
10440
|
__export(exports_review, {
|
|
10254
10441
|
reviewCommand: () => reviewCommand
|
|
10255
10442
|
});
|
|
10256
|
-
import { execSync as
|
|
10257
|
-
import { existsSync as
|
|
10258
|
-
import { join as
|
|
10443
|
+
import { execSync as execSync17 } from "node:child_process";
|
|
10444
|
+
import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
|
|
10445
|
+
import { join as join19 } from "node:path";
|
|
10259
10446
|
function printHelp2() {
|
|
10260
10447
|
process.stderr.write(`
|
|
10261
10448
|
${bold("locus review")} — AI-powered code review
|
|
@@ -10331,7 +10518,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
|
|
|
10331
10518
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
10332
10519
|
let prInfo;
|
|
10333
10520
|
try {
|
|
10334
|
-
const result =
|
|
10521
|
+
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
10522
|
const raw = JSON.parse(result);
|
|
10336
10523
|
prInfo = {
|
|
10337
10524
|
number: raw.number,
|
|
@@ -10397,7 +10584,7 @@ ${output.slice(0, 60000)}
|
|
|
10397
10584
|
|
|
10398
10585
|
---
|
|
10399
10586
|
_Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
|
|
10400
|
-
|
|
10587
|
+
execSync17(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10401
10588
|
process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
|
|
10402
10589
|
`);
|
|
10403
10590
|
} catch (e) {
|
|
@@ -10412,47 +10599,55 @@ _Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_
|
|
|
10412
10599
|
}
|
|
10413
10600
|
function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
10414
10601
|
const parts = [];
|
|
10415
|
-
parts.push(
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10602
|
+
parts.push(`<role>
|
|
10603
|
+
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
10604
|
+
</role>`);
|
|
10605
|
+
const locusPath = join19(projectRoot, ".locus", "LOCUS.md");
|
|
10606
|
+
if (existsSync19(locusPath)) {
|
|
10419
10607
|
const content = readFileSync14(locusPath, "utf-8");
|
|
10420
|
-
parts.push(
|
|
10421
|
-
|
|
10422
|
-
|
|
10608
|
+
parts.push(`<project-context>
|
|
10609
|
+
${content.slice(0, 2000)}
|
|
10610
|
+
</project-context>`);
|
|
10423
10611
|
}
|
|
10424
|
-
|
|
10425
|
-
parts.push(`Branch: ${pr.head} → ${pr.base}`);
|
|
10612
|
+
const prMeta = [`Branch: ${pr.head} → ${pr.base}`];
|
|
10426
10613
|
if (pr.body) {
|
|
10427
|
-
|
|
10614
|
+
prMeta.push(`Description:
|
|
10428
10615
|
${pr.body.slice(0, 1000)}`);
|
|
10429
10616
|
}
|
|
10430
|
-
parts.push(""
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
parts.push(
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10617
|
+
parts.push(`<pull-request number="${pr.number}" title="${pr.title}">
|
|
10618
|
+
${prMeta.join(`
|
|
10619
|
+
`)}
|
|
10620
|
+
</pull-request>`);
|
|
10621
|
+
parts.push(`<diff>
|
|
10622
|
+
${diff.slice(0, 50000)}
|
|
10623
|
+
</diff>`);
|
|
10624
|
+
let instructions = `Provide a thorough code review. For each issue found, describe:
|
|
10625
|
+
1. The file and approximate location
|
|
10626
|
+
2. What the issue is
|
|
10627
|
+
3. Why it matters
|
|
10628
|
+
4. How to fix it
|
|
10629
|
+
|
|
10630
|
+
Categories to check:
|
|
10631
|
+
- Correctness: bugs, logic errors, edge cases
|
|
10632
|
+
- Security: injection, XSS, auth issues, secret exposure
|
|
10633
|
+
- Performance: N+1 queries, unnecessary allocations, missing caching
|
|
10634
|
+
- Maintainability: naming, complexity, code organization
|
|
10635
|
+
- Testing: missing tests, inadequate coverage`;
|
|
10447
10636
|
if (focus) {
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10637
|
+
instructions += `
|
|
10638
|
+
|
|
10639
|
+
**Focus areas:** ${focus}
|
|
10640
|
+
Pay special attention to the above areas.`;
|
|
10451
10641
|
}
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10642
|
+
instructions += `
|
|
10643
|
+
|
|
10644
|
+
End with an overall assessment: APPROVE, REQUEST_CHANGES, or COMMENT.
|
|
10645
|
+
Be constructive and specific. Praise good patterns too.`;
|
|
10646
|
+
parts.push(`<review-instructions>
|
|
10647
|
+
${instructions}
|
|
10648
|
+
</review-instructions>`);
|
|
10455
10649
|
return parts.join(`
|
|
10650
|
+
|
|
10456
10651
|
`);
|
|
10457
10652
|
}
|
|
10458
10653
|
var init_review = __esm(() => {
|
|
@@ -10468,7 +10663,7 @@ var exports_iterate = {};
|
|
|
10468
10663
|
__export(exports_iterate, {
|
|
10469
10664
|
iterateCommand: () => iterateCommand
|
|
10470
10665
|
});
|
|
10471
|
-
import { execSync as
|
|
10666
|
+
import { execSync as execSync18 } from "node:child_process";
|
|
10472
10667
|
function printHelp3() {
|
|
10473
10668
|
process.stderr.write(`
|
|
10474
10669
|
${bold("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -10678,12 +10873,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
|
|
|
10678
10873
|
}
|
|
10679
10874
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
10680
10875
|
try {
|
|
10681
|
-
const result =
|
|
10876
|
+
const result = execSync18(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10682
10877
|
const parsed = JSON.parse(result);
|
|
10683
10878
|
if (parsed.length > 0) {
|
|
10684
10879
|
return parsed[0].number;
|
|
10685
10880
|
}
|
|
10686
|
-
const branchResult =
|
|
10881
|
+
const branchResult = execSync18(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10687
10882
|
const branchParsed = JSON.parse(branchResult);
|
|
10688
10883
|
if (branchParsed.length > 0) {
|
|
10689
10884
|
return branchParsed[0].number;
|
|
@@ -10718,14 +10913,14 @@ __export(exports_discuss, {
|
|
|
10718
10913
|
discussCommand: () => discussCommand
|
|
10719
10914
|
});
|
|
10720
10915
|
import {
|
|
10721
|
-
existsSync as
|
|
10722
|
-
mkdirSync as
|
|
10916
|
+
existsSync as existsSync20,
|
|
10917
|
+
mkdirSync as mkdirSync13,
|
|
10723
10918
|
readdirSync as readdirSync8,
|
|
10724
10919
|
readFileSync as readFileSync15,
|
|
10725
10920
|
unlinkSync as unlinkSync5,
|
|
10726
10921
|
writeFileSync as writeFileSync10
|
|
10727
10922
|
} from "node:fs";
|
|
10728
|
-
import { join as
|
|
10923
|
+
import { join as join20 } from "node:path";
|
|
10729
10924
|
function printHelp4() {
|
|
10730
10925
|
process.stderr.write(`
|
|
10731
10926
|
${bold("locus discuss")} — AI-powered architectural discussions
|
|
@@ -10747,12 +10942,12 @@ ${bold("Examples:")}
|
|
|
10747
10942
|
`);
|
|
10748
10943
|
}
|
|
10749
10944
|
function getDiscussionsDir(projectRoot) {
|
|
10750
|
-
return
|
|
10945
|
+
return join20(projectRoot, ".locus", "discussions");
|
|
10751
10946
|
}
|
|
10752
10947
|
function ensureDiscussionsDir(projectRoot) {
|
|
10753
10948
|
const dir = getDiscussionsDir(projectRoot);
|
|
10754
|
-
if (!
|
|
10755
|
-
|
|
10949
|
+
if (!existsSync20(dir)) {
|
|
10950
|
+
mkdirSync13(dir, { recursive: true });
|
|
10756
10951
|
}
|
|
10757
10952
|
return dir;
|
|
10758
10953
|
}
|
|
@@ -10786,7 +10981,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
10786
10981
|
}
|
|
10787
10982
|
function listDiscussions(projectRoot) {
|
|
10788
10983
|
const dir = getDiscussionsDir(projectRoot);
|
|
10789
|
-
if (!
|
|
10984
|
+
if (!existsSync20(dir)) {
|
|
10790
10985
|
process.stderr.write(`${dim("No discussions yet.")}
|
|
10791
10986
|
`);
|
|
10792
10987
|
return;
|
|
@@ -10803,7 +10998,7 @@ ${bold("Discussions:")}
|
|
|
10803
10998
|
`);
|
|
10804
10999
|
for (const file of files) {
|
|
10805
11000
|
const id = file.replace(".md", "");
|
|
10806
|
-
const content = readFileSync15(
|
|
11001
|
+
const content = readFileSync15(join20(dir, file), "utf-8");
|
|
10807
11002
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
10808
11003
|
const title = titleMatch ? titleMatch[1] : id;
|
|
10809
11004
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -10821,7 +11016,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
10821
11016
|
return;
|
|
10822
11017
|
}
|
|
10823
11018
|
const dir = getDiscussionsDir(projectRoot);
|
|
10824
|
-
if (!
|
|
11019
|
+
if (!existsSync20(dir)) {
|
|
10825
11020
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10826
11021
|
`);
|
|
10827
11022
|
return;
|
|
@@ -10833,7 +11028,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
10833
11028
|
`);
|
|
10834
11029
|
return;
|
|
10835
11030
|
}
|
|
10836
|
-
const content = readFileSync15(
|
|
11031
|
+
const content = readFileSync15(join20(dir, match), "utf-8");
|
|
10837
11032
|
process.stdout.write(`${content}
|
|
10838
11033
|
`);
|
|
10839
11034
|
}
|
|
@@ -10844,7 +11039,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
10844
11039
|
return;
|
|
10845
11040
|
}
|
|
10846
11041
|
const dir = getDiscussionsDir(projectRoot);
|
|
10847
|
-
if (!
|
|
11042
|
+
if (!existsSync20(dir)) {
|
|
10848
11043
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10849
11044
|
`);
|
|
10850
11045
|
return;
|
|
@@ -10856,7 +11051,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
10856
11051
|
`);
|
|
10857
11052
|
return;
|
|
10858
11053
|
}
|
|
10859
|
-
unlinkSync5(
|
|
11054
|
+
unlinkSync5(join20(dir, match));
|
|
10860
11055
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
10861
11056
|
`);
|
|
10862
11057
|
}
|
|
@@ -10869,7 +11064,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
10869
11064
|
return;
|
|
10870
11065
|
}
|
|
10871
11066
|
const dir = getDiscussionsDir(projectRoot);
|
|
10872
|
-
if (!
|
|
11067
|
+
if (!existsSync20(dir)) {
|
|
10873
11068
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10874
11069
|
`);
|
|
10875
11070
|
return;
|
|
@@ -10881,7 +11076,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
10881
11076
|
`);
|
|
10882
11077
|
return;
|
|
10883
11078
|
}
|
|
10884
|
-
const content = readFileSync15(
|
|
11079
|
+
const content = readFileSync15(join20(dir, match), "utf-8");
|
|
10885
11080
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
10886
11081
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
10887
11082
|
await planCommand(projectRoot, [
|
|
@@ -10995,7 +11190,7 @@ ${turn.content}`;
|
|
|
10995
11190
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
10996
11191
|
].join(`
|
|
10997
11192
|
`);
|
|
10998
|
-
writeFileSync10(
|
|
11193
|
+
writeFileSync10(join20(dir, `${id}.md`), markdown, "utf-8");
|
|
10999
11194
|
process.stderr.write(`
|
|
11000
11195
|
${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
|
|
11001
11196
|
`);
|
|
@@ -11007,55 +11202,67 @@ ${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
|
|
|
11007
11202
|
}
|
|
11008
11203
|
function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
|
|
11009
11204
|
const parts = [];
|
|
11010
|
-
parts.push(
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11205
|
+
parts.push(`<role>
|
|
11206
|
+
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
11207
|
+
</role>`);
|
|
11208
|
+
const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
|
|
11209
|
+
if (existsSync20(locusPath)) {
|
|
11014
11210
|
const content = readFileSync15(locusPath, "utf-8");
|
|
11015
|
-
parts.push(
|
|
11016
|
-
|
|
11017
|
-
|
|
11211
|
+
parts.push(`<project-context>
|
|
11212
|
+
${content.slice(0, 3000)}
|
|
11213
|
+
</project-context>`);
|
|
11018
11214
|
}
|
|
11019
|
-
const learningsPath =
|
|
11020
|
-
if (
|
|
11215
|
+
const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
|
|
11216
|
+
if (existsSync20(learningsPath)) {
|
|
11021
11217
|
const content = readFileSync15(learningsPath, "utf-8");
|
|
11022
|
-
parts.push(
|
|
11023
|
-
|
|
11024
|
-
|
|
11218
|
+
parts.push(`<past-learnings>
|
|
11219
|
+
${content.slice(0, 2000)}
|
|
11220
|
+
</past-learnings>`);
|
|
11025
11221
|
}
|
|
11026
|
-
parts.push(
|
|
11027
|
-
|
|
11222
|
+
parts.push(`<discussion-topic>
|
|
11223
|
+
${topic}
|
|
11224
|
+
</discussion-topic>`);
|
|
11028
11225
|
if (conversation.length === 0) {
|
|
11029
|
-
parts.push(
|
|
11030
|
-
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
|
|
11226
|
+
parts.push(`<instructions>
|
|
11227
|
+
Before providing recommendations, you need to ask targeted clarifying questions.
|
|
11228
|
+
|
|
11229
|
+
Ask 3-5 focused questions that will significantly improve the quality of your analysis.
|
|
11230
|
+
Format as a numbered list. Be specific and focused on the most important unknowns.
|
|
11231
|
+
Do NOT provide any analysis yet — questions only.
|
|
11232
|
+
</instructions>`);
|
|
11034
11233
|
} else {
|
|
11035
|
-
|
|
11036
|
-
parts.push("");
|
|
11234
|
+
const historyLines = [];
|
|
11037
11235
|
for (const turn of conversation) {
|
|
11038
11236
|
if (turn.role === "user") {
|
|
11039
|
-
|
|
11237
|
+
historyLines.push(`USER: ${turn.content}`);
|
|
11040
11238
|
} else {
|
|
11041
|
-
|
|
11239
|
+
historyLines.push(`ASSISTANT: ${turn.content}`);
|
|
11042
11240
|
}
|
|
11043
|
-
parts.push("");
|
|
11044
11241
|
}
|
|
11242
|
+
parts.push(`<conversation-history>
|
|
11243
|
+
${historyLines.join(`
|
|
11244
|
+
|
|
11245
|
+
`)}
|
|
11246
|
+
</conversation-history>`);
|
|
11045
11247
|
if (forceFinal) {
|
|
11046
|
-
parts.push(
|
|
11047
|
-
|
|
11248
|
+
parts.push(`<instructions>
|
|
11249
|
+
Based on everything discussed, provide your complete analysis and recommendations now.
|
|
11250
|
+
Format as a thorough markdown document with a clear title (# Heading), sections, trade-offs, and actionable recommendations.
|
|
11251
|
+
</instructions>`);
|
|
11048
11252
|
} else {
|
|
11049
|
-
parts.push(
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11253
|
+
parts.push(`<instructions>
|
|
11254
|
+
Review the information gathered so far.
|
|
11255
|
+
|
|
11256
|
+
If you have enough information to make a thorough recommendation:
|
|
11257
|
+
→ Provide a complete analysis as a markdown document with a title (# Heading), sections, trade-offs, and concrete recommendations.
|
|
11258
|
+
|
|
11259
|
+
If you still need key information to give a good answer:
|
|
11260
|
+
→ Ask 2-3 more focused follow-up questions (numbered list only, no analysis yet).
|
|
11261
|
+
</instructions>`);
|
|
11056
11262
|
}
|
|
11057
11263
|
}
|
|
11058
11264
|
return parts.join(`
|
|
11265
|
+
|
|
11059
11266
|
`);
|
|
11060
11267
|
}
|
|
11061
11268
|
var MAX_DISCUSSION_ROUNDS = 5;
|
|
@@ -11077,8 +11284,8 @@ __export(exports_artifacts, {
|
|
|
11077
11284
|
formatDate: () => formatDate2,
|
|
11078
11285
|
artifactsCommand: () => artifactsCommand
|
|
11079
11286
|
});
|
|
11080
|
-
import { existsSync as
|
|
11081
|
-
import { join as
|
|
11287
|
+
import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
|
|
11288
|
+
import { join as join21 } from "node:path";
|
|
11082
11289
|
function printHelp5() {
|
|
11083
11290
|
process.stderr.write(`
|
|
11084
11291
|
${bold("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -11098,14 +11305,14 @@ ${dim("Artifact names support partial matching.")}
|
|
|
11098
11305
|
`);
|
|
11099
11306
|
}
|
|
11100
11307
|
function getArtifactsDir(projectRoot) {
|
|
11101
|
-
return
|
|
11308
|
+
return join21(projectRoot, ".locus", "artifacts");
|
|
11102
11309
|
}
|
|
11103
11310
|
function listArtifacts(projectRoot) {
|
|
11104
11311
|
const dir = getArtifactsDir(projectRoot);
|
|
11105
|
-
if (!
|
|
11312
|
+
if (!existsSync21(dir))
|
|
11106
11313
|
return [];
|
|
11107
11314
|
return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
11108
|
-
const filePath =
|
|
11315
|
+
const filePath = join21(dir, fileName);
|
|
11109
11316
|
const stat = statSync4(filePath);
|
|
11110
11317
|
return {
|
|
11111
11318
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -11118,8 +11325,8 @@ function listArtifacts(projectRoot) {
|
|
|
11118
11325
|
function readArtifact(projectRoot, name) {
|
|
11119
11326
|
const dir = getArtifactsDir(projectRoot);
|
|
11120
11327
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
11121
|
-
const filePath =
|
|
11122
|
-
if (!
|
|
11328
|
+
const filePath = join21(dir, fileName);
|
|
11329
|
+
if (!existsSync21(filePath))
|
|
11123
11330
|
return null;
|
|
11124
11331
|
const stat = statSync4(filePath);
|
|
11125
11332
|
return {
|
|
@@ -11282,11 +11489,11 @@ var init_artifacts = __esm(() => {
|
|
|
11282
11489
|
});
|
|
11283
11490
|
|
|
11284
11491
|
// src/commands/sandbox.ts
|
|
11285
|
-
var
|
|
11286
|
-
__export(
|
|
11492
|
+
var exports_sandbox2 = {};
|
|
11493
|
+
__export(exports_sandbox2, {
|
|
11287
11494
|
sandboxCommand: () => sandboxCommand
|
|
11288
11495
|
});
|
|
11289
|
-
import { execSync as
|
|
11496
|
+
import { execSync as execSync19, spawn as spawn6 } from "node:child_process";
|
|
11290
11497
|
function printSandboxHelp() {
|
|
11291
11498
|
process.stderr.write(`
|
|
11292
11499
|
${bold("locus sandbox")} — Manage Docker sandbox lifecycle
|
|
@@ -11400,7 +11607,7 @@ async function handleAgentLogin(projectRoot, agent) {
|
|
|
11400
11607
|
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11401
11608
|
`);
|
|
11402
11609
|
try {
|
|
11403
|
-
|
|
11610
|
+
execSync19(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
11404
11611
|
} catch {}
|
|
11405
11612
|
if (!isSandboxAlive(sandboxName)) {
|
|
11406
11613
|
process.stderr.write(`${red("✗")} Failed to create sandbox.
|
|
@@ -11463,7 +11670,7 @@ function handleRemove(projectRoot) {
|
|
|
11463
11670
|
process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
|
|
11464
11671
|
`);
|
|
11465
11672
|
try {
|
|
11466
|
-
|
|
11673
|
+
execSync19(`docker sandbox rm ${sandboxName}`, {
|
|
11467
11674
|
encoding: "utf-8",
|
|
11468
11675
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11469
11676
|
timeout: 15000
|
|
@@ -11500,7 +11707,7 @@ ${bold("Sandbox Status")}
|
|
|
11500
11707
|
}
|
|
11501
11708
|
async function ensureCodexInSandbox(sandboxName) {
|
|
11502
11709
|
try {
|
|
11503
|
-
|
|
11710
|
+
execSync19(`docker sandbox exec ${sandboxName} which codex`, {
|
|
11504
11711
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11505
11712
|
timeout: 5000
|
|
11506
11713
|
});
|
|
@@ -11508,7 +11715,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11508
11715
|
process.stderr.write(`Installing codex in sandbox...
|
|
11509
11716
|
`);
|
|
11510
11717
|
try {
|
|
11511
|
-
|
|
11718
|
+
execSync19(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
11512
11719
|
} catch {
|
|
11513
11720
|
process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
|
|
11514
11721
|
`);
|
|
@@ -11517,7 +11724,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11517
11724
|
}
|
|
11518
11725
|
function isSandboxAlive(name) {
|
|
11519
11726
|
try {
|
|
11520
|
-
const output =
|
|
11727
|
+
const output = execSync19("docker sandbox ls", {
|
|
11521
11728
|
encoding: "utf-8",
|
|
11522
11729
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11523
11730
|
timeout: 5000
|
|
@@ -11540,13 +11747,13 @@ init_context();
|
|
|
11540
11747
|
init_logger();
|
|
11541
11748
|
init_rate_limiter();
|
|
11542
11749
|
init_terminal();
|
|
11543
|
-
import { existsSync as
|
|
11544
|
-
import { join as
|
|
11750
|
+
import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
|
|
11751
|
+
import { join as join22 } from "node:path";
|
|
11545
11752
|
import { fileURLToPath } from "node:url";
|
|
11546
11753
|
function getCliVersion() {
|
|
11547
11754
|
const fallbackVersion = "0.0.0";
|
|
11548
|
-
const packageJsonPath =
|
|
11549
|
-
if (!
|
|
11755
|
+
const packageJsonPath = join22(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
11756
|
+
if (!existsSync22(packageJsonPath)) {
|
|
11550
11757
|
return fallbackVersion;
|
|
11551
11758
|
}
|
|
11552
11759
|
try {
|
|
@@ -11720,6 +11927,46 @@ function resolveAlias(command) {
|
|
|
11720
11927
|
};
|
|
11721
11928
|
return aliases[command] ?? command;
|
|
11722
11929
|
}
|
|
11930
|
+
function requiresSandboxSync(command, args, flags) {
|
|
11931
|
+
if (flags.noSandbox)
|
|
11932
|
+
return false;
|
|
11933
|
+
if (flags.help)
|
|
11934
|
+
return false;
|
|
11935
|
+
switch (command) {
|
|
11936
|
+
case "run":
|
|
11937
|
+
case "review":
|
|
11938
|
+
case "iterate":
|
|
11939
|
+
return true;
|
|
11940
|
+
case "exec":
|
|
11941
|
+
return args[0] !== "sessions" && args[0] !== "help";
|
|
11942
|
+
case "issue":
|
|
11943
|
+
return args[0] === "create";
|
|
11944
|
+
case "plan":
|
|
11945
|
+
if (args.length === 0)
|
|
11946
|
+
return false;
|
|
11947
|
+
return !["list", "show", "approve", "help"].includes(args[0]);
|
|
11948
|
+
case "discuss":
|
|
11949
|
+
if (args.length === 0)
|
|
11950
|
+
return false;
|
|
11951
|
+
return !["list", "show", "delete", "help"].includes(args[0]);
|
|
11952
|
+
case "config":
|
|
11953
|
+
return args[0] === "set";
|
|
11954
|
+
default:
|
|
11955
|
+
return false;
|
|
11956
|
+
}
|
|
11957
|
+
}
|
|
11958
|
+
async function prepareSandbox() {
|
|
11959
|
+
const { Spinner: Spinner2 } = await Promise.resolve().then(() => (init_progress(), exports_progress));
|
|
11960
|
+
const { detectSandboxSupport: detectSandboxSupport2 } = await Promise.resolve().then(() => (init_sandbox(), exports_sandbox));
|
|
11961
|
+
const spinner = new Spinner2;
|
|
11962
|
+
spinner.start("Preparing sandbox...");
|
|
11963
|
+
const status = await detectSandboxSupport2();
|
|
11964
|
+
if (status.available) {
|
|
11965
|
+
spinner.succeed("Sandbox ready");
|
|
11966
|
+
} else {
|
|
11967
|
+
spinner.warn(`Sandbox not available: ${status.reason}`);
|
|
11968
|
+
}
|
|
11969
|
+
}
|
|
11723
11970
|
async function main() {
|
|
11724
11971
|
const parsed = parseArgs(process.argv);
|
|
11725
11972
|
if (parsed.flags.version) {
|
|
@@ -11739,7 +11986,7 @@ async function main() {
|
|
|
11739
11986
|
try {
|
|
11740
11987
|
const root = getGitRoot(cwd);
|
|
11741
11988
|
if (isInitialized(root)) {
|
|
11742
|
-
logDir =
|
|
11989
|
+
logDir = join22(root, ".locus", "logs");
|
|
11743
11990
|
getRateLimiter(root);
|
|
11744
11991
|
}
|
|
11745
11992
|
} catch {}
|
|
@@ -11821,6 +12068,12 @@ async function main() {
|
|
|
11821
12068
|
`);
|
|
11822
12069
|
process.exit(1);
|
|
11823
12070
|
}
|
|
12071
|
+
if (requiresSandboxSync(command, parsed.args, parsed.flags)) {
|
|
12072
|
+
const config = loadConfig(projectRoot);
|
|
12073
|
+
if (config.sandbox.enabled) {
|
|
12074
|
+
await prepareSandbox();
|
|
12075
|
+
}
|
|
12076
|
+
}
|
|
11824
12077
|
switch (command) {
|
|
11825
12078
|
case "config": {
|
|
11826
12079
|
const { configCommand: configCommand2 } = await Promise.resolve().then(() => (init_config2(), exports_config));
|
|
@@ -11917,7 +12170,7 @@ async function main() {
|
|
|
11917
12170
|
break;
|
|
11918
12171
|
}
|
|
11919
12172
|
case "sandbox": {
|
|
11920
|
-
const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(),
|
|
12173
|
+
const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(), exports_sandbox2));
|
|
11921
12174
|
const sandboxArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
11922
12175
|
await sandboxCommand2(projectRoot, sandboxArgs);
|
|
11923
12176
|
break;
|