diff --git a/src/features/team-mode/team-registry/paths.test.ts b/src/features/team-mode/team-registry/paths.test.ts
new file mode 100644
index 000000000..c2262eed1
--- /dev/null
+++ b/src/features/team-mode/team-registry/paths.test.ts
@@ -0,0 +1,119 @@
+///
+
+import { afterEach, describe, expect, mock, test } from "bun:test"
+import { mkdtemp, mkdir, rm, stat, writeFile } from "node:fs/promises"
+import { homedir, tmpdir } from "node:os"
+import path from "node:path"
+import { randomUUID } from "node:crypto"
+
+import { TeamModeConfigSchema } from "../../../config/schema/team-mode"
+
+const logCalls: Array<[string, unknown?]> = []
+
+mock.module("../../../shared/logger", () => ({
+ log: (message: string, data?: unknown) => {
+ logCalls.push([message, data])
+ },
+}))
+
+const { discoverTeamSpecs, ensureBaseDirs, resolveBaseDir } = await import("./paths")
+
+async function createTemporaryRoot(): Promise {
+ return await mkdtemp(path.join(tmpdir(), "team-mode-paths-"))
+}
+
+describe("paths", () => {
+ const temporaryDirectories: string[] = []
+
+ afterEach(async () => {
+ logCalls.splice(0)
+ await Promise.all(temporaryDirectories.splice(0).map(async (directoryPath) => {
+ await rm(directoryPath, { recursive: true, force: true })
+ }))
+ })
+
+ test("resolveBaseDir defaults to ~/.omo", () => {
+ // given
+ const config = TeamModeConfigSchema.parse({ base_dir: undefined })
+
+ // when
+ const resolvedBaseDir = resolveBaseDir(config)
+
+ // then
+ expect(resolvedBaseDir).toBe(path.join(homedir(), ".omo"))
+ })
+
+ test("resolveBaseDir honors override", () => {
+ // given
+ const config = TeamModeConfigSchema.parse({ base_dir: "/tmp/test-abc" })
+
+ // when
+ const resolvedBaseDir = resolveBaseDir(config)
+
+ // then
+ expect(resolvedBaseDir).toBe("/tmp/test-abc")
+ })
+
+ test("discoverTeamSpecs prefers project scope", async () => {
+ // given
+ const rootDirectory = await createTemporaryRoot()
+ temporaryDirectories.push(rootDirectory)
+
+ const projectRoot = path.join(rootDirectory, "project")
+ const userBaseDir = path.join(rootDirectory, "home", ".omo")
+ const projectTeamDir = path.join(projectRoot, ".omo", "teams", "foo")
+ const userTeamDir = path.join(userBaseDir, "teams", "foo")
+
+ await mkdir(projectTeamDir, { recursive: true })
+ await mkdir(userTeamDir, { recursive: true })
+
+ await writeFile(path.join(projectTeamDir, "config.json"), "{}")
+ await writeFile(path.join(userTeamDir, "config.json"), "{}")
+
+ // when
+ const teamSpecs = await discoverTeamSpecs(TeamModeConfigSchema.parse({ base_dir: userBaseDir }), projectRoot)
+
+ // then
+ expect(teamSpecs).toEqual([
+ {
+ name: "foo",
+ scope: "project",
+ path: path.join(projectTeamDir, "config.json"),
+ },
+ ])
+ expect(logCalls).toEqual([
+ [
+ "team-spec collision",
+ {
+ event: "team-spec-collision",
+ teamName: "foo",
+ projectPath: path.join(projectTeamDir, "config.json"),
+ userPath: path.join(userTeamDir, "config.json"),
+ },
+ ],
+ ])
+ })
+
+ test("ensureBaseDirs creates all dirs with mode 0700", async () => {
+ // given
+ const baseDir = path.join(tmpdir(), `omo-test-${randomUUID()}`)
+
+ // when
+ await ensureBaseDirs(baseDir)
+ await ensureBaseDirs(baseDir)
+
+ // then
+ const directoryPaths = [
+ baseDir,
+ path.join(baseDir, "teams"),
+ path.join(baseDir, "runtime"),
+ path.join(baseDir, "worktrees"),
+ ]
+
+ for (const directoryPath of directoryPaths) {
+ const directoryStat = await stat(directoryPath)
+ expect(directoryStat.isDirectory()).toBe(true)
+ expect(directoryStat.mode & 0o777).toBe(0o700)
+ }
+ })
+})
diff --git a/src/features/team-mode/team-registry/paths.ts b/src/features/team-mode/team-registry/paths.ts
new file mode 100644
index 000000000..c80032575
--- /dev/null
+++ b/src/features/team-mode/team-registry/paths.ts
@@ -0,0 +1,122 @@
+import { mkdir, readdir, stat, chmod } from "node:fs/promises"
+import { homedir } from "node:os"
+import path from "node:path"
+
+import type { TeamModeConfig } from "../../../config/schema/team-mode"
+import { log } from "../../../shared/logger"
+
+type TeamSpecEntry = {
+ name: string
+ scope: "project" | "user"
+ path: string
+}
+
+function getTeamDirectory(baseDir: string, teamName: string, scope: "user" | "project", projectRoot?: string): string {
+ if (scope === "project") {
+ return path.join(projectRoot ?? "", ".omo", "teams", teamName)
+ }
+
+ return path.join(baseDir, "teams", teamName)
+}
+
+export function resolveBaseDir(config: TeamModeConfig): string {
+ return config.base_dir ?? path.join(homedir(), ".omo")
+}
+
+export function getTeamSpecPath(
+ baseDir: string,
+ teamName: string,
+ scope: "user" | "project",
+ projectRoot?: string,
+): string {
+ return path.join(getTeamDirectory(baseDir, teamName, scope, projectRoot), "config.json")
+}
+
+export function getRuntimeStateDir(baseDir: string, teamRunId: string): string {
+ return path.join(baseDir, "runtime", teamRunId)
+}
+
+export function getInboxDir(baseDir: string, teamRunId: string, memberName: string): string {
+ return path.join(baseDir, "runtime", teamRunId, "inboxes", memberName)
+}
+
+export function getTasksDir(baseDir: string, teamRunId: string): string {
+ return path.join(baseDir, "runtime", teamRunId, "tasks")
+}
+
+export function getWorktreeDir(baseDir: string, teamRunId: string, memberName: string): string {
+ return path.join(baseDir, "worktrees", teamRunId, memberName)
+}
+
+async function readTeamSpecDirectories(directoryPath: string, scope: "project" | "user"): Promise {
+ try {
+ const entries = await readdir(directoryPath, { withFileTypes: true })
+
+ return entries
+ .filter((entry) => entry.isDirectory())
+ .map((entry) => ({
+ name: entry.name,
+ scope,
+ path: path.resolve(directoryPath, entry.name, "config.json"),
+ }))
+ } catch {
+ return []
+ }
+}
+
+export async function discoverTeamSpecs(
+ config: TeamModeConfig,
+ projectRoot: string,
+): Promise> {
+ const baseDir = resolveBaseDir(config)
+ const projectTeamsDir = path.resolve(projectRoot, ".omo", "teams")
+ const userTeamsDir = path.resolve(baseDir, "teams")
+
+ const [projectTeamSpecs, userTeamSpecs] = await Promise.all([
+ readTeamSpecDirectories(projectTeamsDir, "project"),
+ readTeamSpecDirectories(userTeamsDir, "user"),
+ ])
+
+ const discoveredTeamSpecs: TeamSpecEntry[] = [...projectTeamSpecs]
+ const projectTeamNames = new Set(projectTeamSpecs.map((entry) => entry.name))
+
+ for (const userTeamSpec of userTeamSpecs) {
+ if (projectTeamNames.has(userTeamSpec.name)) {
+ const projectTeamSpec = projectTeamSpecs.find((entry) => entry.name === userTeamSpec.name)
+ if (projectTeamSpec) {
+ log("team-spec collision", {
+ event: "team-spec-collision",
+ teamName: userTeamSpec.name,
+ projectPath: projectTeamSpec.path,
+ userPath: userTeamSpec.path,
+ })
+ }
+ continue
+ }
+
+ discoveredTeamSpecs.push(userTeamSpec)
+ }
+
+ return discoveredTeamSpecs
+}
+
+export async function ensureBaseDirs(baseDir: string): Promise {
+ const directories = [
+ baseDir,
+ path.join(baseDir, "teams"),
+ path.join(baseDir, "runtime"),
+ path.join(baseDir, "worktrees"),
+ ]
+
+ for (const directoryPath of directories) {
+ await mkdir(directoryPath, { recursive: true, mode: 0o700 })
+ await chmod(directoryPath, 0o700)
+ }
+
+ await Promise.all(directories.map(async (directoryPath) => {
+ const directoryStat = await stat(directoryPath)
+ if ((directoryStat.mode & 0o777) !== 0o700) {
+ await chmod(directoryPath, 0o700)
+ }
+ }))
+}