@locusai/cli 0.24.8 → 0.25.0

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