@locusai/cli 0.18.2 → 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 +1017 -1234
- 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
|
|
|
@@ -1816,14 +1797,13 @@ ${bold("Sandbox mode")} ${dim("(recommended)")}
|
|
|
1816
1797
|
process.stderr.write(` Run AI agents in an isolated Docker sandbox for safety.
|
|
1817
1798
|
|
|
1818
1799
|
`);
|
|
1819
|
-
process.stderr.write(` ${gray("1.")} ${cyan("locus sandbox")} ${dim("Create
|
|
1800
|
+
process.stderr.write(` ${gray("1.")} ${cyan("locus sandbox")} ${dim("Create claude/codex sandboxes")}
|
|
1820
1801
|
`);
|
|
1821
1802
|
process.stderr.write(` ${gray("2.")} ${cyan("locus sandbox claude")} ${dim("Login to Claude inside the sandbox")}
|
|
1822
1803
|
`);
|
|
1823
|
-
process.stderr.write(` ${gray("3.")} ${cyan("locus
|
|
1804
|
+
process.stderr.write(` ${gray("3.")} ${cyan("locus sandbox codex")} ${dim("Login to Codex inside the sandbox")}
|
|
1824
1805
|
`);
|
|
1825
|
-
process.stderr.write(`
|
|
1826
|
-
${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")}
|
|
1827
1807
|
`);
|
|
1828
1808
|
process.stderr.write(` ${dim("Learn more:")} ${cyan("locus sandbox help")}
|
|
1829
1809
|
`);
|
|
@@ -4402,449 +4382,161 @@ var init_sandbox_ignore = __esm(() => {
|
|
|
4402
4382
|
execAsync = promisify(exec);
|
|
4403
4383
|
});
|
|
4404
4384
|
|
|
4405
|
-
// src/
|
|
4406
|
-
import {
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
return join12(projectRoot, ".locus", "run-state.json");
|
|
4416
|
-
}
|
|
4417
|
-
function loadRunState(projectRoot) {
|
|
4418
|
-
const path = getRunStatePath(projectRoot);
|
|
4419
|
-
if (!existsSync13(path))
|
|
4420
|
-
return null;
|
|
4421
|
-
try {
|
|
4422
|
-
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
4423
|
-
} catch {
|
|
4424
|
-
getLogger().warn("Corrupted run-state.json, ignoring");
|
|
4425
|
-
return null;
|
|
4426
|
-
}
|
|
4427
|
-
}
|
|
4428
|
-
function saveRunState(projectRoot, state) {
|
|
4429
|
-
const path = getRunStatePath(projectRoot);
|
|
4430
|
-
const dir = dirname3(path);
|
|
4431
|
-
if (!existsSync13(dir)) {
|
|
4432
|
-
mkdirSync9(dir, { recursive: true });
|
|
4433
|
-
}
|
|
4434
|
-
writeFileSync6(path, `${JSON.stringify(state, null, 2)}
|
|
4435
|
-
`, "utf-8");
|
|
4436
|
-
}
|
|
4437
|
-
function clearRunState(projectRoot) {
|
|
4438
|
-
const path = getRunStatePath(projectRoot);
|
|
4439
|
-
if (existsSync13(path)) {
|
|
4440
|
-
unlinkSync3(path);
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
|
-
function createSprintRunState(sprint, branch, issues) {
|
|
4444
|
-
return {
|
|
4445
|
-
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
4446
|
-
type: "sprint",
|
|
4447
|
-
sprint,
|
|
4448
|
-
branch,
|
|
4449
|
-
startedAt: new Date().toISOString(),
|
|
4450
|
-
tasks: issues.map(({ number, order }) => ({
|
|
4451
|
-
issue: number,
|
|
4452
|
-
order,
|
|
4453
|
-
status: "pending"
|
|
4454
|
-
}))
|
|
4455
|
-
};
|
|
4456
|
-
}
|
|
4457
|
-
function createParallelRunState(issueNumbers) {
|
|
4458
|
-
return {
|
|
4459
|
-
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
4460
|
-
type: "parallel",
|
|
4461
|
-
startedAt: new Date().toISOString(),
|
|
4462
|
-
tasks: issueNumbers.map((issue, i) => ({
|
|
4463
|
-
issue,
|
|
4464
|
-
order: i + 1,
|
|
4465
|
-
status: "pending"
|
|
4466
|
-
}))
|
|
4467
|
-
};
|
|
4468
|
-
}
|
|
4469
|
-
function markTaskInProgress(state, issueNumber) {
|
|
4470
|
-
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
4471
|
-
if (task) {
|
|
4472
|
-
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;
|
|
4473
4395
|
}
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
task.status = "done";
|
|
4479
|
-
task.completedAt = new Date().toISOString();
|
|
4480
|
-
if (prNumber)
|
|
4481
|
-
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();
|
|
4482
4400
|
}
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
task.status = "failed";
|
|
4488
|
-
task.failedAt = new Date().toISOString();
|
|
4489
|
-
task.error = error;
|
|
4490
|
-
}
|
|
4491
|
-
}
|
|
4492
|
-
function getRunStats(state) {
|
|
4493
|
-
const tasks = state.tasks;
|
|
4494
|
-
return {
|
|
4495
|
-
total: tasks.length,
|
|
4496
|
-
done: tasks.filter((t) => t.status === "done").length,
|
|
4497
|
-
failed: tasks.filter((t) => t.status === "failed").length,
|
|
4498
|
-
pending: tasks.filter((t) => t.status === "pending").length,
|
|
4499
|
-
inProgress: tasks.filter((t) => t.status === "in_progress").length
|
|
4500
|
-
};
|
|
4501
|
-
}
|
|
4502
|
-
function getNextTask(state) {
|
|
4503
|
-
const failed = state.tasks.find((t) => t.status === "failed");
|
|
4504
|
-
if (failed)
|
|
4505
|
-
return failed;
|
|
4506
|
-
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
4507
|
-
}
|
|
4508
|
-
var init_run_state = __esm(() => {
|
|
4509
|
-
init_logger();
|
|
4510
|
-
});
|
|
4511
|
-
|
|
4512
|
-
// src/core/shutdown.ts
|
|
4513
|
-
import { execSync as execSync6 } from "node:child_process";
|
|
4514
|
-
function registerActiveSandbox(name) {
|
|
4515
|
-
activeSandboxes.add(name);
|
|
4516
|
-
}
|
|
4517
|
-
function unregisterActiveSandbox(name) {
|
|
4518
|
-
activeSandboxes.delete(name);
|
|
4519
|
-
}
|
|
4520
|
-
function cleanupActiveSandboxes() {
|
|
4521
|
-
for (const name of activeSandboxes) {
|
|
4522
|
-
try {
|
|
4523
|
-
execSync6(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
4524
|
-
} catch {}
|
|
4525
|
-
}
|
|
4526
|
-
activeSandboxes.clear();
|
|
4527
|
-
}
|
|
4528
|
-
function registerShutdownHandlers(ctx) {
|
|
4529
|
-
shutdownContext = ctx;
|
|
4530
|
-
interruptCount = 0;
|
|
4531
|
-
const handler = () => {
|
|
4532
|
-
interruptCount++;
|
|
4533
|
-
if (interruptCount >= 2) {
|
|
4534
|
-
process.stderr.write(`
|
|
4535
|
-
Force exit.
|
|
4536
|
-
`);
|
|
4537
|
-
process.exit(1);
|
|
4538
|
-
}
|
|
4539
|
-
process.stderr.write(`
|
|
4540
|
-
|
|
4541
|
-
Interrupted. Saving state...
|
|
4542
|
-
`);
|
|
4543
|
-
const state = shutdownContext?.getRunState?.();
|
|
4544
|
-
if (state && shutdownContext) {
|
|
4545
|
-
for (const task of state.tasks) {
|
|
4546
|
-
if (task.status === "in_progress") {
|
|
4547
|
-
task.status = "failed";
|
|
4548
|
-
task.failedAt = new Date().toISOString();
|
|
4549
|
-
task.error = "Interrupted by user";
|
|
4550
|
-
}
|
|
4551
|
-
}
|
|
4552
|
-
try {
|
|
4553
|
-
saveRunState(shutdownContext.projectRoot, state);
|
|
4554
|
-
process.stderr.write(`State saved. Resume with: locus run --resume
|
|
4555
|
-
`);
|
|
4556
|
-
} catch {
|
|
4557
|
-
process.stderr.write(`Warning: Could not save run state.
|
|
4558
|
-
`);
|
|
4559
|
-
}
|
|
4560
|
-
}
|
|
4561
|
-
cleanupActiveSandboxes();
|
|
4562
|
-
shutdownContext?.onShutdown?.();
|
|
4563
|
-
if (interruptTimer)
|
|
4564
|
-
clearTimeout(interruptTimer);
|
|
4565
|
-
interruptTimer = setTimeout(() => {
|
|
4566
|
-
interruptCount = 0;
|
|
4567
|
-
}, 2000);
|
|
4568
|
-
setTimeout(() => {
|
|
4569
|
-
process.exit(130);
|
|
4570
|
-
}, 100);
|
|
4571
|
-
};
|
|
4572
|
-
if (!shutdownRegistered) {
|
|
4573
|
-
process.on("SIGINT", handler);
|
|
4574
|
-
process.on("SIGTERM", handler);
|
|
4575
|
-
shutdownRegistered = true;
|
|
4576
|
-
}
|
|
4577
|
-
return () => {
|
|
4578
|
-
process.removeListener("SIGINT", handler);
|
|
4579
|
-
process.removeListener("SIGTERM", handler);
|
|
4580
|
-
shutdownRegistered = false;
|
|
4581
|
-
shutdownContext = null;
|
|
4582
|
-
interruptCount = 0;
|
|
4583
|
-
if (interruptTimer) {
|
|
4584
|
-
clearTimeout(interruptTimer);
|
|
4585
|
-
interruptTimer = null;
|
|
4586
|
-
}
|
|
4587
|
-
};
|
|
4588
|
-
}
|
|
4589
|
-
var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
|
|
4590
|
-
var init_shutdown = __esm(() => {
|
|
4591
|
-
init_run_state();
|
|
4592
|
-
activeSandboxes = new Set;
|
|
4593
|
-
});
|
|
4594
|
-
|
|
4595
|
-
// src/ai/claude-sandbox.ts
|
|
4596
|
-
import { execSync as execSync7, spawn as spawn3 } from "node:child_process";
|
|
4597
|
-
|
|
4598
|
-
class SandboxedClaudeRunner {
|
|
4599
|
-
name = "claude-sandboxed";
|
|
4600
|
-
process = null;
|
|
4601
|
-
aborted = false;
|
|
4602
|
-
sandboxName = null;
|
|
4603
|
-
persistent;
|
|
4604
|
-
sandboxCreated = false;
|
|
4605
|
-
userManaged = false;
|
|
4606
|
-
constructor(persistentName, userManaged = false) {
|
|
4607
|
-
if (persistentName) {
|
|
4608
|
-
this.persistent = true;
|
|
4609
|
-
this.sandboxName = persistentName;
|
|
4610
|
-
this.userManaged = userManaged;
|
|
4611
|
-
if (userManaged) {
|
|
4612
|
-
this.sandboxCreated = true;
|
|
4613
|
-
}
|
|
4614
|
-
} else {
|
|
4615
|
-
this.persistent = false;
|
|
4616
|
-
}
|
|
4617
|
-
}
|
|
4618
|
-
async isAvailable() {
|
|
4619
|
-
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4620
|
-
const delegate = new ClaudeRunner2;
|
|
4621
|
-
return delegate.isAvailable();
|
|
4622
|
-
}
|
|
4623
|
-
async getVersion() {
|
|
4624
|
-
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
4625
|
-
const delegate = new ClaudeRunner2;
|
|
4626
|
-
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();
|
|
4627
4405
|
}
|
|
4628
4406
|
async execute(options) {
|
|
4629
4407
|
const log = getLogger();
|
|
4630
4408
|
this.aborted = false;
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
if (!name) {
|
|
4639
|
-
throw new Error("Sandbox name is required");
|
|
4640
|
-
}
|
|
4641
|
-
options.onStatusChange?.("Syncing sandbox...");
|
|
4642
|
-
await enforceSandboxIgnore(name, options.cwd);
|
|
4643
|
-
options.onStatusChange?.("Thinking...");
|
|
4644
|
-
dockerArgs = [
|
|
4645
|
-
"sandbox",
|
|
4646
|
-
"exec",
|
|
4647
|
-
"-w",
|
|
4648
|
-
options.cwd,
|
|
4649
|
-
name,
|
|
4650
|
-
"claude",
|
|
4651
|
-
...claudeArgs
|
|
4652
|
-
];
|
|
4653
|
-
} else {
|
|
4654
|
-
if (!this.persistent) {
|
|
4655
|
-
this.sandboxName = buildSandboxName(options);
|
|
4656
|
-
}
|
|
4657
|
-
const name = this.sandboxName;
|
|
4658
|
-
if (!name) {
|
|
4659
|
-
throw new Error("Sandbox name is required");
|
|
4660
|
-
}
|
|
4661
|
-
registerActiveSandbox(name);
|
|
4662
|
-
options.onStatusChange?.("Syncing sandbox...");
|
|
4663
|
-
dockerArgs = [
|
|
4664
|
-
"sandbox",
|
|
4665
|
-
"run",
|
|
4666
|
-
"--name",
|
|
4667
|
-
name,
|
|
4668
|
-
"claude",
|
|
4669
|
-
options.cwd,
|
|
4670
|
-
"--",
|
|
4671
|
-
...claudeArgs
|
|
4672
|
-
];
|
|
4409
|
+
if (!await this.isSandboxRunning()) {
|
|
4410
|
+
return {
|
|
4411
|
+
success: false,
|
|
4412
|
+
output: "",
|
|
4413
|
+
error: `Sandbox is not running: ${this.sandboxName}`,
|
|
4414
|
+
exitCode: 1
|
|
4415
|
+
};
|
|
4673
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
|
+
];
|
|
4674
4430
|
log.debug("Spawning sandboxed claude", {
|
|
4675
4431
|
sandboxName: this.sandboxName,
|
|
4676
|
-
persistent: this.persistent,
|
|
4677
|
-
reusing: this.persistent && this.sandboxCreated,
|
|
4678
4432
|
args: dockerArgs.join(" "),
|
|
4679
4433
|
cwd: options.cwd
|
|
4680
4434
|
});
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
const
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
const event = JSON.parse(line);
|
|
4707
|
-
if (event.type === "assistant" && event.message?.content) {
|
|
4708
|
-
for (const item of event.message.content) {
|
|
4709
|
-
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
4710
|
-
seenToolIds.add(item.id);
|
|
4711
|
-
options.onToolActivity?.(formatToolCall2(item.name ?? "", item.input ?? {}));
|
|
4712
|
-
}
|
|
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 ?? {}));
|
|
4713
4460
|
}
|
|
4714
|
-
} else if (event.type === "result") {
|
|
4715
|
-
const text = event.result ?? "";
|
|
4716
|
-
output = text;
|
|
4717
|
-
options.onOutput?.(text);
|
|
4718
4461
|
}
|
|
4719
|
-
}
|
|
4720
|
-
const
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
options.onOutput?.(newLine);
|
|
4462
|
+
} else if (event.type === "result") {
|
|
4463
|
+
const text = event.result ?? "";
|
|
4464
|
+
output = text;
|
|
4465
|
+
options.onOutput?.(text);
|
|
4724
4466
|
}
|
|
4467
|
+
} catch {
|
|
4468
|
+
const newLine = `${line}
|
|
4469
|
+
`;
|
|
4470
|
+
output += newLine;
|
|
4471
|
+
options.onOutput?.(newLine);
|
|
4725
4472
|
}
|
|
4726
|
-
}
|
|
4727
|
-
}
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
output += text;
|
|
4731
|
-
options.onOutput?.(text);
|
|
4732
|
-
});
|
|
4733
|
-
}
|
|
4734
|
-
this.process.stderr?.on("data", (chunk) => {
|
|
4473
|
+
}
|
|
4474
|
+
});
|
|
4475
|
+
} else {
|
|
4476
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4735
4477
|
const text = chunk.toString();
|
|
4736
|
-
|
|
4737
|
-
log.debug("sandboxed claude stderr", { text: text.slice(0, 500) });
|
|
4478
|
+
output += text;
|
|
4738
4479
|
options.onOutput?.(text);
|
|
4739
4480
|
});
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
}
|
|
4751
|
-
if (code === 0) {
|
|
4752
|
-
resolve2({
|
|
4753
|
-
success: true,
|
|
4754
|
-
output,
|
|
4755
|
-
exitCode: 0
|
|
4756
|
-
});
|
|
4757
|
-
} else {
|
|
4758
|
-
resolve2({
|
|
4759
|
-
success: false,
|
|
4760
|
-
output,
|
|
4761
|
-
error: errorOutput || `sandboxed claude exited with code ${code}`,
|
|
4762
|
-
exitCode: code ?? 1
|
|
4763
|
-
});
|
|
4764
|
-
}
|
|
4765
|
-
});
|
|
4766
|
-
this.process.on("error", (err) => {
|
|
4767
|
-
this.process = null;
|
|
4768
|
-
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) {
|
|
4769
4491
|
resolve2({
|
|
4770
4492
|
success: false,
|
|
4771
4493
|
output,
|
|
4772
|
-
error:
|
|
4773
|
-
exitCode:
|
|
4494
|
+
error: "Aborted by user",
|
|
4495
|
+
exitCode: code ?? 143
|
|
4774
4496
|
});
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
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
|
|
4779
4507
|
});
|
|
4780
4508
|
}
|
|
4781
4509
|
});
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
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
|
+
});
|
|
4785
4523
|
}
|
|
4786
|
-
}
|
|
4524
|
+
});
|
|
4787
4525
|
}
|
|
4788
4526
|
abort() {
|
|
4789
4527
|
this.aborted = true;
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
});
|
|
4528
|
+
if (!this.process)
|
|
4529
|
+
return;
|
|
4530
|
+
this.process.kill("SIGTERM");
|
|
4531
|
+
const timer = setTimeout(() => {
|
|
4795
4532
|
if (this.process) {
|
|
4796
|
-
this.process.kill("
|
|
4797
|
-
const timer = setTimeout(() => {
|
|
4798
|
-
if (this.process) {
|
|
4799
|
-
this.process.kill("SIGKILL");
|
|
4800
|
-
}
|
|
4801
|
-
}, 3000);
|
|
4802
|
-
if (timer.unref)
|
|
4803
|
-
timer.unref();
|
|
4533
|
+
this.process.kill("SIGKILL");
|
|
4804
4534
|
}
|
|
4805
|
-
}
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
log.debug("Aborting sandboxed claude (ephemeral — removing sandbox)", {
|
|
4809
|
-
sandboxName: this.sandboxName
|
|
4810
|
-
});
|
|
4811
|
-
try {
|
|
4812
|
-
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4813
|
-
} catch {}
|
|
4814
|
-
}
|
|
4815
|
-
}
|
|
4816
|
-
destroy() {
|
|
4817
|
-
if (!this.sandboxName)
|
|
4818
|
-
return;
|
|
4819
|
-
if (this.userManaged) {
|
|
4820
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
4821
|
-
return;
|
|
4822
|
-
}
|
|
4823
|
-
const log = getLogger();
|
|
4824
|
-
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
4825
|
-
try {
|
|
4826
|
-
execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
4827
|
-
} catch {}
|
|
4828
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
4829
|
-
this.sandboxName = null;
|
|
4830
|
-
this.sandboxCreated = false;
|
|
4831
|
-
}
|
|
4832
|
-
cleanupSandbox() {
|
|
4833
|
-
if (!this.sandboxName)
|
|
4834
|
-
return;
|
|
4835
|
-
const log = getLogger();
|
|
4836
|
-
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
4837
|
-
try {
|
|
4838
|
-
execSync7(`docker sandbox rm ${this.sandboxName}`, {
|
|
4839
|
-
timeout: 60000
|
|
4840
|
-
});
|
|
4841
|
-
} catch {}
|
|
4842
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
4843
|
-
this.sandboxName = null;
|
|
4535
|
+
}, 3000);
|
|
4536
|
+
if (timer.unref)
|
|
4537
|
+
timer.unref();
|
|
4844
4538
|
}
|
|
4845
4539
|
async isSandboxRunning() {
|
|
4846
|
-
if (!this.sandboxName)
|
|
4847
|
-
return false;
|
|
4848
4540
|
try {
|
|
4849
4541
|
const { promisify: promisify2 } = await import("node:util");
|
|
4850
4542
|
const { exec: exec2 } = await import("node:child_process");
|
|
@@ -4857,24 +4549,6 @@ class SandboxedClaudeRunner {
|
|
|
4857
4549
|
return false;
|
|
4858
4550
|
}
|
|
4859
4551
|
}
|
|
4860
|
-
getSandboxName() {
|
|
4861
|
-
return this.sandboxName;
|
|
4862
|
-
}
|
|
4863
|
-
}
|
|
4864
|
-
function buildSandboxName(options) {
|
|
4865
|
-
const ts = Date.now();
|
|
4866
|
-
if (options.activity) {
|
|
4867
|
-
const match = options.activity.match(/issue\s*#(\d+)/i);
|
|
4868
|
-
if (match) {
|
|
4869
|
-
return `locus-issue-${match[1]}-${ts}`;
|
|
4870
|
-
}
|
|
4871
|
-
}
|
|
4872
|
-
const segment = options.cwd.split("/").pop() ?? "run";
|
|
4873
|
-
return `locus-${segment}-${ts}`;
|
|
4874
|
-
}
|
|
4875
|
-
function buildPersistentSandboxName(cwd) {
|
|
4876
|
-
const segment = cwd.split("/").pop() ?? "repl";
|
|
4877
|
-
return `locus-${segment}-${Date.now()}`;
|
|
4878
4552
|
}
|
|
4879
4553
|
function formatToolCall2(name, input) {
|
|
4880
4554
|
switch (name) {
|
|
@@ -4898,7 +4572,7 @@ function formatToolCall2(name, input) {
|
|
|
4898
4572
|
case "WebSearch":
|
|
4899
4573
|
return `searching: ${input.query ?? ""}`;
|
|
4900
4574
|
case "Task":
|
|
4901
|
-
return
|
|
4575
|
+
return "spawning agent";
|
|
4902
4576
|
default:
|
|
4903
4577
|
return name;
|
|
4904
4578
|
}
|
|
@@ -4906,12 +4580,11 @@ function formatToolCall2(name, input) {
|
|
|
4906
4580
|
var init_claude_sandbox = __esm(() => {
|
|
4907
4581
|
init_logger();
|
|
4908
4582
|
init_sandbox_ignore();
|
|
4909
|
-
init_shutdown();
|
|
4910
4583
|
init_claude();
|
|
4911
4584
|
});
|
|
4912
4585
|
|
|
4913
4586
|
// src/ai/codex.ts
|
|
4914
|
-
import { execSync as
|
|
4587
|
+
import { execSync as execSync6, spawn as spawn4 } from "node:child_process";
|
|
4915
4588
|
function buildCodexArgs(model) {
|
|
4916
4589
|
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
|
|
4917
4590
|
if (model) {
|
|
@@ -4927,7 +4600,7 @@ class CodexRunner {
|
|
|
4927
4600
|
aborted = false;
|
|
4928
4601
|
async isAvailable() {
|
|
4929
4602
|
try {
|
|
4930
|
-
|
|
4603
|
+
execSync6("codex --version", {
|
|
4931
4604
|
encoding: "utf-8",
|
|
4932
4605
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4933
4606
|
});
|
|
@@ -4938,7 +4611,7 @@ class CodexRunner {
|
|
|
4938
4611
|
}
|
|
4939
4612
|
async getVersion() {
|
|
4940
4613
|
try {
|
|
4941
|
-
const output =
|
|
4614
|
+
const output = execSync6("codex --version", {
|
|
4942
4615
|
encoding: "utf-8",
|
|
4943
4616
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4944
4617
|
}).trim();
|
|
@@ -5086,28 +4759,16 @@ var init_codex = __esm(() => {
|
|
|
5086
4759
|
});
|
|
5087
4760
|
|
|
5088
4761
|
// src/ai/codex-sandbox.ts
|
|
5089
|
-
import {
|
|
4762
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
5090
4763
|
|
|
5091
4764
|
class SandboxedCodexRunner {
|
|
4765
|
+
sandboxName;
|
|
5092
4766
|
name = "codex-sandboxed";
|
|
5093
4767
|
process = null;
|
|
5094
4768
|
aborted = false;
|
|
5095
|
-
sandboxName = null;
|
|
5096
|
-
persistent;
|
|
5097
|
-
sandboxCreated = false;
|
|
5098
|
-
userManaged = false;
|
|
5099
4769
|
codexInstalled = false;
|
|
5100
|
-
constructor(
|
|
5101
|
-
|
|
5102
|
-
this.persistent = true;
|
|
5103
|
-
this.sandboxName = persistentName;
|
|
5104
|
-
this.userManaged = userManaged;
|
|
5105
|
-
if (userManaged) {
|
|
5106
|
-
this.sandboxCreated = true;
|
|
5107
|
-
}
|
|
5108
|
-
} else {
|
|
5109
|
-
this.persistent = false;
|
|
5110
|
-
}
|
|
4770
|
+
constructor(sandboxName) {
|
|
4771
|
+
this.sandboxName = sandboxName;
|
|
5111
4772
|
}
|
|
5112
4773
|
async isAvailable() {
|
|
5113
4774
|
const delegate = new CodexRunner;
|
|
@@ -5120,251 +4781,158 @@ class SandboxedCodexRunner {
|
|
|
5120
4781
|
async execute(options) {
|
|
5121
4782
|
const log = getLogger();
|
|
5122
4783
|
this.aborted = false;
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
4784
|
+
if (!await this.isSandboxRunning()) {
|
|
4785
|
+
return {
|
|
4786
|
+
success: false,
|
|
4787
|
+
output: "",
|
|
4788
|
+
error: `Sandbox is not running: ${this.sandboxName}`,
|
|
4789
|
+
exitCode: 1
|
|
4790
|
+
};
|
|
5127
4791
|
}
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
await enforceSandboxIgnore(name, options.cwd);
|
|
5135
|
-
if (!this.codexInstalled) {
|
|
5136
|
-
options.onStatusChange?.("Checking codex...");
|
|
5137
|
-
await this.ensureCodexInstalled(name);
|
|
5138
|
-
this.codexInstalled = true;
|
|
5139
|
-
}
|
|
5140
|
-
options.onStatusChange?.("Thinking...");
|
|
5141
|
-
dockerArgs = [
|
|
5142
|
-
"sandbox",
|
|
5143
|
-
"exec",
|
|
5144
|
-
"-i",
|
|
5145
|
-
"-w",
|
|
5146
|
-
options.cwd,
|
|
5147
|
-
name,
|
|
5148
|
-
"codex",
|
|
5149
|
-
...codexArgs
|
|
5150
|
-
];
|
|
5151
|
-
} else {
|
|
5152
|
-
if (!this.persistent) {
|
|
5153
|
-
this.sandboxName = buildSandboxName2(options);
|
|
5154
|
-
}
|
|
5155
|
-
const name = this.sandboxName;
|
|
5156
|
-
if (!name) {
|
|
5157
|
-
throw new Error("Sandbox name is required");
|
|
5158
|
-
}
|
|
5159
|
-
registerActiveSandbox(name);
|
|
5160
|
-
options.onStatusChange?.("Creating sandbox...");
|
|
5161
|
-
await this.createSandboxWithClaude(name, options.cwd);
|
|
5162
|
-
options.onStatusChange?.("Installing codex...");
|
|
5163
|
-
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);
|
|
5164
4798
|
this.codexInstalled = true;
|
|
5165
|
-
options.onStatusChange?.("Syncing sandbox...");
|
|
5166
|
-
await enforceSandboxIgnore(name, options.cwd);
|
|
5167
|
-
options.onStatusChange?.("Thinking...");
|
|
5168
|
-
dockerArgs = [
|
|
5169
|
-
"sandbox",
|
|
5170
|
-
"exec",
|
|
5171
|
-
"-i",
|
|
5172
|
-
"-w",
|
|
5173
|
-
options.cwd,
|
|
5174
|
-
name,
|
|
5175
|
-
"codex",
|
|
5176
|
-
...codexArgs
|
|
5177
|
-
];
|
|
5178
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
|
+
];
|
|
5179
4811
|
log.debug("Spawning sandboxed codex", {
|
|
5180
4812
|
sandboxName: this.sandboxName,
|
|
5181
|
-
persistent: this.persistent,
|
|
5182
|
-
reusing: this.persistent && this.sandboxCreated,
|
|
5183
4813
|
args: dockerArgs.join(" "),
|
|
5184
4814
|
cwd: options.cwd
|
|
5185
4815
|
});
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
});
|
|
5198
|
-
}
|
|
5199
|
-
let agentMessages = [];
|
|
5200
|
-
const flushAgentMessages = () => {
|
|
5201
|
-
if (agentMessages.length > 0) {
|
|
5202
|
-
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(`
|
|
5203
4827
|
|
|
5204
4828
|
`));
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
4829
|
+
agentMessages = [];
|
|
4830
|
+
}
|
|
4831
|
+
};
|
|
4832
|
+
let lineBuffer = "";
|
|
4833
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
4834
|
+
lineBuffer += chunk.toString();
|
|
4835
|
+
const lines = lineBuffer.split(`
|
|
5212
4836
|
`);
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
4837
|
+
lineBuffer = lines.pop() ?? "";
|
|
4838
|
+
for (const line of lines) {
|
|
4839
|
+
if (!line.trim())
|
|
4840
|
+
continue;
|
|
4841
|
+
rawOutput += `${line}
|
|
5218
4842
|
`;
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
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(`
|
|
5225
4849
|
`)[0].slice(0, 80);
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
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(`
|
|
5239
4863
|
`)[0].slice(0, 80));
|
|
5240
|
-
}
|
|
5241
|
-
} else if (type === "turn.completed") {
|
|
5242
|
-
flushAgentMessages();
|
|
5243
4864
|
}
|
|
5244
|
-
}
|
|
5245
|
-
|
|
5246
|
-
`;
|
|
5247
|
-
rawOutput += newLine;
|
|
5248
|
-
options.onOutput?.(newLine);
|
|
4865
|
+
} else if (type === "turn.completed") {
|
|
4866
|
+
flushAgentMessages();
|
|
5249
4867
|
}
|
|
4868
|
+
} catch {
|
|
4869
|
+
const newLine = `${line}
|
|
4870
|
+
`;
|
|
4871
|
+
rawOutput += newLine;
|
|
4872
|
+
options.onOutput?.(newLine);
|
|
5250
4873
|
}
|
|
5251
|
-
}
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
});
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
success: false,
|
|
5263
|
-
output: rawOutput,
|
|
5264
|
-
error: "Aborted by user",
|
|
5265
|
-
exitCode: code ?? 143
|
|
5266
|
-
});
|
|
5267
|
-
return;
|
|
5268
|
-
}
|
|
5269
|
-
if (code === 0) {
|
|
5270
|
-
resolve2({
|
|
5271
|
-
success: true,
|
|
5272
|
-
output: rawOutput,
|
|
5273
|
-
exitCode: 0
|
|
5274
|
-
});
|
|
5275
|
-
} else {
|
|
5276
|
-
resolve2({
|
|
5277
|
-
success: false,
|
|
5278
|
-
output: rawOutput,
|
|
5279
|
-
error: errorOutput || `sandboxed codex exited with code ${code}`,
|
|
5280
|
-
exitCode: code ?? 1
|
|
5281
|
-
});
|
|
5282
|
-
}
|
|
5283
|
-
});
|
|
5284
|
-
this.process.on("error", (err) => {
|
|
5285
|
-
this.process = null;
|
|
5286
|
-
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) {
|
|
5287
4885
|
resolve2({
|
|
5288
4886
|
success: false,
|
|
5289
4887
|
output: rawOutput,
|
|
5290
|
-
error:
|
|
5291
|
-
exitCode:
|
|
4888
|
+
error: "Aborted by user",
|
|
4889
|
+
exitCode: code ?? 143
|
|
5292
4890
|
});
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
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
|
|
5297
4901
|
});
|
|
5298
4902
|
}
|
|
5299
|
-
this.process.stdin?.write(options.prompt);
|
|
5300
|
-
this.process.stdin?.end();
|
|
5301
4903
|
});
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
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
|
+
});
|
|
5305
4917
|
}
|
|
5306
|
-
|
|
4918
|
+
this.process.stdin?.write(options.prompt);
|
|
4919
|
+
this.process.stdin?.end();
|
|
4920
|
+
});
|
|
5307
4921
|
}
|
|
5308
4922
|
abort() {
|
|
5309
4923
|
this.aborted = true;
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
});
|
|
4924
|
+
if (!this.process)
|
|
4925
|
+
return;
|
|
4926
|
+
this.process.kill("SIGTERM");
|
|
4927
|
+
const timer = setTimeout(() => {
|
|
5315
4928
|
if (this.process) {
|
|
5316
|
-
this.process.kill("
|
|
5317
|
-
const timer = setTimeout(() => {
|
|
5318
|
-
if (this.process) {
|
|
5319
|
-
this.process.kill("SIGKILL");
|
|
5320
|
-
}
|
|
5321
|
-
}, 3000);
|
|
5322
|
-
if (timer.unref)
|
|
5323
|
-
timer.unref();
|
|
4929
|
+
this.process.kill("SIGKILL");
|
|
5324
4930
|
}
|
|
5325
|
-
}
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
log.debug("Aborting sandboxed codex (ephemeral — removing sandbox)", {
|
|
5329
|
-
sandboxName: this.sandboxName
|
|
5330
|
-
});
|
|
5331
|
-
try {
|
|
5332
|
-
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5333
|
-
} catch {}
|
|
5334
|
-
}
|
|
5335
|
-
}
|
|
5336
|
-
destroy() {
|
|
5337
|
-
if (!this.sandboxName)
|
|
5338
|
-
return;
|
|
5339
|
-
if (this.userManaged) {
|
|
5340
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
5341
|
-
return;
|
|
5342
|
-
}
|
|
5343
|
-
const log = getLogger();
|
|
5344
|
-
log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
|
|
5345
|
-
try {
|
|
5346
|
-
execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
|
|
5347
|
-
} catch {}
|
|
5348
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
5349
|
-
this.sandboxName = null;
|
|
5350
|
-
this.sandboxCreated = false;
|
|
5351
|
-
}
|
|
5352
|
-
cleanupSandbox() {
|
|
5353
|
-
if (!this.sandboxName)
|
|
5354
|
-
return;
|
|
5355
|
-
const log = getLogger();
|
|
5356
|
-
log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
|
|
5357
|
-
try {
|
|
5358
|
-
execSync9(`docker sandbox rm ${this.sandboxName}`, {
|
|
5359
|
-
timeout: 60000
|
|
5360
|
-
});
|
|
5361
|
-
} catch {}
|
|
5362
|
-
unregisterActiveSandbox(this.sandboxName);
|
|
5363
|
-
this.sandboxName = null;
|
|
4931
|
+
}, 3000);
|
|
4932
|
+
if (timer.unref)
|
|
4933
|
+
timer.unref();
|
|
5364
4934
|
}
|
|
5365
4935
|
async isSandboxRunning() {
|
|
5366
|
-
if (!this.sandboxName)
|
|
5367
|
-
return false;
|
|
5368
4936
|
try {
|
|
5369
4937
|
const { promisify: promisify2 } = await import("node:util");
|
|
5370
4938
|
const { exec: exec2 } = await import("node:child_process");
|
|
@@ -5377,14 +4945,6 @@ class SandboxedCodexRunner {
|
|
|
5377
4945
|
return false;
|
|
5378
4946
|
}
|
|
5379
4947
|
}
|
|
5380
|
-
async createSandboxWithClaude(name, cwd) {
|
|
5381
|
-
const { promisify: promisify2 } = await import("node:util");
|
|
5382
|
-
const { exec: exec2 } = await import("node:child_process");
|
|
5383
|
-
const execAsync2 = promisify2(exec2);
|
|
5384
|
-
try {
|
|
5385
|
-
await execAsync2(`docker sandbox run --name ${name} claude ${cwd} -- --version`, { timeout: 120000 });
|
|
5386
|
-
} catch {}
|
|
5387
|
-
}
|
|
5388
4948
|
async ensureCodexInstalled(name) {
|
|
5389
4949
|
const { promisify: promisify2 } = await import("node:util");
|
|
5390
4950
|
const { exec: exec2 } = await import("node:child_process");
|
|
@@ -5394,38 +4954,28 @@ class SandboxedCodexRunner {
|
|
|
5394
4954
|
timeout: 5000
|
|
5395
4955
|
});
|
|
5396
4956
|
} catch {
|
|
5397
|
-
await execAsync2(`docker sandbox exec ${name} npm install -g @openai/codex`, {
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
getSandboxName() {
|
|
5401
|
-
return this.sandboxName;
|
|
5402
|
-
}
|
|
5403
|
-
}
|
|
5404
|
-
function buildSandboxName2(options) {
|
|
5405
|
-
const ts = Date.now();
|
|
5406
|
-
if (options.activity) {
|
|
5407
|
-
const match = options.activity.match(/issue\s*#(\d+)/i);
|
|
5408
|
-
if (match) {
|
|
5409
|
-
return `locus-codex-issue-${match[1]}-${ts}`;
|
|
4957
|
+
await execAsync2(`docker sandbox exec ${name} npm install -g @openai/codex`, {
|
|
4958
|
+
timeout: 120000
|
|
4959
|
+
});
|
|
5410
4960
|
}
|
|
5411
4961
|
}
|
|
5412
|
-
const segment = options.cwd.split("/").pop() ?? "run";
|
|
5413
|
-
return `locus-codex-${segment}-${ts}`;
|
|
5414
4962
|
}
|
|
5415
4963
|
var init_codex_sandbox = __esm(() => {
|
|
5416
4964
|
init_logger();
|
|
5417
4965
|
init_sandbox_ignore();
|
|
5418
|
-
init_shutdown();
|
|
5419
4966
|
init_codex();
|
|
5420
4967
|
});
|
|
5421
4968
|
|
|
5422
4969
|
// src/ai/runner.ts
|
|
5423
4970
|
async function createRunnerAsync(provider, sandboxed) {
|
|
4971
|
+
if (sandboxed) {
|
|
4972
|
+
throw new Error("Sandboxed runner creation requires a provider sandbox name. Use createUserManagedSandboxRunner().");
|
|
4973
|
+
}
|
|
5424
4974
|
switch (provider) {
|
|
5425
4975
|
case "claude":
|
|
5426
|
-
return
|
|
4976
|
+
return new ClaudeRunner;
|
|
5427
4977
|
case "codex":
|
|
5428
|
-
return
|
|
4978
|
+
return new CodexRunner;
|
|
5429
4979
|
default:
|
|
5430
4980
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5431
4981
|
}
|
|
@@ -5433,9 +4983,9 @@ async function createRunnerAsync(provider, sandboxed) {
|
|
|
5433
4983
|
function createUserManagedSandboxRunner(provider, sandboxName) {
|
|
5434
4984
|
switch (provider) {
|
|
5435
4985
|
case "claude":
|
|
5436
|
-
return new SandboxedClaudeRunner(sandboxName
|
|
4986
|
+
return new SandboxedClaudeRunner(sandboxName);
|
|
5437
4987
|
case "codex":
|
|
5438
|
-
return new SandboxedCodexRunner(sandboxName
|
|
4988
|
+
return new SandboxedCodexRunner(sandboxName);
|
|
5439
4989
|
default:
|
|
5440
4990
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5441
4991
|
}
|
|
@@ -5535,10 +5085,20 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
5535
5085
|
});
|
|
5536
5086
|
if (options.runner) {
|
|
5537
5087
|
runner = options.runner;
|
|
5538
|
-
} 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
|
+
}
|
|
5539
5099
|
runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName);
|
|
5540
5100
|
} else {
|
|
5541
|
-
runner = await createRunnerAsync(resolvedProvider,
|
|
5101
|
+
runner = await createRunnerAsync(resolvedProvider, false);
|
|
5542
5102
|
}
|
|
5543
5103
|
const available = await runner.isAvailable();
|
|
5544
5104
|
if (!available) {
|
|
@@ -5864,7 +5424,7 @@ async function issueCreate(projectRoot, parsed) {
|
|
|
5864
5424
|
silent: true,
|
|
5865
5425
|
activity: "generating issue",
|
|
5866
5426
|
sandboxed: config.sandbox.enabled,
|
|
5867
|
-
sandboxName: config.sandbox.
|
|
5427
|
+
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
|
|
5868
5428
|
});
|
|
5869
5429
|
if (!aiResult.success && !aiResult.interrupted) {
|
|
5870
5430
|
process.stderr.write(`${red("✗")} Failed to generate issue: ${aiResult.error}
|
|
@@ -6409,6 +5969,7 @@ var init_issue = __esm(() => {
|
|
|
6409
5969
|
init_config();
|
|
6410
5970
|
init_github();
|
|
6411
5971
|
init_logger();
|
|
5972
|
+
init_sandbox();
|
|
6412
5973
|
init_table();
|
|
6413
5974
|
init_terminal();
|
|
6414
5975
|
init_types();
|
|
@@ -7066,9 +6627,9 @@ var init_sprint = __esm(() => {
|
|
|
7066
6627
|
});
|
|
7067
6628
|
|
|
7068
6629
|
// src/core/prompt-builder.ts
|
|
7069
|
-
import { execSync as
|
|
7070
|
-
import { existsSync as
|
|
7071
|
-
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";
|
|
7072
6633
|
function buildExecutionPrompt(ctx) {
|
|
7073
6634
|
const sections = [];
|
|
7074
6635
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -7098,13 +6659,13 @@ function buildFeedbackPrompt(ctx) {
|
|
|
7098
6659
|
}
|
|
7099
6660
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
7100
6661
|
const sections = [];
|
|
7101
|
-
const locusmd = readFileSafe(
|
|
6662
|
+
const locusmd = readFileSafe(join12(projectRoot, ".locus", "LOCUS.md"));
|
|
7102
6663
|
if (locusmd) {
|
|
7103
6664
|
sections.push(`<project-instructions>
|
|
7104
6665
|
${locusmd}
|
|
7105
6666
|
</project-instructions>`);
|
|
7106
6667
|
}
|
|
7107
|
-
const learnings = readFileSafe(
|
|
6668
|
+
const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
|
|
7108
6669
|
if (learnings) {
|
|
7109
6670
|
sections.push(`<past-learnings>
|
|
7110
6671
|
${learnings}
|
|
@@ -7130,24 +6691,24 @@ ${userMessage}
|
|
|
7130
6691
|
}
|
|
7131
6692
|
function buildSystemContext(projectRoot) {
|
|
7132
6693
|
const parts = [];
|
|
7133
|
-
const locusmd = readFileSafe(
|
|
6694
|
+
const locusmd = readFileSafe(join12(projectRoot, ".locus", "LOCUS.md"));
|
|
7134
6695
|
if (locusmd) {
|
|
7135
6696
|
parts.push(`<project-instructions>
|
|
7136
6697
|
${locusmd}
|
|
7137
6698
|
</project-instructions>`);
|
|
7138
6699
|
}
|
|
7139
|
-
const learnings = readFileSafe(
|
|
6700
|
+
const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
|
|
7140
6701
|
if (learnings) {
|
|
7141
6702
|
parts.push(`<past-learnings>
|
|
7142
6703
|
${learnings}
|
|
7143
6704
|
</past-learnings>`);
|
|
7144
6705
|
}
|
|
7145
|
-
const discussionsDir =
|
|
7146
|
-
if (
|
|
6706
|
+
const discussionsDir = join12(projectRoot, ".locus", "discussions");
|
|
6707
|
+
if (existsSync13(discussionsDir)) {
|
|
7147
6708
|
try {
|
|
7148
6709
|
const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
7149
6710
|
for (const file of files) {
|
|
7150
|
-
const content = readFileSafe(
|
|
6711
|
+
const content = readFileSafe(join12(discussionsDir, file));
|
|
7151
6712
|
if (content) {
|
|
7152
6713
|
const name = file.replace(".md", "");
|
|
7153
6714
|
parts.push(`<discussion name="${name}">
|
|
@@ -7214,7 +6775,7 @@ ${parts.join(`
|
|
|
7214
6775
|
function buildRepoContext(projectRoot) {
|
|
7215
6776
|
const parts = [];
|
|
7216
6777
|
try {
|
|
7217
|
-
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();
|
|
7218
6779
|
if (tree) {
|
|
7219
6780
|
parts.push(`<file-tree>
|
|
7220
6781
|
\`\`\`
|
|
@@ -7224,7 +6785,7 @@ ${tree}
|
|
|
7224
6785
|
}
|
|
7225
6786
|
} catch {}
|
|
7226
6787
|
try {
|
|
7227
|
-
const gitLog =
|
|
6788
|
+
const gitLog = execSync7("git log --oneline -10", {
|
|
7228
6789
|
cwd: projectRoot,
|
|
7229
6790
|
encoding: "utf-8",
|
|
7230
6791
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7238,7 +6799,7 @@ ${gitLog}
|
|
|
7238
6799
|
}
|
|
7239
6800
|
} catch {}
|
|
7240
6801
|
try {
|
|
7241
|
-
const branch =
|
|
6802
|
+
const branch = execSync7("git rev-parse --abbrev-ref HEAD", {
|
|
7242
6803
|
cwd: projectRoot,
|
|
7243
6804
|
encoding: "utf-8",
|
|
7244
6805
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7298,9 +6859,9 @@ function buildFeedbackInstructions() {
|
|
|
7298
6859
|
}
|
|
7299
6860
|
function readFileSafe(path) {
|
|
7300
6861
|
try {
|
|
7301
|
-
if (!
|
|
6862
|
+
if (!existsSync13(path))
|
|
7302
6863
|
return null;
|
|
7303
|
-
return
|
|
6864
|
+
return readFileSync9(path, "utf-8");
|
|
7304
6865
|
} catch {
|
|
7305
6866
|
return null;
|
|
7306
6867
|
}
|
|
@@ -7492,7 +7053,7 @@ var init_diff_renderer = __esm(() => {
|
|
|
7492
7053
|
});
|
|
7493
7054
|
|
|
7494
7055
|
// src/repl/commands.ts
|
|
7495
|
-
import { execSync as
|
|
7056
|
+
import { execSync as execSync8 } from "node:child_process";
|
|
7496
7057
|
function getSlashCommands() {
|
|
7497
7058
|
return [
|
|
7498
7059
|
{
|
|
@@ -7684,7 +7245,7 @@ function cmdModel(args, ctx) {
|
|
|
7684
7245
|
}
|
|
7685
7246
|
function cmdDiff(_args, ctx) {
|
|
7686
7247
|
try {
|
|
7687
|
-
const diff =
|
|
7248
|
+
const diff = execSync8("git diff", {
|
|
7688
7249
|
cwd: ctx.projectRoot,
|
|
7689
7250
|
encoding: "utf-8",
|
|
7690
7251
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7720,7 +7281,7 @@ function cmdDiff(_args, ctx) {
|
|
|
7720
7281
|
}
|
|
7721
7282
|
function cmdUndo(_args, ctx) {
|
|
7722
7283
|
try {
|
|
7723
|
-
const status =
|
|
7284
|
+
const status = execSync8("git status --porcelain", {
|
|
7724
7285
|
cwd: ctx.projectRoot,
|
|
7725
7286
|
encoding: "utf-8",
|
|
7726
7287
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7730,7 +7291,7 @@ function cmdUndo(_args, ctx) {
|
|
|
7730
7291
|
`);
|
|
7731
7292
|
return;
|
|
7732
7293
|
}
|
|
7733
|
-
|
|
7294
|
+
execSync8("git checkout .", {
|
|
7734
7295
|
cwd: ctx.projectRoot,
|
|
7735
7296
|
encoding: "utf-8",
|
|
7736
7297
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7764,7 +7325,7 @@ var init_commands = __esm(() => {
|
|
|
7764
7325
|
|
|
7765
7326
|
// src/repl/completions.ts
|
|
7766
7327
|
import { readdirSync as readdirSync4 } from "node:fs";
|
|
7767
|
-
import { basename as basename2, dirname as
|
|
7328
|
+
import { basename as basename2, dirname as dirname3, join as join13 } from "node:path";
|
|
7768
7329
|
|
|
7769
7330
|
class SlashCommandCompletion {
|
|
7770
7331
|
commands;
|
|
@@ -7819,7 +7380,7 @@ class FilePathCompletion {
|
|
|
7819
7380
|
}
|
|
7820
7381
|
findMatches(partial) {
|
|
7821
7382
|
try {
|
|
7822
|
-
const dir = partial.includes("/") ?
|
|
7383
|
+
const dir = partial.includes("/") ? join13(this.projectRoot, dirname3(partial)) : this.projectRoot;
|
|
7823
7384
|
const prefix = basename2(partial);
|
|
7824
7385
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7825
7386
|
return entries.filter((e) => {
|
|
@@ -7830,7 +7391,7 @@ class FilePathCompletion {
|
|
|
7830
7391
|
return e.name.startsWith(prefix);
|
|
7831
7392
|
}).map((e) => {
|
|
7832
7393
|
const name = e.isDirectory() ? `${e.name}/` : e.name;
|
|
7833
|
-
return partial.includes("/") ? `${
|
|
7394
|
+
return partial.includes("/") ? `${dirname3(partial)}/${name}` : name;
|
|
7834
7395
|
}).slice(0, 20);
|
|
7835
7396
|
} catch {
|
|
7836
7397
|
return [];
|
|
@@ -7855,14 +7416,14 @@ class CombinedCompletion {
|
|
|
7855
7416
|
var init_completions = () => {};
|
|
7856
7417
|
|
|
7857
7418
|
// src/repl/input-history.ts
|
|
7858
|
-
import { existsSync as
|
|
7859
|
-
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";
|
|
7860
7421
|
|
|
7861
7422
|
class InputHistory {
|
|
7862
7423
|
entries = [];
|
|
7863
7424
|
filePath;
|
|
7864
7425
|
constructor(projectRoot) {
|
|
7865
|
-
this.filePath =
|
|
7426
|
+
this.filePath = join14(projectRoot, ".locus", "sessions", ".input-history");
|
|
7866
7427
|
this.load();
|
|
7867
7428
|
}
|
|
7868
7429
|
add(text) {
|
|
@@ -7901,22 +7462,22 @@ class InputHistory {
|
|
|
7901
7462
|
}
|
|
7902
7463
|
load() {
|
|
7903
7464
|
try {
|
|
7904
|
-
if (!
|
|
7465
|
+
if (!existsSync14(this.filePath))
|
|
7905
7466
|
return;
|
|
7906
|
-
const content =
|
|
7467
|
+
const content = readFileSync10(this.filePath, "utf-8");
|
|
7907
7468
|
this.entries = content.split(`
|
|
7908
7469
|
`).map((line) => this.unescape(line)).filter(Boolean);
|
|
7909
7470
|
} catch {}
|
|
7910
7471
|
}
|
|
7911
7472
|
save() {
|
|
7912
7473
|
try {
|
|
7913
|
-
const dir =
|
|
7914
|
-
if (!
|
|
7915
|
-
|
|
7474
|
+
const dir = dirname4(this.filePath);
|
|
7475
|
+
if (!existsSync14(dir)) {
|
|
7476
|
+
mkdirSync9(dir, { recursive: true });
|
|
7916
7477
|
}
|
|
7917
7478
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
7918
7479
|
`);
|
|
7919
|
-
|
|
7480
|
+
writeFileSync6(this.filePath, content, "utf-8");
|
|
7920
7481
|
} catch {}
|
|
7921
7482
|
}
|
|
7922
7483
|
escape(text) {
|
|
@@ -7942,21 +7503,21 @@ var init_model_config = __esm(() => {
|
|
|
7942
7503
|
|
|
7943
7504
|
// src/repl/session-manager.ts
|
|
7944
7505
|
import {
|
|
7945
|
-
existsSync as
|
|
7946
|
-
mkdirSync as
|
|
7506
|
+
existsSync as existsSync15,
|
|
7507
|
+
mkdirSync as mkdirSync10,
|
|
7947
7508
|
readdirSync as readdirSync5,
|
|
7948
|
-
readFileSync as
|
|
7949
|
-
unlinkSync as
|
|
7950
|
-
writeFileSync as
|
|
7509
|
+
readFileSync as readFileSync11,
|
|
7510
|
+
unlinkSync as unlinkSync3,
|
|
7511
|
+
writeFileSync as writeFileSync7
|
|
7951
7512
|
} from "node:fs";
|
|
7952
|
-
import { basename as basename3, join as
|
|
7513
|
+
import { basename as basename3, join as join15 } from "node:path";
|
|
7953
7514
|
|
|
7954
7515
|
class SessionManager {
|
|
7955
7516
|
sessionsDir;
|
|
7956
7517
|
constructor(projectRoot) {
|
|
7957
|
-
this.sessionsDir =
|
|
7958
|
-
if (!
|
|
7959
|
-
|
|
7518
|
+
this.sessionsDir = join15(projectRoot, ".locus", "sessions");
|
|
7519
|
+
if (!existsSync15(this.sessionsDir)) {
|
|
7520
|
+
mkdirSync10(this.sessionsDir, { recursive: true });
|
|
7960
7521
|
}
|
|
7961
7522
|
}
|
|
7962
7523
|
create(options) {
|
|
@@ -7981,14 +7542,14 @@ class SessionManager {
|
|
|
7981
7542
|
}
|
|
7982
7543
|
isPersisted(sessionOrId) {
|
|
7983
7544
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
7984
|
-
return
|
|
7545
|
+
return existsSync15(this.getSessionPath(sessionId));
|
|
7985
7546
|
}
|
|
7986
7547
|
load(idOrPrefix) {
|
|
7987
7548
|
const files = this.listSessionFiles();
|
|
7988
7549
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
7989
|
-
if (
|
|
7550
|
+
if (existsSync15(exactPath)) {
|
|
7990
7551
|
try {
|
|
7991
|
-
return JSON.parse(
|
|
7552
|
+
return JSON.parse(readFileSync11(exactPath, "utf-8"));
|
|
7992
7553
|
} catch {
|
|
7993
7554
|
return null;
|
|
7994
7555
|
}
|
|
@@ -7996,7 +7557,7 @@ class SessionManager {
|
|
|
7996
7557
|
const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
|
|
7997
7558
|
if (matches.length === 1) {
|
|
7998
7559
|
try {
|
|
7999
|
-
return JSON.parse(
|
|
7560
|
+
return JSON.parse(readFileSync11(matches[0], "utf-8"));
|
|
8000
7561
|
} catch {
|
|
8001
7562
|
return null;
|
|
8002
7563
|
}
|
|
@@ -8009,7 +7570,7 @@ class SessionManager {
|
|
|
8009
7570
|
save(session) {
|
|
8010
7571
|
session.updated = new Date().toISOString();
|
|
8011
7572
|
const path = this.getSessionPath(session.id);
|
|
8012
|
-
|
|
7573
|
+
writeFileSync7(path, `${JSON.stringify(session, null, 2)}
|
|
8013
7574
|
`, "utf-8");
|
|
8014
7575
|
}
|
|
8015
7576
|
addMessage(session, message) {
|
|
@@ -8021,7 +7582,7 @@ class SessionManager {
|
|
|
8021
7582
|
const sessions = [];
|
|
8022
7583
|
for (const file of files) {
|
|
8023
7584
|
try {
|
|
8024
|
-
const session = JSON.parse(
|
|
7585
|
+
const session = JSON.parse(readFileSync11(file, "utf-8"));
|
|
8025
7586
|
sessions.push({
|
|
8026
7587
|
id: session.id,
|
|
8027
7588
|
created: session.created,
|
|
@@ -8036,8 +7597,8 @@ class SessionManager {
|
|
|
8036
7597
|
}
|
|
8037
7598
|
delete(sessionId) {
|
|
8038
7599
|
const path = this.getSessionPath(sessionId);
|
|
8039
|
-
if (
|
|
8040
|
-
|
|
7600
|
+
if (existsSync15(path)) {
|
|
7601
|
+
unlinkSync3(path);
|
|
8041
7602
|
return true;
|
|
8042
7603
|
}
|
|
8043
7604
|
return false;
|
|
@@ -8048,7 +7609,7 @@ class SessionManager {
|
|
|
8048
7609
|
let pruned = 0;
|
|
8049
7610
|
const withStats = files.map((f) => {
|
|
8050
7611
|
try {
|
|
8051
|
-
const session = JSON.parse(
|
|
7612
|
+
const session = JSON.parse(readFileSync11(f, "utf-8"));
|
|
8052
7613
|
return { path: f, updated: new Date(session.updated).getTime() };
|
|
8053
7614
|
} catch {
|
|
8054
7615
|
return { path: f, updated: 0 };
|
|
@@ -8058,7 +7619,7 @@ class SessionManager {
|
|
|
8058
7619
|
for (const entry of withStats) {
|
|
8059
7620
|
if (now - entry.updated > SESSION_MAX_AGE_MS) {
|
|
8060
7621
|
try {
|
|
8061
|
-
|
|
7622
|
+
unlinkSync3(entry.path);
|
|
8062
7623
|
pruned++;
|
|
8063
7624
|
} catch {}
|
|
8064
7625
|
}
|
|
@@ -8066,10 +7627,10 @@ class SessionManager {
|
|
|
8066
7627
|
const remaining = withStats.length - pruned;
|
|
8067
7628
|
if (remaining > MAX_SESSIONS) {
|
|
8068
7629
|
const toRemove = remaining - MAX_SESSIONS;
|
|
8069
|
-
const alive = withStats.filter((e) =>
|
|
7630
|
+
const alive = withStats.filter((e) => existsSync15(e.path));
|
|
8070
7631
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
8071
7632
|
try {
|
|
8072
|
-
|
|
7633
|
+
unlinkSync3(alive[i].path);
|
|
8073
7634
|
pruned++;
|
|
8074
7635
|
} catch {}
|
|
8075
7636
|
}
|
|
@@ -8081,7 +7642,7 @@ class SessionManager {
|
|
|
8081
7642
|
}
|
|
8082
7643
|
listSessionFiles() {
|
|
8083
7644
|
try {
|
|
8084
|
-
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));
|
|
8085
7646
|
} catch {
|
|
8086
7647
|
return [];
|
|
8087
7648
|
}
|
|
@@ -8090,7 +7651,7 @@ class SessionManager {
|
|
|
8090
7651
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8091
7652
|
}
|
|
8092
7653
|
getSessionPath(sessionId) {
|
|
8093
|
-
return
|
|
7654
|
+
return join15(this.sessionsDir, `${sessionId}.json`);
|
|
8094
7655
|
}
|
|
8095
7656
|
}
|
|
8096
7657
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -8100,7 +7661,7 @@ var init_session_manager = __esm(() => {
|
|
|
8100
7661
|
});
|
|
8101
7662
|
|
|
8102
7663
|
// src/repl/repl.ts
|
|
8103
|
-
import { execSync as
|
|
7664
|
+
import { execSync as execSync9 } from "node:child_process";
|
|
8104
7665
|
async function startRepl(options) {
|
|
8105
7666
|
const { projectRoot, config } = options;
|
|
8106
7667
|
const sessionManager = new SessionManager(projectRoot);
|
|
@@ -8118,7 +7679,7 @@ async function startRepl(options) {
|
|
|
8118
7679
|
} else {
|
|
8119
7680
|
let branch = "main";
|
|
8120
7681
|
try {
|
|
8121
|
-
branch =
|
|
7682
|
+
branch = execSync9("git rev-parse --abbrev-ref HEAD", {
|
|
8122
7683
|
cwd: projectRoot,
|
|
8123
7684
|
encoding: "utf-8",
|
|
8124
7685
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8161,16 +7722,17 @@ async function executeOneShotPrompt(prompt, session, sessionManager, options) {
|
|
|
8161
7722
|
async function runInteractiveRepl(session, sessionManager, options) {
|
|
8162
7723
|
const { projectRoot, config } = options;
|
|
8163
7724
|
let sandboxRunner = null;
|
|
8164
|
-
if (config.sandbox.enabled
|
|
7725
|
+
if (config.sandbox.enabled) {
|
|
8165
7726
|
const provider = inferProviderFromModel(config.ai.model) || config.ai.provider;
|
|
8166
|
-
|
|
8167
|
-
|
|
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)}
|
|
8168
7731
|
`);
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
sandboxRunner = new SandboxedClaudeRunner(sandboxName);
|
|
8172
|
-
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.`)}
|
|
8173
7734
|
`);
|
|
7735
|
+
}
|
|
8174
7736
|
}
|
|
8175
7737
|
const history = new InputHistory(projectRoot);
|
|
8176
7738
|
const completion = new CombinedCompletion([
|
|
@@ -8204,10 +7766,17 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
8204
7766
|
const providerChanged = inferredProvider !== currentProvider;
|
|
8205
7767
|
currentProvider = inferredProvider;
|
|
8206
7768
|
session.metadata.provider = inferredProvider;
|
|
8207
|
-
if (providerChanged && config.sandbox.enabled
|
|
8208
|
-
|
|
8209
|
-
|
|
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})`)}
|
|
8210
7774
|
`);
|
|
7775
|
+
} else {
|
|
7776
|
+
sandboxRunner = null;
|
|
7777
|
+
process.stderr.write(`${yellow("⚠")} ${dim(`No sandbox configured for ${inferredProvider}. Run locus sandbox.`)}
|
|
7778
|
+
`);
|
|
7779
|
+
}
|
|
8211
7780
|
}
|
|
8212
7781
|
}
|
|
8213
7782
|
persistReplModelSelection(projectRoot, config, model);
|
|
@@ -8279,10 +7848,6 @@ ${red("✗")} ${msg}
|
|
|
8279
7848
|
break;
|
|
8280
7849
|
}
|
|
8281
7850
|
}
|
|
8282
|
-
if (sandboxRunner && "destroy" in sandboxRunner) {
|
|
8283
|
-
const runner = sandboxRunner;
|
|
8284
|
-
runner.destroy();
|
|
8285
|
-
}
|
|
8286
7851
|
const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
|
|
8287
7852
|
if (shouldPersistOnExit) {
|
|
8288
7853
|
sessionManager.save(session);
|
|
@@ -8300,6 +7865,7 @@ ${red("✗")} ${msg}
|
|
|
8300
7865
|
}
|
|
8301
7866
|
async function executeAITurn(prompt, session, options, verbose = false, runner) {
|
|
8302
7867
|
const { config, projectRoot } = options;
|
|
7868
|
+
const sandboxName = getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider);
|
|
8303
7869
|
const aiResult = await runAI({
|
|
8304
7870
|
prompt,
|
|
8305
7871
|
provider: config.ai.provider,
|
|
@@ -8307,7 +7873,7 @@ async function executeAITurn(prompt, session, options, verbose = false, runner)
|
|
|
8307
7873
|
cwd: projectRoot,
|
|
8308
7874
|
verbose,
|
|
8309
7875
|
sandboxed: config.sandbox.enabled,
|
|
8310
|
-
sandboxName
|
|
7876
|
+
sandboxName,
|
|
8311
7877
|
runner
|
|
8312
7878
|
});
|
|
8313
7879
|
if (aiResult.interrupted) {
|
|
@@ -8338,11 +7904,11 @@ function printWelcome(session) {
|
|
|
8338
7904
|
`);
|
|
8339
7905
|
}
|
|
8340
7906
|
var init_repl = __esm(() => {
|
|
8341
|
-
init_claude_sandbox();
|
|
8342
7907
|
init_run_ai();
|
|
8343
7908
|
init_runner();
|
|
8344
7909
|
init_ai_models();
|
|
8345
7910
|
init_prompt_builder();
|
|
7911
|
+
init_sandbox();
|
|
8346
7912
|
init_terminal();
|
|
8347
7913
|
init_commands();
|
|
8348
7914
|
init_completions();
|
|
@@ -8483,7 +8049,12 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
|
|
|
8483
8049
|
stream.emitStatus("thinking");
|
|
8484
8050
|
try {
|
|
8485
8051
|
const fullPrompt = buildReplPrompt(prompt, projectRoot, config);
|
|
8486
|
-
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
|
+
}
|
|
8487
8058
|
const available = await runner.isAvailable();
|
|
8488
8059
|
if (!available) {
|
|
8489
8060
|
stream.emitError(`${config.ai.provider} CLI not available`, false);
|
|
@@ -8525,13 +8096,14 @@ var init_exec = __esm(() => {
|
|
|
8525
8096
|
init_config();
|
|
8526
8097
|
init_logger();
|
|
8527
8098
|
init_prompt_builder();
|
|
8099
|
+
init_sandbox();
|
|
8528
8100
|
init_terminal();
|
|
8529
8101
|
init_repl();
|
|
8530
8102
|
init_session_manager();
|
|
8531
8103
|
});
|
|
8532
8104
|
|
|
8533
8105
|
// src/core/agent.ts
|
|
8534
|
-
import { execSync as
|
|
8106
|
+
import { execSync as execSync10 } from "node:child_process";
|
|
8535
8107
|
async function executeIssue(projectRoot, options) {
|
|
8536
8108
|
const log = getLogger();
|
|
8537
8109
|
const timer = createTimer();
|
|
@@ -8560,7 +8132,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
|
|
|
8560
8132
|
}
|
|
8561
8133
|
let issueComments = [];
|
|
8562
8134
|
try {
|
|
8563
|
-
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();
|
|
8564
8136
|
if (commentsRaw) {
|
|
8565
8137
|
issueComments = commentsRaw.split(`
|
|
8566
8138
|
`).filter(Boolean);
|
|
@@ -8703,7 +8275,7 @@ ${c.body}`),
|
|
|
8703
8275
|
cwd: projectRoot,
|
|
8704
8276
|
activity: `iterating on PR #${prNumber}`,
|
|
8705
8277
|
sandboxed: config.sandbox.enabled,
|
|
8706
|
-
sandboxName: config.sandbox.
|
|
8278
|
+
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
|
|
8707
8279
|
});
|
|
8708
8280
|
if (aiResult.interrupted) {
|
|
8709
8281
|
process.stderr.write(`
|
|
@@ -8724,12 +8296,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
|
|
|
8724
8296
|
}
|
|
8725
8297
|
async function createIssuePR(projectRoot, config, issue) {
|
|
8726
8298
|
try {
|
|
8727
|
-
const currentBranch =
|
|
8299
|
+
const currentBranch = execSync10("git rev-parse --abbrev-ref HEAD", {
|
|
8728
8300
|
cwd: projectRoot,
|
|
8729
8301
|
encoding: "utf-8",
|
|
8730
8302
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8731
8303
|
}).trim();
|
|
8732
|
-
const diff =
|
|
8304
|
+
const diff = execSync10(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8733
8305
|
cwd: projectRoot,
|
|
8734
8306
|
encoding: "utf-8",
|
|
8735
8307
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8738,7 +8310,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
8738
8310
|
getLogger().verbose("No changes to create PR for");
|
|
8739
8311
|
return;
|
|
8740
8312
|
}
|
|
8741
|
-
|
|
8313
|
+
execSync10(`git push -u origin ${currentBranch}`, {
|
|
8742
8314
|
cwd: projectRoot,
|
|
8743
8315
|
encoding: "utf-8",
|
|
8744
8316
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8781,12 +8353,13 @@ var init_agent = __esm(() => {
|
|
|
8781
8353
|
init_github();
|
|
8782
8354
|
init_logger();
|
|
8783
8355
|
init_prompt_builder();
|
|
8356
|
+
init_sandbox();
|
|
8784
8357
|
});
|
|
8785
8358
|
|
|
8786
8359
|
// src/core/conflict.ts
|
|
8787
|
-
import { execSync as
|
|
8360
|
+
import { execSync as execSync11 } from "node:child_process";
|
|
8788
8361
|
function git2(args, cwd) {
|
|
8789
|
-
return
|
|
8362
|
+
return execSync11(`git ${args}`, {
|
|
8790
8363
|
cwd,
|
|
8791
8364
|
encoding: "utf-8",
|
|
8792
8365
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8834,89 +8407,273 @@ function checkForConflicts(cwd, baseBranch) {
|
|
|
8834
8407
|
newCommits: 0
|
|
8835
8408
|
};
|
|
8836
8409
|
}
|
|
8837
|
-
const newCommitsOutput = gitSafe(`rev-list --count ${mergeBase}..origin/${baseBranch}`, cwd)?.trim() ?? "0";
|
|
8838
|
-
const newCommits = Number.parseInt(newCommitsOutput, 10);
|
|
8839
|
-
log.verbose(`Base branch has ${newCommits} new commits`, {
|
|
8840
|
-
baseBranch,
|
|
8841
|
-
mergeBase: mergeBase.slice(0, 8),
|
|
8842
|
-
remoteTip: remoteTip.slice(0, 8)
|
|
8843
|
-
});
|
|
8844
|
-
const ourChanges = gitSafe(`diff --name-only ${mergeBase}..HEAD`, cwd)?.trim().split(`
|
|
8845
|
-
`).filter(Boolean) ?? [];
|
|
8846
|
-
const theirChanges = gitSafe(`diff --name-only ${mergeBase}..origin/${baseBranch}`, cwd)?.trim().split(`
|
|
8847
|
-
`).filter(Boolean) ?? [];
|
|
8848
|
-
const overlapping = ourChanges.filter((f) => theirChanges.includes(f));
|
|
8410
|
+
const newCommitsOutput = gitSafe(`rev-list --count ${mergeBase}..origin/${baseBranch}`, cwd)?.trim() ?? "0";
|
|
8411
|
+
const newCommits = Number.parseInt(newCommitsOutput, 10);
|
|
8412
|
+
log.verbose(`Base branch has ${newCommits} new commits`, {
|
|
8413
|
+
baseBranch,
|
|
8414
|
+
mergeBase: mergeBase.slice(0, 8),
|
|
8415
|
+
remoteTip: remoteTip.slice(0, 8)
|
|
8416
|
+
});
|
|
8417
|
+
const ourChanges = gitSafe(`diff --name-only ${mergeBase}..HEAD`, cwd)?.trim().split(`
|
|
8418
|
+
`).filter(Boolean) ?? [];
|
|
8419
|
+
const theirChanges = gitSafe(`diff --name-only ${mergeBase}..origin/${baseBranch}`, cwd)?.trim().split(`
|
|
8420
|
+
`).filter(Boolean) ?? [];
|
|
8421
|
+
const overlapping = ourChanges.filter((f) => theirChanges.includes(f));
|
|
8422
|
+
return {
|
|
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;
|
|
8849
8576
|
return {
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
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
|
|
8854
8582
|
};
|
|
8855
8583
|
}
|
|
8856
|
-
function
|
|
8857
|
-
const
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
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) {
|
|
8865
8598
|
try {
|
|
8866
|
-
|
|
8867
|
-
conflicts.push(...status.trim().split(`
|
|
8868
|
-
`).filter(Boolean));
|
|
8599
|
+
execSync12(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
8869
8600
|
} catch {}
|
|
8870
|
-
gitSafe("rebase --abort", cwd);
|
|
8871
|
-
return { success: false, conflicts };
|
|
8872
8601
|
}
|
|
8602
|
+
activeSandboxes.clear();
|
|
8873
8603
|
}
|
|
8874
|
-
function
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
process.stderr.write(` Base branch ${cyan(`origin/${baseBranch}`)} has ${result.newCommits} new commit${result.newCommits === 1 ? "" : "s"}
|
|
8883
|
-
`);
|
|
8884
|
-
process.stderr.write(` The following files were modified in both branches:
|
|
8885
|
-
|
|
8886
|
-
`);
|
|
8887
|
-
for (const file of result.conflictingFiles) {
|
|
8888
|
-
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.
|
|
8889
8612
|
`);
|
|
8613
|
+
process.exit(1);
|
|
8890
8614
|
}
|
|
8891
8615
|
process.stderr.write(`
|
|
8892
|
-
${bold("To resolve:")}
|
|
8893
|
-
`);
|
|
8894
|
-
process.stderr.write(` 1. ${dim(`git rebase origin/${baseBranch}`)}
|
|
8895
|
-
`);
|
|
8896
|
-
process.stderr.write(` 2. Resolve conflicts in the listed files
|
|
8897
|
-
`);
|
|
8898
|
-
process.stderr.write(` 3. ${dim("git rebase --continue")}
|
|
8899
|
-
`);
|
|
8900
|
-
process.stderr.write(` 4. ${dim("locus run --resume")} to continue the sprint
|
|
8901
8616
|
|
|
8617
|
+
Interrupted. Saving state...
|
|
8902
8618
|
`);
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
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
|
|
8631
|
+
`);
|
|
8632
|
+
} catch {
|
|
8633
|
+
process.stderr.write(`Warning: Could not save run state.
|
|
8906
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;
|
|
8907
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
|
+
};
|
|
8908
8664
|
}
|
|
8909
|
-
var
|
|
8910
|
-
|
|
8911
|
-
|
|
8665
|
+
var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
|
|
8666
|
+
var init_shutdown = __esm(() => {
|
|
8667
|
+
init_run_state();
|
|
8668
|
+
activeSandboxes = new Set;
|
|
8912
8669
|
});
|
|
8913
8670
|
|
|
8914
8671
|
// src/core/worktree.ts
|
|
8915
|
-
import { execSync as
|
|
8672
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
8916
8673
|
import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
|
|
8917
8674
|
import { join as join17 } from "node:path";
|
|
8918
8675
|
function git3(args, cwd) {
|
|
8919
|
-
return
|
|
8676
|
+
return execSync13(`git ${args}`, {
|
|
8920
8677
|
cwd,
|
|
8921
8678
|
encoding: "utf-8",
|
|
8922
8679
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8941,7 +8698,7 @@ function generateBranchName(issueNumber) {
|
|
|
8941
8698
|
}
|
|
8942
8699
|
function getWorktreeBranch(worktreePath) {
|
|
8943
8700
|
try {
|
|
8944
|
-
return
|
|
8701
|
+
return execSync13("git branch --show-current", {
|
|
8945
8702
|
cwd: worktreePath,
|
|
8946
8703
|
encoding: "utf-8",
|
|
8947
8704
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9066,7 +8823,13 @@ var exports_run = {};
|
|
|
9066
8823
|
__export(exports_run, {
|
|
9067
8824
|
runCommand: () => runCommand
|
|
9068
8825
|
});
|
|
9069
|
-
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
|
+
}
|
|
9070
8833
|
function printRunHelp() {
|
|
9071
8834
|
process.stderr.write(`
|
|
9072
8835
|
${bold("locus run")} — Execute issues using AI agents
|
|
@@ -9149,6 +8912,7 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
9149
8912
|
}
|
|
9150
8913
|
async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
9151
8914
|
const log = getLogger();
|
|
8915
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9152
8916
|
if (!config.sprint.active) {
|
|
9153
8917
|
process.stderr.write(`${red("✗")} No active sprint. Set one with: ${bold("locus sprint active <name>")}
|
|
9154
8918
|
`);
|
|
@@ -9210,7 +8974,7 @@ ${yellow("⚠")} A sprint run is already in progress.
|
|
|
9210
8974
|
}
|
|
9211
8975
|
if (!flags.dryRun) {
|
|
9212
8976
|
try {
|
|
9213
|
-
|
|
8977
|
+
execSync14(`git checkout -B ${branchName}`, {
|
|
9214
8978
|
cwd: projectRoot,
|
|
9215
8979
|
encoding: "utf-8",
|
|
9216
8980
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9260,7 +9024,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
|
|
|
9260
9024
|
let sprintContext;
|
|
9261
9025
|
if (i > 0 && !flags.dryRun) {
|
|
9262
9026
|
try {
|
|
9263
|
-
sprintContext =
|
|
9027
|
+
sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
9264
9028
|
cwd: projectRoot,
|
|
9265
9029
|
encoding: "utf-8",
|
|
9266
9030
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9275,12 +9039,13 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
9275
9039
|
saveRunState(projectRoot, state);
|
|
9276
9040
|
const result = await executeIssue(projectRoot, {
|
|
9277
9041
|
issueNumber: task.issue,
|
|
9278
|
-
provider:
|
|
9279
|
-
model:
|
|
9042
|
+
provider: execution.provider,
|
|
9043
|
+
model: execution.model,
|
|
9280
9044
|
dryRun: flags.dryRun,
|
|
9281
9045
|
sprintContext,
|
|
9282
9046
|
skipPR: true,
|
|
9283
|
-
sandboxed
|
|
9047
|
+
sandboxed,
|
|
9048
|
+
sandboxName: execution.sandboxName
|
|
9284
9049
|
});
|
|
9285
9050
|
if (result.success) {
|
|
9286
9051
|
if (!flags.dryRun) {
|
|
@@ -9324,7 +9089,7 @@ ${bold("Summary:")}
|
|
|
9324
9089
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
9325
9090
|
if (prNumber !== undefined) {
|
|
9326
9091
|
try {
|
|
9327
|
-
|
|
9092
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9328
9093
|
cwd: projectRoot,
|
|
9329
9094
|
encoding: "utf-8",
|
|
9330
9095
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9339,6 +9104,7 @@ ${bold("Summary:")}
|
|
|
9339
9104
|
}
|
|
9340
9105
|
}
|
|
9341
9106
|
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
9107
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9342
9108
|
let isSprintIssue = false;
|
|
9343
9109
|
try {
|
|
9344
9110
|
const issue = getIssue(issueNumber, { cwd: projectRoot });
|
|
@@ -9351,11 +9117,11 @@ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, n
|
|
|
9351
9117
|
`);
|
|
9352
9118
|
await executeIssue(projectRoot, {
|
|
9353
9119
|
issueNumber,
|
|
9354
|
-
provider:
|
|
9355
|
-
model:
|
|
9120
|
+
provider: execution.provider,
|
|
9121
|
+
model: execution.model,
|
|
9356
9122
|
dryRun: flags.dryRun,
|
|
9357
9123
|
sandboxed,
|
|
9358
|
-
sandboxName:
|
|
9124
|
+
sandboxName: execution.sandboxName
|
|
9359
9125
|
});
|
|
9360
9126
|
return;
|
|
9361
9127
|
}
|
|
@@ -9382,11 +9148,11 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
9382
9148
|
const result = await executeIssue(projectRoot, {
|
|
9383
9149
|
issueNumber,
|
|
9384
9150
|
worktreePath,
|
|
9385
|
-
provider:
|
|
9386
|
-
model:
|
|
9151
|
+
provider: execution.provider,
|
|
9152
|
+
model: execution.model,
|
|
9387
9153
|
dryRun: flags.dryRun,
|
|
9388
9154
|
sandboxed,
|
|
9389
|
-
sandboxName:
|
|
9155
|
+
sandboxName: execution.sandboxName
|
|
9390
9156
|
});
|
|
9391
9157
|
if (worktreePath && !flags.dryRun) {
|
|
9392
9158
|
if (result.success) {
|
|
@@ -9401,6 +9167,7 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
9401
9167
|
}
|
|
9402
9168
|
async function handleParallelRun(projectRoot, config, issueNumbers, flags, sandboxed) {
|
|
9403
9169
|
const log = getLogger();
|
|
9170
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9404
9171
|
const maxConcurrent = config.agent.maxParallel;
|
|
9405
9172
|
process.stderr.write(`
|
|
9406
9173
|
${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxConcurrent} parallel, worktrees)`)}
|
|
@@ -9452,11 +9219,11 @@ ${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxCon
|
|
|
9452
9219
|
const result = await executeIssue(projectRoot, {
|
|
9453
9220
|
issueNumber,
|
|
9454
9221
|
worktreePath,
|
|
9455
|
-
provider:
|
|
9456
|
-
model:
|
|
9222
|
+
provider: execution.provider,
|
|
9223
|
+
model: execution.model,
|
|
9457
9224
|
dryRun: flags.dryRun,
|
|
9458
9225
|
sandboxed,
|
|
9459
|
-
sandboxName:
|
|
9226
|
+
sandboxName: execution.sandboxName
|
|
9460
9227
|
});
|
|
9461
9228
|
if (result.success) {
|
|
9462
9229
|
markTaskDone(state, issueNumber, result.prNumber);
|
|
@@ -9506,6 +9273,7 @@ ${yellow("⚠")} Failed worktrees preserved for debugging:
|
|
|
9506
9273
|
}
|
|
9507
9274
|
}
|
|
9508
9275
|
async function handleResume(projectRoot, config, sandboxed) {
|
|
9276
|
+
const execution = resolveExecutionContext(config);
|
|
9509
9277
|
const state = loadRunState(projectRoot);
|
|
9510
9278
|
if (!state) {
|
|
9511
9279
|
process.stderr.write(`${red("✗")} No run state found. Nothing to resume.
|
|
@@ -9521,13 +9289,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9521
9289
|
`);
|
|
9522
9290
|
if (state.type === "sprint" && state.branch) {
|
|
9523
9291
|
try {
|
|
9524
|
-
const currentBranch =
|
|
9292
|
+
const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
|
|
9525
9293
|
cwd: projectRoot,
|
|
9526
9294
|
encoding: "utf-8",
|
|
9527
9295
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9528
9296
|
}).trim();
|
|
9529
9297
|
if (currentBranch !== state.branch) {
|
|
9530
|
-
|
|
9298
|
+
execSync14(`git checkout ${state.branch}`, {
|
|
9531
9299
|
cwd: projectRoot,
|
|
9532
9300
|
encoding: "utf-8",
|
|
9533
9301
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9551,11 +9319,11 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9551
9319
|
saveRunState(projectRoot, state);
|
|
9552
9320
|
const result = await executeIssue(projectRoot, {
|
|
9553
9321
|
issueNumber: task.issue,
|
|
9554
|
-
provider:
|
|
9555
|
-
model:
|
|
9322
|
+
provider: execution.provider,
|
|
9323
|
+
model: execution.model,
|
|
9556
9324
|
skipPR: isSprintRun,
|
|
9557
9325
|
sandboxed,
|
|
9558
|
-
sandboxName:
|
|
9326
|
+
sandboxName: execution.sandboxName
|
|
9559
9327
|
});
|
|
9560
9328
|
if (result.success) {
|
|
9561
9329
|
if (isSprintRun) {
|
|
@@ -9594,7 +9362,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
|
|
|
9594
9362
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
9595
9363
|
if (prNumber !== undefined) {
|
|
9596
9364
|
try {
|
|
9597
|
-
|
|
9365
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9598
9366
|
cwd: projectRoot,
|
|
9599
9367
|
encoding: "utf-8",
|
|
9600
9368
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9625,14 +9393,14 @@ function getOrder2(issue) {
|
|
|
9625
9393
|
}
|
|
9626
9394
|
function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
9627
9395
|
try {
|
|
9628
|
-
const status =
|
|
9396
|
+
const status = execSync14("git status --porcelain", {
|
|
9629
9397
|
cwd: projectRoot,
|
|
9630
9398
|
encoding: "utf-8",
|
|
9631
9399
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9632
9400
|
}).trim();
|
|
9633
9401
|
if (!status)
|
|
9634
9402
|
return;
|
|
9635
|
-
|
|
9403
|
+
execSync14("git add -A", {
|
|
9636
9404
|
cwd: projectRoot,
|
|
9637
9405
|
encoding: "utf-8",
|
|
9638
9406
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9640,7 +9408,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
9640
9408
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
9641
9409
|
|
|
9642
9410
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
9643
|
-
|
|
9411
|
+
execSync14(`git commit -F -`, {
|
|
9644
9412
|
input: message,
|
|
9645
9413
|
cwd: projectRoot,
|
|
9646
9414
|
encoding: "utf-8",
|
|
@@ -9654,7 +9422,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9654
9422
|
if (!config.agent.autoPR)
|
|
9655
9423
|
return;
|
|
9656
9424
|
try {
|
|
9657
|
-
const diff =
|
|
9425
|
+
const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
9658
9426
|
cwd: projectRoot,
|
|
9659
9427
|
encoding: "utf-8",
|
|
9660
9428
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9664,7 +9432,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9664
9432
|
`);
|
|
9665
9433
|
return;
|
|
9666
9434
|
}
|
|
9667
|
-
|
|
9435
|
+
execSync14(`git push -u origin ${branchName}`, {
|
|
9668
9436
|
cwd: projectRoot,
|
|
9669
9437
|
encoding: "utf-8",
|
|
9670
9438
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9690,6 +9458,7 @@ ${taskLines}
|
|
|
9690
9458
|
}
|
|
9691
9459
|
}
|
|
9692
9460
|
var init_run = __esm(() => {
|
|
9461
|
+
init_ai_models();
|
|
9693
9462
|
init_agent();
|
|
9694
9463
|
init_config();
|
|
9695
9464
|
init_conflict();
|
|
@@ -10066,7 +9835,7 @@ ${bold("Planning:")} ${cyan(displayDirective)}
|
|
|
10066
9835
|
cwd: projectRoot,
|
|
10067
9836
|
activity: "planning",
|
|
10068
9837
|
sandboxed: config.sandbox.enabled,
|
|
10069
|
-
sandboxName: config.sandbox.
|
|
9838
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10070
9839
|
});
|
|
10071
9840
|
if (aiResult.interrupted) {
|
|
10072
9841
|
process.stderr.write(`
|
|
@@ -10181,7 +9950,7 @@ Start with foundational/setup tasks, then core features, then integration/testin
|
|
|
10181
9950
|
activity: "issue ordering",
|
|
10182
9951
|
silent: true,
|
|
10183
9952
|
sandboxed: config.sandbox.enabled,
|
|
10184
|
-
sandboxName: config.sandbox.
|
|
9953
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10185
9954
|
});
|
|
10186
9955
|
if (aiResult.interrupted) {
|
|
10187
9956
|
process.stderr.write(`
|
|
@@ -10433,6 +10202,7 @@ var init_plan = __esm(() => {
|
|
|
10433
10202
|
init_config();
|
|
10434
10203
|
init_github();
|
|
10435
10204
|
init_terminal();
|
|
10205
|
+
init_sandbox();
|
|
10436
10206
|
});
|
|
10437
10207
|
|
|
10438
10208
|
// src/commands/review.ts
|
|
@@ -10440,7 +10210,7 @@ var exports_review = {};
|
|
|
10440
10210
|
__export(exports_review, {
|
|
10441
10211
|
reviewCommand: () => reviewCommand
|
|
10442
10212
|
});
|
|
10443
|
-
import { execSync as
|
|
10213
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
10444
10214
|
import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
|
|
10445
10215
|
import { join as join19 } from "node:path";
|
|
10446
10216
|
function printHelp2() {
|
|
@@ -10518,7 +10288,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
|
|
|
10518
10288
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
10519
10289
|
let prInfo;
|
|
10520
10290
|
try {
|
|
10521
|
-
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"] });
|
|
10522
10292
|
const raw = JSON.parse(result);
|
|
10523
10293
|
prInfo = {
|
|
10524
10294
|
number: raw.number,
|
|
@@ -10563,7 +10333,7 @@ async function reviewPR(projectRoot, config, pr, focus, flags) {
|
|
|
10563
10333
|
cwd: projectRoot,
|
|
10564
10334
|
activity: `PR #${pr.number}`,
|
|
10565
10335
|
sandboxed: config.sandbox.enabled,
|
|
10566
|
-
sandboxName: config.sandbox.
|
|
10336
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10567
10337
|
});
|
|
10568
10338
|
if (aiResult.interrupted) {
|
|
10569
10339
|
process.stderr.write(` ${yellow("⚡")} Review interrupted.
|
|
@@ -10584,7 +10354,7 @@ ${output.slice(0, 60000)}
|
|
|
10584
10354
|
|
|
10585
10355
|
---
|
|
10586
10356
|
_Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
|
|
10587
|
-
|
|
10357
|
+
execSync15(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10588
10358
|
process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
|
|
10589
10359
|
`);
|
|
10590
10360
|
} catch (e) {
|
|
@@ -10654,6 +10424,7 @@ var init_review = __esm(() => {
|
|
|
10654
10424
|
init_run_ai();
|
|
10655
10425
|
init_config();
|
|
10656
10426
|
init_github();
|
|
10427
|
+
init_sandbox();
|
|
10657
10428
|
init_progress();
|
|
10658
10429
|
init_terminal();
|
|
10659
10430
|
});
|
|
@@ -10663,7 +10434,7 @@ var exports_iterate = {};
|
|
|
10663
10434
|
__export(exports_iterate, {
|
|
10664
10435
|
iterateCommand: () => iterateCommand
|
|
10665
10436
|
});
|
|
10666
|
-
import { execSync as
|
|
10437
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
10667
10438
|
function printHelp3() {
|
|
10668
10439
|
process.stderr.write(`
|
|
10669
10440
|
${bold("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -10873,12 +10644,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
|
|
|
10873
10644
|
}
|
|
10874
10645
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
10875
10646
|
try {
|
|
10876
|
-
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"] });
|
|
10877
10648
|
const parsed = JSON.parse(result);
|
|
10878
10649
|
if (parsed.length > 0) {
|
|
10879
10650
|
return parsed[0].number;
|
|
10880
10651
|
}
|
|
10881
|
-
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"] });
|
|
10882
10653
|
const branchParsed = JSON.parse(branchResult);
|
|
10883
10654
|
if (branchParsed.length > 0) {
|
|
10884
10655
|
return branchParsed[0].number;
|
|
@@ -11123,7 +10894,7 @@ ${bold("Discussion:")} ${cyan(topic)}
|
|
|
11123
10894
|
cwd: projectRoot,
|
|
11124
10895
|
activity: "discussion",
|
|
11125
10896
|
sandboxed: config.sandbox.enabled,
|
|
11126
|
-
sandboxName: config.sandbox.
|
|
10897
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
11127
10898
|
});
|
|
11128
10899
|
if (aiResult.interrupted) {
|
|
11129
10900
|
process.stderr.write(`
|
|
@@ -11269,6 +11040,7 @@ var MAX_DISCUSSION_ROUNDS = 5;
|
|
|
11269
11040
|
var init_discuss = __esm(() => {
|
|
11270
11041
|
init_run_ai();
|
|
11271
11042
|
init_config();
|
|
11043
|
+
init_sandbox();
|
|
11272
11044
|
init_progress();
|
|
11273
11045
|
init_terminal();
|
|
11274
11046
|
init_input_handler();
|
|
@@ -11493,22 +11265,25 @@ var exports_sandbox2 = {};
|
|
|
11493
11265
|
__export(exports_sandbox2, {
|
|
11494
11266
|
sandboxCommand: () => sandboxCommand
|
|
11495
11267
|
});
|
|
11496
|
-
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";
|
|
11497
11271
|
function printSandboxHelp() {
|
|
11498
11272
|
process.stderr.write(`
|
|
11499
11273
|
${bold("locus sandbox")} — Manage Docker sandbox lifecycle
|
|
11500
11274
|
|
|
11501
11275
|
${bold("Usage:")}
|
|
11502
|
-
locus sandbox ${dim("# Create
|
|
11276
|
+
locus sandbox ${dim("# Create claude/codex sandboxes and enable sandbox mode")}
|
|
11503
11277
|
locus sandbox claude ${dim("# Run claude interactively (for login)")}
|
|
11504
11278
|
locus sandbox codex ${dim("# Run codex interactively (for login)")}
|
|
11505
|
-
locus sandbox rm ${dim("# Destroy
|
|
11279
|
+
locus sandbox rm ${dim("# Destroy all provider sandboxes and disable sandbox mode")}
|
|
11506
11280
|
locus sandbox status ${dim("# Show current sandbox state")}
|
|
11507
11281
|
|
|
11508
11282
|
${bold("Flow:")}
|
|
11509
|
-
1. ${cyan("locus sandbox")} Create
|
|
11510
|
-
2. ${cyan("locus sandbox claude")} Login
|
|
11511
|
-
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
|
|
11512
11287
|
|
|
11513
11288
|
`);
|
|
11514
11289
|
}
|
|
@@ -11536,18 +11311,6 @@ async function sandboxCommand(projectRoot, args) {
|
|
|
11536
11311
|
}
|
|
11537
11312
|
async function handleCreate(projectRoot) {
|
|
11538
11313
|
const config = loadConfig(projectRoot);
|
|
11539
|
-
if (config.sandbox.name) {
|
|
11540
|
-
const alive = isSandboxAlive(config.sandbox.name);
|
|
11541
|
-
if (alive) {
|
|
11542
|
-
process.stderr.write(`${green("✓")} Sandbox already exists: ${bold(config.sandbox.name)}
|
|
11543
|
-
`);
|
|
11544
|
-
process.stderr.write(` Run ${cyan("locus sandbox claude")} or ${cyan("locus sandbox codex")} to login.
|
|
11545
|
-
`);
|
|
11546
|
-
return;
|
|
11547
|
-
}
|
|
11548
|
-
process.stderr.write(`${yellow("⚠")} Previous sandbox ${dim(config.sandbox.name)} is no longer running. Creating a new one.
|
|
11549
|
-
`);
|
|
11550
|
-
}
|
|
11551
11314
|
const status = await detectSandboxSupport();
|
|
11552
11315
|
if (!status.available) {
|
|
11553
11316
|
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
@@ -11556,86 +11319,68 @@ async function handleCreate(projectRoot) {
|
|
|
11556
11319
|
`);
|
|
11557
11320
|
return;
|
|
11558
11321
|
}
|
|
11559
|
-
const
|
|
11560
|
-
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
|
+
}
|
|
11561
11346
|
config.sandbox.enabled = true;
|
|
11562
|
-
config.sandbox.
|
|
11347
|
+
config.sandbox.providers = readySandboxes;
|
|
11563
11348
|
saveConfig(projectRoot, config);
|
|
11564
|
-
|
|
11349
|
+
if (failed) {
|
|
11350
|
+
process.stderr.write(`
|
|
11351
|
+
${yellow("⚠")} Some sandboxes failed to create. Re-run ${cyan("locus sandbox")} after resolving Docker issues.
|
|
11565
11352
|
`);
|
|
11566
|
-
|
|
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.
|
|
11567
11358
|
`);
|
|
11568
11359
|
}
|
|
11569
11360
|
async function handleAgentLogin(projectRoot, agent) {
|
|
11570
11361
|
const config = loadConfig(projectRoot);
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
11575
|
-
`);
|
|
11576
|
-
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.
|
|
11577
11365
|
`);
|
|
11578
|
-
|
|
11579
|
-
}
|
|
11580
|
-
const segment = projectRoot.split("/").pop() ?? "sandbox";
|
|
11581
|
-
config.sandbox.name = `locus-${segment}-${Date.now()}`;
|
|
11582
|
-
config.sandbox.enabled = true;
|
|
11583
|
-
saveConfig(projectRoot, config);
|
|
11366
|
+
return;
|
|
11584
11367
|
}
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
let dockerArgs;
|
|
11588
|
-
if (alive) {
|
|
11589
|
-
if (agent === "codex") {
|
|
11590
|
-
await ensureCodexInSandbox(sandboxName);
|
|
11591
|
-
}
|
|
11592
|
-
process.stderr.write(`Connecting to sandbox ${dim(sandboxName)}...
|
|
11593
|
-
`);
|
|
11594
|
-
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11595
|
-
|
|
11596
|
-
`);
|
|
11597
|
-
dockerArgs = [
|
|
11598
|
-
"sandbox",
|
|
11599
|
-
"exec",
|
|
11600
|
-
"-it",
|
|
11601
|
-
"-w",
|
|
11602
|
-
projectRoot,
|
|
11603
|
-
sandboxName,
|
|
11604
|
-
agent
|
|
11605
|
-
];
|
|
11606
|
-
} else if (agent === "codex") {
|
|
11607
|
-
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)}
|
|
11608
11370
|
`);
|
|
11609
|
-
|
|
11610
|
-
execSync19(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
11611
|
-
} catch {}
|
|
11612
|
-
if (!isSandboxAlive(sandboxName)) {
|
|
11613
|
-
process.stderr.write(`${red("✗")} Failed to create sandbox.
|
|
11371
|
+
process.stderr.write(` Recreate it with ${cyan("locus sandbox")}.
|
|
11614
11372
|
`);
|
|
11615
|
-
|
|
11616
|
-
|
|
11373
|
+
return;
|
|
11374
|
+
}
|
|
11375
|
+
if (agent === "codex") {
|
|
11617
11376
|
await ensureCodexInSandbox(sandboxName);
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
`);
|
|
11621
|
-
dockerArgs = [
|
|
11622
|
-
"sandbox",
|
|
11623
|
-
"exec",
|
|
11624
|
-
"-it",
|
|
11625
|
-
"-w",
|
|
11626
|
-
projectRoot,
|
|
11627
|
-
sandboxName,
|
|
11628
|
-
"codex"
|
|
11629
|
-
];
|
|
11630
|
-
} else {
|
|
11631
|
-
process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
|
|
11377
|
+
}
|
|
11378
|
+
process.stderr.write(`Connecting to ${agent} sandbox ${dim(sandboxName)}...
|
|
11632
11379
|
`);
|
|
11633
|
-
|
|
11380
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11634
11381
|
|
|
11635
11382
|
`);
|
|
11636
|
-
|
|
11637
|
-
}
|
|
11638
|
-
const child = spawn6("docker", dockerArgs, {
|
|
11383
|
+
const child = spawn6("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
|
|
11639
11384
|
stdio: "inherit"
|
|
11640
11385
|
});
|
|
11641
11386
|
await new Promise((resolve2) => {
|
|
@@ -11661,25 +11406,30 @@ ${yellow("⚠")} ${agent} exited with code ${code}.
|
|
|
11661
11406
|
}
|
|
11662
11407
|
function handleRemove(projectRoot) {
|
|
11663
11408
|
const config = loadConfig(projectRoot);
|
|
11664
|
-
|
|
11665
|
-
|
|
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.")}
|
|
11666
11415
|
`);
|
|
11667
11416
|
return;
|
|
11668
11417
|
}
|
|
11669
|
-
const sandboxName
|
|
11670
|
-
|
|
11418
|
+
for (const sandboxName of names) {
|
|
11419
|
+
process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
|
|
11671
11420
|
`);
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
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 = {};
|
|
11680
11430
|
config.sandbox.enabled = false;
|
|
11681
11431
|
saveConfig(projectRoot, config);
|
|
11682
|
-
process.stderr.write(`${green("✓")}
|
|
11432
|
+
process.stderr.write(`${green("✓")} Provider sandboxes removed. Sandbox mode disabled.
|
|
11683
11433
|
`);
|
|
11684
11434
|
}
|
|
11685
11435
|
function handleStatus(projectRoot) {
|
|
@@ -11690,24 +11440,55 @@ ${bold("Sandbox Status")}
|
|
|
11690
11440
|
`);
|
|
11691
11441
|
process.stderr.write(` ${dim("Enabled:")} ${config.sandbox.enabled ? green("yes") : red("no")}
|
|
11692
11442
|
`);
|
|
11693
|
-
|
|
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)")}
|
|
11694
11446
|
`);
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
`);
|
|
11699
|
-
if (!alive) {
|
|
11700
|
-
process.stderr.write(`
|
|
11701
|
-
${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")}
|
|
11702
11450
|
`);
|
|
11703
11451
|
}
|
|
11704
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
|
+
}
|
|
11705
11458
|
process.stderr.write(`
|
|
11706
11459
|
`);
|
|
11707
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
|
+
}
|
|
11708
11489
|
async function ensureCodexInSandbox(sandboxName) {
|
|
11709
11490
|
try {
|
|
11710
|
-
|
|
11491
|
+
execSync17(`docker sandbox exec ${sandboxName} which codex`, {
|
|
11711
11492
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11712
11493
|
timeout: 5000
|
|
11713
11494
|
});
|
|
@@ -11715,7 +11496,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11715
11496
|
process.stderr.write(`Installing codex in sandbox...
|
|
11716
11497
|
`);
|
|
11717
11498
|
try {
|
|
11718
|
-
|
|
11499
|
+
execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
11719
11500
|
} catch {
|
|
11720
11501
|
process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
|
|
11721
11502
|
`);
|
|
@@ -11724,7 +11505,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11724
11505
|
}
|
|
11725
11506
|
function isSandboxAlive(name) {
|
|
11726
11507
|
try {
|
|
11727
|
-
const output =
|
|
11508
|
+
const output = execSync17("docker sandbox ls", {
|
|
11728
11509
|
encoding: "utf-8",
|
|
11729
11510
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11730
11511
|
timeout: 5000
|
|
@@ -11734,11 +11515,13 @@ function isSandboxAlive(name) {
|
|
|
11734
11515
|
return false;
|
|
11735
11516
|
}
|
|
11736
11517
|
}
|
|
11518
|
+
var PROVIDERS;
|
|
11737
11519
|
var init_sandbox2 = __esm(() => {
|
|
11738
11520
|
init_config();
|
|
11739
11521
|
init_sandbox();
|
|
11740
11522
|
init_sandbox_ignore();
|
|
11741
11523
|
init_terminal();
|
|
11524
|
+
PROVIDERS = ["claude", "codex"];
|
|
11742
11525
|
});
|
|
11743
11526
|
|
|
11744
11527
|
// src/cli.ts
|