@locusai/cli 0.24.8 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/locus.js +1277 -579
- package/package.json +2 -2
package/bin/locus.js
CHANGED
|
@@ -39,8 +39,7 @@ var init_dist = __esm(() => {
|
|
|
39
39
|
rebaseBeforeTask: true
|
|
40
40
|
},
|
|
41
41
|
sprint: {
|
|
42
|
-
|
|
43
|
-
stopOnFailure: true
|
|
42
|
+
stopOnFailure: false
|
|
44
43
|
},
|
|
45
44
|
logging: {
|
|
46
45
|
level: "normal",
|
|
@@ -3214,6 +3213,602 @@ var init_uninstall = __esm(() => {
|
|
|
3214
3213
|
init_registry();
|
|
3215
3214
|
});
|
|
3216
3215
|
|
|
3216
|
+
// src/display/table.ts
|
|
3217
|
+
function renderTable(columns, rows, options = {}) {
|
|
3218
|
+
const {
|
|
3219
|
+
indent = 2,
|
|
3220
|
+
headerSeparator = true,
|
|
3221
|
+
maxRows = 0,
|
|
3222
|
+
emptyMessage = "No results."
|
|
3223
|
+
} = options;
|
|
3224
|
+
if (rows.length === 0) {
|
|
3225
|
+
return `${" ".repeat(indent)}${dim2(emptyMessage)}`;
|
|
3226
|
+
}
|
|
3227
|
+
const termWidth = getCapabilities().columns;
|
|
3228
|
+
const indentStr = " ".repeat(indent);
|
|
3229
|
+
const gap = 2;
|
|
3230
|
+
const formattedRows = rows.map((row) => {
|
|
3231
|
+
const formatted = {};
|
|
3232
|
+
for (const col of columns) {
|
|
3233
|
+
if (col.format) {
|
|
3234
|
+
formatted[col.key] = col.format(row[col.key], row);
|
|
3235
|
+
} else {
|
|
3236
|
+
const val = row[col.key];
|
|
3237
|
+
formatted[col.key] = val === null || val === undefined ? "" : String(val);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
return formatted;
|
|
3241
|
+
});
|
|
3242
|
+
const colWidths = columns.map((col, _i) => {
|
|
3243
|
+
const headerWidth = stripAnsi(col.header).length;
|
|
3244
|
+
const minWidth = col.minWidth ?? headerWidth;
|
|
3245
|
+
let maxContent = headerWidth;
|
|
3246
|
+
for (const row of formattedRows) {
|
|
3247
|
+
const cellWidth = stripAnsi(row[col.key] ?? "").length;
|
|
3248
|
+
if (cellWidth > maxContent)
|
|
3249
|
+
maxContent = cellWidth;
|
|
3250
|
+
}
|
|
3251
|
+
let width = Math.max(minWidth, maxContent);
|
|
3252
|
+
if (col.maxWidth && col.maxWidth > 0) {
|
|
3253
|
+
width = Math.min(width, col.maxWidth);
|
|
3254
|
+
}
|
|
3255
|
+
return width;
|
|
3256
|
+
});
|
|
3257
|
+
const totalWidth = indent + colWidths.reduce((s, w) => s + w, 0) + gap * (columns.length - 1);
|
|
3258
|
+
if (totalWidth > termWidth && columns.length > 1) {
|
|
3259
|
+
const overflow = totalWidth - termWidth;
|
|
3260
|
+
let widestIdx = 0;
|
|
3261
|
+
let widestSize = 0;
|
|
3262
|
+
for (let i = 0;i < columns.length; i++) {
|
|
3263
|
+
if (!columns[i].maxWidth && colWidths[i] > widestSize) {
|
|
3264
|
+
widestSize = colWidths[i];
|
|
3265
|
+
widestIdx = i;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
colWidths[widestIdx] = Math.max(10, colWidths[widestIdx] - overflow);
|
|
3269
|
+
}
|
|
3270
|
+
const lines = [];
|
|
3271
|
+
const headerParts = columns.map((col, i) => alignCell(bold2(col.header), colWidths[i], col.align ?? "left"));
|
|
3272
|
+
lines.push(`${indentStr}${headerParts.join(" ".repeat(gap))}`);
|
|
3273
|
+
if (headerSeparator) {
|
|
3274
|
+
const sep = columns.map((_, i) => gray2("─".repeat(colWidths[i]))).join(" ".repeat(gap));
|
|
3275
|
+
lines.push(`${indentStr}${sep}`);
|
|
3276
|
+
}
|
|
3277
|
+
const displayRows = maxRows > 0 ? formattedRows.slice(0, maxRows) : formattedRows;
|
|
3278
|
+
for (const row of displayRows) {
|
|
3279
|
+
const cellParts = columns.map((col, i) => {
|
|
3280
|
+
const raw = row[col.key] ?? "";
|
|
3281
|
+
return alignCell(raw, colWidths[i], col.align ?? "left");
|
|
3282
|
+
});
|
|
3283
|
+
lines.push(`${indentStr}${cellParts.join(" ".repeat(gap))}`);
|
|
3284
|
+
}
|
|
3285
|
+
if (maxRows > 0 && formattedRows.length > maxRows) {
|
|
3286
|
+
const remaining = formattedRows.length - maxRows;
|
|
3287
|
+
lines.push(`${indentStr}${dim2(`... and ${remaining} more`)}`);
|
|
3288
|
+
}
|
|
3289
|
+
return lines.join(`
|
|
3290
|
+
`);
|
|
3291
|
+
}
|
|
3292
|
+
function alignCell(text, width, align) {
|
|
3293
|
+
const visual = stripAnsi(text).length;
|
|
3294
|
+
if (visual > width) {
|
|
3295
|
+
const stripped = stripAnsi(text);
|
|
3296
|
+
return `${stripped.slice(0, width - 1)}…`;
|
|
3297
|
+
}
|
|
3298
|
+
const padding = width - visual;
|
|
3299
|
+
switch (align) {
|
|
3300
|
+
case "right":
|
|
3301
|
+
return " ".repeat(padding) + text;
|
|
3302
|
+
case "center": {
|
|
3303
|
+
const left = Math.floor(padding / 2);
|
|
3304
|
+
const right = padding - left;
|
|
3305
|
+
return " ".repeat(left) + text + " ".repeat(right);
|
|
3306
|
+
}
|
|
3307
|
+
default:
|
|
3308
|
+
return text + " ".repeat(padding);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
function renderDetails(entries, options = {}) {
|
|
3312
|
+
const { indent = 2, labelWidth: fixedWidth } = options;
|
|
3313
|
+
const indentStr = " ".repeat(indent);
|
|
3314
|
+
const labelWidth = fixedWidth ?? Math.max(...entries.map((e) => stripAnsi(e.label).length)) + 1;
|
|
3315
|
+
return entries.map((entry) => {
|
|
3316
|
+
const label = padEnd(dim2(`${entry.label}:`), labelWidth + 1);
|
|
3317
|
+
return `${indentStr}${label} ${entry.value}`;
|
|
3318
|
+
}).join(`
|
|
3319
|
+
`);
|
|
3320
|
+
}
|
|
3321
|
+
var init_table = __esm(() => {
|
|
3322
|
+
init_terminal();
|
|
3323
|
+
});
|
|
3324
|
+
|
|
3325
|
+
// src/skills/types.ts
|
|
3326
|
+
var REGISTRY_REPO = "locusai/skills", REGISTRY_BRANCH = "main", SKILLS_LOCK_FILENAME = "skills-lock.json", CLAUDE_SKILLS_DIR = ".claude/skills", AGENTS_SKILLS_DIR = ".agents/skills";
|
|
3327
|
+
|
|
3328
|
+
// src/skills/lock.ts
|
|
3329
|
+
import { createHash } from "node:crypto";
|
|
3330
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3331
|
+
import { join as join10 } from "node:path";
|
|
3332
|
+
function readLockFile(projectRoot) {
|
|
3333
|
+
const filePath = join10(projectRoot, SKILLS_LOCK_FILENAME);
|
|
3334
|
+
try {
|
|
3335
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
3336
|
+
return JSON.parse(raw);
|
|
3337
|
+
} catch {
|
|
3338
|
+
return { ...DEFAULT_LOCK_FILE, skills: {} };
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
function writeLockFile(projectRoot, lockFile) {
|
|
3342
|
+
const filePath = join10(projectRoot, SKILLS_LOCK_FILENAME);
|
|
3343
|
+
writeFileSync7(filePath, `${JSON.stringify(lockFile, null, 2)}
|
|
3344
|
+
`, "utf-8");
|
|
3345
|
+
}
|
|
3346
|
+
function computeSkillHash(content) {
|
|
3347
|
+
return createHash("sha256").update(content).digest("hex");
|
|
3348
|
+
}
|
|
3349
|
+
var DEFAULT_LOCK_FILE;
|
|
3350
|
+
var init_lock = __esm(() => {
|
|
3351
|
+
DEFAULT_LOCK_FILE = { version: 1, skills: {} };
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
// src/skills/installer.ts
|
|
3355
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
3356
|
+
import { join as join11 } from "node:path";
|
|
3357
|
+
async function installSkill(projectRoot, name, content, source) {
|
|
3358
|
+
const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
|
|
3359
|
+
const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
|
|
3360
|
+
mkdirSync8(claudeDir, { recursive: true });
|
|
3361
|
+
mkdirSync8(agentsDir, { recursive: true });
|
|
3362
|
+
writeFileSync8(join11(claudeDir, "SKILL.md"), content, "utf-8");
|
|
3363
|
+
writeFileSync8(join11(agentsDir, "SKILL.md"), content, "utf-8");
|
|
3364
|
+
const lock = readLockFile(projectRoot);
|
|
3365
|
+
lock.skills[name] = {
|
|
3366
|
+
source,
|
|
3367
|
+
sourceType: "github",
|
|
3368
|
+
computedHash: computeSkillHash(content)
|
|
3369
|
+
};
|
|
3370
|
+
writeLockFile(projectRoot, lock);
|
|
3371
|
+
}
|
|
3372
|
+
async function removeSkill(projectRoot, name) {
|
|
3373
|
+
if (!isSkillInstalled(projectRoot, name)) {
|
|
3374
|
+
console.warn(`Skill "${name}" is not installed.`);
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
|
|
3378
|
+
const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
|
|
3379
|
+
if (existsSync11(claudeDir)) {
|
|
3380
|
+
rmSync(claudeDir, { recursive: true, force: true });
|
|
3381
|
+
}
|
|
3382
|
+
if (existsSync11(agentsDir)) {
|
|
3383
|
+
rmSync(agentsDir, { recursive: true, force: true });
|
|
3384
|
+
}
|
|
3385
|
+
const lock = readLockFile(projectRoot);
|
|
3386
|
+
delete lock.skills[name];
|
|
3387
|
+
writeLockFile(projectRoot, lock);
|
|
3388
|
+
}
|
|
3389
|
+
function isSkillInstalled(projectRoot, name) {
|
|
3390
|
+
const lock = readLockFile(projectRoot);
|
|
3391
|
+
return name in lock.skills;
|
|
3392
|
+
}
|
|
3393
|
+
var init_installer = __esm(() => {
|
|
3394
|
+
init_lock();
|
|
3395
|
+
});
|
|
3396
|
+
|
|
3397
|
+
// src/skills/registry.ts
|
|
3398
|
+
async function fetchRegistry() {
|
|
3399
|
+
const url = `${RAW_BASE}/registry.json`;
|
|
3400
|
+
let res;
|
|
3401
|
+
try {
|
|
3402
|
+
res = await fetch(url);
|
|
3403
|
+
} catch (err) {
|
|
3404
|
+
throw new Error(`Failed to reach the skill registry at ${url}: ${err.message}`);
|
|
3405
|
+
}
|
|
3406
|
+
if (!res.ok) {
|
|
3407
|
+
throw new Error(`Failed to fetch skill registry (HTTP ${res.status}). Check your network connection and try again.`);
|
|
3408
|
+
}
|
|
3409
|
+
let data;
|
|
3410
|
+
try {
|
|
3411
|
+
data = await res.json();
|
|
3412
|
+
} catch {
|
|
3413
|
+
throw new Error("Skill registry returned invalid JSON.");
|
|
3414
|
+
}
|
|
3415
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.skills)) {
|
|
3416
|
+
throw new Error("Skill registry has an unexpected format (missing 'skills' array).");
|
|
3417
|
+
}
|
|
3418
|
+
return data;
|
|
3419
|
+
}
|
|
3420
|
+
async function fetchSkillContent(path) {
|
|
3421
|
+
const url = `${RAW_BASE}/${path}`;
|
|
3422
|
+
let res;
|
|
3423
|
+
try {
|
|
3424
|
+
res = await fetch(url);
|
|
3425
|
+
} catch (err) {
|
|
3426
|
+
throw new Error(`Failed to fetch skill file at ${url}: ${err.message}`);
|
|
3427
|
+
}
|
|
3428
|
+
if (res.status === 404) {
|
|
3429
|
+
throw new Error(`Skill not found: ${path}`);
|
|
3430
|
+
}
|
|
3431
|
+
if (!res.ok) {
|
|
3432
|
+
throw new Error(`Failed to fetch skill file (HTTP ${res.status}): ${path}`);
|
|
3433
|
+
}
|
|
3434
|
+
return res.text();
|
|
3435
|
+
}
|
|
3436
|
+
function findSkillInRegistry(registry, name) {
|
|
3437
|
+
return registry.skills.find((s) => s.name === name);
|
|
3438
|
+
}
|
|
3439
|
+
var RAW_BASE;
|
|
3440
|
+
var init_registry2 = __esm(() => {
|
|
3441
|
+
RAW_BASE = `https://raw.githubusercontent.com/${REGISTRY_REPO}/${REGISTRY_BRANCH}`;
|
|
3442
|
+
});
|
|
3443
|
+
|
|
3444
|
+
// src/commands/skills.ts
|
|
3445
|
+
var exports_skills = {};
|
|
3446
|
+
__export(exports_skills, {
|
|
3447
|
+
skillsCommand: () => skillsCommand
|
|
3448
|
+
});
|
|
3449
|
+
async function listRemoteSkills() {
|
|
3450
|
+
let registry;
|
|
3451
|
+
try {
|
|
3452
|
+
registry = await fetchRegistry();
|
|
3453
|
+
} catch (err) {
|
|
3454
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
3455
|
+
`);
|
|
3456
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3457
|
+
`);
|
|
3458
|
+
process.exit(1);
|
|
3459
|
+
}
|
|
3460
|
+
if (registry.skills.length === 0) {
|
|
3461
|
+
process.stderr.write(`${dim2("No skills available in the registry.")}
|
|
3462
|
+
`);
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
process.stderr.write(`
|
|
3466
|
+
${bold2("Available Skills")}
|
|
3467
|
+
|
|
3468
|
+
`);
|
|
3469
|
+
const columns = [
|
|
3470
|
+
{ key: "name", header: "Name", minWidth: 12, maxWidth: 24 },
|
|
3471
|
+
{ key: "description", header: "Description", minWidth: 20, maxWidth: 50 },
|
|
3472
|
+
{
|
|
3473
|
+
key: "platforms",
|
|
3474
|
+
header: "Platforms",
|
|
3475
|
+
minWidth: 10,
|
|
3476
|
+
maxWidth: 30,
|
|
3477
|
+
format: (val) => dim2(val.join(", "))
|
|
3478
|
+
}
|
|
3479
|
+
];
|
|
3480
|
+
const rows = registry.skills.map((s) => ({
|
|
3481
|
+
name: cyan2(s.name),
|
|
3482
|
+
description: s.description,
|
|
3483
|
+
platforms: s.platforms
|
|
3484
|
+
}));
|
|
3485
|
+
process.stderr.write(`${renderTable(columns, rows)}
|
|
3486
|
+
|
|
3487
|
+
`);
|
|
3488
|
+
process.stderr.write(` ${dim2(`${registry.skills.length} skill(s) available.`)} Install with: ${bold2("locus skills install <name>")}
|
|
3489
|
+
|
|
3490
|
+
`);
|
|
3491
|
+
}
|
|
3492
|
+
function listInstalledSkills() {
|
|
3493
|
+
const cwd = process.cwd();
|
|
3494
|
+
const lockFile = readLockFile(cwd);
|
|
3495
|
+
const entries = Object.entries(lockFile.skills);
|
|
3496
|
+
if (entries.length === 0) {
|
|
3497
|
+
process.stderr.write(`${yellow2("⚠")} No skills installed.
|
|
3498
|
+
`);
|
|
3499
|
+
process.stderr.write(` Browse available skills with: ${bold2("locus skills list")}
|
|
3500
|
+
`);
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
process.stderr.write(`
|
|
3504
|
+
${bold2("Installed Skills")}
|
|
3505
|
+
|
|
3506
|
+
`);
|
|
3507
|
+
const columns = [
|
|
3508
|
+
{ key: "name", header: "Name", minWidth: 12, maxWidth: 24 },
|
|
3509
|
+
{ key: "source", header: "Source", minWidth: 10, maxWidth: 40 },
|
|
3510
|
+
{
|
|
3511
|
+
key: "hash",
|
|
3512
|
+
header: "Hash",
|
|
3513
|
+
minWidth: 10,
|
|
3514
|
+
maxWidth: 16,
|
|
3515
|
+
format: (val) => dim2(val.slice(0, 12))
|
|
3516
|
+
}
|
|
3517
|
+
];
|
|
3518
|
+
const rows = entries.map(([name, entry]) => ({
|
|
3519
|
+
name: cyan2(name),
|
|
3520
|
+
source: entry.source,
|
|
3521
|
+
hash: entry.computedHash
|
|
3522
|
+
}));
|
|
3523
|
+
process.stderr.write(`${renderTable(columns, rows)}
|
|
3524
|
+
|
|
3525
|
+
`);
|
|
3526
|
+
process.stderr.write(` ${dim2(`${entries.length} skill(s) installed.`)}
|
|
3527
|
+
|
|
3528
|
+
`);
|
|
3529
|
+
}
|
|
3530
|
+
async function installRemoteSkill(name) {
|
|
3531
|
+
if (!name) {
|
|
3532
|
+
process.stderr.write(`${red2("✗")} Please specify a skill name.
|
|
3533
|
+
`);
|
|
3534
|
+
process.stderr.write(` Usage: ${bold2("locus skills install <name>")}
|
|
3535
|
+
`);
|
|
3536
|
+
process.exit(1);
|
|
3537
|
+
}
|
|
3538
|
+
let registry;
|
|
3539
|
+
try {
|
|
3540
|
+
registry = await fetchRegistry();
|
|
3541
|
+
} catch (err) {
|
|
3542
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
3543
|
+
`);
|
|
3544
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3545
|
+
`);
|
|
3546
|
+
process.exit(1);
|
|
3547
|
+
}
|
|
3548
|
+
const entry = findSkillInRegistry(registry, name);
|
|
3549
|
+
if (!entry) {
|
|
3550
|
+
process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' not found in the registry.
|
|
3551
|
+
`);
|
|
3552
|
+
process.stderr.write(` Run ${bold2("locus skills list")} to see available skills.
|
|
3553
|
+
`);
|
|
3554
|
+
process.exit(1);
|
|
3555
|
+
}
|
|
3556
|
+
let content;
|
|
3557
|
+
try {
|
|
3558
|
+
content = await fetchSkillContent(entry.path);
|
|
3559
|
+
} catch (err) {
|
|
3560
|
+
process.stderr.write(`${red2("✗")} Failed to download skill '${name}'.
|
|
3561
|
+
`);
|
|
3562
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3563
|
+
`);
|
|
3564
|
+
process.exit(1);
|
|
3565
|
+
}
|
|
3566
|
+
const cwd = process.cwd();
|
|
3567
|
+
const source = `${REGISTRY_REPO}/${entry.path}`;
|
|
3568
|
+
await installSkill(cwd, name, content, source);
|
|
3569
|
+
process.stderr.write(`
|
|
3570
|
+
${green("✓")} Installed skill '${bold2(name)}' from ${REGISTRY_REPO}
|
|
3571
|
+
`);
|
|
3572
|
+
process.stderr.write(` → ${CLAUDE_SKILLS_DIR}/${name}/SKILL.md
|
|
3573
|
+
`);
|
|
3574
|
+
process.stderr.write(` → ${AGENTS_SKILLS_DIR}/${name}/SKILL.md
|
|
3575
|
+
|
|
3576
|
+
`);
|
|
3577
|
+
}
|
|
3578
|
+
async function removeInstalledSkill(name) {
|
|
3579
|
+
if (!name) {
|
|
3580
|
+
process.stderr.write(`${red2("✗")} Please specify a skill name.
|
|
3581
|
+
`);
|
|
3582
|
+
process.stderr.write(` Usage: ${bold2("locus skills remove <name>")}
|
|
3583
|
+
`);
|
|
3584
|
+
process.exit(1);
|
|
3585
|
+
}
|
|
3586
|
+
const cwd = process.cwd();
|
|
3587
|
+
if (!isSkillInstalled(cwd, name)) {
|
|
3588
|
+
process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' is not installed.
|
|
3589
|
+
`);
|
|
3590
|
+
process.stderr.write(` Run ${bold2("locus skills list --installed")} to see installed skills.
|
|
3591
|
+
`);
|
|
3592
|
+
process.exit(1);
|
|
3593
|
+
}
|
|
3594
|
+
await removeSkill(cwd, name);
|
|
3595
|
+
process.stderr.write(`${green("✓")} Removed skill '${bold2(name)}'
|
|
3596
|
+
`);
|
|
3597
|
+
}
|
|
3598
|
+
async function updateSkills(name) {
|
|
3599
|
+
const cwd = process.cwd();
|
|
3600
|
+
const lockFile = readLockFile(cwd);
|
|
3601
|
+
const installed = Object.entries(lockFile.skills);
|
|
3602
|
+
if (installed.length === 0) {
|
|
3603
|
+
process.stderr.write(`${yellow2("⚠")} No skills installed.
|
|
3604
|
+
`);
|
|
3605
|
+
process.stderr.write(` Install skills with: ${bold2("locus skills install <name>")}
|
|
3606
|
+
`);
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
const targets = name ? installed.filter(([n]) => n === name) : installed;
|
|
3610
|
+
if (name && targets.length === 0) {
|
|
3611
|
+
process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' is not installed.
|
|
3612
|
+
`);
|
|
3613
|
+
process.stderr.write(` Run ${bold2("locus skills list --installed")} to see installed skills.
|
|
3614
|
+
`);
|
|
3615
|
+
process.exit(1);
|
|
3616
|
+
}
|
|
3617
|
+
let registry;
|
|
3618
|
+
try {
|
|
3619
|
+
registry = await fetchRegistry();
|
|
3620
|
+
} catch (err) {
|
|
3621
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
3622
|
+
`);
|
|
3623
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3624
|
+
`);
|
|
3625
|
+
process.exit(1);
|
|
3626
|
+
}
|
|
3627
|
+
process.stderr.write(`
|
|
3628
|
+
`);
|
|
3629
|
+
let updatedCount = 0;
|
|
3630
|
+
for (const [skillName, lockEntry] of targets) {
|
|
3631
|
+
const entry = findSkillInRegistry(registry, skillName);
|
|
3632
|
+
if (!entry) {
|
|
3633
|
+
process.stderr.write(`${yellow2("⚠")} '${bold2(skillName)}' is no longer in the registry (skipped)
|
|
3634
|
+
`);
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
let content;
|
|
3638
|
+
try {
|
|
3639
|
+
content = await fetchSkillContent(entry.path);
|
|
3640
|
+
} catch (err) {
|
|
3641
|
+
process.stderr.write(`${red2("✗")} Failed to download skill '${skillName}': ${dim2(err.message)}
|
|
3642
|
+
`);
|
|
3643
|
+
continue;
|
|
3644
|
+
}
|
|
3645
|
+
const newHash = computeSkillHash(content);
|
|
3646
|
+
if (newHash === lockEntry.computedHash) {
|
|
3647
|
+
process.stderr.write(` '${cyan2(skillName)}' is already up to date
|
|
3648
|
+
`);
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
const source = `${REGISTRY_REPO}/${entry.path}`;
|
|
3652
|
+
await installSkill(cwd, skillName, content, source);
|
|
3653
|
+
updatedCount++;
|
|
3654
|
+
process.stderr.write(`${green("✓")} Updated '${bold2(skillName)}' (hash changed)
|
|
3655
|
+
`);
|
|
3656
|
+
}
|
|
3657
|
+
process.stderr.write(`
|
|
3658
|
+
`);
|
|
3659
|
+
if (updatedCount === 0) {
|
|
3660
|
+
process.stderr.write(` ${dim2("All skills are up to date.")}
|
|
3661
|
+
|
|
3662
|
+
`);
|
|
3663
|
+
} else {
|
|
3664
|
+
process.stderr.write(` ${dim2(`${updatedCount} skill(s) updated.`)}
|
|
3665
|
+
|
|
3666
|
+
`);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
async function infoSkill(name) {
|
|
3670
|
+
if (!name) {
|
|
3671
|
+
process.stderr.write(`${red2("✗")} Please specify a skill name.
|
|
3672
|
+
`);
|
|
3673
|
+
process.stderr.write(` Usage: ${bold2("locus skills info <name>")}
|
|
3674
|
+
`);
|
|
3675
|
+
process.exit(1);
|
|
3676
|
+
}
|
|
3677
|
+
let registry;
|
|
3678
|
+
try {
|
|
3679
|
+
registry = await fetchRegistry();
|
|
3680
|
+
} catch (err) {
|
|
3681
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
3682
|
+
`);
|
|
3683
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3684
|
+
`);
|
|
3685
|
+
process.exit(1);
|
|
3686
|
+
}
|
|
3687
|
+
const entry = findSkillInRegistry(registry, name);
|
|
3688
|
+
const cwd = process.cwd();
|
|
3689
|
+
const lockFile = readLockFile(cwd);
|
|
3690
|
+
const lockEntry = lockFile.skills[name];
|
|
3691
|
+
if (!entry && !lockEntry) {
|
|
3692
|
+
process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' not found in the registry or locally.
|
|
3693
|
+
`);
|
|
3694
|
+
process.stderr.write(` Run ${bold2("locus skills list")} to see available skills.
|
|
3695
|
+
`);
|
|
3696
|
+
process.exit(1);
|
|
3697
|
+
}
|
|
3698
|
+
process.stderr.write(`
|
|
3699
|
+
${bold2("Skill Information")}
|
|
3700
|
+
|
|
3701
|
+
`);
|
|
3702
|
+
if (entry) {
|
|
3703
|
+
process.stderr.write(` ${bold2("Name:")} ${cyan2(entry.name)}
|
|
3704
|
+
`);
|
|
3705
|
+
process.stderr.write(` ${bold2("Description:")} ${entry.description}
|
|
3706
|
+
`);
|
|
3707
|
+
process.stderr.write(` ${bold2("Platforms:")} ${entry.platforms.join(", ")}
|
|
3708
|
+
`);
|
|
3709
|
+
process.stderr.write(` ${bold2("Tags:")} ${entry.tags.join(", ")}
|
|
3710
|
+
`);
|
|
3711
|
+
process.stderr.write(` ${bold2("Author:")} ${entry.author}
|
|
3712
|
+
`);
|
|
3713
|
+
process.stderr.write(` ${bold2("Source:")} ${REGISTRY_REPO}/${entry.path}
|
|
3714
|
+
`);
|
|
3715
|
+
} else {
|
|
3716
|
+
process.stderr.write(` ${bold2("Name:")} ${cyan2(name)}
|
|
3717
|
+
`);
|
|
3718
|
+
process.stderr.write(` ${dim2("(not found in remote registry)")}
|
|
3719
|
+
`);
|
|
3720
|
+
}
|
|
3721
|
+
process.stderr.write(`
|
|
3722
|
+
`);
|
|
3723
|
+
if (lockEntry) {
|
|
3724
|
+
process.stderr.write(` ${bold2("Installed:")} ${green("yes")}
|
|
3725
|
+
`);
|
|
3726
|
+
process.stderr.write(` ${bold2("Hash:")} ${dim2(lockEntry.computedHash.slice(0, 10))}
|
|
3727
|
+
`);
|
|
3728
|
+
process.stderr.write(` ${bold2("Source:")} ${lockEntry.source}
|
|
3729
|
+
`);
|
|
3730
|
+
} else {
|
|
3731
|
+
process.stderr.write(` ${bold2("Installed:")} ${dim2("no")}
|
|
3732
|
+
`);
|
|
3733
|
+
}
|
|
3734
|
+
process.stderr.write(`
|
|
3735
|
+
`);
|
|
3736
|
+
}
|
|
3737
|
+
function printSkillsHelp() {
|
|
3738
|
+
process.stderr.write(`
|
|
3739
|
+
${bold2("Usage:")}
|
|
3740
|
+
locus skills <subcommand> [options]
|
|
3741
|
+
|
|
3742
|
+
${bold2("Subcommands:")}
|
|
3743
|
+
${cyan2("list")} List available skills from the registry
|
|
3744
|
+
${cyan2("list")} ${dim2("--installed")} List locally installed skills
|
|
3745
|
+
${cyan2("install")} ${dim2("<name>")} Install a skill from the registry
|
|
3746
|
+
${cyan2("remove")} ${dim2("<name>")} Remove an installed skill
|
|
3747
|
+
${cyan2("update")} ${dim2("[name]")} Update installed skill(s) from registry
|
|
3748
|
+
${cyan2("info")} ${dim2("<name>")} Show skill metadata and install status
|
|
3749
|
+
|
|
3750
|
+
${bold2("Examples:")}
|
|
3751
|
+
locus skills list ${dim2("# Browse available skills")}
|
|
3752
|
+
locus skills list --installed ${dim2("# Show installed skills")}
|
|
3753
|
+
locus skills install code-review ${dim2("# Install a skill")}
|
|
3754
|
+
locus skills remove code-review ${dim2("# Remove a skill")}
|
|
3755
|
+
locus skills update ${dim2("# Update all installed skills")}
|
|
3756
|
+
locus skills info code-review ${dim2("# Show skill details")}
|
|
3757
|
+
|
|
3758
|
+
`);
|
|
3759
|
+
}
|
|
3760
|
+
async function skillsCommand(args, flags) {
|
|
3761
|
+
const subcommand = args[0] ?? "list";
|
|
3762
|
+
switch (subcommand) {
|
|
3763
|
+
case "help": {
|
|
3764
|
+
printSkillsHelp();
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
case "list": {
|
|
3768
|
+
const isInstalled = flags.installed !== undefined || args.includes("--installed");
|
|
3769
|
+
if (isInstalled) {
|
|
3770
|
+
listInstalledSkills();
|
|
3771
|
+
} else {
|
|
3772
|
+
await listRemoteSkills();
|
|
3773
|
+
}
|
|
3774
|
+
break;
|
|
3775
|
+
}
|
|
3776
|
+
case "install": {
|
|
3777
|
+
const skillName = args[1];
|
|
3778
|
+
await installRemoteSkill(skillName);
|
|
3779
|
+
break;
|
|
3780
|
+
}
|
|
3781
|
+
case "remove": {
|
|
3782
|
+
const skillName = args[1];
|
|
3783
|
+
await removeInstalledSkill(skillName);
|
|
3784
|
+
break;
|
|
3785
|
+
}
|
|
3786
|
+
case "update": {
|
|
3787
|
+
const skillName = args[1];
|
|
3788
|
+
await updateSkills(skillName);
|
|
3789
|
+
break;
|
|
3790
|
+
}
|
|
3791
|
+
case "info": {
|
|
3792
|
+
const skillName = args[1];
|
|
3793
|
+
await infoSkill(skillName);
|
|
3794
|
+
break;
|
|
3795
|
+
}
|
|
3796
|
+
default:
|
|
3797
|
+
process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
|
|
3798
|
+
`);
|
|
3799
|
+
process.stderr.write(` Available: ${bold2("list")}, ${bold2("install")}, ${bold2("remove")}, ${bold2("update")}, ${bold2("info")}
|
|
3800
|
+
`);
|
|
3801
|
+
process.exit(1);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
var init_skills = __esm(() => {
|
|
3805
|
+
init_table();
|
|
3806
|
+
init_terminal();
|
|
3807
|
+
init_installer();
|
|
3808
|
+
init_lock();
|
|
3809
|
+
init_registry2();
|
|
3810
|
+
});
|
|
3811
|
+
|
|
3217
3812
|
// src/commands/config.ts
|
|
3218
3813
|
var exports_config = {};
|
|
3219
3814
|
__export(exports_config, {
|
|
@@ -3277,10 +3872,7 @@ ${bold2("Locus Configuration")}
|
|
|
3277
3872
|
},
|
|
3278
3873
|
{
|
|
3279
3874
|
title: "Sprint",
|
|
3280
|
-
entries: [
|
|
3281
|
-
["Active", config.sprint.active ?? dim2("(none)")],
|
|
3282
|
-
["Stop on Failure", String(config.sprint.stopOnFailure)]
|
|
3283
|
-
]
|
|
3875
|
+
entries: [["Stop on Failure", String(config.sprint.stopOnFailure)]]
|
|
3284
3876
|
},
|
|
3285
3877
|
{
|
|
3286
3878
|
title: "Logging",
|
|
@@ -3353,16 +3945,16 @@ __export(exports_logs, {
|
|
|
3353
3945
|
logsCommand: () => logsCommand
|
|
3354
3946
|
});
|
|
3355
3947
|
import {
|
|
3356
|
-
existsSync as
|
|
3948
|
+
existsSync as existsSync12,
|
|
3357
3949
|
readdirSync as readdirSync2,
|
|
3358
|
-
readFileSync as
|
|
3950
|
+
readFileSync as readFileSync8,
|
|
3359
3951
|
statSync as statSync2,
|
|
3360
3952
|
unlinkSync as unlinkSync2
|
|
3361
3953
|
} from "node:fs";
|
|
3362
|
-
import { join as
|
|
3954
|
+
import { join as join12 } from "node:path";
|
|
3363
3955
|
async function logsCommand(cwd, options) {
|
|
3364
|
-
const logsDir =
|
|
3365
|
-
if (!
|
|
3956
|
+
const logsDir = join12(cwd, ".locus", "logs");
|
|
3957
|
+
if (!existsSync12(logsDir)) {
|
|
3366
3958
|
process.stderr.write(`${dim2("No logs found.")}
|
|
3367
3959
|
`);
|
|
3368
3960
|
return;
|
|
@@ -3382,7 +3974,7 @@ async function logsCommand(cwd, options) {
|
|
|
3382
3974
|
return viewLog(logFiles[0], options.level, options.lines ?? 50);
|
|
3383
3975
|
}
|
|
3384
3976
|
function viewLog(logFile, levelFilter, maxLines) {
|
|
3385
|
-
const content =
|
|
3977
|
+
const content = readFileSync8(logFile, "utf-8");
|
|
3386
3978
|
const lines = content.trim().split(`
|
|
3387
3979
|
`).filter(Boolean);
|
|
3388
3980
|
process.stderr.write(`
|
|
@@ -3417,9 +4009,9 @@ async function tailLog(logFile, levelFilter) {
|
|
|
3417
4009
|
process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
|
|
3418
4010
|
|
|
3419
4011
|
`);
|
|
3420
|
-
let lastSize =
|
|
3421
|
-
if (
|
|
3422
|
-
const content =
|
|
4012
|
+
let lastSize = existsSync12(logFile) ? statSync2(logFile).size : 0;
|
|
4013
|
+
if (existsSync12(logFile)) {
|
|
4014
|
+
const content = readFileSync8(logFile, "utf-8");
|
|
3423
4015
|
const lines = content.trim().split(`
|
|
3424
4016
|
`).filter(Boolean);
|
|
3425
4017
|
const recent = lines.slice(-10);
|
|
@@ -3437,12 +4029,12 @@ async function tailLog(logFile, levelFilter) {
|
|
|
3437
4029
|
}
|
|
3438
4030
|
return new Promise((resolve) => {
|
|
3439
4031
|
const interval = setInterval(() => {
|
|
3440
|
-
if (!
|
|
4032
|
+
if (!existsSync12(logFile))
|
|
3441
4033
|
return;
|
|
3442
4034
|
const currentSize = statSync2(logFile).size;
|
|
3443
4035
|
if (currentSize <= lastSize)
|
|
3444
4036
|
return;
|
|
3445
|
-
const content =
|
|
4037
|
+
const content = readFileSync8(logFile, "utf-8");
|
|
3446
4038
|
const allLines = content.trim().split(`
|
|
3447
4039
|
`).filter(Boolean);
|
|
3448
4040
|
const oldContent = content.slice(0, lastSize);
|
|
@@ -3497,7 +4089,7 @@ function cleanLogs(logsDir) {
|
|
|
3497
4089
|
`);
|
|
3498
4090
|
}
|
|
3499
4091
|
function getLogFiles(logsDir) {
|
|
3500
|
-
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) =>
|
|
4092
|
+
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join12(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
|
|
3501
4093
|
}
|
|
3502
4094
|
function formatEntry(entry) {
|
|
3503
4095
|
const time = dim2(new Date(entry.ts).toLocaleTimeString());
|
|
@@ -3807,9 +4399,9 @@ var init_stream_renderer = __esm(() => {
|
|
|
3807
4399
|
|
|
3808
4400
|
// src/repl/clipboard.ts
|
|
3809
4401
|
import { execSync as execSync6 } from "node:child_process";
|
|
3810
|
-
import { existsSync as
|
|
4402
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
|
|
3811
4403
|
import { tmpdir } from "node:os";
|
|
3812
|
-
import { join as
|
|
4404
|
+
import { join as join13 } from "node:path";
|
|
3813
4405
|
function readClipboardImage() {
|
|
3814
4406
|
if (process.platform === "darwin") {
|
|
3815
4407
|
return readMacOSClipboardImage();
|
|
@@ -3820,14 +4412,14 @@ function readClipboardImage() {
|
|
|
3820
4412
|
return null;
|
|
3821
4413
|
}
|
|
3822
4414
|
function ensureStableDir() {
|
|
3823
|
-
if (!
|
|
3824
|
-
|
|
4415
|
+
if (!existsSync13(STABLE_DIR)) {
|
|
4416
|
+
mkdirSync9(STABLE_DIR, { recursive: true });
|
|
3825
4417
|
}
|
|
3826
4418
|
}
|
|
3827
4419
|
function readMacOSClipboardImage() {
|
|
3828
4420
|
try {
|
|
3829
4421
|
ensureStableDir();
|
|
3830
|
-
const destPath =
|
|
4422
|
+
const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3831
4423
|
const script = [
|
|
3832
4424
|
`set destPath to POSIX file "${destPath}"`,
|
|
3833
4425
|
"try",
|
|
@@ -3851,7 +4443,7 @@ function readMacOSClipboardImage() {
|
|
|
3851
4443
|
timeout: 5000,
|
|
3852
4444
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3853
4445
|
}).trim();
|
|
3854
|
-
if (result === "ok" &&
|
|
4446
|
+
if (result === "ok" && existsSync13(destPath)) {
|
|
3855
4447
|
return destPath;
|
|
3856
4448
|
}
|
|
3857
4449
|
} catch {}
|
|
@@ -3864,9 +4456,9 @@ function readLinuxClipboardImage() {
|
|
|
3864
4456
|
return null;
|
|
3865
4457
|
}
|
|
3866
4458
|
ensureStableDir();
|
|
3867
|
-
const destPath =
|
|
4459
|
+
const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3868
4460
|
execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
3869
|
-
if (
|
|
4461
|
+
if (existsSync13(destPath)) {
|
|
3870
4462
|
return destPath;
|
|
3871
4463
|
}
|
|
3872
4464
|
} catch {}
|
|
@@ -3874,13 +4466,13 @@ function readLinuxClipboardImage() {
|
|
|
3874
4466
|
}
|
|
3875
4467
|
var STABLE_DIR;
|
|
3876
4468
|
var init_clipboard = __esm(() => {
|
|
3877
|
-
STABLE_DIR =
|
|
4469
|
+
STABLE_DIR = join13(tmpdir(), "locus-images");
|
|
3878
4470
|
});
|
|
3879
4471
|
|
|
3880
4472
|
// src/repl/image-detect.ts
|
|
3881
|
-
import { copyFileSync, existsSync as
|
|
4473
|
+
import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10 } from "node:fs";
|
|
3882
4474
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
3883
|
-
import { basename, extname, join as
|
|
4475
|
+
import { basename, extname, join as join14, resolve } from "node:path";
|
|
3884
4476
|
function detectImages(input) {
|
|
3885
4477
|
const detected = [];
|
|
3886
4478
|
const byResolved = new Map;
|
|
@@ -3974,15 +4566,15 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
3974
4566
|
return dedupeByResolvedPath(selected);
|
|
3975
4567
|
}
|
|
3976
4568
|
function relocateImages(images, projectRoot) {
|
|
3977
|
-
const targetDir =
|
|
4569
|
+
const targetDir = join14(projectRoot, ".locus", "tmp", "images");
|
|
3978
4570
|
for (const img of images) {
|
|
3979
4571
|
if (!img.exists)
|
|
3980
4572
|
continue;
|
|
3981
4573
|
try {
|
|
3982
|
-
if (!
|
|
3983
|
-
|
|
4574
|
+
if (!existsSync14(targetDir)) {
|
|
4575
|
+
mkdirSync10(targetDir, { recursive: true });
|
|
3984
4576
|
}
|
|
3985
|
-
const dest =
|
|
4577
|
+
const dest = join14(targetDir, basename(img.stablePath));
|
|
3986
4578
|
copyFileSync(img.stablePath, dest);
|
|
3987
4579
|
img.stablePath = dest;
|
|
3988
4580
|
} catch {}
|
|
@@ -3994,7 +4586,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
3994
4586
|
return;
|
|
3995
4587
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
3996
4588
|
if (resolved.startsWith("~/")) {
|
|
3997
|
-
resolved =
|
|
4589
|
+
resolved = join14(homedir3(), resolved.slice(2));
|
|
3998
4590
|
}
|
|
3999
4591
|
resolved = resolve(resolved);
|
|
4000
4592
|
const existing = byResolved.get(resolved);
|
|
@@ -4007,7 +4599,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
4007
4599
|
]);
|
|
4008
4600
|
return;
|
|
4009
4601
|
}
|
|
4010
|
-
const exists =
|
|
4602
|
+
const exists = existsSync14(resolved);
|
|
4011
4603
|
let stablePath = resolved;
|
|
4012
4604
|
if (exists) {
|
|
4013
4605
|
stablePath = copyToStable(resolved);
|
|
@@ -4061,10 +4653,10 @@ function dedupeByResolvedPath(images) {
|
|
|
4061
4653
|
}
|
|
4062
4654
|
function copyToStable(sourcePath) {
|
|
4063
4655
|
try {
|
|
4064
|
-
if (!
|
|
4065
|
-
|
|
4656
|
+
if (!existsSync14(STABLE_DIR2)) {
|
|
4657
|
+
mkdirSync10(STABLE_DIR2, { recursive: true });
|
|
4066
4658
|
}
|
|
4067
|
-
const dest =
|
|
4659
|
+
const dest = join14(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
4068
4660
|
copyFileSync(sourcePath, dest);
|
|
4069
4661
|
return dest;
|
|
4070
4662
|
} catch {
|
|
@@ -4084,7 +4676,7 @@ var init_image_detect = __esm(() => {
|
|
|
4084
4676
|
".tif",
|
|
4085
4677
|
".tiff"
|
|
4086
4678
|
]);
|
|
4087
|
-
STABLE_DIR2 =
|
|
4679
|
+
STABLE_DIR2 = join14(tmpdir2(), "locus-images");
|
|
4088
4680
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
4089
4681
|
});
|
|
4090
4682
|
|
|
@@ -5093,21 +5685,21 @@ var init_claude = __esm(() => {
|
|
|
5093
5685
|
import { exec } from "node:child_process";
|
|
5094
5686
|
import {
|
|
5095
5687
|
cpSync,
|
|
5096
|
-
existsSync as
|
|
5097
|
-
mkdirSync as
|
|
5688
|
+
existsSync as existsSync15,
|
|
5689
|
+
mkdirSync as mkdirSync11,
|
|
5098
5690
|
mkdtempSync,
|
|
5099
5691
|
readdirSync as readdirSync3,
|
|
5100
|
-
readFileSync as
|
|
5101
|
-
rmSync,
|
|
5692
|
+
readFileSync as readFileSync9,
|
|
5693
|
+
rmSync as rmSync2,
|
|
5102
5694
|
statSync as statSync3
|
|
5103
5695
|
} from "node:fs";
|
|
5104
5696
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
5105
|
-
import { dirname as dirname3, join as
|
|
5697
|
+
import { dirname as dirname3, join as join15, relative } from "node:path";
|
|
5106
5698
|
import { promisify } from "node:util";
|
|
5107
5699
|
function parseIgnoreFile(filePath) {
|
|
5108
|
-
if (!
|
|
5700
|
+
if (!existsSync15(filePath))
|
|
5109
5701
|
return [];
|
|
5110
|
-
const content =
|
|
5702
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
5111
5703
|
const rules = [];
|
|
5112
5704
|
for (const rawLine of content.split(`
|
|
5113
5705
|
`)) {
|
|
@@ -5175,7 +5767,7 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
5175
5767
|
for (const name of entries) {
|
|
5176
5768
|
if (SKIP_DIRS.has(name))
|
|
5177
5769
|
continue;
|
|
5178
|
-
const fullPath =
|
|
5770
|
+
const fullPath = join15(dir, name);
|
|
5179
5771
|
let stat = null;
|
|
5180
5772
|
try {
|
|
5181
5773
|
stat = statSync3(fullPath);
|
|
@@ -5209,7 +5801,7 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
5209
5801
|
}
|
|
5210
5802
|
function backupIgnoredFiles(projectRoot) {
|
|
5211
5803
|
const log = getLogger();
|
|
5212
|
-
const ignorePath =
|
|
5804
|
+
const ignorePath = join15(projectRoot, ".sandboxignore");
|
|
5213
5805
|
const rules = parseIgnoreFile(ignorePath);
|
|
5214
5806
|
if (rules.length === 0)
|
|
5215
5807
|
return NOOP_BACKUP;
|
|
@@ -5218,7 +5810,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5218
5810
|
return NOOP_BACKUP;
|
|
5219
5811
|
let backupDir;
|
|
5220
5812
|
try {
|
|
5221
|
-
backupDir = mkdtempSync(
|
|
5813
|
+
backupDir = mkdtempSync(join15(tmpdir3(), "locus-sandbox-backup-"));
|
|
5222
5814
|
} catch (err) {
|
|
5223
5815
|
log.debug("Failed to create sandbox backup dir", {
|
|
5224
5816
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -5228,9 +5820,9 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5228
5820
|
const backed = [];
|
|
5229
5821
|
for (const src of paths) {
|
|
5230
5822
|
const rel = relative(projectRoot, src);
|
|
5231
|
-
const dest =
|
|
5823
|
+
const dest = join15(backupDir, rel);
|
|
5232
5824
|
try {
|
|
5233
|
-
|
|
5825
|
+
mkdirSync11(dirname3(dest), { recursive: true });
|
|
5234
5826
|
cpSync(src, dest, { recursive: true, preserveTimestamps: true });
|
|
5235
5827
|
backed.push({ src, dest });
|
|
5236
5828
|
} catch (err) {
|
|
@@ -5241,7 +5833,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5241
5833
|
}
|
|
5242
5834
|
}
|
|
5243
5835
|
if (backed.length === 0) {
|
|
5244
|
-
|
|
5836
|
+
rmSync2(backupDir, { recursive: true, force: true });
|
|
5245
5837
|
return NOOP_BACKUP;
|
|
5246
5838
|
}
|
|
5247
5839
|
log.debug("Backed up sandbox-ignored files", {
|
|
@@ -5252,7 +5844,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5252
5844
|
restore() {
|
|
5253
5845
|
for (const { src, dest } of backed) {
|
|
5254
5846
|
try {
|
|
5255
|
-
|
|
5847
|
+
mkdirSync11(dirname3(src), { recursive: true });
|
|
5256
5848
|
cpSync(dest, src, { recursive: true, preserveTimestamps: true });
|
|
5257
5849
|
} catch (err) {
|
|
5258
5850
|
log.debug("Failed to restore ignored file (potential data loss)", {
|
|
@@ -5262,7 +5854,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5262
5854
|
}
|
|
5263
5855
|
}
|
|
5264
5856
|
try {
|
|
5265
|
-
|
|
5857
|
+
rmSync2(backupDir, { recursive: true, force: true });
|
|
5266
5858
|
} catch {}
|
|
5267
5859
|
log.debug("Restored sandbox-ignored files", { count: backed.length });
|
|
5268
5860
|
}
|
|
@@ -5270,7 +5862,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5270
5862
|
}
|
|
5271
5863
|
async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
|
|
5272
5864
|
const log = getLogger();
|
|
5273
|
-
const ignorePath =
|
|
5865
|
+
const ignorePath = join15(projectRoot, ".sandboxignore");
|
|
5274
5866
|
const rules = parseIgnoreFile(ignorePath);
|
|
5275
5867
|
if (rules.length === 0)
|
|
5276
5868
|
return;
|
|
@@ -5488,17 +6080,22 @@ class SandboxedClaudeRunner {
|
|
|
5488
6080
|
timer.unref();
|
|
5489
6081
|
}
|
|
5490
6082
|
async isSandboxRunning() {
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
6083
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
6084
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
6085
|
+
const execAsync2 = promisify2(exec2);
|
|
6086
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
6087
|
+
try {
|
|
6088
|
+
const { stdout } = await execAsync2("docker sandbox ls", {
|
|
6089
|
+
timeout: 15000
|
|
6090
|
+
});
|
|
6091
|
+
return stdout.includes(this.sandboxName);
|
|
6092
|
+
} catch {
|
|
6093
|
+
if (attempt < 2) {
|
|
6094
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
5501
6097
|
}
|
|
6098
|
+
return false;
|
|
5502
6099
|
}
|
|
5503
6100
|
}
|
|
5504
6101
|
function formatToolCall2(name, input) {
|
|
@@ -5899,17 +6496,22 @@ class SandboxedCodexRunner {
|
|
|
5899
6496
|
timer.unref();
|
|
5900
6497
|
}
|
|
5901
6498
|
async isSandboxRunning() {
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
6499
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
6500
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
6501
|
+
const execAsync2 = promisify2(exec2);
|
|
6502
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
6503
|
+
try {
|
|
6504
|
+
const { stdout } = await execAsync2("docker sandbox ls", {
|
|
6505
|
+
timeout: 15000
|
|
6506
|
+
});
|
|
6507
|
+
return stdout.includes(this.sandboxName);
|
|
6508
|
+
} catch {
|
|
6509
|
+
if (attempt < 2) {
|
|
6510
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
5912
6513
|
}
|
|
6514
|
+
return false;
|
|
5913
6515
|
}
|
|
5914
6516
|
async ensureCodexInstalled(name) {
|
|
5915
6517
|
const { promisify: promisify2 } = await import("node:util");
|
|
@@ -6166,115 +6768,6 @@ var init_run_ai = __esm(() => {
|
|
|
6166
6768
|
init_runner();
|
|
6167
6769
|
});
|
|
6168
6770
|
|
|
6169
|
-
// src/display/table.ts
|
|
6170
|
-
function renderTable(columns, rows, options = {}) {
|
|
6171
|
-
const {
|
|
6172
|
-
indent = 2,
|
|
6173
|
-
headerSeparator = true,
|
|
6174
|
-
maxRows = 0,
|
|
6175
|
-
emptyMessage = "No results."
|
|
6176
|
-
} = options;
|
|
6177
|
-
if (rows.length === 0) {
|
|
6178
|
-
return `${" ".repeat(indent)}${dim2(emptyMessage)}`;
|
|
6179
|
-
}
|
|
6180
|
-
const termWidth = getCapabilities().columns;
|
|
6181
|
-
const indentStr = " ".repeat(indent);
|
|
6182
|
-
const gap = 2;
|
|
6183
|
-
const formattedRows = rows.map((row) => {
|
|
6184
|
-
const formatted = {};
|
|
6185
|
-
for (const col of columns) {
|
|
6186
|
-
if (col.format) {
|
|
6187
|
-
formatted[col.key] = col.format(row[col.key], row);
|
|
6188
|
-
} else {
|
|
6189
|
-
const val = row[col.key];
|
|
6190
|
-
formatted[col.key] = val === null || val === undefined ? "" : String(val);
|
|
6191
|
-
}
|
|
6192
|
-
}
|
|
6193
|
-
return formatted;
|
|
6194
|
-
});
|
|
6195
|
-
const colWidths = columns.map((col, _i) => {
|
|
6196
|
-
const headerWidth = stripAnsi(col.header).length;
|
|
6197
|
-
const minWidth = col.minWidth ?? headerWidth;
|
|
6198
|
-
let maxContent = headerWidth;
|
|
6199
|
-
for (const row of formattedRows) {
|
|
6200
|
-
const cellWidth = stripAnsi(row[col.key] ?? "").length;
|
|
6201
|
-
if (cellWidth > maxContent)
|
|
6202
|
-
maxContent = cellWidth;
|
|
6203
|
-
}
|
|
6204
|
-
let width = Math.max(minWidth, maxContent);
|
|
6205
|
-
if (col.maxWidth && col.maxWidth > 0) {
|
|
6206
|
-
width = Math.min(width, col.maxWidth);
|
|
6207
|
-
}
|
|
6208
|
-
return width;
|
|
6209
|
-
});
|
|
6210
|
-
const totalWidth = indent + colWidths.reduce((s, w) => s + w, 0) + gap * (columns.length - 1);
|
|
6211
|
-
if (totalWidth > termWidth && columns.length > 1) {
|
|
6212
|
-
const overflow = totalWidth - termWidth;
|
|
6213
|
-
let widestIdx = 0;
|
|
6214
|
-
let widestSize = 0;
|
|
6215
|
-
for (let i = 0;i < columns.length; i++) {
|
|
6216
|
-
if (!columns[i].maxWidth && colWidths[i] > widestSize) {
|
|
6217
|
-
widestSize = colWidths[i];
|
|
6218
|
-
widestIdx = i;
|
|
6219
|
-
}
|
|
6220
|
-
}
|
|
6221
|
-
colWidths[widestIdx] = Math.max(10, colWidths[widestIdx] - overflow);
|
|
6222
|
-
}
|
|
6223
|
-
const lines = [];
|
|
6224
|
-
const headerParts = columns.map((col, i) => alignCell(bold2(col.header), colWidths[i], col.align ?? "left"));
|
|
6225
|
-
lines.push(`${indentStr}${headerParts.join(" ".repeat(gap))}`);
|
|
6226
|
-
if (headerSeparator) {
|
|
6227
|
-
const sep = columns.map((_, i) => gray2("─".repeat(colWidths[i]))).join(" ".repeat(gap));
|
|
6228
|
-
lines.push(`${indentStr}${sep}`);
|
|
6229
|
-
}
|
|
6230
|
-
const displayRows = maxRows > 0 ? formattedRows.slice(0, maxRows) : formattedRows;
|
|
6231
|
-
for (const row of displayRows) {
|
|
6232
|
-
const cellParts = columns.map((col, i) => {
|
|
6233
|
-
const raw = row[col.key] ?? "";
|
|
6234
|
-
return alignCell(raw, colWidths[i], col.align ?? "left");
|
|
6235
|
-
});
|
|
6236
|
-
lines.push(`${indentStr}${cellParts.join(" ".repeat(gap))}`);
|
|
6237
|
-
}
|
|
6238
|
-
if (maxRows > 0 && formattedRows.length > maxRows) {
|
|
6239
|
-
const remaining = formattedRows.length - maxRows;
|
|
6240
|
-
lines.push(`${indentStr}${dim2(`... and ${remaining} more`)}`);
|
|
6241
|
-
}
|
|
6242
|
-
return lines.join(`
|
|
6243
|
-
`);
|
|
6244
|
-
}
|
|
6245
|
-
function alignCell(text, width, align) {
|
|
6246
|
-
const visual = stripAnsi(text).length;
|
|
6247
|
-
if (visual > width) {
|
|
6248
|
-
const stripped = stripAnsi(text);
|
|
6249
|
-
return `${stripped.slice(0, width - 1)}…`;
|
|
6250
|
-
}
|
|
6251
|
-
const padding = width - visual;
|
|
6252
|
-
switch (align) {
|
|
6253
|
-
case "right":
|
|
6254
|
-
return " ".repeat(padding) + text;
|
|
6255
|
-
case "center": {
|
|
6256
|
-
const left = Math.floor(padding / 2);
|
|
6257
|
-
const right = padding - left;
|
|
6258
|
-
return " ".repeat(left) + text + " ".repeat(right);
|
|
6259
|
-
}
|
|
6260
|
-
default:
|
|
6261
|
-
return text + " ".repeat(padding);
|
|
6262
|
-
}
|
|
6263
|
-
}
|
|
6264
|
-
function renderDetails(entries, options = {}) {
|
|
6265
|
-
const { indent = 2, labelWidth: fixedWidth } = options;
|
|
6266
|
-
const indentStr = " ".repeat(indent);
|
|
6267
|
-
const labelWidth = fixedWidth ?? Math.max(...entries.map((e) => stripAnsi(e.label).length)) + 1;
|
|
6268
|
-
return entries.map((entry) => {
|
|
6269
|
-
const label = padEnd(dim2(`${entry.label}:`), labelWidth + 1);
|
|
6270
|
-
return `${indentStr}${label} ${entry.value}`;
|
|
6271
|
-
}).join(`
|
|
6272
|
-
`);
|
|
6273
|
-
}
|
|
6274
|
-
var init_table = __esm(() => {
|
|
6275
|
-
init_terminal();
|
|
6276
|
-
});
|
|
6277
|
-
|
|
6278
6771
|
// src/commands/issue.ts
|
|
6279
6772
|
var exports_issue = {};
|
|
6280
6773
|
__export(exports_issue, {
|
|
@@ -7013,9 +7506,6 @@ async function sprintCommand(projectRoot, args) {
|
|
|
7013
7506
|
case "show":
|
|
7014
7507
|
await sprintShow(projectRoot, parsed);
|
|
7015
7508
|
break;
|
|
7016
|
-
case "active":
|
|
7017
|
-
await sprintActive(projectRoot, parsed);
|
|
7018
|
-
break;
|
|
7019
7509
|
case "order":
|
|
7020
7510
|
await sprintOrder(projectRoot, parsed);
|
|
7021
7511
|
break;
|
|
@@ -7056,8 +7546,6 @@ async function sprintCreate(projectRoot, parsed) {
|
|
|
7056
7546
|
process.stderr.write(` Due: ${parsed.flags.due}
|
|
7057
7547
|
`);
|
|
7058
7548
|
}
|
|
7059
|
-
process.stderr.write(` Set as active sprint: ${bold2(`locus sprint active "${title}"`)}
|
|
7060
|
-
`);
|
|
7061
7549
|
} catch (e) {
|
|
7062
7550
|
process.stderr.write(`\r${red2("✗")} Failed to create sprint: ${e.message}
|
|
7063
7551
|
`);
|
|
@@ -7067,8 +7555,6 @@ async function sprintCreate(projectRoot, parsed) {
|
|
|
7067
7555
|
async function sprintList(projectRoot, parsed) {
|
|
7068
7556
|
const config = loadConfig(projectRoot);
|
|
7069
7557
|
const { owner, repo } = config.github;
|
|
7070
|
-
const activeSprint = config.sprint.active;
|
|
7071
|
-
const normalizedActiveSprint = activeSprint ? normalizeMilestoneTitle(activeSprint) : null;
|
|
7072
7558
|
const state = parsed.flags.all ? "all" : "open";
|
|
7073
7559
|
process.stderr.write(`${cyan2("●")} Fetching sprints...`);
|
|
7074
7560
|
let milestones;
|
|
@@ -7094,16 +7580,10 @@ async function sprintList(projectRoot, parsed) {
|
|
|
7094
7580
|
|
|
7095
7581
|
`);
|
|
7096
7582
|
const columns = [
|
|
7097
|
-
{
|
|
7098
|
-
key: "active",
|
|
7099
|
-
header: " ",
|
|
7100
|
-
minWidth: 2,
|
|
7101
|
-
format: (_, row) => normalizedActiveSprint !== null && normalizeMilestoneTitle(String(row.title)) === normalizedActiveSprint ? green("●") : " "
|
|
7102
|
-
},
|
|
7103
7583
|
{
|
|
7104
7584
|
key: "title",
|
|
7105
7585
|
header: "Sprint",
|
|
7106
|
-
format: (v
|
|
7586
|
+
format: (v) => bold2(String(v))
|
|
7107
7587
|
},
|
|
7108
7588
|
{
|
|
7109
7589
|
key: "progress",
|
|
@@ -7148,7 +7628,6 @@ async function sprintList(projectRoot, parsed) {
|
|
|
7148
7628
|
}
|
|
7149
7629
|
];
|
|
7150
7630
|
const rows = milestones.map((m) => ({
|
|
7151
|
-
active: null,
|
|
7152
7631
|
title: m.title,
|
|
7153
7632
|
openIssues: m.openIssues,
|
|
7154
7633
|
closedIssues: m.closedIssues,
|
|
@@ -7159,21 +7638,15 @@ async function sprintList(projectRoot, parsed) {
|
|
|
7159
7638
|
process.stderr.write(`${renderTable(columns, rows)}
|
|
7160
7639
|
|
|
7161
7640
|
`);
|
|
7162
|
-
if (activeSprint) {
|
|
7163
|
-
process.stderr.write(` ${green("●")} = active sprint
|
|
7164
|
-
`);
|
|
7165
|
-
}
|
|
7166
7641
|
}
|
|
7167
7642
|
async function sprintShow(projectRoot, parsed) {
|
|
7168
7643
|
const config = loadConfig(projectRoot);
|
|
7169
7644
|
const { owner, repo } = config.github;
|
|
7170
|
-
const sprintName = parsed.positional[0]
|
|
7645
|
+
const sprintName = parsed.positional[0];
|
|
7171
7646
|
if (!sprintName) {
|
|
7172
|
-
process.stderr.write(`${red2("✗")}
|
|
7647
|
+
process.stderr.write(`${red2("✗")} Missing sprint name.
|
|
7173
7648
|
`);
|
|
7174
7649
|
process.stderr.write(` Usage: ${bold2('locus sprint show "Sprint 1"')}
|
|
7175
|
-
`);
|
|
7176
|
-
process.stderr.write(` Or set active: ${bold2('locus sprint active "Sprint 1"')}
|
|
7177
7650
|
`);
|
|
7178
7651
|
process.exit(1);
|
|
7179
7652
|
}
|
|
@@ -7226,10 +7699,6 @@ ${bold2(`Sprint: ${milestone.title}`)} ${dim2(`(${milestone.state})`)}
|
|
|
7226
7699
|
{
|
|
7227
7700
|
label: "State",
|
|
7228
7701
|
value: milestone.state === "open" ? green("open") : dim2("closed")
|
|
7229
|
-
},
|
|
7230
|
-
{
|
|
7231
|
-
label: "Active",
|
|
7232
|
-
value: config.sprint.active && normalizeMilestoneTitle(config.sprint.active) === normalizeMilestoneTitle(milestone.title) ? green("yes") : dim2("no")
|
|
7233
7702
|
}
|
|
7234
7703
|
]);
|
|
7235
7704
|
process.stderr.write(`${details}
|
|
@@ -7298,49 +7767,12 @@ ${bold2("Tasks")} ${dim2(`(by execution order)`)}
|
|
|
7298
7767
|
process.stderr.write(`
|
|
7299
7768
|
`);
|
|
7300
7769
|
}
|
|
7301
|
-
async function
|
|
7770
|
+
async function sprintOrder(projectRoot, parsed) {
|
|
7302
7771
|
const sprintName = parsed.positional[0];
|
|
7303
7772
|
if (!sprintName) {
|
|
7304
|
-
|
|
7305
|
-
if (config2.sprint.active) {
|
|
7306
|
-
process.stderr.write(`Active sprint: ${bold2(green(config2.sprint.active))}
|
|
7307
|
-
`);
|
|
7308
|
-
} else {
|
|
7309
|
-
process.stderr.write(`${dim2("No active sprint set.")}
|
|
7310
|
-
`);
|
|
7311
|
-
process.stderr.write(` Set one: ${bold2('locus sprint active "Sprint 1"')}
|
|
7312
|
-
`);
|
|
7313
|
-
}
|
|
7314
|
-
return;
|
|
7315
|
-
}
|
|
7316
|
-
const config = loadConfig(projectRoot);
|
|
7317
|
-
const { owner, repo } = config.github;
|
|
7318
|
-
process.stderr.write(`${cyan2("●")} Verifying sprint...`);
|
|
7319
|
-
let milestones;
|
|
7320
|
-
try {
|
|
7321
|
-
milestones = listMilestones(owner, repo, "open", { cwd: projectRoot });
|
|
7322
|
-
} catch (e) {
|
|
7323
|
-
process.stderr.write(`\r${red2("✗")} ${e.message}
|
|
7324
|
-
`);
|
|
7325
|
-
process.exit(1);
|
|
7326
|
-
return;
|
|
7327
|
-
}
|
|
7328
|
-
const found = findMilestoneByTitle(milestones, sprintName);
|
|
7329
|
-
if (!found) {
|
|
7330
|
-
process.stderr.write(`\r${red2("✗")} Sprint "${sprintName}" not found (or is closed).
|
|
7331
|
-
`);
|
|
7332
|
-
process.exit(1);
|
|
7333
|
-
return;
|
|
7334
|
-
}
|
|
7335
|
-
updateConfigValue(projectRoot, "sprint.active", found.title);
|
|
7336
|
-
process.stderr.write(`\r${green("✓")} Active sprint set to "${bold2(found.title)}"
|
|
7773
|
+
process.stderr.write(`${red2("✗")} Missing sprint name.
|
|
7337
7774
|
`);
|
|
7338
|
-
}
|
|
7339
|
-
async function sprintOrder(projectRoot, parsed) {
|
|
7340
|
-
const config = loadConfig(projectRoot);
|
|
7341
|
-
const sprintName = parsed.positional[0] ?? config.sprint.active;
|
|
7342
|
-
if (!sprintName) {
|
|
7343
|
-
process.stderr.write(`${red2("✗")} No sprint specified and no active sprint set.
|
|
7775
|
+
process.stderr.write(` Usage: ${bold2('locus sprint order "Sprint 1" 17 15 16')}
|
|
7344
7776
|
`);
|
|
7345
7777
|
process.exit(1);
|
|
7346
7778
|
}
|
|
@@ -7475,15 +7907,15 @@ ${green("✓")} Sprint order updated.
|
|
|
7475
7907
|
`);
|
|
7476
7908
|
}
|
|
7477
7909
|
async function sprintClose(projectRoot, parsed) {
|
|
7478
|
-
const
|
|
7479
|
-
const sprintName = parsed.positional[0] ?? config.sprint.active;
|
|
7910
|
+
const sprintName = parsed.positional[0];
|
|
7480
7911
|
if (!sprintName) {
|
|
7481
|
-
process.stderr.write(`${red2("✗")}
|
|
7912
|
+
process.stderr.write(`${red2("✗")} Missing sprint name.
|
|
7482
7913
|
`);
|
|
7483
7914
|
process.stderr.write(` Usage: ${bold2('locus sprint close "Sprint 1"')}
|
|
7484
7915
|
`);
|
|
7485
7916
|
process.exit(1);
|
|
7486
7917
|
}
|
|
7918
|
+
const config = loadConfig(projectRoot);
|
|
7487
7919
|
const { owner, repo } = config.github;
|
|
7488
7920
|
process.stderr.write(`${cyan2("●")} Closing sprint "${sprintName}"...`);
|
|
7489
7921
|
let milestones;
|
|
@@ -7512,11 +7944,6 @@ async function sprintClose(projectRoot, parsed) {
|
|
|
7512
7944
|
}
|
|
7513
7945
|
process.stderr.write(`\r${green("✓")} Closed sprint "${bold2(milestone.title)}"
|
|
7514
7946
|
`);
|
|
7515
|
-
if (config.sprint.active && normalizeMilestoneTitle(config.sprint.active) === normalizeMilestoneTitle(milestone.title)) {
|
|
7516
|
-
updateConfigValue(projectRoot, "sprint.active", null);
|
|
7517
|
-
process.stderr.write(` ${dim2(`Cleared active sprint (was ${milestone.title})`)}
|
|
7518
|
-
`);
|
|
7519
|
-
}
|
|
7520
7947
|
}
|
|
7521
7948
|
function getOrder(labels) {
|
|
7522
7949
|
for (const label of labels) {
|
|
@@ -7567,7 +7994,6 @@ ${bold2("Subcommands:")}
|
|
|
7567
7994
|
${cyan2("create")} ${dim2("(c)")} Create a new sprint
|
|
7568
7995
|
${cyan2("list")} ${dim2("(ls)")} List sprints (default)
|
|
7569
7996
|
${cyan2("show")} Show sprint details and task order
|
|
7570
|
-
${cyan2("active")} Set or show the active sprint
|
|
7571
7997
|
${cyan2("order")} Reorder sprint tasks
|
|
7572
7998
|
${cyan2("close")} Close a sprint
|
|
7573
7999
|
|
|
@@ -7585,7 +8011,6 @@ ${bold2("Examples:")}
|
|
|
7585
8011
|
locus sprint create "Sprint 1" --due 2026-03-07
|
|
7586
8012
|
locus sprint list
|
|
7587
8013
|
locus sprint show "Sprint 1"
|
|
7588
|
-
locus sprint active "Sprint 1"
|
|
7589
8014
|
locus sprint order "Sprint 1" 17 15 16
|
|
7590
8015
|
locus sprint order "Sprint 1" --show
|
|
7591
8016
|
locus sprint close "Sprint 1"
|
|
@@ -7603,8 +8028,8 @@ var init_sprint = __esm(() => {
|
|
|
7603
8028
|
|
|
7604
8029
|
// src/core/prompt-builder.ts
|
|
7605
8030
|
import { execSync as execSync9 } from "node:child_process";
|
|
7606
|
-
import { existsSync as
|
|
7607
|
-
import { join as
|
|
8031
|
+
import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
8032
|
+
import { join as join16 } from "node:path";
|
|
7608
8033
|
function buildExecutionPrompt(ctx) {
|
|
7609
8034
|
const sections = [];
|
|
7610
8035
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -7634,7 +8059,7 @@ function buildFeedbackPrompt(ctx) {
|
|
|
7634
8059
|
}
|
|
7635
8060
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
7636
8061
|
const sections = [];
|
|
7637
|
-
const locusmd = readFileSafe(
|
|
8062
|
+
const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
|
|
7638
8063
|
if (locusmd) {
|
|
7639
8064
|
sections.push(`<project-instructions>
|
|
7640
8065
|
${locusmd}
|
|
@@ -7663,7 +8088,7 @@ ${userMessage}
|
|
|
7663
8088
|
}
|
|
7664
8089
|
function buildSystemContext(projectRoot) {
|
|
7665
8090
|
const parts = [];
|
|
7666
|
-
const locusmd = readFileSafe(
|
|
8091
|
+
const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
|
|
7667
8092
|
if (locusmd) {
|
|
7668
8093
|
parts.push(`<project-instructions>
|
|
7669
8094
|
${locusmd}
|
|
@@ -7671,12 +8096,12 @@ ${locusmd}
|
|
|
7671
8096
|
}
|
|
7672
8097
|
parts.push(`<past-learnings>
|
|
7673
8098
|
Past learnings are located in \`.locus/LEARNINGS.md\`.</past-learnings>`);
|
|
7674
|
-
const discussionsDir =
|
|
7675
|
-
if (
|
|
8099
|
+
const discussionsDir = join16(projectRoot, ".locus", "discussions");
|
|
8100
|
+
if (existsSync16(discussionsDir)) {
|
|
7676
8101
|
try {
|
|
7677
8102
|
const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
7678
8103
|
for (const file of files) {
|
|
7679
|
-
const content = readFileSafe(
|
|
8104
|
+
const content = readFileSafe(join16(discussionsDir, file));
|
|
7680
8105
|
if (content) {
|
|
7681
8106
|
const name = file.replace(".md", "");
|
|
7682
8107
|
parts.push(`<discussion name="${name}">
|
|
@@ -7829,9 +8254,9 @@ function buildFeedbackInstructions() {
|
|
|
7829
8254
|
}
|
|
7830
8255
|
function readFileSafe(path) {
|
|
7831
8256
|
try {
|
|
7832
|
-
if (!
|
|
8257
|
+
if (!existsSync16(path))
|
|
7833
8258
|
return null;
|
|
7834
|
-
return
|
|
8259
|
+
return readFileSync10(path, "utf-8");
|
|
7835
8260
|
} catch {
|
|
7836
8261
|
return null;
|
|
7837
8262
|
}
|
|
@@ -8309,7 +8734,7 @@ var init_commands = __esm(() => {
|
|
|
8309
8734
|
|
|
8310
8735
|
// src/repl/completions.ts
|
|
8311
8736
|
import { readdirSync as readdirSync5 } from "node:fs";
|
|
8312
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
8737
|
+
import { basename as basename2, dirname as dirname4, join as join17 } from "node:path";
|
|
8313
8738
|
|
|
8314
8739
|
class SlashCommandCompletion {
|
|
8315
8740
|
commands;
|
|
@@ -8364,7 +8789,7 @@ class FilePathCompletion {
|
|
|
8364
8789
|
}
|
|
8365
8790
|
findMatches(partial) {
|
|
8366
8791
|
try {
|
|
8367
|
-
const dir = partial.includes("/") ?
|
|
8792
|
+
const dir = partial.includes("/") ? join17(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
8368
8793
|
const prefix = basename2(partial);
|
|
8369
8794
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
8370
8795
|
return entries.filter((e) => {
|
|
@@ -8400,14 +8825,14 @@ class CombinedCompletion {
|
|
|
8400
8825
|
var init_completions = () => {};
|
|
8401
8826
|
|
|
8402
8827
|
// src/repl/input-history.ts
|
|
8403
|
-
import { existsSync as
|
|
8404
|
-
import { dirname as dirname5, join as
|
|
8828
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "node:fs";
|
|
8829
|
+
import { dirname as dirname5, join as join18 } from "node:path";
|
|
8405
8830
|
|
|
8406
8831
|
class InputHistory {
|
|
8407
8832
|
entries = [];
|
|
8408
8833
|
filePath;
|
|
8409
8834
|
constructor(projectRoot) {
|
|
8410
|
-
this.filePath =
|
|
8835
|
+
this.filePath = join18(projectRoot, ".locus", "sessions", ".input-history");
|
|
8411
8836
|
this.load();
|
|
8412
8837
|
}
|
|
8413
8838
|
add(text) {
|
|
@@ -8446,9 +8871,9 @@ class InputHistory {
|
|
|
8446
8871
|
}
|
|
8447
8872
|
load() {
|
|
8448
8873
|
try {
|
|
8449
|
-
if (!
|
|
8874
|
+
if (!existsSync17(this.filePath))
|
|
8450
8875
|
return;
|
|
8451
|
-
const content =
|
|
8876
|
+
const content = readFileSync11(this.filePath, "utf-8");
|
|
8452
8877
|
this.entries = content.split(`
|
|
8453
8878
|
`).map((line) => this.unescape(line)).filter(Boolean);
|
|
8454
8879
|
} catch {}
|
|
@@ -8456,12 +8881,12 @@ class InputHistory {
|
|
|
8456
8881
|
save() {
|
|
8457
8882
|
try {
|
|
8458
8883
|
const dir = dirname5(this.filePath);
|
|
8459
|
-
if (!
|
|
8460
|
-
|
|
8884
|
+
if (!existsSync17(dir)) {
|
|
8885
|
+
mkdirSync12(dir, { recursive: true });
|
|
8461
8886
|
}
|
|
8462
8887
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
8463
8888
|
`);
|
|
8464
|
-
|
|
8889
|
+
writeFileSync9(this.filePath, content, "utf-8");
|
|
8465
8890
|
} catch {}
|
|
8466
8891
|
}
|
|
8467
8892
|
escape(text) {
|
|
@@ -8487,21 +8912,21 @@ var init_model_config = __esm(() => {
|
|
|
8487
8912
|
|
|
8488
8913
|
// src/repl/session-manager.ts
|
|
8489
8914
|
import {
|
|
8490
|
-
existsSync as
|
|
8491
|
-
mkdirSync as
|
|
8915
|
+
existsSync as existsSync18,
|
|
8916
|
+
mkdirSync as mkdirSync13,
|
|
8492
8917
|
readdirSync as readdirSync6,
|
|
8493
|
-
readFileSync as
|
|
8918
|
+
readFileSync as readFileSync12,
|
|
8494
8919
|
unlinkSync as unlinkSync3,
|
|
8495
|
-
writeFileSync as
|
|
8920
|
+
writeFileSync as writeFileSync10
|
|
8496
8921
|
} from "node:fs";
|
|
8497
|
-
import { basename as basename3, join as
|
|
8922
|
+
import { basename as basename3, join as join19 } from "node:path";
|
|
8498
8923
|
|
|
8499
8924
|
class SessionManager {
|
|
8500
8925
|
sessionsDir;
|
|
8501
8926
|
constructor(projectRoot) {
|
|
8502
|
-
this.sessionsDir =
|
|
8503
|
-
if (!
|
|
8504
|
-
|
|
8927
|
+
this.sessionsDir = join19(projectRoot, ".locus", "sessions");
|
|
8928
|
+
if (!existsSync18(this.sessionsDir)) {
|
|
8929
|
+
mkdirSync13(this.sessionsDir, { recursive: true });
|
|
8505
8930
|
}
|
|
8506
8931
|
}
|
|
8507
8932
|
create(options) {
|
|
@@ -8526,14 +8951,14 @@ class SessionManager {
|
|
|
8526
8951
|
}
|
|
8527
8952
|
isPersisted(sessionOrId) {
|
|
8528
8953
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
8529
|
-
return
|
|
8954
|
+
return existsSync18(this.getSessionPath(sessionId));
|
|
8530
8955
|
}
|
|
8531
8956
|
load(idOrPrefix) {
|
|
8532
8957
|
const files = this.listSessionFiles();
|
|
8533
8958
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
8534
|
-
if (
|
|
8959
|
+
if (existsSync18(exactPath)) {
|
|
8535
8960
|
try {
|
|
8536
|
-
return JSON.parse(
|
|
8961
|
+
return JSON.parse(readFileSync12(exactPath, "utf-8"));
|
|
8537
8962
|
} catch {
|
|
8538
8963
|
return null;
|
|
8539
8964
|
}
|
|
@@ -8541,7 +8966,7 @@ class SessionManager {
|
|
|
8541
8966
|
const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
|
|
8542
8967
|
if (matches.length === 1) {
|
|
8543
8968
|
try {
|
|
8544
|
-
return JSON.parse(
|
|
8969
|
+
return JSON.parse(readFileSync12(matches[0], "utf-8"));
|
|
8545
8970
|
} catch {
|
|
8546
8971
|
return null;
|
|
8547
8972
|
}
|
|
@@ -8554,7 +8979,7 @@ class SessionManager {
|
|
|
8554
8979
|
save(session) {
|
|
8555
8980
|
session.updated = new Date().toISOString();
|
|
8556
8981
|
const path = this.getSessionPath(session.id);
|
|
8557
|
-
|
|
8982
|
+
writeFileSync10(path, `${JSON.stringify(session, null, 2)}
|
|
8558
8983
|
`, "utf-8");
|
|
8559
8984
|
}
|
|
8560
8985
|
addMessage(session, message) {
|
|
@@ -8566,7 +8991,7 @@ class SessionManager {
|
|
|
8566
8991
|
const sessions = [];
|
|
8567
8992
|
for (const file of files) {
|
|
8568
8993
|
try {
|
|
8569
|
-
const session = JSON.parse(
|
|
8994
|
+
const session = JSON.parse(readFileSync12(file, "utf-8"));
|
|
8570
8995
|
sessions.push({
|
|
8571
8996
|
id: session.id,
|
|
8572
8997
|
created: session.created,
|
|
@@ -8581,7 +9006,7 @@ class SessionManager {
|
|
|
8581
9006
|
}
|
|
8582
9007
|
delete(sessionId) {
|
|
8583
9008
|
const path = this.getSessionPath(sessionId);
|
|
8584
|
-
if (
|
|
9009
|
+
if (existsSync18(path)) {
|
|
8585
9010
|
unlinkSync3(path);
|
|
8586
9011
|
return true;
|
|
8587
9012
|
}
|
|
@@ -8593,7 +9018,7 @@ class SessionManager {
|
|
|
8593
9018
|
let pruned = 0;
|
|
8594
9019
|
const withStats = files.map((f) => {
|
|
8595
9020
|
try {
|
|
8596
|
-
const session = JSON.parse(
|
|
9021
|
+
const session = JSON.parse(readFileSync12(f, "utf-8"));
|
|
8597
9022
|
return { path: f, updated: new Date(session.updated).getTime() };
|
|
8598
9023
|
} catch {
|
|
8599
9024
|
return { path: f, updated: 0 };
|
|
@@ -8611,7 +9036,7 @@ class SessionManager {
|
|
|
8611
9036
|
const remaining = withStats.length - pruned;
|
|
8612
9037
|
if (remaining > MAX_SESSIONS) {
|
|
8613
9038
|
const toRemove = remaining - MAX_SESSIONS;
|
|
8614
|
-
const alive = withStats.filter((e) =>
|
|
9039
|
+
const alive = withStats.filter((e) => existsSync18(e.path));
|
|
8615
9040
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
8616
9041
|
try {
|
|
8617
9042
|
unlinkSync3(alive[i].path);
|
|
@@ -8626,7 +9051,7 @@ class SessionManager {
|
|
|
8626
9051
|
}
|
|
8627
9052
|
listSessionFiles() {
|
|
8628
9053
|
try {
|
|
8629
|
-
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
9054
|
+
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join19(this.sessionsDir, f));
|
|
8630
9055
|
} catch {
|
|
8631
9056
|
return [];
|
|
8632
9057
|
}
|
|
@@ -8635,7 +9060,7 @@ class SessionManager {
|
|
|
8635
9060
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8636
9061
|
}
|
|
8637
9062
|
getSessionPath(sessionId) {
|
|
8638
|
-
return
|
|
9063
|
+
return join19(this.sessionsDir, `${sessionId}.json`);
|
|
8639
9064
|
}
|
|
8640
9065
|
}
|
|
8641
9066
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -8646,11 +9071,11 @@ var init_session_manager = __esm(() => {
|
|
|
8646
9071
|
|
|
8647
9072
|
// src/repl/voice.ts
|
|
8648
9073
|
import { execSync as execSync11, spawn as spawn6 } from "node:child_process";
|
|
8649
|
-
import { existsSync as
|
|
9074
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
|
|
8650
9075
|
import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
|
|
8651
|
-
import { join as
|
|
9076
|
+
import { join as join20 } from "node:path";
|
|
8652
9077
|
function getWhisperModelPath() {
|
|
8653
|
-
return
|
|
9078
|
+
return join20(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
|
|
8654
9079
|
}
|
|
8655
9080
|
function commandExists(cmd) {
|
|
8656
9081
|
try {
|
|
@@ -8668,16 +9093,16 @@ function findWhisperBinary() {
|
|
|
8668
9093
|
return name;
|
|
8669
9094
|
}
|
|
8670
9095
|
for (const name of candidates) {
|
|
8671
|
-
const fullPath =
|
|
8672
|
-
if (
|
|
9096
|
+
const fullPath = join20(LOCUS_BIN_DIR, name);
|
|
9097
|
+
if (existsSync19(fullPath))
|
|
8673
9098
|
return fullPath;
|
|
8674
9099
|
}
|
|
8675
9100
|
if (platform() === "darwin") {
|
|
8676
9101
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
8677
9102
|
for (const dir of brewDirs) {
|
|
8678
9103
|
for (const name of candidates) {
|
|
8679
|
-
const fullPath =
|
|
8680
|
-
if (
|
|
9104
|
+
const fullPath = join20(dir, name);
|
|
9105
|
+
if (existsSync19(fullPath))
|
|
8681
9106
|
return fullPath;
|
|
8682
9107
|
}
|
|
8683
9108
|
}
|
|
@@ -8692,11 +9117,11 @@ function findSoxRecBinary() {
|
|
|
8692
9117
|
if (platform() === "darwin") {
|
|
8693
9118
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
8694
9119
|
for (const dir of brewDirs) {
|
|
8695
|
-
const recPath =
|
|
8696
|
-
if (
|
|
9120
|
+
const recPath = join20(dir, "rec");
|
|
9121
|
+
if (existsSync19(recPath))
|
|
8697
9122
|
return recPath;
|
|
8698
|
-
const soxPath =
|
|
8699
|
-
if (
|
|
9123
|
+
const soxPath = join20(dir, "sox");
|
|
9124
|
+
if (existsSync19(soxPath))
|
|
8700
9125
|
return soxPath;
|
|
8701
9126
|
}
|
|
8702
9127
|
}
|
|
@@ -8705,7 +9130,7 @@ function findSoxRecBinary() {
|
|
|
8705
9130
|
function checkDependencies() {
|
|
8706
9131
|
const soxBinary = findSoxRecBinary();
|
|
8707
9132
|
const whisperBinary = findWhisperBinary();
|
|
8708
|
-
const modelDownloaded =
|
|
9133
|
+
const modelDownloaded = existsSync19(getWhisperModelPath());
|
|
8709
9134
|
return {
|
|
8710
9135
|
sox: soxBinary !== null,
|
|
8711
9136
|
whisper: whisperBinary !== null,
|
|
@@ -8859,19 +9284,19 @@ function ensureBuildDeps(pm) {
|
|
|
8859
9284
|
}
|
|
8860
9285
|
function buildWhisperFromSource(pm) {
|
|
8861
9286
|
const out = process.stderr;
|
|
8862
|
-
const buildDir =
|
|
9287
|
+
const buildDir = join20(tmpdir4(), `locus-whisper-build-${process.pid}`);
|
|
8863
9288
|
if (!ensureBuildDeps(pm)) {
|
|
8864
9289
|
out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
|
|
8865
9290
|
`);
|
|
8866
9291
|
return false;
|
|
8867
9292
|
}
|
|
8868
9293
|
try {
|
|
8869
|
-
|
|
8870
|
-
|
|
9294
|
+
mkdirSync14(buildDir, { recursive: true });
|
|
9295
|
+
mkdirSync14(LOCUS_BIN_DIR, { recursive: true });
|
|
8871
9296
|
out.write(` ${dim2("Cloning whisper.cpp...")}
|
|
8872
9297
|
`);
|
|
8873
|
-
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${
|
|
8874
|
-
const srcDir =
|
|
9298
|
+
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join20(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
9299
|
+
const srcDir = join20(buildDir, "whisper.cpp");
|
|
8875
9300
|
const numCpus = cpus().length || 2;
|
|
8876
9301
|
out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
|
|
8877
9302
|
`);
|
|
@@ -8885,13 +9310,13 @@ function buildWhisperFromSource(pm) {
|
|
|
8885
9310
|
stdio: ["pipe", "pipe", "pipe"],
|
|
8886
9311
|
timeout: 600000
|
|
8887
9312
|
});
|
|
8888
|
-
const destPath =
|
|
9313
|
+
const destPath = join20(LOCUS_BIN_DIR, "whisper-cli");
|
|
8889
9314
|
const binaryCandidates = [
|
|
8890
|
-
|
|
8891
|
-
|
|
9315
|
+
join20(srcDir, "build", "bin", "whisper-cli"),
|
|
9316
|
+
join20(srcDir, "build", "bin", "main")
|
|
8892
9317
|
];
|
|
8893
9318
|
for (const candidate of binaryCandidates) {
|
|
8894
|
-
if (
|
|
9319
|
+
if (existsSync19(candidate)) {
|
|
8895
9320
|
execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
|
|
8896
9321
|
stdio: "pipe"
|
|
8897
9322
|
});
|
|
@@ -8970,9 +9395,9 @@ ${bold2("Installing voice dependencies...")}
|
|
|
8970
9395
|
}
|
|
8971
9396
|
function downloadModel() {
|
|
8972
9397
|
const modelPath = getWhisperModelPath();
|
|
8973
|
-
if (
|
|
9398
|
+
if (existsSync19(modelPath))
|
|
8974
9399
|
return true;
|
|
8975
|
-
|
|
9400
|
+
mkdirSync14(WHISPER_MODELS_DIR, { recursive: true });
|
|
8976
9401
|
const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
|
|
8977
9402
|
process.stderr.write(`${dim2("Downloading whisper model")} ${bold2(WHISPER_MODEL)} ${dim2("(~150MB)...")}
|
|
8978
9403
|
`);
|
|
@@ -9017,7 +9442,7 @@ class VoiceController {
|
|
|
9017
9442
|
onStateChange;
|
|
9018
9443
|
constructor(options) {
|
|
9019
9444
|
this.onStateChange = options.onStateChange;
|
|
9020
|
-
this.tempFile =
|
|
9445
|
+
this.tempFile = join20(tmpdir4(), `locus-voice-${process.pid}.wav`);
|
|
9021
9446
|
this.deps = checkDependencies();
|
|
9022
9447
|
}
|
|
9023
9448
|
getState() {
|
|
@@ -9085,7 +9510,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
|
|
|
9085
9510
|
this.recordProcess = null;
|
|
9086
9511
|
this.setState("idle");
|
|
9087
9512
|
await sleep2(200);
|
|
9088
|
-
if (!
|
|
9513
|
+
if (!existsSync19(this.tempFile)) {
|
|
9089
9514
|
return null;
|
|
9090
9515
|
}
|
|
9091
9516
|
try {
|
|
@@ -9176,8 +9601,8 @@ function sleep2(ms) {
|
|
|
9176
9601
|
var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
|
|
9177
9602
|
var init_voice = __esm(() => {
|
|
9178
9603
|
init_terminal();
|
|
9179
|
-
WHISPER_MODELS_DIR =
|
|
9180
|
-
LOCUS_BIN_DIR =
|
|
9604
|
+
WHISPER_MODELS_DIR = join20(homedir4(), ".locus", "whisper-models");
|
|
9605
|
+
LOCUS_BIN_DIR = join20(homedir4(), ".locus", "bin");
|
|
9181
9606
|
});
|
|
9182
9607
|
|
|
9183
9608
|
// src/repl/repl.ts
|
|
@@ -9689,8 +10114,8 @@ var init_exec = __esm(() => {
|
|
|
9689
10114
|
|
|
9690
10115
|
// src/core/submodule.ts
|
|
9691
10116
|
import { execSync as execSync13 } from "node:child_process";
|
|
9692
|
-
import { existsSync as
|
|
9693
|
-
import { join as
|
|
10117
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
10118
|
+
import { join as join21 } from "node:path";
|
|
9694
10119
|
function git2(args, cwd) {
|
|
9695
10120
|
return execSync13(`git ${args}`, {
|
|
9696
10121
|
cwd,
|
|
@@ -9706,7 +10131,7 @@ function gitSafe(args, cwd) {
|
|
|
9706
10131
|
}
|
|
9707
10132
|
}
|
|
9708
10133
|
function hasSubmodules(cwd) {
|
|
9709
|
-
return
|
|
10134
|
+
return existsSync20(join21(cwd, ".gitmodules"));
|
|
9710
10135
|
}
|
|
9711
10136
|
function listSubmodules(cwd) {
|
|
9712
10137
|
if (!hasSubmodules(cwd))
|
|
@@ -9726,7 +10151,7 @@ function listSubmodules(cwd) {
|
|
|
9726
10151
|
continue;
|
|
9727
10152
|
submodules.push({
|
|
9728
10153
|
path,
|
|
9729
|
-
absolutePath:
|
|
10154
|
+
absolutePath: join21(cwd, path),
|
|
9730
10155
|
dirty
|
|
9731
10156
|
});
|
|
9732
10157
|
}
|
|
@@ -9739,7 +10164,7 @@ function getDirtySubmodules(cwd) {
|
|
|
9739
10164
|
const submodules = listSubmodules(cwd);
|
|
9740
10165
|
const dirty = [];
|
|
9741
10166
|
for (const sub of submodules) {
|
|
9742
|
-
if (!
|
|
10167
|
+
if (!existsSync20(sub.absolutePath))
|
|
9743
10168
|
continue;
|
|
9744
10169
|
const status = gitSafe("status --porcelain", sub.absolutePath);
|
|
9745
10170
|
if (status && status.trim().length > 0) {
|
|
@@ -9826,7 +10251,7 @@ function pushSubmoduleBranches(cwd) {
|
|
|
9826
10251
|
const log = getLogger();
|
|
9827
10252
|
const submodules = listSubmodules(cwd);
|
|
9828
10253
|
for (const sub of submodules) {
|
|
9829
|
-
if (!
|
|
10254
|
+
if (!existsSync20(sub.absolutePath))
|
|
9830
10255
|
continue;
|
|
9831
10256
|
const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
|
|
9832
10257
|
if (!branch || branch === "HEAD")
|
|
@@ -10075,9 +10500,32 @@ ${submoduleSummary}`;
|
|
|
10075
10500
|
|
|
10076
10501
|
\uD83E\uDD16 Automated by [Locus](https://github.com/asgarovf/locusai)`;
|
|
10077
10502
|
const prTitle = `${issue.title} (#${issue.number})`;
|
|
10078
|
-
|
|
10079
|
-
|
|
10503
|
+
let prNumber;
|
|
10504
|
+
try {
|
|
10505
|
+
const existing = execSync14(`gh pr list --head ${currentBranch} --base ${config.agent.baseBranch} --json number --limit 1`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10506
|
+
const parsed = JSON.parse(existing);
|
|
10507
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
10508
|
+
prNumber = parsed[0].number;
|
|
10509
|
+
}
|
|
10510
|
+
} catch {}
|
|
10511
|
+
if (prNumber) {
|
|
10512
|
+
try {
|
|
10513
|
+
execSync14(`gh pr edit ${prNumber} --title ${JSON.stringify(prTitle)} --body-file -`, {
|
|
10514
|
+
input: prBody,
|
|
10515
|
+
cwd: projectRoot,
|
|
10516
|
+
encoding: "utf-8",
|
|
10517
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
10518
|
+
});
|
|
10519
|
+
process.stderr.write(` ${green("✓")} Updated existing PR #${prNumber}
|
|
10520
|
+
`);
|
|
10521
|
+
} catch (editErr) {
|
|
10522
|
+
getLogger().warn(`Failed to update PR #${prNumber}: ${editErr}`);
|
|
10523
|
+
}
|
|
10524
|
+
} else {
|
|
10525
|
+
prNumber = createPR(prTitle, prBody, currentBranch, config.agent.baseBranch, { cwd: projectRoot });
|
|
10526
|
+
process.stderr.write(` ${green("✓")} Created PR #${prNumber}
|
|
10080
10527
|
`);
|
|
10528
|
+
}
|
|
10081
10529
|
return prNumber;
|
|
10082
10530
|
} catch (e) {
|
|
10083
10531
|
getLogger().warn(`Failed to create PR: ${e}`);
|
|
@@ -10243,15 +10691,15 @@ var init_conflict = __esm(() => {
|
|
|
10243
10691
|
|
|
10244
10692
|
// src/core/run-state.ts
|
|
10245
10693
|
import {
|
|
10246
|
-
existsSync as
|
|
10247
|
-
mkdirSync as
|
|
10248
|
-
readFileSync as
|
|
10694
|
+
existsSync as existsSync21,
|
|
10695
|
+
mkdirSync as mkdirSync15,
|
|
10696
|
+
readFileSync as readFileSync13,
|
|
10249
10697
|
unlinkSync as unlinkSync5,
|
|
10250
|
-
writeFileSync as
|
|
10698
|
+
writeFileSync as writeFileSync11
|
|
10251
10699
|
} from "node:fs";
|
|
10252
|
-
import { dirname as dirname6, join as
|
|
10700
|
+
import { dirname as dirname6, join as join22 } from "node:path";
|
|
10253
10701
|
function getRunStateDir(projectRoot) {
|
|
10254
|
-
return
|
|
10702
|
+
return join22(projectRoot, ".locus", "run-state");
|
|
10255
10703
|
}
|
|
10256
10704
|
function sprintSlug(name) {
|
|
10257
10705
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -10259,16 +10707,16 @@ function sprintSlug(name) {
|
|
|
10259
10707
|
function getRunStatePath(projectRoot, sprintName) {
|
|
10260
10708
|
const dir = getRunStateDir(projectRoot);
|
|
10261
10709
|
if (sprintName) {
|
|
10262
|
-
return
|
|
10710
|
+
return join22(dir, `${sprintSlug(sprintName)}.json`);
|
|
10263
10711
|
}
|
|
10264
|
-
return
|
|
10712
|
+
return join22(dir, "_parallel.json");
|
|
10265
10713
|
}
|
|
10266
10714
|
function loadRunState(projectRoot, sprintName) {
|
|
10267
10715
|
const path = getRunStatePath(projectRoot, sprintName);
|
|
10268
|
-
if (!
|
|
10716
|
+
if (!existsSync21(path))
|
|
10269
10717
|
return null;
|
|
10270
10718
|
try {
|
|
10271
|
-
return JSON.parse(
|
|
10719
|
+
return JSON.parse(readFileSync13(path, "utf-8"));
|
|
10272
10720
|
} catch {
|
|
10273
10721
|
getLogger().warn("Corrupted run state file, ignoring");
|
|
10274
10722
|
return null;
|
|
@@ -10277,15 +10725,15 @@ function loadRunState(projectRoot, sprintName) {
|
|
|
10277
10725
|
function saveRunState(projectRoot, state) {
|
|
10278
10726
|
const path = getRunStatePath(projectRoot, state.sprint);
|
|
10279
10727
|
const dir = dirname6(path);
|
|
10280
|
-
if (!
|
|
10281
|
-
|
|
10728
|
+
if (!existsSync21(dir)) {
|
|
10729
|
+
mkdirSync15(dir, { recursive: true });
|
|
10282
10730
|
}
|
|
10283
|
-
|
|
10731
|
+
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
10284
10732
|
`, "utf-8");
|
|
10285
10733
|
}
|
|
10286
10734
|
function clearRunState(projectRoot, sprintName) {
|
|
10287
10735
|
const path = getRunStatePath(projectRoot, sprintName);
|
|
10288
|
-
if (
|
|
10736
|
+
if (existsSync21(path)) {
|
|
10289
10737
|
unlinkSync5(path);
|
|
10290
10738
|
}
|
|
10291
10739
|
}
|
|
@@ -10425,9 +10873,43 @@ var init_shutdown = __esm(() => {
|
|
|
10425
10873
|
});
|
|
10426
10874
|
|
|
10427
10875
|
// src/core/worktree.ts
|
|
10876
|
+
var exports_worktree = {};
|
|
10877
|
+
__export(exports_worktree, {
|
|
10878
|
+
sprintSlug: () => sprintSlug2,
|
|
10879
|
+
removeWorktree: () => removeWorktree,
|
|
10880
|
+
removeSprintWorktree: () => removeSprintWorktree,
|
|
10881
|
+
pushWorktreeBranch: () => pushWorktreeBranch,
|
|
10882
|
+
listWorktrees: () => listWorktrees,
|
|
10883
|
+
hasWorktreeChanges: () => hasWorktreeChanges,
|
|
10884
|
+
getWorktreePath: () => getWorktreePath,
|
|
10885
|
+
getWorktreeAge: () => getWorktreeAge,
|
|
10886
|
+
getSprintWorktreePath: () => getSprintWorktreePath,
|
|
10887
|
+
createWorktree: () => createWorktree,
|
|
10888
|
+
createSprintWorktree: () => createSprintWorktree,
|
|
10889
|
+
cleanupStaleWorktrees: () => cleanupStaleWorktrees
|
|
10890
|
+
});
|
|
10428
10891
|
import { execSync as execSync16 } from "node:child_process";
|
|
10429
|
-
import {
|
|
10430
|
-
|
|
10892
|
+
import {
|
|
10893
|
+
cpSync as cpSync2,
|
|
10894
|
+
existsSync as existsSync22,
|
|
10895
|
+
mkdirSync as mkdirSync16,
|
|
10896
|
+
readdirSync as readdirSync7,
|
|
10897
|
+
realpathSync,
|
|
10898
|
+
statSync as statSync4
|
|
10899
|
+
} from "node:fs";
|
|
10900
|
+
import { join as join23 } from "node:path";
|
|
10901
|
+
function copyLocusDir(projectRoot, worktreePath) {
|
|
10902
|
+
const srcLocus = join23(projectRoot, ".locus");
|
|
10903
|
+
if (!existsSync22(srcLocus))
|
|
10904
|
+
return;
|
|
10905
|
+
const destLocus = join23(worktreePath, ".locus");
|
|
10906
|
+
mkdirSync16(destLocus, { recursive: true });
|
|
10907
|
+
for (const entry of readdirSync7(srcLocus)) {
|
|
10908
|
+
if (entry === "worktrees")
|
|
10909
|
+
continue;
|
|
10910
|
+
cpSync2(join23(srcLocus, entry), join23(destLocus, entry), { recursive: true });
|
|
10911
|
+
}
|
|
10912
|
+
}
|
|
10431
10913
|
function git4(args, cwd) {
|
|
10432
10914
|
return execSync16(`git ${args}`, {
|
|
10433
10915
|
cwd,
|
|
@@ -10443,15 +10925,62 @@ function gitSafe3(args, cwd) {
|
|
|
10443
10925
|
}
|
|
10444
10926
|
}
|
|
10445
10927
|
function getWorktreeDir(projectRoot) {
|
|
10446
|
-
return
|
|
10928
|
+
return join23(projectRoot, ".locus", "worktrees");
|
|
10447
10929
|
}
|
|
10448
10930
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
10449
|
-
return
|
|
10931
|
+
return join23(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
10932
|
+
}
|
|
10933
|
+
function getSprintWorktreePath(projectRoot, sprintSlug2) {
|
|
10934
|
+
return join23(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
|
|
10450
10935
|
}
|
|
10451
10936
|
function generateBranchName(issueNumber) {
|
|
10452
10937
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
10453
10938
|
return `locus/issue-${issueNumber}-${randomSuffix}`;
|
|
10454
10939
|
}
|
|
10940
|
+
function sprintSlug2(name) {
|
|
10941
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
10942
|
+
}
|
|
10943
|
+
function createSprintWorktree(projectRoot, sprintName, baseBranch) {
|
|
10944
|
+
const log = getLogger();
|
|
10945
|
+
const slug = sprintSlug2(sprintName);
|
|
10946
|
+
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
10947
|
+
if (existsSync22(worktreePath)) {
|
|
10948
|
+
log.verbose(`Sprint worktree already exists for "${sprintName}"`);
|
|
10949
|
+
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/sprint-${slug}`;
|
|
10950
|
+
return { path: worktreePath, branch: existingBranch };
|
|
10951
|
+
}
|
|
10952
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
10953
|
+
const branch = `locus/sprint-${slug}-${randomSuffix}`;
|
|
10954
|
+
git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
|
|
10955
|
+
initSubmodules(worktreePath);
|
|
10956
|
+
copyLocusDir(projectRoot, worktreePath);
|
|
10957
|
+
log.info(`Created sprint worktree for "${sprintName}"`, {
|
|
10958
|
+
path: worktreePath,
|
|
10959
|
+
branch,
|
|
10960
|
+
baseBranch
|
|
10961
|
+
});
|
|
10962
|
+
return { path: worktreePath, branch };
|
|
10963
|
+
}
|
|
10964
|
+
function removeSprintWorktree(projectRoot, sprintName) {
|
|
10965
|
+
const log = getLogger();
|
|
10966
|
+
const slug = sprintSlug2(sprintName);
|
|
10967
|
+
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
10968
|
+
if (!existsSync22(worktreePath)) {
|
|
10969
|
+
log.verbose(`Sprint worktree for "${sprintName}" does not exist`);
|
|
10970
|
+
return;
|
|
10971
|
+
}
|
|
10972
|
+
const branch = getWorktreeBranch(worktreePath);
|
|
10973
|
+
try {
|
|
10974
|
+
git4(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
|
|
10975
|
+
log.info(`Removed sprint worktree for "${sprintName}"`);
|
|
10976
|
+
} catch (e) {
|
|
10977
|
+
log.warn(`Failed to remove sprint worktree: ${e}`);
|
|
10978
|
+
gitSafe3(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
|
|
10979
|
+
}
|
|
10980
|
+
if (branch) {
|
|
10981
|
+
gitSafe3(`branch -D ${branch}`, projectRoot);
|
|
10982
|
+
}
|
|
10983
|
+
}
|
|
10455
10984
|
function getWorktreeBranch(worktreePath) {
|
|
10456
10985
|
try {
|
|
10457
10986
|
return execSync16("git branch --show-current", {
|
|
@@ -10466,7 +10995,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
10466
10995
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
10467
10996
|
const log = getLogger();
|
|
10468
10997
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10469
|
-
if (
|
|
10998
|
+
if (existsSync22(worktreePath)) {
|
|
10470
10999
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
10471
11000
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
10472
11001
|
return {
|
|
@@ -10479,6 +11008,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
10479
11008
|
const branch = generateBranchName(issueNumber);
|
|
10480
11009
|
git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
|
|
10481
11010
|
initSubmodules(worktreePath);
|
|
11011
|
+
copyLocusDir(projectRoot, worktreePath);
|
|
10482
11012
|
log.info(`Created worktree for issue #${issueNumber}`, {
|
|
10483
11013
|
path: worktreePath,
|
|
10484
11014
|
branch,
|
|
@@ -10494,7 +11024,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
10494
11024
|
function removeWorktree(projectRoot, issueNumber) {
|
|
10495
11025
|
const log = getLogger();
|
|
10496
11026
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10497
|
-
if (!
|
|
11027
|
+
if (!existsSync22(worktreePath)) {
|
|
10498
11028
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
10499
11029
|
return;
|
|
10500
11030
|
}
|
|
@@ -10513,7 +11043,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
10513
11043
|
function listWorktrees(projectRoot) {
|
|
10514
11044
|
const log = getLogger();
|
|
10515
11045
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
10516
|
-
if (!
|
|
11046
|
+
if (!existsSync22(worktreeDir)) {
|
|
10517
11047
|
return [];
|
|
10518
11048
|
}
|
|
10519
11049
|
const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -10533,7 +11063,7 @@ function listWorktrees(projectRoot) {
|
|
|
10533
11063
|
if (!match)
|
|
10534
11064
|
continue;
|
|
10535
11065
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
10536
|
-
const path =
|
|
11066
|
+
const path = join23(worktreeDir, entry);
|
|
10537
11067
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
10538
11068
|
let resolvedPath;
|
|
10539
11069
|
try {
|
|
@@ -10571,6 +11101,36 @@ function cleanupStaleWorktrees(projectRoot) {
|
|
|
10571
11101
|
}
|
|
10572
11102
|
return cleaned;
|
|
10573
11103
|
}
|
|
11104
|
+
function pushWorktreeBranch(projectRoot, issueNumber) {
|
|
11105
|
+
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11106
|
+
if (!existsSync22(worktreePath)) {
|
|
11107
|
+
throw new Error(`Worktree for issue #${issueNumber} does not exist`);
|
|
11108
|
+
}
|
|
11109
|
+
const branch = getWorktreeBranch(worktreePath);
|
|
11110
|
+
if (!branch) {
|
|
11111
|
+
throw new Error(`Could not determine branch for worktree #${issueNumber}`);
|
|
11112
|
+
}
|
|
11113
|
+
git4(`push -u origin ${branch}`, worktreePath);
|
|
11114
|
+
return branch;
|
|
11115
|
+
}
|
|
11116
|
+
function hasWorktreeChanges(projectRoot, issueNumber) {
|
|
11117
|
+
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11118
|
+
if (!existsSync22(worktreePath))
|
|
11119
|
+
return false;
|
|
11120
|
+
const status = gitSafe3("status --porcelain", worktreePath);
|
|
11121
|
+
return status !== null && status.trim().length > 0;
|
|
11122
|
+
}
|
|
11123
|
+
function getWorktreeAge(projectRoot, issueNumber) {
|
|
11124
|
+
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11125
|
+
if (!existsSync22(worktreePath))
|
|
11126
|
+
return 0;
|
|
11127
|
+
try {
|
|
11128
|
+
const stat = statSync4(worktreePath);
|
|
11129
|
+
return Date.now() - stat.ctimeMs;
|
|
11130
|
+
} catch {
|
|
11131
|
+
return 0;
|
|
11132
|
+
}
|
|
11133
|
+
}
|
|
10574
11134
|
var init_worktree = __esm(() => {
|
|
10575
11135
|
init_logger();
|
|
10576
11136
|
init_submodule();
|
|
@@ -10582,6 +11142,8 @@ __export(exports_run, {
|
|
|
10582
11142
|
runCommand: () => runCommand
|
|
10583
11143
|
});
|
|
10584
11144
|
import { execSync as execSync17 } from "node:child_process";
|
|
11145
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
11146
|
+
import { join as join24 } from "node:path";
|
|
10585
11147
|
function resolveExecutionContext(config, modelOverride) {
|
|
10586
11148
|
const model = modelOverride ?? config.ai.model;
|
|
10587
11149
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
@@ -10598,12 +11160,14 @@ function printRunHelp() {
|
|
|
10598
11160
|
${bold2("locus run")} — Execute issues using AI agents
|
|
10599
11161
|
|
|
10600
11162
|
${bold2("Usage:")}
|
|
10601
|
-
locus run ${dim2("# Run
|
|
11163
|
+
locus run ${dim2("# Run all open sprints (parallel)")}
|
|
11164
|
+
locus run --sprint <name> ${dim2("# Run a specific sprint")}
|
|
10602
11165
|
locus run <issue> ${dim2("# Run single issue (worktree)")}
|
|
10603
11166
|
locus run <issue> <issue> ... ${dim2("# Run multiple issues (parallel)")}
|
|
10604
11167
|
locus run --resume ${dim2("# Resume interrupted run")}
|
|
10605
11168
|
|
|
10606
11169
|
${bold2("Options:")}
|
|
11170
|
+
--sprint <name> Run a specific sprint (instead of all active)
|
|
10607
11171
|
--resume Resume a previously interrupted run
|
|
10608
11172
|
--dry-run Show what would happen without executing
|
|
10609
11173
|
--model <name> Override the AI model for this run
|
|
@@ -10616,7 +11180,8 @@ ${bold2("Sandbox:")}
|
|
|
10616
11180
|
unsandboxed with a warning.
|
|
10617
11181
|
|
|
10618
11182
|
${bold2("Examples:")}
|
|
10619
|
-
locus run ${dim2("# Execute
|
|
11183
|
+
locus run ${dim2("# Execute all open sprints")}
|
|
11184
|
+
locus run --sprint "Sprint 1" ${dim2("# Run a specific sprint")}
|
|
10620
11185
|
locus run 42 ${dim2("# Run single issue")}
|
|
10621
11186
|
locus run 42 43 44 ${dim2("# Run issues in parallel")}
|
|
10622
11187
|
locus run --resume ${dim2("# Resume after failure")}
|
|
@@ -10669,11 +11234,11 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
10669
11234
|
}
|
|
10670
11235
|
}
|
|
10671
11236
|
if (flags.resume) {
|
|
10672
|
-
return handleResume(projectRoot, config, sandboxed,
|
|
11237
|
+
return handleResume(projectRoot, config, sandboxed, flags);
|
|
10673
11238
|
}
|
|
10674
11239
|
const issueNumbers = args.filter((a) => /^\d+$/.test(a)).map(Number);
|
|
10675
11240
|
if (issueNumbers.length === 0) {
|
|
10676
|
-
return handleSprintRun(projectRoot, config, flags, sandboxed
|
|
11241
|
+
return handleSprintRun(projectRoot, config, flags, sandboxed);
|
|
10677
11242
|
}
|
|
10678
11243
|
if (issueNumbers.length === 1) {
|
|
10679
11244
|
return handleSingleIssue(projectRoot, config, issueNumbers[0], flags, sandboxed);
|
|
@@ -10683,18 +11248,56 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
10683
11248
|
cleanupShutdown();
|
|
10684
11249
|
}
|
|
10685
11250
|
}
|
|
10686
|
-
async function handleSprintRun(projectRoot, config, flags, sandboxed
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
11251
|
+
async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
11252
|
+
let sprintNames;
|
|
11253
|
+
if (flags.sprint) {
|
|
11254
|
+
sprintNames = [flags.sprint];
|
|
11255
|
+
} else {
|
|
11256
|
+
process.stderr.write(`${cyan2("●")} Detecting open sprints...`);
|
|
11257
|
+
try {
|
|
11258
|
+
const milestones = listMilestones(config.github.owner, config.github.repo, "open", { cwd: projectRoot });
|
|
11259
|
+
sprintNames = milestones.map((m) => m.title);
|
|
11260
|
+
} catch (e) {
|
|
11261
|
+
process.stderr.write(`\r${red2("✗")} Failed to fetch sprints: ${e.message}
|
|
11262
|
+
`);
|
|
11263
|
+
return;
|
|
11264
|
+
}
|
|
11265
|
+
process.stderr.write("\r\x1B[2K");
|
|
11266
|
+
}
|
|
11267
|
+
if (sprintNames.length === 0) {
|
|
11268
|
+
process.stderr.write(`${red2("✗")} No open sprints found.
|
|
11269
|
+
`);
|
|
11270
|
+
process.stderr.write(` Create one: ${bold2('locus sprint create "Sprint 1"')}
|
|
10691
11271
|
`);
|
|
10692
11272
|
process.stderr.write(` Or specify issue numbers: ${bold2("locus run 42 43 44")}
|
|
10693
11273
|
`);
|
|
10694
11274
|
return;
|
|
10695
11275
|
}
|
|
10696
|
-
const
|
|
10697
|
-
|
|
11276
|
+
const cleaned = cleanupStaleWorktrees(projectRoot);
|
|
11277
|
+
if (cleaned > 0) {
|
|
11278
|
+
process.stderr.write(` ${dim2(`Cleaned up ${cleaned} stale worktree${cleaned === 1 ? "" : "s"}.`)}
|
|
11279
|
+
`);
|
|
11280
|
+
}
|
|
11281
|
+
if (sprintNames.length === 1) {
|
|
11282
|
+
await executeSingleSprint(projectRoot, config, sprintNames[0], flags, sandboxed);
|
|
11283
|
+
} else {
|
|
11284
|
+
process.stderr.write(`
|
|
11285
|
+
${bold2("Running")} ${cyan2(`${sprintNames.length} sprints`)} ${dim2("(parallel, worktrees)")}
|
|
11286
|
+
|
|
11287
|
+
`);
|
|
11288
|
+
const timer = createTimer();
|
|
11289
|
+
const promises = sprintNames.map((name) => executeSingleSprint(projectRoot, config, name, flags, sandboxed).catch((e) => {
|
|
11290
|
+
getLogger().warn(`Sprint "${name}" threw: ${e}`);
|
|
11291
|
+
}));
|
|
11292
|
+
await Promise.allSettled(promises);
|
|
11293
|
+
process.stderr.write(`
|
|
11294
|
+
${bold2("All sprints complete")} ${dim2(`(${timer.formatted()})`)}
|
|
11295
|
+
|
|
11296
|
+
`);
|
|
11297
|
+
}
|
|
11298
|
+
}
|
|
11299
|
+
async function executeSingleSprint(projectRoot, config, sprintName, flags, sandboxed) {
|
|
11300
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
10698
11301
|
process.stderr.write(`
|
|
10699
11302
|
${bold2("Sprint:")} ${cyan2(sprintName)}
|
|
10700
11303
|
`);
|
|
@@ -10703,7 +11306,7 @@ ${bold2("Sprint:")} ${cyan2(sprintName)}
|
|
|
10703
11306
|
const stats2 = getRunStats(existingState);
|
|
10704
11307
|
if (stats2.inProgress > 0 || stats2.pending > 0) {
|
|
10705
11308
|
process.stderr.write(`
|
|
10706
|
-
${yellow2("⚠")} A run for
|
|
11309
|
+
${yellow2("⚠")} A run for sprint "${sprintName}" is already in progress.
|
|
10707
11310
|
`);
|
|
10708
11311
|
process.stderr.write(` Use ${bold2("locus run --resume")} to continue.
|
|
10709
11312
|
`);
|
|
@@ -10724,14 +11327,33 @@ ${yellow2("⚠")} A run for this sprint is already in progress.
|
|
|
10724
11327
|
return;
|
|
10725
11328
|
}
|
|
10726
11329
|
issues = sortByOrder2(issues);
|
|
10727
|
-
|
|
10728
|
-
|
|
11330
|
+
let worktreePath;
|
|
11331
|
+
let branchName;
|
|
11332
|
+
if (!flags.dryRun) {
|
|
11333
|
+
try {
|
|
11334
|
+
const wt = createSprintWorktree(projectRoot, sprintName, config.agent.baseBranch);
|
|
11335
|
+
worktreePath = wt.path;
|
|
11336
|
+
branchName = wt.branch;
|
|
11337
|
+
process.stderr.write(` ${dim2(`Worktree: ${wt.path}`)}
|
|
11338
|
+
`);
|
|
11339
|
+
process.stderr.write(` ${dim2(`Branch: ${wt.branch}`)}
|
|
11340
|
+
`);
|
|
11341
|
+
} catch (e) {
|
|
11342
|
+
process.stderr.write(`${red2("✗")} Failed to create worktree: ${e}
|
|
11343
|
+
`);
|
|
11344
|
+
return;
|
|
11345
|
+
}
|
|
11346
|
+
} else {
|
|
11347
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
11348
|
+
branchName = `locus/sprint-${sprintName.toLowerCase().replace(/\s+/g, "-")}-${randomSuffix}`;
|
|
11349
|
+
}
|
|
11350
|
+
const workDir = worktreePath ?? projectRoot;
|
|
10729
11351
|
const state = createSprintRunState(sprintName, branchName, issues.map((issue, i) => ({
|
|
10730
11352
|
number: issue.number,
|
|
10731
11353
|
order: getOrder2(issue) ?? i + 1
|
|
10732
11354
|
})));
|
|
10733
11355
|
saveRunState(projectRoot, state);
|
|
10734
|
-
process.stderr.write(` ${dim2(`${issues.length} tasks
|
|
11356
|
+
process.stderr.write(` ${dim2(`${issues.length} tasks`)}
|
|
10735
11357
|
|
|
10736
11358
|
`);
|
|
10737
11359
|
for (const task of state.tasks) {
|
|
@@ -10746,20 +11368,6 @@ ${yellow2("⚠")} A run for this sprint is already in progress.
|
|
|
10746
11368
|
|
|
10747
11369
|
`);
|
|
10748
11370
|
}
|
|
10749
|
-
if (!flags.dryRun) {
|
|
10750
|
-
try {
|
|
10751
|
-
execSync17(`git checkout -B ${branchName}`, {
|
|
10752
|
-
cwd: projectRoot,
|
|
10753
|
-
encoding: "utf-8",
|
|
10754
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
10755
|
-
});
|
|
10756
|
-
log.info(`Checked out branch ${branchName}`);
|
|
10757
|
-
} catch (e) {
|
|
10758
|
-
process.stderr.write(`${red2("✗")} Failed to create branch: ${e}
|
|
10759
|
-
`);
|
|
10760
|
-
return;
|
|
10761
|
-
}
|
|
10762
|
-
}
|
|
10763
11371
|
const timer = createTimer();
|
|
10764
11372
|
for (let i = 0;i < state.tasks.length; i++) {
|
|
10765
11373
|
const task = state.tasks[i];
|
|
@@ -10771,27 +11379,37 @@ ${yellow2("⚠")} A run for this sprint is already in progress.
|
|
|
10771
11379
|
}
|
|
10772
11380
|
await getRateLimiter().checkBeforeRequest();
|
|
10773
11381
|
if (config.agent.rebaseBeforeTask && !flags.dryRun && i > 0) {
|
|
10774
|
-
const conflictResult = checkForConflicts(
|
|
11382
|
+
const conflictResult = checkForConflicts(workDir, config.agent.baseBranch);
|
|
10775
11383
|
if (conflictResult.baseAdvanced) {
|
|
10776
11384
|
printConflictReport(conflictResult, config.agent.baseBranch);
|
|
10777
11385
|
if (conflictResult.hasConflict) {
|
|
10778
11386
|
markTaskFailed(state, task.issue, "Merge conflict with base branch");
|
|
10779
11387
|
saveRunState(projectRoot, state);
|
|
10780
|
-
process.stderr.write(`
|
|
11388
|
+
process.stderr.write(` ${red2("✗")} Task #${task.issue} skipped due to conflicts.
|
|
11389
|
+
`);
|
|
11390
|
+
if (config.sprint.stopOnFailure) {
|
|
11391
|
+
process.stderr.write(`
|
|
10781
11392
|
${red2("✗")} Sprint stopped due to conflicts.
|
|
10782
11393
|
`);
|
|
10783
|
-
|
|
11394
|
+
process.stderr.write(` Resolve conflicts and run: ${bold2("locus run --resume")}
|
|
10784
11395
|
`);
|
|
10785
|
-
|
|
11396
|
+
break;
|
|
11397
|
+
}
|
|
11398
|
+
continue;
|
|
10786
11399
|
}
|
|
10787
|
-
const rebaseResult = attemptRebase(
|
|
11400
|
+
const rebaseResult = attemptRebase(workDir, config.agent.baseBranch);
|
|
10788
11401
|
if (!rebaseResult.success) {
|
|
10789
11402
|
markTaskFailed(state, task.issue, "Rebase failed");
|
|
10790
11403
|
saveRunState(projectRoot, state);
|
|
10791
|
-
process.stderr.write(`
|
|
10792
|
-
${red2("✗")} Auto-rebase failed. Resolve manually.
|
|
11404
|
+
process.stderr.write(` ${red2("✗")} Auto-rebase failed for task #${task.issue}.
|
|
10793
11405
|
`);
|
|
10794
|
-
|
|
11406
|
+
if (config.sprint.stopOnFailure) {
|
|
11407
|
+
process.stderr.write(`
|
|
11408
|
+
${red2("✗")} Sprint stopped. Resolve manually.
|
|
11409
|
+
`);
|
|
11410
|
+
break;
|
|
11411
|
+
}
|
|
11412
|
+
continue;
|
|
10795
11413
|
}
|
|
10796
11414
|
}
|
|
10797
11415
|
}
|
|
@@ -10799,19 +11417,19 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
|
|
|
10799
11417
|
if (i > 0 && !flags.dryRun) {
|
|
10800
11418
|
try {
|
|
10801
11419
|
sprintContext = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
10802
|
-
cwd:
|
|
11420
|
+
cwd: workDir,
|
|
10803
11421
|
encoding: "utf-8",
|
|
10804
11422
|
stdio: ["pipe", "pipe", "pipe"]
|
|
10805
11423
|
}).trim();
|
|
10806
11424
|
} catch {}
|
|
10807
11425
|
}
|
|
10808
11426
|
process.stderr.write(`
|
|
10809
|
-
${progressBar(i, state.tasks.length, { label:
|
|
11427
|
+
${progressBar(i, state.tasks.length, { label: `Sprint: ${sprintName}` })}
|
|
10810
11428
|
|
|
10811
11429
|
`);
|
|
10812
11430
|
markTaskInProgress(state, task.issue);
|
|
10813
11431
|
saveRunState(projectRoot, state);
|
|
10814
|
-
const result = await executeIssue(
|
|
11432
|
+
const result = await executeIssue(workDir, {
|
|
10815
11433
|
issueNumber: task.issue,
|
|
10816
11434
|
provider: execution.provider,
|
|
10817
11435
|
model: execution.model,
|
|
@@ -10825,7 +11443,7 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
10825
11443
|
if (result.success) {
|
|
10826
11444
|
if (!flags.dryRun) {
|
|
10827
11445
|
const issueTitle = issue?.title ?? "";
|
|
10828
|
-
ensureTaskCommit(
|
|
11446
|
+
ensureTaskCommit(workDir, task.issue, issueTitle);
|
|
10829
11447
|
if (sandboxed && i < state.tasks.length - 1) {
|
|
10830
11448
|
process.stderr.write(` ${dim2("↻ Sandbox will resync on next task")}
|
|
10831
11449
|
`);
|
|
@@ -10837,18 +11455,20 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
10837
11455
|
saveRunState(projectRoot, state);
|
|
10838
11456
|
if (config.sprint.stopOnFailure) {
|
|
10839
11457
|
process.stderr.write(`
|
|
10840
|
-
${red2("✗")} Sprint stopped: task #${task.issue} failed.
|
|
11458
|
+
${red2("✗")} Sprint "${sprintName}" stopped: task #${task.issue} failed.
|
|
10841
11459
|
`);
|
|
10842
11460
|
process.stderr.write(` Resume with: ${bold2("locus run --resume")}
|
|
10843
11461
|
`);
|
|
10844
|
-
|
|
11462
|
+
break;
|
|
10845
11463
|
}
|
|
11464
|
+
process.stderr.write(` ${yellow2("⚠")} Task #${task.issue} failed, continuing to next task.
|
|
11465
|
+
`);
|
|
10846
11466
|
}
|
|
10847
11467
|
saveRunState(projectRoot, state);
|
|
10848
11468
|
}
|
|
10849
11469
|
const stats = getRunStats(state);
|
|
10850
11470
|
process.stderr.write(`
|
|
10851
|
-
${progressBar(stats.done, stats.total, { label:
|
|
11471
|
+
${progressBar(stats.done, stats.total, { label: `Sprint Complete: ${sprintName}` })}
|
|
10852
11472
|
`);
|
|
10853
11473
|
process.stderr.write(`
|
|
10854
11474
|
${bold2("Summary:")}
|
|
@@ -10861,21 +11481,18 @@ ${bold2("Summary:")}
|
|
|
10861
11481
|
issue: t.issue,
|
|
10862
11482
|
title: issues.find((i) => i.number === t.issue)?.title
|
|
10863
11483
|
}));
|
|
10864
|
-
|
|
10865
|
-
if (prNumber !== undefined) {
|
|
10866
|
-
try {
|
|
10867
|
-
execSync17(`git checkout ${config.agent.baseBranch}`, {
|
|
10868
|
-
cwd: projectRoot,
|
|
10869
|
-
encoding: "utf-8",
|
|
10870
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
10871
|
-
});
|
|
10872
|
-
process.stderr.write(` ${dim2(`Checked out ${config.agent.baseBranch}`)}
|
|
10873
|
-
`);
|
|
10874
|
-
} catch {}
|
|
10875
|
-
}
|
|
11484
|
+
await createSprintPR(workDir, config, sprintName, branchName, completedTasks);
|
|
10876
11485
|
}
|
|
10877
11486
|
if (stats.failed === 0) {
|
|
10878
11487
|
clearRunState(projectRoot, sprintName);
|
|
11488
|
+
if (worktreePath) {
|
|
11489
|
+
removeSprintWorktree(projectRoot, sprintName);
|
|
11490
|
+
}
|
|
11491
|
+
} else {
|
|
11492
|
+
if (worktreePath) {
|
|
11493
|
+
process.stderr.write(` ${yellow2("⚠")} Sprint worktree preserved: ${dim2(worktreePath)}
|
|
11494
|
+
`);
|
|
11495
|
+
}
|
|
10879
11496
|
}
|
|
10880
11497
|
}
|
|
10881
11498
|
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
@@ -11056,43 +11673,87 @@ ${yellow2("⚠")} Failed worktrees preserved for debugging:
|
|
|
11056
11673
|
clearRunState(projectRoot);
|
|
11057
11674
|
}
|
|
11058
11675
|
}
|
|
11059
|
-
async function handleResume(projectRoot, config, sandboxed,
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11676
|
+
async function handleResume(projectRoot, config, sandboxed, flags) {
|
|
11677
|
+
if (flags.sprint) {
|
|
11678
|
+
const state = loadRunState(projectRoot, flags.sprint);
|
|
11679
|
+
if (!state) {
|
|
11680
|
+
process.stderr.write(`${red2("✗")} No run state found for sprint "${flags.sprint}".
|
|
11681
|
+
`);
|
|
11682
|
+
return;
|
|
11683
|
+
}
|
|
11684
|
+
await resumeSingleRun(projectRoot, config, state, sandboxed);
|
|
11685
|
+
return;
|
|
11065
11686
|
}
|
|
11066
|
-
|
|
11687
|
+
const sprintsToResume = [];
|
|
11688
|
+
try {
|
|
11689
|
+
const { readdirSync: readdirSync8 } = await import("node:fs");
|
|
11690
|
+
const runStateDir = join24(projectRoot, ".locus", "run-state");
|
|
11691
|
+
if (existsSync23(runStateDir)) {
|
|
11692
|
+
const files = readdirSync8(runStateDir).filter((f) => f.endsWith(".json"));
|
|
11693
|
+
for (const file of files) {
|
|
11694
|
+
const sprintName = file === "_parallel.json" ? undefined : file.replace(/\.json$/, "");
|
|
11695
|
+
const state = loadRunState(projectRoot, sprintName);
|
|
11696
|
+
if (state) {
|
|
11697
|
+
const stats = getRunStats(state);
|
|
11698
|
+
if (stats.failed > 0 || stats.pending > 0 || stats.inProgress > 0) {
|
|
11699
|
+
sprintsToResume.push(state);
|
|
11700
|
+
}
|
|
11701
|
+
}
|
|
11702
|
+
}
|
|
11703
|
+
}
|
|
11704
|
+
} catch {}
|
|
11705
|
+
if (sprintsToResume.length === 0) {
|
|
11067
11706
|
process.stderr.write(`${red2("✗")} No run state found. Nothing to resume.
|
|
11068
11707
|
`);
|
|
11069
11708
|
return;
|
|
11070
11709
|
}
|
|
11071
|
-
|
|
11710
|
+
if (sprintsToResume.length === 1) {
|
|
11711
|
+
await resumeSingleRun(projectRoot, config, sprintsToResume[0], sandboxed);
|
|
11712
|
+
} else {
|
|
11713
|
+
process.stderr.write(`
|
|
11714
|
+
${bold2("Resuming")} ${cyan2(`${sprintsToResume.length} runs`)} ${dim2("(parallel)")}
|
|
11715
|
+
|
|
11716
|
+
`);
|
|
11717
|
+
const promises = sprintsToResume.map((state) => resumeSingleRun(projectRoot, config, state, sandboxed).catch((e) => {
|
|
11718
|
+
getLogger().warn(`Resume for "${state.sprint}" threw: ${e}`);
|
|
11719
|
+
}));
|
|
11720
|
+
await Promise.allSettled(promises);
|
|
11721
|
+
}
|
|
11722
|
+
}
|
|
11723
|
+
async function resumeSingleRun(projectRoot, config, state, sandboxed) {
|
|
11724
|
+
const execution = resolveExecutionContext(config);
|
|
11072
11725
|
const stats = getRunStats(state);
|
|
11073
11726
|
process.stderr.write(`
|
|
11074
|
-
${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
11727
|
+
${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}${state.sprint ? ` (${cyan2(state.sprint)})` : ""}
|
|
11075
11728
|
`);
|
|
11076
11729
|
process.stderr.write(` Done: ${stats.done}, Failed: ${stats.failed}, Pending: ${stats.pending}
|
|
11077
11730
|
|
|
11078
11731
|
`);
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11732
|
+
let workDir = projectRoot;
|
|
11733
|
+
if (state.type === "sprint" && state.sprint) {
|
|
11734
|
+
const { getSprintWorktreePath: getSprintWorktreePath2, sprintSlug: sprintSlug3 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
|
|
11735
|
+
const { existsSync: existsSync24 } = await import("node:fs");
|
|
11736
|
+
const wtPath = getSprintWorktreePath2(projectRoot, sprintSlug3(state.sprint));
|
|
11737
|
+
if (existsSync24(wtPath)) {
|
|
11738
|
+
workDir = wtPath;
|
|
11739
|
+
} else if (state.branch) {
|
|
11740
|
+
try {
|
|
11741
|
+
const currentBranch = execSync17("git rev-parse --abbrev-ref HEAD", {
|
|
11088
11742
|
cwd: projectRoot,
|
|
11089
11743
|
encoding: "utf-8",
|
|
11090
11744
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11091
|
-
});
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11745
|
+
}).trim();
|
|
11746
|
+
if (currentBranch !== state.branch) {
|
|
11747
|
+
execSync17(`git checkout ${state.branch}`, {
|
|
11748
|
+
cwd: projectRoot,
|
|
11749
|
+
encoding: "utf-8",
|
|
11750
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11751
|
+
});
|
|
11752
|
+
}
|
|
11753
|
+
} catch {
|
|
11754
|
+
process.stderr.write(`${yellow2("⚠")} Could not checkout branch ${state.branch}
|
|
11095
11755
|
`);
|
|
11756
|
+
}
|
|
11096
11757
|
}
|
|
11097
11758
|
}
|
|
11098
11759
|
const isSprintRun = state.type === "sprint";
|
|
@@ -11106,7 +11767,7 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
|
11106
11767
|
}
|
|
11107
11768
|
markTaskInProgress(state, task.issue);
|
|
11108
11769
|
saveRunState(projectRoot, state);
|
|
11109
|
-
const result = await executeIssue(
|
|
11770
|
+
const result = await executeIssue(workDir, {
|
|
11110
11771
|
issueNumber: task.issue,
|
|
11111
11772
|
provider: execution.provider,
|
|
11112
11773
|
model: execution.model,
|
|
@@ -11122,7 +11783,7 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
|
11122
11783
|
const iss = getIssue(task.issue, { cwd: projectRoot });
|
|
11123
11784
|
issueTitle = iss.title;
|
|
11124
11785
|
} catch {}
|
|
11125
|
-
ensureTaskCommit(
|
|
11786
|
+
ensureTaskCommit(workDir, task.issue, issueTitle);
|
|
11126
11787
|
if (sandboxed) {
|
|
11127
11788
|
process.stderr.write(` ${dim2("↻ Sandbox will resync on next task")}
|
|
11128
11789
|
`);
|
|
@@ -11138,6 +11799,8 @@ ${red2("✗")} Sprint stopped: task #${task.issue} failed.
|
|
|
11138
11799
|
`);
|
|
11139
11800
|
return;
|
|
11140
11801
|
}
|
|
11802
|
+
process.stderr.write(` ${yellow2("⚠")} Task #${task.issue} failed, continuing to next task.
|
|
11803
|
+
`);
|
|
11141
11804
|
}
|
|
11142
11805
|
saveRunState(projectRoot, state);
|
|
11143
11806
|
task = getNextTask(state);
|
|
@@ -11149,21 +11812,13 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
|
|
|
11149
11812
|
`);
|
|
11150
11813
|
if (isSprintRun && state.branch && state.sprint && finalStats.done > 0) {
|
|
11151
11814
|
const completedTasks = state.tasks.filter((t) => t.status === "done").map((t) => ({ issue: t.issue }));
|
|
11152
|
-
|
|
11153
|
-
if (prNumber !== undefined) {
|
|
11154
|
-
try {
|
|
11155
|
-
execSync17(`git checkout ${config.agent.baseBranch}`, {
|
|
11156
|
-
cwd: projectRoot,
|
|
11157
|
-
encoding: "utf-8",
|
|
11158
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
11159
|
-
});
|
|
11160
|
-
process.stderr.write(` ${dim2(`Checked out ${config.agent.baseBranch}`)}
|
|
11161
|
-
`);
|
|
11162
|
-
} catch {}
|
|
11163
|
-
}
|
|
11815
|
+
await createSprintPR(workDir, config, state.sprint, state.branch, completedTasks);
|
|
11164
11816
|
}
|
|
11165
11817
|
if (finalStats.failed === 0) {
|
|
11166
11818
|
clearRunState(projectRoot, state.sprint);
|
|
11819
|
+
if (isSprintRun && state.sprint) {
|
|
11820
|
+
removeSprintWorktree(projectRoot, state.sprint);
|
|
11821
|
+
}
|
|
11167
11822
|
}
|
|
11168
11823
|
}
|
|
11169
11824
|
function sortByOrder2(issues) {
|
|
@@ -11181,22 +11836,22 @@ function getOrder2(issue) {
|
|
|
11181
11836
|
}
|
|
11182
11837
|
return null;
|
|
11183
11838
|
}
|
|
11184
|
-
function ensureTaskCommit(
|
|
11839
|
+
function ensureTaskCommit(workDir, issueNumber, issueTitle) {
|
|
11185
11840
|
try {
|
|
11186
|
-
const committedSubmodules = commitDirtySubmodules(
|
|
11841
|
+
const committedSubmodules = commitDirtySubmodules(workDir, issueNumber, issueTitle);
|
|
11187
11842
|
if (committedSubmodules.length > 0) {
|
|
11188
11843
|
process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
|
|
11189
11844
|
`);
|
|
11190
11845
|
}
|
|
11191
11846
|
const status = execSync17("git status --porcelain", {
|
|
11192
|
-
cwd:
|
|
11847
|
+
cwd: workDir,
|
|
11193
11848
|
encoding: "utf-8",
|
|
11194
11849
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11195
11850
|
}).trim();
|
|
11196
11851
|
if (!status)
|
|
11197
11852
|
return;
|
|
11198
11853
|
execSync17("git add -A", {
|
|
11199
|
-
cwd:
|
|
11854
|
+
cwd: workDir,
|
|
11200
11855
|
encoding: "utf-8",
|
|
11201
11856
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11202
11857
|
});
|
|
@@ -11205,7 +11860,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
11205
11860
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
11206
11861
|
execSync17(`git commit -F -`, {
|
|
11207
11862
|
input: message,
|
|
11208
|
-
cwd:
|
|
11863
|
+
cwd: workDir,
|
|
11209
11864
|
encoding: "utf-8",
|
|
11210
11865
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11211
11866
|
});
|
|
@@ -11213,12 +11868,12 @@ Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
|
11213
11868
|
`);
|
|
11214
11869
|
} catch {}
|
|
11215
11870
|
}
|
|
11216
|
-
async function createSprintPR(
|
|
11871
|
+
async function createSprintPR(workDir, config, sprintName, branchName, tasks) {
|
|
11217
11872
|
if (!config.agent.autoPR)
|
|
11218
11873
|
return;
|
|
11219
11874
|
try {
|
|
11220
11875
|
const diff = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
11221
|
-
cwd:
|
|
11876
|
+
cwd: workDir,
|
|
11222
11877
|
encoding: "utf-8",
|
|
11223
11878
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11224
11879
|
}).trim();
|
|
@@ -11227,15 +11882,15 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
11227
11882
|
`);
|
|
11228
11883
|
return;
|
|
11229
11884
|
}
|
|
11230
|
-
pushSubmoduleBranches(
|
|
11885
|
+
pushSubmoduleBranches(workDir);
|
|
11231
11886
|
execSync17(`git push -u origin ${branchName}`, {
|
|
11232
|
-
cwd:
|
|
11887
|
+
cwd: workDir,
|
|
11233
11888
|
encoding: "utf-8",
|
|
11234
11889
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11235
11890
|
});
|
|
11236
11891
|
const taskLines = tasks.map((t) => `- Closes #${t.issue}${t.title ? `: ${t.title}` : ""}`).join(`
|
|
11237
11892
|
`);
|
|
11238
|
-
const submoduleSummary = getSubmoduleChangeSummary(
|
|
11893
|
+
const submoduleSummary = getSubmoduleChangeSummary(workDir, config.agent.baseBranch);
|
|
11239
11894
|
let prBody = `## Sprint: ${sprintName}
|
|
11240
11895
|
|
|
11241
11896
|
${taskLines}`;
|
|
@@ -11249,9 +11904,35 @@ ${submoduleSummary}`;
|
|
|
11249
11904
|
---
|
|
11250
11905
|
|
|
11251
11906
|
\uD83E\uDD16 Automated by [Locus](https://github.com/asgarovf/locusai)`;
|
|
11252
|
-
const
|
|
11253
|
-
|
|
11907
|
+
const prTitle = `Sprint: ${sprintName}`;
|
|
11908
|
+
let prNumber;
|
|
11909
|
+
try {
|
|
11910
|
+
const existing = execSync17(`gh pr list --head ${branchName} --base ${config.agent.baseBranch} --json number --limit 1`, { cwd: workDir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
11911
|
+
const parsed = JSON.parse(existing);
|
|
11912
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
11913
|
+
prNumber = parsed[0].number;
|
|
11914
|
+
}
|
|
11915
|
+
} catch {}
|
|
11916
|
+
if (prNumber) {
|
|
11917
|
+
try {
|
|
11918
|
+
execSync17(`gh pr edit ${prNumber} --title ${JSON.stringify(prTitle)} --body-file -`, {
|
|
11919
|
+
input: prBody,
|
|
11920
|
+
cwd: workDir,
|
|
11921
|
+
encoding: "utf-8",
|
|
11922
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11923
|
+
});
|
|
11924
|
+
process.stderr.write(` ${green("✓")} Updated existing sprint PR #${prNumber}
|
|
11925
|
+
`);
|
|
11926
|
+
} catch (editErr) {
|
|
11927
|
+
getLogger().warn(`Failed to update sprint PR #${prNumber}: ${editErr}`);
|
|
11928
|
+
process.stderr.write(` ${yellow2("⚠")} PR #${prNumber} exists but could not update: ${editErr}
|
|
11929
|
+
`);
|
|
11930
|
+
}
|
|
11931
|
+
} else {
|
|
11932
|
+
prNumber = createPR(prTitle, prBody, branchName, config.agent.baseBranch, { cwd: workDir });
|
|
11933
|
+
process.stderr.write(` ${green("✓")} Created sprint PR #${prNumber}
|
|
11254
11934
|
`);
|
|
11935
|
+
}
|
|
11255
11936
|
return prNumber;
|
|
11256
11937
|
} catch (e) {
|
|
11257
11938
|
getLogger().warn(`Failed to create sprint PR: ${e}`);
|
|
@@ -11283,8 +11964,8 @@ __export(exports_status, {
|
|
|
11283
11964
|
statusCommand: () => statusCommand
|
|
11284
11965
|
});
|
|
11285
11966
|
import { execSync as execSync18 } from "node:child_process";
|
|
11286
|
-
import { existsSync as
|
|
11287
|
-
import { dirname as dirname7, join as
|
|
11967
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
11968
|
+
import { dirname as dirname7, join as join25 } from "node:path";
|
|
11288
11969
|
async function statusCommand(projectRoot) {
|
|
11289
11970
|
const config = loadConfig(projectRoot);
|
|
11290
11971
|
const spinner = new Spinner;
|
|
@@ -11293,21 +11974,19 @@ async function statusCommand(projectRoot) {
|
|
|
11293
11974
|
lines.push(` ${dim2("Repo:")} ${cyan2(`${config.github.owner}/${config.github.repo}`)}`);
|
|
11294
11975
|
lines.push(` ${dim2("Provider:")} ${config.ai.provider} / ${config.ai.model}`);
|
|
11295
11976
|
lines.push(` ${dim2("Branch:")} ${config.agent.baseBranch}`);
|
|
11296
|
-
|
|
11297
|
-
const
|
|
11298
|
-
|
|
11299
|
-
const
|
|
11300
|
-
const milestone = milestones.find((m) => m.title.toLowerCase() === sprintName.toLowerCase());
|
|
11301
|
-
if (milestone) {
|
|
11977
|
+
try {
|
|
11978
|
+
const milestones = listMilestones(config.github.owner, config.github.repo, "open", { cwd: projectRoot });
|
|
11979
|
+
if (milestones.length > 0) {
|
|
11980
|
+
for (const milestone of milestones) {
|
|
11302
11981
|
const total = milestone.openIssues + milestone.closedIssues;
|
|
11303
11982
|
const done = milestone.closedIssues;
|
|
11304
11983
|
const dueStr = milestone.dueOn ? ` due ${new Date(milestone.dueOn).toLocaleDateString("en-US", { month: "short", day: "numeric" })}` : "";
|
|
11305
11984
|
lines.push("");
|
|
11306
|
-
lines.push(` ${bold2("Sprint:")} ${cyan2(
|
|
11985
|
+
lines.push(` ${bold2("Sprint:")} ${cyan2(milestone.title)} (${done} of ${total} done${dueStr})`);
|
|
11307
11986
|
if (total > 0) {
|
|
11308
11987
|
lines.push(` ${progressBar(done, total, { width: 30 })}`);
|
|
11309
11988
|
}
|
|
11310
|
-
const issues = listIssues({ milestone:
|
|
11989
|
+
const issues = listIssues({ milestone: milestone.title, state: "all" }, { cwd: projectRoot });
|
|
11311
11990
|
const queued = issues.filter((i) => i.labels.some((l) => l === "locus:queued")).length;
|
|
11312
11991
|
const inProgress = issues.filter((i) => i.labels.some((l) => l === "locus:in-progress")).length;
|
|
11313
11992
|
const failed = issues.filter((i) => i.labels.some((l) => l === "locus:failed")).length;
|
|
@@ -11323,17 +12002,14 @@ async function statusCommand(projectRoot) {
|
|
|
11323
12002
|
if (parts.length > 0) {
|
|
11324
12003
|
lines.push(` ${parts.join(" ")}`);
|
|
11325
12004
|
}
|
|
11326
|
-
} else {
|
|
11327
|
-
lines.push("");
|
|
11328
|
-
lines.push(` ${bold2("Sprint:")} ${cyan2(sprintName)} ${dim2("(not found)")}`);
|
|
11329
12005
|
}
|
|
11330
|
-
}
|
|
12006
|
+
} else {
|
|
11331
12007
|
lines.push("");
|
|
11332
|
-
lines.push(` ${
|
|
12008
|
+
lines.push(` ${dim2("Sprint:")} ${dim2("no open sprints")}`);
|
|
11333
12009
|
}
|
|
11334
|
-
}
|
|
12010
|
+
} catch {
|
|
11335
12011
|
lines.push("");
|
|
11336
|
-
lines.push(` ${dim2("Sprint:")} ${dim2("
|
|
12012
|
+
lines.push(` ${dim2("Sprint:")} ${dim2("(could not fetch)")}`);
|
|
11337
12013
|
}
|
|
11338
12014
|
const runState = loadRunState(projectRoot);
|
|
11339
12015
|
if (runState) {
|
|
@@ -11421,13 +12097,13 @@ ${drawBox(lines, { title: "Locus Status" })}
|
|
|
11421
12097
|
`);
|
|
11422
12098
|
}
|
|
11423
12099
|
function getPm2Bin() {
|
|
11424
|
-
const pkgsBin =
|
|
11425
|
-
if (
|
|
12100
|
+
const pkgsBin = join25(getPackagesDir(), "node_modules", ".bin", "pm2");
|
|
12101
|
+
if (existsSync24(pkgsBin))
|
|
11426
12102
|
return pkgsBin;
|
|
11427
12103
|
let dir = process.cwd();
|
|
11428
12104
|
while (dir !== dirname7(dir)) {
|
|
11429
|
-
const candidate =
|
|
11430
|
-
if (
|
|
12105
|
+
const candidate = join25(dir, "node_modules", ".bin", "pm2");
|
|
12106
|
+
if (existsSync24(candidate))
|
|
11431
12107
|
return candidate;
|
|
11432
12108
|
dir = dirname7(dir);
|
|
11433
12109
|
}
|
|
@@ -11623,13 +12299,13 @@ __export(exports_plan, {
|
|
|
11623
12299
|
parsePlanArgs: () => parsePlanArgs
|
|
11624
12300
|
});
|
|
11625
12301
|
import {
|
|
11626
|
-
existsSync as
|
|
11627
|
-
mkdirSync as
|
|
12302
|
+
existsSync as existsSync25,
|
|
12303
|
+
mkdirSync as mkdirSync17,
|
|
11628
12304
|
readdirSync as readdirSync8,
|
|
11629
|
-
readFileSync as
|
|
11630
|
-
writeFileSync as
|
|
12305
|
+
readFileSync as readFileSync14,
|
|
12306
|
+
writeFileSync as writeFileSync12
|
|
11631
12307
|
} from "node:fs";
|
|
11632
|
-
import { join as
|
|
12308
|
+
import { join as join26 } from "node:path";
|
|
11633
12309
|
function printHelp2() {
|
|
11634
12310
|
process.stderr.write(`
|
|
11635
12311
|
${bold2("locus plan")} — AI-powered sprint planning
|
|
@@ -11661,12 +12337,12 @@ function normalizeSprintName(name) {
|
|
|
11661
12337
|
return name.trim().toLowerCase();
|
|
11662
12338
|
}
|
|
11663
12339
|
function getPlansDir(projectRoot) {
|
|
11664
|
-
return
|
|
12340
|
+
return join26(projectRoot, ".locus", "plans");
|
|
11665
12341
|
}
|
|
11666
12342
|
function ensurePlansDir(projectRoot) {
|
|
11667
12343
|
const dir = getPlansDir(projectRoot);
|
|
11668
|
-
if (!
|
|
11669
|
-
|
|
12344
|
+
if (!existsSync25(dir)) {
|
|
12345
|
+
mkdirSync17(dir, { recursive: true });
|
|
11670
12346
|
}
|
|
11671
12347
|
return dir;
|
|
11672
12348
|
}
|
|
@@ -11675,14 +12351,14 @@ function generateId() {
|
|
|
11675
12351
|
}
|
|
11676
12352
|
function loadPlanFile(projectRoot, id) {
|
|
11677
12353
|
const dir = getPlansDir(projectRoot);
|
|
11678
|
-
if (!
|
|
12354
|
+
if (!existsSync25(dir))
|
|
11679
12355
|
return null;
|
|
11680
12356
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
|
|
11681
12357
|
const match = files.find((f) => f.startsWith(id));
|
|
11682
12358
|
if (!match)
|
|
11683
12359
|
return null;
|
|
11684
12360
|
try {
|
|
11685
|
-
const content =
|
|
12361
|
+
const content = readFileSync14(join26(dir, match), "utf-8");
|
|
11686
12362
|
return JSON.parse(content);
|
|
11687
12363
|
} catch {
|
|
11688
12364
|
return null;
|
|
@@ -11749,7 +12425,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
11749
12425
|
}
|
|
11750
12426
|
function handleListPlans(projectRoot) {
|
|
11751
12427
|
const dir = getPlansDir(projectRoot);
|
|
11752
|
-
if (!
|
|
12428
|
+
if (!existsSync25(dir)) {
|
|
11753
12429
|
process.stderr.write(`${dim2("No saved plans yet.")}
|
|
11754
12430
|
`);
|
|
11755
12431
|
return;
|
|
@@ -11767,7 +12443,7 @@ ${bold2("Saved Plans:")}
|
|
|
11767
12443
|
for (const file of files) {
|
|
11768
12444
|
const id = file.replace(".json", "");
|
|
11769
12445
|
try {
|
|
11770
|
-
const content =
|
|
12446
|
+
const content = readFileSync14(join26(dir, file), "utf-8");
|
|
11771
12447
|
const plan = JSON.parse(content);
|
|
11772
12448
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
11773
12449
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -11835,7 +12511,7 @@ async function handleRefinePlan(projectRoot, id, feedback, flags) {
|
|
|
11835
12511
|
return;
|
|
11836
12512
|
}
|
|
11837
12513
|
const config = loadConfig(projectRoot);
|
|
11838
|
-
const planPath =
|
|
12514
|
+
const planPath = join26(getPlansDir(projectRoot), `${plan.id}.json`);
|
|
11839
12515
|
const planPathRelative = `.locus/plans/${plan.id}.json`;
|
|
11840
12516
|
process.stderr.write(`
|
|
11841
12517
|
${bold2("Refining plan:")} ${cyan2(plan.directive)}
|
|
@@ -11874,7 +12550,7 @@ ${red2("✗")} Refinement failed: ${aiResult.error}
|
|
|
11874
12550
|
`);
|
|
11875
12551
|
return;
|
|
11876
12552
|
}
|
|
11877
|
-
if (!
|
|
12553
|
+
if (!existsSync25(planPath)) {
|
|
11878
12554
|
process.stderr.write(`
|
|
11879
12555
|
${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
11880
12556
|
`);
|
|
@@ -11882,7 +12558,7 @@ ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
|
11882
12558
|
}
|
|
11883
12559
|
let updatedPlan;
|
|
11884
12560
|
try {
|
|
11885
|
-
const content =
|
|
12561
|
+
const content = readFileSync14(planPath, "utf-8");
|
|
11886
12562
|
updatedPlan = JSON.parse(content);
|
|
11887
12563
|
} catch {
|
|
11888
12564
|
process.stderr.write(`
|
|
@@ -11900,7 +12576,7 @@ ${yellow2("⚠")} Refined plan has no issues.
|
|
|
11900
12576
|
updatedPlan.directive = plan.directive;
|
|
11901
12577
|
updatedPlan.sprint = updatedPlan.sprint ?? plan.sprint;
|
|
11902
12578
|
updatedPlan.createdAt = plan.createdAt;
|
|
11903
|
-
|
|
12579
|
+
writeFileSync12(planPath, JSON.stringify(updatedPlan, null, 2), "utf-8");
|
|
11904
12580
|
process.stderr.write(`
|
|
11905
12581
|
${bold2("Plan refined:")} ${cyan2(plan.id)}
|
|
11906
12582
|
|
|
@@ -11972,7 +12648,7 @@ ${bold2("Approving plan:")}
|
|
|
11972
12648
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
11973
12649
|
const id = generateId();
|
|
11974
12650
|
const plansDir = ensurePlansDir(projectRoot);
|
|
11975
|
-
const planPath =
|
|
12651
|
+
const planPath = join26(plansDir, `${id}.json`);
|
|
11976
12652
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
11977
12653
|
const displayDirective = directive;
|
|
11978
12654
|
process.stderr.write(`
|
|
@@ -12015,7 +12691,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
|
|
|
12015
12691
|
`);
|
|
12016
12692
|
return;
|
|
12017
12693
|
}
|
|
12018
|
-
if (!
|
|
12694
|
+
if (!existsSync25(planPath)) {
|
|
12019
12695
|
process.stderr.write(`
|
|
12020
12696
|
${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
12021
12697
|
`);
|
|
@@ -12025,7 +12701,7 @@ ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
|
12025
12701
|
}
|
|
12026
12702
|
let plan;
|
|
12027
12703
|
try {
|
|
12028
|
-
const content =
|
|
12704
|
+
const content = readFileSync14(planPath, "utf-8");
|
|
12029
12705
|
plan = JSON.parse(content);
|
|
12030
12706
|
} catch {
|
|
12031
12707
|
process.stderr.write(`
|
|
@@ -12047,7 +12723,7 @@ ${yellow2("⚠")} Plan file has no issues.
|
|
|
12047
12723
|
plan.sprint = sprintName;
|
|
12048
12724
|
if (!plan.createdAt)
|
|
12049
12725
|
plan.createdAt = new Date().toISOString();
|
|
12050
|
-
|
|
12726
|
+
writeFileSync12(planPath, JSON.stringify(plan, null, 2), "utf-8");
|
|
12051
12727
|
process.stderr.write(`
|
|
12052
12728
|
${bold2("Plan saved:")} ${cyan2(id)}
|
|
12053
12729
|
|
|
@@ -12197,16 +12873,16 @@ ${directive}${sprintName ? `
|
|
|
12197
12873
|
|
|
12198
12874
|
**Sprint:** ${sprintName}` : ""}
|
|
12199
12875
|
</directive>`);
|
|
12200
|
-
const locusPath =
|
|
12201
|
-
if (
|
|
12202
|
-
const content =
|
|
12876
|
+
const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
|
|
12877
|
+
if (existsSync25(locusPath)) {
|
|
12878
|
+
const content = readFileSync14(locusPath, "utf-8");
|
|
12203
12879
|
parts.push(`<project-context>
|
|
12204
12880
|
${content.slice(0, 3000)}
|
|
12205
12881
|
</project-context>`);
|
|
12206
12882
|
}
|
|
12207
|
-
const learningsPath =
|
|
12208
|
-
if (
|
|
12209
|
-
const content =
|
|
12883
|
+
const learningsPath = join26(projectRoot, ".locus", "LEARNINGS.md");
|
|
12884
|
+
if (existsSync25(learningsPath)) {
|
|
12885
|
+
const content = readFileSync14(learningsPath, "utf-8");
|
|
12210
12886
|
parts.push(`<past-learnings>
|
|
12211
12887
|
${content.slice(0, 2000)}
|
|
12212
12888
|
</past-learnings>`);
|
|
@@ -12258,9 +12934,9 @@ ${JSON.stringify(plan, null, 2)}
|
|
|
12258
12934
|
parts.push(`<feedback>
|
|
12259
12935
|
${feedback}
|
|
12260
12936
|
</feedback>`);
|
|
12261
|
-
const locusPath =
|
|
12262
|
-
if (
|
|
12263
|
-
const content =
|
|
12937
|
+
const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
|
|
12938
|
+
if (existsSync25(locusPath)) {
|
|
12939
|
+
const content = readFileSync14(locusPath, "utf-8");
|
|
12264
12940
|
parts.push(`<project-context>
|
|
12265
12941
|
${content.slice(0, 3000)}
|
|
12266
12942
|
</project-context>`);
|
|
@@ -12442,8 +13118,8 @@ __export(exports_review, {
|
|
|
12442
13118
|
reviewCommand: () => reviewCommand
|
|
12443
13119
|
});
|
|
12444
13120
|
import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
|
|
12445
|
-
import { existsSync as
|
|
12446
|
-
import { join as
|
|
13121
|
+
import { existsSync as existsSync26, readFileSync as readFileSync15 } from "node:fs";
|
|
13122
|
+
import { join as join27 } from "node:path";
|
|
12447
13123
|
function printHelp3() {
|
|
12448
13124
|
process.stderr.write(`
|
|
12449
13125
|
${bold2("locus review")} — AI-powered code review
|
|
@@ -12612,9 +13288,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
12612
13288
|
parts.push(`<role>
|
|
12613
13289
|
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
12614
13290
|
</role>`);
|
|
12615
|
-
const locusPath =
|
|
12616
|
-
if (
|
|
12617
|
-
const content =
|
|
13291
|
+
const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
|
|
13292
|
+
if (existsSync26(locusPath)) {
|
|
13293
|
+
const content = readFileSync15(locusPath, "utf-8");
|
|
12618
13294
|
parts.push(`<project-context>
|
|
12619
13295
|
${content.slice(0, 2000)}
|
|
12620
13296
|
</project-context>`);
|
|
@@ -12789,13 +13465,8 @@ ${bold2("Finding PR for issue")} ${cyan2(`#${issueNumber}`)}...
|
|
|
12789
13465
|
return handleSinglePR(projectRoot, config, prNumber, flags);
|
|
12790
13466
|
}
|
|
12791
13467
|
async function handleSprint(projectRoot, config, flags) {
|
|
12792
|
-
if (!config.sprint.active) {
|
|
12793
|
-
process.stderr.write(`${red2("✗")} No active sprint set.
|
|
12794
|
-
`);
|
|
12795
|
-
return;
|
|
12796
|
-
}
|
|
12797
13468
|
process.stderr.write(`
|
|
12798
|
-
${bold2("Iterating on sprint
|
|
13469
|
+
${bold2("Iterating on sprint PRs...")}
|
|
12799
13470
|
|
|
12800
13471
|
`);
|
|
12801
13472
|
const prs = listPRs({ label: "agent:managed", state: "open" }, { cwd: projectRoot });
|
|
@@ -12934,14 +13605,14 @@ __export(exports_discuss, {
|
|
|
12934
13605
|
discussCommand: () => discussCommand
|
|
12935
13606
|
});
|
|
12936
13607
|
import {
|
|
12937
|
-
existsSync as
|
|
12938
|
-
mkdirSync as
|
|
13608
|
+
existsSync as existsSync27,
|
|
13609
|
+
mkdirSync as mkdirSync18,
|
|
12939
13610
|
readdirSync as readdirSync9,
|
|
12940
|
-
readFileSync as
|
|
13611
|
+
readFileSync as readFileSync16,
|
|
12941
13612
|
unlinkSync as unlinkSync6,
|
|
12942
|
-
writeFileSync as
|
|
13613
|
+
writeFileSync as writeFileSync13
|
|
12943
13614
|
} from "node:fs";
|
|
12944
|
-
import { join as
|
|
13615
|
+
import { join as join28 } from "node:path";
|
|
12945
13616
|
function printHelp5() {
|
|
12946
13617
|
process.stderr.write(`
|
|
12947
13618
|
${bold2("locus discuss")} — AI-powered architectural discussions
|
|
@@ -12963,12 +13634,12 @@ ${bold2("Examples:")}
|
|
|
12963
13634
|
`);
|
|
12964
13635
|
}
|
|
12965
13636
|
function getDiscussionsDir(projectRoot) {
|
|
12966
|
-
return
|
|
13637
|
+
return join28(projectRoot, ".locus", "discussions");
|
|
12967
13638
|
}
|
|
12968
13639
|
function ensureDiscussionsDir(projectRoot) {
|
|
12969
13640
|
const dir = getDiscussionsDir(projectRoot);
|
|
12970
|
-
if (!
|
|
12971
|
-
|
|
13641
|
+
if (!existsSync27(dir)) {
|
|
13642
|
+
mkdirSync18(dir, { recursive: true });
|
|
12972
13643
|
}
|
|
12973
13644
|
return dir;
|
|
12974
13645
|
}
|
|
@@ -13002,7 +13673,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
13002
13673
|
}
|
|
13003
13674
|
function listDiscussions(projectRoot) {
|
|
13004
13675
|
const dir = getDiscussionsDir(projectRoot);
|
|
13005
|
-
if (!
|
|
13676
|
+
if (!existsSync27(dir)) {
|
|
13006
13677
|
process.stderr.write(`${dim2("No discussions yet.")}
|
|
13007
13678
|
`);
|
|
13008
13679
|
return;
|
|
@@ -13019,7 +13690,7 @@ ${bold2("Discussions:")}
|
|
|
13019
13690
|
`);
|
|
13020
13691
|
for (const file of files) {
|
|
13021
13692
|
const id = file.replace(".md", "");
|
|
13022
|
-
const content =
|
|
13693
|
+
const content = readFileSync16(join28(dir, file), "utf-8");
|
|
13023
13694
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
13024
13695
|
const title = titleMatch ? titleMatch[1] : id;
|
|
13025
13696
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -13037,7 +13708,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13037
13708
|
return;
|
|
13038
13709
|
}
|
|
13039
13710
|
const dir = getDiscussionsDir(projectRoot);
|
|
13040
|
-
if (!
|
|
13711
|
+
if (!existsSync27(dir)) {
|
|
13041
13712
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13042
13713
|
`);
|
|
13043
13714
|
return;
|
|
@@ -13049,7 +13720,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13049
13720
|
`);
|
|
13050
13721
|
return;
|
|
13051
13722
|
}
|
|
13052
|
-
const content =
|
|
13723
|
+
const content = readFileSync16(join28(dir, match), "utf-8");
|
|
13053
13724
|
process.stdout.write(`${content}
|
|
13054
13725
|
`);
|
|
13055
13726
|
}
|
|
@@ -13060,7 +13731,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13060
13731
|
return;
|
|
13061
13732
|
}
|
|
13062
13733
|
const dir = getDiscussionsDir(projectRoot);
|
|
13063
|
-
if (!
|
|
13734
|
+
if (!existsSync27(dir)) {
|
|
13064
13735
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13065
13736
|
`);
|
|
13066
13737
|
return;
|
|
@@ -13072,7 +13743,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13072
13743
|
`);
|
|
13073
13744
|
return;
|
|
13074
13745
|
}
|
|
13075
|
-
unlinkSync6(
|
|
13746
|
+
unlinkSync6(join28(dir, match));
|
|
13076
13747
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
13077
13748
|
`);
|
|
13078
13749
|
}
|
|
@@ -13085,7 +13756,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
13085
13756
|
return;
|
|
13086
13757
|
}
|
|
13087
13758
|
const dir = getDiscussionsDir(projectRoot);
|
|
13088
|
-
if (!
|
|
13759
|
+
if (!existsSync27(dir)) {
|
|
13089
13760
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13090
13761
|
`);
|
|
13091
13762
|
return;
|
|
@@ -13097,7 +13768,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
13097
13768
|
`);
|
|
13098
13769
|
return;
|
|
13099
13770
|
}
|
|
13100
|
-
const content =
|
|
13771
|
+
const content = readFileSync16(join28(dir, match), "utf-8");
|
|
13101
13772
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
13102
13773
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
13103
13774
|
await planCommand(projectRoot, [
|
|
@@ -13223,7 +13894,7 @@ ${turn.content}`;
|
|
|
13223
13894
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
13224
13895
|
].join(`
|
|
13225
13896
|
`);
|
|
13226
|
-
|
|
13897
|
+
writeFileSync13(join28(dir, `${id}.md`), markdown, "utf-8");
|
|
13227
13898
|
process.stderr.write(`
|
|
13228
13899
|
${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
13229
13900
|
`);
|
|
@@ -13238,16 +13909,16 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
13238
13909
|
parts.push(`<role>
|
|
13239
13910
|
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
13240
13911
|
</role>`);
|
|
13241
|
-
const locusPath =
|
|
13242
|
-
if (
|
|
13243
|
-
const content =
|
|
13912
|
+
const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
|
|
13913
|
+
if (existsSync27(locusPath)) {
|
|
13914
|
+
const content = readFileSync16(locusPath, "utf-8");
|
|
13244
13915
|
parts.push(`<project-context>
|
|
13245
13916
|
${content.slice(0, 3000)}
|
|
13246
13917
|
</project-context>`);
|
|
13247
13918
|
}
|
|
13248
|
-
const learningsPath =
|
|
13249
|
-
if (
|
|
13250
|
-
const content =
|
|
13919
|
+
const learningsPath = join28(projectRoot, ".locus", "LEARNINGS.md");
|
|
13920
|
+
if (existsSync27(learningsPath)) {
|
|
13921
|
+
const content = readFileSync16(learningsPath, "utf-8");
|
|
13251
13922
|
parts.push(`<past-learnings>
|
|
13252
13923
|
${content.slice(0, 2000)}
|
|
13253
13924
|
</past-learnings>`);
|
|
@@ -13318,8 +13989,8 @@ __export(exports_artifacts, {
|
|
|
13318
13989
|
formatDate: () => formatDate2,
|
|
13319
13990
|
artifactsCommand: () => artifactsCommand
|
|
13320
13991
|
});
|
|
13321
|
-
import { existsSync as
|
|
13322
|
-
import { join as
|
|
13992
|
+
import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as statSync5 } from "node:fs";
|
|
13993
|
+
import { join as join29 } from "node:path";
|
|
13323
13994
|
function printHelp6() {
|
|
13324
13995
|
process.stderr.write(`
|
|
13325
13996
|
${bold2("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -13339,14 +14010,14 @@ ${dim2("Artifact names support partial matching.")}
|
|
|
13339
14010
|
`);
|
|
13340
14011
|
}
|
|
13341
14012
|
function getArtifactsDir(projectRoot) {
|
|
13342
|
-
return
|
|
14013
|
+
return join29(projectRoot, ".locus", "artifacts");
|
|
13343
14014
|
}
|
|
13344
14015
|
function listArtifacts(projectRoot) {
|
|
13345
14016
|
const dir = getArtifactsDir(projectRoot);
|
|
13346
|
-
if (!
|
|
14017
|
+
if (!existsSync28(dir))
|
|
13347
14018
|
return [];
|
|
13348
14019
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
13349
|
-
const filePath =
|
|
14020
|
+
const filePath = join29(dir, fileName);
|
|
13350
14021
|
const stat = statSync5(filePath);
|
|
13351
14022
|
return {
|
|
13352
14023
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -13359,12 +14030,12 @@ function listArtifacts(projectRoot) {
|
|
|
13359
14030
|
function readArtifact(projectRoot, name) {
|
|
13360
14031
|
const dir = getArtifactsDir(projectRoot);
|
|
13361
14032
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
13362
|
-
const filePath =
|
|
13363
|
-
if (!
|
|
14033
|
+
const filePath = join29(dir, fileName);
|
|
14034
|
+
if (!existsSync28(filePath))
|
|
13364
14035
|
return null;
|
|
13365
14036
|
const stat = statSync5(filePath);
|
|
13366
14037
|
return {
|
|
13367
|
-
content:
|
|
14038
|
+
content: readFileSync17(filePath, "utf-8"),
|
|
13368
14039
|
info: {
|
|
13369
14040
|
name: fileName.replace(/\.md$/, ""),
|
|
13370
14041
|
fileName,
|
|
@@ -13720,9 +14391,9 @@ __export(exports_sandbox2, {
|
|
|
13720
14391
|
parseSandboxInstallArgs: () => parseSandboxInstallArgs
|
|
13721
14392
|
});
|
|
13722
14393
|
import { execSync as execSync22, spawn as spawn7 } from "node:child_process";
|
|
13723
|
-
import { createHash } from "node:crypto";
|
|
13724
|
-
import { existsSync as
|
|
13725
|
-
import { basename as basename4, join as
|
|
14394
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
14395
|
+
import { existsSync as existsSync29, readFileSync as readFileSync18 } from "node:fs";
|
|
14396
|
+
import { basename as basename4, join as join30 } from "node:path";
|
|
13726
14397
|
import { createInterface as createInterface3 } from "node:readline";
|
|
13727
14398
|
function printSandboxHelp() {
|
|
13728
14399
|
process.stderr.write(`
|
|
@@ -14252,7 +14923,7 @@ async function handleLogs(projectRoot, args) {
|
|
|
14252
14923
|
}
|
|
14253
14924
|
function detectPackageManager2(projectRoot) {
|
|
14254
14925
|
try {
|
|
14255
|
-
const raw =
|
|
14926
|
+
const raw = readFileSync18(join30(projectRoot, "package.json"), "utf-8");
|
|
14256
14927
|
const pkgJson = JSON.parse(raw);
|
|
14257
14928
|
if (typeof pkgJson.packageManager === "string") {
|
|
14258
14929
|
const name = pkgJson.packageManager.split("@")[0];
|
|
@@ -14261,13 +14932,13 @@ function detectPackageManager2(projectRoot) {
|
|
|
14261
14932
|
}
|
|
14262
14933
|
}
|
|
14263
14934
|
} catch {}
|
|
14264
|
-
if (
|
|
14935
|
+
if (existsSync29(join30(projectRoot, "bun.lock")) || existsSync29(join30(projectRoot, "bun.lockb"))) {
|
|
14265
14936
|
return "bun";
|
|
14266
14937
|
}
|
|
14267
|
-
if (
|
|
14938
|
+
if (existsSync29(join30(projectRoot, "yarn.lock"))) {
|
|
14268
14939
|
return "yarn";
|
|
14269
14940
|
}
|
|
14270
|
-
if (
|
|
14941
|
+
if (existsSync29(join30(projectRoot, "pnpm-lock.yaml"))) {
|
|
14271
14942
|
return "pnpm";
|
|
14272
14943
|
}
|
|
14273
14944
|
return "npm";
|
|
@@ -14370,9 +15041,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
|
|
|
14370
15041
|
${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
14371
15042
|
`);
|
|
14372
15043
|
}
|
|
14373
|
-
const setupScript =
|
|
14374
|
-
const containerSetupScript = containerWorkdir ?
|
|
14375
|
-
if (
|
|
15044
|
+
const setupScript = join30(projectRoot, ".locus", "sandbox-setup.sh");
|
|
15045
|
+
const containerSetupScript = containerWorkdir ? join30(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
|
|
15046
|
+
if (existsSync29(setupScript)) {
|
|
14376
15047
|
process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
|
|
14377
15048
|
`);
|
|
14378
15049
|
const hookOk = await runInteractiveCommand("docker", [
|
|
@@ -14419,7 +15090,7 @@ async function handleSetup(projectRoot) {
|
|
|
14419
15090
|
}
|
|
14420
15091
|
function buildProviderSandboxNames(projectRoot) {
|
|
14421
15092
|
const segment = sanitizeSegment(basename4(projectRoot));
|
|
14422
|
-
const hash =
|
|
15093
|
+
const hash = createHash2("sha1").update(projectRoot).digest("hex").slice(0, 8);
|
|
14423
15094
|
return {
|
|
14424
15095
|
claude: `locus-${segment}-claude-${hash}`,
|
|
14425
15096
|
codex: `locus-${segment}-codex-${hash}`
|
|
@@ -14513,16 +15184,21 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
14513
15184
|
}
|
|
14514
15185
|
}
|
|
14515
15186
|
function isSandboxAlive(name) {
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
15187
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
15188
|
+
try {
|
|
15189
|
+
const output = execSync22("docker sandbox ls", {
|
|
15190
|
+
encoding: "utf-8",
|
|
15191
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
15192
|
+
timeout: 15000
|
|
15193
|
+
});
|
|
15194
|
+
return output.includes(name);
|
|
15195
|
+
} catch {
|
|
15196
|
+
if (attempt < 2) {
|
|
15197
|
+
execSync22("sleep 2", { stdio: "ignore" });
|
|
15198
|
+
}
|
|
15199
|
+
}
|
|
14525
15200
|
}
|
|
15201
|
+
return false;
|
|
14526
15202
|
}
|
|
14527
15203
|
var PROVIDERS;
|
|
14528
15204
|
var init_sandbox2 = __esm(() => {
|
|
@@ -14540,17 +15216,17 @@ init_context();
|
|
|
14540
15216
|
init_logger();
|
|
14541
15217
|
init_rate_limiter();
|
|
14542
15218
|
init_terminal();
|
|
14543
|
-
import { existsSync as
|
|
14544
|
-
import { join as
|
|
15219
|
+
import { existsSync as existsSync30, readFileSync as readFileSync19 } from "node:fs";
|
|
15220
|
+
import { join as join31 } from "node:path";
|
|
14545
15221
|
import { fileURLToPath } from "node:url";
|
|
14546
15222
|
function getCliVersion() {
|
|
14547
15223
|
const fallbackVersion = "0.0.0";
|
|
14548
|
-
const packageJsonPath =
|
|
14549
|
-
if (!
|
|
15224
|
+
const packageJsonPath = join31(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
15225
|
+
if (!existsSync30(packageJsonPath)) {
|
|
14550
15226
|
return fallbackVersion;
|
|
14551
15227
|
}
|
|
14552
15228
|
try {
|
|
14553
|
-
const parsed = JSON.parse(
|
|
15229
|
+
const parsed = JSON.parse(readFileSync19(packageJsonPath, "utf-8"));
|
|
14554
15230
|
return parsed.version ?? fallbackVersion;
|
|
14555
15231
|
} catch {
|
|
14556
15232
|
return fallbackVersion;
|
|
@@ -14580,6 +15256,7 @@ function parseArgs(argv) {
|
|
|
14580
15256
|
check: false,
|
|
14581
15257
|
upgrade: false,
|
|
14582
15258
|
list: false,
|
|
15259
|
+
installed: false,
|
|
14583
15260
|
noSandbox: false
|
|
14584
15261
|
};
|
|
14585
15262
|
const positional = [];
|
|
@@ -14657,9 +15334,15 @@ function parseArgs(argv) {
|
|
|
14657
15334
|
case "--target-version":
|
|
14658
15335
|
flags.targetVersion = rawArgs[++i];
|
|
14659
15336
|
break;
|
|
15337
|
+
case "--installed":
|
|
15338
|
+
flags.installed = true;
|
|
15339
|
+
break;
|
|
14660
15340
|
case "--no-sandbox":
|
|
14661
15341
|
flags.noSandbox = true;
|
|
14662
15342
|
break;
|
|
15343
|
+
case "--sprint":
|
|
15344
|
+
flags.sprint = rawArgs[++i];
|
|
15345
|
+
break;
|
|
14663
15346
|
default:
|
|
14664
15347
|
if (arg.startsWith("--sandbox=")) {
|
|
14665
15348
|
flags.sandbox = arg.slice("--sandbox=".length);
|
|
@@ -14732,6 +15415,7 @@ ${bold2("Commands:")}
|
|
|
14732
15415
|
${cyan2("uninstall")} Remove an installed package
|
|
14733
15416
|
${cyan2("packages")} Manage installed packages (list, outdated)
|
|
14734
15417
|
${cyan2("pkg")} ${dim2("<name> [cmd]")} Run a command from an installed package
|
|
15418
|
+
${cyan2("skills")} Discover and manage agent skills
|
|
14735
15419
|
${cyan2("sandbox")} Manage Docker sandbox lifecycle
|
|
14736
15420
|
${cyan2("upgrade")} Check for and install updates
|
|
14737
15421
|
|
|
@@ -14750,6 +15434,8 @@ ${bold2("Examples:")}
|
|
|
14750
15434
|
locus run 42 43 ${dim2("# Run issues in parallel")}
|
|
14751
15435
|
locus run 42 --no-sandbox ${dim2("# Run without sandbox")}
|
|
14752
15436
|
locus run 42 --sandbox=require ${dim2("# Require Docker sandbox")}
|
|
15437
|
+
locus skills list ${dim2("# Browse available skills")}
|
|
15438
|
+
locus skills install code-review ${dim2("# Install a skill")}
|
|
14753
15439
|
locus sandbox ${dim2("# Create Docker sandbox")}
|
|
14754
15440
|
locus sandbox claude ${dim2("# Login to Claude in sandbox")}
|
|
14755
15441
|
|
|
@@ -14823,7 +15509,7 @@ async function main() {
|
|
|
14823
15509
|
try {
|
|
14824
15510
|
const root = getGitRoot(cwd);
|
|
14825
15511
|
if (isInitialized(root)) {
|
|
14826
|
-
logDir =
|
|
15512
|
+
logDir = join31(root, ".locus", "logs");
|
|
14827
15513
|
getRateLimiter(root);
|
|
14828
15514
|
}
|
|
14829
15515
|
} catch {}
|
|
@@ -14891,6 +15577,17 @@ async function main() {
|
|
|
14891
15577
|
logger.destroy();
|
|
14892
15578
|
return;
|
|
14893
15579
|
}
|
|
15580
|
+
if (command === "skills") {
|
|
15581
|
+
const { skillsCommand: skillsCommand2 } = await Promise.resolve().then(() => (init_skills(), exports_skills));
|
|
15582
|
+
const skillsArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
15583
|
+
const skillsFlags = {};
|
|
15584
|
+
if (parsed.flags.installed) {
|
|
15585
|
+
skillsFlags.installed = "true";
|
|
15586
|
+
}
|
|
15587
|
+
await skillsCommand2(skillsArgs, skillsFlags);
|
|
15588
|
+
logger.destroy();
|
|
15589
|
+
return;
|
|
15590
|
+
}
|
|
14894
15591
|
if (command === "pkg") {
|
|
14895
15592
|
const { pkgCommand: pkgCommand2 } = await Promise.resolve().then(() => (init_pkg(), exports_pkg));
|
|
14896
15593
|
await pkgCommand2(parsed.args, {});
|
|
@@ -14963,7 +15660,8 @@ async function main() {
|
|
|
14963
15660
|
dryRun: parsed.flags.dryRun,
|
|
14964
15661
|
model: parsed.flags.model,
|
|
14965
15662
|
sandbox: parsed.flags.sandbox,
|
|
14966
|
-
noSandbox: parsed.flags.noSandbox
|
|
15663
|
+
noSandbox: parsed.flags.noSandbox,
|
|
15664
|
+
sprint: parsed.flags.sprint
|
|
14967
15665
|
});
|
|
14968
15666
|
break;
|
|
14969
15667
|
}
|