Skip to content
Merged
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
3 changes: 3 additions & 0 deletions packages/autonav/src/cli/nav-memento.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Command } from "commander";
import chalk from "chalk";
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { runMementoLoop } from "../memento/index.js";
import { resolveAndCreateHarness } from "../harness/index.js";

Expand Down Expand Up @@ -162,6 +163,8 @@ async function executeMemento(
console.log(`${chalk.blue("Code:")} ${resolvedCodeDir}`);
console.log(`${chalk.blue("Navigator:")} ${resolvedNavDir}`);
console.log(`${chalk.blue("Task:")} ${task.substring(0, 80)}${task.length > 80 ? "..." : ""}`);
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
console.log(`${chalk.blue("Claude config:")} ${claudeConfigDir}`);
if (options.branch) {
console.log(`${chalk.blue("Branch:")} ${options.branch}`);
}
Expand Down
109 changes: 109 additions & 0 deletions packages/autonav/src/memento/git-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,112 @@ export function getRemoteUrl(options: GitOptions): string | null {
return null;
}
}

/**
* Detect the default branch (main or master).
*
* Tries `git symbolic-ref refs/remotes/origin/HEAD` first, then falls back
* to checking if `main` or `master` exist locally.
*/
export function getDefaultBranch(options: GitOptions): string {
// Try remote HEAD symbolic ref
try {
const ref = execGit("git symbolic-ref refs/remotes/origin/HEAD", { ...options, verbose: false });
// ref looks like "refs/remotes/origin/main"
const branch = ref.split("/").pop();
if (branch) return branch;
} catch {
// No remote HEAD — fall through to local check
}

// Check if main exists locally
try {
execGit("git rev-parse --verify main", { ...options, verbose: false });
return "main";
} catch {
// fall through
}

// Check if master exists locally
try {
execGit("git rev-parse --verify master", { ...options, verbose: false });
return "master";
} catch {
// fall through
}

// Default to main
return "main";
}

/**
* Create a git worktree checked out on a new branch forked from baseBranch.
*
* Runs: `git worktree add -b <branch> <worktreePath> <baseBranch>` from repoDir.
*/
export function createWorktree(
repoDir: string,
worktreePath: string,
branch: string,
baseBranch?: string
): void {
const base = baseBranch || getDefaultBranch({ cwd: repoDir });
execGit(
`git worktree add -b ${branch} "${worktreePath}" ${base}`,
{ cwd: repoDir }
);
}

/**
* Remove a git worktree and clean up.
*/
export function removeWorktree(repoDir: string, worktreePath: string): void {
try {
execGit(`git worktree remove "${worktreePath}" --force`, { cwd: repoDir });
} catch {
// If git worktree remove fails, try manual cleanup
}

// Clean up directory if it lingers
try {
const fs = require("node:fs");
if (fs.existsSync(worktreePath)) {
fs.rmSync(worktreePath, { recursive: true, force: true });
}
} catch {
// Best effort cleanup
}
}

/**
* Convert a plan summary into a git branch name.
*
* "feat: Add user authentication" → "feat/add-user-authentication"
* Lowercase, replace spaces/special chars with hyphens, truncate to ~50 chars.
*/
export function slugifyBranchName(summary: string): string {
return summary
.toLowerCase()
.replace(/:\s*/g, "/") // "feat: ..." → "feat/..."
.replace(/[^a-z0-9/]+/g, "-") // non-alphanumeric → hyphens
.replace(/-+/g, "-") // collapse multiple hyphens
.replace(/^-|-$/g, "") // trim leading/trailing hyphens
.replace(/\/-|-\//g, "/") // clean hyphens around slashes
.substring(0, 50)
.replace(/-$/, ""); // trim trailing hyphen after truncation
}

/**
* Get list of uncommitted files with their status indicators.
*
* Returns lines like: "M src/foo.ts", "?? new-file.ts", "A added.ts"
*/
export function getUncommittedFiles(options: GitOptions): string[] {
try {
const output = execGit("git status --porcelain", { ...options, verbose: false });
if (!output) return [];
return output.split("\n").filter(Boolean);
} catch {
return [];
}
}
9 changes: 9 additions & 0 deletions packages/autonav/src/memento/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ export {
getDiffStats,
getLastCommitDiffStats,
hasUncommittedChanges,
getUncommittedFiles,
stageAllChanges,
commitChanges,
pushBranch,
createPullRequest,
isGhAvailable,
getRemoteUrl,
getDefaultBranch,
createWorktree,
removeWorktree,
slugifyBranchName,
type DiffStats,
} from "./git-operations.js";

Expand All @@ -66,7 +71,11 @@ export {
buildNavSystemPrompt,
buildImplementerPrompt,
buildImplementerSystemPrompt,
buildReviewPrompt,
buildFixPrompt,
buildFixSystemPrompt,
type NavigatorIdentity,
type ReviewRound,
} from "./prompts.js";

// Implementer agent
Expand Down
Loading
Loading