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
83 changes: 82 additions & 1 deletion packages/cli/__tests__/commands/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
generateExternalId,
getDefaultRuntime,
recordActivityEvent,
sanitizeProjectId,
type SessionManager,
} from "@aoagents/ao-core";

Expand Down Expand Up @@ -2462,7 +2463,7 @@ describe("start command — autoCreateConfig", () => {
process.env["AO_GLOBAL_CONFIG"]!,
[
"projects:",
` ${basename(tmpDir)}:`,
` ${sanitizeProjectId(basename(tmpDir))}:`,
` path: ${join(tmpDir, "other-repo")}`,
"",
].join("\n"),
Expand Down Expand Up @@ -3085,6 +3086,86 @@ describe("start command — path-based deduplication in addProjectToConfig", ()
else process.env["AO_CONFIG_PATH"] = origEnv;
}
});

it("sanitizes dotted directory names before adding a local project key", async () => {
const repoDir = join(tmpDir, "llama.cpp");
createFakeRepo(repoDir, "https://github.com/org/llama.cpp.git");

const configPath = join(tmpDir, "agent-orchestrator.yaml");
const { stringify: yamlStringify } = await import("yaml");
writeFileSync(
configPath,
yamlStringify(
{
defaults: {
runtime: "process",
agent: "claude-code",
workspace: "worktree",
notifiers: [],
},
projects: {
"my-app": {
name: "My App",
repo: "org/my-app",
path: join(tmpDir, "my-app"),
defaultBranch: "main",
sessionPrefix: "app",
},
},
},
{ indent: 2 },
),
);

const shell = await import("../../src/lib/shell.js");
vi.mocked(shell.git).mockImplementation(async (args: string[], workingDir?: string) => {
if (args[0] === "rev-parse" && args[1] === "--git-dir" && workingDir === repoDir) {
return ".git";
}
if (
args[0] === "remote" &&
args[1] === "get-url" &&
args[2] === "origin" &&
workingDir === repoDir
) {
return "https://github.com/org/llama.cpp.git";
}
if (args[0] === "symbolic-ref" && workingDir === repoDir) {
return "refs/remotes/origin/main";
}
if (args[0] === "rev-parse" && args[1] === "--verify" && workingDir === repoDir) {
return "abc";
}
return null;
});

const origEnv = process.env["AO_CONFIG_PATH"];
process.env["AO_CONFIG_PATH"] = configPath;

try {
await program.parseAsync([
"node",
"test",
"start",
repoDir,
"--no-dashboard",
"--no-orchestrator",
]);

const content = readFileSync(configPath, "utf-8");
const parsed = parseYaml(content) as { projects: Record<string, Record<string, unknown>> };
expect(parsed.projects["llama.cpp"]).toBeUndefined();
expect(parsed.projects["llama-cpp"]).toMatchObject({
name: "llama-cpp",
path: repoDir,
defaultBranch: "main",
sessionPrefix: "lc",
});
} finally {
if (origEnv === undefined) delete process.env["AO_CONFIG_PATH"];
else process.env["AO_CONFIG_PATH"] = origEnv;
}
});
});

describe("start command — global registry mutations", () => {
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
loadLocalProjectConfigDetailed,
recordActivityEvent,
registerProjectInGlobalConfig,
sanitizeProjectId,
getGlobalConfigPath,
type OrchestratorConfig,
type LocalProjectConfig,
Expand Down Expand Up @@ -141,14 +142,18 @@ function writeProjectBehaviorConfig(projectPath: string, config: LocalProjectCon
writeLocalProjectConfig(projectPath, config);
}

function deriveProjectIdFromPath(projectPath: string): string {
return sanitizeProjectId(basename(projectPath)) || "project";
}

/**
* Register a flat local config (agent-orchestrator.yaml without `projects:`)
* into the global config so loadConfig can resolve it.
* Returns the registered project ID, or null if registration failed.
*/
async function registerFlatConfig(configPath: string): Promise<string | null> {
const projectPath = resolve(dirname(configPath));
const projectId = basename(projectPath);
const projectId = deriveProjectIdFromPath(projectPath);

// Read flat config fields
const raw = readFileSync(configPath, "utf-8");
Expand Down Expand Up @@ -537,7 +542,7 @@ export async function autoCreateConfig(workingDir: string): Promise<Orchestrator
const agentRules = generateRulesFromTemplates(projectType);

// Build config with smart defaults
const projectId = basename(workingDir);
const projectId = deriveProjectIdFromPath(workingDir);
let repo: string | undefined = env.ownerRepo ?? undefined;
const path = workingDir;
const defaultBranch = env.defaultBranch || "main";
Expand Down Expand Up @@ -662,7 +667,7 @@ async function addProjectToConfig(

await ensureGit("adding projects");

let projectId = basename(resolvedPath);
let projectId = deriveProjectIdFromPath(resolvedPath);

// Avoid overwriting an existing project with the same directory name
if (config.projects[projectId]) {
Expand Down