@locusai/cli 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/locus.js +1295 -1428
- package/package.json +1 -1
package/bin/locus.js
CHANGED
|
@@ -62,6 +62,143 @@ var init_ai_models = __esm(() => {
|
|
|
62
62
|
ALL_MODEL_SET = new Set([...CLAUDE_MODELS, ...CODEX_MODELS]);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
// src/core/config.ts
|
|
66
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
67
|
+
import { dirname, join } from "node:path";
|
|
68
|
+
function getConfigPath(projectRoot) {
|
|
69
|
+
return join(projectRoot, ".locus", "config.json");
|
|
70
|
+
}
|
|
71
|
+
function loadConfig(projectRoot) {
|
|
72
|
+
const configPath = getConfigPath(projectRoot);
|
|
73
|
+
if (!existsSync(configPath)) {
|
|
74
|
+
throw new Error(`No Locus config found at ${configPath}. Run "locus init" first.`);
|
|
75
|
+
}
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw new Error(`Failed to parse config at ${configPath}: ${e}`);
|
|
81
|
+
}
|
|
82
|
+
const config = deepMerge(DEFAULT_CONFIG, raw);
|
|
83
|
+
const inferredProvider = inferProviderFromModel(config.ai.model);
|
|
84
|
+
if (inferredProvider) {
|
|
85
|
+
config.ai.provider = inferredProvider;
|
|
86
|
+
}
|
|
87
|
+
if (raw.version !== config.version) {
|
|
88
|
+
saveConfig(projectRoot, config);
|
|
89
|
+
}
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
function saveConfig(projectRoot, config) {
|
|
93
|
+
const configPath = getConfigPath(projectRoot);
|
|
94
|
+
const dir = dirname(configPath);
|
|
95
|
+
if (!existsSync(dir)) {
|
|
96
|
+
mkdirSync(dir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
99
|
+
`, "utf-8");
|
|
100
|
+
}
|
|
101
|
+
function updateConfigValue(projectRoot, path, value) {
|
|
102
|
+
const config = loadConfig(projectRoot);
|
|
103
|
+
setNestedValue(config, path, value);
|
|
104
|
+
if (path === "ai.model" && typeof value === "string") {
|
|
105
|
+
const inferredProvider = inferProviderFromModel(value);
|
|
106
|
+
if (inferredProvider) {
|
|
107
|
+
config.ai.provider = inferredProvider;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
saveConfig(projectRoot, config);
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
function isInitialized(projectRoot) {
|
|
114
|
+
return existsSync(join(projectRoot, ".locus", "config.json"));
|
|
115
|
+
}
|
|
116
|
+
function deepMerge(target, source) {
|
|
117
|
+
if (typeof target !== "object" || target === null || typeof source !== "object" || source === null) {
|
|
118
|
+
return source ?? target;
|
|
119
|
+
}
|
|
120
|
+
const result = {
|
|
121
|
+
...target
|
|
122
|
+
};
|
|
123
|
+
const src = source;
|
|
124
|
+
for (const key of Object.keys(src)) {
|
|
125
|
+
if (key in result && typeof result[key] === "object" && result[key] !== null && typeof src[key] === "object" && src[key] !== null && !Array.isArray(result[key])) {
|
|
126
|
+
result[key] = deepMerge(result[key], src[key]);
|
|
127
|
+
} else if (src[key] !== undefined) {
|
|
128
|
+
result[key] = src[key];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
function setNestedValue(obj, path, value) {
|
|
134
|
+
const keys = path.split(".");
|
|
135
|
+
let current = obj;
|
|
136
|
+
for (let i = 0;i < keys.length - 1; i++) {
|
|
137
|
+
const key = keys[i];
|
|
138
|
+
if (typeof current[key] !== "object" || current[key] === null) {
|
|
139
|
+
current[key] = {};
|
|
140
|
+
}
|
|
141
|
+
current = current[key];
|
|
142
|
+
}
|
|
143
|
+
const lastKey = keys[keys.length - 1];
|
|
144
|
+
let coerced = value;
|
|
145
|
+
if (value === "true")
|
|
146
|
+
coerced = true;
|
|
147
|
+
else if (value === "false")
|
|
148
|
+
coerced = false;
|
|
149
|
+
else if (typeof value === "string" && /^\d+$/.test(value))
|
|
150
|
+
coerced = Number.parseInt(value, 10);
|
|
151
|
+
current[lastKey] = coerced;
|
|
152
|
+
}
|
|
153
|
+
function getNestedValue(obj, path) {
|
|
154
|
+
const keys = path.split(".");
|
|
155
|
+
let current = obj;
|
|
156
|
+
for (const key of keys) {
|
|
157
|
+
if (typeof current !== "object" || current === null)
|
|
158
|
+
return;
|
|
159
|
+
current = current[key];
|
|
160
|
+
}
|
|
161
|
+
return current;
|
|
162
|
+
}
|
|
163
|
+
var DEFAULT_CONFIG;
|
|
164
|
+
var init_config = __esm(() => {
|
|
165
|
+
init_ai_models();
|
|
166
|
+
DEFAULT_CONFIG = {
|
|
167
|
+
version: "3.2.0",
|
|
168
|
+
github: {
|
|
169
|
+
owner: "",
|
|
170
|
+
repo: "",
|
|
171
|
+
defaultBranch: "main"
|
|
172
|
+
},
|
|
173
|
+
ai: {
|
|
174
|
+
provider: "claude",
|
|
175
|
+
model: "claude-sonnet-4-6"
|
|
176
|
+
},
|
|
177
|
+
agent: {
|
|
178
|
+
maxParallel: 3,
|
|
179
|
+
autoLabel: true,
|
|
180
|
+
autoPR: true,
|
|
181
|
+
baseBranch: "main",
|
|
182
|
+
rebaseBeforeTask: true
|
|
183
|
+
},
|
|
184
|
+
sprint: {
|
|
185
|
+
active: null,
|
|
186
|
+
stopOnFailure: true
|
|
187
|
+
},
|
|
188
|
+
logging: {
|
|
189
|
+
level: "normal",
|
|
190
|
+
maxFiles: 20,
|
|
191
|
+
maxTotalSizeMB: 50
|
|
192
|
+
},
|
|
193
|
+
sandbox: {
|
|
194
|
+
enabled: true,
|
|
195
|
+
providers: {},
|
|
196
|
+
extraWorkspaces: [],
|
|
197
|
+
readOnlyPaths: []
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
|
|
65
202
|
// src/display/terminal.ts
|
|
66
203
|
function getCapabilities() {
|
|
67
204
|
if (cachedCapabilities)
|
|
@@ -183,13 +320,13 @@ var init_terminal = __esm(() => {
|
|
|
183
320
|
// src/core/logger.ts
|
|
184
321
|
import {
|
|
185
322
|
appendFileSync,
|
|
186
|
-
existsSync,
|
|
187
|
-
mkdirSync,
|
|
323
|
+
existsSync as existsSync2,
|
|
324
|
+
mkdirSync as mkdirSync2,
|
|
188
325
|
readdirSync,
|
|
189
326
|
statSync,
|
|
190
327
|
unlinkSync
|
|
191
328
|
} from "node:fs";
|
|
192
|
-
import { join } from "node:path";
|
|
329
|
+
import { join as join2 } from "node:path";
|
|
193
330
|
function redactSensitive(text) {
|
|
194
331
|
let result = text;
|
|
195
332
|
for (const pattern of SENSITIVE_PATTERNS) {
|
|
@@ -219,11 +356,11 @@ class Logger {
|
|
|
219
356
|
initLogFile() {
|
|
220
357
|
if (!this.logDir)
|
|
221
358
|
return;
|
|
222
|
-
if (!
|
|
223
|
-
|
|
359
|
+
if (!existsSync2(this.logDir)) {
|
|
360
|
+
mkdirSync2(this.logDir, { recursive: true });
|
|
224
361
|
}
|
|
225
362
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
226
|
-
this.logFile =
|
|
363
|
+
this.logFile = join2(this.logDir, `locus-${timestamp}.log`);
|
|
227
364
|
this.pruneOldLogs();
|
|
228
365
|
}
|
|
229
366
|
startFlushTimer() {
|
|
@@ -318,14 +455,14 @@ class Logger {
|
|
|
318
455
|
}
|
|
319
456
|
}
|
|
320
457
|
pruneOldLogs() {
|
|
321
|
-
if (!this.logDir || !
|
|
458
|
+
if (!this.logDir || !existsSync2(this.logDir))
|
|
322
459
|
return;
|
|
323
460
|
try {
|
|
324
461
|
const logDir = this.logDir;
|
|
325
462
|
const files = readdirSync(logDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => ({
|
|
326
463
|
name: f,
|
|
327
|
-
path:
|
|
328
|
-
stat: statSync(
|
|
464
|
+
path: join2(logDir, f),
|
|
465
|
+
stat: statSync(join2(logDir, f))
|
|
329
466
|
})).sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
|
330
467
|
while (files.length > this.maxFiles) {
|
|
331
468
|
const oldest = files.pop();
|
|
@@ -405,258 +542,92 @@ var init_logger = __esm(() => {
|
|
|
405
542
|
];
|
|
406
543
|
});
|
|
407
544
|
|
|
408
|
-
// src/core/
|
|
409
|
-
import {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
545
|
+
// src/core/context.ts
|
|
546
|
+
import { execSync } from "node:child_process";
|
|
547
|
+
function detectRepoContext(cwd) {
|
|
548
|
+
const log = getLogger();
|
|
549
|
+
const remoteUrl = git("config --get remote.origin.url", cwd).trim();
|
|
550
|
+
if (!remoteUrl) {
|
|
551
|
+
throw new Error("No git remote 'origin' found. Add a GitHub remote: git remote add origin <url>");
|
|
552
|
+
}
|
|
553
|
+
log.verbose("Detected remote URL", { remoteUrl });
|
|
554
|
+
const { owner, repo } = parseRemoteUrl(remoteUrl);
|
|
555
|
+
if (!owner || !repo) {
|
|
556
|
+
throw new Error(`Could not parse owner/repo from remote URL: ${remoteUrl}`);
|
|
557
|
+
}
|
|
558
|
+
const currentBranch = git("rev-parse --abbrev-ref HEAD", cwd).trim();
|
|
559
|
+
let defaultBranch = "main";
|
|
560
|
+
try {
|
|
561
|
+
const ghOutput = execSync("gh repo view --json defaultBranchRef --jq .defaultBranchRef.name", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
562
|
+
if (ghOutput) {
|
|
563
|
+
defaultBranch = ghOutput;
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
try {
|
|
567
|
+
const remoteHead = git("symbolic-ref refs/remotes/origin/HEAD", cwd).trim().replace("refs/remotes/origin/", "");
|
|
568
|
+
if (remoteHead) {
|
|
569
|
+
defaultBranch = remoteHead;
|
|
570
|
+
}
|
|
571
|
+
} catch {
|
|
572
|
+
log.verbose("Could not detect default branch, using 'main'");
|
|
420
573
|
}
|
|
421
574
|
}
|
|
422
|
-
|
|
575
|
+
log.verbose("Repository context", {
|
|
576
|
+
owner,
|
|
577
|
+
repo,
|
|
578
|
+
defaultBranch,
|
|
579
|
+
currentBranch
|
|
580
|
+
});
|
|
581
|
+
return { owner, repo, defaultBranch, currentBranch, remoteUrl };
|
|
423
582
|
}
|
|
424
|
-
function
|
|
425
|
-
|
|
583
|
+
function parseRemoteUrl(url) {
|
|
584
|
+
let match = url.match(/git@github\.com:([^/]+)\/([^/.]+)/);
|
|
585
|
+
if (match)
|
|
586
|
+
return { owner: match[1], repo: match[2] };
|
|
587
|
+
match = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
588
|
+
if (match)
|
|
589
|
+
return { owner: match[1], repo: match[2] };
|
|
590
|
+
match = url.match(/^([^/]+)\/([^/.]+)$/);
|
|
591
|
+
if (match)
|
|
592
|
+
return { owner: match[1], repo: match[2] };
|
|
593
|
+
return { owner: "", repo: "" };
|
|
426
594
|
}
|
|
427
|
-
function
|
|
428
|
-
const configPath = getConfigPath(projectRoot);
|
|
429
|
-
if (!existsSync2(configPath)) {
|
|
430
|
-
throw new Error(`No Locus config found at ${configPath}. Run "locus init" first.`);
|
|
431
|
-
}
|
|
432
|
-
let raw;
|
|
595
|
+
function isGitRepo(cwd) {
|
|
433
596
|
try {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const migrated = applyMigrations(raw);
|
|
439
|
-
const config = deepMerge(DEFAULT_CONFIG, migrated);
|
|
440
|
-
const inferredProvider = inferProviderFromModel(config.ai.model);
|
|
441
|
-
if (inferredProvider) {
|
|
442
|
-
config.ai.provider = inferredProvider;
|
|
443
|
-
}
|
|
444
|
-
if (raw.version !== config.version) {
|
|
445
|
-
saveConfig(projectRoot, config);
|
|
597
|
+
git("rev-parse --git-dir", cwd);
|
|
598
|
+
return true;
|
|
599
|
+
} catch {
|
|
600
|
+
return false;
|
|
446
601
|
}
|
|
447
|
-
return config;
|
|
448
602
|
}
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
603
|
+
function checkGhCli() {
|
|
604
|
+
try {
|
|
605
|
+
execSync("gh --version", {
|
|
606
|
+
encoding: "utf-8",
|
|
607
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
608
|
+
});
|
|
609
|
+
} catch {
|
|
610
|
+
return { installed: false, authenticated: false };
|
|
454
611
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (inferredProvider) {
|
|
464
|
-
config.ai.provider = inferredProvider;
|
|
465
|
-
}
|
|
612
|
+
try {
|
|
613
|
+
execSync("gh auth status", {
|
|
614
|
+
encoding: "utf-8",
|
|
615
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
616
|
+
});
|
|
617
|
+
return { installed: true, authenticated: true };
|
|
618
|
+
} catch {
|
|
619
|
+
return { installed: true, authenticated: false };
|
|
466
620
|
}
|
|
467
|
-
saveConfig(projectRoot, config);
|
|
468
|
-
return config;
|
|
469
621
|
}
|
|
470
|
-
function
|
|
471
|
-
return
|
|
622
|
+
function getGitRoot(cwd) {
|
|
623
|
+
return git("rev-parse --show-toplevel", cwd).trim();
|
|
472
624
|
}
|
|
473
|
-
function
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
};
|
|
480
|
-
const src = source;
|
|
481
|
-
for (const key of Object.keys(src)) {
|
|
482
|
-
if (key in result && typeof result[key] === "object" && result[key] !== null && typeof src[key] === "object" && src[key] !== null && !Array.isArray(result[key])) {
|
|
483
|
-
result[key] = deepMerge(result[key], src[key]);
|
|
484
|
-
} else if (src[key] !== undefined) {
|
|
485
|
-
result[key] = src[key];
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return result;
|
|
489
|
-
}
|
|
490
|
-
function setNestedValue(obj, path, value) {
|
|
491
|
-
const keys = path.split(".");
|
|
492
|
-
let current = obj;
|
|
493
|
-
for (let i = 0;i < keys.length - 1; i++) {
|
|
494
|
-
const key = keys[i];
|
|
495
|
-
if (typeof current[key] !== "object" || current[key] === null) {
|
|
496
|
-
current[key] = {};
|
|
497
|
-
}
|
|
498
|
-
current = current[key];
|
|
499
|
-
}
|
|
500
|
-
const lastKey = keys[keys.length - 1];
|
|
501
|
-
let coerced = value;
|
|
502
|
-
if (value === "true")
|
|
503
|
-
coerced = true;
|
|
504
|
-
else if (value === "false")
|
|
505
|
-
coerced = false;
|
|
506
|
-
else if (typeof value === "string" && /^\d+$/.test(value))
|
|
507
|
-
coerced = Number.parseInt(value, 10);
|
|
508
|
-
current[lastKey] = coerced;
|
|
509
|
-
}
|
|
510
|
-
function getNestedValue(obj, path) {
|
|
511
|
-
const keys = path.split(".");
|
|
512
|
-
let current = obj;
|
|
513
|
-
for (const key of keys) {
|
|
514
|
-
if (typeof current !== "object" || current === null)
|
|
515
|
-
return;
|
|
516
|
-
current = current[key];
|
|
517
|
-
}
|
|
518
|
-
return current;
|
|
519
|
-
}
|
|
520
|
-
var DEFAULT_CONFIG, migrations;
|
|
521
|
-
var init_config = __esm(() => {
|
|
522
|
-
init_ai_models();
|
|
523
|
-
init_logger();
|
|
524
|
-
DEFAULT_CONFIG = {
|
|
525
|
-
version: "3.1.0",
|
|
526
|
-
github: {
|
|
527
|
-
owner: "",
|
|
528
|
-
repo: "",
|
|
529
|
-
defaultBranch: "main"
|
|
530
|
-
},
|
|
531
|
-
ai: {
|
|
532
|
-
provider: "claude",
|
|
533
|
-
model: "claude-sonnet-4-6"
|
|
534
|
-
},
|
|
535
|
-
agent: {
|
|
536
|
-
maxParallel: 3,
|
|
537
|
-
autoLabel: true,
|
|
538
|
-
autoPR: true,
|
|
539
|
-
baseBranch: "main",
|
|
540
|
-
rebaseBeforeTask: true
|
|
541
|
-
},
|
|
542
|
-
sprint: {
|
|
543
|
-
active: null,
|
|
544
|
-
stopOnFailure: true
|
|
545
|
-
},
|
|
546
|
-
logging: {
|
|
547
|
-
level: "normal",
|
|
548
|
-
maxFiles: 20,
|
|
549
|
-
maxTotalSizeMB: 50
|
|
550
|
-
},
|
|
551
|
-
sandbox: {
|
|
552
|
-
enabled: true,
|
|
553
|
-
extraWorkspaces: [],
|
|
554
|
-
readOnlyPaths: []
|
|
555
|
-
}
|
|
556
|
-
};
|
|
557
|
-
migrations = [
|
|
558
|
-
{
|
|
559
|
-
from: "3.0",
|
|
560
|
-
to: "3.1.0",
|
|
561
|
-
migrate: (config) => {
|
|
562
|
-
config.sandbox ??= {
|
|
563
|
-
enabled: true,
|
|
564
|
-
extraWorkspaces: [],
|
|
565
|
-
readOnlyPaths: []
|
|
566
|
-
};
|
|
567
|
-
config.version = "3.1.0";
|
|
568
|
-
return config;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
];
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// src/core/context.ts
|
|
575
|
-
import { execSync } from "node:child_process";
|
|
576
|
-
function detectRepoContext(cwd) {
|
|
577
|
-
const log = getLogger();
|
|
578
|
-
const remoteUrl = git("config --get remote.origin.url", cwd).trim();
|
|
579
|
-
if (!remoteUrl) {
|
|
580
|
-
throw new Error("No git remote 'origin' found. Add a GitHub remote: git remote add origin <url>");
|
|
581
|
-
}
|
|
582
|
-
log.verbose("Detected remote URL", { remoteUrl });
|
|
583
|
-
const { owner, repo } = parseRemoteUrl(remoteUrl);
|
|
584
|
-
if (!owner || !repo) {
|
|
585
|
-
throw new Error(`Could not parse owner/repo from remote URL: ${remoteUrl}`);
|
|
586
|
-
}
|
|
587
|
-
const currentBranch = git("rev-parse --abbrev-ref HEAD", cwd).trim();
|
|
588
|
-
let defaultBranch = "main";
|
|
589
|
-
try {
|
|
590
|
-
const ghOutput = execSync("gh repo view --json defaultBranchRef --jq .defaultBranchRef.name", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
591
|
-
if (ghOutput) {
|
|
592
|
-
defaultBranch = ghOutput;
|
|
593
|
-
}
|
|
594
|
-
} catch {
|
|
595
|
-
try {
|
|
596
|
-
const remoteHead = git("symbolic-ref refs/remotes/origin/HEAD", cwd).trim().replace("refs/remotes/origin/", "");
|
|
597
|
-
if (remoteHead) {
|
|
598
|
-
defaultBranch = remoteHead;
|
|
599
|
-
}
|
|
600
|
-
} catch {
|
|
601
|
-
log.verbose("Could not detect default branch, using 'main'");
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
log.verbose("Repository context", {
|
|
605
|
-
owner,
|
|
606
|
-
repo,
|
|
607
|
-
defaultBranch,
|
|
608
|
-
currentBranch
|
|
609
|
-
});
|
|
610
|
-
return { owner, repo, defaultBranch, currentBranch, remoteUrl };
|
|
611
|
-
}
|
|
612
|
-
function parseRemoteUrl(url) {
|
|
613
|
-
let match = url.match(/git@github\.com:([^/]+)\/([^/.]+)/);
|
|
614
|
-
if (match)
|
|
615
|
-
return { owner: match[1], repo: match[2] };
|
|
616
|
-
match = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
617
|
-
if (match)
|
|
618
|
-
return { owner: match[1], repo: match[2] };
|
|
619
|
-
match = url.match(/^([^/]+)\/([^/.]+)$/);
|
|
620
|
-
if (match)
|
|
621
|
-
return { owner: match[1], repo: match[2] };
|
|
622
|
-
return { owner: "", repo: "" };
|
|
623
|
-
}
|
|
624
|
-
function isGitRepo(cwd) {
|
|
625
|
-
try {
|
|
626
|
-
git("rev-parse --git-dir", cwd);
|
|
627
|
-
return true;
|
|
628
|
-
} catch {
|
|
629
|
-
return false;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
function checkGhCli() {
|
|
633
|
-
try {
|
|
634
|
-
execSync("gh --version", {
|
|
635
|
-
encoding: "utf-8",
|
|
636
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
637
|
-
});
|
|
638
|
-
} catch {
|
|
639
|
-
return { installed: false, authenticated: false };
|
|
640
|
-
}
|
|
641
|
-
try {
|
|
642
|
-
execSync("gh auth status", {
|
|
643
|
-
encoding: "utf-8",
|
|
644
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
645
|
-
});
|
|
646
|
-
return { installed: true, authenticated: true };
|
|
647
|
-
} catch {
|
|
648
|
-
return { installed: true, authenticated: false };
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
function getGitRoot(cwd) {
|
|
652
|
-
return git("rev-parse --show-toplevel", cwd).trim();
|
|
653
|
-
}
|
|
654
|
-
function git(args, cwd) {
|
|
655
|
-
return execSync(`git ${args}`, {
|
|
656
|
-
cwd,
|
|
657
|
-
encoding: "utf-8",
|
|
658
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
659
|
-
});
|
|
625
|
+
function git(args, cwd) {
|
|
626
|
+
return execSync(`git ${args}`, {
|
|
627
|
+
cwd,
|
|
628
|
+
encoding: "utf-8",
|
|
629
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
630
|
+
});
|
|
660
631
|
}
|
|
661
632
|
var init_context = __esm(() => {
|
|
662
633
|
init_logger();
|
|
@@ -915,11 +886,20 @@ var init_progress = __esm(() => {
|
|
|
915
886
|
var exports_sandbox = {};
|
|
916
887
|
__export(exports_sandbox, {
|
|
917
888
|
resolveSandboxMode: () => resolveSandboxMode,
|
|
889
|
+
getProviderSandboxName: () => getProviderSandboxName,
|
|
890
|
+
getModelSandboxName: () => getModelSandboxName,
|
|
918
891
|
displaySandboxWarning: () => displaySandboxWarning,
|
|
919
892
|
detectSandboxSupport: () => detectSandboxSupport
|
|
920
893
|
});
|
|
921
894
|
import { execFile } from "node:child_process";
|
|
922
895
|
import { createInterface } from "node:readline";
|
|
896
|
+
function getProviderSandboxName(config, provider) {
|
|
897
|
+
return config.providers[provider];
|
|
898
|
+
}
|
|
899
|
+
function getModelSandboxName(config, model, fallbackProvider) {
|
|
900
|
+
const provider = inferProviderFromModel(model) ?? fallbackProvider;
|
|
901
|
+
return getProviderSandboxName(config, provider);
|
|
902
|
+
}
|
|
923
903
|
async function detectSandboxSupport() {
|
|
924
904
|
if (cachedStatus)
|
|
925
905
|
return cachedStatus;
|
|
@@ -1051,6 +1031,7 @@ function waitForEnter() {
|
|
|
1051
1031
|
var TIMEOUT_MS = 5000, cachedStatus = null;
|
|
1052
1032
|
var init_sandbox = __esm(() => {
|
|
1053
1033
|
init_terminal();
|
|
1034
|
+
init_ai_models();
|
|
1054
1035
|
init_logger();
|
|
1055
1036
|
});
|
|
1056
1037
|
|
|
@@ -1440,6 +1421,9 @@ function updateIssueLabels(number, addLabels, removeLabels, options = {}) {
|
|
|
1440
1421
|
}
|
|
1441
1422
|
gh(args, options);
|
|
1442
1423
|
}
|
|
1424
|
+
function deleteIssue(number, options = {}) {
|
|
1425
|
+
gh(`issue delete ${number} --yes`, options);
|
|
1426
|
+
}
|
|
1443
1427
|
function addIssueComment(number, body, options = {}) {
|
|
1444
1428
|
const cwd = options.cwd ?? process.cwd();
|
|
1445
1429
|
execFileSync("gh", ["issue", "comment", String(number), "--body", body], {
|
|
@@ -1813,14 +1797,13 @@ ${bold("Sandbox mode")} ${dim("(recommended)")}
|
|
|
1813
1797
|
process.stderr.write(` Run AI agents in an isolated Docker sandbox for safety.
|
|
1814
1798
|
|
|
1815
1799
|
`);
|
|
1816
|
-
process.stderr.write(` ${gray("1.")} ${cyan("locus sandbox")} ${dim("Create
|
|
1800
|
+
process.stderr.write(` ${gray("1.")} ${cyan("locus sandbox")} ${dim("Create claude/codex sandboxes")}
|
|
1817
1801
|
`);
|
|
1818
1802
|
process.stderr.write(` ${gray("2.")} ${cyan("locus sandbox claude")} ${dim("Login to Claude inside the sandbox")}
|
|
1819
1803
|
`);
|
|
1820
|
-
process.stderr.write(` ${gray("3.")} ${cyan("locus
|
|
1804
|
+
process.stderr.write(` ${gray("3.")} ${cyan("locus sandbox codex")} ${dim("Login to Codex inside the sandbox")}
|
|
1821
1805
|
`);
|
|
1822
|
-
process.stderr.write(`
|
|
1823
|
-
${dim("Using Codex? Run")} ${cyan("locus sandbox codex")} ${dim("instead of step 2.")}
|
|
1806
|
+
process.stderr.write(` ${gray("4.")} ${cyan("locus exec")} ${dim("All commands now run sandboxed")}
|
|
1824
1807
|
`);
|
|
1825
1808
|
process.stderr.write(` ${dim("Learn more:")} ${cyan("locus sandbox help")}
|
|
1826
1809
|
`);
|
|
@@ -4399,449 +4382,161 @@ var init_sandbox_ignore = __esm(() => {
|
|
|
4399
4382
|
execAsync = promisify(exec);
|
|
4400
4383
|
});
|
|
4401
4384
|
|
|
4402
|
-
// src/
|
|
4403
|
-
import {
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
return join12(projectRoot, ".locus", "run-state.json");
|
|
4413
|
-
}
|
|
4414
|
-
function loadRunState(projectRoot) {
|
|
4415
|
-
const path = getRunStatePath(projectRoot);
|
|
4416
|
-
if (!existsSync13(path))
|
|
4417
|
-
return null;
|
|
4418
|
-
try {
|
|
4419
|
-
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
4420
|
-
} catch {
|
|
4421
|
-
getLogger().warn("Corrupted run-state.json, ignoring");
|
|
4422
|
-
return null;
|
|
4423
|
-
}
|
|
4424
|
-
}
|
|
4425
|
-
function saveRunState(projectRoot, state) {
|
|
4426
|
-
const path = getRunStatePath(projectRoot);
|
|
4427
|
-
const dir = dirname3(path);
|
|
4428
|
-
if (!existsSync13(dir)) {
|
|
4429
|
-
mkdirSync9(dir, { recursive: true });
|
|
4430
|
-
}
|
|
4431
|
-
writeFileSync6(path, `${JSON.stringify(state, null, 2)}
|
|
4432
|
-
`, "utf-8");
|
|
4433
|
-
}
|
|
4434
|
-
function clearRunState(projectRoot) {
|
|
4435
|
-
const path = getRunStatePath(projectRoot);
|
|
4436
|
-
if (existsSync13(path)) {
|
|
4437
|
-
unlinkSync3(path);
|
|
4438
|
-
}
|
|
4439
|
-
}
|
|
4440
|
-
function createSprintRunState(sprint, branch, issues) {
|
|
4441
|
-
return {
|
|
4442
|
-
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
4443
|
-
type: "sprint",
|
|
4444
|
-
sprint,
|
|
4445
|
-
branch,
|
|
4446
|
-
startedAt: new Date().toISOString(),
|
|
4447
|
-
tasks: issues.map(({ number, order }) => ({
|
|
4448
|
-
issue: number,
|
|
4449
|
-
order,
|
|
4450
|
-
status: "pending"
|
|
4451
|
-
}))
|
|
4452
|
-
};
|
|
4453
|
-
}
|
|
4454
|
-
function createParallelRunState(issueNumbers) {
|
|
4455
|
-
return {
|
|
4456
|
-
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
4457
|
-
type: "parallel",
|
|
4458
|
-
startedAt: new Date().toISOString(),
|
|
4459
|
-
tasks: issueNumbers.map((issue, i) => ({
|
|
4460
|
-
issue,
|
|
4461
|
-
order: i + 1,
|
|
4462
|
-
status: "pending"
|
|
4463
|
-
}))
|
|
4464
|
-
};
|
|
4465
|
-
}
|
|
4466
|
-
function markTaskInProgress(state, issueNumber) {
|
|
4467
|
-
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
4468
|
-
if (task) {
|
|
4469
|
-
task.status = "in_progress";
|
|
4385
|
+
// src/ai/claude-sandbox.ts
|
|
4386
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
4387
|
+
|
|
4388
|
+
class SandboxedClaudeRunner {
|
|
4389
|
+
sandboxName;
|
|
4390
|
+
name = "claude-sandboxed";
|
|
4391
|
+
process = null;
|
|
4392
|
+
aborted = false;
|
|
4393
|
+
constructor(sandboxName) {
|
|
4394
|
+
this.sandboxName = sandboxName;
|
|
4470
4395
|
}
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
task.status = "done";
|
|
4476
|
-
task.completedAt = new Date().toISOString();
|
|
4477
|
-
if (prNumber)
|
|
4478
|
-
task.pr = prNumber;
|
|
4396
|
+
async isAvailable() {
|
|
4397
|
+
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4398
|
+
const delegate = new ClaudeRunner2;
|
|
4399
|
+
return delegate.isAvailable();
|
|
4479
4400
|
}
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
task.status = "failed";
|
|
4485
|
-
task.failedAt = new Date().toISOString();
|
|
4486
|
-
task.error = error;
|
|
4487
|
-
}
|
|
4488
|
-
}
|
|
4489
|
-
function getRunStats(state) {
|
|
4490
|
-
const tasks = state.tasks;
|
|
4491
|
-
return {
|
|
4492
|
-
total: tasks.length,
|
|
4493
|
-
done: tasks.filter((t) => t.status === "done").length,
|
|
4494
|
-
failed: tasks.filter((t) => t.status === "failed").length,
|
|
4495
|
-
pending: tasks.filter((t) => t.status === "pending").length,
|
|
4496
|
-
inProgress: tasks.filter((t) => t.status === "in_progress").length
|
|
4497
|
-
};
|
|
4498
|
-
}
|
|
4499
|
-
function getNextTask(state) {
|
|
4500
|
-
const failed = state.tasks.find((t) => t.status === "failed");
|
|
4501
|
-
if (failed)
|
|
4502
|
-
return failed;
|
|
4503
|
-
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
4504
|
-
}
|
|
4505
|
-
var init_run_state = __esm(() => {
|
|
4506
|
-
init_logger();
|
|
4507
|
-
});
|
|
4508
|
-
|
|
4509
|
-
// src/core/shutdown.ts
|
|
4510
|
-
import { execSync as execSync6 } from "node:child_process";
|
|
4511
|
-
function registerActiveSandbox(name) {
|
|
4512
|
-
activeSandboxes.add(name);
|
|
4513
|
-
}
|
|
4514
|
-
function unregisterActiveSandbox(name) {
|
|
4515
|
-
activeSandboxes.delete(name);
|
|
4516
|
-
}
|
|
4517
|
-
function cleanupActiveSandboxes() {
|
|
4518
|
-
for (const name of activeSandboxes) {
|
|
4519
|
-
try {
|
|
4520
|
-
execSync6(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
4521
|
-
} catch {}
|
|
4522
|
-
}
|
|
4523
|
-
activeSandboxes.clear();
|
|
4524
|
-
}
|
|
4525
|
-
function registerShutdownHandlers(ctx) {
|
|
4526
|
-
shutdownContext = ctx;
|
|
4527
|
-
interruptCount = 0;
|
|
4528
|
-
const handler = () => {
|
|
4529
|
-
interruptCount++;
|
|
4530
|
-
if (interruptCount >= 2) {
|
|
4531
|
-
process.stderr.write(`
|
|
4532
|
-
Force exit.
|
|
4533
|
-
`);
|
|
4534
|
-
process.exit(1);
|
|
4535
|
-
}
|
|
4536
|
-
process.stderr.write(`
|
|
4537
|
-
|
|
4538
|
-
Interrupted. Saving state...
|
|
4539
|
-
`);
|
|
4540
|
-
const state = shutdownContext?.getRunState?.();
|
|
4541
|
-
if (state && shutdownContext) {
|
|
4542
|
-
for (const task of state.tasks) {
|
|
4543
|
-
if (task.status === "in_progress") {
|
|
4544
|
-
task.status = "failed";
|
|
4545
|
-
task.failedAt = new Date().toISOString();
|
|
4546
|
-
task.error = "Interrupted by user";
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
try {
|
|
4550
|
-
saveRunState(shutdownContext.projectRoot, state);
|
|
4551
|
-
process.stderr.write(`State saved. Resume with: locus run --resume
|
|
4552
|
-
`);
|
|
4553
|
-
} catch {
|
|
4554
|
-
process.stderr.write(`Warning: Could not save run state.
|
|
4555
|
-
`);
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
cleanupActiveSandboxes();
|
|
4559
|
-
shutdownContext?.onShutdown?.();
|
|
4560
|
-
if (interruptTimer)
|
|
4561
|
-
clearTimeout(interruptTimer);
|
|
4562
|
-
interruptTimer = setTimeout(() => {
|
|
4563
|
-
interruptCount = 0;
|
|
4564
|
-
}, 2000);
|
|
4565
|
-
setTimeout(() => {
|
|
4566
|
-
process.exit(130);
|
|
4567
|
-
}, 100);
|
|
4568
|
-
};
|
|
4569
|
-
if (!shutdownRegistered) {
|
|
4570
|
-
process.on("SIGINT", handler);
|
|
4571
|
-
process.on("SIGTERM", handler);
|
|
4572
|
-
shutdownRegistered = true;
|
|
4573
|
-
}
|
|
4574
|
-
return () => {
|
|
4575
|
-
process.removeListener("SIGINT", handler);
|
|
4576
|
-
process.removeListener("SIGTERM", handler);
|
|
4577
|
-
shutdownRegistered = false;
|
|
4578
|
-
shutdownContext = null;
|
|
4579
|
-
interruptCount = 0;
|
|
4580
|
-
if (interruptTimer) {
|
|
4581
|
-
clearTimeout(interruptTimer);
|
|
4582
|
-
interruptTimer = null;
|
|
4583
|
-
}
|
|
4584
|
-
};
|
|
4585
|
-
}
|
|
4586
|
-
var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
|
|
4587
|
-
var init_shutdown = __esm(() => {
|
|
4588
|
-
init_run_state();
|
|
4589
|
-
activeSandboxes = new Set;
|
|
4590
|
-
});
|
|
4591
|
-
|
|
4592
|
-
// src/ai/claude-sandbox.ts
|
|
4593
|
-
import { execSync as execSync7, spawn as spawn3 } from "node:child_process";
|
|
4594
|
-
|
|
4595
|
-
class SandboxedClaudeRunner {
|
|
4596
|
-
name = "claude-sandboxed";
|
|
4597
|
-
process = null;
|
|
4598
|
-
aborted = false;
|
|
4599
|
-
sandboxName = null;
|
|
4600
|
-
persistent;
|
|
4601
|
-
sandboxCreated = false;
|
|
4602
|
-
userManaged = false;
|
|
4603
|
-
constructor(persistentName, userManaged = false) {
|
|
4604
|
-
if (persistentName) {
|
|
4605
|
-
this.persistent = true;
|
|
4606
|
-
this.sandboxName = persistentName;
|
|
4607
|
-
this.userManaged = userManaged;
|
|
4608
|
-
if (userManaged) {
|
|
4609
|
-
this.sandboxCreated = true;
|
|
4610
|
-
}
|
|
4611
|
-
} else {
|
|
4612
|
-
this.persistent = false;
|
|
4613
|
-
}
|
|
4614
|
-
}
|
|
4615
|
-
async isAvailable() {
|
|
4616
|
-
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4617
|
-
const delegate = new ClaudeRunner2;
|
|
4618
|
-
return delegate.isAvailable();
|
|
4619
|
-
}
|
|
4620
|
-
async getVersion() {
|
|
4621
|
-
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4622
|
-
const delegate = new ClaudeRunner2;
|
|
4623
|
-
return delegate.getVersion();
|
|
4401
|
+
async getVersion() {
|
|
4402
|
+
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4403
|
+
const delegate = new ClaudeRunner2;
|
|
4404
|
+
return delegate.getVersion();
|
|
4624
4405
|
}
|
|
4625
4406
|
async execute(options) {
|
|
4626
4407
|
const log = getLogger();
|
|
4627
4408
|
this.aborted = false;
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
if (!name) {
|
|
4636
|
-
throw new Error("Sandbox name is required");
|
|
4637
|
-
}
|
|
4638
|
-
options.onStatusChange?.("Syncing sandbox...");
|
|
4639
|
-
await enforceSandboxIgnore(name, options.cwd);
|
|
4640
|
-
options.onStatusChange?.("Thinking...");
|
|
4641
|
-
dockerArgs = [
|
|
4642
|
-
"sandbox",
|
|
4643
|
-
"exec",
|
|
4644
|
-
"-w",
|
|
4645
|
-
options.cwd,
|
|
4646
|
-
name,
|
|
4647
|
-
"claude",
|
|
4648
|
-
...claudeArgs
|
|
4649
|
-
];
|
|
4650
|
-
} else {
|
|
4651
|
-
if (!this.persistent) {
|
|
4652
|
-
this.sandboxName = buildSandboxName(options);
|
|
4653
|
-
}
|
|
4654
|
-
const name = this.sandboxName;
|
|
4655
|
-
if (!name) {
|
|
4656
|
-
throw new Error("Sandbox name is required");
|
|
4657
|
-
}
|
|
4658
|
-
registerActiveSandbox(name);
|
|
4659
|
-
options.onStatusChange?.("Syncing sandbox...");
|
|
4660
|
-
dockerArgs = [
|
|
4661
|
-
"sandbox",
|
|
4662
|
-
"run",
|
|
4663
|
-
"--name",
|
|
4664
|
-
name,
|
|
4665
|
-
"claude",
|
|
4666
|
-
options.cwd,
|
|
4667
|
-
"--",
|
|
4668
|
-
...claudeArgs
|
|
4669
|
-
];
|
|
4409
|
+
if (!await this.isSandboxRunning()) {
|
|
4410
|
+
return {
|
|
4411
|
+
success: false,
|
|
4412
|
+
output: "",
|
|
4413
|
+
error: `Sandbox is not running: ${this.sandboxName}`,
|
|
4414
|
+
exitCode: 1
|
|
4415
|
+
};
|
|
4670
4416
|
}
|
|
4417
|
+
const claudeArgs = ["-p", options.prompt, ...buildClaudeArgs(options)];
|
|
4418
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
4419
|
+
await enforceSandboxIgnore(this.sandboxName, options.cwd);
|
|
4420
|
+
options.onStatusChange?.("Thinking...");
|
|
4421
|
+
const dockerArgs = [
|
|
4422
|
+
"sandbox",
|
|
4423
|
+
"exec",
|
|
4424
|
+
"-w",
|
|
4425
|
+
options.cwd,
|
|
4426
|
+
this.sandboxName,
|
|
4427
|
+
"claude",
|
|
4428
|
+
...claudeArgs
|
|
4429
|
+
];
|
|
4671
4430
|
log.debug("Spawning sandboxed claude", {
|
|
4672
4431
|
sandboxName: this.sandboxName,
|
|
4673
|
-
persistent: this.persistent,
|
|
4674
|
-
reusing: this.persistent && this.sandboxCreated,
|
|
4675
4432
|
args: dockerArgs.join(" "),
|
|
4676
4433
|
cwd: options.cwd
|
|
4677
4434
|
});
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
const
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
const event = JSON.parse(line);
|
|
4704
|
-
if (event.type === "assistant" && event.message?.content) {
|
|
4705
|
-
for (const item of event.message.content) {
|
|
4706
|
-
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
4707
|
-
seenToolIds.add(item.id);
|
|
4708
|
-
options.onToolActivity?.(formatToolCall2(item.name ?? "", item.input ?? {}));
|
|
4709
|
-
}
|
|
4435
|
+
return await new Promise((resolve2) => {
|
|
4436
|
+
let output = "";
|
|
4437
|
+
let errorOutput = "";
|
|
4438
|
+
this.process = spawn3("docker", dockerArgs, {
|
|
4439
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4440
|
+
env: process.env
|
|
4441
|
+
});
|
|
4442
|
+
if (options.verbose) {
|
|
4443
|
+
let lineBuffer = "";
|
|
4444
|
+
const seenToolIds = new Set;
|
|
4445
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4446
|
+
lineBuffer += chunk.toString();
|
|
4447
|
+
const lines = lineBuffer.split(`
|
|
4448
|
+
`);
|
|
4449
|
+
lineBuffer = lines.pop() ?? "";
|
|
4450
|
+
for (const line of lines) {
|
|
4451
|
+
if (!line.trim())
|
|
4452
|
+
continue;
|
|
4453
|
+
try {
|
|
4454
|
+
const event = JSON.parse(line);
|
|
4455
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
4456
|
+
for (const item of event.message.content) {
|
|
4457
|
+
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
4458
|
+
seenToolIds.add(item.id);
|
|
4459
|
+
options.onToolActivity?.(formatToolCall2(item.name ?? "", item.input ?? {}));
|
|
4710
4460
|
}
|
|
4711
|
-
} else if (event.type === "result") {
|
|
4712
|
-
const text = event.result ?? "";
|
|
4713
|
-
output = text;
|
|
4714
|
-
options.onOutput?.(text);
|
|
4715
4461
|
}
|
|
4716
|
-
}
|
|
4717
|
-
const
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
options.onOutput?.(newLine);
|
|
4462
|
+
} else if (event.type === "result") {
|
|
4463
|
+
const text = event.result ?? "";
|
|
4464
|
+
output = text;
|
|
4465
|
+
options.onOutput?.(text);
|
|
4721
4466
|
}
|
|
4467
|
+
} catch {
|
|
4468
|
+
const newLine = `${line}
|
|
4469
|
+
`;
|
|
4470
|
+
output += newLine;
|
|
4471
|
+
options.onOutput?.(newLine);
|
|
4722
4472
|
}
|
|
4723
|
-
}
|
|
4724
|
-
}
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
output += text;
|
|
4728
|
-
options.onOutput?.(text);
|
|
4729
|
-
});
|
|
4730
|
-
}
|
|
4731
|
-
this.process.stderr?.on("data", (chunk) => {
|
|
4473
|
+
}
|
|
4474
|
+
});
|
|
4475
|
+
} else {
|
|
4476
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4732
4477
|
const text = chunk.toString();
|
|
4733
|
-
|
|
4734
|
-
log.debug("sandboxed claude stderr", { text: text.slice(0, 500) });
|
|
4478
|
+
output += text;
|
|
4735
4479
|
options.onOutput?.(text);
|
|
4736
4480
|
});
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
}
|
|
4748
|
-
if (code === 0) {
|
|
4749
|
-
resolve2({
|
|
4750
|
-
success: true,
|
|
4751
|
-
output,
|
|
4752
|
-
exitCode: 0
|
|
4753
|
-
});
|
|
4754
|
-
} else {
|
|
4755
|
-
resolve2({
|
|
4756
|
-
success: false,
|
|
4757
|
-
output,
|
|
4758
|
-
error: errorOutput || `sandboxed claude exited with code ${code}`,
|
|
4759
|
-
exitCode: code ?? 1
|
|
4760
|
-
});
|
|
4761
|
-
}
|
|
4762
|
-
});
|
|
4763
|
-
this.process.on("error", (err) => {
|
|
4764
|
-
this.process = null;
|
|
4765
|
-
if (this.persistent && !this.sandboxCreated) {}
|
|
4481
|
+
}
|
|
4482
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
4483
|
+
const text = chunk.toString();
|
|
4484
|
+
errorOutput += text;
|
|
4485
|
+
log.debug("sandboxed claude stderr", { text: text.slice(0, 500) });
|
|
4486
|
+
options.onOutput?.(text);
|
|
4487
|
+
});
|
|
4488
|
+
this.process.on("close", (code) => {
|
|
4489
|
+
this.process = null;
|
|
4490
|
+
if (this.aborted) {
|
|
4766
4491
|
resolve2({
|
|
4767
4492
|
success: false,
|
|
4768
4493
|
output,
|
|
4769
|
-
error:
|
|
4770
|
-
exitCode:
|
|
4494
|
+
error: "Aborted by user",
|
|
4495
|
+
exitCode: code ?? 143
|
|
4771
4496
|
});
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
4499
|
+
if (code === 0) {
|
|
4500
|
+
resolve2({ success: true, output, exitCode: 0 });
|
|
4501
|
+
} else {
|
|
4502
|
+
resolve2({
|
|
4503
|
+
success: false,
|
|
4504
|
+
output,
|
|
4505
|
+
error: errorOutput || `sandboxed claude exited with code ${code}`,
|
|
4506
|
+
exitCode: code ?? 1
|
|
4776
4507
|
});
|
|
4777
4508
|
}
|
|
4778
4509
|
});
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4510
|
+
this.process.on("error", (err) => {
|
|
4511
|
+
this.process = null;
|
|
4512
|
+
resolve2({
|
|
4513
|
+
success: false,
|
|
4514
|
+
output,
|
|
4515
|
+
error: `Failed to spawn docker sandbox: ${err.message}`,
|
|
4516
|
+
exitCode: 1
|
|
4517
|
+
});
|
|
4518
|
+
});
|
|
4519
|
+
if (options.signal) {
|
|
4520
|
+
options.signal.addEventListener("abort", () => {
|
|
4521
|
+
this.abort();
|
|
4522
|
+
});
|
|
4782
4523
|
}
|
|
4783
|
-
}
|
|
4524
|
+
});
|
|
4784
4525
|
}
|
|
4785
4526
|
abort() {
|
|
4786
4527
|
this.aborted = true;
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
});
|
|
4528
|
+
if (!this.process)
|
|
4529
|
+
return;
|
|
4530
|
+
this.process.kill("SIGTERM");
|
|
4531
|
+
const timer = setTimeout(() => {
|
|
4792
4532
|
if (this.process) {
|
|
4793
|
-
this.process.kill("
|
|
4794
|
-
const timer = setTimeout(() => {
|
|
4795
|
-
if (this.process) {
|
|
4796
|
-
this.process.kill("SIGKILL");
|
|
4797
|
-
}
|
|
4798
|
-
}, 3000);
|
|
4799
|
-
if (timer.unref)
|
|
4800
|
-
timer.unref();
|
|
4533
|
+
this.process.kill("SIGKILL");
|
|
4801
4534
|
}
|
|
4802
|
-
}
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
log.debug("Aborting sandboxed claude (ephemeral — removing sandbox)", {
|
|
4806
|
-
sandboxName: this.sandboxName
|
|
4807
|
-
});
|
|
4808
|
-
try {
|
|
4809
|
-
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4810
|
-
} catch {}
|
|
4811
|
-
}
|
|
4812
|
-
}
|
|
4813
|
-
destroy() {
|
|
4814
|
-
if (!this.sandboxName)
|
|
4815
|
-
return;
|
|
4816
|
-
if (this.userManaged) {
|
|
4817
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
4818
|
-
return;
|
|
4819
|
-
}
|
|
4820
|
-
const log = getLogger();
|
|
4821
|
-
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
4822
|
-
try {
|
|
4823
|
-
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4824
|
-
} catch {}
|
|
4825
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
4826
|
-
this.sandboxName = null;
|
|
4827
|
-
this.sandboxCreated = false;
|
|
4828
|
-
}
|
|
4829
|
-
cleanupSandbox() {
|
|
4830
|
-
if (!this.sandboxName)
|
|
4831
|
-
return;
|
|
4832
|
-
const log = getLogger();
|
|
4833
|
-
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
4834
|
-
try {
|
|
4835
|
-
execSync7(`docker sandbox rm ${this.sandboxName}`, {
|
|
4836
|
-
timeout: 60000
|
|
4837
|
-
});
|
|
4838
|
-
} catch {}
|
|
4839
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
4840
|
-
this.sandboxName = null;
|
|
4535
|
+
}, 3000);
|
|
4536
|
+
if (timer.unref)
|
|
4537
|
+
timer.unref();
|
|
4841
4538
|
}
|
|
4842
4539
|
async isSandboxRunning() {
|
|
4843
|
-
if (!this.sandboxName)
|
|
4844
|
-
return false;
|
|
4845
4540
|
try {
|
|
4846
4541
|
const { promisify: promisify2 } = await import("node:util");
|
|
4847
4542
|
const { exec: exec2 } = await import("node:child_process");
|
|
@@ -4854,24 +4549,6 @@ class SandboxedClaudeRunner {
|
|
|
4854
4549
|
return false;
|
|
4855
4550
|
}
|
|
4856
4551
|
}
|
|
4857
|
-
getSandboxName() {
|
|
4858
|
-
return this.sandboxName;
|
|
4859
|
-
}
|
|
4860
|
-
}
|
|
4861
|
-
function buildSandboxName(options) {
|
|
4862
|
-
const ts = Date.now();
|
|
4863
|
-
if (options.activity) {
|
|
4864
|
-
const match = options.activity.match(/issue\s*#(\d+)/i);
|
|
4865
|
-
if (match) {
|
|
4866
|
-
return `locus-issue-${match[1]}-${ts}`;
|
|
4867
|
-
}
|
|
4868
|
-
}
|
|
4869
|
-
const segment = options.cwd.split("/").pop() ?? "run";
|
|
4870
|
-
return `locus-${segment}-${ts}`;
|
|
4871
|
-
}
|
|
4872
|
-
function buildPersistentSandboxName(cwd) {
|
|
4873
|
-
const segment = cwd.split("/").pop() ?? "repl";
|
|
4874
|
-
return `locus-${segment}-${Date.now()}`;
|
|
4875
4552
|
}
|
|
4876
4553
|
function formatToolCall2(name, input) {
|
|
4877
4554
|
switch (name) {
|
|
@@ -4895,7 +4572,7 @@ function formatToolCall2(name, input) {
|
|
|
4895
4572
|
case "WebSearch":
|
|
4896
4573
|
return `searching: ${input.query ?? ""}`;
|
|
4897
4574
|
case "Task":
|
|
4898
|
-
return
|
|
4575
|
+
return "spawning agent";
|
|
4899
4576
|
default:
|
|
4900
4577
|
return name;
|
|
4901
4578
|
}
|
|
@@ -4903,12 +4580,11 @@ function formatToolCall2(name, input) {
|
|
|
4903
4580
|
var init_claude_sandbox = __esm(() => {
|
|
4904
4581
|
init_logger();
|
|
4905
4582
|
init_sandbox_ignore();
|
|
4906
|
-
init_shutdown();
|
|
4907
4583
|
init_claude();
|
|
4908
4584
|
});
|
|
4909
4585
|
|
|
4910
4586
|
// src/ai/codex.ts
|
|
4911
|
-
import { execSync as
|
|
4587
|
+
import { execSync as execSync6, spawn as spawn4 } from "node:child_process";
|
|
4912
4588
|
function buildCodexArgs(model) {
|
|
4913
4589
|
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
|
|
4914
4590
|
if (model) {
|
|
@@ -4924,7 +4600,7 @@ class CodexRunner {
|
|
|
4924
4600
|
aborted = false;
|
|
4925
4601
|
async isAvailable() {
|
|
4926
4602
|
try {
|
|
4927
|
-
|
|
4603
|
+
execSync6("codex --version", {
|
|
4928
4604
|
encoding: "utf-8",
|
|
4929
4605
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4930
4606
|
});
|
|
@@ -4935,7 +4611,7 @@ class CodexRunner {
|
|
|
4935
4611
|
}
|
|
4936
4612
|
async getVersion() {
|
|
4937
4613
|
try {
|
|
4938
|
-
const output =
|
|
4614
|
+
const output = execSync6("codex --version", {
|
|
4939
4615
|
encoding: "utf-8",
|
|
4940
4616
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4941
4617
|
}).trim();
|
|
@@ -5083,28 +4759,16 @@ var init_codex = __esm(() => {
|
|
|
5083
4759
|
});
|
|
5084
4760
|
|
|
5085
4761
|
// src/ai/codex-sandbox.ts
|
|
5086
|
-
import {
|
|
4762
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
5087
4763
|
|
|
5088
4764
|
class SandboxedCodexRunner {
|
|
4765
|
+
sandboxName;
|
|
5089
4766
|
name = "codex-sandboxed";
|
|
5090
4767
|
process = null;
|
|
5091
4768
|
aborted = false;
|
|
5092
|
-
sandboxName = null;
|
|
5093
|
-
persistent;
|
|
5094
|
-
sandboxCreated = false;
|
|
5095
|
-
userManaged = false;
|
|
5096
4769
|
codexInstalled = false;
|
|
5097
|
-
constructor(
|
|
5098
|
-
|
|
5099
|
-
this.persistent = true;
|
|
5100
|
-
this.sandboxName = persistentName;
|
|
5101
|
-
this.userManaged = userManaged;
|
|
5102
|
-
if (userManaged) {
|
|
5103
|
-
this.sandboxCreated = true;
|
|
5104
|
-
}
|
|
5105
|
-
} else {
|
|
5106
|
-
this.persistent = false;
|
|
5107
|
-
}
|
|
4770
|
+
constructor(sandboxName) {
|
|
4771
|
+
this.sandboxName = sandboxName;
|
|
5108
4772
|
}
|
|
5109
4773
|
async isAvailable() {
|
|
5110
4774
|
const delegate = new CodexRunner;
|
|
@@ -5117,251 +4781,158 @@ class SandboxedCodexRunner {
|
|
|
5117
4781
|
async execute(options) {
|
|
5118
4782
|
const log = getLogger();
|
|
5119
4783
|
this.aborted = false;
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
4784
|
+
if (!await this.isSandboxRunning()) {
|
|
4785
|
+
return {
|
|
4786
|
+
success: false,
|
|
4787
|
+
output: "",
|
|
4788
|
+
error: `Sandbox is not running: ${this.sandboxName}`,
|
|
4789
|
+
exitCode: 1
|
|
4790
|
+
};
|
|
5124
4791
|
}
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
await enforceSandboxIgnore(name, options.cwd);
|
|
5132
|
-
if (!this.codexInstalled) {
|
|
5133
|
-
options.onStatusChange?.("Checking codex...");
|
|
5134
|
-
await this.ensureCodexInstalled(name);
|
|
5135
|
-
this.codexInstalled = true;
|
|
5136
|
-
}
|
|
5137
|
-
options.onStatusChange?.("Thinking...");
|
|
5138
|
-
dockerArgs = [
|
|
5139
|
-
"sandbox",
|
|
5140
|
-
"exec",
|
|
5141
|
-
"-i",
|
|
5142
|
-
"-w",
|
|
5143
|
-
options.cwd,
|
|
5144
|
-
name,
|
|
5145
|
-
"codex",
|
|
5146
|
-
...codexArgs
|
|
5147
|
-
];
|
|
5148
|
-
} else {
|
|
5149
|
-
if (!this.persistent) {
|
|
5150
|
-
this.sandboxName = buildSandboxName2(options);
|
|
5151
|
-
}
|
|
5152
|
-
const name = this.sandboxName;
|
|
5153
|
-
if (!name) {
|
|
5154
|
-
throw new Error("Sandbox name is required");
|
|
5155
|
-
}
|
|
5156
|
-
registerActiveSandbox(name);
|
|
5157
|
-
options.onStatusChange?.("Creating sandbox...");
|
|
5158
|
-
await this.createSandboxWithClaude(name, options.cwd);
|
|
5159
|
-
options.onStatusChange?.("Installing codex...");
|
|
5160
|
-
await this.ensureCodexInstalled(name);
|
|
4792
|
+
const codexArgs = buildCodexArgs(options.model);
|
|
4793
|
+
options.onStatusChange?.("Syncing sandbox...");
|
|
4794
|
+
await enforceSandboxIgnore(this.sandboxName, options.cwd);
|
|
4795
|
+
if (!this.codexInstalled) {
|
|
4796
|
+
options.onStatusChange?.("Checking codex...");
|
|
4797
|
+
await this.ensureCodexInstalled(this.sandboxName);
|
|
5161
4798
|
this.codexInstalled = true;
|
|
5162
|
-
options.onStatusChange?.("Syncing sandbox...");
|
|
5163
|
-
await enforceSandboxIgnore(name, options.cwd);
|
|
5164
|
-
options.onStatusChange?.("Thinking...");
|
|
5165
|
-
dockerArgs = [
|
|
5166
|
-
"sandbox",
|
|
5167
|
-
"exec",
|
|
5168
|
-
"-i",
|
|
5169
|
-
"-w",
|
|
5170
|
-
options.cwd,
|
|
5171
|
-
name,
|
|
5172
|
-
"codex",
|
|
5173
|
-
...codexArgs
|
|
5174
|
-
];
|
|
5175
4799
|
}
|
|
4800
|
+
options.onStatusChange?.("Thinking...");
|
|
4801
|
+
const dockerArgs = [
|
|
4802
|
+
"sandbox",
|
|
4803
|
+
"exec",
|
|
4804
|
+
"-i",
|
|
4805
|
+
"-w",
|
|
4806
|
+
options.cwd,
|
|
4807
|
+
this.sandboxName,
|
|
4808
|
+
"codex",
|
|
4809
|
+
...codexArgs
|
|
4810
|
+
];
|
|
5176
4811
|
log.debug("Spawning sandboxed codex", {
|
|
5177
4812
|
sandboxName: this.sandboxName,
|
|
5178
|
-
persistent: this.persistent,
|
|
5179
|
-
reusing: this.persistent && this.sandboxCreated,
|
|
5180
4813
|
args: dockerArgs.join(" "),
|
|
5181
4814
|
cwd: options.cwd
|
|
5182
4815
|
});
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
});
|
|
5195
|
-
}
|
|
5196
|
-
let agentMessages = [];
|
|
5197
|
-
const flushAgentMessages = () => {
|
|
5198
|
-
if (agentMessages.length > 0) {
|
|
5199
|
-
options.onOutput?.(agentMessages.join(`
|
|
4816
|
+
return await new Promise((resolve2) => {
|
|
4817
|
+
let rawOutput = "";
|
|
4818
|
+
let errorOutput = "";
|
|
4819
|
+
this.process = spawn5("docker", dockerArgs, {
|
|
4820
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4821
|
+
env: process.env
|
|
4822
|
+
});
|
|
4823
|
+
let agentMessages = [];
|
|
4824
|
+
const flushAgentMessages = () => {
|
|
4825
|
+
if (agentMessages.length > 0) {
|
|
4826
|
+
options.onOutput?.(agentMessages.join(`
|
|
5200
4827
|
|
|
5201
4828
|
`));
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
4829
|
+
agentMessages = [];
|
|
4830
|
+
}
|
|
4831
|
+
};
|
|
4832
|
+
let lineBuffer = "";
|
|
4833
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4834
|
+
lineBuffer += chunk.toString();
|
|
4835
|
+
const lines = lineBuffer.split(`
|
|
5209
4836
|
`);
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
4837
|
+
lineBuffer = lines.pop() ?? "";
|
|
4838
|
+
for (const line of lines) {
|
|
4839
|
+
if (!line.trim())
|
|
4840
|
+
continue;
|
|
4841
|
+
rawOutput += `${line}
|
|
5215
4842
|
`;
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
4843
|
+
log.debug("sandboxed codex stdout line", { line });
|
|
4844
|
+
try {
|
|
4845
|
+
const event = JSON.parse(line);
|
|
4846
|
+
const { type, item } = event;
|
|
4847
|
+
if (type === "item.started" && item?.type === "command_execution") {
|
|
4848
|
+
const cmd = (item.command ?? "").split(`
|
|
5222
4849
|
`)[0].slice(0, 80);
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
4850
|
+
options.onToolActivity?.(`running: ${cmd}`);
|
|
4851
|
+
} else if (type === "item.completed" && item?.type === "command_execution") {
|
|
4852
|
+
const code = item.exit_code;
|
|
4853
|
+
options.onToolActivity?.(code === 0 ? "done" : `exit ${code}`);
|
|
4854
|
+
} else if (type === "item.completed" && item?.type === "reasoning") {
|
|
4855
|
+
const text = (item.text ?? "").trim().replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
|
|
4856
|
+
if (text)
|
|
4857
|
+
options.onToolActivity?.(text);
|
|
4858
|
+
} else if (type === "item.completed" && item?.type === "agent_message") {
|
|
4859
|
+
const text = item.text ?? "";
|
|
4860
|
+
if (text) {
|
|
4861
|
+
agentMessages.push(text);
|
|
4862
|
+
options.onToolActivity?.(text.split(`
|
|
5236
4863
|
`)[0].slice(0, 80));
|
|
5237
|
-
}
|
|
5238
|
-
} else if (type === "turn.completed") {
|
|
5239
|
-
flushAgentMessages();
|
|
5240
4864
|
}
|
|
5241
|
-
}
|
|
5242
|
-
|
|
5243
|
-
`;
|
|
5244
|
-
rawOutput += newLine;
|
|
5245
|
-
options.onOutput?.(newLine);
|
|
4865
|
+
} else if (type === "turn.completed") {
|
|
4866
|
+
flushAgentMessages();
|
|
5246
4867
|
}
|
|
4868
|
+
} catch {
|
|
4869
|
+
const newLine = `${line}
|
|
4870
|
+
`;
|
|
4871
|
+
rawOutput += newLine;
|
|
4872
|
+
options.onOutput?.(newLine);
|
|
5247
4873
|
}
|
|
5248
|
-
}
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
});
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
success: false,
|
|
5260
|
-
output: rawOutput,
|
|
5261
|
-
error: "Aborted by user",
|
|
5262
|
-
exitCode: code ?? 143
|
|
5263
|
-
});
|
|
5264
|
-
return;
|
|
5265
|
-
}
|
|
5266
|
-
if (code === 0) {
|
|
5267
|
-
resolve2({
|
|
5268
|
-
success: true,
|
|
5269
|
-
output: rawOutput,
|
|
5270
|
-
exitCode: 0
|
|
5271
|
-
});
|
|
5272
|
-
} else {
|
|
5273
|
-
resolve2({
|
|
5274
|
-
success: false,
|
|
5275
|
-
output: rawOutput,
|
|
5276
|
-
error: errorOutput || `sandboxed codex exited with code ${code}`,
|
|
5277
|
-
exitCode: code ?? 1
|
|
5278
|
-
});
|
|
5279
|
-
}
|
|
5280
|
-
});
|
|
5281
|
-
this.process.on("error", (err) => {
|
|
5282
|
-
this.process = null;
|
|
5283
|
-
if (this.persistent && !this.sandboxCreated) {}
|
|
4874
|
+
}
|
|
4875
|
+
});
|
|
4876
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
4877
|
+
const text = chunk.toString();
|
|
4878
|
+
errorOutput += text;
|
|
4879
|
+
log.debug("sandboxed codex stderr", { text: text.slice(0, 500) });
|
|
4880
|
+
});
|
|
4881
|
+
this.process.on("close", (code) => {
|
|
4882
|
+
this.process = null;
|
|
4883
|
+
flushAgentMessages();
|
|
4884
|
+
if (this.aborted) {
|
|
5284
4885
|
resolve2({
|
|
5285
4886
|
success: false,
|
|
5286
4887
|
output: rawOutput,
|
|
5287
|
-
error:
|
|
5288
|
-
exitCode:
|
|
4888
|
+
error: "Aborted by user",
|
|
4889
|
+
exitCode: code ?? 143
|
|
5289
4890
|
});
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4893
|
+
if (code === 0) {
|
|
4894
|
+
resolve2({ success: true, output: rawOutput, exitCode: 0 });
|
|
4895
|
+
} else {
|
|
4896
|
+
resolve2({
|
|
4897
|
+
success: false,
|
|
4898
|
+
output: rawOutput,
|
|
4899
|
+
error: errorOutput || `sandboxed codex exited with code ${code}`,
|
|
4900
|
+
exitCode: code ?? 1
|
|
5294
4901
|
});
|
|
5295
4902
|
}
|
|
5296
|
-
this.process.stdin?.write(options.prompt);
|
|
5297
|
-
this.process.stdin?.end();
|
|
5298
4903
|
});
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
4904
|
+
this.process.on("error", (err) => {
|
|
4905
|
+
this.process = null;
|
|
4906
|
+
resolve2({
|
|
4907
|
+
success: false,
|
|
4908
|
+
output: rawOutput,
|
|
4909
|
+
error: `Failed to spawn docker sandbox: ${err.message}`,
|
|
4910
|
+
exitCode: 1
|
|
4911
|
+
});
|
|
4912
|
+
});
|
|
4913
|
+
if (options.signal) {
|
|
4914
|
+
options.signal.addEventListener("abort", () => {
|
|
4915
|
+
this.abort();
|
|
4916
|
+
});
|
|
5302
4917
|
}
|
|
5303
|
-
|
|
4918
|
+
this.process.stdin?.write(options.prompt);
|
|
4919
|
+
this.process.stdin?.end();
|
|
4920
|
+
});
|
|
5304
4921
|
}
|
|
5305
4922
|
abort() {
|
|
5306
4923
|
this.aborted = true;
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
});
|
|
4924
|
+
if (!this.process)
|
|
4925
|
+
return;
|
|
4926
|
+
this.process.kill("SIGTERM");
|
|
4927
|
+
const timer = setTimeout(() => {
|
|
5312
4928
|
if (this.process) {
|
|
5313
|
-
this.process.kill("
|
|
5314
|
-
const timer = setTimeout(() => {
|
|
5315
|
-
if (this.process) {
|
|
5316
|
-
this.process.kill("SIGKILL");
|
|
5317
|
-
}
|
|
5318
|
-
}, 3000);
|
|
5319
|
-
if (timer.unref)
|
|
5320
|
-
timer.unref();
|
|
4929
|
+
this.process.kill("SIGKILL");
|
|
5321
4930
|
}
|
|
5322
|
-
}
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
log.debug("Aborting sandboxed codex (ephemeral — removing sandbox)", {
|
|
5326
|
-
sandboxName: this.sandboxName
|
|
5327
|
-
});
|
|
5328
|
-
try {
|
|
5329
|
-
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5330
|
-
} catch {}
|
|
5331
|
-
}
|
|
5332
|
-
}
|
|
5333
|
-
destroy() {
|
|
5334
|
-
if (!this.sandboxName)
|
|
5335
|
-
return;
|
|
5336
|
-
if (this.userManaged) {
|
|
5337
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
5338
|
-
return;
|
|
5339
|
-
}
|
|
5340
|
-
const log = getLogger();
|
|
5341
|
-
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
5342
|
-
try {
|
|
5343
|
-
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5344
|
-
} catch {}
|
|
5345
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
5346
|
-
this.sandboxName = null;
|
|
5347
|
-
this.sandboxCreated = false;
|
|
5348
|
-
}
|
|
5349
|
-
cleanupSandbox() {
|
|
5350
|
-
if (!this.sandboxName)
|
|
5351
|
-
return;
|
|
5352
|
-
const log = getLogger();
|
|
5353
|
-
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
5354
|
-
try {
|
|
5355
|
-
execSync9(`docker sandbox rm ${this.sandboxName}`, {
|
|
5356
|
-
timeout: 60000
|
|
5357
|
-
});
|
|
5358
|
-
} catch {}
|
|
5359
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
5360
|
-
this.sandboxName = null;
|
|
4931
|
+
}, 3000);
|
|
4932
|
+
if (timer.unref)
|
|
4933
|
+
timer.unref();
|
|
5361
4934
|
}
|
|
5362
4935
|
async isSandboxRunning() {
|
|
5363
|
-
if (!this.sandboxName)
|
|
5364
|
-
return false;
|
|
5365
4936
|
try {
|
|
5366
4937
|
const { promisify: promisify2 } = await import("node:util");
|
|
5367
4938
|
const { exec: exec2 } = await import("node:child_process");
|
|
@@ -5374,14 +4945,6 @@ class SandboxedCodexRunner {
|
|
|
5374
4945
|
return false;
|
|
5375
4946
|
}
|
|
5376
4947
|
}
|
|
5377
|
-
async createSandboxWithClaude(name, cwd) {
|
|
5378
|
-
const { promisify: promisify2 } = await import("node:util");
|
|
5379
|
-
const { exec: exec2 } = await import("node:child_process");
|
|
5380
|
-
const execAsync2 = promisify2(exec2);
|
|
5381
|
-
try {
|
|
5382
|
-
await execAsync2(`docker sandbox run --name ${name} claude ${cwd} -- --version`, { timeout: 120000 });
|
|
5383
|
-
} catch {}
|
|
5384
|
-
}
|
|
5385
4948
|
async ensureCodexInstalled(name) {
|
|
5386
4949
|
const { promisify: promisify2 } = await import("node:util");
|
|
5387
4950
|
const { exec: exec2 } = await import("node:child_process");
|
|
@@ -5391,38 +4954,28 @@ class SandboxedCodexRunner {
|
|
|
5391
4954
|
timeout: 5000
|
|
5392
4955
|
});
|
|
5393
4956
|
} catch {
|
|
5394
|
-
await execAsync2(`docker sandbox exec ${name} npm install -g @openai/codex`, {
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
getSandboxName() {
|
|
5398
|
-
return this.sandboxName;
|
|
5399
|
-
}
|
|
5400
|
-
}
|
|
5401
|
-
function buildSandboxName2(options) {
|
|
5402
|
-
const ts = Date.now();
|
|
5403
|
-
if (options.activity) {
|
|
5404
|
-
const match = options.activity.match(/issue\s*#(\d+)/i);
|
|
5405
|
-
if (match) {
|
|
5406
|
-
return `locus-codex-issue-${match[1]}-${ts}`;
|
|
4957
|
+
await execAsync2(`docker sandbox exec ${name} npm install -g @openai/codex`, {
|
|
4958
|
+
timeout: 120000
|
|
4959
|
+
});
|
|
5407
4960
|
}
|
|
5408
4961
|
}
|
|
5409
|
-
const segment = options.cwd.split("/").pop() ?? "run";
|
|
5410
|
-
return `locus-codex-${segment}-${ts}`;
|
|
5411
4962
|
}
|
|
5412
4963
|
var init_codex_sandbox = __esm(() => {
|
|
5413
4964
|
init_logger();
|
|
5414
4965
|
init_sandbox_ignore();
|
|
5415
|
-
init_shutdown();
|
|
5416
4966
|
init_codex();
|
|
5417
4967
|
});
|
|
5418
4968
|
|
|
5419
4969
|
// src/ai/runner.ts
|
|
5420
4970
|
async function createRunnerAsync(provider, sandboxed) {
|
|
4971
|
+
if (sandboxed) {
|
|
4972
|
+
throw new Error("Sandboxed runner creation requires a provider sandbox name. Use createUserManagedSandboxRunner().");
|
|
4973
|
+
}
|
|
5421
4974
|
switch (provider) {
|
|
5422
4975
|
case "claude":
|
|
5423
|
-
return
|
|
4976
|
+
return new ClaudeRunner;
|
|
5424
4977
|
case "codex":
|
|
5425
|
-
return
|
|
4978
|
+
return new CodexRunner;
|
|
5426
4979
|
default:
|
|
5427
4980
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5428
4981
|
}
|
|
@@ -5430,9 +4983,9 @@ async function createRunnerAsync(provider, sandboxed) {
|
|
|
5430
4983
|
function createUserManagedSandboxRunner(provider, sandboxName) {
|
|
5431
4984
|
switch (provider) {
|
|
5432
4985
|
case "claude":
|
|
5433
|
-
return new SandboxedClaudeRunner(sandboxName
|
|
4986
|
+
return new SandboxedClaudeRunner(sandboxName);
|
|
5434
4987
|
case "codex":
|
|
5435
|
-
return new SandboxedCodexRunner(sandboxName
|
|
4988
|
+
return new SandboxedCodexRunner(sandboxName);
|
|
5436
4989
|
default:
|
|
5437
4990
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5438
4991
|
}
|
|
@@ -5532,10 +5085,20 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
5532
5085
|
});
|
|
5533
5086
|
if (options.runner) {
|
|
5534
5087
|
runner = options.runner;
|
|
5535
|
-
} else if (options.
|
|
5088
|
+
} else if (options.sandboxed ?? true) {
|
|
5089
|
+
if (!options.sandboxName) {
|
|
5090
|
+
indicator.stop();
|
|
5091
|
+
return {
|
|
5092
|
+
success: false,
|
|
5093
|
+
output: "",
|
|
5094
|
+
error: `Sandbox for provider "${resolvedProvider}" is not configured. ` + `Run "locus sandbox" and authenticate via "locus sandbox ${resolvedProvider}".`,
|
|
5095
|
+
interrupted: false,
|
|
5096
|
+
exitCode: 1
|
|
5097
|
+
};
|
|
5098
|
+
}
|
|
5536
5099
|
runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName);
|
|
5537
5100
|
} else {
|
|
5538
|
-
runner = await createRunnerAsync(resolvedProvider,
|
|
5101
|
+
runner = await createRunnerAsync(resolvedProvider, false);
|
|
5539
5102
|
}
|
|
5540
5103
|
const available = await runner.isAvailable();
|
|
5541
5104
|
if (!available) {
|
|
@@ -5822,6 +5385,10 @@ async function issueCommand(projectRoot, args) {
|
|
|
5822
5385
|
case "close":
|
|
5823
5386
|
await issueClose(projectRoot, parsed);
|
|
5824
5387
|
break;
|
|
5388
|
+
case "delete":
|
|
5389
|
+
case "rm":
|
|
5390
|
+
await issueDelete(projectRoot, parsed);
|
|
5391
|
+
break;
|
|
5825
5392
|
default:
|
|
5826
5393
|
if (/^\d+$/.test(parsed.subcommand)) {
|
|
5827
5394
|
parsed.positional.unshift(parsed.subcommand);
|
|
@@ -5857,7 +5424,7 @@ async function issueCreate(projectRoot, parsed) {
|
|
|
5857
5424
|
silent: true,
|
|
5858
5425
|
activity: "generating issue",
|
|
5859
5426
|
sandboxed: config.sandbox.enabled,
|
|
5860
|
-
sandboxName: config.sandbox.
|
|
5427
|
+
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
|
|
5861
5428
|
});
|
|
5862
5429
|
if (!aiResult.success && !aiResult.interrupted) {
|
|
5863
5430
|
process.stderr.write(`${red("✗")} Failed to generate issue: ${aiResult.error}
|
|
@@ -5949,21 +5516,26 @@ ${dim("────────────────────────
|
|
|
5949
5516
|
}
|
|
5950
5517
|
}
|
|
5951
5518
|
function buildIssueCreationPrompt(userRequest) {
|
|
5952
|
-
return
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5519
|
+
return `<role>
|
|
5520
|
+
You are a task planner for a software development team.
|
|
5521
|
+
Given a user request, create a well-structured GitHub issue.
|
|
5522
|
+
</role>
|
|
5523
|
+
|
|
5524
|
+
<output-format>
|
|
5525
|
+
Output ONLY a valid JSON object with exactly these fields:
|
|
5526
|
+
- "title": A concise, actionable issue title (max 80 characters)
|
|
5527
|
+
- "body": Detailed markdown description with context, acceptance criteria, and technical notes
|
|
5528
|
+
- "priority": One of: critical, high, medium, low
|
|
5529
|
+
- "type": One of: feature, bug, chore, refactor, docs
|
|
5530
|
+
</output-format>
|
|
5531
|
+
|
|
5532
|
+
<user-request>
|
|
5533
|
+
${userRequest}
|
|
5534
|
+
</user-request>
|
|
5535
|
+
|
|
5536
|
+
<constraints>
|
|
5537
|
+
Output ONLY the JSON object. No explanations, no code execution, no other text.
|
|
5538
|
+
</constraints>`;
|
|
5967
5539
|
}
|
|
5968
5540
|
function extractJSON(text) {
|
|
5969
5541
|
const codeBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
@@ -6227,6 +5799,43 @@ async function issueClose(projectRoot, parsed) {
|
|
|
6227
5799
|
process.exit(1);
|
|
6228
5800
|
}
|
|
6229
5801
|
}
|
|
5802
|
+
async function issueDelete(projectRoot, parsed) {
|
|
5803
|
+
const issueNumbers = parsed.positional.filter((a) => /^\d+$/.test(a)).map(Number);
|
|
5804
|
+
if (issueNumbers.length === 0) {
|
|
5805
|
+
process.stderr.write(`${red("✗")} No issue numbers provided.
|
|
5806
|
+
`);
|
|
5807
|
+
process.stderr.write(` Usage: ${bold("locus issue delete <number...>")}
|
|
5808
|
+
`);
|
|
5809
|
+
process.exit(1);
|
|
5810
|
+
}
|
|
5811
|
+
const label = issueNumbers.length === 1 ? `issue #${issueNumbers[0]}` : `${issueNumbers.length} issues (#${issueNumbers.join(", #")})`;
|
|
5812
|
+
process.stderr.write(`${yellow("⚠")} This will ${bold("permanently delete")} ${label}.
|
|
5813
|
+
`);
|
|
5814
|
+
const answer = await askQuestion(`${cyan("?")} Are you sure? ${dim("[y/N]")} `);
|
|
5815
|
+
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
5816
|
+
process.stderr.write(`${yellow("○")} Cancelled.
|
|
5817
|
+
`);
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
let failed = 0;
|
|
5821
|
+
for (const num of issueNumbers) {
|
|
5822
|
+
process.stderr.write(`${cyan("●")} Deleting issue #${num}...`);
|
|
5823
|
+
try {
|
|
5824
|
+
deleteIssue(num, { cwd: projectRoot });
|
|
5825
|
+
process.stderr.write(`\r${green("✓")} Deleted issue #${num}
|
|
5826
|
+
`);
|
|
5827
|
+
} catch (e) {
|
|
5828
|
+
process.stderr.write(`\r${red("✗")} #${num}: ${e.message}
|
|
5829
|
+
`);
|
|
5830
|
+
failed++;
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
if (issueNumbers.length > 1 && failed === 0) {
|
|
5834
|
+
process.stderr.write(`
|
|
5835
|
+
${green("✓")} All ${issueNumbers.length} issues deleted.
|
|
5836
|
+
`);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
6230
5839
|
function formatPriority(labels) {
|
|
6231
5840
|
for (const label of labels) {
|
|
6232
5841
|
if (label === "p:critical")
|
|
@@ -6329,6 +5938,7 @@ ${bold("Subcommands:")}
|
|
|
6329
5938
|
${cyan("show")} Show issue details
|
|
6330
5939
|
${cyan("label")} Bulk-update labels / sprint assignment
|
|
6331
5940
|
${cyan("close")} Close an issue
|
|
5941
|
+
${cyan("delete")} ${dim("(rm)")} Permanently delete issues (bulk)
|
|
6332
5942
|
|
|
6333
5943
|
${bold("Create options:")}
|
|
6334
5944
|
${dim("--sprint, -s")} Assign to sprint (milestone)
|
|
@@ -6350,6 +5960,7 @@ ${bold("Examples:")}
|
|
|
6350
5960
|
locus issue show 42
|
|
6351
5961
|
locus issue label 42 43 --sprint "Sprint 2"
|
|
6352
5962
|
locus issue close 42
|
|
5963
|
+
locus issue delete 42 43 44
|
|
6353
5964
|
|
|
6354
5965
|
`);
|
|
6355
5966
|
}
|
|
@@ -6358,6 +5969,7 @@ var init_issue = __esm(() => {
|
|
|
6358
5969
|
init_config();
|
|
6359
5970
|
init_github();
|
|
6360
5971
|
init_logger();
|
|
5972
|
+
init_sandbox();
|
|
6361
5973
|
init_table();
|
|
6362
5974
|
init_terminal();
|
|
6363
5975
|
init_types();
|
|
@@ -7015,9 +6627,9 @@ var init_sprint = __esm(() => {
|
|
|
7015
6627
|
});
|
|
7016
6628
|
|
|
7017
6629
|
// src/core/prompt-builder.ts
|
|
7018
|
-
import { execSync as
|
|
7019
|
-
import { existsSync as
|
|
7020
|
-
import { join as
|
|
6630
|
+
import { execSync as execSync7 } from "node:child_process";
|
|
6631
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
|
|
6632
|
+
import { join as join12 } from "node:path";
|
|
7021
6633
|
function buildExecutionPrompt(ctx) {
|
|
7022
6634
|
const sections = [];
|
|
7023
6635
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -7047,30 +6659,30 @@ function buildFeedbackPrompt(ctx) {
|
|
|
7047
6659
|
}
|
|
7048
6660
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
7049
6661
|
const sections = [];
|
|
7050
|
-
const locusmd = readFileSafe(
|
|
6662
|
+
const locusmd = readFileSafe(join12(projectRoot, ".locus", "LOCUS.md"));
|
|
7051
6663
|
if (locusmd) {
|
|
7052
|
-
sections.push(
|
|
7053
|
-
|
|
7054
|
-
|
|
6664
|
+
sections.push(`<project-instructions>
|
|
6665
|
+
${locusmd}
|
|
6666
|
+
</project-instructions>`);
|
|
7055
6667
|
}
|
|
7056
|
-
const learnings = readFileSafe(
|
|
6668
|
+
const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
|
|
7057
6669
|
if (learnings) {
|
|
7058
|
-
sections.push(
|
|
7059
|
-
|
|
7060
|
-
|
|
6670
|
+
sections.push(`<past-learnings>
|
|
6671
|
+
${learnings}
|
|
6672
|
+
</past-learnings>`);
|
|
7061
6673
|
}
|
|
7062
6674
|
if (previousMessages && previousMessages.length > 0) {
|
|
7063
6675
|
const recent = previousMessages.slice(-10);
|
|
7064
6676
|
const historyLines = recent.map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`);
|
|
7065
|
-
sections.push(
|
|
7066
|
-
|
|
6677
|
+
sections.push(`<previous-conversation>
|
|
7067
6678
|
${historyLines.join(`
|
|
7068
6679
|
|
|
7069
|
-
`)}
|
|
6680
|
+
`)}
|
|
6681
|
+
</previous-conversation>`);
|
|
7070
6682
|
}
|
|
7071
|
-
sections.push(
|
|
7072
|
-
|
|
7073
|
-
|
|
6683
|
+
sections.push(`<current-request>
|
|
6684
|
+
${userMessage}
|
|
6685
|
+
</current-request>`);
|
|
7074
6686
|
return sections.join(`
|
|
7075
6687
|
|
|
7076
6688
|
---
|
|
@@ -7078,63 +6690,66 @@ ${userMessage}`);
|
|
|
7078
6690
|
`);
|
|
7079
6691
|
}
|
|
7080
6692
|
function buildSystemContext(projectRoot) {
|
|
7081
|
-
const parts = [
|
|
7082
|
-
const locusmd = readFileSafe(
|
|
6693
|
+
const parts = [];
|
|
6694
|
+
const locusmd = readFileSafe(join12(projectRoot, ".locus", "LOCUS.md"));
|
|
7083
6695
|
if (locusmd) {
|
|
7084
|
-
parts.push(
|
|
7085
|
-
|
|
7086
|
-
|
|
6696
|
+
parts.push(`<project-instructions>
|
|
6697
|
+
${locusmd}
|
|
6698
|
+
</project-instructions>`);
|
|
7087
6699
|
}
|
|
7088
|
-
const learnings = readFileSafe(
|
|
6700
|
+
const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
|
|
7089
6701
|
if (learnings) {
|
|
7090
|
-
parts.push(
|
|
7091
|
-
|
|
7092
|
-
|
|
6702
|
+
parts.push(`<past-learnings>
|
|
6703
|
+
${learnings}
|
|
6704
|
+
</past-learnings>`);
|
|
7093
6705
|
}
|
|
7094
|
-
const discussionsDir =
|
|
7095
|
-
if (
|
|
6706
|
+
const discussionsDir = join12(projectRoot, ".locus", "discussions");
|
|
6707
|
+
if (existsSync13(discussionsDir)) {
|
|
7096
6708
|
try {
|
|
7097
6709
|
const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
7098
6710
|
for (const file of files) {
|
|
7099
|
-
const content = readFileSafe(
|
|
6711
|
+
const content = readFileSafe(join12(discussionsDir, file));
|
|
7100
6712
|
if (content) {
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
${content.slice(0, 2000)}
|
|
6713
|
+
const name = file.replace(".md", "");
|
|
6714
|
+
parts.push(`<discussion name="${name}">
|
|
6715
|
+
${content.slice(0, 2000)}
|
|
6716
|
+
</discussion>`);
|
|
7104
6717
|
}
|
|
7105
6718
|
}
|
|
7106
6719
|
} catch {}
|
|
7107
6720
|
}
|
|
7108
|
-
return
|
|
6721
|
+
return `<system-context>
|
|
6722
|
+
${parts.join(`
|
|
7109
6723
|
|
|
7110
|
-
`)
|
|
6724
|
+
`)}
|
|
6725
|
+
</system-context>`;
|
|
7111
6726
|
}
|
|
7112
6727
|
function buildTaskContext(issue, comments) {
|
|
7113
|
-
const parts = [
|
|
7114
|
-
|
|
7115
|
-
``,
|
|
7116
|
-
`## Issue #${issue.number}: ${issue.title}`,
|
|
7117
|
-
``,
|
|
7118
|
-
issue.body || "_No description provided._"
|
|
7119
|
-
];
|
|
6728
|
+
const parts = [];
|
|
6729
|
+
const issueParts = [issue.body || "_No description provided._"];
|
|
7120
6730
|
const labels = issue.labels.filter((l) => l.startsWith("p:") || l.startsWith("type:"));
|
|
7121
6731
|
if (labels.length > 0) {
|
|
7122
|
-
|
|
7123
|
-
**Labels:** ${labels.join(", ")}`);
|
|
6732
|
+
issueParts.push(`**Labels:** ${labels.join(", ")}`);
|
|
7124
6733
|
}
|
|
6734
|
+
parts.push(`<issue number="${issue.number}" title="${issue.title}">
|
|
6735
|
+
${issueParts.join(`
|
|
6736
|
+
|
|
6737
|
+
`)}
|
|
6738
|
+
</issue>`);
|
|
7125
6739
|
if (comments && comments.length > 0) {
|
|
7126
|
-
parts.push(
|
|
7127
|
-
|
|
7128
|
-
`)
|
|
7129
|
-
|
|
7130
|
-
parts.push(comment);
|
|
7131
|
-
}
|
|
6740
|
+
parts.push(`<issue-comments>
|
|
6741
|
+
${comments.join(`
|
|
6742
|
+
`)}
|
|
6743
|
+
</issue-comments>`);
|
|
7132
6744
|
}
|
|
7133
|
-
return
|
|
7134
|
-
`
|
|
6745
|
+
return `<task-context>
|
|
6746
|
+
${parts.join(`
|
|
6747
|
+
|
|
6748
|
+
`)}
|
|
6749
|
+
</task-context>`;
|
|
7135
6750
|
}
|
|
7136
6751
|
function buildSprintContext(sprintName, position, diffSummary) {
|
|
7137
|
-
const parts = [
|
|
6752
|
+
const parts = [];
|
|
7138
6753
|
if (sprintName) {
|
|
7139
6754
|
parts.push(`**Sprint:** ${sprintName}`);
|
|
7140
6755
|
}
|
|
@@ -7142,61 +6757,63 @@ function buildSprintContext(sprintName, position, diffSummary) {
|
|
|
7142
6757
|
parts.push(`**Position:** Task ${position}`);
|
|
7143
6758
|
}
|
|
7144
6759
|
if (diffSummary) {
|
|
7145
|
-
parts.push(
|
|
7146
|
-
## Changes from Previous Tasks
|
|
7147
|
-
|
|
6760
|
+
parts.push(`<previous-changes>
|
|
7148
6761
|
The following changes have already been made by earlier tasks in this sprint:
|
|
7149
6762
|
|
|
7150
6763
|
\`\`\`diff
|
|
7151
6764
|
${diffSummary}
|
|
7152
|
-
|
|
6765
|
+
\`\`\`
|
|
6766
|
+
</previous-changes>`);
|
|
7153
6767
|
}
|
|
7154
|
-
parts.push(
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
6768
|
+
parts.push(`**Important:** Build upon the changes from previous tasks. Do not revert or undo their work.`);
|
|
6769
|
+
return `<sprint-context>
|
|
6770
|
+
${parts.join(`
|
|
6771
|
+
|
|
6772
|
+
`)}
|
|
6773
|
+
</sprint-context>`;
|
|
7158
6774
|
}
|
|
7159
6775
|
function buildRepoContext(projectRoot) {
|
|
7160
|
-
const parts = [
|
|
6776
|
+
const parts = [];
|
|
7161
6777
|
try {
|
|
7162
|
-
const tree =
|
|
6778
|
+
const tree = execSync7("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();
|
|
7163
6779
|
if (tree) {
|
|
7164
|
-
parts.push(
|
|
7165
|
-
|
|
6780
|
+
parts.push(`<file-tree>
|
|
7166
6781
|
\`\`\`
|
|
7167
6782
|
${tree}
|
|
7168
|
-
|
|
6783
|
+
\`\`\`
|
|
6784
|
+
</file-tree>`);
|
|
7169
6785
|
}
|
|
7170
6786
|
} catch {}
|
|
7171
6787
|
try {
|
|
7172
|
-
const gitLog =
|
|
6788
|
+
const gitLog = execSync7("git log --oneline -10", {
|
|
7173
6789
|
cwd: projectRoot,
|
|
7174
6790
|
encoding: "utf-8",
|
|
7175
6791
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7176
6792
|
}).trim();
|
|
7177
6793
|
if (gitLog) {
|
|
7178
|
-
parts.push(
|
|
7179
|
-
|
|
6794
|
+
parts.push(`<recent-commits>
|
|
7180
6795
|
\`\`\`
|
|
7181
6796
|
${gitLog}
|
|
7182
|
-
|
|
6797
|
+
\`\`\`
|
|
6798
|
+
</recent-commits>`);
|
|
7183
6799
|
}
|
|
7184
6800
|
} catch {}
|
|
7185
6801
|
try {
|
|
7186
|
-
const branch =
|
|
6802
|
+
const branch = execSync7("git rev-parse --abbrev-ref HEAD", {
|
|
7187
6803
|
cwd: projectRoot,
|
|
7188
6804
|
encoding: "utf-8",
|
|
7189
6805
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7190
6806
|
}).trim();
|
|
7191
6807
|
parts.push(`**Current branch:** ${branch}`);
|
|
7192
6808
|
} catch {}
|
|
7193
|
-
return
|
|
6809
|
+
return `<repository-context>
|
|
6810
|
+
${parts.join(`
|
|
7194
6811
|
|
|
7195
|
-
`)
|
|
6812
|
+
`)}
|
|
6813
|
+
</repository-context>`;
|
|
7196
6814
|
}
|
|
7197
6815
|
function buildExecutionRules(config) {
|
|
7198
|
-
return
|
|
7199
|
-
|
|
6816
|
+
return `<execution-rules>
|
|
7200
6817
|
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.
|
|
7201
6818
|
2. **Code quality:** Follow existing code style. Run linters/formatters if available.
|
|
7202
6819
|
3. **Testing:** If test files exist for modified code, update them accordingly.
|
|
@@ -7208,43 +6825,43 @@ function buildExecutionRules(config) {
|
|
|
7208
6825
|
5. **Base branch:** ${config.agent.baseBranch}
|
|
7209
6826
|
6. **Provider:** ${config.ai.provider} / ${config.ai.model}
|
|
7210
6827
|
|
|
7211
|
-
When you are done, provide a brief summary of what you changed and why
|
|
6828
|
+
When you are done, provide a brief summary of what you changed and why.
|
|
6829
|
+
</execution-rules>`;
|
|
7212
6830
|
}
|
|
7213
6831
|
function buildPRContext(prNumber, diff, comments) {
|
|
7214
6832
|
const parts = [
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
diff.slice(0, 1e4),
|
|
7221
|
-
"```"
|
|
6833
|
+
`<pr-diff>
|
|
6834
|
+
\`\`\`diff
|
|
6835
|
+
${diff.slice(0, 1e4)}
|
|
6836
|
+
\`\`\`
|
|
6837
|
+
</pr-diff>`
|
|
7222
6838
|
];
|
|
7223
6839
|
if (comments.length > 0) {
|
|
7224
|
-
parts.push(
|
|
7225
|
-
|
|
7226
|
-
`)
|
|
7227
|
-
|
|
7228
|
-
parts.push(comment);
|
|
7229
|
-
}
|
|
6840
|
+
parts.push(`<review-comments>
|
|
6841
|
+
${comments.join(`
|
|
6842
|
+
`)}
|
|
6843
|
+
</review-comments>`);
|
|
7230
6844
|
}
|
|
7231
|
-
return
|
|
7232
|
-
`
|
|
6845
|
+
return `<pr-context number="${prNumber}">
|
|
6846
|
+
${parts.join(`
|
|
6847
|
+
|
|
6848
|
+
`)}
|
|
6849
|
+
</pr-context>`;
|
|
7233
6850
|
}
|
|
7234
6851
|
function buildFeedbackInstructions() {
|
|
7235
|
-
return
|
|
7236
|
-
|
|
6852
|
+
return `<instructions>
|
|
7237
6853
|
1. Address ALL review feedback from the comments above.
|
|
7238
6854
|
2. Make targeted changes — do NOT rewrite code from scratch.
|
|
7239
6855
|
3. If a reviewer comment is unclear, make your best judgment and note your interpretation.
|
|
7240
6856
|
4. Push changes to the same branch — do NOT create a new PR.
|
|
7241
|
-
5. When done, summarize what you changed in response to each comment
|
|
6857
|
+
5. When done, summarize what you changed in response to each comment.
|
|
6858
|
+
</instructions>`;
|
|
7242
6859
|
}
|
|
7243
6860
|
function readFileSafe(path) {
|
|
7244
6861
|
try {
|
|
7245
|
-
if (!
|
|
6862
|
+
if (!existsSync13(path))
|
|
7246
6863
|
return null;
|
|
7247
|
-
return
|
|
6864
|
+
return readFileSync9(path, "utf-8");
|
|
7248
6865
|
} catch {
|
|
7249
6866
|
return null;
|
|
7250
6867
|
}
|
|
@@ -7436,7 +7053,7 @@ var init_diff_renderer = __esm(() => {
|
|
|
7436
7053
|
});
|
|
7437
7054
|
|
|
7438
7055
|
// src/repl/commands.ts
|
|
7439
|
-
import { execSync as
|
|
7056
|
+
import { execSync as execSync8 } from "node:child_process";
|
|
7440
7057
|
function getSlashCommands() {
|
|
7441
7058
|
return [
|
|
7442
7059
|
{
|
|
@@ -7628,7 +7245,7 @@ function cmdModel(args, ctx) {
|
|
|
7628
7245
|
}
|
|
7629
7246
|
function cmdDiff(_args, ctx) {
|
|
7630
7247
|
try {
|
|
7631
|
-
const diff =
|
|
7248
|
+
const diff = execSync8("git diff", {
|
|
7632
7249
|
cwd: ctx.projectRoot,
|
|
7633
7250
|
encoding: "utf-8",
|
|
7634
7251
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7664,7 +7281,7 @@ function cmdDiff(_args, ctx) {
|
|
|
7664
7281
|
}
|
|
7665
7282
|
function cmdUndo(_args, ctx) {
|
|
7666
7283
|
try {
|
|
7667
|
-
const status =
|
|
7284
|
+
const status = execSync8("git status --porcelain", {
|
|
7668
7285
|
cwd: ctx.projectRoot,
|
|
7669
7286
|
encoding: "utf-8",
|
|
7670
7287
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7674,7 +7291,7 @@ function cmdUndo(_args, ctx) {
|
|
|
7674
7291
|
`);
|
|
7675
7292
|
return;
|
|
7676
7293
|
}
|
|
7677
|
-
|
|
7294
|
+
execSync8("git checkout .", {
|
|
7678
7295
|
cwd: ctx.projectRoot,
|
|
7679
7296
|
encoding: "utf-8",
|
|
7680
7297
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7708,7 +7325,7 @@ var init_commands = __esm(() => {
|
|
|
7708
7325
|
|
|
7709
7326
|
// src/repl/completions.ts
|
|
7710
7327
|
import { readdirSync as readdirSync4 } from "node:fs";
|
|
7711
|
-
import { basename as basename2, dirname as
|
|
7328
|
+
import { basename as basename2, dirname as dirname3, join as join13 } from "node:path";
|
|
7712
7329
|
|
|
7713
7330
|
class SlashCommandCompletion {
|
|
7714
7331
|
commands;
|
|
@@ -7763,7 +7380,7 @@ class FilePathCompletion {
|
|
|
7763
7380
|
}
|
|
7764
7381
|
findMatches(partial) {
|
|
7765
7382
|
try {
|
|
7766
|
-
const dir = partial.includes("/") ?
|
|
7383
|
+
const dir = partial.includes("/") ? join13(this.projectRoot, dirname3(partial)) : this.projectRoot;
|
|
7767
7384
|
const prefix = basename2(partial);
|
|
7768
7385
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7769
7386
|
return entries.filter((e) => {
|
|
@@ -7774,7 +7391,7 @@ class FilePathCompletion {
|
|
|
7774
7391
|
return e.name.startsWith(prefix);
|
|
7775
7392
|
}).map((e) => {
|
|
7776
7393
|
const name = e.isDirectory() ? `${e.name}/` : e.name;
|
|
7777
|
-
return partial.includes("/") ? `${
|
|
7394
|
+
return partial.includes("/") ? `${dirname3(partial)}/${name}` : name;
|
|
7778
7395
|
}).slice(0, 20);
|
|
7779
7396
|
} catch {
|
|
7780
7397
|
return [];
|
|
@@ -7799,14 +7416,14 @@ class CombinedCompletion {
|
|
|
7799
7416
|
var init_completions = () => {};
|
|
7800
7417
|
|
|
7801
7418
|
// src/repl/input-history.ts
|
|
7802
|
-
import { existsSync as
|
|
7803
|
-
import { dirname as
|
|
7419
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7420
|
+
import { dirname as dirname4, join as join14 } from "node:path";
|
|
7804
7421
|
|
|
7805
7422
|
class InputHistory {
|
|
7806
7423
|
entries = [];
|
|
7807
7424
|
filePath;
|
|
7808
7425
|
constructor(projectRoot) {
|
|
7809
|
-
this.filePath =
|
|
7426
|
+
this.filePath = join14(projectRoot, ".locus", "sessions", ".input-history");
|
|
7810
7427
|
this.load();
|
|
7811
7428
|
}
|
|
7812
7429
|
add(text) {
|
|
@@ -7845,22 +7462,22 @@ class InputHistory {
|
|
|
7845
7462
|
}
|
|
7846
7463
|
load() {
|
|
7847
7464
|
try {
|
|
7848
|
-
if (!
|
|
7465
|
+
if (!existsSync14(this.filePath))
|
|
7849
7466
|
return;
|
|
7850
|
-
const content =
|
|
7467
|
+
const content = readFileSync10(this.filePath, "utf-8");
|
|
7851
7468
|
this.entries = content.split(`
|
|
7852
7469
|
`).map((line) => this.unescape(line)).filter(Boolean);
|
|
7853
7470
|
} catch {}
|
|
7854
7471
|
}
|
|
7855
7472
|
save() {
|
|
7856
7473
|
try {
|
|
7857
|
-
const dir =
|
|
7858
|
-
if (!
|
|
7859
|
-
|
|
7474
|
+
const dir = dirname4(this.filePath);
|
|
7475
|
+
if (!existsSync14(dir)) {
|
|
7476
|
+
mkdirSync9(dir, { recursive: true });
|
|
7860
7477
|
}
|
|
7861
7478
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
7862
7479
|
`);
|
|
7863
|
-
|
|
7480
|
+
writeFileSync6(this.filePath, content, "utf-8");
|
|
7864
7481
|
} catch {}
|
|
7865
7482
|
}
|
|
7866
7483
|
escape(text) {
|
|
@@ -7886,21 +7503,21 @@ var init_model_config = __esm(() => {
|
|
|
7886
7503
|
|
|
7887
7504
|
// src/repl/session-manager.ts
|
|
7888
7505
|
import {
|
|
7889
|
-
existsSync as
|
|
7890
|
-
mkdirSync as
|
|
7506
|
+
existsSync as existsSync15,
|
|
7507
|
+
mkdirSync as mkdirSync10,
|
|
7891
7508
|
readdirSync as readdirSync5,
|
|
7892
|
-
readFileSync as
|
|
7893
|
-
unlinkSync as
|
|
7894
|
-
writeFileSync as
|
|
7509
|
+
readFileSync as readFileSync11,
|
|
7510
|
+
unlinkSync as unlinkSync3,
|
|
7511
|
+
writeFileSync as writeFileSync7
|
|
7895
7512
|
} from "node:fs";
|
|
7896
|
-
import { basename as basename3, join as
|
|
7513
|
+
import { basename as basename3, join as join15 } from "node:path";
|
|
7897
7514
|
|
|
7898
7515
|
class SessionManager {
|
|
7899
7516
|
sessionsDir;
|
|
7900
7517
|
constructor(projectRoot) {
|
|
7901
|
-
this.sessionsDir =
|
|
7902
|
-
if (!
|
|
7903
|
-
|
|
7518
|
+
this.sessionsDir = join15(projectRoot, ".locus", "sessions");
|
|
7519
|
+
if (!existsSync15(this.sessionsDir)) {
|
|
7520
|
+
mkdirSync10(this.sessionsDir, { recursive: true });
|
|
7904
7521
|
}
|
|
7905
7522
|
}
|
|
7906
7523
|
create(options) {
|
|
@@ -7925,14 +7542,14 @@ class SessionManager {
|
|
|
7925
7542
|
}
|
|
7926
7543
|
isPersisted(sessionOrId) {
|
|
7927
7544
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
7928
|
-
return
|
|
7545
|
+
return existsSync15(this.getSessionPath(sessionId));
|
|
7929
7546
|
}
|
|
7930
7547
|
load(idOrPrefix) {
|
|
7931
7548
|
const files = this.listSessionFiles();
|
|
7932
7549
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
7933
|
-
if (
|
|
7550
|
+
if (existsSync15(exactPath)) {
|
|
7934
7551
|
try {
|
|
7935
|
-
return JSON.parse(
|
|
7552
|
+
return JSON.parse(readFileSync11(exactPath, "utf-8"));
|
|
7936
7553
|
} catch {
|
|
7937
7554
|
return null;
|
|
7938
7555
|
}
|
|
@@ -7940,7 +7557,7 @@ class SessionManager {
|
|
|
7940
7557
|
const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
|
|
7941
7558
|
if (matches.length === 1) {
|
|
7942
7559
|
try {
|
|
7943
|
-
return JSON.parse(
|
|
7560
|
+
return JSON.parse(readFileSync11(matches[0], "utf-8"));
|
|
7944
7561
|
} catch {
|
|
7945
7562
|
return null;
|
|
7946
7563
|
}
|
|
@@ -7953,7 +7570,7 @@ class SessionManager {
|
|
|
7953
7570
|
save(session) {
|
|
7954
7571
|
session.updated = new Date().toISOString();
|
|
7955
7572
|
const path = this.getSessionPath(session.id);
|
|
7956
|
-
|
|
7573
|
+
writeFileSync7(path, `${JSON.stringify(session, null, 2)}
|
|
7957
7574
|
`, "utf-8");
|
|
7958
7575
|
}
|
|
7959
7576
|
addMessage(session, message) {
|
|
@@ -7965,7 +7582,7 @@ class SessionManager {
|
|
|
7965
7582
|
const sessions = [];
|
|
7966
7583
|
for (const file of files) {
|
|
7967
7584
|
try {
|
|
7968
|
-
const session = JSON.parse(
|
|
7585
|
+
const session = JSON.parse(readFileSync11(file, "utf-8"));
|
|
7969
7586
|
sessions.push({
|
|
7970
7587
|
id: session.id,
|
|
7971
7588
|
created: session.created,
|
|
@@ -7980,8 +7597,8 @@ class SessionManager {
|
|
|
7980
7597
|
}
|
|
7981
7598
|
delete(sessionId) {
|
|
7982
7599
|
const path = this.getSessionPath(sessionId);
|
|
7983
|
-
if (
|
|
7984
|
-
|
|
7600
|
+
if (existsSync15(path)) {
|
|
7601
|
+
unlinkSync3(path);
|
|
7985
7602
|
return true;
|
|
7986
7603
|
}
|
|
7987
7604
|
return false;
|
|
@@ -7992,7 +7609,7 @@ class SessionManager {
|
|
|
7992
7609
|
let pruned = 0;
|
|
7993
7610
|
const withStats = files.map((f) => {
|
|
7994
7611
|
try {
|
|
7995
|
-
const session = JSON.parse(
|
|
7612
|
+
const session = JSON.parse(readFileSync11(f, "utf-8"));
|
|
7996
7613
|
return { path: f, updated: new Date(session.updated).getTime() };
|
|
7997
7614
|
} catch {
|
|
7998
7615
|
return { path: f, updated: 0 };
|
|
@@ -8002,7 +7619,7 @@ class SessionManager {
|
|
|
8002
7619
|
for (const entry of withStats) {
|
|
8003
7620
|
if (now - entry.updated > SESSION_MAX_AGE_MS) {
|
|
8004
7621
|
try {
|
|
8005
|
-
|
|
7622
|
+
unlinkSync3(entry.path);
|
|
8006
7623
|
pruned++;
|
|
8007
7624
|
} catch {}
|
|
8008
7625
|
}
|
|
@@ -8010,10 +7627,10 @@ class SessionManager {
|
|
|
8010
7627
|
const remaining = withStats.length - pruned;
|
|
8011
7628
|
if (remaining > MAX_SESSIONS) {
|
|
8012
7629
|
const toRemove = remaining - MAX_SESSIONS;
|
|
8013
|
-
const alive = withStats.filter((e) =>
|
|
7630
|
+
const alive = withStats.filter((e) => existsSync15(e.path));
|
|
8014
7631
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
8015
7632
|
try {
|
|
8016
|
-
|
|
7633
|
+
unlinkSync3(alive[i].path);
|
|
8017
7634
|
pruned++;
|
|
8018
7635
|
} catch {}
|
|
8019
7636
|
}
|
|
@@ -8025,7 +7642,7 @@ class SessionManager {
|
|
|
8025
7642
|
}
|
|
8026
7643
|
listSessionFiles() {
|
|
8027
7644
|
try {
|
|
8028
|
-
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
7645
|
+
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join15(this.sessionsDir, f));
|
|
8029
7646
|
} catch {
|
|
8030
7647
|
return [];
|
|
8031
7648
|
}
|
|
@@ -8034,7 +7651,7 @@ class SessionManager {
|
|
|
8034
7651
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8035
7652
|
}
|
|
8036
7653
|
getSessionPath(sessionId) {
|
|
8037
|
-
return
|
|
7654
|
+
return join15(this.sessionsDir, `${sessionId}.json`);
|
|
8038
7655
|
}
|
|
8039
7656
|
}
|
|
8040
7657
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -8044,7 +7661,7 @@ var init_session_manager = __esm(() => {
|
|
|
8044
7661
|
});
|
|
8045
7662
|
|
|
8046
7663
|
// src/repl/repl.ts
|
|
8047
|
-
import { execSync as
|
|
7664
|
+
import { execSync as execSync9 } from "node:child_process";
|
|
8048
7665
|
async function startRepl(options) {
|
|
8049
7666
|
const { projectRoot, config } = options;
|
|
8050
7667
|
const sessionManager = new SessionManager(projectRoot);
|
|
@@ -8062,7 +7679,7 @@ async function startRepl(options) {
|
|
|
8062
7679
|
} else {
|
|
8063
7680
|
let branch = "main";
|
|
8064
7681
|
try {
|
|
8065
|
-
branch =
|
|
7682
|
+
branch = execSync9("git rev-parse --abbrev-ref HEAD", {
|
|
8066
7683
|
cwd: projectRoot,
|
|
8067
7684
|
encoding: "utf-8",
|
|
8068
7685
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8105,16 +7722,17 @@ async function executeOneShotPrompt(prompt, session, sessionManager, options) {
|
|
|
8105
7722
|
async function runInteractiveRepl(session, sessionManager, options) {
|
|
8106
7723
|
const { projectRoot, config } = options;
|
|
8107
7724
|
let sandboxRunner = null;
|
|
8108
|
-
if (config.sandbox.enabled
|
|
7725
|
+
if (config.sandbox.enabled) {
|
|
8109
7726
|
const provider = inferProviderFromModel(config.ai.model) || config.ai.provider;
|
|
8110
|
-
|
|
8111
|
-
|
|
7727
|
+
const sandboxName = getProviderSandboxName(config.sandbox, provider);
|
|
7728
|
+
if (sandboxName) {
|
|
7729
|
+
sandboxRunner = createUserManagedSandboxRunner(provider, sandboxName);
|
|
7730
|
+
process.stderr.write(`${dim("Using")} ${dim(provider)} ${dim("sandbox")} ${dim(sandboxName)}
|
|
8112
7731
|
`);
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
sandboxRunner = new SandboxedClaudeRunner(sandboxName);
|
|
8116
|
-
process.stderr.write(`${dim("Sandbox mode: prompts will share sandbox")} ${dim(sandboxName)}
|
|
7732
|
+
} else {
|
|
7733
|
+
process.stderr.write(`${yellow("⚠")} ${dim(`No sandbox configured for ${provider}. Run locus sandbox.`)}
|
|
8117
7734
|
`);
|
|
7735
|
+
}
|
|
8118
7736
|
}
|
|
8119
7737
|
const history = new InputHistory(projectRoot);
|
|
8120
7738
|
const completion = new CombinedCompletion([
|
|
@@ -8148,10 +7766,17 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
8148
7766
|
const providerChanged = inferredProvider !== currentProvider;
|
|
8149
7767
|
currentProvider = inferredProvider;
|
|
8150
7768
|
session.metadata.provider = inferredProvider;
|
|
8151
|
-
if (providerChanged && config.sandbox.enabled
|
|
8152
|
-
|
|
8153
|
-
|
|
7769
|
+
if (providerChanged && config.sandbox.enabled) {
|
|
7770
|
+
const sandboxName = getProviderSandboxName(config.sandbox, inferredProvider);
|
|
7771
|
+
if (sandboxName) {
|
|
7772
|
+
sandboxRunner = createUserManagedSandboxRunner(inferredProvider, sandboxName);
|
|
7773
|
+
process.stderr.write(`${dim("Switched sandbox agent to")} ${dim(inferredProvider)} ${dim(`(${sandboxName})`)}
|
|
7774
|
+
`);
|
|
7775
|
+
} else {
|
|
7776
|
+
sandboxRunner = null;
|
|
7777
|
+
process.stderr.write(`${yellow("⚠")} ${dim(`No sandbox configured for ${inferredProvider}. Run locus sandbox.`)}
|
|
8154
7778
|
`);
|
|
7779
|
+
}
|
|
8155
7780
|
}
|
|
8156
7781
|
}
|
|
8157
7782
|
persistReplModelSelection(projectRoot, config, model);
|
|
@@ -8223,10 +7848,6 @@ ${red("✗")} ${msg}
|
|
|
8223
7848
|
break;
|
|
8224
7849
|
}
|
|
8225
7850
|
}
|
|
8226
|
-
if (sandboxRunner && "destroy" in sandboxRunner) {
|
|
8227
|
-
const runner = sandboxRunner;
|
|
8228
|
-
runner.destroy();
|
|
8229
|
-
}
|
|
8230
7851
|
const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
|
|
8231
7852
|
if (shouldPersistOnExit) {
|
|
8232
7853
|
sessionManager.save(session);
|
|
@@ -8244,6 +7865,7 @@ ${red("✗")} ${msg}
|
|
|
8244
7865
|
}
|
|
8245
7866
|
async function executeAITurn(prompt, session, options, verbose = false, runner) {
|
|
8246
7867
|
const { config, projectRoot } = options;
|
|
7868
|
+
const sandboxName = getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider);
|
|
8247
7869
|
const aiResult = await runAI({
|
|
8248
7870
|
prompt,
|
|
8249
7871
|
provider: config.ai.provider,
|
|
@@ -8251,7 +7873,7 @@ async function executeAITurn(prompt, session, options, verbose = false, runner)
|
|
|
8251
7873
|
cwd: projectRoot,
|
|
8252
7874
|
verbose,
|
|
8253
7875
|
sandboxed: config.sandbox.enabled,
|
|
8254
|
-
sandboxName
|
|
7876
|
+
sandboxName,
|
|
8255
7877
|
runner
|
|
8256
7878
|
});
|
|
8257
7879
|
if (aiResult.interrupted) {
|
|
@@ -8282,11 +7904,11 @@ function printWelcome(session) {
|
|
|
8282
7904
|
`);
|
|
8283
7905
|
}
|
|
8284
7906
|
var init_repl = __esm(() => {
|
|
8285
|
-
init_claude_sandbox();
|
|
8286
7907
|
init_run_ai();
|
|
8287
7908
|
init_runner();
|
|
8288
7909
|
init_ai_models();
|
|
8289
7910
|
init_prompt_builder();
|
|
7911
|
+
init_sandbox();
|
|
8290
7912
|
init_terminal();
|
|
8291
7913
|
init_commands();
|
|
8292
7914
|
init_completions();
|
|
@@ -8427,7 +8049,12 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
|
|
|
8427
8049
|
stream.emitStatus("thinking");
|
|
8428
8050
|
try {
|
|
8429
8051
|
const fullPrompt = buildReplPrompt(prompt, projectRoot, config);
|
|
8430
|
-
const
|
|
8052
|
+
const sandboxName = getProviderSandboxName(config.sandbox, config.ai.provider);
|
|
8053
|
+
const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName) : null : await createRunnerAsync(config.ai.provider, false);
|
|
8054
|
+
if (!runner) {
|
|
8055
|
+
stream.emitError(`Sandbox for provider "${config.ai.provider}" is not configured. Run locus sandbox.`, false);
|
|
8056
|
+
return;
|
|
8057
|
+
}
|
|
8431
8058
|
const available = await runner.isAvailable();
|
|
8432
8059
|
if (!available) {
|
|
8433
8060
|
stream.emitError(`${config.ai.provider} CLI not available`, false);
|
|
@@ -8469,13 +8096,14 @@ var init_exec = __esm(() => {
|
|
|
8469
8096
|
init_config();
|
|
8470
8097
|
init_logger();
|
|
8471
8098
|
init_prompt_builder();
|
|
8099
|
+
init_sandbox();
|
|
8472
8100
|
init_terminal();
|
|
8473
8101
|
init_repl();
|
|
8474
8102
|
init_session_manager();
|
|
8475
8103
|
});
|
|
8476
8104
|
|
|
8477
8105
|
// src/core/agent.ts
|
|
8478
|
-
import { execSync as
|
|
8106
|
+
import { execSync as execSync10 } from "node:child_process";
|
|
8479
8107
|
async function executeIssue(projectRoot, options) {
|
|
8480
8108
|
const log = getLogger();
|
|
8481
8109
|
const timer = createTimer();
|
|
@@ -8504,7 +8132,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
|
|
|
8504
8132
|
}
|
|
8505
8133
|
let issueComments = [];
|
|
8506
8134
|
try {
|
|
8507
|
-
const commentsRaw =
|
|
8135
|
+
const commentsRaw = execSync10(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8508
8136
|
if (commentsRaw) {
|
|
8509
8137
|
issueComments = commentsRaw.split(`
|
|
8510
8138
|
`).filter(Boolean);
|
|
@@ -8647,7 +8275,7 @@ ${c.body}`),
|
|
|
8647
8275
|
cwd: projectRoot,
|
|
8648
8276
|
activity: `iterating on PR #${prNumber}`,
|
|
8649
8277
|
sandboxed: config.sandbox.enabled,
|
|
8650
|
-
sandboxName: config.sandbox.
|
|
8278
|
+
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
|
|
8651
8279
|
});
|
|
8652
8280
|
if (aiResult.interrupted) {
|
|
8653
8281
|
process.stderr.write(`
|
|
@@ -8668,12 +8296,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
|
|
|
8668
8296
|
}
|
|
8669
8297
|
async function createIssuePR(projectRoot, config, issue) {
|
|
8670
8298
|
try {
|
|
8671
|
-
const currentBranch =
|
|
8299
|
+
const currentBranch = execSync10("git rev-parse --abbrev-ref HEAD", {
|
|
8672
8300
|
cwd: projectRoot,
|
|
8673
8301
|
encoding: "utf-8",
|
|
8674
8302
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8675
8303
|
}).trim();
|
|
8676
|
-
const diff =
|
|
8304
|
+
const diff = execSync10(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8677
8305
|
cwd: projectRoot,
|
|
8678
8306
|
encoding: "utf-8",
|
|
8679
8307
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8682,7 +8310,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
8682
8310
|
getLogger().verbose("No changes to create PR for");
|
|
8683
8311
|
return;
|
|
8684
8312
|
}
|
|
8685
|
-
|
|
8313
|
+
execSync10(`git push -u origin ${currentBranch}`, {
|
|
8686
8314
|
cwd: projectRoot,
|
|
8687
8315
|
encoding: "utf-8",
|
|
8688
8316
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8725,12 +8353,13 @@ var init_agent = __esm(() => {
|
|
|
8725
8353
|
init_github();
|
|
8726
8354
|
init_logger();
|
|
8727
8355
|
init_prompt_builder();
|
|
8356
|
+
init_sandbox();
|
|
8728
8357
|
});
|
|
8729
8358
|
|
|
8730
8359
|
// src/core/conflict.ts
|
|
8731
|
-
import { execSync as
|
|
8360
|
+
import { execSync as execSync11 } from "node:child_process";
|
|
8732
8361
|
function git2(args, cwd) {
|
|
8733
|
-
return
|
|
8362
|
+
return execSync11(`git ${args}`, {
|
|
8734
8363
|
cwd,
|
|
8735
8364
|
encoding: "utf-8",
|
|
8736
8365
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8791,76 +8420,260 @@ function checkForConflicts(cwd, baseBranch) {
|
|
|
8791
8420
|
`).filter(Boolean) ?? [];
|
|
8792
8421
|
const overlapping = ourChanges.filter((f) => theirChanges.includes(f));
|
|
8793
8422
|
return {
|
|
8794
|
-
hasConflict: overlapping.length > 0,
|
|
8795
|
-
conflictingFiles: overlapping,
|
|
8796
|
-
baseAdvanced: true,
|
|
8797
|
-
newCommits
|
|
8423
|
+
hasConflict: overlapping.length > 0,
|
|
8424
|
+
conflictingFiles: overlapping,
|
|
8425
|
+
baseAdvanced: true,
|
|
8426
|
+
newCommits
|
|
8427
|
+
};
|
|
8428
|
+
}
|
|
8429
|
+
function attemptRebase(cwd, baseBranch) {
|
|
8430
|
+
const log = getLogger();
|
|
8431
|
+
try {
|
|
8432
|
+
git2(`rebase origin/${baseBranch}`, cwd);
|
|
8433
|
+
log.info(`Successfully rebased onto origin/${baseBranch}`);
|
|
8434
|
+
return { success: true };
|
|
8435
|
+
} catch (_e) {
|
|
8436
|
+
log.warn("Rebase failed, aborting");
|
|
8437
|
+
const conflicts = [];
|
|
8438
|
+
try {
|
|
8439
|
+
const status = git2("diff --name-only --diff-filter=U", cwd);
|
|
8440
|
+
conflicts.push(...status.trim().split(`
|
|
8441
|
+
`).filter(Boolean));
|
|
8442
|
+
} catch {}
|
|
8443
|
+
gitSafe("rebase --abort", cwd);
|
|
8444
|
+
return { success: false, conflicts };
|
|
8445
|
+
}
|
|
8446
|
+
}
|
|
8447
|
+
function printConflictReport(result, baseBranch) {
|
|
8448
|
+
if (!result.baseAdvanced)
|
|
8449
|
+
return;
|
|
8450
|
+
if (result.hasConflict) {
|
|
8451
|
+
process.stderr.write(`
|
|
8452
|
+
${bold(red("✗"))} ${bold("Merge conflict detected")}
|
|
8453
|
+
|
|
8454
|
+
`);
|
|
8455
|
+
process.stderr.write(` Base branch ${cyan(`origin/${baseBranch}`)} has ${result.newCommits} new commit${result.newCommits === 1 ? "" : "s"}
|
|
8456
|
+
`);
|
|
8457
|
+
process.stderr.write(` The following files were modified in both branches:
|
|
8458
|
+
|
|
8459
|
+
`);
|
|
8460
|
+
for (const file of result.conflictingFiles) {
|
|
8461
|
+
process.stderr.write(` ${red("•")} ${file}
|
|
8462
|
+
`);
|
|
8463
|
+
}
|
|
8464
|
+
process.stderr.write(`
|
|
8465
|
+
${bold("To resolve:")}
|
|
8466
|
+
`);
|
|
8467
|
+
process.stderr.write(` 1. ${dim(`git rebase origin/${baseBranch}`)}
|
|
8468
|
+
`);
|
|
8469
|
+
process.stderr.write(` 2. Resolve conflicts in the listed files
|
|
8470
|
+
`);
|
|
8471
|
+
process.stderr.write(` 3. ${dim("git rebase --continue")}
|
|
8472
|
+
`);
|
|
8473
|
+
process.stderr.write(` 4. ${dim("locus run --resume")} to continue the sprint
|
|
8474
|
+
|
|
8475
|
+
`);
|
|
8476
|
+
} else if (result.newCommits > 0) {
|
|
8477
|
+
process.stderr.write(`
|
|
8478
|
+
${bold(yellow("⚠"))} Base branch has ${result.newCommits} new commit${result.newCommits === 1 ? "" : "s"} — auto-rebasing...
|
|
8479
|
+
`);
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
var init_conflict = __esm(() => {
|
|
8483
|
+
init_terminal();
|
|
8484
|
+
init_logger();
|
|
8485
|
+
});
|
|
8486
|
+
|
|
8487
|
+
// src/core/run-state.ts
|
|
8488
|
+
import {
|
|
8489
|
+
existsSync as existsSync16,
|
|
8490
|
+
mkdirSync as mkdirSync11,
|
|
8491
|
+
readFileSync as readFileSync12,
|
|
8492
|
+
unlinkSync as unlinkSync4,
|
|
8493
|
+
writeFileSync as writeFileSync8
|
|
8494
|
+
} from "node:fs";
|
|
8495
|
+
import { dirname as dirname5, join as join16 } from "node:path";
|
|
8496
|
+
function getRunStatePath(projectRoot) {
|
|
8497
|
+
return join16(projectRoot, ".locus", "run-state.json");
|
|
8498
|
+
}
|
|
8499
|
+
function loadRunState(projectRoot) {
|
|
8500
|
+
const path = getRunStatePath(projectRoot);
|
|
8501
|
+
if (!existsSync16(path))
|
|
8502
|
+
return null;
|
|
8503
|
+
try {
|
|
8504
|
+
return JSON.parse(readFileSync12(path, "utf-8"));
|
|
8505
|
+
} catch {
|
|
8506
|
+
getLogger().warn("Corrupted run-state.json, ignoring");
|
|
8507
|
+
return null;
|
|
8508
|
+
}
|
|
8509
|
+
}
|
|
8510
|
+
function saveRunState(projectRoot, state) {
|
|
8511
|
+
const path = getRunStatePath(projectRoot);
|
|
8512
|
+
const dir = dirname5(path);
|
|
8513
|
+
if (!existsSync16(dir)) {
|
|
8514
|
+
mkdirSync11(dir, { recursive: true });
|
|
8515
|
+
}
|
|
8516
|
+
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
8517
|
+
`, "utf-8");
|
|
8518
|
+
}
|
|
8519
|
+
function clearRunState(projectRoot) {
|
|
8520
|
+
const path = getRunStatePath(projectRoot);
|
|
8521
|
+
if (existsSync16(path)) {
|
|
8522
|
+
unlinkSync4(path);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
function createSprintRunState(sprint, branch, issues) {
|
|
8526
|
+
return {
|
|
8527
|
+
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
8528
|
+
type: "sprint",
|
|
8529
|
+
sprint,
|
|
8530
|
+
branch,
|
|
8531
|
+
startedAt: new Date().toISOString(),
|
|
8532
|
+
tasks: issues.map(({ number, order }) => ({
|
|
8533
|
+
issue: number,
|
|
8534
|
+
order,
|
|
8535
|
+
status: "pending"
|
|
8536
|
+
}))
|
|
8537
|
+
};
|
|
8538
|
+
}
|
|
8539
|
+
function createParallelRunState(issueNumbers) {
|
|
8540
|
+
return {
|
|
8541
|
+
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
8542
|
+
type: "parallel",
|
|
8543
|
+
startedAt: new Date().toISOString(),
|
|
8544
|
+
tasks: issueNumbers.map((issue, i) => ({
|
|
8545
|
+
issue,
|
|
8546
|
+
order: i + 1,
|
|
8547
|
+
status: "pending"
|
|
8548
|
+
}))
|
|
8549
|
+
};
|
|
8550
|
+
}
|
|
8551
|
+
function markTaskInProgress(state, issueNumber) {
|
|
8552
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
8553
|
+
if (task) {
|
|
8554
|
+
task.status = "in_progress";
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
function markTaskDone(state, issueNumber, prNumber) {
|
|
8558
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
8559
|
+
if (task) {
|
|
8560
|
+
task.status = "done";
|
|
8561
|
+
task.completedAt = new Date().toISOString();
|
|
8562
|
+
if (prNumber)
|
|
8563
|
+
task.pr = prNumber;
|
|
8564
|
+
}
|
|
8565
|
+
}
|
|
8566
|
+
function markTaskFailed(state, issueNumber, error) {
|
|
8567
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
8568
|
+
if (task) {
|
|
8569
|
+
task.status = "failed";
|
|
8570
|
+
task.failedAt = new Date().toISOString();
|
|
8571
|
+
task.error = error;
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
function getRunStats(state) {
|
|
8575
|
+
const tasks = state.tasks;
|
|
8576
|
+
return {
|
|
8577
|
+
total: tasks.length,
|
|
8578
|
+
done: tasks.filter((t) => t.status === "done").length,
|
|
8579
|
+
failed: tasks.filter((t) => t.status === "failed").length,
|
|
8580
|
+
pending: tasks.filter((t) => t.status === "pending").length,
|
|
8581
|
+
inProgress: tasks.filter((t) => t.status === "in_progress").length
|
|
8798
8582
|
};
|
|
8799
8583
|
}
|
|
8800
|
-
function
|
|
8801
|
-
const
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8584
|
+
function getNextTask(state) {
|
|
8585
|
+
const failed = state.tasks.find((t) => t.status === "failed");
|
|
8586
|
+
if (failed)
|
|
8587
|
+
return failed;
|
|
8588
|
+
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
8589
|
+
}
|
|
8590
|
+
var init_run_state = __esm(() => {
|
|
8591
|
+
init_logger();
|
|
8592
|
+
});
|
|
8593
|
+
|
|
8594
|
+
// src/core/shutdown.ts
|
|
8595
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
8596
|
+
function cleanupActiveSandboxes() {
|
|
8597
|
+
for (const name of activeSandboxes) {
|
|
8809
8598
|
try {
|
|
8810
|
-
|
|
8811
|
-
conflicts.push(...status.trim().split(`
|
|
8812
|
-
`).filter(Boolean));
|
|
8599
|
+
execSync12(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
8813
8600
|
} catch {}
|
|
8814
|
-
gitSafe("rebase --abort", cwd);
|
|
8815
|
-
return { success: false, conflicts };
|
|
8816
8601
|
}
|
|
8602
|
+
activeSandboxes.clear();
|
|
8817
8603
|
}
|
|
8818
|
-
function
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
process.stderr.write(` Base branch ${cyan(`origin/${baseBranch}`)} has ${result.newCommits} new commit${result.newCommits === 1 ? "" : "s"}
|
|
8827
|
-
`);
|
|
8828
|
-
process.stderr.write(` The following files were modified in both branches:
|
|
8829
|
-
|
|
8830
|
-
`);
|
|
8831
|
-
for (const file of result.conflictingFiles) {
|
|
8832
|
-
process.stderr.write(` ${red("•")} ${file}
|
|
8604
|
+
function registerShutdownHandlers(ctx) {
|
|
8605
|
+
shutdownContext = ctx;
|
|
8606
|
+
interruptCount = 0;
|
|
8607
|
+
const handler = () => {
|
|
8608
|
+
interruptCount++;
|
|
8609
|
+
if (interruptCount >= 2) {
|
|
8610
|
+
process.stderr.write(`
|
|
8611
|
+
Force exit.
|
|
8833
8612
|
`);
|
|
8613
|
+
process.exit(1);
|
|
8834
8614
|
}
|
|
8835
8615
|
process.stderr.write(`
|
|
8836
|
-
${bold("To resolve:")}
|
|
8837
|
-
`);
|
|
8838
|
-
process.stderr.write(` 1. ${dim(`git rebase origin/${baseBranch}`)}
|
|
8839
|
-
`);
|
|
8840
|
-
process.stderr.write(` 2. Resolve conflicts in the listed files
|
|
8841
|
-
`);
|
|
8842
|
-
process.stderr.write(` 3. ${dim("git rebase --continue")}
|
|
8843
|
-
`);
|
|
8844
|
-
process.stderr.write(` 4. ${dim("locus run --resume")} to continue the sprint
|
|
8845
8616
|
|
|
8617
|
+
Interrupted. Saving state...
|
|
8846
8618
|
`);
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8619
|
+
const state = shutdownContext?.getRunState?.();
|
|
8620
|
+
if (state && shutdownContext) {
|
|
8621
|
+
for (const task of state.tasks) {
|
|
8622
|
+
if (task.status === "in_progress") {
|
|
8623
|
+
task.status = "failed";
|
|
8624
|
+
task.failedAt = new Date().toISOString();
|
|
8625
|
+
task.error = "Interrupted by user";
|
|
8626
|
+
}
|
|
8627
|
+
}
|
|
8628
|
+
try {
|
|
8629
|
+
saveRunState(shutdownContext.projectRoot, state);
|
|
8630
|
+
process.stderr.write(`State saved. Resume with: locus run --resume
|
|
8850
8631
|
`);
|
|
8632
|
+
} catch {
|
|
8633
|
+
process.stderr.write(`Warning: Could not save run state.
|
|
8634
|
+
`);
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8637
|
+
cleanupActiveSandboxes();
|
|
8638
|
+
shutdownContext?.onShutdown?.();
|
|
8639
|
+
if (interruptTimer)
|
|
8640
|
+
clearTimeout(interruptTimer);
|
|
8641
|
+
interruptTimer = setTimeout(() => {
|
|
8642
|
+
interruptCount = 0;
|
|
8643
|
+
}, 2000);
|
|
8644
|
+
setTimeout(() => {
|
|
8645
|
+
process.exit(130);
|
|
8646
|
+
}, 100);
|
|
8647
|
+
};
|
|
8648
|
+
if (!shutdownRegistered) {
|
|
8649
|
+
process.on("SIGINT", handler);
|
|
8650
|
+
process.on("SIGTERM", handler);
|
|
8651
|
+
shutdownRegistered = true;
|
|
8851
8652
|
}
|
|
8653
|
+
return () => {
|
|
8654
|
+
process.removeListener("SIGINT", handler);
|
|
8655
|
+
process.removeListener("SIGTERM", handler);
|
|
8656
|
+
shutdownRegistered = false;
|
|
8657
|
+
shutdownContext = null;
|
|
8658
|
+
interruptCount = 0;
|
|
8659
|
+
if (interruptTimer) {
|
|
8660
|
+
clearTimeout(interruptTimer);
|
|
8661
|
+
interruptTimer = null;
|
|
8662
|
+
}
|
|
8663
|
+
};
|
|
8852
8664
|
}
|
|
8853
|
-
var
|
|
8854
|
-
|
|
8855
|
-
|
|
8665
|
+
var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
|
|
8666
|
+
var init_shutdown = __esm(() => {
|
|
8667
|
+
init_run_state();
|
|
8668
|
+
activeSandboxes = new Set;
|
|
8856
8669
|
});
|
|
8857
8670
|
|
|
8858
8671
|
// src/core/worktree.ts
|
|
8859
|
-
import { execSync as
|
|
8672
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
8860
8673
|
import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
|
|
8861
8674
|
import { join as join17 } from "node:path";
|
|
8862
8675
|
function git3(args, cwd) {
|
|
8863
|
-
return
|
|
8676
|
+
return execSync13(`git ${args}`, {
|
|
8864
8677
|
cwd,
|
|
8865
8678
|
encoding: "utf-8",
|
|
8866
8679
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8885,7 +8698,7 @@ function generateBranchName(issueNumber) {
|
|
|
8885
8698
|
}
|
|
8886
8699
|
function getWorktreeBranch(worktreePath) {
|
|
8887
8700
|
try {
|
|
8888
|
-
return
|
|
8701
|
+
return execSync13("git branch --show-current", {
|
|
8889
8702
|
cwd: worktreePath,
|
|
8890
8703
|
encoding: "utf-8",
|
|
8891
8704
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9010,7 +8823,13 @@ var exports_run = {};
|
|
|
9010
8823
|
__export(exports_run, {
|
|
9011
8824
|
runCommand: () => runCommand
|
|
9012
8825
|
});
|
|
9013
|
-
import { execSync as
|
|
8826
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
8827
|
+
function resolveExecutionContext(config, modelOverride) {
|
|
8828
|
+
const model = modelOverride ?? config.ai.model;
|
|
8829
|
+
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
8830
|
+
const sandboxName = getModelSandboxName(config.sandbox, model, provider);
|
|
8831
|
+
return { provider, model, sandboxName };
|
|
8832
|
+
}
|
|
9014
8833
|
function printRunHelp() {
|
|
9015
8834
|
process.stderr.write(`
|
|
9016
8835
|
${bold("locus run")} — Execute issues using AI agents
|
|
@@ -9093,6 +8912,7 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
9093
8912
|
}
|
|
9094
8913
|
async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
9095
8914
|
const log = getLogger();
|
|
8915
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9096
8916
|
if (!config.sprint.active) {
|
|
9097
8917
|
process.stderr.write(`${red("✗")} No active sprint. Set one with: ${bold("locus sprint active <name>")}
|
|
9098
8918
|
`);
|
|
@@ -9154,7 +8974,7 @@ ${yellow("⚠")} A sprint run is already in progress.
|
|
|
9154
8974
|
}
|
|
9155
8975
|
if (!flags.dryRun) {
|
|
9156
8976
|
try {
|
|
9157
|
-
|
|
8977
|
+
execSync14(`git checkout -B ${branchName}`, {
|
|
9158
8978
|
cwd: projectRoot,
|
|
9159
8979
|
encoding: "utf-8",
|
|
9160
8980
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9204,7 +9024,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
|
|
|
9204
9024
|
let sprintContext;
|
|
9205
9025
|
if (i > 0 && !flags.dryRun) {
|
|
9206
9026
|
try {
|
|
9207
|
-
sprintContext =
|
|
9027
|
+
sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
9208
9028
|
cwd: projectRoot,
|
|
9209
9029
|
encoding: "utf-8",
|
|
9210
9030
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9219,12 +9039,13 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
9219
9039
|
saveRunState(projectRoot, state);
|
|
9220
9040
|
const result = await executeIssue(projectRoot, {
|
|
9221
9041
|
issueNumber: task.issue,
|
|
9222
|
-
provider:
|
|
9223
|
-
model:
|
|
9042
|
+
provider: execution.provider,
|
|
9043
|
+
model: execution.model,
|
|
9224
9044
|
dryRun: flags.dryRun,
|
|
9225
9045
|
sprintContext,
|
|
9226
9046
|
skipPR: true,
|
|
9227
|
-
sandboxed
|
|
9047
|
+
sandboxed,
|
|
9048
|
+
sandboxName: execution.sandboxName
|
|
9228
9049
|
});
|
|
9229
9050
|
if (result.success) {
|
|
9230
9051
|
if (!flags.dryRun) {
|
|
@@ -9268,7 +9089,7 @@ ${bold("Summary:")}
|
|
|
9268
9089
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
9269
9090
|
if (prNumber !== undefined) {
|
|
9270
9091
|
try {
|
|
9271
|
-
|
|
9092
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9272
9093
|
cwd: projectRoot,
|
|
9273
9094
|
encoding: "utf-8",
|
|
9274
9095
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9283,6 +9104,7 @@ ${bold("Summary:")}
|
|
|
9283
9104
|
}
|
|
9284
9105
|
}
|
|
9285
9106
|
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
9107
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9286
9108
|
let isSprintIssue = false;
|
|
9287
9109
|
try {
|
|
9288
9110
|
const issue = getIssue(issueNumber, { cwd: projectRoot });
|
|
@@ -9295,11 +9117,11 @@ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, n
|
|
|
9295
9117
|
`);
|
|
9296
9118
|
await executeIssue(projectRoot, {
|
|
9297
9119
|
issueNumber,
|
|
9298
|
-
provider:
|
|
9299
|
-
model:
|
|
9120
|
+
provider: execution.provider,
|
|
9121
|
+
model: execution.model,
|
|
9300
9122
|
dryRun: flags.dryRun,
|
|
9301
9123
|
sandboxed,
|
|
9302
|
-
sandboxName:
|
|
9124
|
+
sandboxName: execution.sandboxName
|
|
9303
9125
|
});
|
|
9304
9126
|
return;
|
|
9305
9127
|
}
|
|
@@ -9326,11 +9148,11 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
9326
9148
|
const result = await executeIssue(projectRoot, {
|
|
9327
9149
|
issueNumber,
|
|
9328
9150
|
worktreePath,
|
|
9329
|
-
provider:
|
|
9330
|
-
model:
|
|
9151
|
+
provider: execution.provider,
|
|
9152
|
+
model: execution.model,
|
|
9331
9153
|
dryRun: flags.dryRun,
|
|
9332
9154
|
sandboxed,
|
|
9333
|
-
sandboxName:
|
|
9155
|
+
sandboxName: execution.sandboxName
|
|
9334
9156
|
});
|
|
9335
9157
|
if (worktreePath && !flags.dryRun) {
|
|
9336
9158
|
if (result.success) {
|
|
@@ -9345,6 +9167,7 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
9345
9167
|
}
|
|
9346
9168
|
async function handleParallelRun(projectRoot, config, issueNumbers, flags, sandboxed) {
|
|
9347
9169
|
const log = getLogger();
|
|
9170
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9348
9171
|
const maxConcurrent = config.agent.maxParallel;
|
|
9349
9172
|
process.stderr.write(`
|
|
9350
9173
|
${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxConcurrent} parallel, worktrees)`)}
|
|
@@ -9396,11 +9219,11 @@ ${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxCon
|
|
|
9396
9219
|
const result = await executeIssue(projectRoot, {
|
|
9397
9220
|
issueNumber,
|
|
9398
9221
|
worktreePath,
|
|
9399
|
-
provider:
|
|
9400
|
-
model:
|
|
9222
|
+
provider: execution.provider,
|
|
9223
|
+
model: execution.model,
|
|
9401
9224
|
dryRun: flags.dryRun,
|
|
9402
9225
|
sandboxed,
|
|
9403
|
-
sandboxName:
|
|
9226
|
+
sandboxName: execution.sandboxName
|
|
9404
9227
|
});
|
|
9405
9228
|
if (result.success) {
|
|
9406
9229
|
markTaskDone(state, issueNumber, result.prNumber);
|
|
@@ -9450,6 +9273,7 @@ ${yellow("⚠")} Failed worktrees preserved for debugging:
|
|
|
9450
9273
|
}
|
|
9451
9274
|
}
|
|
9452
9275
|
async function handleResume(projectRoot, config, sandboxed) {
|
|
9276
|
+
const execution = resolveExecutionContext(config);
|
|
9453
9277
|
const state = loadRunState(projectRoot);
|
|
9454
9278
|
if (!state) {
|
|
9455
9279
|
process.stderr.write(`${red("✗")} No run state found. Nothing to resume.
|
|
@@ -9465,13 +9289,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9465
9289
|
`);
|
|
9466
9290
|
if (state.type === "sprint" && state.branch) {
|
|
9467
9291
|
try {
|
|
9468
|
-
const currentBranch =
|
|
9292
|
+
const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
|
|
9469
9293
|
cwd: projectRoot,
|
|
9470
9294
|
encoding: "utf-8",
|
|
9471
9295
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9472
9296
|
}).trim();
|
|
9473
9297
|
if (currentBranch !== state.branch) {
|
|
9474
|
-
|
|
9298
|
+
execSync14(`git checkout ${state.branch}`, {
|
|
9475
9299
|
cwd: projectRoot,
|
|
9476
9300
|
encoding: "utf-8",
|
|
9477
9301
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9495,11 +9319,11 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9495
9319
|
saveRunState(projectRoot, state);
|
|
9496
9320
|
const result = await executeIssue(projectRoot, {
|
|
9497
9321
|
issueNumber: task.issue,
|
|
9498
|
-
provider:
|
|
9499
|
-
model:
|
|
9322
|
+
provider: execution.provider,
|
|
9323
|
+
model: execution.model,
|
|
9500
9324
|
skipPR: isSprintRun,
|
|
9501
9325
|
sandboxed,
|
|
9502
|
-
sandboxName:
|
|
9326
|
+
sandboxName: execution.sandboxName
|
|
9503
9327
|
});
|
|
9504
9328
|
if (result.success) {
|
|
9505
9329
|
if (isSprintRun) {
|
|
@@ -9538,7 +9362,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
|
|
|
9538
9362
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
9539
9363
|
if (prNumber !== undefined) {
|
|
9540
9364
|
try {
|
|
9541
|
-
|
|
9365
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9542
9366
|
cwd: projectRoot,
|
|
9543
9367
|
encoding: "utf-8",
|
|
9544
9368
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9569,14 +9393,14 @@ function getOrder2(issue) {
|
|
|
9569
9393
|
}
|
|
9570
9394
|
function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
9571
9395
|
try {
|
|
9572
|
-
const status =
|
|
9396
|
+
const status = execSync14("git status --porcelain", {
|
|
9573
9397
|
cwd: projectRoot,
|
|
9574
9398
|
encoding: "utf-8",
|
|
9575
9399
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9576
9400
|
}).trim();
|
|
9577
9401
|
if (!status)
|
|
9578
9402
|
return;
|
|
9579
|
-
|
|
9403
|
+
execSync14("git add -A", {
|
|
9580
9404
|
cwd: projectRoot,
|
|
9581
9405
|
encoding: "utf-8",
|
|
9582
9406
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9584,7 +9408,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
9584
9408
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
9585
9409
|
|
|
9586
9410
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
9587
|
-
|
|
9411
|
+
execSync14(`git commit -F -`, {
|
|
9588
9412
|
input: message,
|
|
9589
9413
|
cwd: projectRoot,
|
|
9590
9414
|
encoding: "utf-8",
|
|
@@ -9598,7 +9422,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9598
9422
|
if (!config.agent.autoPR)
|
|
9599
9423
|
return;
|
|
9600
9424
|
try {
|
|
9601
|
-
const diff =
|
|
9425
|
+
const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
9602
9426
|
cwd: projectRoot,
|
|
9603
9427
|
encoding: "utf-8",
|
|
9604
9428
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9608,7 +9432,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9608
9432
|
`);
|
|
9609
9433
|
return;
|
|
9610
9434
|
}
|
|
9611
|
-
|
|
9435
|
+
execSync14(`git push -u origin ${branchName}`, {
|
|
9612
9436
|
cwd: projectRoot,
|
|
9613
9437
|
encoding: "utf-8",
|
|
9614
9438
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9634,6 +9458,7 @@ ${taskLines}
|
|
|
9634
9458
|
}
|
|
9635
9459
|
}
|
|
9636
9460
|
var init_run = __esm(() => {
|
|
9461
|
+
init_ai_models();
|
|
9637
9462
|
init_agent();
|
|
9638
9463
|
init_config();
|
|
9639
9464
|
init_conflict();
|
|
@@ -10010,7 +9835,7 @@ ${bold("Planning:")} ${cyan(displayDirective)}
|
|
|
10010
9835
|
cwd: projectRoot,
|
|
10011
9836
|
activity: "planning",
|
|
10012
9837
|
sandboxed: config.sandbox.enabled,
|
|
10013
|
-
sandboxName: config.sandbox.
|
|
9838
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10014
9839
|
});
|
|
10015
9840
|
if (aiResult.interrupted) {
|
|
10016
9841
|
process.stderr.write(`
|
|
@@ -10102,16 +9927,21 @@ ${i.body?.slice(0, 300) ?? ""}`).join(`
|
|
|
10102
9927
|
|
|
10103
9928
|
`);
|
|
10104
9929
|
const { runAI: runAI2 } = await Promise.resolve().then(() => (init_run_ai(), exports_run_ai));
|
|
10105
|
-
const prompt =
|
|
9930
|
+
const prompt = `<role>
|
|
9931
|
+
You are organizing GitHub issues for a sprint. Analyze these issues and suggest the optimal execution order.
|
|
9932
|
+
</role>
|
|
10106
9933
|
|
|
10107
|
-
|
|
9934
|
+
<issues>
|
|
10108
9935
|
${issueDescriptions}
|
|
9936
|
+
</issues>
|
|
10109
9937
|
|
|
9938
|
+
<instructions>
|
|
10110
9939
|
For each issue, output a line in this format:
|
|
10111
9940
|
ORDER: #<number> <reason for this position>
|
|
10112
9941
|
|
|
10113
9942
|
Order them so that dependencies are respected (issues that produce code needed by later issues should come first).
|
|
10114
|
-
Start with foundational/setup tasks, then core features, then integration/testing
|
|
9943
|
+
Start with foundational/setup tasks, then core features, then integration/testing.
|
|
9944
|
+
</instructions>`;
|
|
10115
9945
|
const aiResult = await runAI2({
|
|
10116
9946
|
prompt,
|
|
10117
9947
|
provider: config.ai.provider,
|
|
@@ -10120,7 +9950,7 @@ Start with foundational/setup tasks, then core features, then integration/testin
|
|
|
10120
9950
|
activity: "issue ordering",
|
|
10121
9951
|
silent: true,
|
|
10122
9952
|
sandboxed: config.sandbox.enabled,
|
|
10123
|
-
sandboxName: config.sandbox.
|
|
9953
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10124
9954
|
});
|
|
10125
9955
|
if (aiResult.interrupted) {
|
|
10126
9956
|
process.stderr.write(`
|
|
@@ -10184,59 +10014,62 @@ ${bold("Suggested Order:")}
|
|
|
10184
10014
|
}
|
|
10185
10015
|
function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative) {
|
|
10186
10016
|
const parts = [];
|
|
10187
|
-
parts.push(
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10017
|
+
parts.push(`<role>
|
|
10018
|
+
You are a sprint planning assistant for the GitHub repository ${config.github.owner}/${config.github.repo}.
|
|
10019
|
+
</role>`);
|
|
10020
|
+
parts.push(`<directive>
|
|
10021
|
+
${directive}${sprintName ? `
|
|
10022
|
+
|
|
10023
|
+
**Sprint:** ${sprintName}` : ""}
|
|
10024
|
+
</directive>`);
|
|
10025
|
+
const locusPath = join18(projectRoot, ".locus", "LOCUS.md");
|
|
10195
10026
|
if (existsSync18(locusPath)) {
|
|
10196
10027
|
const content = readFileSync13(locusPath, "utf-8");
|
|
10197
|
-
parts.push(
|
|
10198
|
-
|
|
10199
|
-
|
|
10028
|
+
parts.push(`<project-context>
|
|
10029
|
+
${content.slice(0, 3000)}
|
|
10030
|
+
</project-context>`);
|
|
10200
10031
|
}
|
|
10201
10032
|
const learningsPath = join18(projectRoot, ".locus", "LEARNINGS.md");
|
|
10202
10033
|
if (existsSync18(learningsPath)) {
|
|
10203
10034
|
const content = readFileSync13(learningsPath, "utf-8");
|
|
10204
|
-
parts.push(
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
}
|
|
10208
|
-
parts.push(
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
parts.push(
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10035
|
+
parts.push(`<past-learnings>
|
|
10036
|
+
${content.slice(0, 2000)}
|
|
10037
|
+
</past-learnings>`);
|
|
10038
|
+
}
|
|
10039
|
+
parts.push(`<task>
|
|
10040
|
+
Break down the directive into specific, actionable GitHub issues and write them to the file: ${planPathRelative}
|
|
10041
|
+
|
|
10042
|
+
Write ONLY a valid JSON file to ${planPathRelative} with this exact structure:
|
|
10043
|
+
|
|
10044
|
+
\`\`\`json
|
|
10045
|
+
{
|
|
10046
|
+
"id": "${id}",
|
|
10047
|
+
"directive": ${JSON.stringify(directive)},
|
|
10048
|
+
"sprint": ${sprintName ? JSON.stringify(sprintName) : "null"},
|
|
10049
|
+
"createdAt": "${new Date().toISOString()}",
|
|
10050
|
+
"issues": [
|
|
10051
|
+
{
|
|
10052
|
+
"order": 1,
|
|
10053
|
+
"title": "concise issue title",
|
|
10054
|
+
"body": "detailed markdown body with acceptance criteria",
|
|
10055
|
+
"priority": "critical|high|medium|low",
|
|
10056
|
+
"type": "feature|bug|chore|refactor|docs",
|
|
10057
|
+
"dependsOn": "none or comma-separated order numbers"
|
|
10058
|
+
}
|
|
10059
|
+
]
|
|
10060
|
+
}
|
|
10061
|
+
\`\`\`
|
|
10062
|
+
</task>`);
|
|
10063
|
+
parts.push(`<requirements>
|
|
10064
|
+
- Break the directive into 3-10 specific, actionable issues
|
|
10065
|
+
- Each issue must be independently executable by an AI agent
|
|
10066
|
+
- Order them so dependencies are respected (foundational tasks first)
|
|
10067
|
+
- Write detailed issue bodies with clear acceptance criteria
|
|
10068
|
+
- Use valid GitHub Markdown only in issue bodies
|
|
10069
|
+
- Create the file using the Write tool — do not print the JSON to the terminal
|
|
10070
|
+
</requirements>`);
|
|
10239
10071
|
return parts.join(`
|
|
10072
|
+
|
|
10240
10073
|
`);
|
|
10241
10074
|
}
|
|
10242
10075
|
function sanitizePlanOutput(output) {
|
|
@@ -10369,6 +10202,7 @@ var init_plan = __esm(() => {
|
|
|
10369
10202
|
init_config();
|
|
10370
10203
|
init_github();
|
|
10371
10204
|
init_terminal();
|
|
10205
|
+
init_sandbox();
|
|
10372
10206
|
});
|
|
10373
10207
|
|
|
10374
10208
|
// src/commands/review.ts
|
|
@@ -10376,7 +10210,7 @@ var exports_review = {};
|
|
|
10376
10210
|
__export(exports_review, {
|
|
10377
10211
|
reviewCommand: () => reviewCommand
|
|
10378
10212
|
});
|
|
10379
|
-
import { execSync as
|
|
10213
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
10380
10214
|
import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
|
|
10381
10215
|
import { join as join19 } from "node:path";
|
|
10382
10216
|
function printHelp2() {
|
|
@@ -10454,7 +10288,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
|
|
|
10454
10288
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
10455
10289
|
let prInfo;
|
|
10456
10290
|
try {
|
|
10457
|
-
const result =
|
|
10291
|
+
const result = execSync15(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10458
10292
|
const raw = JSON.parse(result);
|
|
10459
10293
|
prInfo = {
|
|
10460
10294
|
number: raw.number,
|
|
@@ -10499,7 +10333,7 @@ async function reviewPR(projectRoot, config, pr, focus, flags) {
|
|
|
10499
10333
|
cwd: projectRoot,
|
|
10500
10334
|
activity: `PR #${pr.number}`,
|
|
10501
10335
|
sandboxed: config.sandbox.enabled,
|
|
10502
|
-
sandboxName: config.sandbox.
|
|
10336
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10503
10337
|
});
|
|
10504
10338
|
if (aiResult.interrupted) {
|
|
10505
10339
|
process.stderr.write(` ${yellow("⚡")} Review interrupted.
|
|
@@ -10520,7 +10354,7 @@ ${output.slice(0, 60000)}
|
|
|
10520
10354
|
|
|
10521
10355
|
---
|
|
10522
10356
|
_Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
|
|
10523
|
-
|
|
10357
|
+
execSync15(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10524
10358
|
process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
|
|
10525
10359
|
`);
|
|
10526
10360
|
} catch (e) {
|
|
@@ -10535,53 +10369,62 @@ _Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_
|
|
|
10535
10369
|
}
|
|
10536
10370
|
function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
10537
10371
|
const parts = [];
|
|
10538
|
-
parts.push(
|
|
10539
|
-
|
|
10540
|
-
|
|
10372
|
+
parts.push(`<role>
|
|
10373
|
+
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
10374
|
+
</role>`);
|
|
10375
|
+
const locusPath = join19(projectRoot, ".locus", "LOCUS.md");
|
|
10541
10376
|
if (existsSync19(locusPath)) {
|
|
10542
10377
|
const content = readFileSync14(locusPath, "utf-8");
|
|
10543
|
-
parts.push(
|
|
10544
|
-
|
|
10545
|
-
|
|
10378
|
+
parts.push(`<project-context>
|
|
10379
|
+
${content.slice(0, 2000)}
|
|
10380
|
+
</project-context>`);
|
|
10546
10381
|
}
|
|
10547
|
-
|
|
10548
|
-
parts.push(`Branch: ${pr.head} → ${pr.base}`);
|
|
10382
|
+
const prMeta = [`Branch: ${pr.head} → ${pr.base}`];
|
|
10549
10383
|
if (pr.body) {
|
|
10550
|
-
|
|
10384
|
+
prMeta.push(`Description:
|
|
10551
10385
|
${pr.body.slice(0, 1000)}`);
|
|
10552
10386
|
}
|
|
10553
|
-
parts.push(""
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
parts.push(
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10387
|
+
parts.push(`<pull-request number="${pr.number}" title="${pr.title}">
|
|
10388
|
+
${prMeta.join(`
|
|
10389
|
+
`)}
|
|
10390
|
+
</pull-request>`);
|
|
10391
|
+
parts.push(`<diff>
|
|
10392
|
+
${diff.slice(0, 50000)}
|
|
10393
|
+
</diff>`);
|
|
10394
|
+
let instructions = `Provide a thorough code review. For each issue found, describe:
|
|
10395
|
+
1. The file and approximate location
|
|
10396
|
+
2. What the issue is
|
|
10397
|
+
3. Why it matters
|
|
10398
|
+
4. How to fix it
|
|
10399
|
+
|
|
10400
|
+
Categories to check:
|
|
10401
|
+
- Correctness: bugs, logic errors, edge cases
|
|
10402
|
+
- Security: injection, XSS, auth issues, secret exposure
|
|
10403
|
+
- Performance: N+1 queries, unnecessary allocations, missing caching
|
|
10404
|
+
- Maintainability: naming, complexity, code organization
|
|
10405
|
+
- Testing: missing tests, inadequate coverage`;
|
|
10570
10406
|
if (focus) {
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10407
|
+
instructions += `
|
|
10408
|
+
|
|
10409
|
+
**Focus areas:** ${focus}
|
|
10410
|
+
Pay special attention to the above areas.`;
|
|
10574
10411
|
}
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10412
|
+
instructions += `
|
|
10413
|
+
|
|
10414
|
+
End with an overall assessment: APPROVE, REQUEST_CHANGES, or COMMENT.
|
|
10415
|
+
Be constructive and specific. Praise good patterns too.`;
|
|
10416
|
+
parts.push(`<review-instructions>
|
|
10417
|
+
${instructions}
|
|
10418
|
+
</review-instructions>`);
|
|
10578
10419
|
return parts.join(`
|
|
10420
|
+
|
|
10579
10421
|
`);
|
|
10580
10422
|
}
|
|
10581
10423
|
var init_review = __esm(() => {
|
|
10582
10424
|
init_run_ai();
|
|
10583
10425
|
init_config();
|
|
10584
10426
|
init_github();
|
|
10427
|
+
init_sandbox();
|
|
10585
10428
|
init_progress();
|
|
10586
10429
|
init_terminal();
|
|
10587
10430
|
});
|
|
@@ -10591,7 +10434,7 @@ var exports_iterate = {};
|
|
|
10591
10434
|
__export(exports_iterate, {
|
|
10592
10435
|
iterateCommand: () => iterateCommand
|
|
10593
10436
|
});
|
|
10594
|
-
import { execSync as
|
|
10437
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
10595
10438
|
function printHelp3() {
|
|
10596
10439
|
process.stderr.write(`
|
|
10597
10440
|
${bold("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -10801,12 +10644,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
|
|
|
10801
10644
|
}
|
|
10802
10645
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
10803
10646
|
try {
|
|
10804
|
-
const result =
|
|
10647
|
+
const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10805
10648
|
const parsed = JSON.parse(result);
|
|
10806
10649
|
if (parsed.length > 0) {
|
|
10807
10650
|
return parsed[0].number;
|
|
10808
10651
|
}
|
|
10809
|
-
const branchResult =
|
|
10652
|
+
const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10810
10653
|
const branchParsed = JSON.parse(branchResult);
|
|
10811
10654
|
if (branchParsed.length > 0) {
|
|
10812
10655
|
return branchParsed[0].number;
|
|
@@ -11051,7 +10894,7 @@ ${bold("Discussion:")} ${cyan(topic)}
|
|
|
11051
10894
|
cwd: projectRoot,
|
|
11052
10895
|
activity: "discussion",
|
|
11053
10896
|
sandboxed: config.sandbox.enabled,
|
|
11054
|
-
sandboxName: config.sandbox.
|
|
10897
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
11055
10898
|
});
|
|
11056
10899
|
if (aiResult.interrupted) {
|
|
11057
10900
|
process.stderr.write(`
|
|
@@ -11130,61 +10973,74 @@ ${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
|
|
|
11130
10973
|
}
|
|
11131
10974
|
function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
|
|
11132
10975
|
const parts = [];
|
|
11133
|
-
parts.push(
|
|
11134
|
-
|
|
11135
|
-
|
|
10976
|
+
parts.push(`<role>
|
|
10977
|
+
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
10978
|
+
</role>`);
|
|
10979
|
+
const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
|
|
11136
10980
|
if (existsSync20(locusPath)) {
|
|
11137
10981
|
const content = readFileSync15(locusPath, "utf-8");
|
|
11138
|
-
parts.push(
|
|
11139
|
-
|
|
11140
|
-
|
|
10982
|
+
parts.push(`<project-context>
|
|
10983
|
+
${content.slice(0, 3000)}
|
|
10984
|
+
</project-context>`);
|
|
11141
10985
|
}
|
|
11142
10986
|
const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
|
|
11143
10987
|
if (existsSync20(learningsPath)) {
|
|
11144
10988
|
const content = readFileSync15(learningsPath, "utf-8");
|
|
11145
|
-
parts.push(
|
|
11146
|
-
|
|
11147
|
-
|
|
10989
|
+
parts.push(`<past-learnings>
|
|
10990
|
+
${content.slice(0, 2000)}
|
|
10991
|
+
</past-learnings>`);
|
|
11148
10992
|
}
|
|
11149
|
-
parts.push(
|
|
11150
|
-
|
|
10993
|
+
parts.push(`<discussion-topic>
|
|
10994
|
+
${topic}
|
|
10995
|
+
</discussion-topic>`);
|
|
11151
10996
|
if (conversation.length === 0) {
|
|
11152
|
-
parts.push(
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
10997
|
+
parts.push(`<instructions>
|
|
10998
|
+
Before providing recommendations, you need to ask targeted clarifying questions.
|
|
10999
|
+
|
|
11000
|
+
Ask 3-5 focused questions that will significantly improve the quality of your analysis.
|
|
11001
|
+
Format as a numbered list. Be specific and focused on the most important unknowns.
|
|
11002
|
+
Do NOT provide any analysis yet — questions only.
|
|
11003
|
+
</instructions>`);
|
|
11157
11004
|
} else {
|
|
11158
|
-
|
|
11159
|
-
parts.push("");
|
|
11005
|
+
const historyLines = [];
|
|
11160
11006
|
for (const turn of conversation) {
|
|
11161
11007
|
if (turn.role === "user") {
|
|
11162
|
-
|
|
11008
|
+
historyLines.push(`USER: ${turn.content}`);
|
|
11163
11009
|
} else {
|
|
11164
|
-
|
|
11010
|
+
historyLines.push(`ASSISTANT: ${turn.content}`);
|
|
11165
11011
|
}
|
|
11166
|
-
parts.push("");
|
|
11167
11012
|
}
|
|
11013
|
+
parts.push(`<conversation-history>
|
|
11014
|
+
${historyLines.join(`
|
|
11015
|
+
|
|
11016
|
+
`)}
|
|
11017
|
+
</conversation-history>`);
|
|
11168
11018
|
if (forceFinal) {
|
|
11169
|
-
parts.push(
|
|
11170
|
-
|
|
11019
|
+
parts.push(`<instructions>
|
|
11020
|
+
Based on everything discussed, provide your complete analysis and recommendations now.
|
|
11021
|
+
Format as a thorough markdown document with a clear title (# Heading), sections, trade-offs, and actionable recommendations.
|
|
11022
|
+
</instructions>`);
|
|
11171
11023
|
} else {
|
|
11172
|
-
parts.push(
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11024
|
+
parts.push(`<instructions>
|
|
11025
|
+
Review the information gathered so far.
|
|
11026
|
+
|
|
11027
|
+
If you have enough information to make a thorough recommendation:
|
|
11028
|
+
→ Provide a complete analysis as a markdown document with a title (# Heading), sections, trade-offs, and concrete recommendations.
|
|
11029
|
+
|
|
11030
|
+
If you still need key information to give a good answer:
|
|
11031
|
+
→ Ask 2-3 more focused follow-up questions (numbered list only, no analysis yet).
|
|
11032
|
+
</instructions>`);
|
|
11179
11033
|
}
|
|
11180
11034
|
}
|
|
11181
11035
|
return parts.join(`
|
|
11036
|
+
|
|
11182
11037
|
`);
|
|
11183
11038
|
}
|
|
11184
11039
|
var MAX_DISCUSSION_ROUNDS = 5;
|
|
11185
11040
|
var init_discuss = __esm(() => {
|
|
11186
11041
|
init_run_ai();
|
|
11187
11042
|
init_config();
|
|
11043
|
+
init_sandbox();
|
|
11188
11044
|
init_progress();
|
|
11189
11045
|
init_terminal();
|
|
11190
11046
|
init_input_handler();
|
|
@@ -11409,22 +11265,25 @@ var exports_sandbox2 = {};
|
|
|
11409
11265
|
__export(exports_sandbox2, {
|
|
11410
11266
|
sandboxCommand: () => sandboxCommand
|
|
11411
11267
|
});
|
|
11412
|
-
import { execSync as
|
|
11268
|
+
import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
|
|
11269
|
+
import { createHash } from "node:crypto";
|
|
11270
|
+
import { basename as basename4 } from "node:path";
|
|
11413
11271
|
function printSandboxHelp() {
|
|
11414
11272
|
process.stderr.write(`
|
|
11415
11273
|
${bold("locus sandbox")} — Manage Docker sandbox lifecycle
|
|
11416
11274
|
|
|
11417
11275
|
${bold("Usage:")}
|
|
11418
|
-
locus sandbox ${dim("# Create
|
|
11276
|
+
locus sandbox ${dim("# Create claude/codex sandboxes and enable sandbox mode")}
|
|
11419
11277
|
locus sandbox claude ${dim("# Run claude interactively (for login)")}
|
|
11420
11278
|
locus sandbox codex ${dim("# Run codex interactively (for login)")}
|
|
11421
|
-
locus sandbox rm ${dim("# Destroy
|
|
11279
|
+
locus sandbox rm ${dim("# Destroy all provider sandboxes and disable sandbox mode")}
|
|
11422
11280
|
locus sandbox status ${dim("# Show current sandbox state")}
|
|
11423
11281
|
|
|
11424
11282
|
${bold("Flow:")}
|
|
11425
|
-
1. ${cyan("locus sandbox")} Create
|
|
11426
|
-
2. ${cyan("locus sandbox claude")} Login
|
|
11427
|
-
3. ${cyan("locus
|
|
11283
|
+
1. ${cyan("locus sandbox")} Create provider sandboxes
|
|
11284
|
+
2. ${cyan("locus sandbox claude")} Login Claude inside its sandbox
|
|
11285
|
+
3. ${cyan("locus sandbox codex")} Login Codex inside its sandbox
|
|
11286
|
+
4. ${cyan("locus exec")}/${cyan("locus run")} Commands resync + execute in provider sandbox
|
|
11428
11287
|
|
|
11429
11288
|
`);
|
|
11430
11289
|
}
|
|
@@ -11452,18 +11311,6 @@ async function sandboxCommand(projectRoot, args) {
|
|
|
11452
11311
|
}
|
|
11453
11312
|
async function handleCreate(projectRoot) {
|
|
11454
11313
|
const config = loadConfig(projectRoot);
|
|
11455
|
-
if (config.sandbox.name) {
|
|
11456
|
-
const alive = isSandboxAlive(config.sandbox.name);
|
|
11457
|
-
if (alive) {
|
|
11458
|
-
process.stderr.write(`${green("✓")} Sandbox already exists: ${bold(config.sandbox.name)}
|
|
11459
|
-
`);
|
|
11460
|
-
process.stderr.write(` Run ${cyan("locus sandbox claude")} or ${cyan("locus sandbox codex")} to login.
|
|
11461
|
-
`);
|
|
11462
|
-
return;
|
|
11463
|
-
}
|
|
11464
|
-
process.stderr.write(`${yellow("⚠")} Previous sandbox ${dim(config.sandbox.name)} is no longer running. Creating a new one.
|
|
11465
|
-
`);
|
|
11466
|
-
}
|
|
11467
11314
|
const status = await detectSandboxSupport();
|
|
11468
11315
|
if (!status.available) {
|
|
11469
11316
|
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
@@ -11472,86 +11319,68 @@ async function handleCreate(projectRoot) {
|
|
|
11472
11319
|
`);
|
|
11473
11320
|
return;
|
|
11474
11321
|
}
|
|
11475
|
-
const
|
|
11476
|
-
const
|
|
11322
|
+
const sandboxNames = buildProviderSandboxNames(projectRoot);
|
|
11323
|
+
const readySandboxes = {};
|
|
11324
|
+
let failed = false;
|
|
11325
|
+
for (const provider of PROVIDERS) {
|
|
11326
|
+
const name = sandboxNames[provider];
|
|
11327
|
+
if (isSandboxAlive(name)) {
|
|
11328
|
+
process.stderr.write(`${green("✓")} ${provider} sandbox ready: ${bold(name)}
|
|
11329
|
+
`);
|
|
11330
|
+
readySandboxes[provider] = name;
|
|
11331
|
+
continue;
|
|
11332
|
+
}
|
|
11333
|
+
process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
|
|
11334
|
+
`);
|
|
11335
|
+
const created = await createProviderSandbox(provider, name, projectRoot);
|
|
11336
|
+
if (!created) {
|
|
11337
|
+
process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
|
|
11338
|
+
`);
|
|
11339
|
+
failed = true;
|
|
11340
|
+
continue;
|
|
11341
|
+
}
|
|
11342
|
+
process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
|
|
11343
|
+
`);
|
|
11344
|
+
readySandboxes[provider] = name;
|
|
11345
|
+
}
|
|
11477
11346
|
config.sandbox.enabled = true;
|
|
11478
|
-
config.sandbox.
|
|
11347
|
+
config.sandbox.providers = readySandboxes;
|
|
11479
11348
|
saveConfig(projectRoot, config);
|
|
11480
|
-
|
|
11349
|
+
if (failed) {
|
|
11350
|
+
process.stderr.write(`
|
|
11351
|
+
${yellow("⚠")} Some sandboxes failed to create. Re-run ${cyan("locus sandbox")} after resolving Docker issues.
|
|
11481
11352
|
`);
|
|
11482
|
-
|
|
11353
|
+
}
|
|
11354
|
+
process.stderr.write(`
|
|
11355
|
+
${green("✓")} Sandbox mode enabled with provider-specific sandboxes.
|
|
11356
|
+
`);
|
|
11357
|
+
process.stderr.write(` Next: run ${cyan("locus sandbox claude")} and ${cyan("locus sandbox codex")} to authenticate both providers.
|
|
11483
11358
|
`);
|
|
11484
11359
|
}
|
|
11485
11360
|
async function handleAgentLogin(projectRoot, agent) {
|
|
11486
11361
|
const config = loadConfig(projectRoot);
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
11491
|
-
`);
|
|
11492
|
-
process.stderr.write(` Install Docker Desktop 4.58+ with sandbox support.
|
|
11362
|
+
const sandboxName = getProviderSandboxName(config.sandbox, agent);
|
|
11363
|
+
if (!sandboxName) {
|
|
11364
|
+
process.stderr.write(`${red("✗")} No ${agent} sandbox configured. Run ${cyan("locus sandbox")} first.
|
|
11493
11365
|
`);
|
|
11494
|
-
|
|
11495
|
-
}
|
|
11496
|
-
const segment = projectRoot.split("/").pop() ?? "sandbox";
|
|
11497
|
-
config.sandbox.name = `locus-${segment}-${Date.now()}`;
|
|
11498
|
-
config.sandbox.enabled = true;
|
|
11499
|
-
saveConfig(projectRoot, config);
|
|
11366
|
+
return;
|
|
11500
11367
|
}
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
let dockerArgs;
|
|
11504
|
-
if (alive) {
|
|
11505
|
-
if (agent === "codex") {
|
|
11506
|
-
await ensureCodexInSandbox(sandboxName);
|
|
11507
|
-
}
|
|
11508
|
-
process.stderr.write(`Connecting to sandbox ${dim(sandboxName)}...
|
|
11509
|
-
`);
|
|
11510
|
-
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11511
|
-
|
|
11512
|
-
`);
|
|
11513
|
-
dockerArgs = [
|
|
11514
|
-
"sandbox",
|
|
11515
|
-
"exec",
|
|
11516
|
-
"-it",
|
|
11517
|
-
"-w",
|
|
11518
|
-
projectRoot,
|
|
11519
|
-
sandboxName,
|
|
11520
|
-
agent
|
|
11521
|
-
];
|
|
11522
|
-
} else if (agent === "codex") {
|
|
11523
|
-
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11368
|
+
if (!isSandboxAlive(sandboxName)) {
|
|
11369
|
+
process.stderr.write(`${red("✗")} ${agent} sandbox is not running: ${dim(sandboxName)}
|
|
11524
11370
|
`);
|
|
11525
|
-
|
|
11526
|
-
execSync19(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
11527
|
-
} catch {}
|
|
11528
|
-
if (!isSandboxAlive(sandboxName)) {
|
|
11529
|
-
process.stderr.write(`${red("✗")} Failed to create sandbox.
|
|
11371
|
+
process.stderr.write(` Recreate it with ${cyan("locus sandbox")}.
|
|
11530
11372
|
`);
|
|
11531
|
-
|
|
11532
|
-
|
|
11373
|
+
return;
|
|
11374
|
+
}
|
|
11375
|
+
if (agent === "codex") {
|
|
11533
11376
|
await ensureCodexInSandbox(sandboxName);
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
`);
|
|
11537
|
-
dockerArgs = [
|
|
11538
|
-
"sandbox",
|
|
11539
|
-
"exec",
|
|
11540
|
-
"-it",
|
|
11541
|
-
"-w",
|
|
11542
|
-
projectRoot,
|
|
11543
|
-
sandboxName,
|
|
11544
|
-
"codex"
|
|
11545
|
-
];
|
|
11546
|
-
} else {
|
|
11547
|
-
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11377
|
+
}
|
|
11378
|
+
process.stderr.write(`Connecting to ${agent} sandbox ${dim(sandboxName)}...
|
|
11548
11379
|
`);
|
|
11549
|
-
|
|
11380
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11550
11381
|
|
|
11551
11382
|
`);
|
|
11552
|
-
|
|
11553
|
-
}
|
|
11554
|
-
const child = spawn6("docker", dockerArgs, {
|
|
11383
|
+
const child = spawn6("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
|
|
11555
11384
|
stdio: "inherit"
|
|
11556
11385
|
});
|
|
11557
11386
|
await new Promise((resolve2) => {
|
|
@@ -11577,25 +11406,30 @@ ${yellow("⚠")} ${agent} exited with code ${code}.
|
|
|
11577
11406
|
}
|
|
11578
11407
|
function handleRemove(projectRoot) {
|
|
11579
11408
|
const config = loadConfig(projectRoot);
|
|
11580
|
-
|
|
11581
|
-
|
|
11409
|
+
const names = Array.from(new Set(Object.values(config.sandbox.providers).filter((value) => typeof value === "string" && value.length > 0)));
|
|
11410
|
+
if (names.length === 0) {
|
|
11411
|
+
config.sandbox.enabled = false;
|
|
11412
|
+
config.sandbox.providers = {};
|
|
11413
|
+
saveConfig(projectRoot, config);
|
|
11414
|
+
process.stderr.write(`${dim("No sandboxes to remove. Sandbox mode disabled.")}
|
|
11582
11415
|
`);
|
|
11583
11416
|
return;
|
|
11584
11417
|
}
|
|
11585
|
-
const sandboxName
|
|
11586
|
-
|
|
11418
|
+
for (const sandboxName of names) {
|
|
11419
|
+
process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
|
|
11587
11420
|
`);
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11421
|
+
try {
|
|
11422
|
+
execSync17(`docker sandbox rm ${sandboxName}`, {
|
|
11423
|
+
encoding: "utf-8",
|
|
11424
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11425
|
+
timeout: 15000
|
|
11426
|
+
});
|
|
11427
|
+
} catch {}
|
|
11428
|
+
}
|
|
11429
|
+
config.sandbox.providers = {};
|
|
11596
11430
|
config.sandbox.enabled = false;
|
|
11597
11431
|
saveConfig(projectRoot, config);
|
|
11598
|
-
process.stderr.write(`${green("✓")}
|
|
11432
|
+
process.stderr.write(`${green("✓")} Provider sandboxes removed. Sandbox mode disabled.
|
|
11599
11433
|
`);
|
|
11600
11434
|
}
|
|
11601
11435
|
function handleStatus(projectRoot) {
|
|
@@ -11606,24 +11440,55 @@ ${bold("Sandbox Status")}
|
|
|
11606
11440
|
`);
|
|
11607
11441
|
process.stderr.write(` ${dim("Enabled:")} ${config.sandbox.enabled ? green("yes") : red("no")}
|
|
11608
11442
|
`);
|
|
11609
|
-
|
|
11443
|
+
for (const provider of PROVIDERS) {
|
|
11444
|
+
const name = config.sandbox.providers[provider];
|
|
11445
|
+
process.stderr.write(` ${dim(`${provider}:`).padEnd(15)}${name ? bold(name) : dim("(not configured)")}
|
|
11610
11446
|
`);
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
`);
|
|
11615
|
-
if (!alive) {
|
|
11616
|
-
process.stderr.write(`
|
|
11617
|
-
${yellow("⚠")} Sandbox is not running. Run ${bold("locus sandbox")} to create a new one.
|
|
11447
|
+
if (name) {
|
|
11448
|
+
const alive = isSandboxAlive(name);
|
|
11449
|
+
process.stderr.write(` ${dim(`${provider} running:`).padEnd(15)}${alive ? green("yes") : red("no")}
|
|
11618
11450
|
`);
|
|
11619
11451
|
}
|
|
11620
11452
|
}
|
|
11453
|
+
if (!config.sandbox.providers.claude || !config.sandbox.providers.codex) {
|
|
11454
|
+
process.stderr.write(`
|
|
11455
|
+
${yellow("⚠")} Provider sandboxes are incomplete. Run ${bold("locus sandbox")}.
|
|
11456
|
+
`);
|
|
11457
|
+
}
|
|
11621
11458
|
process.stderr.write(`
|
|
11622
11459
|
`);
|
|
11623
11460
|
}
|
|
11461
|
+
function buildProviderSandboxNames(projectRoot) {
|
|
11462
|
+
const segment = sanitizeSegment(basename4(projectRoot));
|
|
11463
|
+
const hash = createHash("sha1").update(projectRoot).digest("hex").slice(0, 8);
|
|
11464
|
+
return {
|
|
11465
|
+
claude: `locus-${segment}-claude-${hash}`,
|
|
11466
|
+
codex: `locus-${segment}-codex-${hash}`
|
|
11467
|
+
};
|
|
11468
|
+
}
|
|
11469
|
+
function sanitizeSegment(input) {
|
|
11470
|
+
const cleaned = input.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
11471
|
+
return cleaned || "workspace";
|
|
11472
|
+
}
|
|
11473
|
+
async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
11474
|
+
try {
|
|
11475
|
+
execSync17(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, {
|
|
11476
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11477
|
+
timeout: 120000
|
|
11478
|
+
});
|
|
11479
|
+
} catch {}
|
|
11480
|
+
if (!isSandboxAlive(sandboxName)) {
|
|
11481
|
+
return false;
|
|
11482
|
+
}
|
|
11483
|
+
if (provider === "codex") {
|
|
11484
|
+
await ensureCodexInSandbox(sandboxName);
|
|
11485
|
+
}
|
|
11486
|
+
await enforceSandboxIgnore(sandboxName, projectRoot);
|
|
11487
|
+
return true;
|
|
11488
|
+
}
|
|
11624
11489
|
async function ensureCodexInSandbox(sandboxName) {
|
|
11625
11490
|
try {
|
|
11626
|
-
|
|
11491
|
+
execSync17(`docker sandbox exec ${sandboxName} which codex`, {
|
|
11627
11492
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11628
11493
|
timeout: 5000
|
|
11629
11494
|
});
|
|
@@ -11631,7 +11496,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11631
11496
|
process.stderr.write(`Installing codex in sandbox...
|
|
11632
11497
|
`);
|
|
11633
11498
|
try {
|
|
11634
|
-
|
|
11499
|
+
execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
11635
11500
|
} catch {
|
|
11636
11501
|
process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
|
|
11637
11502
|
`);
|
|
@@ -11640,7 +11505,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11640
11505
|
}
|
|
11641
11506
|
function isSandboxAlive(name) {
|
|
11642
11507
|
try {
|
|
11643
|
-
const output =
|
|
11508
|
+
const output = execSync17("docker sandbox ls", {
|
|
11644
11509
|
encoding: "utf-8",
|
|
11645
11510
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11646
11511
|
timeout: 5000
|
|
@@ -11650,11 +11515,13 @@ function isSandboxAlive(name) {
|
|
|
11650
11515
|
return false;
|
|
11651
11516
|
}
|
|
11652
11517
|
}
|
|
11518
|
+
var PROVIDERS;
|
|
11653
11519
|
var init_sandbox2 = __esm(() => {
|
|
11654
11520
|
init_config();
|
|
11655
11521
|
init_sandbox();
|
|
11656
11522
|
init_sandbox_ignore();
|
|
11657
11523
|
init_terminal();
|
|
11524
|
+
PROVIDERS = ["claude", "codex"];
|
|
11658
11525
|
});
|
|
11659
11526
|
|
|
11660
11527
|
// src/cli.ts
|