Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/autonav/src/cli/nav-memento.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,11 @@ async function executeMemento(
console.log(`${chalk.blue("Branch:")} ${result.branch}`);
}

if (result.prUrl) {
console.log(`${chalk.blue("PR:")} ${result.prUrl}`);
if (result.prUrls && result.prUrls.length > 0) {
console.log(chalk.blue(`PRs (${result.prUrls.length}):`));
for (const url of result.prUrls) {
console.log(` ${url}`);
}
}

if (result.completionMessage) {
Expand Down
167 changes: 94 additions & 73 deletions packages/autonav/src/memento/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1451,19 +1451,33 @@ export async function runMementoLoop(
const defaultBranch = getDefaultBranch({ cwd: codeDirectory });
const worktreesBase = path.join(resolveConfigDir(), "worktrees");

// Track the effective branch and worktree for this run
let activeBranch: string | undefined = options.branch;
let worktreePath: string | null = null;
// Per-iteration PR URLs (one PR per iteration when --pr is used)
const prUrls: string[] = [];

// The working directory for the implementer — starts as codeDirectory,
// switches to worktree once created after the first plan
// Safety tracker: if an error interrupts mid-iteration, clean up this worktree
let currentIterWorktree: string | null = null;

// The working directory for the implementer — resets to codeDirectory each iteration,
// switches to a fresh worktree after the plan is received
let workDir = codeDirectory;

const errors: string[] = [];

// Graceful shutdown: finish current iteration's PR before exiting
let shuttingDown = false;
const sigintHandler = (): void => {
if (shuttingDown) {
process.stdout.write("\x1B[?25h");
process.exit(1);
}
shuttingDown = true;
console.log(chalk.yellow("\n\nGraceful shutdown after this iteration..."));
};
process.on("SIGINT", sigintHandler);

try {
// Main loop - only max-iterations stops the loop (0 = unlimited = run forever)
while (maxIterations === 0 || state.iteration < maxIterations) {
// Main loop - only max-iterations or SIGINT stops the loop (0 = unlimited = run forever)
while (!shuttingDown && (maxIterations === 0 || state.iteration < maxIterations)) {
state.iteration += 1;

if (verbose) {
Expand Down Expand Up @@ -1507,6 +1521,8 @@ export async function runMementoLoop(
let iterationTokens = 0;
let implementerTokens = 0;
let implementerSummary = "";
let iterBranch = "";
let iterWorktreePath = "";

try {
// Query navigator for plan (with rate limit retry)
Expand Down Expand Up @@ -1556,28 +1572,26 @@ export async function runMementoLoop(
// Record plan in memory (for PR body)
state.planHistory.push({ iteration: state.iteration, summary: plan.summary });

// Create worktree after first plan returns
if (state.iteration === 1 && !worktreePath) {
const branchName = activeBranch || slugifyBranchName(plan.summary);
activeBranch = branchName;
worktreePath = path.join(worktreesBase, branchName);

// Ensure worktrees base directory exists
fs.mkdirSync(worktreesBase, { recursive: true });
// Create per-iteration worktree forked from default branch
iterBranch = options.branch
? `${options.branch}-${state.iteration}`
: `memento/${state.iteration}-${slugifyBranchName(plan.summary)}`;
iterWorktreePath = path.join(worktreesBase, iterBranch);
fs.mkdirSync(worktreesBase, { recursive: true });

if (verbose) {
console.log(`[Memento] Creating worktree: ${worktreePath}`);
console.log(`[Memento] Branch: ${branchName} (from ${defaultBranch})`);
}
if (verbose) {
console.log(`[Memento] Creating worktree: ${iterWorktreePath}`);
console.log(`[Memento] Branch: ${iterBranch} (from ${defaultBranch})`);
}

createWorktree(codeDirectory, worktreePath, branchName, defaultBranch);
workDir = worktreePath;
createWorktree(codeDirectory, iterWorktreePath, iterBranch, defaultBranch);
workDir = iterWorktreePath;
currentIterWorktree = iterWorktreePath;

if (!verbose) {
animation.stop();
console.log(chalk.dim(` └─ Branch: ${branchName}`));
animation.start();
}
if (!verbose) {
animation.stop();
console.log(chalk.dim(` └─ Branch: ${iterBranch}`));
animation.start();
}

// Show what the implementer will be working on (truncate to ~60 chars)
Expand Down Expand Up @@ -1674,50 +1688,53 @@ export async function runMementoLoop(
isComplete: plan?.isComplete,
completionMessage: plan?.completionMessage,
});
}

// Loop exited - must have hit max iterations
console.log(`\nMax iterations (${maxIterations}) reached.`);

// Handle PR creation if requested
let prUrl: string | undefined;

if (pr && activeBranch) {
if (!isGhAvailable()) {
console.warn(
"\nWarning: gh CLI not available. Cannot create PR. Install and authenticate gh CLI."
);
} else {
console.log("\nCreating pull request...");

// Push branch (from worktree where commits live)
pushBranch(activeBranch, {
cwd: workDir,
verbose,
setUpstream: true,
});

// Create PR
const prBody = `## Summary

${state.completionMessage || task}

## Iterations

${state.planHistory.map((h) => `- **${h.iteration}**: ${h.summary}`).join("\n")}
// Per-iteration PR creation
if (pr && commitHash) {
if (isGhAvailable()) {
pushBranch(iterBranch, { cwd: workDir, verbose, setUpstream: true });
const prTitle = plan?.summary
? (plan.summary.length > 70 ? `${plan.summary.substring(0, 67)}...` : plan.summary)
: `memento iteration ${state.iteration}`;
const prBody = `## Summary\n\n${plan?.summary || task}\n\n---\n*Created by autonav memento loop (iteration ${state.iteration})*`;
const url = createPullRequest({
cwd: workDir,
verbose,
title: prTitle,
body: prBody,
});
prUrls.push(url);
console.log(`PR created: ${url}`);
} else {
console.warn("Warning: gh CLI not available. Skipping PR.");
}
}

---
*Created by autonav memento loop*`;
// Cleanup iteration worktree
try {
removeWorktree(codeDirectory, iterWorktreePath);
} catch (err) {
if (verbose) {
console.log(
chalk.yellow(
`[Memento] Worktree cleanup failed: ${err instanceof Error ? err.message : err}`
)
);
}
}
currentIterWorktree = null;
workDir = codeDirectory; // reset for next iteration
}

prUrl = createPullRequest({
cwd: workDir,
verbose,
title: task.length > 70 ? `${task.substring(0, 67)}...` : task,
body: prBody,
});
// Loop exited
if (shuttingDown) {
console.log(chalk.yellow("\nShutdown complete."));
} else if (maxIterations > 0) {
console.log(`\nMax iterations (${maxIterations}) reached.`);
}

console.log(`PR created: ${prUrl}`);
}
if (prUrls.length > 0) {
console.log(`\nCreated ${prUrls.length} PR(s).`);
}

const durationMs = Date.now() - startTime;
Expand All @@ -1726,8 +1743,8 @@ ${state.planHistory.map((h) => `- **${h.iteration}**: ${h.summary}`).join("\n")}
success: true, // Completed max iterations without fatal error
iterations: state.iteration,
completionMessage: state.completionMessage,
prUrl,
branch: activeBranch || getCurrentBranch({ cwd: workDir }),
prUrls: prUrls.length > 0 ? prUrls : undefined,
branch: prUrls.length > 0 ? "multiple" : getCurrentBranch({ cwd: codeDirectory }),
durationMs,
errors: errors.length > 0 ? errors : undefined,
};
Expand All @@ -1739,18 +1756,22 @@ ${state.planHistory.map((h) => `- **${h.iteration}**: ${h.summary}`).join("\n")}
return {
success: false,
iterations: state.iteration,
branch: activeBranch || getCurrentBranch({ cwd: workDir }),
prUrls: prUrls.length > 0 ? prUrls : undefined,
branch: getCurrentBranch({ cwd: codeDirectory }),
durationMs,
errors,
};
} finally {
// Clean up worktree on exit
if (worktreePath) {
// Remove SIGINT handler
process.removeListener("SIGINT", sigintHandler);

// Safety cleanup: if an error interrupted mid-iteration, clean up the worktree
if (currentIterWorktree) {
if (verbose) {
console.log(`[Memento] Cleaning up worktree: ${worktreePath}`);
console.log(`[Memento] Cleaning up interrupted worktree: ${currentIterWorktree}`);
}
try {
removeWorktree(codeDirectory, worktreePath);
removeWorktree(codeDirectory, currentIterWorktree);
} catch (err) {
if (verbose) {
console.log(
Expand Down
4 changes: 2 additions & 2 deletions packages/autonav/src/memento/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ export interface MementoResult {
/** Completion message from nav */
completionMessage?: string;

/** PR URL if --pr was used */
prUrl?: string;
/** PR URLs if --pr was used (one per iteration) */
prUrls?: string[];

/** Git branch name */
branch?: string;
Expand Down
Loading