@locusai/cli 0.24.6 → 0.24.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/locus.js +381 -271
- package/package.json +2 -2
package/bin/locus.js
CHANGED
|
@@ -39,8 +39,7 @@ var init_dist = __esm(() => {
|
|
|
39
39
|
rebaseBeforeTask: true
|
|
40
40
|
},
|
|
41
41
|
sprint: {
|
|
42
|
-
|
|
43
|
-
stopOnFailure: true
|
|
42
|
+
stopOnFailure: false
|
|
44
43
|
},
|
|
45
44
|
logging: {
|
|
46
45
|
level: "normal",
|
|
@@ -3278,7 +3277,6 @@ ${bold2("Locus Configuration")}
|
|
|
3278
3277
|
{
|
|
3279
3278
|
title: "Sprint",
|
|
3280
3279
|
entries: [
|
|
3281
|
-
["Active", config.sprint.active ?? dim2("(none)")],
|
|
3282
3280
|
["Stop on Failure", String(config.sprint.stopOnFailure)]
|
|
3283
3281
|
]
|
|
3284
3282
|
},
|
|
@@ -7013,9 +7011,6 @@ async function sprintCommand(projectRoot, args) {
|
|
|
7013
7011
|
case "show":
|
|
7014
7012
|
await sprintShow(projectRoot, parsed);
|
|
7015
7013
|
break;
|
|
7016
|
-
case "active":
|
|
7017
|
-
await sprintActive(projectRoot, parsed);
|
|
7018
|
-
break;
|
|
7019
7014
|
case "order":
|
|
7020
7015
|
await sprintOrder(projectRoot, parsed);
|
|
7021
7016
|
break;
|
|
@@ -7056,8 +7051,6 @@ async function sprintCreate(projectRoot, parsed) {
|
|
|
7056
7051
|
process.stderr.write(` Due: ${parsed.flags.due}
|
|
7057
7052
|
`);
|
|
7058
7053
|
}
|
|
7059
|
-
process.stderr.write(` Set as active sprint: ${bold2(`locus sprint active "${title}"`)}
|
|
7060
|
-
`);
|
|
7061
7054
|
} catch (e) {
|
|
7062
7055
|
process.stderr.write(`\r${red2("✗")} Failed to create sprint: ${e.message}
|
|
7063
7056
|
`);
|
|
@@ -7067,8 +7060,6 @@ async function sprintCreate(projectRoot, parsed) {
|
|
|
7067
7060
|
async function sprintList(projectRoot, parsed) {
|
|
7068
7061
|
const config = loadConfig(projectRoot);
|
|
7069
7062
|
const { owner, repo } = config.github;
|
|
7070
|
-
const activeSprint = config.sprint.active;
|
|
7071
|
-
const normalizedActiveSprint = activeSprint ? normalizeMilestoneTitle(activeSprint) : null;
|
|
7072
7063
|
const state = parsed.flags.all ? "all" : "open";
|
|
7073
7064
|
process.stderr.write(`${cyan2("●")} Fetching sprints...`);
|
|
7074
7065
|
let milestones;
|
|
@@ -7094,16 +7085,10 @@ async function sprintList(projectRoot, parsed) {
|
|
|
7094
7085
|
|
|
7095
7086
|
`);
|
|
7096
7087
|
const columns = [
|
|
7097
|
-
{
|
|
7098
|
-
key: "active",
|
|
7099
|
-
header: " ",
|
|
7100
|
-
minWidth: 2,
|
|
7101
|
-
format: (_, row) => normalizedActiveSprint !== null && normalizeMilestoneTitle(String(row.title)) === normalizedActiveSprint ? green("●") : " "
|
|
7102
|
-
},
|
|
7103
7088
|
{
|
|
7104
7089
|
key: "title",
|
|
7105
7090
|
header: "Sprint",
|
|
7106
|
-
format: (v
|
|
7091
|
+
format: (v) => bold2(String(v))
|
|
7107
7092
|
},
|
|
7108
7093
|
{
|
|
7109
7094
|
key: "progress",
|
|
@@ -7148,7 +7133,6 @@ async function sprintList(projectRoot, parsed) {
|
|
|
7148
7133
|
}
|
|
7149
7134
|
];
|
|
7150
7135
|
const rows = milestones.map((m) => ({
|
|
7151
|
-
active: null,
|
|
7152
7136
|
title: m.title,
|
|
7153
7137
|
openIssues: m.openIssues,
|
|
7154
7138
|
closedIssues: m.closedIssues,
|
|
@@ -7159,21 +7143,15 @@ async function sprintList(projectRoot, parsed) {
|
|
|
7159
7143
|
process.stderr.write(`${renderTable(columns, rows)}
|
|
7160
7144
|
|
|
7161
7145
|
`);
|
|
7162
|
-
if (activeSprint) {
|
|
7163
|
-
process.stderr.write(` ${green("●")} = active sprint
|
|
7164
|
-
`);
|
|
7165
|
-
}
|
|
7166
7146
|
}
|
|
7167
7147
|
async function sprintShow(projectRoot, parsed) {
|
|
7168
7148
|
const config = loadConfig(projectRoot);
|
|
7169
7149
|
const { owner, repo } = config.github;
|
|
7170
|
-
const sprintName = parsed.positional[0]
|
|
7150
|
+
const sprintName = parsed.positional[0];
|
|
7171
7151
|
if (!sprintName) {
|
|
7172
|
-
process.stderr.write(`${red2("✗")}
|
|
7152
|
+
process.stderr.write(`${red2("✗")} Missing sprint name.
|
|
7173
7153
|
`);
|
|
7174
7154
|
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
7155
|
`);
|
|
7178
7156
|
process.exit(1);
|
|
7179
7157
|
}
|
|
@@ -7226,10 +7204,6 @@ ${bold2(`Sprint: ${milestone.title}`)} ${dim2(`(${milestone.state})`)}
|
|
|
7226
7204
|
{
|
|
7227
7205
|
label: "State",
|
|
7228
7206
|
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
7207
|
}
|
|
7234
7208
|
]);
|
|
7235
7209
|
process.stderr.write(`${details}
|
|
@@ -7298,49 +7272,12 @@ ${bold2("Tasks")} ${dim2(`(by execution order)`)}
|
|
|
7298
7272
|
process.stderr.write(`
|
|
7299
7273
|
`);
|
|
7300
7274
|
}
|
|
7301
|
-
async function
|
|
7275
|
+
async function sprintOrder(projectRoot, parsed) {
|
|
7302
7276
|
const sprintName = parsed.positional[0];
|
|
7303
7277
|
if (!sprintName) {
|
|
7304
|
-
|
|
7305
|
-
if (config2.sprint.active) {
|
|
7306
|
-
process.stderr.write(`Active sprint: ${bold2(green(config2.sprint.active))}
|
|
7307
|
-
`);
|
|
7308
|
-
} else {
|
|
7309
|
-
process.stderr.write(`${dim2("No active sprint set.")}
|
|
7310
|
-
`);
|
|
7311
|
-
process.stderr.write(` Set one: ${bold2('locus sprint active "Sprint 1"')}
|
|
7312
|
-
`);
|
|
7313
|
-
}
|
|
7314
|
-
return;
|
|
7315
|
-
}
|
|
7316
|
-
const config = loadConfig(projectRoot);
|
|
7317
|
-
const { owner, repo } = config.github;
|
|
7318
|
-
process.stderr.write(`${cyan2("●")} Verifying sprint...`);
|
|
7319
|
-
let milestones;
|
|
7320
|
-
try {
|
|
7321
|
-
milestones = listMilestones(owner, repo, "open", { cwd: projectRoot });
|
|
7322
|
-
} catch (e) {
|
|
7323
|
-
process.stderr.write(`\r${red2("✗")} ${e.message}
|
|
7324
|
-
`);
|
|
7325
|
-
process.exit(1);
|
|
7326
|
-
return;
|
|
7327
|
-
}
|
|
7328
|
-
const found = findMilestoneByTitle(milestones, sprintName);
|
|
7329
|
-
if (!found) {
|
|
7330
|
-
process.stderr.write(`\r${red2("✗")} Sprint "${sprintName}" not found (or is closed).
|
|
7331
|
-
`);
|
|
7332
|
-
process.exit(1);
|
|
7333
|
-
return;
|
|
7334
|
-
}
|
|
7335
|
-
updateConfigValue(projectRoot, "sprint.active", found.title);
|
|
7336
|
-
process.stderr.write(`\r${green("✓")} Active sprint set to "${bold2(found.title)}"
|
|
7278
|
+
process.stderr.write(`${red2("✗")} Missing sprint name.
|
|
7337
7279
|
`);
|
|
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.
|
|
7280
|
+
process.stderr.write(` Usage: ${bold2('locus sprint order "Sprint 1" 17 15 16')}
|
|
7344
7281
|
`);
|
|
7345
7282
|
process.exit(1);
|
|
7346
7283
|
}
|
|
@@ -7475,15 +7412,15 @@ ${green("✓")} Sprint order updated.
|
|
|
7475
7412
|
`);
|
|
7476
7413
|
}
|
|
7477
7414
|
async function sprintClose(projectRoot, parsed) {
|
|
7478
|
-
const
|
|
7479
|
-
const sprintName = parsed.positional[0] ?? config.sprint.active;
|
|
7415
|
+
const sprintName = parsed.positional[0];
|
|
7480
7416
|
if (!sprintName) {
|
|
7481
|
-
process.stderr.write(`${red2("✗")}
|
|
7417
|
+
process.stderr.write(`${red2("✗")} Missing sprint name.
|
|
7482
7418
|
`);
|
|
7483
7419
|
process.stderr.write(` Usage: ${bold2('locus sprint close "Sprint 1"')}
|
|
7484
7420
|
`);
|
|
7485
7421
|
process.exit(1);
|
|
7486
7422
|
}
|
|
7423
|
+
const config = loadConfig(projectRoot);
|
|
7487
7424
|
const { owner, repo } = config.github;
|
|
7488
7425
|
process.stderr.write(`${cyan2("●")} Closing sprint "${sprintName}"...`);
|
|
7489
7426
|
let milestones;
|
|
@@ -7512,11 +7449,6 @@ async function sprintClose(projectRoot, parsed) {
|
|
|
7512
7449
|
}
|
|
7513
7450
|
process.stderr.write(`\r${green("✓")} Closed sprint "${bold2(milestone.title)}"
|
|
7514
7451
|
`);
|
|
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
7452
|
}
|
|
7521
7453
|
function getOrder(labels) {
|
|
7522
7454
|
for (const label of labels) {
|
|
@@ -7567,7 +7499,6 @@ ${bold2("Subcommands:")}
|
|
|
7567
7499
|
${cyan2("create")} ${dim2("(c)")} Create a new sprint
|
|
7568
7500
|
${cyan2("list")} ${dim2("(ls)")} List sprints (default)
|
|
7569
7501
|
${cyan2("show")} Show sprint details and task order
|
|
7570
|
-
${cyan2("active")} Set or show the active sprint
|
|
7571
7502
|
${cyan2("order")} Reorder sprint tasks
|
|
7572
7503
|
${cyan2("close")} Close a sprint
|
|
7573
7504
|
|
|
@@ -7585,7 +7516,6 @@ ${bold2("Examples:")}
|
|
|
7585
7516
|
locus sprint create "Sprint 1" --due 2026-03-07
|
|
7586
7517
|
locus sprint list
|
|
7587
7518
|
locus sprint show "Sprint 1"
|
|
7588
|
-
locus sprint active "Sprint 1"
|
|
7589
7519
|
locus sprint order "Sprint 1" 17 15 16
|
|
7590
7520
|
locus sprint order "Sprint 1" --show
|
|
7591
7521
|
locus sprint close "Sprint 1"
|
|
@@ -10425,6 +10355,21 @@ var init_shutdown = __esm(() => {
|
|
|
10425
10355
|
});
|
|
10426
10356
|
|
|
10427
10357
|
// src/core/worktree.ts
|
|
10358
|
+
var exports_worktree = {};
|
|
10359
|
+
__export(exports_worktree, {
|
|
10360
|
+
sprintSlug: () => sprintSlug2,
|
|
10361
|
+
removeWorktree: () => removeWorktree,
|
|
10362
|
+
removeSprintWorktree: () => removeSprintWorktree,
|
|
10363
|
+
pushWorktreeBranch: () => pushWorktreeBranch,
|
|
10364
|
+
listWorktrees: () => listWorktrees,
|
|
10365
|
+
hasWorktreeChanges: () => hasWorktreeChanges,
|
|
10366
|
+
getWorktreePath: () => getWorktreePath,
|
|
10367
|
+
getWorktreeAge: () => getWorktreeAge,
|
|
10368
|
+
getSprintWorktreePath: () => getSprintWorktreePath,
|
|
10369
|
+
createWorktree: () => createWorktree,
|
|
10370
|
+
createSprintWorktree: () => createSprintWorktree,
|
|
10371
|
+
cleanupStaleWorktrees: () => cleanupStaleWorktrees
|
|
10372
|
+
});
|
|
10428
10373
|
import { execSync as execSync16 } from "node:child_process";
|
|
10429
10374
|
import { existsSync as existsSync21, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
|
|
10430
10375
|
import { join as join21 } from "node:path";
|
|
@@ -10448,10 +10393,56 @@ function getWorktreeDir(projectRoot) {
|
|
|
10448
10393
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
10449
10394
|
return join21(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
10450
10395
|
}
|
|
10396
|
+
function getSprintWorktreePath(projectRoot, sprintSlug2) {
|
|
10397
|
+
return join21(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
|
|
10398
|
+
}
|
|
10451
10399
|
function generateBranchName(issueNumber) {
|
|
10452
10400
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
10453
10401
|
return `locus/issue-${issueNumber}-${randomSuffix}`;
|
|
10454
10402
|
}
|
|
10403
|
+
function sprintSlug2(name) {
|
|
10404
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
10405
|
+
}
|
|
10406
|
+
function createSprintWorktree(projectRoot, sprintName, baseBranch) {
|
|
10407
|
+
const log = getLogger();
|
|
10408
|
+
const slug = sprintSlug2(sprintName);
|
|
10409
|
+
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
10410
|
+
if (existsSync21(worktreePath)) {
|
|
10411
|
+
log.verbose(`Sprint worktree already exists for "${sprintName}"`);
|
|
10412
|
+
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/sprint-${slug}`;
|
|
10413
|
+
return { path: worktreePath, branch: existingBranch };
|
|
10414
|
+
}
|
|
10415
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
10416
|
+
const branch = `locus/sprint-${slug}-${randomSuffix}`;
|
|
10417
|
+
git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
|
|
10418
|
+
initSubmodules(worktreePath);
|
|
10419
|
+
log.info(`Created sprint worktree for "${sprintName}"`, {
|
|
10420
|
+
path: worktreePath,
|
|
10421
|
+
branch,
|
|
10422
|
+
baseBranch
|
|
10423
|
+
});
|
|
10424
|
+
return { path: worktreePath, branch };
|
|
10425
|
+
}
|
|
10426
|
+
function removeSprintWorktree(projectRoot, sprintName) {
|
|
10427
|
+
const log = getLogger();
|
|
10428
|
+
const slug = sprintSlug2(sprintName);
|
|
10429
|
+
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
10430
|
+
if (!existsSync21(worktreePath)) {
|
|
10431
|
+
log.verbose(`Sprint worktree for "${sprintName}" does not exist`);
|
|
10432
|
+
return;
|
|
10433
|
+
}
|
|
10434
|
+
const branch = getWorktreeBranch(worktreePath);
|
|
10435
|
+
try {
|
|
10436
|
+
git4(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
|
|
10437
|
+
log.info(`Removed sprint worktree for "${sprintName}"`);
|
|
10438
|
+
} catch (e) {
|
|
10439
|
+
log.warn(`Failed to remove sprint worktree: ${e}`);
|
|
10440
|
+
gitSafe3(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
|
|
10441
|
+
}
|
|
10442
|
+
if (branch) {
|
|
10443
|
+
gitSafe3(`branch -D ${branch}`, projectRoot);
|
|
10444
|
+
}
|
|
10445
|
+
}
|
|
10455
10446
|
function getWorktreeBranch(worktreePath) {
|
|
10456
10447
|
try {
|
|
10457
10448
|
return execSync16("git branch --show-current", {
|
|
@@ -10571,6 +10562,36 @@ function cleanupStaleWorktrees(projectRoot) {
|
|
|
10571
10562
|
}
|
|
10572
10563
|
return cleaned;
|
|
10573
10564
|
}
|
|
10565
|
+
function pushWorktreeBranch(projectRoot, issueNumber) {
|
|
10566
|
+
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10567
|
+
if (!existsSync21(worktreePath)) {
|
|
10568
|
+
throw new Error(`Worktree for issue #${issueNumber} does not exist`);
|
|
10569
|
+
}
|
|
10570
|
+
const branch = getWorktreeBranch(worktreePath);
|
|
10571
|
+
if (!branch) {
|
|
10572
|
+
throw new Error(`Could not determine branch for worktree #${issueNumber}`);
|
|
10573
|
+
}
|
|
10574
|
+
git4(`push -u origin ${branch}`, worktreePath);
|
|
10575
|
+
return branch;
|
|
10576
|
+
}
|
|
10577
|
+
function hasWorktreeChanges(projectRoot, issueNumber) {
|
|
10578
|
+
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10579
|
+
if (!existsSync21(worktreePath))
|
|
10580
|
+
return false;
|
|
10581
|
+
const status = gitSafe3("status --porcelain", worktreePath);
|
|
10582
|
+
return status !== null && status.trim().length > 0;
|
|
10583
|
+
}
|
|
10584
|
+
function getWorktreeAge(projectRoot, issueNumber) {
|
|
10585
|
+
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10586
|
+
if (!existsSync21(worktreePath))
|
|
10587
|
+
return 0;
|
|
10588
|
+
try {
|
|
10589
|
+
const stat = statSync4(worktreePath);
|
|
10590
|
+
return Date.now() - stat.ctimeMs;
|
|
10591
|
+
} catch {
|
|
10592
|
+
return 0;
|
|
10593
|
+
}
|
|
10594
|
+
}
|
|
10574
10595
|
var init_worktree = __esm(() => {
|
|
10575
10596
|
init_logger();
|
|
10576
10597
|
init_submodule();
|
|
@@ -10582,6 +10603,8 @@ __export(exports_run, {
|
|
|
10582
10603
|
runCommand: () => runCommand
|
|
10583
10604
|
});
|
|
10584
10605
|
import { execSync as execSync17 } from "node:child_process";
|
|
10606
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
10607
|
+
import { join as join22 } from "node:path";
|
|
10585
10608
|
function resolveExecutionContext(config, modelOverride) {
|
|
10586
10609
|
const model = modelOverride ?? config.ai.model;
|
|
10587
10610
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
@@ -10598,12 +10621,14 @@ function printRunHelp() {
|
|
|
10598
10621
|
${bold2("locus run")} — Execute issues using AI agents
|
|
10599
10622
|
|
|
10600
10623
|
${bold2("Usage:")}
|
|
10601
|
-
locus run ${dim2("# Run
|
|
10624
|
+
locus run ${dim2("# Run all open sprints (parallel)")}
|
|
10625
|
+
locus run --sprint <name> ${dim2("# Run a specific sprint")}
|
|
10602
10626
|
locus run <issue> ${dim2("# Run single issue (worktree)")}
|
|
10603
10627
|
locus run <issue> <issue> ... ${dim2("# Run multiple issues (parallel)")}
|
|
10604
10628
|
locus run --resume ${dim2("# Resume interrupted run")}
|
|
10605
10629
|
|
|
10606
10630
|
${bold2("Options:")}
|
|
10631
|
+
--sprint <name> Run a specific sprint (instead of all active)
|
|
10607
10632
|
--resume Resume a previously interrupted run
|
|
10608
10633
|
--dry-run Show what would happen without executing
|
|
10609
10634
|
--model <name> Override the AI model for this run
|
|
@@ -10616,7 +10641,8 @@ ${bold2("Sandbox:")}
|
|
|
10616
10641
|
unsandboxed with a warning.
|
|
10617
10642
|
|
|
10618
10643
|
${bold2("Examples:")}
|
|
10619
|
-
locus run ${dim2("# Execute
|
|
10644
|
+
locus run ${dim2("# Execute all open sprints")}
|
|
10645
|
+
locus run --sprint "Sprint 1" ${dim2("# Run a specific sprint")}
|
|
10620
10646
|
locus run 42 ${dim2("# Run single issue")}
|
|
10621
10647
|
locus run 42 43 44 ${dim2("# Run issues in parallel")}
|
|
10622
10648
|
locus run --resume ${dim2("# Resume after failure")}
|
|
@@ -10669,11 +10695,11 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
10669
10695
|
}
|
|
10670
10696
|
}
|
|
10671
10697
|
if (flags.resume) {
|
|
10672
|
-
return handleResume(projectRoot, config, sandboxed,
|
|
10698
|
+
return handleResume(projectRoot, config, sandboxed, flags);
|
|
10673
10699
|
}
|
|
10674
10700
|
const issueNumbers = args.filter((a) => /^\d+$/.test(a)).map(Number);
|
|
10675
10701
|
if (issueNumbers.length === 0) {
|
|
10676
|
-
return handleSprintRun(projectRoot, config, flags, sandboxed
|
|
10702
|
+
return handleSprintRun(projectRoot, config, flags, sandboxed);
|
|
10677
10703
|
}
|
|
10678
10704
|
if (issueNumbers.length === 1) {
|
|
10679
10705
|
return handleSingleIssue(projectRoot, config, issueNumbers[0], flags, sandboxed);
|
|
@@ -10683,18 +10709,56 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
10683
10709
|
cleanupShutdown();
|
|
10684
10710
|
}
|
|
10685
10711
|
}
|
|
10686
|
-
async function handleSprintRun(projectRoot, config, flags, sandboxed
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10712
|
+
async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
10713
|
+
let sprintNames;
|
|
10714
|
+
if (flags.sprint) {
|
|
10715
|
+
sprintNames = [flags.sprint];
|
|
10716
|
+
} else {
|
|
10717
|
+
process.stderr.write(`${cyan2("●")} Detecting open sprints...`);
|
|
10718
|
+
try {
|
|
10719
|
+
const milestones = listMilestones(config.github.owner, config.github.repo, "open", { cwd: projectRoot });
|
|
10720
|
+
sprintNames = milestones.map((m) => m.title);
|
|
10721
|
+
} catch (e) {
|
|
10722
|
+
process.stderr.write(`\r${red2("✗")} Failed to fetch sprints: ${e.message}
|
|
10723
|
+
`);
|
|
10724
|
+
return;
|
|
10725
|
+
}
|
|
10726
|
+
process.stderr.write("\r\x1B[2K");
|
|
10727
|
+
}
|
|
10728
|
+
if (sprintNames.length === 0) {
|
|
10729
|
+
process.stderr.write(`${red2("✗")} No open sprints found.
|
|
10730
|
+
`);
|
|
10731
|
+
process.stderr.write(` Create one: ${bold2('locus sprint create "Sprint 1"')}
|
|
10691
10732
|
`);
|
|
10692
10733
|
process.stderr.write(` Or specify issue numbers: ${bold2("locus run 42 43 44")}
|
|
10693
10734
|
`);
|
|
10694
10735
|
return;
|
|
10695
10736
|
}
|
|
10696
|
-
const
|
|
10697
|
-
|
|
10737
|
+
const cleaned = cleanupStaleWorktrees(projectRoot);
|
|
10738
|
+
if (cleaned > 0) {
|
|
10739
|
+
process.stderr.write(` ${dim2(`Cleaned up ${cleaned} stale worktree${cleaned === 1 ? "" : "s"}.`)}
|
|
10740
|
+
`);
|
|
10741
|
+
}
|
|
10742
|
+
if (sprintNames.length === 1) {
|
|
10743
|
+
await executeSingleSprint(projectRoot, config, sprintNames[0], flags, sandboxed);
|
|
10744
|
+
} else {
|
|
10745
|
+
process.stderr.write(`
|
|
10746
|
+
${bold2("Running")} ${cyan2(`${sprintNames.length} sprints`)} ${dim2("(parallel, worktrees)")}
|
|
10747
|
+
|
|
10748
|
+
`);
|
|
10749
|
+
const timer = createTimer();
|
|
10750
|
+
const promises = sprintNames.map((name) => executeSingleSprint(projectRoot, config, name, flags, sandboxed).catch((e) => {
|
|
10751
|
+
getLogger().warn(`Sprint "${name}" threw: ${e}`);
|
|
10752
|
+
}));
|
|
10753
|
+
await Promise.allSettled(promises);
|
|
10754
|
+
process.stderr.write(`
|
|
10755
|
+
${bold2("All sprints complete")} ${dim2(`(${timer.formatted()})`)}
|
|
10756
|
+
|
|
10757
|
+
`);
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
async function executeSingleSprint(projectRoot, config, sprintName, flags, sandboxed) {
|
|
10761
|
+
const execution = resolveExecutionContext(config, flags.model);
|
|
10698
10762
|
process.stderr.write(`
|
|
10699
10763
|
${bold2("Sprint:")} ${cyan2(sprintName)}
|
|
10700
10764
|
`);
|
|
@@ -10703,7 +10767,7 @@ ${bold2("Sprint:")} ${cyan2(sprintName)}
|
|
|
10703
10767
|
const stats2 = getRunStats(existingState);
|
|
10704
10768
|
if (stats2.inProgress > 0 || stats2.pending > 0) {
|
|
10705
10769
|
process.stderr.write(`
|
|
10706
|
-
${yellow2("⚠")} A run for
|
|
10770
|
+
${yellow2("⚠")} A run for sprint "${sprintName}" is already in progress.
|
|
10707
10771
|
`);
|
|
10708
10772
|
process.stderr.write(` Use ${bold2("locus run --resume")} to continue.
|
|
10709
10773
|
`);
|
|
@@ -10724,14 +10788,33 @@ ${yellow2("⚠")} A run for this sprint is already in progress.
|
|
|
10724
10788
|
return;
|
|
10725
10789
|
}
|
|
10726
10790
|
issues = sortByOrder2(issues);
|
|
10727
|
-
|
|
10728
|
-
|
|
10791
|
+
let worktreePath;
|
|
10792
|
+
let branchName;
|
|
10793
|
+
if (!flags.dryRun) {
|
|
10794
|
+
try {
|
|
10795
|
+
const wt = createSprintWorktree(projectRoot, sprintName, config.agent.baseBranch);
|
|
10796
|
+
worktreePath = wt.path;
|
|
10797
|
+
branchName = wt.branch;
|
|
10798
|
+
process.stderr.write(` ${dim2(`Worktree: ${wt.path}`)}
|
|
10799
|
+
`);
|
|
10800
|
+
process.stderr.write(` ${dim2(`Branch: ${wt.branch}`)}
|
|
10801
|
+
`);
|
|
10802
|
+
} catch (e) {
|
|
10803
|
+
process.stderr.write(`${red2("✗")} Failed to create worktree: ${e}
|
|
10804
|
+
`);
|
|
10805
|
+
return;
|
|
10806
|
+
}
|
|
10807
|
+
} else {
|
|
10808
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
10809
|
+
branchName = `locus/sprint-${sprintName.toLowerCase().replace(/\s+/g, "-")}-${randomSuffix}`;
|
|
10810
|
+
}
|
|
10811
|
+
const workDir = worktreePath ?? projectRoot;
|
|
10729
10812
|
const state = createSprintRunState(sprintName, branchName, issues.map((issue, i) => ({
|
|
10730
10813
|
number: issue.number,
|
|
10731
10814
|
order: getOrder2(issue) ?? i + 1
|
|
10732
10815
|
})));
|
|
10733
10816
|
saveRunState(projectRoot, state);
|
|
10734
|
-
process.stderr.write(` ${dim2(`${issues.length} tasks
|
|
10817
|
+
process.stderr.write(` ${dim2(`${issues.length} tasks`)}
|
|
10735
10818
|
|
|
10736
10819
|
`);
|
|
10737
10820
|
for (const task of state.tasks) {
|
|
@@ -10746,20 +10829,6 @@ ${yellow2("⚠")} A run for this sprint is already in progress.
|
|
|
10746
10829
|
|
|
10747
10830
|
`);
|
|
10748
10831
|
}
|
|
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
10832
|
const timer = createTimer();
|
|
10764
10833
|
for (let i = 0;i < state.tasks.length; i++) {
|
|
10765
10834
|
const task = state.tasks[i];
|
|
@@ -10771,27 +10840,37 @@ ${yellow2("⚠")} A run for this sprint is already in progress.
|
|
|
10771
10840
|
}
|
|
10772
10841
|
await getRateLimiter().checkBeforeRequest();
|
|
10773
10842
|
if (config.agent.rebaseBeforeTask && !flags.dryRun && i > 0) {
|
|
10774
|
-
const conflictResult = checkForConflicts(
|
|
10843
|
+
const conflictResult = checkForConflicts(workDir, config.agent.baseBranch);
|
|
10775
10844
|
if (conflictResult.baseAdvanced) {
|
|
10776
10845
|
printConflictReport(conflictResult, config.agent.baseBranch);
|
|
10777
10846
|
if (conflictResult.hasConflict) {
|
|
10778
10847
|
markTaskFailed(state, task.issue, "Merge conflict with base branch");
|
|
10779
10848
|
saveRunState(projectRoot, state);
|
|
10780
|
-
process.stderr.write(`
|
|
10849
|
+
process.stderr.write(` ${red2("✗")} Task #${task.issue} skipped due to conflicts.
|
|
10850
|
+
`);
|
|
10851
|
+
if (config.sprint.stopOnFailure) {
|
|
10852
|
+
process.stderr.write(`
|
|
10781
10853
|
${red2("✗")} Sprint stopped due to conflicts.
|
|
10782
10854
|
`);
|
|
10783
|
-
|
|
10855
|
+
process.stderr.write(` Resolve conflicts and run: ${bold2("locus run --resume")}
|
|
10784
10856
|
`);
|
|
10785
|
-
|
|
10857
|
+
break;
|
|
10858
|
+
}
|
|
10859
|
+
continue;
|
|
10786
10860
|
}
|
|
10787
|
-
const rebaseResult = attemptRebase(
|
|
10861
|
+
const rebaseResult = attemptRebase(workDir, config.agent.baseBranch);
|
|
10788
10862
|
if (!rebaseResult.success) {
|
|
10789
10863
|
markTaskFailed(state, task.issue, "Rebase failed");
|
|
10790
10864
|
saveRunState(projectRoot, state);
|
|
10791
|
-
process.stderr.write(`
|
|
10792
|
-
${red2("✗")} Auto-rebase failed. Resolve manually.
|
|
10865
|
+
process.stderr.write(` ${red2("✗")} Auto-rebase failed for task #${task.issue}.
|
|
10793
10866
|
`);
|
|
10794
|
-
|
|
10867
|
+
if (config.sprint.stopOnFailure) {
|
|
10868
|
+
process.stderr.write(`
|
|
10869
|
+
${red2("✗")} Sprint stopped. Resolve manually.
|
|
10870
|
+
`);
|
|
10871
|
+
break;
|
|
10872
|
+
}
|
|
10873
|
+
continue;
|
|
10795
10874
|
}
|
|
10796
10875
|
}
|
|
10797
10876
|
}
|
|
@@ -10799,19 +10878,19 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
|
|
|
10799
10878
|
if (i > 0 && !flags.dryRun) {
|
|
10800
10879
|
try {
|
|
10801
10880
|
sprintContext = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
10802
|
-
cwd:
|
|
10881
|
+
cwd: workDir,
|
|
10803
10882
|
encoding: "utf-8",
|
|
10804
10883
|
stdio: ["pipe", "pipe", "pipe"]
|
|
10805
10884
|
}).trim();
|
|
10806
10885
|
} catch {}
|
|
10807
10886
|
}
|
|
10808
10887
|
process.stderr.write(`
|
|
10809
|
-
${progressBar(i, state.tasks.length, { label:
|
|
10888
|
+
${progressBar(i, state.tasks.length, { label: `Sprint: ${sprintName}` })}
|
|
10810
10889
|
|
|
10811
10890
|
`);
|
|
10812
10891
|
markTaskInProgress(state, task.issue);
|
|
10813
10892
|
saveRunState(projectRoot, state);
|
|
10814
|
-
const result = await executeIssue(
|
|
10893
|
+
const result = await executeIssue(workDir, {
|
|
10815
10894
|
issueNumber: task.issue,
|
|
10816
10895
|
provider: execution.provider,
|
|
10817
10896
|
model: execution.model,
|
|
@@ -10825,7 +10904,7 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
10825
10904
|
if (result.success) {
|
|
10826
10905
|
if (!flags.dryRun) {
|
|
10827
10906
|
const issueTitle = issue?.title ?? "";
|
|
10828
|
-
ensureTaskCommit(
|
|
10907
|
+
ensureTaskCommit(workDir, task.issue, issueTitle);
|
|
10829
10908
|
if (sandboxed && i < state.tasks.length - 1) {
|
|
10830
10909
|
process.stderr.write(` ${dim2("↻ Sandbox will resync on next task")}
|
|
10831
10910
|
`);
|
|
@@ -10837,18 +10916,20 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
10837
10916
|
saveRunState(projectRoot, state);
|
|
10838
10917
|
if (config.sprint.stopOnFailure) {
|
|
10839
10918
|
process.stderr.write(`
|
|
10840
|
-
${red2("✗")} Sprint stopped: task #${task.issue} failed.
|
|
10919
|
+
${red2("✗")} Sprint "${sprintName}" stopped: task #${task.issue} failed.
|
|
10841
10920
|
`);
|
|
10842
10921
|
process.stderr.write(` Resume with: ${bold2("locus run --resume")}
|
|
10843
10922
|
`);
|
|
10844
|
-
|
|
10923
|
+
break;
|
|
10845
10924
|
}
|
|
10925
|
+
process.stderr.write(` ${yellow2("⚠")} Task #${task.issue} failed, continuing to next task.
|
|
10926
|
+
`);
|
|
10846
10927
|
}
|
|
10847
10928
|
saveRunState(projectRoot, state);
|
|
10848
10929
|
}
|
|
10849
10930
|
const stats = getRunStats(state);
|
|
10850
10931
|
process.stderr.write(`
|
|
10851
|
-
${progressBar(stats.done, stats.total, { label:
|
|
10932
|
+
${progressBar(stats.done, stats.total, { label: `Sprint Complete: ${sprintName}` })}
|
|
10852
10933
|
`);
|
|
10853
10934
|
process.stderr.write(`
|
|
10854
10935
|
${bold2("Summary:")}
|
|
@@ -10861,21 +10942,18 @@ ${bold2("Summary:")}
|
|
|
10861
10942
|
issue: t.issue,
|
|
10862
10943
|
title: issues.find((i) => i.number === t.issue)?.title
|
|
10863
10944
|
}));
|
|
10864
|
-
|
|
10865
|
-
if (prNumber !== undefined) {
|
|
10866
|
-
try {
|
|
10867
|
-
execSync17(`git checkout ${config.agent.baseBranch}`, {
|
|
10868
|
-
cwd: projectRoot,
|
|
10869
|
-
encoding: "utf-8",
|
|
10870
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
10871
|
-
});
|
|
10872
|
-
process.stderr.write(` ${dim2(`Checked out ${config.agent.baseBranch}`)}
|
|
10873
|
-
`);
|
|
10874
|
-
} catch {}
|
|
10875
|
-
}
|
|
10945
|
+
await createSprintPR(workDir, config, sprintName, branchName, completedTasks);
|
|
10876
10946
|
}
|
|
10877
10947
|
if (stats.failed === 0) {
|
|
10878
10948
|
clearRunState(projectRoot, sprintName);
|
|
10949
|
+
if (worktreePath) {
|
|
10950
|
+
removeSprintWorktree(projectRoot, sprintName);
|
|
10951
|
+
}
|
|
10952
|
+
} else {
|
|
10953
|
+
if (worktreePath) {
|
|
10954
|
+
process.stderr.write(` ${yellow2("⚠")} Sprint worktree preserved: ${dim2(worktreePath)}
|
|
10955
|
+
`);
|
|
10956
|
+
}
|
|
10879
10957
|
}
|
|
10880
10958
|
}
|
|
10881
10959
|
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
@@ -11056,43 +11134,87 @@ ${yellow2("⚠")} Failed worktrees preserved for debugging:
|
|
|
11056
11134
|
clearRunState(projectRoot);
|
|
11057
11135
|
}
|
|
11058
11136
|
}
|
|
11059
|
-
async function handleResume(projectRoot, config, sandboxed,
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11137
|
+
async function handleResume(projectRoot, config, sandboxed, flags) {
|
|
11138
|
+
if (flags.sprint) {
|
|
11139
|
+
const state = loadRunState(projectRoot, flags.sprint);
|
|
11140
|
+
if (!state) {
|
|
11141
|
+
process.stderr.write(`${red2("✗")} No run state found for sprint "${flags.sprint}".
|
|
11142
|
+
`);
|
|
11143
|
+
return;
|
|
11144
|
+
}
|
|
11145
|
+
await resumeSingleRun(projectRoot, config, state, sandboxed);
|
|
11146
|
+
return;
|
|
11065
11147
|
}
|
|
11066
|
-
|
|
11148
|
+
const sprintsToResume = [];
|
|
11149
|
+
try {
|
|
11150
|
+
const { readdirSync: readdirSync8 } = await import("node:fs");
|
|
11151
|
+
const runStateDir = join22(projectRoot, ".locus", "run-state");
|
|
11152
|
+
if (existsSync22(runStateDir)) {
|
|
11153
|
+
const files = readdirSync8(runStateDir).filter((f) => f.endsWith(".json"));
|
|
11154
|
+
for (const file of files) {
|
|
11155
|
+
const sprintName = file === "_parallel.json" ? undefined : file.replace(/\.json$/, "");
|
|
11156
|
+
const state = loadRunState(projectRoot, sprintName);
|
|
11157
|
+
if (state) {
|
|
11158
|
+
const stats = getRunStats(state);
|
|
11159
|
+
if (stats.failed > 0 || stats.pending > 0 || stats.inProgress > 0) {
|
|
11160
|
+
sprintsToResume.push(state);
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
}
|
|
11164
|
+
}
|
|
11165
|
+
} catch {}
|
|
11166
|
+
if (sprintsToResume.length === 0) {
|
|
11067
11167
|
process.stderr.write(`${red2("✗")} No run state found. Nothing to resume.
|
|
11068
11168
|
`);
|
|
11069
11169
|
return;
|
|
11070
11170
|
}
|
|
11071
|
-
|
|
11171
|
+
if (sprintsToResume.length === 1) {
|
|
11172
|
+
await resumeSingleRun(projectRoot, config, sprintsToResume[0], sandboxed);
|
|
11173
|
+
} else {
|
|
11174
|
+
process.stderr.write(`
|
|
11175
|
+
${bold2("Resuming")} ${cyan2(`${sprintsToResume.length} runs`)} ${dim2("(parallel)")}
|
|
11176
|
+
|
|
11177
|
+
`);
|
|
11178
|
+
const promises = sprintsToResume.map((state) => resumeSingleRun(projectRoot, config, state, sandboxed).catch((e) => {
|
|
11179
|
+
getLogger().warn(`Resume for "${state.sprint}" threw: ${e}`);
|
|
11180
|
+
}));
|
|
11181
|
+
await Promise.allSettled(promises);
|
|
11182
|
+
}
|
|
11183
|
+
}
|
|
11184
|
+
async function resumeSingleRun(projectRoot, config, state, sandboxed) {
|
|
11185
|
+
const execution = resolveExecutionContext(config);
|
|
11072
11186
|
const stats = getRunStats(state);
|
|
11073
11187
|
process.stderr.write(`
|
|
11074
|
-
${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
11188
|
+
${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}${state.sprint ? ` (${cyan2(state.sprint)})` : ""}
|
|
11075
11189
|
`);
|
|
11076
11190
|
process.stderr.write(` Done: ${stats.done}, Failed: ${stats.failed}, Pending: ${stats.pending}
|
|
11077
11191
|
|
|
11078
11192
|
`);
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11193
|
+
let workDir = projectRoot;
|
|
11194
|
+
if (state.type === "sprint" && state.sprint) {
|
|
11195
|
+
const { getSprintWorktreePath: getSprintWorktreePath2, sprintSlug: sprintSlug3 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
|
|
11196
|
+
const { existsSync: existsSync23 } = await import("node:fs");
|
|
11197
|
+
const wtPath = getSprintWorktreePath2(projectRoot, sprintSlug3(state.sprint));
|
|
11198
|
+
if (existsSync23(wtPath)) {
|
|
11199
|
+
workDir = wtPath;
|
|
11200
|
+
} else if (state.branch) {
|
|
11201
|
+
try {
|
|
11202
|
+
const currentBranch = execSync17("git rev-parse --abbrev-ref HEAD", {
|
|
11088
11203
|
cwd: projectRoot,
|
|
11089
11204
|
encoding: "utf-8",
|
|
11090
11205
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11091
|
-
});
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11206
|
+
}).trim();
|
|
11207
|
+
if (currentBranch !== state.branch) {
|
|
11208
|
+
execSync17(`git checkout ${state.branch}`, {
|
|
11209
|
+
cwd: projectRoot,
|
|
11210
|
+
encoding: "utf-8",
|
|
11211
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11212
|
+
});
|
|
11213
|
+
}
|
|
11214
|
+
} catch {
|
|
11215
|
+
process.stderr.write(`${yellow2("⚠")} Could not checkout branch ${state.branch}
|
|
11095
11216
|
`);
|
|
11217
|
+
}
|
|
11096
11218
|
}
|
|
11097
11219
|
}
|
|
11098
11220
|
const isSprintRun = state.type === "sprint";
|
|
@@ -11106,7 +11228,7 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
|
11106
11228
|
}
|
|
11107
11229
|
markTaskInProgress(state, task.issue);
|
|
11108
11230
|
saveRunState(projectRoot, state);
|
|
11109
|
-
const result = await executeIssue(
|
|
11231
|
+
const result = await executeIssue(workDir, {
|
|
11110
11232
|
issueNumber: task.issue,
|
|
11111
11233
|
provider: execution.provider,
|
|
11112
11234
|
model: execution.model,
|
|
@@ -11122,7 +11244,7 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
|
11122
11244
|
const iss = getIssue(task.issue, { cwd: projectRoot });
|
|
11123
11245
|
issueTitle = iss.title;
|
|
11124
11246
|
} catch {}
|
|
11125
|
-
ensureTaskCommit(
|
|
11247
|
+
ensureTaskCommit(workDir, task.issue, issueTitle);
|
|
11126
11248
|
if (sandboxed) {
|
|
11127
11249
|
process.stderr.write(` ${dim2("↻ Sandbox will resync on next task")}
|
|
11128
11250
|
`);
|
|
@@ -11138,6 +11260,8 @@ ${red2("✗")} Sprint stopped: task #${task.issue} failed.
|
|
|
11138
11260
|
`);
|
|
11139
11261
|
return;
|
|
11140
11262
|
}
|
|
11263
|
+
process.stderr.write(` ${yellow2("⚠")} Task #${task.issue} failed, continuing to next task.
|
|
11264
|
+
`);
|
|
11141
11265
|
}
|
|
11142
11266
|
saveRunState(projectRoot, state);
|
|
11143
11267
|
task = getNextTask(state);
|
|
@@ -11149,21 +11273,13 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
|
|
|
11149
11273
|
`);
|
|
11150
11274
|
if (isSprintRun && state.branch && state.sprint && finalStats.done > 0) {
|
|
11151
11275
|
const completedTasks = state.tasks.filter((t) => t.status === "done").map((t) => ({ issue: t.issue }));
|
|
11152
|
-
|
|
11153
|
-
if (prNumber !== undefined) {
|
|
11154
|
-
try {
|
|
11155
|
-
execSync17(`git checkout ${config.agent.baseBranch}`, {
|
|
11156
|
-
cwd: projectRoot,
|
|
11157
|
-
encoding: "utf-8",
|
|
11158
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
11159
|
-
});
|
|
11160
|
-
process.stderr.write(` ${dim2(`Checked out ${config.agent.baseBranch}`)}
|
|
11161
|
-
`);
|
|
11162
|
-
} catch {}
|
|
11163
|
-
}
|
|
11276
|
+
await createSprintPR(workDir, config, state.sprint, state.branch, completedTasks);
|
|
11164
11277
|
}
|
|
11165
11278
|
if (finalStats.failed === 0) {
|
|
11166
11279
|
clearRunState(projectRoot, state.sprint);
|
|
11280
|
+
if (isSprintRun && state.sprint) {
|
|
11281
|
+
removeSprintWorktree(projectRoot, state.sprint);
|
|
11282
|
+
}
|
|
11167
11283
|
}
|
|
11168
11284
|
}
|
|
11169
11285
|
function sortByOrder2(issues) {
|
|
@@ -11181,22 +11297,22 @@ function getOrder2(issue) {
|
|
|
11181
11297
|
}
|
|
11182
11298
|
return null;
|
|
11183
11299
|
}
|
|
11184
|
-
function ensureTaskCommit(
|
|
11300
|
+
function ensureTaskCommit(workDir, issueNumber, issueTitle) {
|
|
11185
11301
|
try {
|
|
11186
|
-
const committedSubmodules = commitDirtySubmodules(
|
|
11302
|
+
const committedSubmodules = commitDirtySubmodules(workDir, issueNumber, issueTitle);
|
|
11187
11303
|
if (committedSubmodules.length > 0) {
|
|
11188
11304
|
process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
|
|
11189
11305
|
`);
|
|
11190
11306
|
}
|
|
11191
11307
|
const status = execSync17("git status --porcelain", {
|
|
11192
|
-
cwd:
|
|
11308
|
+
cwd: workDir,
|
|
11193
11309
|
encoding: "utf-8",
|
|
11194
11310
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11195
11311
|
}).trim();
|
|
11196
11312
|
if (!status)
|
|
11197
11313
|
return;
|
|
11198
11314
|
execSync17("git add -A", {
|
|
11199
|
-
cwd:
|
|
11315
|
+
cwd: workDir,
|
|
11200
11316
|
encoding: "utf-8",
|
|
11201
11317
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11202
11318
|
});
|
|
@@ -11205,7 +11321,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
11205
11321
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
11206
11322
|
execSync17(`git commit -F -`, {
|
|
11207
11323
|
input: message,
|
|
11208
|
-
cwd:
|
|
11324
|
+
cwd: workDir,
|
|
11209
11325
|
encoding: "utf-8",
|
|
11210
11326
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11211
11327
|
});
|
|
@@ -11213,12 +11329,12 @@ Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
|
11213
11329
|
`);
|
|
11214
11330
|
} catch {}
|
|
11215
11331
|
}
|
|
11216
|
-
async function createSprintPR(
|
|
11332
|
+
async function createSprintPR(workDir, config, sprintName, branchName, tasks) {
|
|
11217
11333
|
if (!config.agent.autoPR)
|
|
11218
11334
|
return;
|
|
11219
11335
|
try {
|
|
11220
11336
|
const diff = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
11221
|
-
cwd:
|
|
11337
|
+
cwd: workDir,
|
|
11222
11338
|
encoding: "utf-8",
|
|
11223
11339
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11224
11340
|
}).trim();
|
|
@@ -11227,15 +11343,15 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
11227
11343
|
`);
|
|
11228
11344
|
return;
|
|
11229
11345
|
}
|
|
11230
|
-
pushSubmoduleBranches(
|
|
11346
|
+
pushSubmoduleBranches(workDir);
|
|
11231
11347
|
execSync17(`git push -u origin ${branchName}`, {
|
|
11232
|
-
cwd:
|
|
11348
|
+
cwd: workDir,
|
|
11233
11349
|
encoding: "utf-8",
|
|
11234
11350
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11235
11351
|
});
|
|
11236
11352
|
const taskLines = tasks.map((t) => `- Closes #${t.issue}${t.title ? `: ${t.title}` : ""}`).join(`
|
|
11237
11353
|
`);
|
|
11238
|
-
const submoduleSummary = getSubmoduleChangeSummary(
|
|
11354
|
+
const submoduleSummary = getSubmoduleChangeSummary(workDir, config.agent.baseBranch);
|
|
11239
11355
|
let prBody = `## Sprint: ${sprintName}
|
|
11240
11356
|
|
|
11241
11357
|
${taskLines}`;
|
|
@@ -11249,7 +11365,7 @@ ${submoduleSummary}`;
|
|
|
11249
11365
|
---
|
|
11250
11366
|
|
|
11251
11367
|
\uD83E\uDD16 Automated by [Locus](https://github.com/asgarovf/locusai)`;
|
|
11252
|
-
const prNumber = createPR(`Sprint: ${sprintName}`, prBody, branchName, config.agent.baseBranch, { cwd:
|
|
11368
|
+
const prNumber = createPR(`Sprint: ${sprintName}`, prBody, branchName, config.agent.baseBranch, { cwd: workDir });
|
|
11253
11369
|
process.stderr.write(` ${green("✓")} Created sprint PR #${prNumber}
|
|
11254
11370
|
`);
|
|
11255
11371
|
return prNumber;
|
|
@@ -11283,8 +11399,8 @@ __export(exports_status, {
|
|
|
11283
11399
|
statusCommand: () => statusCommand
|
|
11284
11400
|
});
|
|
11285
11401
|
import { execSync as execSync18 } from "node:child_process";
|
|
11286
|
-
import { existsSync as
|
|
11287
|
-
import { dirname as dirname7, join as
|
|
11402
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
11403
|
+
import { dirname as dirname7, join as join23 } from "node:path";
|
|
11288
11404
|
async function statusCommand(projectRoot) {
|
|
11289
11405
|
const config = loadConfig(projectRoot);
|
|
11290
11406
|
const spinner = new Spinner;
|
|
@@ -11293,21 +11409,19 @@ async function statusCommand(projectRoot) {
|
|
|
11293
11409
|
lines.push(` ${dim2("Repo:")} ${cyan2(`${config.github.owner}/${config.github.repo}`)}`);
|
|
11294
11410
|
lines.push(` ${dim2("Provider:")} ${config.ai.provider} / ${config.ai.model}`);
|
|
11295
11411
|
lines.push(` ${dim2("Branch:")} ${config.agent.baseBranch}`);
|
|
11296
|
-
|
|
11297
|
-
const
|
|
11298
|
-
|
|
11299
|
-
const
|
|
11300
|
-
const milestone = milestones.find((m) => m.title.toLowerCase() === sprintName.toLowerCase());
|
|
11301
|
-
if (milestone) {
|
|
11412
|
+
try {
|
|
11413
|
+
const milestones = listMilestones(config.github.owner, config.github.repo, "open", { cwd: projectRoot });
|
|
11414
|
+
if (milestones.length > 0) {
|
|
11415
|
+
for (const milestone of milestones) {
|
|
11302
11416
|
const total = milestone.openIssues + milestone.closedIssues;
|
|
11303
11417
|
const done = milestone.closedIssues;
|
|
11304
11418
|
const dueStr = milestone.dueOn ? ` due ${new Date(milestone.dueOn).toLocaleDateString("en-US", { month: "short", day: "numeric" })}` : "";
|
|
11305
11419
|
lines.push("");
|
|
11306
|
-
lines.push(` ${bold2("Sprint:")} ${cyan2(
|
|
11420
|
+
lines.push(` ${bold2("Sprint:")} ${cyan2(milestone.title)} (${done} of ${total} done${dueStr})`);
|
|
11307
11421
|
if (total > 0) {
|
|
11308
11422
|
lines.push(` ${progressBar(done, total, { width: 30 })}`);
|
|
11309
11423
|
}
|
|
11310
|
-
const issues = listIssues({ milestone:
|
|
11424
|
+
const issues = listIssues({ milestone: milestone.title, state: "all" }, { cwd: projectRoot });
|
|
11311
11425
|
const queued = issues.filter((i) => i.labels.some((l) => l === "locus:queued")).length;
|
|
11312
11426
|
const inProgress = issues.filter((i) => i.labels.some((l) => l === "locus:in-progress")).length;
|
|
11313
11427
|
const failed = issues.filter((i) => i.labels.some((l) => l === "locus:failed")).length;
|
|
@@ -11323,17 +11437,14 @@ async function statusCommand(projectRoot) {
|
|
|
11323
11437
|
if (parts.length > 0) {
|
|
11324
11438
|
lines.push(` ${parts.join(" ")}`);
|
|
11325
11439
|
}
|
|
11326
|
-
} else {
|
|
11327
|
-
lines.push("");
|
|
11328
|
-
lines.push(` ${bold2("Sprint:")} ${cyan2(sprintName)} ${dim2("(not found)")}`);
|
|
11329
11440
|
}
|
|
11330
|
-
}
|
|
11441
|
+
} else {
|
|
11331
11442
|
lines.push("");
|
|
11332
|
-
lines.push(` ${
|
|
11443
|
+
lines.push(` ${dim2("Sprint:")} ${dim2("no open sprints")}`);
|
|
11333
11444
|
}
|
|
11334
|
-
}
|
|
11445
|
+
} catch {
|
|
11335
11446
|
lines.push("");
|
|
11336
|
-
lines.push(` ${dim2("Sprint:")} ${dim2("
|
|
11447
|
+
lines.push(` ${dim2("Sprint:")} ${dim2("(could not fetch)")}`);
|
|
11337
11448
|
}
|
|
11338
11449
|
const runState = loadRunState(projectRoot);
|
|
11339
11450
|
if (runState) {
|
|
@@ -11421,13 +11532,13 @@ ${drawBox(lines, { title: "Locus Status" })}
|
|
|
11421
11532
|
`);
|
|
11422
11533
|
}
|
|
11423
11534
|
function getPm2Bin() {
|
|
11424
|
-
const pkgsBin =
|
|
11425
|
-
if (
|
|
11535
|
+
const pkgsBin = join23(getPackagesDir(), "node_modules", ".bin", "pm2");
|
|
11536
|
+
if (existsSync23(pkgsBin))
|
|
11426
11537
|
return pkgsBin;
|
|
11427
11538
|
let dir = process.cwd();
|
|
11428
11539
|
while (dir !== dirname7(dir)) {
|
|
11429
|
-
const candidate =
|
|
11430
|
-
if (
|
|
11540
|
+
const candidate = join23(dir, "node_modules", ".bin", "pm2");
|
|
11541
|
+
if (existsSync23(candidate))
|
|
11431
11542
|
return candidate;
|
|
11432
11543
|
dir = dirname7(dir);
|
|
11433
11544
|
}
|
|
@@ -11623,13 +11734,13 @@ __export(exports_plan, {
|
|
|
11623
11734
|
parsePlanArgs: () => parsePlanArgs
|
|
11624
11735
|
});
|
|
11625
11736
|
import {
|
|
11626
|
-
existsSync as
|
|
11737
|
+
existsSync as existsSync24,
|
|
11627
11738
|
mkdirSync as mkdirSync15,
|
|
11628
11739
|
readdirSync as readdirSync8,
|
|
11629
11740
|
readFileSync as readFileSync13,
|
|
11630
11741
|
writeFileSync as writeFileSync10
|
|
11631
11742
|
} from "node:fs";
|
|
11632
|
-
import { join as
|
|
11743
|
+
import { join as join24 } from "node:path";
|
|
11633
11744
|
function printHelp2() {
|
|
11634
11745
|
process.stderr.write(`
|
|
11635
11746
|
${bold2("locus plan")} — AI-powered sprint planning
|
|
@@ -11661,11 +11772,11 @@ function normalizeSprintName(name) {
|
|
|
11661
11772
|
return name.trim().toLowerCase();
|
|
11662
11773
|
}
|
|
11663
11774
|
function getPlansDir(projectRoot) {
|
|
11664
|
-
return
|
|
11775
|
+
return join24(projectRoot, ".locus", "plans");
|
|
11665
11776
|
}
|
|
11666
11777
|
function ensurePlansDir(projectRoot) {
|
|
11667
11778
|
const dir = getPlansDir(projectRoot);
|
|
11668
|
-
if (!
|
|
11779
|
+
if (!existsSync24(dir)) {
|
|
11669
11780
|
mkdirSync15(dir, { recursive: true });
|
|
11670
11781
|
}
|
|
11671
11782
|
return dir;
|
|
@@ -11675,14 +11786,14 @@ function generateId() {
|
|
|
11675
11786
|
}
|
|
11676
11787
|
function loadPlanFile(projectRoot, id) {
|
|
11677
11788
|
const dir = getPlansDir(projectRoot);
|
|
11678
|
-
if (!
|
|
11789
|
+
if (!existsSync24(dir))
|
|
11679
11790
|
return null;
|
|
11680
11791
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
|
|
11681
11792
|
const match = files.find((f) => f.startsWith(id));
|
|
11682
11793
|
if (!match)
|
|
11683
11794
|
return null;
|
|
11684
11795
|
try {
|
|
11685
|
-
const content = readFileSync13(
|
|
11796
|
+
const content = readFileSync13(join24(dir, match), "utf-8");
|
|
11686
11797
|
return JSON.parse(content);
|
|
11687
11798
|
} catch {
|
|
11688
11799
|
return null;
|
|
@@ -11749,7 +11860,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
11749
11860
|
}
|
|
11750
11861
|
function handleListPlans(projectRoot) {
|
|
11751
11862
|
const dir = getPlansDir(projectRoot);
|
|
11752
|
-
if (!
|
|
11863
|
+
if (!existsSync24(dir)) {
|
|
11753
11864
|
process.stderr.write(`${dim2("No saved plans yet.")}
|
|
11754
11865
|
`);
|
|
11755
11866
|
return;
|
|
@@ -11767,7 +11878,7 @@ ${bold2("Saved Plans:")}
|
|
|
11767
11878
|
for (const file of files) {
|
|
11768
11879
|
const id = file.replace(".json", "");
|
|
11769
11880
|
try {
|
|
11770
|
-
const content = readFileSync13(
|
|
11881
|
+
const content = readFileSync13(join24(dir, file), "utf-8");
|
|
11771
11882
|
const plan = JSON.parse(content);
|
|
11772
11883
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
11773
11884
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -11835,7 +11946,7 @@ async function handleRefinePlan(projectRoot, id, feedback, flags) {
|
|
|
11835
11946
|
return;
|
|
11836
11947
|
}
|
|
11837
11948
|
const config = loadConfig(projectRoot);
|
|
11838
|
-
const planPath =
|
|
11949
|
+
const planPath = join24(getPlansDir(projectRoot), `${plan.id}.json`);
|
|
11839
11950
|
const planPathRelative = `.locus/plans/${plan.id}.json`;
|
|
11840
11951
|
process.stderr.write(`
|
|
11841
11952
|
${bold2("Refining plan:")} ${cyan2(plan.directive)}
|
|
@@ -11874,7 +11985,7 @@ ${red2("✗")} Refinement failed: ${aiResult.error}
|
|
|
11874
11985
|
`);
|
|
11875
11986
|
return;
|
|
11876
11987
|
}
|
|
11877
|
-
if (!
|
|
11988
|
+
if (!existsSync24(planPath)) {
|
|
11878
11989
|
process.stderr.write(`
|
|
11879
11990
|
${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
11880
11991
|
`);
|
|
@@ -11972,7 +12083,7 @@ ${bold2("Approving plan:")}
|
|
|
11972
12083
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
11973
12084
|
const id = generateId();
|
|
11974
12085
|
const plansDir = ensurePlansDir(projectRoot);
|
|
11975
|
-
const planPath =
|
|
12086
|
+
const planPath = join24(plansDir, `${id}.json`);
|
|
11976
12087
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
11977
12088
|
const displayDirective = directive;
|
|
11978
12089
|
process.stderr.write(`
|
|
@@ -12015,7 +12126,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
|
|
|
12015
12126
|
`);
|
|
12016
12127
|
return;
|
|
12017
12128
|
}
|
|
12018
|
-
if (!
|
|
12129
|
+
if (!existsSync24(planPath)) {
|
|
12019
12130
|
process.stderr.write(`
|
|
12020
12131
|
${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
12021
12132
|
`);
|
|
@@ -12197,15 +12308,15 @@ ${directive}${sprintName ? `
|
|
|
12197
12308
|
|
|
12198
12309
|
**Sprint:** ${sprintName}` : ""}
|
|
12199
12310
|
</directive>`);
|
|
12200
|
-
const locusPath =
|
|
12201
|
-
if (
|
|
12311
|
+
const locusPath = join24(projectRoot, ".locus", "LOCUS.md");
|
|
12312
|
+
if (existsSync24(locusPath)) {
|
|
12202
12313
|
const content = readFileSync13(locusPath, "utf-8");
|
|
12203
12314
|
parts.push(`<project-context>
|
|
12204
12315
|
${content.slice(0, 3000)}
|
|
12205
12316
|
</project-context>`);
|
|
12206
12317
|
}
|
|
12207
|
-
const learningsPath =
|
|
12208
|
-
if (
|
|
12318
|
+
const learningsPath = join24(projectRoot, ".locus", "LEARNINGS.md");
|
|
12319
|
+
if (existsSync24(learningsPath)) {
|
|
12209
12320
|
const content = readFileSync13(learningsPath, "utf-8");
|
|
12210
12321
|
parts.push(`<past-learnings>
|
|
12211
12322
|
${content.slice(0, 2000)}
|
|
@@ -12258,8 +12369,8 @@ ${JSON.stringify(plan, null, 2)}
|
|
|
12258
12369
|
parts.push(`<feedback>
|
|
12259
12370
|
${feedback}
|
|
12260
12371
|
</feedback>`);
|
|
12261
|
-
const locusPath =
|
|
12262
|
-
if (
|
|
12372
|
+
const locusPath = join24(projectRoot, ".locus", "LOCUS.md");
|
|
12373
|
+
if (existsSync24(locusPath)) {
|
|
12263
12374
|
const content = readFileSync13(locusPath, "utf-8");
|
|
12264
12375
|
parts.push(`<project-context>
|
|
12265
12376
|
${content.slice(0, 3000)}
|
|
@@ -12442,8 +12553,8 @@ __export(exports_review, {
|
|
|
12442
12553
|
reviewCommand: () => reviewCommand
|
|
12443
12554
|
});
|
|
12444
12555
|
import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
|
|
12445
|
-
import { existsSync as
|
|
12446
|
-
import { join as
|
|
12556
|
+
import { existsSync as existsSync25, readFileSync as readFileSync14 } from "node:fs";
|
|
12557
|
+
import { join as join25 } from "node:path";
|
|
12447
12558
|
function printHelp3() {
|
|
12448
12559
|
process.stderr.write(`
|
|
12449
12560
|
${bold2("locus review")} — AI-powered code review
|
|
@@ -12612,8 +12723,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
12612
12723
|
parts.push(`<role>
|
|
12613
12724
|
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
12614
12725
|
</role>`);
|
|
12615
|
-
const locusPath =
|
|
12616
|
-
if (
|
|
12726
|
+
const locusPath = join25(projectRoot, ".locus", "LOCUS.md");
|
|
12727
|
+
if (existsSync25(locusPath)) {
|
|
12617
12728
|
const content = readFileSync14(locusPath, "utf-8");
|
|
12618
12729
|
parts.push(`<project-context>
|
|
12619
12730
|
${content.slice(0, 2000)}
|
|
@@ -12789,13 +12900,8 @@ ${bold2("Finding PR for issue")} ${cyan2(`#${issueNumber}`)}...
|
|
|
12789
12900
|
return handleSinglePR(projectRoot, config, prNumber, flags);
|
|
12790
12901
|
}
|
|
12791
12902
|
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
12903
|
process.stderr.write(`
|
|
12798
|
-
${bold2("Iterating on sprint
|
|
12904
|
+
${bold2("Iterating on sprint PRs...")}
|
|
12799
12905
|
|
|
12800
12906
|
`);
|
|
12801
12907
|
const prs = listPRs({ label: "agent:managed", state: "open" }, { cwd: projectRoot });
|
|
@@ -12934,14 +13040,14 @@ __export(exports_discuss, {
|
|
|
12934
13040
|
discussCommand: () => discussCommand
|
|
12935
13041
|
});
|
|
12936
13042
|
import {
|
|
12937
|
-
existsSync as
|
|
13043
|
+
existsSync as existsSync26,
|
|
12938
13044
|
mkdirSync as mkdirSync16,
|
|
12939
13045
|
readdirSync as readdirSync9,
|
|
12940
13046
|
readFileSync as readFileSync15,
|
|
12941
13047
|
unlinkSync as unlinkSync6,
|
|
12942
13048
|
writeFileSync as writeFileSync11
|
|
12943
13049
|
} from "node:fs";
|
|
12944
|
-
import { join as
|
|
13050
|
+
import { join as join26 } from "node:path";
|
|
12945
13051
|
function printHelp5() {
|
|
12946
13052
|
process.stderr.write(`
|
|
12947
13053
|
${bold2("locus discuss")} — AI-powered architectural discussions
|
|
@@ -12963,11 +13069,11 @@ ${bold2("Examples:")}
|
|
|
12963
13069
|
`);
|
|
12964
13070
|
}
|
|
12965
13071
|
function getDiscussionsDir(projectRoot) {
|
|
12966
|
-
return
|
|
13072
|
+
return join26(projectRoot, ".locus", "discussions");
|
|
12967
13073
|
}
|
|
12968
13074
|
function ensureDiscussionsDir(projectRoot) {
|
|
12969
13075
|
const dir = getDiscussionsDir(projectRoot);
|
|
12970
|
-
if (!
|
|
13076
|
+
if (!existsSync26(dir)) {
|
|
12971
13077
|
mkdirSync16(dir, { recursive: true });
|
|
12972
13078
|
}
|
|
12973
13079
|
return dir;
|
|
@@ -13002,7 +13108,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
13002
13108
|
}
|
|
13003
13109
|
function listDiscussions(projectRoot) {
|
|
13004
13110
|
const dir = getDiscussionsDir(projectRoot);
|
|
13005
|
-
if (!
|
|
13111
|
+
if (!existsSync26(dir)) {
|
|
13006
13112
|
process.stderr.write(`${dim2("No discussions yet.")}
|
|
13007
13113
|
`);
|
|
13008
13114
|
return;
|
|
@@ -13019,7 +13125,7 @@ ${bold2("Discussions:")}
|
|
|
13019
13125
|
`);
|
|
13020
13126
|
for (const file of files) {
|
|
13021
13127
|
const id = file.replace(".md", "");
|
|
13022
|
-
const content = readFileSync15(
|
|
13128
|
+
const content = readFileSync15(join26(dir, file), "utf-8");
|
|
13023
13129
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
13024
13130
|
const title = titleMatch ? titleMatch[1] : id;
|
|
13025
13131
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -13037,7 +13143,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13037
13143
|
return;
|
|
13038
13144
|
}
|
|
13039
13145
|
const dir = getDiscussionsDir(projectRoot);
|
|
13040
|
-
if (!
|
|
13146
|
+
if (!existsSync26(dir)) {
|
|
13041
13147
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13042
13148
|
`);
|
|
13043
13149
|
return;
|
|
@@ -13049,7 +13155,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13049
13155
|
`);
|
|
13050
13156
|
return;
|
|
13051
13157
|
}
|
|
13052
|
-
const content = readFileSync15(
|
|
13158
|
+
const content = readFileSync15(join26(dir, match), "utf-8");
|
|
13053
13159
|
process.stdout.write(`${content}
|
|
13054
13160
|
`);
|
|
13055
13161
|
}
|
|
@@ -13060,7 +13166,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13060
13166
|
return;
|
|
13061
13167
|
}
|
|
13062
13168
|
const dir = getDiscussionsDir(projectRoot);
|
|
13063
|
-
if (!
|
|
13169
|
+
if (!existsSync26(dir)) {
|
|
13064
13170
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13065
13171
|
`);
|
|
13066
13172
|
return;
|
|
@@ -13072,7 +13178,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13072
13178
|
`);
|
|
13073
13179
|
return;
|
|
13074
13180
|
}
|
|
13075
|
-
unlinkSync6(
|
|
13181
|
+
unlinkSync6(join26(dir, match));
|
|
13076
13182
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
13077
13183
|
`);
|
|
13078
13184
|
}
|
|
@@ -13085,7 +13191,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
13085
13191
|
return;
|
|
13086
13192
|
}
|
|
13087
13193
|
const dir = getDiscussionsDir(projectRoot);
|
|
13088
|
-
if (!
|
|
13194
|
+
if (!existsSync26(dir)) {
|
|
13089
13195
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13090
13196
|
`);
|
|
13091
13197
|
return;
|
|
@@ -13097,7 +13203,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
13097
13203
|
`);
|
|
13098
13204
|
return;
|
|
13099
13205
|
}
|
|
13100
|
-
const content = readFileSync15(
|
|
13206
|
+
const content = readFileSync15(join26(dir, match), "utf-8");
|
|
13101
13207
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
13102
13208
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
13103
13209
|
await planCommand(projectRoot, [
|
|
@@ -13223,7 +13329,7 @@ ${turn.content}`;
|
|
|
13223
13329
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
13224
13330
|
].join(`
|
|
13225
13331
|
`);
|
|
13226
|
-
writeFileSync11(
|
|
13332
|
+
writeFileSync11(join26(dir, `${id}.md`), markdown, "utf-8");
|
|
13227
13333
|
process.stderr.write(`
|
|
13228
13334
|
${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
13229
13335
|
`);
|
|
@@ -13238,15 +13344,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
13238
13344
|
parts.push(`<role>
|
|
13239
13345
|
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
13240
13346
|
</role>`);
|
|
13241
|
-
const locusPath =
|
|
13242
|
-
if (
|
|
13347
|
+
const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
|
|
13348
|
+
if (existsSync26(locusPath)) {
|
|
13243
13349
|
const content = readFileSync15(locusPath, "utf-8");
|
|
13244
13350
|
parts.push(`<project-context>
|
|
13245
13351
|
${content.slice(0, 3000)}
|
|
13246
13352
|
</project-context>`);
|
|
13247
13353
|
}
|
|
13248
|
-
const learningsPath =
|
|
13249
|
-
if (
|
|
13354
|
+
const learningsPath = join26(projectRoot, ".locus", "LEARNINGS.md");
|
|
13355
|
+
if (existsSync26(learningsPath)) {
|
|
13250
13356
|
const content = readFileSync15(learningsPath, "utf-8");
|
|
13251
13357
|
parts.push(`<past-learnings>
|
|
13252
13358
|
${content.slice(0, 2000)}
|
|
@@ -13318,8 +13424,8 @@ __export(exports_artifacts, {
|
|
|
13318
13424
|
formatDate: () => formatDate2,
|
|
13319
13425
|
artifactsCommand: () => artifactsCommand
|
|
13320
13426
|
});
|
|
13321
|
-
import { existsSync as
|
|
13322
|
-
import { join as
|
|
13427
|
+
import { existsSync as existsSync27, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
|
|
13428
|
+
import { join as join27 } from "node:path";
|
|
13323
13429
|
function printHelp6() {
|
|
13324
13430
|
process.stderr.write(`
|
|
13325
13431
|
${bold2("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -13339,14 +13445,14 @@ ${dim2("Artifact names support partial matching.")}
|
|
|
13339
13445
|
`);
|
|
13340
13446
|
}
|
|
13341
13447
|
function getArtifactsDir(projectRoot) {
|
|
13342
|
-
return
|
|
13448
|
+
return join27(projectRoot, ".locus", "artifacts");
|
|
13343
13449
|
}
|
|
13344
13450
|
function listArtifacts(projectRoot) {
|
|
13345
13451
|
const dir = getArtifactsDir(projectRoot);
|
|
13346
|
-
if (!
|
|
13452
|
+
if (!existsSync27(dir))
|
|
13347
13453
|
return [];
|
|
13348
13454
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
13349
|
-
const filePath =
|
|
13455
|
+
const filePath = join27(dir, fileName);
|
|
13350
13456
|
const stat = statSync5(filePath);
|
|
13351
13457
|
return {
|
|
13352
13458
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -13359,8 +13465,8 @@ function listArtifacts(projectRoot) {
|
|
|
13359
13465
|
function readArtifact(projectRoot, name) {
|
|
13360
13466
|
const dir = getArtifactsDir(projectRoot);
|
|
13361
13467
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
13362
|
-
const filePath =
|
|
13363
|
-
if (!
|
|
13468
|
+
const filePath = join27(dir, fileName);
|
|
13469
|
+
if (!existsSync27(filePath))
|
|
13364
13470
|
return null;
|
|
13365
13471
|
const stat = statSync5(filePath);
|
|
13366
13472
|
return {
|
|
@@ -13721,8 +13827,8 @@ __export(exports_sandbox2, {
|
|
|
13721
13827
|
});
|
|
13722
13828
|
import { execSync as execSync22, spawn as spawn7 } from "node:child_process";
|
|
13723
13829
|
import { createHash } from "node:crypto";
|
|
13724
|
-
import { existsSync as
|
|
13725
|
-
import { basename as basename4, join as
|
|
13830
|
+
import { existsSync as existsSync28, readFileSync as readFileSync17 } from "node:fs";
|
|
13831
|
+
import { basename as basename4, join as join28 } from "node:path";
|
|
13726
13832
|
import { createInterface as createInterface3 } from "node:readline";
|
|
13727
13833
|
function printSandboxHelp() {
|
|
13728
13834
|
process.stderr.write(`
|
|
@@ -14252,7 +14358,7 @@ async function handleLogs(projectRoot, args) {
|
|
|
14252
14358
|
}
|
|
14253
14359
|
function detectPackageManager2(projectRoot) {
|
|
14254
14360
|
try {
|
|
14255
|
-
const raw = readFileSync17(
|
|
14361
|
+
const raw = readFileSync17(join28(projectRoot, "package.json"), "utf-8");
|
|
14256
14362
|
const pkgJson = JSON.parse(raw);
|
|
14257
14363
|
if (typeof pkgJson.packageManager === "string") {
|
|
14258
14364
|
const name = pkgJson.packageManager.split("@")[0];
|
|
@@ -14261,13 +14367,13 @@ function detectPackageManager2(projectRoot) {
|
|
|
14261
14367
|
}
|
|
14262
14368
|
}
|
|
14263
14369
|
} catch {}
|
|
14264
|
-
if (
|
|
14370
|
+
if (existsSync28(join28(projectRoot, "bun.lock")) || existsSync28(join28(projectRoot, "bun.lockb"))) {
|
|
14265
14371
|
return "bun";
|
|
14266
14372
|
}
|
|
14267
|
-
if (
|
|
14373
|
+
if (existsSync28(join28(projectRoot, "yarn.lock"))) {
|
|
14268
14374
|
return "yarn";
|
|
14269
14375
|
}
|
|
14270
|
-
if (
|
|
14376
|
+
if (existsSync28(join28(projectRoot, "pnpm-lock.yaml"))) {
|
|
14271
14377
|
return "pnpm";
|
|
14272
14378
|
}
|
|
14273
14379
|
return "npm";
|
|
@@ -14370,9 +14476,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
|
|
|
14370
14476
|
${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
14371
14477
|
`);
|
|
14372
14478
|
}
|
|
14373
|
-
const setupScript =
|
|
14374
|
-
const containerSetupScript = containerWorkdir ?
|
|
14375
|
-
if (
|
|
14479
|
+
const setupScript = join28(projectRoot, ".locus", "sandbox-setup.sh");
|
|
14480
|
+
const containerSetupScript = containerWorkdir ? join28(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
|
|
14481
|
+
if (existsSync28(setupScript)) {
|
|
14376
14482
|
process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
|
|
14377
14483
|
`);
|
|
14378
14484
|
const hookOk = await runInteractiveCommand("docker", [
|
|
@@ -14540,13 +14646,13 @@ init_context();
|
|
|
14540
14646
|
init_logger();
|
|
14541
14647
|
init_rate_limiter();
|
|
14542
14648
|
init_terminal();
|
|
14543
|
-
import { existsSync as
|
|
14544
|
-
import { join as
|
|
14649
|
+
import { existsSync as existsSync29, readFileSync as readFileSync18 } from "node:fs";
|
|
14650
|
+
import { join as join29 } from "node:path";
|
|
14545
14651
|
import { fileURLToPath } from "node:url";
|
|
14546
14652
|
function getCliVersion() {
|
|
14547
14653
|
const fallbackVersion = "0.0.0";
|
|
14548
|
-
const packageJsonPath =
|
|
14549
|
-
if (!
|
|
14654
|
+
const packageJsonPath = join29(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
14655
|
+
if (!existsSync29(packageJsonPath)) {
|
|
14550
14656
|
return fallbackVersion;
|
|
14551
14657
|
}
|
|
14552
14658
|
try {
|
|
@@ -14660,6 +14766,9 @@ function parseArgs(argv) {
|
|
|
14660
14766
|
case "--no-sandbox":
|
|
14661
14767
|
flags.noSandbox = true;
|
|
14662
14768
|
break;
|
|
14769
|
+
case "--sprint":
|
|
14770
|
+
flags.sprint = rawArgs[++i];
|
|
14771
|
+
break;
|
|
14663
14772
|
default:
|
|
14664
14773
|
if (arg.startsWith("--sandbox=")) {
|
|
14665
14774
|
flags.sandbox = arg.slice("--sandbox=".length);
|
|
@@ -14823,7 +14932,7 @@ async function main() {
|
|
|
14823
14932
|
try {
|
|
14824
14933
|
const root = getGitRoot(cwd);
|
|
14825
14934
|
if (isInitialized(root)) {
|
|
14826
|
-
logDir =
|
|
14935
|
+
logDir = join29(root, ".locus", "logs");
|
|
14827
14936
|
getRateLimiter(root);
|
|
14828
14937
|
}
|
|
14829
14938
|
} catch {}
|
|
@@ -14963,7 +15072,8 @@ async function main() {
|
|
|
14963
15072
|
dryRun: parsed.flags.dryRun,
|
|
14964
15073
|
model: parsed.flags.model,
|
|
14965
15074
|
sandbox: parsed.flags.sandbox,
|
|
14966
|
-
noSandbox: parsed.flags.noSandbox
|
|
15075
|
+
noSandbox: parsed.flags.noSandbox,
|
|
15076
|
+
sprint: parsed.flags.sprint
|
|
14967
15077
|
});
|
|
14968
15078
|
break;
|
|
14969
15079
|
}
|