@locusai/cli 0.24.9 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +91 -4
  2. package/bin/locus.js +975 -387
  3. package/package.json +2 -2
package/bin/locus.js CHANGED
@@ -3213,6 +3213,602 @@ var init_uninstall = __esm(() => {
3213
3213
  init_registry();
3214
3214
  });
3215
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
+
3216
3812
  // src/commands/config.ts
3217
3813
  var exports_config = {};
3218
3814
  __export(exports_config, {
@@ -3276,9 +3872,7 @@ ${bold2("Locus Configuration")}
3276
3872
  },
3277
3873
  {
3278
3874
  title: "Sprint",
3279
- entries: [
3280
- ["Stop on Failure", String(config.sprint.stopOnFailure)]
3281
- ]
3875
+ entries: [["Stop on Failure", String(config.sprint.stopOnFailure)]]
3282
3876
  },
3283
3877
  {
3284
3878
  title: "Logging",
@@ -3351,16 +3945,16 @@ __export(exports_logs, {
3351
3945
  logsCommand: () => logsCommand
3352
3946
  });
3353
3947
  import {
3354
- existsSync as existsSync11,
3948
+ existsSync as existsSync12,
3355
3949
  readdirSync as readdirSync2,
3356
- readFileSync as readFileSync7,
3950
+ readFileSync as readFileSync8,
3357
3951
  statSync as statSync2,
3358
3952
  unlinkSync as unlinkSync2
3359
3953
  } from "node:fs";
3360
- import { join as join10 } from "node:path";
3954
+ import { join as join12 } from "node:path";
3361
3955
  async function logsCommand(cwd, options) {
3362
- const logsDir = join10(cwd, ".locus", "logs");
3363
- if (!existsSync11(logsDir)) {
3956
+ const logsDir = join12(cwd, ".locus", "logs");
3957
+ if (!existsSync12(logsDir)) {
3364
3958
  process.stderr.write(`${dim2("No logs found.")}
3365
3959
  `);
3366
3960
  return;
@@ -3380,7 +3974,7 @@ async function logsCommand(cwd, options) {
3380
3974
  return viewLog(logFiles[0], options.level, options.lines ?? 50);
3381
3975
  }
3382
3976
  function viewLog(logFile, levelFilter, maxLines) {
3383
- const content = readFileSync7(logFile, "utf-8");
3977
+ const content = readFileSync8(logFile, "utf-8");
3384
3978
  const lines = content.trim().split(`
3385
3979
  `).filter(Boolean);
3386
3980
  process.stderr.write(`
@@ -3415,9 +4009,9 @@ async function tailLog(logFile, levelFilter) {
3415
4009
  process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
3416
4010
 
3417
4011
  `);
3418
- let lastSize = existsSync11(logFile) ? statSync2(logFile).size : 0;
3419
- if (existsSync11(logFile)) {
3420
- const content = readFileSync7(logFile, "utf-8");
4012
+ let lastSize = existsSync12(logFile) ? statSync2(logFile).size : 0;
4013
+ if (existsSync12(logFile)) {
4014
+ const content = readFileSync8(logFile, "utf-8");
3421
4015
  const lines = content.trim().split(`
3422
4016
  `).filter(Boolean);
3423
4017
  const recent = lines.slice(-10);
@@ -3435,12 +4029,12 @@ async function tailLog(logFile, levelFilter) {
3435
4029
  }
3436
4030
  return new Promise((resolve) => {
3437
4031
  const interval = setInterval(() => {
3438
- if (!existsSync11(logFile))
4032
+ if (!existsSync12(logFile))
3439
4033
  return;
3440
4034
  const currentSize = statSync2(logFile).size;
3441
4035
  if (currentSize <= lastSize)
3442
4036
  return;
3443
- const content = readFileSync7(logFile, "utf-8");
4037
+ const content = readFileSync8(logFile, "utf-8");
3444
4038
  const allLines = content.trim().split(`
3445
4039
  `).filter(Boolean);
3446
4040
  const oldContent = content.slice(0, lastSize);
@@ -3495,7 +4089,7 @@ function cleanLogs(logsDir) {
3495
4089
  `);
3496
4090
  }
3497
4091
  function getLogFiles(logsDir) {
3498
- return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join10(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
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);
3499
4093
  }
3500
4094
  function formatEntry(entry) {
3501
4095
  const time = dim2(new Date(entry.ts).toLocaleTimeString());
@@ -3805,9 +4399,9 @@ var init_stream_renderer = __esm(() => {
3805
4399
 
3806
4400
  // src/repl/clipboard.ts
3807
4401
  import { execSync as execSync6 } from "node:child_process";
3808
- import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
4402
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
3809
4403
  import { tmpdir } from "node:os";
3810
- import { join as join11 } from "node:path";
4404
+ import { join as join13 } from "node:path";
3811
4405
  function readClipboardImage() {
3812
4406
  if (process.platform === "darwin") {
3813
4407
  return readMacOSClipboardImage();
@@ -3818,14 +4412,14 @@ function readClipboardImage() {
3818
4412
  return null;
3819
4413
  }
3820
4414
  function ensureStableDir() {
3821
- if (!existsSync12(STABLE_DIR)) {
3822
- mkdirSync8(STABLE_DIR, { recursive: true });
4415
+ if (!existsSync13(STABLE_DIR)) {
4416
+ mkdirSync9(STABLE_DIR, { recursive: true });
3823
4417
  }
3824
4418
  }
3825
4419
  function readMacOSClipboardImage() {
3826
4420
  try {
3827
4421
  ensureStableDir();
3828
- const destPath = join11(STABLE_DIR, `clipboard-${Date.now()}.png`);
4422
+ const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
3829
4423
  const script = [
3830
4424
  `set destPath to POSIX file "${destPath}"`,
3831
4425
  "try",
@@ -3849,7 +4443,7 @@ function readMacOSClipboardImage() {
3849
4443
  timeout: 5000,
3850
4444
  stdio: ["pipe", "pipe", "pipe"]
3851
4445
  }).trim();
3852
- if (result === "ok" && existsSync12(destPath)) {
4446
+ if (result === "ok" && existsSync13(destPath)) {
3853
4447
  return destPath;
3854
4448
  }
3855
4449
  } catch {}
@@ -3862,9 +4456,9 @@ function readLinuxClipboardImage() {
3862
4456
  return null;
3863
4457
  }
3864
4458
  ensureStableDir();
3865
- const destPath = join11(STABLE_DIR, `clipboard-${Date.now()}.png`);
4459
+ const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
3866
4460
  execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3867
- if (existsSync12(destPath)) {
4461
+ if (existsSync13(destPath)) {
3868
4462
  return destPath;
3869
4463
  }
3870
4464
  } catch {}
@@ -3872,13 +4466,13 @@ function readLinuxClipboardImage() {
3872
4466
  }
3873
4467
  var STABLE_DIR;
3874
4468
  var init_clipboard = __esm(() => {
3875
- STABLE_DIR = join11(tmpdir(), "locus-images");
4469
+ STABLE_DIR = join13(tmpdir(), "locus-images");
3876
4470
  });
3877
4471
 
3878
4472
  // src/repl/image-detect.ts
3879
- import { copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
4473
+ import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10 } from "node:fs";
3880
4474
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
3881
- import { basename, extname, join as join12, resolve } from "node:path";
4475
+ import { basename, extname, join as join14, resolve } from "node:path";
3882
4476
  function detectImages(input) {
3883
4477
  const detected = [];
3884
4478
  const byResolved = new Map;
@@ -3972,15 +4566,15 @@ function collectReferencedAttachments(input, attachments) {
3972
4566
  return dedupeByResolvedPath(selected);
3973
4567
  }
3974
4568
  function relocateImages(images, projectRoot) {
3975
- const targetDir = join12(projectRoot, ".locus", "tmp", "images");
4569
+ const targetDir = join14(projectRoot, ".locus", "tmp", "images");
3976
4570
  for (const img of images) {
3977
4571
  if (!img.exists)
3978
4572
  continue;
3979
4573
  try {
3980
- if (!existsSync13(targetDir)) {
3981
- mkdirSync9(targetDir, { recursive: true });
4574
+ if (!existsSync14(targetDir)) {
4575
+ mkdirSync10(targetDir, { recursive: true });
3982
4576
  }
3983
- const dest = join12(targetDir, basename(img.stablePath));
4577
+ const dest = join14(targetDir, basename(img.stablePath));
3984
4578
  copyFileSync(img.stablePath, dest);
3985
4579
  img.stablePath = dest;
3986
4580
  } catch {}
@@ -3992,7 +4586,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
3992
4586
  return;
3993
4587
  let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
3994
4588
  if (resolved.startsWith("~/")) {
3995
- resolved = join12(homedir3(), resolved.slice(2));
4589
+ resolved = join14(homedir3(), resolved.slice(2));
3996
4590
  }
3997
4591
  resolved = resolve(resolved);
3998
4592
  const existing = byResolved.get(resolved);
@@ -4005,7 +4599,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
4005
4599
  ]);
4006
4600
  return;
4007
4601
  }
4008
- const exists = existsSync13(resolved);
4602
+ const exists = existsSync14(resolved);
4009
4603
  let stablePath = resolved;
4010
4604
  if (exists) {
4011
4605
  stablePath = copyToStable(resolved);
@@ -4059,10 +4653,10 @@ function dedupeByResolvedPath(images) {
4059
4653
  }
4060
4654
  function copyToStable(sourcePath) {
4061
4655
  try {
4062
- if (!existsSync13(STABLE_DIR2)) {
4063
- mkdirSync9(STABLE_DIR2, { recursive: true });
4656
+ if (!existsSync14(STABLE_DIR2)) {
4657
+ mkdirSync10(STABLE_DIR2, { recursive: true });
4064
4658
  }
4065
- const dest = join12(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
4659
+ const dest = join14(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
4066
4660
  copyFileSync(sourcePath, dest);
4067
4661
  return dest;
4068
4662
  } catch {
@@ -4082,7 +4676,7 @@ var init_image_detect = __esm(() => {
4082
4676
  ".tif",
4083
4677
  ".tiff"
4084
4678
  ]);
4085
- STABLE_DIR2 = join12(tmpdir2(), "locus-images");
4679
+ STABLE_DIR2 = join14(tmpdir2(), "locus-images");
4086
4680
  PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
4087
4681
  });
4088
4682
 
@@ -5091,21 +5685,21 @@ var init_claude = __esm(() => {
5091
5685
  import { exec } from "node:child_process";
5092
5686
  import {
5093
5687
  cpSync,
5094
- existsSync as existsSync14,
5095
- mkdirSync as mkdirSync10,
5688
+ existsSync as existsSync15,
5689
+ mkdirSync as mkdirSync11,
5096
5690
  mkdtempSync,
5097
5691
  readdirSync as readdirSync3,
5098
- readFileSync as readFileSync8,
5099
- rmSync,
5692
+ readFileSync as readFileSync9,
5693
+ rmSync as rmSync2,
5100
5694
  statSync as statSync3
5101
5695
  } from "node:fs";
5102
5696
  import { tmpdir as tmpdir3 } from "node:os";
5103
- import { dirname as dirname3, join as join13, relative } from "node:path";
5697
+ import { dirname as dirname3, join as join15, relative } from "node:path";
5104
5698
  import { promisify } from "node:util";
5105
5699
  function parseIgnoreFile(filePath) {
5106
- if (!existsSync14(filePath))
5700
+ if (!existsSync15(filePath))
5107
5701
  return [];
5108
- const content = readFileSync8(filePath, "utf-8");
5702
+ const content = readFileSync9(filePath, "utf-8");
5109
5703
  const rules = [];
5110
5704
  for (const rawLine of content.split(`
5111
5705
  `)) {
@@ -5173,7 +5767,7 @@ function findIgnoredPaths(projectRoot, rules) {
5173
5767
  for (const name of entries) {
5174
5768
  if (SKIP_DIRS.has(name))
5175
5769
  continue;
5176
- const fullPath = join13(dir, name);
5770
+ const fullPath = join15(dir, name);
5177
5771
  let stat = null;
5178
5772
  try {
5179
5773
  stat = statSync3(fullPath);
@@ -5207,7 +5801,7 @@ function findIgnoredPaths(projectRoot, rules) {
5207
5801
  }
5208
5802
  function backupIgnoredFiles(projectRoot) {
5209
5803
  const log = getLogger();
5210
- const ignorePath = join13(projectRoot, ".sandboxignore");
5804
+ const ignorePath = join15(projectRoot, ".sandboxignore");
5211
5805
  const rules = parseIgnoreFile(ignorePath);
5212
5806
  if (rules.length === 0)
5213
5807
  return NOOP_BACKUP;
@@ -5216,7 +5810,7 @@ function backupIgnoredFiles(projectRoot) {
5216
5810
  return NOOP_BACKUP;
5217
5811
  let backupDir;
5218
5812
  try {
5219
- backupDir = mkdtempSync(join13(tmpdir3(), "locus-sandbox-backup-"));
5813
+ backupDir = mkdtempSync(join15(tmpdir3(), "locus-sandbox-backup-"));
5220
5814
  } catch (err) {
5221
5815
  log.debug("Failed to create sandbox backup dir", {
5222
5816
  error: err instanceof Error ? err.message : String(err)
@@ -5226,9 +5820,9 @@ function backupIgnoredFiles(projectRoot) {
5226
5820
  const backed = [];
5227
5821
  for (const src of paths) {
5228
5822
  const rel = relative(projectRoot, src);
5229
- const dest = join13(backupDir, rel);
5823
+ const dest = join15(backupDir, rel);
5230
5824
  try {
5231
- mkdirSync10(dirname3(dest), { recursive: true });
5825
+ mkdirSync11(dirname3(dest), { recursive: true });
5232
5826
  cpSync(src, dest, { recursive: true, preserveTimestamps: true });
5233
5827
  backed.push({ src, dest });
5234
5828
  } catch (err) {
@@ -5239,7 +5833,7 @@ function backupIgnoredFiles(projectRoot) {
5239
5833
  }
5240
5834
  }
5241
5835
  if (backed.length === 0) {
5242
- rmSync(backupDir, { recursive: true, force: true });
5836
+ rmSync2(backupDir, { recursive: true, force: true });
5243
5837
  return NOOP_BACKUP;
5244
5838
  }
5245
5839
  log.debug("Backed up sandbox-ignored files", {
@@ -5250,7 +5844,7 @@ function backupIgnoredFiles(projectRoot) {
5250
5844
  restore() {
5251
5845
  for (const { src, dest } of backed) {
5252
5846
  try {
5253
- mkdirSync10(dirname3(src), { recursive: true });
5847
+ mkdirSync11(dirname3(src), { recursive: true });
5254
5848
  cpSync(dest, src, { recursive: true, preserveTimestamps: true });
5255
5849
  } catch (err) {
5256
5850
  log.debug("Failed to restore ignored file (potential data loss)", {
@@ -5260,7 +5854,7 @@ function backupIgnoredFiles(projectRoot) {
5260
5854
  }
5261
5855
  }
5262
5856
  try {
5263
- rmSync(backupDir, { recursive: true, force: true });
5857
+ rmSync2(backupDir, { recursive: true, force: true });
5264
5858
  } catch {}
5265
5859
  log.debug("Restored sandbox-ignored files", { count: backed.length });
5266
5860
  }
@@ -5268,7 +5862,7 @@ function backupIgnoredFiles(projectRoot) {
5268
5862
  }
5269
5863
  async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
5270
5864
  const log = getLogger();
5271
- const ignorePath = join13(projectRoot, ".sandboxignore");
5865
+ const ignorePath = join15(projectRoot, ".sandboxignore");
5272
5866
  const rules = parseIgnoreFile(ignorePath);
5273
5867
  if (rules.length === 0)
5274
5868
  return;
@@ -5486,17 +6080,22 @@ class SandboxedClaudeRunner {
5486
6080
  timer.unref();
5487
6081
  }
5488
6082
  async isSandboxRunning() {
5489
- try {
5490
- const { promisify: promisify2 } = await import("node:util");
5491
- const { exec: exec2 } = await import("node:child_process");
5492
- const execAsync2 = promisify2(exec2);
5493
- const { stdout } = await execAsync2("docker sandbox ls", {
5494
- timeout: 5000
5495
- });
5496
- return stdout.includes(this.sandboxName);
5497
- } catch {
5498
- return false;
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
+ }
5499
6097
  }
6098
+ return false;
5500
6099
  }
5501
6100
  }
5502
6101
  function formatToolCall2(name, input) {
@@ -5897,17 +6496,22 @@ class SandboxedCodexRunner {
5897
6496
  timer.unref();
5898
6497
  }
5899
6498
  async isSandboxRunning() {
5900
- try {
5901
- const { promisify: promisify2 } = await import("node:util");
5902
- const { exec: exec2 } = await import("node:child_process");
5903
- const execAsync2 = promisify2(exec2);
5904
- const { stdout } = await execAsync2("docker sandbox ls", {
5905
- timeout: 5000
5906
- });
5907
- return stdout.includes(this.sandboxName);
5908
- } catch {
5909
- return false;
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
+ }
5910
6513
  }
6514
+ return false;
5911
6515
  }
5912
6516
  async ensureCodexInstalled(name) {
5913
6517
  const { promisify: promisify2 } = await import("node:util");
@@ -6164,115 +6768,6 @@ var init_run_ai = __esm(() => {
6164
6768
  init_runner();
6165
6769
  });
6166
6770
 
6167
- // src/display/table.ts
6168
- function renderTable(columns, rows, options = {}) {
6169
- const {
6170
- indent = 2,
6171
- headerSeparator = true,
6172
- maxRows = 0,
6173
- emptyMessage = "No results."
6174
- } = options;
6175
- if (rows.length === 0) {
6176
- return `${" ".repeat(indent)}${dim2(emptyMessage)}`;
6177
- }
6178
- const termWidth = getCapabilities().columns;
6179
- const indentStr = " ".repeat(indent);
6180
- const gap = 2;
6181
- const formattedRows = rows.map((row) => {
6182
- const formatted = {};
6183
- for (const col of columns) {
6184
- if (col.format) {
6185
- formatted[col.key] = col.format(row[col.key], row);
6186
- } else {
6187
- const val = row[col.key];
6188
- formatted[col.key] = val === null || val === undefined ? "" : String(val);
6189
- }
6190
- }
6191
- return formatted;
6192
- });
6193
- const colWidths = columns.map((col, _i) => {
6194
- const headerWidth = stripAnsi(col.header).length;
6195
- const minWidth = col.minWidth ?? headerWidth;
6196
- let maxContent = headerWidth;
6197
- for (const row of formattedRows) {
6198
- const cellWidth = stripAnsi(row[col.key] ?? "").length;
6199
- if (cellWidth > maxContent)
6200
- maxContent = cellWidth;
6201
- }
6202
- let width = Math.max(minWidth, maxContent);
6203
- if (col.maxWidth && col.maxWidth > 0) {
6204
- width = Math.min(width, col.maxWidth);
6205
- }
6206
- return width;
6207
- });
6208
- const totalWidth = indent + colWidths.reduce((s, w) => s + w, 0) + gap * (columns.length - 1);
6209
- if (totalWidth > termWidth && columns.length > 1) {
6210
- const overflow = totalWidth - termWidth;
6211
- let widestIdx = 0;
6212
- let widestSize = 0;
6213
- for (let i = 0;i < columns.length; i++) {
6214
- if (!columns[i].maxWidth && colWidths[i] > widestSize) {
6215
- widestSize = colWidths[i];
6216
- widestIdx = i;
6217
- }
6218
- }
6219
- colWidths[widestIdx] = Math.max(10, colWidths[widestIdx] - overflow);
6220
- }
6221
- const lines = [];
6222
- const headerParts = columns.map((col, i) => alignCell(bold2(col.header), colWidths[i], col.align ?? "left"));
6223
- lines.push(`${indentStr}${headerParts.join(" ".repeat(gap))}`);
6224
- if (headerSeparator) {
6225
- const sep = columns.map((_, i) => gray2("─".repeat(colWidths[i]))).join(" ".repeat(gap));
6226
- lines.push(`${indentStr}${sep}`);
6227
- }
6228
- const displayRows = maxRows > 0 ? formattedRows.slice(0, maxRows) : formattedRows;
6229
- for (const row of displayRows) {
6230
- const cellParts = columns.map((col, i) => {
6231
- const raw = row[col.key] ?? "";
6232
- return alignCell(raw, colWidths[i], col.align ?? "left");
6233
- });
6234
- lines.push(`${indentStr}${cellParts.join(" ".repeat(gap))}`);
6235
- }
6236
- if (maxRows > 0 && formattedRows.length > maxRows) {
6237
- const remaining = formattedRows.length - maxRows;
6238
- lines.push(`${indentStr}${dim2(`... and ${remaining} more`)}`);
6239
- }
6240
- return lines.join(`
6241
- `);
6242
- }
6243
- function alignCell(text, width, align) {
6244
- const visual = stripAnsi(text).length;
6245
- if (visual > width) {
6246
- const stripped = stripAnsi(text);
6247
- return `${stripped.slice(0, width - 1)}…`;
6248
- }
6249
- const padding = width - visual;
6250
- switch (align) {
6251
- case "right":
6252
- return " ".repeat(padding) + text;
6253
- case "center": {
6254
- const left = Math.floor(padding / 2);
6255
- const right = padding - left;
6256
- return " ".repeat(left) + text + " ".repeat(right);
6257
- }
6258
- default:
6259
- return text + " ".repeat(padding);
6260
- }
6261
- }
6262
- function renderDetails(entries, options = {}) {
6263
- const { indent = 2, labelWidth: fixedWidth } = options;
6264
- const indentStr = " ".repeat(indent);
6265
- const labelWidth = fixedWidth ?? Math.max(...entries.map((e) => stripAnsi(e.label).length)) + 1;
6266
- return entries.map((entry) => {
6267
- const label = padEnd(dim2(`${entry.label}:`), labelWidth + 1);
6268
- return `${indentStr}${label} ${entry.value}`;
6269
- }).join(`
6270
- `);
6271
- }
6272
- var init_table = __esm(() => {
6273
- init_terminal();
6274
- });
6275
-
6276
6771
  // src/commands/issue.ts
6277
6772
  var exports_issue = {};
6278
6773
  __export(exports_issue, {
@@ -7533,8 +8028,8 @@ var init_sprint = __esm(() => {
7533
8028
 
7534
8029
  // src/core/prompt-builder.ts
7535
8030
  import { execSync as execSync9 } from "node:child_process";
7536
- import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
7537
- import { join as join14 } from "node:path";
8031
+ import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
8032
+ import { join as join16 } from "node:path";
7538
8033
  function buildExecutionPrompt(ctx) {
7539
8034
  const sections = [];
7540
8035
  sections.push(buildSystemContext(ctx.projectRoot));
@@ -7564,7 +8059,7 @@ function buildFeedbackPrompt(ctx) {
7564
8059
  }
7565
8060
  function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
7566
8061
  const sections = [];
7567
- const locusmd = readFileSafe(join14(projectRoot, ".locus", "LOCUS.md"));
8062
+ const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
7568
8063
  if (locusmd) {
7569
8064
  sections.push(`<project-instructions>
7570
8065
  ${locusmd}
@@ -7593,7 +8088,7 @@ ${userMessage}
7593
8088
  }
7594
8089
  function buildSystemContext(projectRoot) {
7595
8090
  const parts = [];
7596
- const locusmd = readFileSafe(join14(projectRoot, ".locus", "LOCUS.md"));
8091
+ const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
7597
8092
  if (locusmd) {
7598
8093
  parts.push(`<project-instructions>
7599
8094
  ${locusmd}
@@ -7601,12 +8096,12 @@ ${locusmd}
7601
8096
  }
7602
8097
  parts.push(`<past-learnings>
7603
8098
  Past learnings are located in \`.locus/LEARNINGS.md\`.</past-learnings>`);
7604
- const discussionsDir = join14(projectRoot, ".locus", "discussions");
7605
- if (existsSync15(discussionsDir)) {
8099
+ const discussionsDir = join16(projectRoot, ".locus", "discussions");
8100
+ if (existsSync16(discussionsDir)) {
7606
8101
  try {
7607
8102
  const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
7608
8103
  for (const file of files) {
7609
- const content = readFileSafe(join14(discussionsDir, file));
8104
+ const content = readFileSafe(join16(discussionsDir, file));
7610
8105
  if (content) {
7611
8106
  const name = file.replace(".md", "");
7612
8107
  parts.push(`<discussion name="${name}">
@@ -7759,9 +8254,9 @@ function buildFeedbackInstructions() {
7759
8254
  }
7760
8255
  function readFileSafe(path) {
7761
8256
  try {
7762
- if (!existsSync15(path))
8257
+ if (!existsSync16(path))
7763
8258
  return null;
7764
- return readFileSync9(path, "utf-8");
8259
+ return readFileSync10(path, "utf-8");
7765
8260
  } catch {
7766
8261
  return null;
7767
8262
  }
@@ -8239,7 +8734,7 @@ var init_commands = __esm(() => {
8239
8734
 
8240
8735
  // src/repl/completions.ts
8241
8736
  import { readdirSync as readdirSync5 } from "node:fs";
8242
- import { basename as basename2, dirname as dirname4, join as join15 } from "node:path";
8737
+ import { basename as basename2, dirname as dirname4, join as join17 } from "node:path";
8243
8738
 
8244
8739
  class SlashCommandCompletion {
8245
8740
  commands;
@@ -8294,7 +8789,7 @@ class FilePathCompletion {
8294
8789
  }
8295
8790
  findMatches(partial) {
8296
8791
  try {
8297
- const dir = partial.includes("/") ? join15(this.projectRoot, dirname4(partial)) : this.projectRoot;
8792
+ const dir = partial.includes("/") ? join17(this.projectRoot, dirname4(partial)) : this.projectRoot;
8298
8793
  const prefix = basename2(partial);
8299
8794
  const entries = readdirSync5(dir, { withFileTypes: true });
8300
8795
  return entries.filter((e) => {
@@ -8330,14 +8825,14 @@ class CombinedCompletion {
8330
8825
  var init_completions = () => {};
8331
8826
 
8332
8827
  // src/repl/input-history.ts
8333
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
8334
- import { dirname as dirname5, join as join16 } from "node:path";
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";
8335
8830
 
8336
8831
  class InputHistory {
8337
8832
  entries = [];
8338
8833
  filePath;
8339
8834
  constructor(projectRoot) {
8340
- this.filePath = join16(projectRoot, ".locus", "sessions", ".input-history");
8835
+ this.filePath = join18(projectRoot, ".locus", "sessions", ".input-history");
8341
8836
  this.load();
8342
8837
  }
8343
8838
  add(text) {
@@ -8376,9 +8871,9 @@ class InputHistory {
8376
8871
  }
8377
8872
  load() {
8378
8873
  try {
8379
- if (!existsSync16(this.filePath))
8874
+ if (!existsSync17(this.filePath))
8380
8875
  return;
8381
- const content = readFileSync10(this.filePath, "utf-8");
8876
+ const content = readFileSync11(this.filePath, "utf-8");
8382
8877
  this.entries = content.split(`
8383
8878
  `).map((line) => this.unescape(line)).filter(Boolean);
8384
8879
  } catch {}
@@ -8386,12 +8881,12 @@ class InputHistory {
8386
8881
  save() {
8387
8882
  try {
8388
8883
  const dir = dirname5(this.filePath);
8389
- if (!existsSync16(dir)) {
8390
- mkdirSync11(dir, { recursive: true });
8884
+ if (!existsSync17(dir)) {
8885
+ mkdirSync12(dir, { recursive: true });
8391
8886
  }
8392
8887
  const content = this.entries.map((e) => this.escape(e)).join(`
8393
8888
  `);
8394
- writeFileSync7(this.filePath, content, "utf-8");
8889
+ writeFileSync9(this.filePath, content, "utf-8");
8395
8890
  } catch {}
8396
8891
  }
8397
8892
  escape(text) {
@@ -8417,21 +8912,21 @@ var init_model_config = __esm(() => {
8417
8912
 
8418
8913
  // src/repl/session-manager.ts
8419
8914
  import {
8420
- existsSync as existsSync17,
8421
- mkdirSync as mkdirSync12,
8915
+ existsSync as existsSync18,
8916
+ mkdirSync as mkdirSync13,
8422
8917
  readdirSync as readdirSync6,
8423
- readFileSync as readFileSync11,
8918
+ readFileSync as readFileSync12,
8424
8919
  unlinkSync as unlinkSync3,
8425
- writeFileSync as writeFileSync8
8920
+ writeFileSync as writeFileSync10
8426
8921
  } from "node:fs";
8427
- import { basename as basename3, join as join17 } from "node:path";
8922
+ import { basename as basename3, join as join19 } from "node:path";
8428
8923
 
8429
8924
  class SessionManager {
8430
8925
  sessionsDir;
8431
8926
  constructor(projectRoot) {
8432
- this.sessionsDir = join17(projectRoot, ".locus", "sessions");
8433
- if (!existsSync17(this.sessionsDir)) {
8434
- mkdirSync12(this.sessionsDir, { recursive: true });
8927
+ this.sessionsDir = join19(projectRoot, ".locus", "sessions");
8928
+ if (!existsSync18(this.sessionsDir)) {
8929
+ mkdirSync13(this.sessionsDir, { recursive: true });
8435
8930
  }
8436
8931
  }
8437
8932
  create(options) {
@@ -8456,14 +8951,14 @@ class SessionManager {
8456
8951
  }
8457
8952
  isPersisted(sessionOrId) {
8458
8953
  const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
8459
- return existsSync17(this.getSessionPath(sessionId));
8954
+ return existsSync18(this.getSessionPath(sessionId));
8460
8955
  }
8461
8956
  load(idOrPrefix) {
8462
8957
  const files = this.listSessionFiles();
8463
8958
  const exactPath = this.getSessionPath(idOrPrefix);
8464
- if (existsSync17(exactPath)) {
8959
+ if (existsSync18(exactPath)) {
8465
8960
  try {
8466
- return JSON.parse(readFileSync11(exactPath, "utf-8"));
8961
+ return JSON.parse(readFileSync12(exactPath, "utf-8"));
8467
8962
  } catch {
8468
8963
  return null;
8469
8964
  }
@@ -8471,7 +8966,7 @@ class SessionManager {
8471
8966
  const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
8472
8967
  if (matches.length === 1) {
8473
8968
  try {
8474
- return JSON.parse(readFileSync11(matches[0], "utf-8"));
8969
+ return JSON.parse(readFileSync12(matches[0], "utf-8"));
8475
8970
  } catch {
8476
8971
  return null;
8477
8972
  }
@@ -8484,7 +8979,7 @@ class SessionManager {
8484
8979
  save(session) {
8485
8980
  session.updated = new Date().toISOString();
8486
8981
  const path = this.getSessionPath(session.id);
8487
- writeFileSync8(path, `${JSON.stringify(session, null, 2)}
8982
+ writeFileSync10(path, `${JSON.stringify(session, null, 2)}
8488
8983
  `, "utf-8");
8489
8984
  }
8490
8985
  addMessage(session, message) {
@@ -8496,7 +8991,7 @@ class SessionManager {
8496
8991
  const sessions = [];
8497
8992
  for (const file of files) {
8498
8993
  try {
8499
- const session = JSON.parse(readFileSync11(file, "utf-8"));
8994
+ const session = JSON.parse(readFileSync12(file, "utf-8"));
8500
8995
  sessions.push({
8501
8996
  id: session.id,
8502
8997
  created: session.created,
@@ -8511,7 +9006,7 @@ class SessionManager {
8511
9006
  }
8512
9007
  delete(sessionId) {
8513
9008
  const path = this.getSessionPath(sessionId);
8514
- if (existsSync17(path)) {
9009
+ if (existsSync18(path)) {
8515
9010
  unlinkSync3(path);
8516
9011
  return true;
8517
9012
  }
@@ -8523,7 +9018,7 @@ class SessionManager {
8523
9018
  let pruned = 0;
8524
9019
  const withStats = files.map((f) => {
8525
9020
  try {
8526
- const session = JSON.parse(readFileSync11(f, "utf-8"));
9021
+ const session = JSON.parse(readFileSync12(f, "utf-8"));
8527
9022
  return { path: f, updated: new Date(session.updated).getTime() };
8528
9023
  } catch {
8529
9024
  return { path: f, updated: 0 };
@@ -8541,7 +9036,7 @@ class SessionManager {
8541
9036
  const remaining = withStats.length - pruned;
8542
9037
  if (remaining > MAX_SESSIONS) {
8543
9038
  const toRemove = remaining - MAX_SESSIONS;
8544
- const alive = withStats.filter((e) => existsSync17(e.path));
9039
+ const alive = withStats.filter((e) => existsSync18(e.path));
8545
9040
  for (let i = 0;i < toRemove && i < alive.length; i++) {
8546
9041
  try {
8547
9042
  unlinkSync3(alive[i].path);
@@ -8556,7 +9051,7 @@ class SessionManager {
8556
9051
  }
8557
9052
  listSessionFiles() {
8558
9053
  try {
8559
- return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join17(this.sessionsDir, f));
9054
+ return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join19(this.sessionsDir, f));
8560
9055
  } catch {
8561
9056
  return [];
8562
9057
  }
@@ -8565,7 +9060,7 @@ class SessionManager {
8565
9060
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
8566
9061
  }
8567
9062
  getSessionPath(sessionId) {
8568
- return join17(this.sessionsDir, `${sessionId}.json`);
9063
+ return join19(this.sessionsDir, `${sessionId}.json`);
8569
9064
  }
8570
9065
  }
8571
9066
  var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
@@ -8576,11 +9071,11 @@ var init_session_manager = __esm(() => {
8576
9071
 
8577
9072
  // src/repl/voice.ts
8578
9073
  import { execSync as execSync11, spawn as spawn6 } from "node:child_process";
8579
- import { existsSync as existsSync18, mkdirSync as mkdirSync13, unlinkSync as unlinkSync4 } from "node:fs";
9074
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
8580
9075
  import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
8581
- import { join as join18 } from "node:path";
9076
+ import { join as join20 } from "node:path";
8582
9077
  function getWhisperModelPath() {
8583
- return join18(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
9078
+ return join20(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
8584
9079
  }
8585
9080
  function commandExists(cmd) {
8586
9081
  try {
@@ -8598,16 +9093,16 @@ function findWhisperBinary() {
8598
9093
  return name;
8599
9094
  }
8600
9095
  for (const name of candidates) {
8601
- const fullPath = join18(LOCUS_BIN_DIR, name);
8602
- if (existsSync18(fullPath))
9096
+ const fullPath = join20(LOCUS_BIN_DIR, name);
9097
+ if (existsSync19(fullPath))
8603
9098
  return fullPath;
8604
9099
  }
8605
9100
  if (platform() === "darwin") {
8606
9101
  const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
8607
9102
  for (const dir of brewDirs) {
8608
9103
  for (const name of candidates) {
8609
- const fullPath = join18(dir, name);
8610
- if (existsSync18(fullPath))
9104
+ const fullPath = join20(dir, name);
9105
+ if (existsSync19(fullPath))
8611
9106
  return fullPath;
8612
9107
  }
8613
9108
  }
@@ -8622,11 +9117,11 @@ function findSoxRecBinary() {
8622
9117
  if (platform() === "darwin") {
8623
9118
  const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
8624
9119
  for (const dir of brewDirs) {
8625
- const recPath = join18(dir, "rec");
8626
- if (existsSync18(recPath))
9120
+ const recPath = join20(dir, "rec");
9121
+ if (existsSync19(recPath))
8627
9122
  return recPath;
8628
- const soxPath = join18(dir, "sox");
8629
- if (existsSync18(soxPath))
9123
+ const soxPath = join20(dir, "sox");
9124
+ if (existsSync19(soxPath))
8630
9125
  return soxPath;
8631
9126
  }
8632
9127
  }
@@ -8635,7 +9130,7 @@ function findSoxRecBinary() {
8635
9130
  function checkDependencies() {
8636
9131
  const soxBinary = findSoxRecBinary();
8637
9132
  const whisperBinary = findWhisperBinary();
8638
- const modelDownloaded = existsSync18(getWhisperModelPath());
9133
+ const modelDownloaded = existsSync19(getWhisperModelPath());
8639
9134
  return {
8640
9135
  sox: soxBinary !== null,
8641
9136
  whisper: whisperBinary !== null,
@@ -8789,19 +9284,19 @@ function ensureBuildDeps(pm) {
8789
9284
  }
8790
9285
  function buildWhisperFromSource(pm) {
8791
9286
  const out = process.stderr;
8792
- const buildDir = join18(tmpdir4(), `locus-whisper-build-${process.pid}`);
9287
+ const buildDir = join20(tmpdir4(), `locus-whisper-build-${process.pid}`);
8793
9288
  if (!ensureBuildDeps(pm)) {
8794
9289
  out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
8795
9290
  `);
8796
9291
  return false;
8797
9292
  }
8798
9293
  try {
8799
- mkdirSync13(buildDir, { recursive: true });
8800
- mkdirSync13(LOCUS_BIN_DIR, { recursive: true });
9294
+ mkdirSync14(buildDir, { recursive: true });
9295
+ mkdirSync14(LOCUS_BIN_DIR, { recursive: true });
8801
9296
  out.write(` ${dim2("Cloning whisper.cpp...")}
8802
9297
  `);
8803
- execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join18(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
8804
- const srcDir = join18(buildDir, "whisper.cpp");
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");
8805
9300
  const numCpus = cpus().length || 2;
8806
9301
  out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
8807
9302
  `);
@@ -8815,13 +9310,13 @@ function buildWhisperFromSource(pm) {
8815
9310
  stdio: ["pipe", "pipe", "pipe"],
8816
9311
  timeout: 600000
8817
9312
  });
8818
- const destPath = join18(LOCUS_BIN_DIR, "whisper-cli");
9313
+ const destPath = join20(LOCUS_BIN_DIR, "whisper-cli");
8819
9314
  const binaryCandidates = [
8820
- join18(srcDir, "build", "bin", "whisper-cli"),
8821
- join18(srcDir, "build", "bin", "main")
9315
+ join20(srcDir, "build", "bin", "whisper-cli"),
9316
+ join20(srcDir, "build", "bin", "main")
8822
9317
  ];
8823
9318
  for (const candidate of binaryCandidates) {
8824
- if (existsSync18(candidate)) {
9319
+ if (existsSync19(candidate)) {
8825
9320
  execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
8826
9321
  stdio: "pipe"
8827
9322
  });
@@ -8900,9 +9395,9 @@ ${bold2("Installing voice dependencies...")}
8900
9395
  }
8901
9396
  function downloadModel() {
8902
9397
  const modelPath = getWhisperModelPath();
8903
- if (existsSync18(modelPath))
9398
+ if (existsSync19(modelPath))
8904
9399
  return true;
8905
- mkdirSync13(WHISPER_MODELS_DIR, { recursive: true });
9400
+ mkdirSync14(WHISPER_MODELS_DIR, { recursive: true });
8906
9401
  const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
8907
9402
  process.stderr.write(`${dim2("Downloading whisper model")} ${bold2(WHISPER_MODEL)} ${dim2("(~150MB)...")}
8908
9403
  `);
@@ -8947,7 +9442,7 @@ class VoiceController {
8947
9442
  onStateChange;
8948
9443
  constructor(options) {
8949
9444
  this.onStateChange = options.onStateChange;
8950
- this.tempFile = join18(tmpdir4(), `locus-voice-${process.pid}.wav`);
9445
+ this.tempFile = join20(tmpdir4(), `locus-voice-${process.pid}.wav`);
8951
9446
  this.deps = checkDependencies();
8952
9447
  }
8953
9448
  getState() {
@@ -9015,7 +9510,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
9015
9510
  this.recordProcess = null;
9016
9511
  this.setState("idle");
9017
9512
  await sleep2(200);
9018
- if (!existsSync18(this.tempFile)) {
9513
+ if (!existsSync19(this.tempFile)) {
9019
9514
  return null;
9020
9515
  }
9021
9516
  try {
@@ -9106,8 +9601,8 @@ function sleep2(ms) {
9106
9601
  var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
9107
9602
  var init_voice = __esm(() => {
9108
9603
  init_terminal();
9109
- WHISPER_MODELS_DIR = join18(homedir4(), ".locus", "whisper-models");
9110
- LOCUS_BIN_DIR = join18(homedir4(), ".locus", "bin");
9604
+ WHISPER_MODELS_DIR = join20(homedir4(), ".locus", "whisper-models");
9605
+ LOCUS_BIN_DIR = join20(homedir4(), ".locus", "bin");
9111
9606
  });
9112
9607
 
9113
9608
  // src/repl/repl.ts
@@ -9619,8 +10114,8 @@ var init_exec = __esm(() => {
9619
10114
 
9620
10115
  // src/core/submodule.ts
9621
10116
  import { execSync as execSync13 } from "node:child_process";
9622
- import { existsSync as existsSync19 } from "node:fs";
9623
- import { join as join19 } from "node:path";
10117
+ import { existsSync as existsSync20 } from "node:fs";
10118
+ import { join as join21 } from "node:path";
9624
10119
  function git2(args, cwd) {
9625
10120
  return execSync13(`git ${args}`, {
9626
10121
  cwd,
@@ -9636,7 +10131,7 @@ function gitSafe(args, cwd) {
9636
10131
  }
9637
10132
  }
9638
10133
  function hasSubmodules(cwd) {
9639
- return existsSync19(join19(cwd, ".gitmodules"));
10134
+ return existsSync20(join21(cwd, ".gitmodules"));
9640
10135
  }
9641
10136
  function listSubmodules(cwd) {
9642
10137
  if (!hasSubmodules(cwd))
@@ -9656,7 +10151,7 @@ function listSubmodules(cwd) {
9656
10151
  continue;
9657
10152
  submodules.push({
9658
10153
  path,
9659
- absolutePath: join19(cwd, path),
10154
+ absolutePath: join21(cwd, path),
9660
10155
  dirty
9661
10156
  });
9662
10157
  }
@@ -9669,7 +10164,7 @@ function getDirtySubmodules(cwd) {
9669
10164
  const submodules = listSubmodules(cwd);
9670
10165
  const dirty = [];
9671
10166
  for (const sub of submodules) {
9672
- if (!existsSync19(sub.absolutePath))
10167
+ if (!existsSync20(sub.absolutePath))
9673
10168
  continue;
9674
10169
  const status = gitSafe("status --porcelain", sub.absolutePath);
9675
10170
  if (status && status.trim().length > 0) {
@@ -9756,7 +10251,7 @@ function pushSubmoduleBranches(cwd) {
9756
10251
  const log = getLogger();
9757
10252
  const submodules = listSubmodules(cwd);
9758
10253
  for (const sub of submodules) {
9759
- if (!existsSync19(sub.absolutePath))
10254
+ if (!existsSync20(sub.absolutePath))
9760
10255
  continue;
9761
10256
  const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
9762
10257
  if (!branch || branch === "HEAD")
@@ -10005,9 +10500,32 @@ ${submoduleSummary}`;
10005
10500
 
10006
10501
  \uD83E\uDD16 Automated by [Locus](https://github.com/asgarovf/locusai)`;
10007
10502
  const prTitle = `${issue.title} (#${issue.number})`;
10008
- const prNumber = createPR(prTitle, prBody, currentBranch, config.agent.baseBranch, { cwd: projectRoot });
10009
- process.stderr.write(` ${green("✓")} Created PR #${prNumber}
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}
10010
10527
  `);
10528
+ }
10011
10529
  return prNumber;
10012
10530
  } catch (e) {
10013
10531
  getLogger().warn(`Failed to create PR: ${e}`);
@@ -10173,15 +10691,15 @@ var init_conflict = __esm(() => {
10173
10691
 
10174
10692
  // src/core/run-state.ts
10175
10693
  import {
10176
- existsSync as existsSync20,
10177
- mkdirSync as mkdirSync14,
10178
- readFileSync as readFileSync12,
10694
+ existsSync as existsSync21,
10695
+ mkdirSync as mkdirSync15,
10696
+ readFileSync as readFileSync13,
10179
10697
  unlinkSync as unlinkSync5,
10180
- writeFileSync as writeFileSync9
10698
+ writeFileSync as writeFileSync11
10181
10699
  } from "node:fs";
10182
- import { dirname as dirname6, join as join20 } from "node:path";
10700
+ import { dirname as dirname6, join as join22 } from "node:path";
10183
10701
  function getRunStateDir(projectRoot) {
10184
- return join20(projectRoot, ".locus", "run-state");
10702
+ return join22(projectRoot, ".locus", "run-state");
10185
10703
  }
10186
10704
  function sprintSlug(name) {
10187
10705
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -10189,16 +10707,16 @@ function sprintSlug(name) {
10189
10707
  function getRunStatePath(projectRoot, sprintName) {
10190
10708
  const dir = getRunStateDir(projectRoot);
10191
10709
  if (sprintName) {
10192
- return join20(dir, `${sprintSlug(sprintName)}.json`);
10710
+ return join22(dir, `${sprintSlug(sprintName)}.json`);
10193
10711
  }
10194
- return join20(dir, "_parallel.json");
10712
+ return join22(dir, "_parallel.json");
10195
10713
  }
10196
10714
  function loadRunState(projectRoot, sprintName) {
10197
10715
  const path = getRunStatePath(projectRoot, sprintName);
10198
- if (!existsSync20(path))
10716
+ if (!existsSync21(path))
10199
10717
  return null;
10200
10718
  try {
10201
- return JSON.parse(readFileSync12(path, "utf-8"));
10719
+ return JSON.parse(readFileSync13(path, "utf-8"));
10202
10720
  } catch {
10203
10721
  getLogger().warn("Corrupted run state file, ignoring");
10204
10722
  return null;
@@ -10207,15 +10725,15 @@ function loadRunState(projectRoot, sprintName) {
10207
10725
  function saveRunState(projectRoot, state) {
10208
10726
  const path = getRunStatePath(projectRoot, state.sprint);
10209
10727
  const dir = dirname6(path);
10210
- if (!existsSync20(dir)) {
10211
- mkdirSync14(dir, { recursive: true });
10728
+ if (!existsSync21(dir)) {
10729
+ mkdirSync15(dir, { recursive: true });
10212
10730
  }
10213
- writeFileSync9(path, `${JSON.stringify(state, null, 2)}
10731
+ writeFileSync11(path, `${JSON.stringify(state, null, 2)}
10214
10732
  `, "utf-8");
10215
10733
  }
10216
10734
  function clearRunState(projectRoot, sprintName) {
10217
10735
  const path = getRunStatePath(projectRoot, sprintName);
10218
- if (existsSync20(path)) {
10736
+ if (existsSync21(path)) {
10219
10737
  unlinkSync5(path);
10220
10738
  }
10221
10739
  }
@@ -10371,8 +10889,27 @@ __export(exports_worktree, {
10371
10889
  cleanupStaleWorktrees: () => cleanupStaleWorktrees
10372
10890
  });
10373
10891
  import { execSync as execSync16 } from "node:child_process";
10374
- import { existsSync as existsSync21, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
10375
- import { join as join21 } from "node:path";
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
+ }
10376
10913
  function git4(args, cwd) {
10377
10914
  return execSync16(`git ${args}`, {
10378
10915
  cwd,
@@ -10388,13 +10925,13 @@ function gitSafe3(args, cwd) {
10388
10925
  }
10389
10926
  }
10390
10927
  function getWorktreeDir(projectRoot) {
10391
- return join21(projectRoot, ".locus", "worktrees");
10928
+ return join23(projectRoot, ".locus", "worktrees");
10392
10929
  }
10393
10930
  function getWorktreePath(projectRoot, issueNumber) {
10394
- return join21(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
10931
+ return join23(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
10395
10932
  }
10396
10933
  function getSprintWorktreePath(projectRoot, sprintSlug2) {
10397
- return join21(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
10934
+ return join23(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
10398
10935
  }
10399
10936
  function generateBranchName(issueNumber) {
10400
10937
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -10407,7 +10944,7 @@ function createSprintWorktree(projectRoot, sprintName, baseBranch) {
10407
10944
  const log = getLogger();
10408
10945
  const slug = sprintSlug2(sprintName);
10409
10946
  const worktreePath = getSprintWorktreePath(projectRoot, slug);
10410
- if (existsSync21(worktreePath)) {
10947
+ if (existsSync22(worktreePath)) {
10411
10948
  log.verbose(`Sprint worktree already exists for "${sprintName}"`);
10412
10949
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/sprint-${slug}`;
10413
10950
  return { path: worktreePath, branch: existingBranch };
@@ -10416,6 +10953,7 @@ function createSprintWorktree(projectRoot, sprintName, baseBranch) {
10416
10953
  const branch = `locus/sprint-${slug}-${randomSuffix}`;
10417
10954
  git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
10418
10955
  initSubmodules(worktreePath);
10956
+ copyLocusDir(projectRoot, worktreePath);
10419
10957
  log.info(`Created sprint worktree for "${sprintName}"`, {
10420
10958
  path: worktreePath,
10421
10959
  branch,
@@ -10427,7 +10965,7 @@ function removeSprintWorktree(projectRoot, sprintName) {
10427
10965
  const log = getLogger();
10428
10966
  const slug = sprintSlug2(sprintName);
10429
10967
  const worktreePath = getSprintWorktreePath(projectRoot, slug);
10430
- if (!existsSync21(worktreePath)) {
10968
+ if (!existsSync22(worktreePath)) {
10431
10969
  log.verbose(`Sprint worktree for "${sprintName}" does not exist`);
10432
10970
  return;
10433
10971
  }
@@ -10457,7 +10995,7 @@ function getWorktreeBranch(worktreePath) {
10457
10995
  function createWorktree(projectRoot, issueNumber, baseBranch) {
10458
10996
  const log = getLogger();
10459
10997
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
10460
- if (existsSync21(worktreePath)) {
10998
+ if (existsSync22(worktreePath)) {
10461
10999
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
10462
11000
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
10463
11001
  return {
@@ -10470,6 +11008,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
10470
11008
  const branch = generateBranchName(issueNumber);
10471
11009
  git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
10472
11010
  initSubmodules(worktreePath);
11011
+ copyLocusDir(projectRoot, worktreePath);
10473
11012
  log.info(`Created worktree for issue #${issueNumber}`, {
10474
11013
  path: worktreePath,
10475
11014
  branch,
@@ -10485,7 +11024,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
10485
11024
  function removeWorktree(projectRoot, issueNumber) {
10486
11025
  const log = getLogger();
10487
11026
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
10488
- if (!existsSync21(worktreePath)) {
11027
+ if (!existsSync22(worktreePath)) {
10489
11028
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
10490
11029
  return;
10491
11030
  }
@@ -10504,7 +11043,7 @@ function removeWorktree(projectRoot, issueNumber) {
10504
11043
  function listWorktrees(projectRoot) {
10505
11044
  const log = getLogger();
10506
11045
  const worktreeDir = getWorktreeDir(projectRoot);
10507
- if (!existsSync21(worktreeDir)) {
11046
+ if (!existsSync22(worktreeDir)) {
10508
11047
  return [];
10509
11048
  }
10510
11049
  const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
@@ -10524,7 +11063,7 @@ function listWorktrees(projectRoot) {
10524
11063
  if (!match)
10525
11064
  continue;
10526
11065
  const issueNumber = Number.parseInt(match[1], 10);
10527
- const path = join21(worktreeDir, entry);
11066
+ const path = join23(worktreeDir, entry);
10528
11067
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
10529
11068
  let resolvedPath;
10530
11069
  try {
@@ -10564,7 +11103,7 @@ function cleanupStaleWorktrees(projectRoot) {
10564
11103
  }
10565
11104
  function pushWorktreeBranch(projectRoot, issueNumber) {
10566
11105
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
10567
- if (!existsSync21(worktreePath)) {
11106
+ if (!existsSync22(worktreePath)) {
10568
11107
  throw new Error(`Worktree for issue #${issueNumber} does not exist`);
10569
11108
  }
10570
11109
  const branch = getWorktreeBranch(worktreePath);
@@ -10576,14 +11115,14 @@ function pushWorktreeBranch(projectRoot, issueNumber) {
10576
11115
  }
10577
11116
  function hasWorktreeChanges(projectRoot, issueNumber) {
10578
11117
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
10579
- if (!existsSync21(worktreePath))
11118
+ if (!existsSync22(worktreePath))
10580
11119
  return false;
10581
11120
  const status = gitSafe3("status --porcelain", worktreePath);
10582
11121
  return status !== null && status.trim().length > 0;
10583
11122
  }
10584
11123
  function getWorktreeAge(projectRoot, issueNumber) {
10585
11124
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
10586
- if (!existsSync21(worktreePath))
11125
+ if (!existsSync22(worktreePath))
10587
11126
  return 0;
10588
11127
  try {
10589
11128
  const stat = statSync4(worktreePath);
@@ -10603,8 +11142,8 @@ __export(exports_run, {
10603
11142
  runCommand: () => runCommand
10604
11143
  });
10605
11144
  import { execSync as execSync17 } from "node:child_process";
10606
- import { existsSync as existsSync22 } from "node:fs";
10607
- import { join as join22 } from "node:path";
11145
+ import { existsSync as existsSync23 } from "node:fs";
11146
+ import { join as join24 } from "node:path";
10608
11147
  function resolveExecutionContext(config, modelOverride) {
10609
11148
  const model = modelOverride ?? config.ai.model;
10610
11149
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -11148,8 +11687,8 @@ async function handleResume(projectRoot, config, sandboxed, flags) {
11148
11687
  const sprintsToResume = [];
11149
11688
  try {
11150
11689
  const { readdirSync: readdirSync8 } = await import("node:fs");
11151
- const runStateDir = join22(projectRoot, ".locus", "run-state");
11152
- if (existsSync22(runStateDir)) {
11690
+ const runStateDir = join24(projectRoot, ".locus", "run-state");
11691
+ if (existsSync23(runStateDir)) {
11153
11692
  const files = readdirSync8(runStateDir).filter((f) => f.endsWith(".json"));
11154
11693
  for (const file of files) {
11155
11694
  const sprintName = file === "_parallel.json" ? undefined : file.replace(/\.json$/, "");
@@ -11193,9 +11732,9 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}${state.sprint ? ` ($
11193
11732
  let workDir = projectRoot;
11194
11733
  if (state.type === "sprint" && state.sprint) {
11195
11734
  const { getSprintWorktreePath: getSprintWorktreePath2, sprintSlug: sprintSlug3 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
11196
- const { existsSync: existsSync23 } = await import("node:fs");
11735
+ const { existsSync: existsSync24 } = await import("node:fs");
11197
11736
  const wtPath = getSprintWorktreePath2(projectRoot, sprintSlug3(state.sprint));
11198
- if (existsSync23(wtPath)) {
11737
+ if (existsSync24(wtPath)) {
11199
11738
  workDir = wtPath;
11200
11739
  } else if (state.branch) {
11201
11740
  try {
@@ -11365,9 +11904,35 @@ ${submoduleSummary}`;
11365
11904
  ---
11366
11905
 
11367
11906
  \uD83E\uDD16 Automated by [Locus](https://github.com/asgarovf/locusai)`;
11368
- const prNumber = createPR(`Sprint: ${sprintName}`, prBody, branchName, config.agent.baseBranch, { cwd: workDir });
11369
- process.stderr.write(` ${green("✓")} Created sprint PR #${prNumber}
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}
11370
11929
  `);
11930
+ }
11931
+ } else {
11932
+ prNumber = createPR(prTitle, prBody, branchName, config.agent.baseBranch, { cwd: workDir });
11933
+ process.stderr.write(` ${green("✓")} Created sprint PR #${prNumber}
11934
+ `);
11935
+ }
11371
11936
  return prNumber;
11372
11937
  } catch (e) {
11373
11938
  getLogger().warn(`Failed to create sprint PR: ${e}`);
@@ -11399,8 +11964,8 @@ __export(exports_status, {
11399
11964
  statusCommand: () => statusCommand
11400
11965
  });
11401
11966
  import { execSync as execSync18 } from "node:child_process";
11402
- import { existsSync as existsSync23 } from "node:fs";
11403
- import { dirname as dirname7, join as join23 } from "node:path";
11967
+ import { existsSync as existsSync24 } from "node:fs";
11968
+ import { dirname as dirname7, join as join25 } from "node:path";
11404
11969
  async function statusCommand(projectRoot) {
11405
11970
  const config = loadConfig(projectRoot);
11406
11971
  const spinner = new Spinner;
@@ -11532,13 +12097,13 @@ ${drawBox(lines, { title: "Locus Status" })}
11532
12097
  `);
11533
12098
  }
11534
12099
  function getPm2Bin() {
11535
- const pkgsBin = join23(getPackagesDir(), "node_modules", ".bin", "pm2");
11536
- if (existsSync23(pkgsBin))
12100
+ const pkgsBin = join25(getPackagesDir(), "node_modules", ".bin", "pm2");
12101
+ if (existsSync24(pkgsBin))
11537
12102
  return pkgsBin;
11538
12103
  let dir = process.cwd();
11539
12104
  while (dir !== dirname7(dir)) {
11540
- const candidate = join23(dir, "node_modules", ".bin", "pm2");
11541
- if (existsSync23(candidate))
12105
+ const candidate = join25(dir, "node_modules", ".bin", "pm2");
12106
+ if (existsSync24(candidate))
11542
12107
  return candidate;
11543
12108
  dir = dirname7(dir);
11544
12109
  }
@@ -11734,13 +12299,13 @@ __export(exports_plan, {
11734
12299
  parsePlanArgs: () => parsePlanArgs
11735
12300
  });
11736
12301
  import {
11737
- existsSync as existsSync24,
11738
- mkdirSync as mkdirSync15,
12302
+ existsSync as existsSync25,
12303
+ mkdirSync as mkdirSync17,
11739
12304
  readdirSync as readdirSync8,
11740
- readFileSync as readFileSync13,
11741
- writeFileSync as writeFileSync10
12305
+ readFileSync as readFileSync14,
12306
+ writeFileSync as writeFileSync12
11742
12307
  } from "node:fs";
11743
- import { join as join24 } from "node:path";
12308
+ import { join as join26 } from "node:path";
11744
12309
  function printHelp2() {
11745
12310
  process.stderr.write(`
11746
12311
  ${bold2("locus plan")} — AI-powered sprint planning
@@ -11772,12 +12337,12 @@ function normalizeSprintName(name) {
11772
12337
  return name.trim().toLowerCase();
11773
12338
  }
11774
12339
  function getPlansDir(projectRoot) {
11775
- return join24(projectRoot, ".locus", "plans");
12340
+ return join26(projectRoot, ".locus", "plans");
11776
12341
  }
11777
12342
  function ensurePlansDir(projectRoot) {
11778
12343
  const dir = getPlansDir(projectRoot);
11779
- if (!existsSync24(dir)) {
11780
- mkdirSync15(dir, { recursive: true });
12344
+ if (!existsSync25(dir)) {
12345
+ mkdirSync17(dir, { recursive: true });
11781
12346
  }
11782
12347
  return dir;
11783
12348
  }
@@ -11786,14 +12351,14 @@ function generateId() {
11786
12351
  }
11787
12352
  function loadPlanFile(projectRoot, id) {
11788
12353
  const dir = getPlansDir(projectRoot);
11789
- if (!existsSync24(dir))
12354
+ if (!existsSync25(dir))
11790
12355
  return null;
11791
12356
  const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
11792
12357
  const match = files.find((f) => f.startsWith(id));
11793
12358
  if (!match)
11794
12359
  return null;
11795
12360
  try {
11796
- const content = readFileSync13(join24(dir, match), "utf-8");
12361
+ const content = readFileSync14(join26(dir, match), "utf-8");
11797
12362
  return JSON.parse(content);
11798
12363
  } catch {
11799
12364
  return null;
@@ -11860,7 +12425,7 @@ async function planCommand(projectRoot, args, flags = {}) {
11860
12425
  }
11861
12426
  function handleListPlans(projectRoot) {
11862
12427
  const dir = getPlansDir(projectRoot);
11863
- if (!existsSync24(dir)) {
12428
+ if (!existsSync25(dir)) {
11864
12429
  process.stderr.write(`${dim2("No saved plans yet.")}
11865
12430
  `);
11866
12431
  return;
@@ -11878,7 +12443,7 @@ ${bold2("Saved Plans:")}
11878
12443
  for (const file of files) {
11879
12444
  const id = file.replace(".json", "");
11880
12445
  try {
11881
- const content = readFileSync13(join24(dir, file), "utf-8");
12446
+ const content = readFileSync14(join26(dir, file), "utf-8");
11882
12447
  const plan = JSON.parse(content);
11883
12448
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
11884
12449
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -11946,7 +12511,7 @@ async function handleRefinePlan(projectRoot, id, feedback, flags) {
11946
12511
  return;
11947
12512
  }
11948
12513
  const config = loadConfig(projectRoot);
11949
- const planPath = join24(getPlansDir(projectRoot), `${plan.id}.json`);
12514
+ const planPath = join26(getPlansDir(projectRoot), `${plan.id}.json`);
11950
12515
  const planPathRelative = `.locus/plans/${plan.id}.json`;
11951
12516
  process.stderr.write(`
11952
12517
  ${bold2("Refining plan:")} ${cyan2(plan.directive)}
@@ -11985,7 +12550,7 @@ ${red2("✗")} Refinement failed: ${aiResult.error}
11985
12550
  `);
11986
12551
  return;
11987
12552
  }
11988
- if (!existsSync24(planPath)) {
12553
+ if (!existsSync25(planPath)) {
11989
12554
  process.stderr.write(`
11990
12555
  ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
11991
12556
  `);
@@ -11993,7 +12558,7 @@ ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
11993
12558
  }
11994
12559
  let updatedPlan;
11995
12560
  try {
11996
- const content = readFileSync13(planPath, "utf-8");
12561
+ const content = readFileSync14(planPath, "utf-8");
11997
12562
  updatedPlan = JSON.parse(content);
11998
12563
  } catch {
11999
12564
  process.stderr.write(`
@@ -12011,7 +12576,7 @@ ${yellow2("⚠")} Refined plan has no issues.
12011
12576
  updatedPlan.directive = plan.directive;
12012
12577
  updatedPlan.sprint = updatedPlan.sprint ?? plan.sprint;
12013
12578
  updatedPlan.createdAt = plan.createdAt;
12014
- writeFileSync10(planPath, JSON.stringify(updatedPlan, null, 2), "utf-8");
12579
+ writeFileSync12(planPath, JSON.stringify(updatedPlan, null, 2), "utf-8");
12015
12580
  process.stderr.write(`
12016
12581
  ${bold2("Plan refined:")} ${cyan2(plan.id)}
12017
12582
 
@@ -12083,7 +12648,7 @@ ${bold2("Approving plan:")}
12083
12648
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
12084
12649
  const id = generateId();
12085
12650
  const plansDir = ensurePlansDir(projectRoot);
12086
- const planPath = join24(plansDir, `${id}.json`);
12651
+ const planPath = join26(plansDir, `${id}.json`);
12087
12652
  const planPathRelative = `.locus/plans/${id}.json`;
12088
12653
  const displayDirective = directive;
12089
12654
  process.stderr.write(`
@@ -12126,7 +12691,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
12126
12691
  `);
12127
12692
  return;
12128
12693
  }
12129
- if (!existsSync24(planPath)) {
12694
+ if (!existsSync25(planPath)) {
12130
12695
  process.stderr.write(`
12131
12696
  ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
12132
12697
  `);
@@ -12136,7 +12701,7 @@ ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
12136
12701
  }
12137
12702
  let plan;
12138
12703
  try {
12139
- const content = readFileSync13(planPath, "utf-8");
12704
+ const content = readFileSync14(planPath, "utf-8");
12140
12705
  plan = JSON.parse(content);
12141
12706
  } catch {
12142
12707
  process.stderr.write(`
@@ -12158,7 +12723,7 @@ ${yellow2("⚠")} Plan file has no issues.
12158
12723
  plan.sprint = sprintName;
12159
12724
  if (!plan.createdAt)
12160
12725
  plan.createdAt = new Date().toISOString();
12161
- writeFileSync10(planPath, JSON.stringify(plan, null, 2), "utf-8");
12726
+ writeFileSync12(planPath, JSON.stringify(plan, null, 2), "utf-8");
12162
12727
  process.stderr.write(`
12163
12728
  ${bold2("Plan saved:")} ${cyan2(id)}
12164
12729
 
@@ -12308,16 +12873,16 @@ ${directive}${sprintName ? `
12308
12873
 
12309
12874
  **Sprint:** ${sprintName}` : ""}
12310
12875
  </directive>`);
12311
- const locusPath = join24(projectRoot, ".locus", "LOCUS.md");
12312
- if (existsSync24(locusPath)) {
12313
- const content = readFileSync13(locusPath, "utf-8");
12876
+ const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
12877
+ if (existsSync25(locusPath)) {
12878
+ const content = readFileSync14(locusPath, "utf-8");
12314
12879
  parts.push(`<project-context>
12315
12880
  ${content.slice(0, 3000)}
12316
12881
  </project-context>`);
12317
12882
  }
12318
- const learningsPath = join24(projectRoot, ".locus", "LEARNINGS.md");
12319
- if (existsSync24(learningsPath)) {
12320
- const content = readFileSync13(learningsPath, "utf-8");
12883
+ const learningsPath = join26(projectRoot, ".locus", "LEARNINGS.md");
12884
+ if (existsSync25(learningsPath)) {
12885
+ const content = readFileSync14(learningsPath, "utf-8");
12321
12886
  parts.push(`<past-learnings>
12322
12887
  ${content.slice(0, 2000)}
12323
12888
  </past-learnings>`);
@@ -12369,9 +12934,9 @@ ${JSON.stringify(plan, null, 2)}
12369
12934
  parts.push(`<feedback>
12370
12935
  ${feedback}
12371
12936
  </feedback>`);
12372
- const locusPath = join24(projectRoot, ".locus", "LOCUS.md");
12373
- if (existsSync24(locusPath)) {
12374
- const content = readFileSync13(locusPath, "utf-8");
12937
+ const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
12938
+ if (existsSync25(locusPath)) {
12939
+ const content = readFileSync14(locusPath, "utf-8");
12375
12940
  parts.push(`<project-context>
12376
12941
  ${content.slice(0, 3000)}
12377
12942
  </project-context>`);
@@ -12553,8 +13118,8 @@ __export(exports_review, {
12553
13118
  reviewCommand: () => reviewCommand
12554
13119
  });
12555
13120
  import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
12556
- import { existsSync as existsSync25, readFileSync as readFileSync14 } from "node:fs";
12557
- import { join as join25 } from "node:path";
13121
+ import { existsSync as existsSync26, readFileSync as readFileSync15 } from "node:fs";
13122
+ import { join as join27 } from "node:path";
12558
13123
  function printHelp3() {
12559
13124
  process.stderr.write(`
12560
13125
  ${bold2("locus review")} — AI-powered code review
@@ -12723,9 +13288,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
12723
13288
  parts.push(`<role>
12724
13289
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
12725
13290
  </role>`);
12726
- const locusPath = join25(projectRoot, ".locus", "LOCUS.md");
12727
- if (existsSync25(locusPath)) {
12728
- const content = readFileSync14(locusPath, "utf-8");
13291
+ const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
13292
+ if (existsSync26(locusPath)) {
13293
+ const content = readFileSync15(locusPath, "utf-8");
12729
13294
  parts.push(`<project-context>
12730
13295
  ${content.slice(0, 2000)}
12731
13296
  </project-context>`);
@@ -13040,14 +13605,14 @@ __export(exports_discuss, {
13040
13605
  discussCommand: () => discussCommand
13041
13606
  });
13042
13607
  import {
13043
- existsSync as existsSync26,
13044
- mkdirSync as mkdirSync16,
13608
+ existsSync as existsSync27,
13609
+ mkdirSync as mkdirSync18,
13045
13610
  readdirSync as readdirSync9,
13046
- readFileSync as readFileSync15,
13611
+ readFileSync as readFileSync16,
13047
13612
  unlinkSync as unlinkSync6,
13048
- writeFileSync as writeFileSync11
13613
+ writeFileSync as writeFileSync13
13049
13614
  } from "node:fs";
13050
- import { join as join26 } from "node:path";
13615
+ import { join as join28 } from "node:path";
13051
13616
  function printHelp5() {
13052
13617
  process.stderr.write(`
13053
13618
  ${bold2("locus discuss")} — AI-powered architectural discussions
@@ -13069,12 +13634,12 @@ ${bold2("Examples:")}
13069
13634
  `);
13070
13635
  }
13071
13636
  function getDiscussionsDir(projectRoot) {
13072
- return join26(projectRoot, ".locus", "discussions");
13637
+ return join28(projectRoot, ".locus", "discussions");
13073
13638
  }
13074
13639
  function ensureDiscussionsDir(projectRoot) {
13075
13640
  const dir = getDiscussionsDir(projectRoot);
13076
- if (!existsSync26(dir)) {
13077
- mkdirSync16(dir, { recursive: true });
13641
+ if (!existsSync27(dir)) {
13642
+ mkdirSync18(dir, { recursive: true });
13078
13643
  }
13079
13644
  return dir;
13080
13645
  }
@@ -13108,7 +13673,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
13108
13673
  }
13109
13674
  function listDiscussions(projectRoot) {
13110
13675
  const dir = getDiscussionsDir(projectRoot);
13111
- if (!existsSync26(dir)) {
13676
+ if (!existsSync27(dir)) {
13112
13677
  process.stderr.write(`${dim2("No discussions yet.")}
13113
13678
  `);
13114
13679
  return;
@@ -13125,7 +13690,7 @@ ${bold2("Discussions:")}
13125
13690
  `);
13126
13691
  for (const file of files) {
13127
13692
  const id = file.replace(".md", "");
13128
- const content = readFileSync15(join26(dir, file), "utf-8");
13693
+ const content = readFileSync16(join28(dir, file), "utf-8");
13129
13694
  const titleMatch = content.match(/^#\s+(.+)/m);
13130
13695
  const title = titleMatch ? titleMatch[1] : id;
13131
13696
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -13143,7 +13708,7 @@ function showDiscussion(projectRoot, id) {
13143
13708
  return;
13144
13709
  }
13145
13710
  const dir = getDiscussionsDir(projectRoot);
13146
- if (!existsSync26(dir)) {
13711
+ if (!existsSync27(dir)) {
13147
13712
  process.stderr.write(`${red2("✗")} No discussions found.
13148
13713
  `);
13149
13714
  return;
@@ -13155,7 +13720,7 @@ function showDiscussion(projectRoot, id) {
13155
13720
  `);
13156
13721
  return;
13157
13722
  }
13158
- const content = readFileSync15(join26(dir, match), "utf-8");
13723
+ const content = readFileSync16(join28(dir, match), "utf-8");
13159
13724
  process.stdout.write(`${content}
13160
13725
  `);
13161
13726
  }
@@ -13166,7 +13731,7 @@ function deleteDiscussion(projectRoot, id) {
13166
13731
  return;
13167
13732
  }
13168
13733
  const dir = getDiscussionsDir(projectRoot);
13169
- if (!existsSync26(dir)) {
13734
+ if (!existsSync27(dir)) {
13170
13735
  process.stderr.write(`${red2("✗")} No discussions found.
13171
13736
  `);
13172
13737
  return;
@@ -13178,7 +13743,7 @@ function deleteDiscussion(projectRoot, id) {
13178
13743
  `);
13179
13744
  return;
13180
13745
  }
13181
- unlinkSync6(join26(dir, match));
13746
+ unlinkSync6(join28(dir, match));
13182
13747
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
13183
13748
  `);
13184
13749
  }
@@ -13191,7 +13756,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
13191
13756
  return;
13192
13757
  }
13193
13758
  const dir = getDiscussionsDir(projectRoot);
13194
- if (!existsSync26(dir)) {
13759
+ if (!existsSync27(dir)) {
13195
13760
  process.stderr.write(`${red2("✗")} No discussions found.
13196
13761
  `);
13197
13762
  return;
@@ -13203,7 +13768,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
13203
13768
  `);
13204
13769
  return;
13205
13770
  }
13206
- const content = readFileSync15(join26(dir, match), "utf-8");
13771
+ const content = readFileSync16(join28(dir, match), "utf-8");
13207
13772
  const titleMatch = content.match(/^#\s+(.+)/m);
13208
13773
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
13209
13774
  await planCommand(projectRoot, [
@@ -13329,7 +13894,7 @@ ${turn.content}`;
13329
13894
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
13330
13895
  ].join(`
13331
13896
  `);
13332
- writeFileSync11(join26(dir, `${id}.md`), markdown, "utf-8");
13897
+ writeFileSync13(join28(dir, `${id}.md`), markdown, "utf-8");
13333
13898
  process.stderr.write(`
13334
13899
  ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
13335
13900
  `);
@@ -13344,16 +13909,16 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
13344
13909
  parts.push(`<role>
13345
13910
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
13346
13911
  </role>`);
13347
- const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
13348
- if (existsSync26(locusPath)) {
13349
- const content = readFileSync15(locusPath, "utf-8");
13912
+ const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
13913
+ if (existsSync27(locusPath)) {
13914
+ const content = readFileSync16(locusPath, "utf-8");
13350
13915
  parts.push(`<project-context>
13351
13916
  ${content.slice(0, 3000)}
13352
13917
  </project-context>`);
13353
13918
  }
13354
- const learningsPath = join26(projectRoot, ".locus", "LEARNINGS.md");
13355
- if (existsSync26(learningsPath)) {
13356
- const content = readFileSync15(learningsPath, "utf-8");
13919
+ const learningsPath = join28(projectRoot, ".locus", "LEARNINGS.md");
13920
+ if (existsSync27(learningsPath)) {
13921
+ const content = readFileSync16(learningsPath, "utf-8");
13357
13922
  parts.push(`<past-learnings>
13358
13923
  ${content.slice(0, 2000)}
13359
13924
  </past-learnings>`);
@@ -13424,8 +13989,8 @@ __export(exports_artifacts, {
13424
13989
  formatDate: () => formatDate2,
13425
13990
  artifactsCommand: () => artifactsCommand
13426
13991
  });
13427
- import { existsSync as existsSync27, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
13428
- import { join as join27 } from "node:path";
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";
13429
13994
  function printHelp6() {
13430
13995
  process.stderr.write(`
13431
13996
  ${bold2("locus artifacts")} — View and manage AI-generated artifacts
@@ -13445,14 +14010,14 @@ ${dim2("Artifact names support partial matching.")}
13445
14010
  `);
13446
14011
  }
13447
14012
  function getArtifactsDir(projectRoot) {
13448
- return join27(projectRoot, ".locus", "artifacts");
14013
+ return join29(projectRoot, ".locus", "artifacts");
13449
14014
  }
13450
14015
  function listArtifacts(projectRoot) {
13451
14016
  const dir = getArtifactsDir(projectRoot);
13452
- if (!existsSync27(dir))
14017
+ if (!existsSync28(dir))
13453
14018
  return [];
13454
14019
  return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
13455
- const filePath = join27(dir, fileName);
14020
+ const filePath = join29(dir, fileName);
13456
14021
  const stat = statSync5(filePath);
13457
14022
  return {
13458
14023
  name: fileName.replace(/\.md$/, ""),
@@ -13465,12 +14030,12 @@ function listArtifacts(projectRoot) {
13465
14030
  function readArtifact(projectRoot, name) {
13466
14031
  const dir = getArtifactsDir(projectRoot);
13467
14032
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
13468
- const filePath = join27(dir, fileName);
13469
- if (!existsSync27(filePath))
14033
+ const filePath = join29(dir, fileName);
14034
+ if (!existsSync28(filePath))
13470
14035
  return null;
13471
14036
  const stat = statSync5(filePath);
13472
14037
  return {
13473
- content: readFileSync16(filePath, "utf-8"),
14038
+ content: readFileSync17(filePath, "utf-8"),
13474
14039
  info: {
13475
14040
  name: fileName.replace(/\.md$/, ""),
13476
14041
  fileName,
@@ -13826,9 +14391,9 @@ __export(exports_sandbox2, {
13826
14391
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
13827
14392
  });
13828
14393
  import { execSync as execSync22, spawn as spawn7 } from "node:child_process";
13829
- import { createHash } from "node:crypto";
13830
- import { existsSync as existsSync28, readFileSync as readFileSync17 } from "node:fs";
13831
- import { basename as basename4, join as join28 } from "node:path";
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";
13832
14397
  import { createInterface as createInterface3 } from "node:readline";
13833
14398
  function printSandboxHelp() {
13834
14399
  process.stderr.write(`
@@ -14358,7 +14923,7 @@ async function handleLogs(projectRoot, args) {
14358
14923
  }
14359
14924
  function detectPackageManager2(projectRoot) {
14360
14925
  try {
14361
- const raw = readFileSync17(join28(projectRoot, "package.json"), "utf-8");
14926
+ const raw = readFileSync18(join30(projectRoot, "package.json"), "utf-8");
14362
14927
  const pkgJson = JSON.parse(raw);
14363
14928
  if (typeof pkgJson.packageManager === "string") {
14364
14929
  const name = pkgJson.packageManager.split("@")[0];
@@ -14367,13 +14932,13 @@ function detectPackageManager2(projectRoot) {
14367
14932
  }
14368
14933
  }
14369
14934
  } catch {}
14370
- if (existsSync28(join28(projectRoot, "bun.lock")) || existsSync28(join28(projectRoot, "bun.lockb"))) {
14935
+ if (existsSync29(join30(projectRoot, "bun.lock")) || existsSync29(join30(projectRoot, "bun.lockb"))) {
14371
14936
  return "bun";
14372
14937
  }
14373
- if (existsSync28(join28(projectRoot, "yarn.lock"))) {
14938
+ if (existsSync29(join30(projectRoot, "yarn.lock"))) {
14374
14939
  return "yarn";
14375
14940
  }
14376
- if (existsSync28(join28(projectRoot, "pnpm-lock.yaml"))) {
14941
+ if (existsSync29(join30(projectRoot, "pnpm-lock.yaml"))) {
14377
14942
  return "pnpm";
14378
14943
  }
14379
14944
  return "npm";
@@ -14476,9 +15041,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
14476
15041
  ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
14477
15042
  `);
14478
15043
  }
14479
- const setupScript = join28(projectRoot, ".locus", "sandbox-setup.sh");
14480
- const containerSetupScript = containerWorkdir ? join28(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
14481
- if (existsSync28(setupScript)) {
15044
+ const setupScript = join30(projectRoot, ".locus", "sandbox-setup.sh");
15045
+ const containerSetupScript = containerWorkdir ? join30(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
15046
+ if (existsSync29(setupScript)) {
14482
15047
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
14483
15048
  `);
14484
15049
  const hookOk = await runInteractiveCommand("docker", [
@@ -14525,7 +15090,7 @@ async function handleSetup(projectRoot) {
14525
15090
  }
14526
15091
  function buildProviderSandboxNames(projectRoot) {
14527
15092
  const segment = sanitizeSegment(basename4(projectRoot));
14528
- const hash = createHash("sha1").update(projectRoot).digest("hex").slice(0, 8);
15093
+ const hash = createHash2("sha1").update(projectRoot).digest("hex").slice(0, 8);
14529
15094
  return {
14530
15095
  claude: `locus-${segment}-claude-${hash}`,
14531
15096
  codex: `locus-${segment}-codex-${hash}`
@@ -14619,16 +15184,21 @@ async function ensureCodexInSandbox(sandboxName) {
14619
15184
  }
14620
15185
  }
14621
15186
  function isSandboxAlive(name) {
14622
- try {
14623
- const output = execSync22("docker sandbox ls", {
14624
- encoding: "utf-8",
14625
- stdio: ["pipe", "pipe", "pipe"],
14626
- timeout: 5000
14627
- });
14628
- return output.includes(name);
14629
- } catch {
14630
- return false;
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
+ }
14631
15200
  }
15201
+ return false;
14632
15202
  }
14633
15203
  var PROVIDERS;
14634
15204
  var init_sandbox2 = __esm(() => {
@@ -14646,17 +15216,17 @@ init_context();
14646
15216
  init_logger();
14647
15217
  init_rate_limiter();
14648
15218
  init_terminal();
14649
- import { existsSync as existsSync29, readFileSync as readFileSync18 } from "node:fs";
14650
- import { join as join29 } from "node:path";
15219
+ import { existsSync as existsSync30, readFileSync as readFileSync19 } from "node:fs";
15220
+ import { join as join31 } from "node:path";
14651
15221
  import { fileURLToPath } from "node:url";
14652
15222
  function getCliVersion() {
14653
15223
  const fallbackVersion = "0.0.0";
14654
- const packageJsonPath = join29(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
14655
- if (!existsSync29(packageJsonPath)) {
15224
+ const packageJsonPath = join31(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
15225
+ if (!existsSync30(packageJsonPath)) {
14656
15226
  return fallbackVersion;
14657
15227
  }
14658
15228
  try {
14659
- const parsed = JSON.parse(readFileSync18(packageJsonPath, "utf-8"));
15229
+ const parsed = JSON.parse(readFileSync19(packageJsonPath, "utf-8"));
14660
15230
  return parsed.version ?? fallbackVersion;
14661
15231
  } catch {
14662
15232
  return fallbackVersion;
@@ -14686,6 +15256,7 @@ function parseArgs(argv) {
14686
15256
  check: false,
14687
15257
  upgrade: false,
14688
15258
  list: false,
15259
+ installed: false,
14689
15260
  noSandbox: false
14690
15261
  };
14691
15262
  const positional = [];
@@ -14763,6 +15334,9 @@ function parseArgs(argv) {
14763
15334
  case "--target-version":
14764
15335
  flags.targetVersion = rawArgs[++i];
14765
15336
  break;
15337
+ case "--installed":
15338
+ flags.installed = true;
15339
+ break;
14766
15340
  case "--no-sandbox":
14767
15341
  flags.noSandbox = true;
14768
15342
  break;
@@ -14841,6 +15415,7 @@ ${bold2("Commands:")}
14841
15415
  ${cyan2("uninstall")} Remove an installed package
14842
15416
  ${cyan2("packages")} Manage installed packages (list, outdated)
14843
15417
  ${cyan2("pkg")} ${dim2("<name> [cmd]")} Run a command from an installed package
15418
+ ${cyan2("skills")} Discover and manage agent skills
14844
15419
  ${cyan2("sandbox")} Manage Docker sandbox lifecycle
14845
15420
  ${cyan2("upgrade")} Check for and install updates
14846
15421
 
@@ -14859,6 +15434,8 @@ ${bold2("Examples:")}
14859
15434
  locus run 42 43 ${dim2("# Run issues in parallel")}
14860
15435
  locus run 42 --no-sandbox ${dim2("# Run without sandbox")}
14861
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")}
14862
15439
  locus sandbox ${dim2("# Create Docker sandbox")}
14863
15440
  locus sandbox claude ${dim2("# Login to Claude in sandbox")}
14864
15441
 
@@ -14932,7 +15509,7 @@ async function main() {
14932
15509
  try {
14933
15510
  const root = getGitRoot(cwd);
14934
15511
  if (isInitialized(root)) {
14935
- logDir = join29(root, ".locus", "logs");
15512
+ logDir = join31(root, ".locus", "logs");
14936
15513
  getRateLimiter(root);
14937
15514
  }
14938
15515
  } catch {}
@@ -15000,6 +15577,17 @@ async function main() {
15000
15577
  logger.destroy();
15001
15578
  return;
15002
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
+ }
15003
15591
  if (command === "pkg") {
15004
15592
  const { pkgCommand: pkgCommand2 } = await Promise.resolve().then(() => (init_pkg(), exports_pkg));
15005
15593
  await pkgCommand2(parsed.args, {});