@locusai/cli 0.18.2 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/locus.js +1260 -1181
- package/package.json +3 -3
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})`)}
|
|
7774
|
+
`);
|
|
7775
|
+
} else {
|
|
7776
|
+
sandboxRunner = null;
|
|
7777
|
+
process.stderr.write(`${yellow("⚠")} ${dim(`No sandbox configured for ${inferredProvider}. Run locus sandbox.`)}
|
|
8210
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();
|
|
@@ -8360,7 +7926,6 @@ __export(exports_exec, {
|
|
|
8360
7926
|
});
|
|
8361
7927
|
async function execCommand(projectRoot, args, flags = {}) {
|
|
8362
7928
|
const config = loadConfig(projectRoot);
|
|
8363
|
-
const _log = getLogger();
|
|
8364
7929
|
if (args[0] === "sessions") {
|
|
8365
7930
|
return handleSessionSubcommand(projectRoot, args.slice(1));
|
|
8366
7931
|
}
|
|
@@ -8483,7 +8048,12 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
|
|
|
8483
8048
|
stream.emitStatus("thinking");
|
|
8484
8049
|
try {
|
|
8485
8050
|
const fullPrompt = buildReplPrompt(prompt, projectRoot, config);
|
|
8486
|
-
const
|
|
8051
|
+
const sandboxName = getProviderSandboxName(config.sandbox, config.ai.provider);
|
|
8052
|
+
const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName) : null : await createRunnerAsync(config.ai.provider, false);
|
|
8053
|
+
if (!runner) {
|
|
8054
|
+
stream.emitError(`Sandbox for provider "${config.ai.provider}" is not configured. Run locus sandbox.`, false);
|
|
8055
|
+
return;
|
|
8056
|
+
}
|
|
8487
8057
|
const available = await runner.isAvailable();
|
|
8488
8058
|
if (!available) {
|
|
8489
8059
|
stream.emitError(`${config.ai.provider} CLI not available`, false);
|
|
@@ -8523,15 +8093,15 @@ function formatAge(isoDate) {
|
|
|
8523
8093
|
var init_exec = __esm(() => {
|
|
8524
8094
|
init_runner();
|
|
8525
8095
|
init_config();
|
|
8526
|
-
init_logger();
|
|
8527
8096
|
init_prompt_builder();
|
|
8097
|
+
init_sandbox();
|
|
8528
8098
|
init_terminal();
|
|
8529
8099
|
init_repl();
|
|
8530
8100
|
init_session_manager();
|
|
8531
8101
|
});
|
|
8532
8102
|
|
|
8533
8103
|
// src/core/agent.ts
|
|
8534
|
-
import { execSync as
|
|
8104
|
+
import { execSync as execSync10 } from "node:child_process";
|
|
8535
8105
|
async function executeIssue(projectRoot, options) {
|
|
8536
8106
|
const log = getLogger();
|
|
8537
8107
|
const timer = createTimer();
|
|
@@ -8560,7 +8130,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
|
|
|
8560
8130
|
}
|
|
8561
8131
|
let issueComments = [];
|
|
8562
8132
|
try {
|
|
8563
|
-
const commentsRaw =
|
|
8133
|
+
const commentsRaw = execSync10(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8564
8134
|
if (commentsRaw) {
|
|
8565
8135
|
issueComments = commentsRaw.split(`
|
|
8566
8136
|
`).filter(Boolean);
|
|
@@ -8703,7 +8273,7 @@ ${c.body}`),
|
|
|
8703
8273
|
cwd: projectRoot,
|
|
8704
8274
|
activity: `iterating on PR #${prNumber}`,
|
|
8705
8275
|
sandboxed: config.sandbox.enabled,
|
|
8706
|
-
sandboxName: config.sandbox.
|
|
8276
|
+
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
|
|
8707
8277
|
});
|
|
8708
8278
|
if (aiResult.interrupted) {
|
|
8709
8279
|
process.stderr.write(`
|
|
@@ -8724,12 +8294,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
|
|
|
8724
8294
|
}
|
|
8725
8295
|
async function createIssuePR(projectRoot, config, issue) {
|
|
8726
8296
|
try {
|
|
8727
|
-
const currentBranch =
|
|
8297
|
+
const currentBranch = execSync10("git rev-parse --abbrev-ref HEAD", {
|
|
8728
8298
|
cwd: projectRoot,
|
|
8729
8299
|
encoding: "utf-8",
|
|
8730
8300
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8731
8301
|
}).trim();
|
|
8732
|
-
const diff =
|
|
8302
|
+
const diff = execSync10(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8733
8303
|
cwd: projectRoot,
|
|
8734
8304
|
encoding: "utf-8",
|
|
8735
8305
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8738,7 +8308,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
8738
8308
|
getLogger().verbose("No changes to create PR for");
|
|
8739
8309
|
return;
|
|
8740
8310
|
}
|
|
8741
|
-
|
|
8311
|
+
execSync10(`git push -u origin ${currentBranch}`, {
|
|
8742
8312
|
cwd: projectRoot,
|
|
8743
8313
|
encoding: "utf-8",
|
|
8744
8314
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8781,12 +8351,13 @@ var init_agent = __esm(() => {
|
|
|
8781
8351
|
init_github();
|
|
8782
8352
|
init_logger();
|
|
8783
8353
|
init_prompt_builder();
|
|
8354
|
+
init_sandbox();
|
|
8784
8355
|
});
|
|
8785
8356
|
|
|
8786
8357
|
// src/core/conflict.ts
|
|
8787
|
-
import { execSync as
|
|
8358
|
+
import { execSync as execSync11 } from "node:child_process";
|
|
8788
8359
|
function git2(args, cwd) {
|
|
8789
|
-
return
|
|
8360
|
+
return execSync11(`git ${args}`, {
|
|
8790
8361
|
cwd,
|
|
8791
8362
|
encoding: "utf-8",
|
|
8792
8363
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8906,17 +8477,201 @@ ${bold(yellow("⚠"))} Base branch has ${result.newCommits} new commit${result.n
|
|
|
8906
8477
|
`);
|
|
8907
8478
|
}
|
|
8908
8479
|
}
|
|
8909
|
-
var init_conflict = __esm(() => {
|
|
8910
|
-
init_terminal();
|
|
8911
|
-
init_logger();
|
|
8480
|
+
var init_conflict = __esm(() => {
|
|
8481
|
+
init_terminal();
|
|
8482
|
+
init_logger();
|
|
8483
|
+
});
|
|
8484
|
+
|
|
8485
|
+
// src/core/run-state.ts
|
|
8486
|
+
import {
|
|
8487
|
+
existsSync as existsSync16,
|
|
8488
|
+
mkdirSync as mkdirSync11,
|
|
8489
|
+
readFileSync as readFileSync12,
|
|
8490
|
+
unlinkSync as unlinkSync4,
|
|
8491
|
+
writeFileSync as writeFileSync8
|
|
8492
|
+
} from "node:fs";
|
|
8493
|
+
import { dirname as dirname5, join as join16 } from "node:path";
|
|
8494
|
+
function getRunStatePath(projectRoot) {
|
|
8495
|
+
return join16(projectRoot, ".locus", "run-state.json");
|
|
8496
|
+
}
|
|
8497
|
+
function loadRunState(projectRoot) {
|
|
8498
|
+
const path = getRunStatePath(projectRoot);
|
|
8499
|
+
if (!existsSync16(path))
|
|
8500
|
+
return null;
|
|
8501
|
+
try {
|
|
8502
|
+
return JSON.parse(readFileSync12(path, "utf-8"));
|
|
8503
|
+
} catch {
|
|
8504
|
+
getLogger().warn("Corrupted run-state.json, ignoring");
|
|
8505
|
+
return null;
|
|
8506
|
+
}
|
|
8507
|
+
}
|
|
8508
|
+
function saveRunState(projectRoot, state) {
|
|
8509
|
+
const path = getRunStatePath(projectRoot);
|
|
8510
|
+
const dir = dirname5(path);
|
|
8511
|
+
if (!existsSync16(dir)) {
|
|
8512
|
+
mkdirSync11(dir, { recursive: true });
|
|
8513
|
+
}
|
|
8514
|
+
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
8515
|
+
`, "utf-8");
|
|
8516
|
+
}
|
|
8517
|
+
function clearRunState(projectRoot) {
|
|
8518
|
+
const path = getRunStatePath(projectRoot);
|
|
8519
|
+
if (existsSync16(path)) {
|
|
8520
|
+
unlinkSync4(path);
|
|
8521
|
+
}
|
|
8522
|
+
}
|
|
8523
|
+
function createSprintRunState(sprint, branch, issues) {
|
|
8524
|
+
return {
|
|
8525
|
+
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
8526
|
+
type: "sprint",
|
|
8527
|
+
sprint,
|
|
8528
|
+
branch,
|
|
8529
|
+
startedAt: new Date().toISOString(),
|
|
8530
|
+
tasks: issues.map(({ number, order }) => ({
|
|
8531
|
+
issue: number,
|
|
8532
|
+
order,
|
|
8533
|
+
status: "pending"
|
|
8534
|
+
}))
|
|
8535
|
+
};
|
|
8536
|
+
}
|
|
8537
|
+
function createParallelRunState(issueNumbers) {
|
|
8538
|
+
return {
|
|
8539
|
+
runId: `run-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-")}`,
|
|
8540
|
+
type: "parallel",
|
|
8541
|
+
startedAt: new Date().toISOString(),
|
|
8542
|
+
tasks: issueNumbers.map((issue, i) => ({
|
|
8543
|
+
issue,
|
|
8544
|
+
order: i + 1,
|
|
8545
|
+
status: "pending"
|
|
8546
|
+
}))
|
|
8547
|
+
};
|
|
8548
|
+
}
|
|
8549
|
+
function markTaskInProgress(state, issueNumber) {
|
|
8550
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
8551
|
+
if (task) {
|
|
8552
|
+
task.status = "in_progress";
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
function markTaskDone(state, issueNumber, prNumber) {
|
|
8556
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
8557
|
+
if (task) {
|
|
8558
|
+
task.status = "done";
|
|
8559
|
+
task.completedAt = new Date().toISOString();
|
|
8560
|
+
if (prNumber)
|
|
8561
|
+
task.pr = prNumber;
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
function markTaskFailed(state, issueNumber, error) {
|
|
8565
|
+
const task = state.tasks.find((t) => t.issue === issueNumber);
|
|
8566
|
+
if (task) {
|
|
8567
|
+
task.status = "failed";
|
|
8568
|
+
task.failedAt = new Date().toISOString();
|
|
8569
|
+
task.error = error;
|
|
8570
|
+
}
|
|
8571
|
+
}
|
|
8572
|
+
function getRunStats(state) {
|
|
8573
|
+
const tasks = state.tasks;
|
|
8574
|
+
return {
|
|
8575
|
+
total: tasks.length,
|
|
8576
|
+
done: tasks.filter((t) => t.status === "done").length,
|
|
8577
|
+
failed: tasks.filter((t) => t.status === "failed").length,
|
|
8578
|
+
pending: tasks.filter((t) => t.status === "pending").length,
|
|
8579
|
+
inProgress: tasks.filter((t) => t.status === "in_progress").length
|
|
8580
|
+
};
|
|
8581
|
+
}
|
|
8582
|
+
function getNextTask(state) {
|
|
8583
|
+
const failed = state.tasks.find((t) => t.status === "failed");
|
|
8584
|
+
if (failed)
|
|
8585
|
+
return failed;
|
|
8586
|
+
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
8587
|
+
}
|
|
8588
|
+
var init_run_state = __esm(() => {
|
|
8589
|
+
init_logger();
|
|
8590
|
+
});
|
|
8591
|
+
|
|
8592
|
+
// src/core/shutdown.ts
|
|
8593
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
8594
|
+
function cleanupActiveSandboxes() {
|
|
8595
|
+
for (const name of activeSandboxes) {
|
|
8596
|
+
try {
|
|
8597
|
+
execSync12(`docker sandbox rm ${name}`, { timeout: 1e4 });
|
|
8598
|
+
} catch {}
|
|
8599
|
+
}
|
|
8600
|
+
activeSandboxes.clear();
|
|
8601
|
+
}
|
|
8602
|
+
function registerShutdownHandlers(ctx) {
|
|
8603
|
+
shutdownContext = ctx;
|
|
8604
|
+
interruptCount = 0;
|
|
8605
|
+
const handler = () => {
|
|
8606
|
+
interruptCount++;
|
|
8607
|
+
if (interruptCount >= 2) {
|
|
8608
|
+
process.stderr.write(`
|
|
8609
|
+
Force exit.
|
|
8610
|
+
`);
|
|
8611
|
+
process.exit(1);
|
|
8612
|
+
}
|
|
8613
|
+
process.stderr.write(`
|
|
8614
|
+
|
|
8615
|
+
Interrupted. Saving state...
|
|
8616
|
+
`);
|
|
8617
|
+
const state = shutdownContext?.getRunState?.();
|
|
8618
|
+
if (state && shutdownContext) {
|
|
8619
|
+
for (const task of state.tasks) {
|
|
8620
|
+
if (task.status === "in_progress") {
|
|
8621
|
+
task.status = "failed";
|
|
8622
|
+
task.failedAt = new Date().toISOString();
|
|
8623
|
+
task.error = "Interrupted by user";
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
try {
|
|
8627
|
+
saveRunState(shutdownContext.projectRoot, state);
|
|
8628
|
+
process.stderr.write(`State saved. Resume with: locus run --resume
|
|
8629
|
+
`);
|
|
8630
|
+
} catch {
|
|
8631
|
+
process.stderr.write(`Warning: Could not save run state.
|
|
8632
|
+
`);
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8635
|
+
cleanupActiveSandboxes();
|
|
8636
|
+
shutdownContext?.onShutdown?.();
|
|
8637
|
+
if (interruptTimer)
|
|
8638
|
+
clearTimeout(interruptTimer);
|
|
8639
|
+
interruptTimer = setTimeout(() => {
|
|
8640
|
+
interruptCount = 0;
|
|
8641
|
+
}, 2000);
|
|
8642
|
+
setTimeout(() => {
|
|
8643
|
+
process.exit(130);
|
|
8644
|
+
}, 100);
|
|
8645
|
+
};
|
|
8646
|
+
if (!shutdownRegistered) {
|
|
8647
|
+
process.on("SIGINT", handler);
|
|
8648
|
+
process.on("SIGTERM", handler);
|
|
8649
|
+
shutdownRegistered = true;
|
|
8650
|
+
}
|
|
8651
|
+
return () => {
|
|
8652
|
+
process.removeListener("SIGINT", handler);
|
|
8653
|
+
process.removeListener("SIGTERM", handler);
|
|
8654
|
+
shutdownRegistered = false;
|
|
8655
|
+
shutdownContext = null;
|
|
8656
|
+
interruptCount = 0;
|
|
8657
|
+
if (interruptTimer) {
|
|
8658
|
+
clearTimeout(interruptTimer);
|
|
8659
|
+
interruptTimer = null;
|
|
8660
|
+
}
|
|
8661
|
+
};
|
|
8662
|
+
}
|
|
8663
|
+
var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
|
|
8664
|
+
var init_shutdown = __esm(() => {
|
|
8665
|
+
init_run_state();
|
|
8666
|
+
activeSandboxes = new Set;
|
|
8912
8667
|
});
|
|
8913
8668
|
|
|
8914
8669
|
// src/core/worktree.ts
|
|
8915
|
-
import { execSync as
|
|
8670
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
8916
8671
|
import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
|
|
8917
8672
|
import { join as join17 } from "node:path";
|
|
8918
8673
|
function git3(args, cwd) {
|
|
8919
|
-
return
|
|
8674
|
+
return execSync13(`git ${args}`, {
|
|
8920
8675
|
cwd,
|
|
8921
8676
|
encoding: "utf-8",
|
|
8922
8677
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8941,7 +8696,7 @@ function generateBranchName(issueNumber) {
|
|
|
8941
8696
|
}
|
|
8942
8697
|
function getWorktreeBranch(worktreePath) {
|
|
8943
8698
|
try {
|
|
8944
|
-
return
|
|
8699
|
+
return execSync13("git branch --show-current", {
|
|
8945
8700
|
cwd: worktreePath,
|
|
8946
8701
|
encoding: "utf-8",
|
|
8947
8702
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9066,7 +8821,13 @@ var exports_run = {};
|
|
|
9066
8821
|
__export(exports_run, {
|
|
9067
8822
|
runCommand: () => runCommand
|
|
9068
8823
|
});
|
|
9069
|
-
import { execSync as
|
|
8824
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
8825
|
+
function resolveExecutionContext(config, modelOverride) {
|
|
8826
|
+
const model = modelOverride ?? config.ai.model;
|
|
8827
|
+
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
8828
|
+
const sandboxName = getModelSandboxName(config.sandbox, model, provider);
|
|
8829
|
+
return { provider, model, sandboxName };
|
|
8830
|
+
}
|
|
9070
8831
|
function printRunHelp() {
|
|
9071
8832
|
process.stderr.write(`
|
|
9072
8833
|
${bold("locus run")} — Execute issues using AI agents
|
|
@@ -9149,6 +8910,7 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
9149
8910
|
}
|
|
9150
8911
|
async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
9151
8912
|
const log = getLogger();
|
|
8913
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9152
8914
|
if (!config.sprint.active) {
|
|
9153
8915
|
process.stderr.write(`${red("✗")} No active sprint. Set one with: ${bold("locus sprint active <name>")}
|
|
9154
8916
|
`);
|
|
@@ -9210,7 +8972,7 @@ ${yellow("⚠")} A sprint run is already in progress.
|
|
|
9210
8972
|
}
|
|
9211
8973
|
if (!flags.dryRun) {
|
|
9212
8974
|
try {
|
|
9213
|
-
|
|
8975
|
+
execSync14(`git checkout -B ${branchName}`, {
|
|
9214
8976
|
cwd: projectRoot,
|
|
9215
8977
|
encoding: "utf-8",
|
|
9216
8978
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9260,7 +9022,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
|
|
|
9260
9022
|
let sprintContext;
|
|
9261
9023
|
if (i > 0 && !flags.dryRun) {
|
|
9262
9024
|
try {
|
|
9263
|
-
sprintContext =
|
|
9025
|
+
sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
9264
9026
|
cwd: projectRoot,
|
|
9265
9027
|
encoding: "utf-8",
|
|
9266
9028
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9275,12 +9037,13 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
9275
9037
|
saveRunState(projectRoot, state);
|
|
9276
9038
|
const result = await executeIssue(projectRoot, {
|
|
9277
9039
|
issueNumber: task.issue,
|
|
9278
|
-
provider:
|
|
9279
|
-
model:
|
|
9040
|
+
provider: execution.provider,
|
|
9041
|
+
model: execution.model,
|
|
9280
9042
|
dryRun: flags.dryRun,
|
|
9281
9043
|
sprintContext,
|
|
9282
9044
|
skipPR: true,
|
|
9283
|
-
sandboxed
|
|
9045
|
+
sandboxed,
|
|
9046
|
+
sandboxName: execution.sandboxName
|
|
9284
9047
|
});
|
|
9285
9048
|
if (result.success) {
|
|
9286
9049
|
if (!flags.dryRun) {
|
|
@@ -9324,7 +9087,7 @@ ${bold("Summary:")}
|
|
|
9324
9087
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
9325
9088
|
if (prNumber !== undefined) {
|
|
9326
9089
|
try {
|
|
9327
|
-
|
|
9090
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9328
9091
|
cwd: projectRoot,
|
|
9329
9092
|
encoding: "utf-8",
|
|
9330
9093
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9339,6 +9102,7 @@ ${bold("Summary:")}
|
|
|
9339
9102
|
}
|
|
9340
9103
|
}
|
|
9341
9104
|
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
9105
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9342
9106
|
let isSprintIssue = false;
|
|
9343
9107
|
try {
|
|
9344
9108
|
const issue = getIssue(issueNumber, { cwd: projectRoot });
|
|
@@ -9351,11 +9115,11 @@ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, n
|
|
|
9351
9115
|
`);
|
|
9352
9116
|
await executeIssue(projectRoot, {
|
|
9353
9117
|
issueNumber,
|
|
9354
|
-
provider:
|
|
9355
|
-
model:
|
|
9118
|
+
provider: execution.provider,
|
|
9119
|
+
model: execution.model,
|
|
9356
9120
|
dryRun: flags.dryRun,
|
|
9357
9121
|
sandboxed,
|
|
9358
|
-
sandboxName:
|
|
9122
|
+
sandboxName: execution.sandboxName
|
|
9359
9123
|
});
|
|
9360
9124
|
return;
|
|
9361
9125
|
}
|
|
@@ -9382,11 +9146,11 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
9382
9146
|
const result = await executeIssue(projectRoot, {
|
|
9383
9147
|
issueNumber,
|
|
9384
9148
|
worktreePath,
|
|
9385
|
-
provider:
|
|
9386
|
-
model:
|
|
9149
|
+
provider: execution.provider,
|
|
9150
|
+
model: execution.model,
|
|
9387
9151
|
dryRun: flags.dryRun,
|
|
9388
9152
|
sandboxed,
|
|
9389
|
-
sandboxName:
|
|
9153
|
+
sandboxName: execution.sandboxName
|
|
9390
9154
|
});
|
|
9391
9155
|
if (worktreePath && !flags.dryRun) {
|
|
9392
9156
|
if (result.success) {
|
|
@@ -9401,6 +9165,7 @@ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
|
|
|
9401
9165
|
}
|
|
9402
9166
|
async function handleParallelRun(projectRoot, config, issueNumbers, flags, sandboxed) {
|
|
9403
9167
|
const log = getLogger();
|
|
9168
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
9404
9169
|
const maxConcurrent = config.agent.maxParallel;
|
|
9405
9170
|
process.stderr.write(`
|
|
9406
9171
|
${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxConcurrent} parallel, worktrees)`)}
|
|
@@ -9452,11 +9217,11 @@ ${bold("Running")} ${cyan(`${issueNumbers.length} issues`)} ${dim(`(max ${maxCon
|
|
|
9452
9217
|
const result = await executeIssue(projectRoot, {
|
|
9453
9218
|
issueNumber,
|
|
9454
9219
|
worktreePath,
|
|
9455
|
-
provider:
|
|
9456
|
-
model:
|
|
9220
|
+
provider: execution.provider,
|
|
9221
|
+
model: execution.model,
|
|
9457
9222
|
dryRun: flags.dryRun,
|
|
9458
9223
|
sandboxed,
|
|
9459
|
-
sandboxName:
|
|
9224
|
+
sandboxName: execution.sandboxName
|
|
9460
9225
|
});
|
|
9461
9226
|
if (result.success) {
|
|
9462
9227
|
markTaskDone(state, issueNumber, result.prNumber);
|
|
@@ -9506,6 +9271,7 @@ ${yellow("⚠")} Failed worktrees preserved for debugging:
|
|
|
9506
9271
|
}
|
|
9507
9272
|
}
|
|
9508
9273
|
async function handleResume(projectRoot, config, sandboxed) {
|
|
9274
|
+
const execution = resolveExecutionContext(config);
|
|
9509
9275
|
const state = loadRunState(projectRoot);
|
|
9510
9276
|
if (!state) {
|
|
9511
9277
|
process.stderr.write(`${red("✗")} No run state found. Nothing to resume.
|
|
@@ -9521,13 +9287,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9521
9287
|
`);
|
|
9522
9288
|
if (state.type === "sprint" && state.branch) {
|
|
9523
9289
|
try {
|
|
9524
|
-
const currentBranch =
|
|
9290
|
+
const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
|
|
9525
9291
|
cwd: projectRoot,
|
|
9526
9292
|
encoding: "utf-8",
|
|
9527
9293
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9528
9294
|
}).trim();
|
|
9529
9295
|
if (currentBranch !== state.branch) {
|
|
9530
|
-
|
|
9296
|
+
execSync14(`git checkout ${state.branch}`, {
|
|
9531
9297
|
cwd: projectRoot,
|
|
9532
9298
|
encoding: "utf-8",
|
|
9533
9299
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9551,11 +9317,11 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9551
9317
|
saveRunState(projectRoot, state);
|
|
9552
9318
|
const result = await executeIssue(projectRoot, {
|
|
9553
9319
|
issueNumber: task.issue,
|
|
9554
|
-
provider:
|
|
9555
|
-
model:
|
|
9320
|
+
provider: execution.provider,
|
|
9321
|
+
model: execution.model,
|
|
9556
9322
|
skipPR: isSprintRun,
|
|
9557
9323
|
sandboxed,
|
|
9558
|
-
sandboxName:
|
|
9324
|
+
sandboxName: execution.sandboxName
|
|
9559
9325
|
});
|
|
9560
9326
|
if (result.success) {
|
|
9561
9327
|
if (isSprintRun) {
|
|
@@ -9594,7 +9360,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
|
|
|
9594
9360
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
9595
9361
|
if (prNumber !== undefined) {
|
|
9596
9362
|
try {
|
|
9597
|
-
|
|
9363
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9598
9364
|
cwd: projectRoot,
|
|
9599
9365
|
encoding: "utf-8",
|
|
9600
9366
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9625,14 +9391,14 @@ function getOrder2(issue) {
|
|
|
9625
9391
|
}
|
|
9626
9392
|
function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
9627
9393
|
try {
|
|
9628
|
-
const status =
|
|
9394
|
+
const status = execSync14("git status --porcelain", {
|
|
9629
9395
|
cwd: projectRoot,
|
|
9630
9396
|
encoding: "utf-8",
|
|
9631
9397
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9632
9398
|
}).trim();
|
|
9633
9399
|
if (!status)
|
|
9634
9400
|
return;
|
|
9635
|
-
|
|
9401
|
+
execSync14("git add -A", {
|
|
9636
9402
|
cwd: projectRoot,
|
|
9637
9403
|
encoding: "utf-8",
|
|
9638
9404
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9640,7 +9406,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
9640
9406
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
9641
9407
|
|
|
9642
9408
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
9643
|
-
|
|
9409
|
+
execSync14(`git commit -F -`, {
|
|
9644
9410
|
input: message,
|
|
9645
9411
|
cwd: projectRoot,
|
|
9646
9412
|
encoding: "utf-8",
|
|
@@ -9654,7 +9420,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9654
9420
|
if (!config.agent.autoPR)
|
|
9655
9421
|
return;
|
|
9656
9422
|
try {
|
|
9657
|
-
const diff =
|
|
9423
|
+
const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
9658
9424
|
cwd: projectRoot,
|
|
9659
9425
|
encoding: "utf-8",
|
|
9660
9426
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9664,7 +9430,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9664
9430
|
`);
|
|
9665
9431
|
return;
|
|
9666
9432
|
}
|
|
9667
|
-
|
|
9433
|
+
execSync14(`git push -u origin ${branchName}`, {
|
|
9668
9434
|
cwd: projectRoot,
|
|
9669
9435
|
encoding: "utf-8",
|
|
9670
9436
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9690,6 +9456,7 @@ ${taskLines}
|
|
|
9690
9456
|
}
|
|
9691
9457
|
}
|
|
9692
9458
|
var init_run = __esm(() => {
|
|
9459
|
+
init_ai_models();
|
|
9693
9460
|
init_agent();
|
|
9694
9461
|
init_config();
|
|
9695
9462
|
init_conflict();
|
|
@@ -10066,7 +9833,7 @@ ${bold("Planning:")} ${cyan(displayDirective)}
|
|
|
10066
9833
|
cwd: projectRoot,
|
|
10067
9834
|
activity: "planning",
|
|
10068
9835
|
sandboxed: config.sandbox.enabled,
|
|
10069
|
-
sandboxName: config.sandbox.
|
|
9836
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10070
9837
|
});
|
|
10071
9838
|
if (aiResult.interrupted) {
|
|
10072
9839
|
process.stderr.write(`
|
|
@@ -10181,7 +9948,7 @@ Start with foundational/setup tasks, then core features, then integration/testin
|
|
|
10181
9948
|
activity: "issue ordering",
|
|
10182
9949
|
silent: true,
|
|
10183
9950
|
sandboxed: config.sandbox.enabled,
|
|
10184
|
-
sandboxName: config.sandbox.
|
|
9951
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10185
9952
|
});
|
|
10186
9953
|
if (aiResult.interrupted) {
|
|
10187
9954
|
process.stderr.write(`
|
|
@@ -10433,6 +10200,7 @@ var init_plan = __esm(() => {
|
|
|
10433
10200
|
init_config();
|
|
10434
10201
|
init_github();
|
|
10435
10202
|
init_terminal();
|
|
10203
|
+
init_sandbox();
|
|
10436
10204
|
});
|
|
10437
10205
|
|
|
10438
10206
|
// src/commands/review.ts
|
|
@@ -10440,7 +10208,7 @@ var exports_review = {};
|
|
|
10440
10208
|
__export(exports_review, {
|
|
10441
10209
|
reviewCommand: () => reviewCommand
|
|
10442
10210
|
});
|
|
10443
|
-
import { execSync as
|
|
10211
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
10444
10212
|
import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
|
|
10445
10213
|
import { join as join19 } from "node:path";
|
|
10446
10214
|
function printHelp2() {
|
|
@@ -10518,7 +10286,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
|
|
|
10518
10286
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
10519
10287
|
let prInfo;
|
|
10520
10288
|
try {
|
|
10521
|
-
const result =
|
|
10289
|
+
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
10290
|
const raw = JSON.parse(result);
|
|
10523
10291
|
prInfo = {
|
|
10524
10292
|
number: raw.number,
|
|
@@ -10563,7 +10331,7 @@ async function reviewPR(projectRoot, config, pr, focus, flags) {
|
|
|
10563
10331
|
cwd: projectRoot,
|
|
10564
10332
|
activity: `PR #${pr.number}`,
|
|
10565
10333
|
sandboxed: config.sandbox.enabled,
|
|
10566
|
-
sandboxName: config.sandbox.
|
|
10334
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
10567
10335
|
});
|
|
10568
10336
|
if (aiResult.interrupted) {
|
|
10569
10337
|
process.stderr.write(` ${yellow("⚡")} Review interrupted.
|
|
@@ -10584,7 +10352,7 @@ ${output.slice(0, 60000)}
|
|
|
10584
10352
|
|
|
10585
10353
|
---
|
|
10586
10354
|
_Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
|
|
10587
|
-
|
|
10355
|
+
execSync15(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10588
10356
|
process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
|
|
10589
10357
|
`);
|
|
10590
10358
|
} catch (e) {
|
|
@@ -10654,6 +10422,7 @@ var init_review = __esm(() => {
|
|
|
10654
10422
|
init_run_ai();
|
|
10655
10423
|
init_config();
|
|
10656
10424
|
init_github();
|
|
10425
|
+
init_sandbox();
|
|
10657
10426
|
init_progress();
|
|
10658
10427
|
init_terminal();
|
|
10659
10428
|
});
|
|
@@ -10663,7 +10432,7 @@ var exports_iterate = {};
|
|
|
10663
10432
|
__export(exports_iterate, {
|
|
10664
10433
|
iterateCommand: () => iterateCommand
|
|
10665
10434
|
});
|
|
10666
|
-
import { execSync as
|
|
10435
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
10667
10436
|
function printHelp3() {
|
|
10668
10437
|
process.stderr.write(`
|
|
10669
10438
|
${bold("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -10873,12 +10642,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
|
|
|
10873
10642
|
}
|
|
10874
10643
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
10875
10644
|
try {
|
|
10876
|
-
const result =
|
|
10645
|
+
const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10877
10646
|
const parsed = JSON.parse(result);
|
|
10878
10647
|
if (parsed.length > 0) {
|
|
10879
10648
|
return parsed[0].number;
|
|
10880
10649
|
}
|
|
10881
|
-
const branchResult =
|
|
10650
|
+
const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10882
10651
|
const branchParsed = JSON.parse(branchResult);
|
|
10883
10652
|
if (branchParsed.length > 0) {
|
|
10884
10653
|
return branchParsed[0].number;
|
|
@@ -11123,7 +10892,7 @@ ${bold("Discussion:")} ${cyan(topic)}
|
|
|
11123
10892
|
cwd: projectRoot,
|
|
11124
10893
|
activity: "discussion",
|
|
11125
10894
|
sandboxed: config.sandbox.enabled,
|
|
11126
|
-
sandboxName: config.sandbox.
|
|
10895
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
11127
10896
|
});
|
|
11128
10897
|
if (aiResult.interrupted) {
|
|
11129
10898
|
process.stderr.write(`
|
|
@@ -11269,6 +11038,7 @@ var MAX_DISCUSSION_ROUNDS = 5;
|
|
|
11269
11038
|
var init_discuss = __esm(() => {
|
|
11270
11039
|
init_run_ai();
|
|
11271
11040
|
init_config();
|
|
11041
|
+
init_sandbox();
|
|
11272
11042
|
init_progress();
|
|
11273
11043
|
init_terminal();
|
|
11274
11044
|
init_input_handler();
|
|
@@ -11491,24 +11261,34 @@ var init_artifacts = __esm(() => {
|
|
|
11491
11261
|
// src/commands/sandbox.ts
|
|
11492
11262
|
var exports_sandbox2 = {};
|
|
11493
11263
|
__export(exports_sandbox2, {
|
|
11494
|
-
sandboxCommand: () => sandboxCommand
|
|
11264
|
+
sandboxCommand: () => sandboxCommand,
|
|
11265
|
+
parseSandboxLogsArgs: () => parseSandboxLogsArgs,
|
|
11266
|
+
parseSandboxInstallArgs: () => parseSandboxInstallArgs,
|
|
11267
|
+
parseSandboxExecArgs: () => parseSandboxExecArgs
|
|
11495
11268
|
});
|
|
11496
|
-
import { execSync as
|
|
11269
|
+
import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
|
|
11270
|
+
import { createHash } from "node:crypto";
|
|
11271
|
+
import { basename as basename4 } from "node:path";
|
|
11497
11272
|
function printSandboxHelp() {
|
|
11498
11273
|
process.stderr.write(`
|
|
11499
11274
|
${bold("locus sandbox")} — Manage Docker sandbox lifecycle
|
|
11500
11275
|
|
|
11501
11276
|
${bold("Usage:")}
|
|
11502
|
-
locus sandbox ${dim("# Create
|
|
11277
|
+
locus sandbox ${dim("# Create claude/codex sandboxes and enable sandbox mode")}
|
|
11503
11278
|
locus sandbox claude ${dim("# Run claude interactively (for login)")}
|
|
11504
11279
|
locus sandbox codex ${dim("# Run codex interactively (for login)")}
|
|
11505
|
-
locus sandbox
|
|
11280
|
+
locus sandbox install <pkg> ${dim("# npm install -g package(s) in sandbox(es)")}
|
|
11281
|
+
locus sandbox exec <provider> -- <cmd...> ${dim("# Run one command inside provider sandbox")}
|
|
11282
|
+
locus sandbox shell <provider> ${dim("# Open interactive shell in provider sandbox")}
|
|
11283
|
+
locus sandbox logs <provider> ${dim("# Show provider sandbox logs")}
|
|
11284
|
+
locus sandbox rm ${dim("# Destroy all provider sandboxes and disable sandbox mode")}
|
|
11506
11285
|
locus sandbox status ${dim("# Show current sandbox state")}
|
|
11507
11286
|
|
|
11508
11287
|
${bold("Flow:")}
|
|
11509
|
-
1. ${cyan("locus sandbox")} Create
|
|
11510
|
-
2. ${cyan("locus sandbox claude")} Login
|
|
11511
|
-
3. ${cyan("locus
|
|
11288
|
+
1. ${cyan("locus sandbox")} Create provider sandboxes
|
|
11289
|
+
2. ${cyan("locus sandbox claude")} Login Claude inside its sandbox
|
|
11290
|
+
3. ${cyan("locus sandbox codex")} Login Codex inside its sandbox
|
|
11291
|
+
4. ${cyan("locus sandbox install bun")} Install extra tools (optional)
|
|
11512
11292
|
|
|
11513
11293
|
`);
|
|
11514
11294
|
}
|
|
@@ -11521,6 +11301,14 @@ async function sandboxCommand(projectRoot, args) {
|
|
|
11521
11301
|
case "claude":
|
|
11522
11302
|
case "codex":
|
|
11523
11303
|
return handleAgentLogin(projectRoot, subcommand);
|
|
11304
|
+
case "install":
|
|
11305
|
+
return handleInstall(projectRoot, args.slice(1));
|
|
11306
|
+
case "exec":
|
|
11307
|
+
return handleExec(projectRoot, args.slice(1));
|
|
11308
|
+
case "shell":
|
|
11309
|
+
return handleShell(projectRoot, args.slice(1));
|
|
11310
|
+
case "logs":
|
|
11311
|
+
return handleLogs(projectRoot, args.slice(1));
|
|
11524
11312
|
case "rm":
|
|
11525
11313
|
return handleRemove(projectRoot);
|
|
11526
11314
|
case "status":
|
|
@@ -11530,24 +11318,12 @@ async function sandboxCommand(projectRoot, args) {
|
|
|
11530
11318
|
default:
|
|
11531
11319
|
process.stderr.write(`${red("✗")} Unknown sandbox subcommand: ${bold(subcommand)}
|
|
11532
11320
|
`);
|
|
11533
|
-
process.stderr.write(` Available: ${cyan("claude")}, ${cyan("codex")}, ${cyan("rm")}, ${cyan("status")}
|
|
11321
|
+
process.stderr.write(` Available: ${cyan("claude")}, ${cyan("codex")}, ${cyan("install")}, ${cyan("exec")}, ${cyan("shell")}, ${cyan("logs")}, ${cyan("rm")}, ${cyan("status")}
|
|
11534
11322
|
`);
|
|
11535
11323
|
}
|
|
11536
11324
|
}
|
|
11537
11325
|
async function handleCreate(projectRoot) {
|
|
11538
11326
|
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
11327
|
const status = await detectSandboxSupport();
|
|
11552
11328
|
if (!status.available) {
|
|
11553
11329
|
process.stderr.write(`${red("✗")} Docker sandbox not available: ${status.reason}
|
|
@@ -11556,86 +11332,68 @@ async function handleCreate(projectRoot) {
|
|
|
11556
11332
|
`);
|
|
11557
11333
|
return;
|
|
11558
11334
|
}
|
|
11559
|
-
const
|
|
11560
|
-
const
|
|
11335
|
+
const sandboxNames = buildProviderSandboxNames(projectRoot);
|
|
11336
|
+
const readySandboxes = {};
|
|
11337
|
+
let failed = false;
|
|
11338
|
+
for (const provider of PROVIDERS) {
|
|
11339
|
+
const name = sandboxNames[provider];
|
|
11340
|
+
if (isSandboxAlive(name)) {
|
|
11341
|
+
process.stderr.write(`${green("✓")} ${provider} sandbox ready: ${bold(name)}
|
|
11342
|
+
`);
|
|
11343
|
+
readySandboxes[provider] = name;
|
|
11344
|
+
continue;
|
|
11345
|
+
}
|
|
11346
|
+
process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
|
|
11347
|
+
`);
|
|
11348
|
+
const created = await createProviderSandbox(provider, name, projectRoot);
|
|
11349
|
+
if (!created) {
|
|
11350
|
+
process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
|
|
11351
|
+
`);
|
|
11352
|
+
failed = true;
|
|
11353
|
+
continue;
|
|
11354
|
+
}
|
|
11355
|
+
process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
|
|
11356
|
+
`);
|
|
11357
|
+
readySandboxes[provider] = name;
|
|
11358
|
+
}
|
|
11561
11359
|
config.sandbox.enabled = true;
|
|
11562
|
-
config.sandbox.
|
|
11360
|
+
config.sandbox.providers = readySandboxes;
|
|
11563
11361
|
saveConfig(projectRoot, config);
|
|
11564
|
-
|
|
11362
|
+
if (failed) {
|
|
11363
|
+
process.stderr.write(`
|
|
11364
|
+
${yellow("⚠")} Some sandboxes failed to create. Re-run ${cyan("locus sandbox")} after resolving Docker issues.
|
|
11565
11365
|
`);
|
|
11566
|
-
|
|
11366
|
+
}
|
|
11367
|
+
process.stderr.write(`
|
|
11368
|
+
${green("✓")} Sandbox mode enabled with provider-specific sandboxes.
|
|
11369
|
+
`);
|
|
11370
|
+
process.stderr.write(` Next: run ${cyan("locus sandbox claude")} and ${cyan("locus sandbox codex")} to authenticate both providers.
|
|
11567
11371
|
`);
|
|
11568
11372
|
}
|
|
11569
11373
|
async function handleAgentLogin(projectRoot, agent) {
|
|
11570
11374
|
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.
|
|
11375
|
+
const sandboxName = getProviderSandboxName(config.sandbox, agent);
|
|
11376
|
+
if (!sandboxName) {
|
|
11377
|
+
process.stderr.write(`${red("✗")} No ${agent} sandbox configured. Run ${cyan("locus sandbox")} first.
|
|
11577
11378
|
`);
|
|
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);
|
|
11379
|
+
return;
|
|
11584
11380
|
}
|
|
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)}...
|
|
11381
|
+
if (!isSandboxAlive(sandboxName)) {
|
|
11382
|
+
process.stderr.write(`${red("✗")} ${agent} sandbox is not running: ${dim(sandboxName)}
|
|
11608
11383
|
`);
|
|
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.
|
|
11384
|
+
process.stderr.write(` Recreate it with ${cyan("locus sandbox")}.
|
|
11614
11385
|
`);
|
|
11615
|
-
|
|
11616
|
-
|
|
11386
|
+
return;
|
|
11387
|
+
}
|
|
11388
|
+
if (agent === "codex") {
|
|
11617
11389
|
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)}...
|
|
11390
|
+
}
|
|
11391
|
+
process.stderr.write(`Connecting to ${agent} sandbox ${dim(sandboxName)}...
|
|
11632
11392
|
`);
|
|
11633
|
-
|
|
11393
|
+
process.stderr.write(`${dim("Login and then exit when ready.")}
|
|
11634
11394
|
|
|
11635
11395
|
`);
|
|
11636
|
-
|
|
11637
|
-
}
|
|
11638
|
-
const child = spawn6("docker", dockerArgs, {
|
|
11396
|
+
const child = spawn6("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
|
|
11639
11397
|
stdio: "inherit"
|
|
11640
11398
|
});
|
|
11641
11399
|
await new Promise((resolve2) => {
|
|
@@ -11661,25 +11419,30 @@ ${yellow("⚠")} ${agent} exited with code ${code}.
|
|
|
11661
11419
|
}
|
|
11662
11420
|
function handleRemove(projectRoot) {
|
|
11663
11421
|
const config = loadConfig(projectRoot);
|
|
11664
|
-
|
|
11665
|
-
|
|
11422
|
+
const names = Array.from(new Set(Object.values(config.sandbox.providers).filter((value) => typeof value === "string" && value.length > 0)));
|
|
11423
|
+
if (names.length === 0) {
|
|
11424
|
+
config.sandbox.enabled = false;
|
|
11425
|
+
config.sandbox.providers = {};
|
|
11426
|
+
saveConfig(projectRoot, config);
|
|
11427
|
+
process.stderr.write(`${dim("No sandboxes to remove. Sandbox mode disabled.")}
|
|
11666
11428
|
`);
|
|
11667
11429
|
return;
|
|
11668
11430
|
}
|
|
11669
|
-
const sandboxName
|
|
11670
|
-
|
|
11431
|
+
for (const sandboxName of names) {
|
|
11432
|
+
process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
|
|
11671
11433
|
`);
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11434
|
+
try {
|
|
11435
|
+
execSync17(`docker sandbox rm ${sandboxName}`, {
|
|
11436
|
+
encoding: "utf-8",
|
|
11437
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11438
|
+
timeout: 15000
|
|
11439
|
+
});
|
|
11440
|
+
} catch {}
|
|
11441
|
+
}
|
|
11442
|
+
config.sandbox.providers = {};
|
|
11680
11443
|
config.sandbox.enabled = false;
|
|
11681
11444
|
saveConfig(projectRoot, config);
|
|
11682
|
-
process.stderr.write(`${green("✓")}
|
|
11445
|
+
process.stderr.write(`${green("✓")} Provider sandboxes removed. Sandbox mode disabled.
|
|
11683
11446
|
`);
|
|
11684
11447
|
}
|
|
11685
11448
|
function handleStatus(projectRoot) {
|
|
@@ -11690,24 +11453,338 @@ ${bold("Sandbox Status")}
|
|
|
11690
11453
|
`);
|
|
11691
11454
|
process.stderr.write(` ${dim("Enabled:")} ${config.sandbox.enabled ? green("yes") : red("no")}
|
|
11692
11455
|
`);
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
const alive = isSandboxAlive(config.sandbox.name);
|
|
11697
|
-
process.stderr.write(` ${dim("Running:")} ${alive ? green("yes") : red("no")}
|
|
11456
|
+
for (const provider of PROVIDERS) {
|
|
11457
|
+
const name = config.sandbox.providers[provider];
|
|
11458
|
+
process.stderr.write(` ${dim(`${provider}:`).padEnd(15)}${name ? bold(name) : dim("(not configured)")}
|
|
11698
11459
|
`);
|
|
11699
|
-
if (
|
|
11700
|
-
|
|
11701
|
-
${
|
|
11460
|
+
if (name) {
|
|
11461
|
+
const alive = isSandboxAlive(name);
|
|
11462
|
+
process.stderr.write(` ${dim(`${provider} running:`).padEnd(15)}${alive ? green("yes") : red("no")}
|
|
11702
11463
|
`);
|
|
11703
11464
|
}
|
|
11704
11465
|
}
|
|
11466
|
+
if (!config.sandbox.providers.claude || !config.sandbox.providers.codex) {
|
|
11467
|
+
process.stderr.write(`
|
|
11468
|
+
${yellow("⚠")} Provider sandboxes are incomplete. Run ${bold("locus sandbox")}.
|
|
11469
|
+
`);
|
|
11470
|
+
}
|
|
11705
11471
|
process.stderr.write(`
|
|
11706
11472
|
`);
|
|
11707
11473
|
}
|
|
11474
|
+
function parseSandboxInstallArgs(args) {
|
|
11475
|
+
const packages = [];
|
|
11476
|
+
let provider = "all";
|
|
11477
|
+
for (let i = 0;i < args.length; i++) {
|
|
11478
|
+
const token = args[i];
|
|
11479
|
+
if (token === "--provider") {
|
|
11480
|
+
const value = args[i + 1];
|
|
11481
|
+
if (!value) {
|
|
11482
|
+
return {
|
|
11483
|
+
provider,
|
|
11484
|
+
packages,
|
|
11485
|
+
error: "Missing value for --provider (expected claude, codex, or all)."
|
|
11486
|
+
};
|
|
11487
|
+
}
|
|
11488
|
+
if (value !== "claude" && value !== "codex" && value !== "all") {
|
|
11489
|
+
return {
|
|
11490
|
+
provider,
|
|
11491
|
+
packages,
|
|
11492
|
+
error: `Invalid provider "${value}". Expected claude, codex, or all.`
|
|
11493
|
+
};
|
|
11494
|
+
}
|
|
11495
|
+
provider = value;
|
|
11496
|
+
i++;
|
|
11497
|
+
continue;
|
|
11498
|
+
}
|
|
11499
|
+
if (token.startsWith("--provider=")) {
|
|
11500
|
+
const value = token.slice("--provider=".length);
|
|
11501
|
+
if (value !== "claude" && value !== "codex" && value !== "all") {
|
|
11502
|
+
return {
|
|
11503
|
+
provider,
|
|
11504
|
+
packages,
|
|
11505
|
+
error: `Invalid provider "${value}". Expected claude, codex, or all.`
|
|
11506
|
+
};
|
|
11507
|
+
}
|
|
11508
|
+
provider = value;
|
|
11509
|
+
continue;
|
|
11510
|
+
}
|
|
11511
|
+
if (token.startsWith("-")) {
|
|
11512
|
+
return {
|
|
11513
|
+
provider,
|
|
11514
|
+
packages,
|
|
11515
|
+
error: `Unknown option "${token}".`
|
|
11516
|
+
};
|
|
11517
|
+
}
|
|
11518
|
+
packages.push(token);
|
|
11519
|
+
}
|
|
11520
|
+
if (packages.length === 0) {
|
|
11521
|
+
return {
|
|
11522
|
+
provider,
|
|
11523
|
+
packages,
|
|
11524
|
+
error: "Usage: locus sandbox install <package...> [--provider claude|codex|all]"
|
|
11525
|
+
};
|
|
11526
|
+
}
|
|
11527
|
+
return { provider, packages };
|
|
11528
|
+
}
|
|
11529
|
+
async function handleInstall(projectRoot, args) {
|
|
11530
|
+
const parsed = parseSandboxInstallArgs(args);
|
|
11531
|
+
if (parsed.error) {
|
|
11532
|
+
process.stderr.write(`${red("✗")} ${parsed.error}
|
|
11533
|
+
`);
|
|
11534
|
+
return;
|
|
11535
|
+
}
|
|
11536
|
+
const config = loadConfig(projectRoot);
|
|
11537
|
+
const targets = getTargetProviders(config.sandbox.providers, parsed.provider);
|
|
11538
|
+
if (targets.length === 0) {
|
|
11539
|
+
process.stderr.write(`${red("✗")} No provider sandboxes are configured. Run ${cyan("locus sandbox")} first.
|
|
11540
|
+
`);
|
|
11541
|
+
return;
|
|
11542
|
+
}
|
|
11543
|
+
let anySucceeded = false;
|
|
11544
|
+
let anyFailed = false;
|
|
11545
|
+
for (const provider of targets) {
|
|
11546
|
+
const sandboxName = config.sandbox.providers[provider];
|
|
11547
|
+
if (!sandboxName) {
|
|
11548
|
+
process.stderr.write(`${yellow("⚠")} ${provider} sandbox is not configured. Run ${cyan("locus sandbox")} first.
|
|
11549
|
+
`);
|
|
11550
|
+
anyFailed = true;
|
|
11551
|
+
continue;
|
|
11552
|
+
}
|
|
11553
|
+
if (!isSandboxAlive(sandboxName)) {
|
|
11554
|
+
process.stderr.write(`${yellow("⚠")} ${provider} sandbox is not running: ${dim(sandboxName)}
|
|
11555
|
+
`);
|
|
11556
|
+
anyFailed = true;
|
|
11557
|
+
continue;
|
|
11558
|
+
}
|
|
11559
|
+
process.stderr.write(`Installing ${bold(parsed.packages.join(", "))} in ${provider} sandbox ${dim(sandboxName)}...
|
|
11560
|
+
`);
|
|
11561
|
+
const ok = await runInteractiveCommand("docker", [
|
|
11562
|
+
"sandbox",
|
|
11563
|
+
"exec",
|
|
11564
|
+
sandboxName,
|
|
11565
|
+
"npm",
|
|
11566
|
+
"install",
|
|
11567
|
+
"-g",
|
|
11568
|
+
...parsed.packages
|
|
11569
|
+
]);
|
|
11570
|
+
if (ok) {
|
|
11571
|
+
anySucceeded = true;
|
|
11572
|
+
process.stderr.write(`${green("✓")} Installed package(s) in ${provider} sandbox.
|
|
11573
|
+
`);
|
|
11574
|
+
} else {
|
|
11575
|
+
anyFailed = true;
|
|
11576
|
+
process.stderr.write(`${red("✗")} Failed to install package(s) in ${provider} sandbox.
|
|
11577
|
+
`);
|
|
11578
|
+
}
|
|
11579
|
+
}
|
|
11580
|
+
if (!anySucceeded && anyFailed) {
|
|
11581
|
+
process.stderr.write(`${yellow("⚠")} No package installs completed successfully.
|
|
11582
|
+
`);
|
|
11583
|
+
}
|
|
11584
|
+
}
|
|
11585
|
+
function parseSandboxExecArgs(args) {
|
|
11586
|
+
if (args.length === 0) {
|
|
11587
|
+
return {
|
|
11588
|
+
command: [],
|
|
11589
|
+
error: "Usage: locus sandbox exec <provider> -- <command...>"
|
|
11590
|
+
};
|
|
11591
|
+
}
|
|
11592
|
+
const provider = args[0];
|
|
11593
|
+
if (provider !== "claude" && provider !== "codex") {
|
|
11594
|
+
return {
|
|
11595
|
+
command: [],
|
|
11596
|
+
error: `Invalid provider "${provider}". Expected claude or codex.`
|
|
11597
|
+
};
|
|
11598
|
+
}
|
|
11599
|
+
const separatorIndex = args.indexOf("--");
|
|
11600
|
+
const command = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : args.slice(1);
|
|
11601
|
+
if (command.length === 0) {
|
|
11602
|
+
return {
|
|
11603
|
+
provider,
|
|
11604
|
+
command: [],
|
|
11605
|
+
error: "Missing command. Example: locus sandbox exec codex -- bun --version"
|
|
11606
|
+
};
|
|
11607
|
+
}
|
|
11608
|
+
return { provider, command };
|
|
11609
|
+
}
|
|
11610
|
+
async function handleExec(projectRoot, args) {
|
|
11611
|
+
const parsed = parseSandboxExecArgs(args);
|
|
11612
|
+
if (parsed.error || !parsed.provider) {
|
|
11613
|
+
process.stderr.write(`${red("✗")} ${parsed.error}
|
|
11614
|
+
`);
|
|
11615
|
+
return;
|
|
11616
|
+
}
|
|
11617
|
+
const sandboxName = getActiveProviderSandbox(projectRoot, parsed.provider);
|
|
11618
|
+
if (!sandboxName) {
|
|
11619
|
+
return;
|
|
11620
|
+
}
|
|
11621
|
+
await runInteractiveCommand("docker", [
|
|
11622
|
+
"sandbox",
|
|
11623
|
+
"exec",
|
|
11624
|
+
"-w",
|
|
11625
|
+
projectRoot,
|
|
11626
|
+
sandboxName,
|
|
11627
|
+
...parsed.command
|
|
11628
|
+
]);
|
|
11629
|
+
}
|
|
11630
|
+
async function handleShell(projectRoot, args) {
|
|
11631
|
+
const provider = args[0];
|
|
11632
|
+
if (provider !== "claude" && provider !== "codex") {
|
|
11633
|
+
process.stderr.write(`${red("✗")} Usage: locus sandbox shell <provider> (provider: claude|codex)
|
|
11634
|
+
`);
|
|
11635
|
+
return;
|
|
11636
|
+
}
|
|
11637
|
+
const sandboxName = getActiveProviderSandbox(projectRoot, provider);
|
|
11638
|
+
if (!sandboxName) {
|
|
11639
|
+
return;
|
|
11640
|
+
}
|
|
11641
|
+
process.stderr.write(`Opening shell in ${provider} sandbox ${dim(sandboxName)}...
|
|
11642
|
+
`);
|
|
11643
|
+
await runInteractiveCommand("docker", [
|
|
11644
|
+
"sandbox",
|
|
11645
|
+
"exec",
|
|
11646
|
+
"-it",
|
|
11647
|
+
"-w",
|
|
11648
|
+
projectRoot,
|
|
11649
|
+
sandboxName,
|
|
11650
|
+
"sh"
|
|
11651
|
+
]);
|
|
11652
|
+
}
|
|
11653
|
+
function parseSandboxLogsArgs(args) {
|
|
11654
|
+
if (args.length === 0) {
|
|
11655
|
+
return {
|
|
11656
|
+
follow: false,
|
|
11657
|
+
error: "Usage: locus sandbox logs <provider> [--follow] [--tail <lines>]"
|
|
11658
|
+
};
|
|
11659
|
+
}
|
|
11660
|
+
const provider = args[0];
|
|
11661
|
+
if (provider !== "claude" && provider !== "codex") {
|
|
11662
|
+
return {
|
|
11663
|
+
follow: false,
|
|
11664
|
+
error: `Invalid provider "${provider}". Expected claude or codex.`
|
|
11665
|
+
};
|
|
11666
|
+
}
|
|
11667
|
+
let follow = false;
|
|
11668
|
+
let tail;
|
|
11669
|
+
for (let i = 1;i < args.length; i++) {
|
|
11670
|
+
const token = args[i];
|
|
11671
|
+
if (token === "--follow" || token === "-f") {
|
|
11672
|
+
follow = true;
|
|
11673
|
+
continue;
|
|
11674
|
+
}
|
|
11675
|
+
if (token === "--tail") {
|
|
11676
|
+
const value = args[i + 1];
|
|
11677
|
+
if (!value) {
|
|
11678
|
+
return { provider, follow, tail, error: "Missing value for --tail." };
|
|
11679
|
+
}
|
|
11680
|
+
const parsedTail = Number.parseInt(value, 10);
|
|
11681
|
+
if (!Number.isFinite(parsedTail) || parsedTail < 0) {
|
|
11682
|
+
return { provider, follow, tail, error: "Invalid --tail value." };
|
|
11683
|
+
}
|
|
11684
|
+
tail = parsedTail;
|
|
11685
|
+
i++;
|
|
11686
|
+
continue;
|
|
11687
|
+
}
|
|
11688
|
+
if (token.startsWith("--tail=")) {
|
|
11689
|
+
const value = token.slice("--tail=".length);
|
|
11690
|
+
const parsedTail = Number.parseInt(value, 10);
|
|
11691
|
+
if (!Number.isFinite(parsedTail) || parsedTail < 0) {
|
|
11692
|
+
return { provider, follow, tail, error: "Invalid --tail value." };
|
|
11693
|
+
}
|
|
11694
|
+
tail = parsedTail;
|
|
11695
|
+
continue;
|
|
11696
|
+
}
|
|
11697
|
+
return {
|
|
11698
|
+
provider,
|
|
11699
|
+
follow,
|
|
11700
|
+
tail,
|
|
11701
|
+
error: `Unknown option "${token}".`
|
|
11702
|
+
};
|
|
11703
|
+
}
|
|
11704
|
+
return { provider, follow, tail };
|
|
11705
|
+
}
|
|
11706
|
+
async function handleLogs(projectRoot, args) {
|
|
11707
|
+
const parsed = parseSandboxLogsArgs(args);
|
|
11708
|
+
if (parsed.error || !parsed.provider) {
|
|
11709
|
+
process.stderr.write(`${red("✗")} ${parsed.error}
|
|
11710
|
+
`);
|
|
11711
|
+
return;
|
|
11712
|
+
}
|
|
11713
|
+
const sandboxName = getActiveProviderSandbox(projectRoot, parsed.provider);
|
|
11714
|
+
if (!sandboxName) {
|
|
11715
|
+
return;
|
|
11716
|
+
}
|
|
11717
|
+
const dockerArgs = ["sandbox", "logs"];
|
|
11718
|
+
if (parsed.follow) {
|
|
11719
|
+
dockerArgs.push("--follow");
|
|
11720
|
+
}
|
|
11721
|
+
if (parsed.tail !== undefined) {
|
|
11722
|
+
dockerArgs.push("--tail", String(parsed.tail));
|
|
11723
|
+
}
|
|
11724
|
+
dockerArgs.push(sandboxName);
|
|
11725
|
+
await runInteractiveCommand("docker", dockerArgs);
|
|
11726
|
+
}
|
|
11727
|
+
function buildProviderSandboxNames(projectRoot) {
|
|
11728
|
+
const segment = sanitizeSegment(basename4(projectRoot));
|
|
11729
|
+
const hash = createHash("sha1").update(projectRoot).digest("hex").slice(0, 8);
|
|
11730
|
+
return {
|
|
11731
|
+
claude: `locus-${segment}-claude-${hash}`,
|
|
11732
|
+
codex: `locus-${segment}-codex-${hash}`
|
|
11733
|
+
};
|
|
11734
|
+
}
|
|
11735
|
+
function sanitizeSegment(input) {
|
|
11736
|
+
const cleaned = input.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
11737
|
+
return cleaned || "workspace";
|
|
11738
|
+
}
|
|
11739
|
+
function getTargetProviders(sandboxes, provider) {
|
|
11740
|
+
if (provider === "claude" || provider === "codex") {
|
|
11741
|
+
return [provider];
|
|
11742
|
+
}
|
|
11743
|
+
return PROVIDERS.filter((name) => Boolean(sandboxes[name]));
|
|
11744
|
+
}
|
|
11745
|
+
function getActiveProviderSandbox(projectRoot, provider) {
|
|
11746
|
+
const config = loadConfig(projectRoot);
|
|
11747
|
+
const sandboxName = config.sandbox.providers[provider];
|
|
11748
|
+
if (!sandboxName) {
|
|
11749
|
+
process.stderr.write(`${red("✗")} No ${provider} sandbox configured. Run ${cyan("locus sandbox")} first.
|
|
11750
|
+
`);
|
|
11751
|
+
return null;
|
|
11752
|
+
}
|
|
11753
|
+
if (!isSandboxAlive(sandboxName)) {
|
|
11754
|
+
process.stderr.write(`${red("✗")} ${provider} sandbox is not running: ${dim(sandboxName)}
|
|
11755
|
+
`);
|
|
11756
|
+
process.stderr.write(` Recreate it with ${cyan("locus sandbox")}.
|
|
11757
|
+
`);
|
|
11758
|
+
return null;
|
|
11759
|
+
}
|
|
11760
|
+
return sandboxName;
|
|
11761
|
+
}
|
|
11762
|
+
function runInteractiveCommand(command, args) {
|
|
11763
|
+
return new Promise((resolve2) => {
|
|
11764
|
+
const child = spawn6(command, args, { stdio: "inherit" });
|
|
11765
|
+
child.on("close", (code) => resolve2(code === 0));
|
|
11766
|
+
child.on("error", () => resolve2(false));
|
|
11767
|
+
});
|
|
11768
|
+
}
|
|
11769
|
+
async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
11770
|
+
try {
|
|
11771
|
+
execSync17(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, {
|
|
11772
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11773
|
+
timeout: 120000
|
|
11774
|
+
});
|
|
11775
|
+
} catch {}
|
|
11776
|
+
if (!isSandboxAlive(sandboxName)) {
|
|
11777
|
+
return false;
|
|
11778
|
+
}
|
|
11779
|
+
if (provider === "codex") {
|
|
11780
|
+
await ensureCodexInSandbox(sandboxName);
|
|
11781
|
+
}
|
|
11782
|
+
await enforceSandboxIgnore(sandboxName, projectRoot);
|
|
11783
|
+
return true;
|
|
11784
|
+
}
|
|
11708
11785
|
async function ensureCodexInSandbox(sandboxName) {
|
|
11709
11786
|
try {
|
|
11710
|
-
|
|
11787
|
+
execSync17(`docker sandbox exec ${sandboxName} which codex`, {
|
|
11711
11788
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11712
11789
|
timeout: 5000
|
|
11713
11790
|
});
|
|
@@ -11715,7 +11792,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11715
11792
|
process.stderr.write(`Installing codex in sandbox...
|
|
11716
11793
|
`);
|
|
11717
11794
|
try {
|
|
11718
|
-
|
|
11795
|
+
execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
11719
11796
|
} catch {
|
|
11720
11797
|
process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
|
|
11721
11798
|
`);
|
|
@@ -11724,7 +11801,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11724
11801
|
}
|
|
11725
11802
|
function isSandboxAlive(name) {
|
|
11726
11803
|
try {
|
|
11727
|
-
const output =
|
|
11804
|
+
const output = execSync17("docker sandbox ls", {
|
|
11728
11805
|
encoding: "utf-8",
|
|
11729
11806
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11730
11807
|
timeout: 5000
|
|
@@ -11734,11 +11811,13 @@ function isSandboxAlive(name) {
|
|
|
11734
11811
|
return false;
|
|
11735
11812
|
}
|
|
11736
11813
|
}
|
|
11814
|
+
var PROVIDERS;
|
|
11737
11815
|
var init_sandbox2 = __esm(() => {
|
|
11738
11816
|
init_config();
|
|
11739
11817
|
init_sandbox();
|
|
11740
11818
|
init_sandbox_ignore();
|
|
11741
11819
|
init_terminal();
|
|
11820
|
+
PROVIDERS = ["claude", "codex"];
|
|
11742
11821
|
});
|
|
11743
11822
|
|
|
11744
11823
|
// src/cli.ts
|