Repository navigation
[bug] project init from a repository subdirectory stores the subdirectory while pull delivers to the repo root #1338
Description
Summary
teamai init --scope project run from a subdirectory of a repository stores and prints that subdirectory as the workspace. Every later command anchors to the repository root instead (src/config.ts anchorProjectConfig, #374). So init announces paths nothing uses, and for tools that share .agents/skills (Muse, Codex) it leaves a stray teamai skill copy in the subdirectory, outside what pull and uninstall manage.
The same mismatch lets the test suite write into the contributor's checkout when TMPDIR is inside it, like #1258 did for git hooks.
Environment
- teamai-cli:
mainat 6c57c3b - scope: project, provider
git, macOS
Steps to reproduce
git init repo && mkdir -p repo/packages/web && cd repo/packages/web
teamai init https://git.example.test/team/t.git --scope project --role dev --agent muse --force
teamai pullActual
ℹ Scope: workspace (…/repo/packages/web)
config → …/repo/packages/web/.teamai/config.yaml
resources → …/repo/packages/web/.claude/skills # with --agent claude
repo/.agents/agents/reviewer.md # pull delivers here
repo/.agents/skills/demo/SKILL.md
repo/.agents/skills/teamai/SKILL.md
repo/packages/web/.agents/skills/teamai/SKILL.md # init's stray copy
config.yaml: projectRoot: …/repo/packages/web # every reader re-anchors it to repo/
Tests: with TMPDIR inside a checkout, npm run test:e2e -- muse-support leaves .agents/, .codex/agents/ and .mcp.json at the checkout root. The full e2e suite fails 38 files and writes .agents .trae .claude .codex .opencode .teamai .github/skills there. Sandboxes made with os.tmpdir() have no .git of their own, so git rev-parse --show-toplevel finds the contributor's checkout.
Expected
init resolves the workspace where pull does, prints and stores the repository root, and writes nothing into the subdirectory. Tests refuse to run with TMPDIR inside a checkout instead of writing into it.
Suggested fix
Two independent changes, each small:
-
init anchors where pull anchors.
resolveInitScopegets the git toplevel when the cwd is inside a repository (both call sites,initandinitHttp):({ scope, projectRoot, explicit, fallbackReason } = resolveInitScope( options.scope, - process.cwd(), + (await resolveAnchors(process.cwd()))?.workspaceRoot ?? process.cwd(), getUserHome(), ));Outside a repository nothing changes.
anchorProjectConfig's override stays: it is deliberate (Proposal: teamai 数据目录全局化 + 按项目分区(业务仓库工作区零残留) #374).Red test, e2e in
muse-support.test.ts:git inita repo, runinit --scope project --agent musefromrepo/packages/web, thenpull. ExpectScope: workspace (<repo>),repo/.agents/skills/teamai/SKILL.md, and nopackages/web/.agents. It fails onmainbecause init prints the subdirectory.Behavior change to name in the PR: when HOME is itself a git repository (a dotfiles repo),
initfrom a non-repo directory under HOME now resolves to HOME, so it hits the existing "Cannot use the workspace scope in your home directory" error or the fallback to user scope. Before, init succeeded and every later command anchored to HOME anyway. -
Tests fail closed on an in-tree TMPDIR. A vitest
globalSetupinvitest.config.tsandvitest.e2e.config.tswalks up fromos.tmpdir()and throws when it finds a.git:export default function requireTmpdirOutsideCheckout(): void { const tmp = os.tmpdir(); for (let dir = fs.realpathSync.native(tmp); ; dir = path.dirname(dir)) { if (fs.existsSync(path.join(dir, '.git'))) { throw new Error(`TMPDIR ${tmp} is inside the git checkout ${dir}, so tests would write project files into that checkout. Set TMPDIR to a directory outside any git checkout and run the tests again.`); } if (path.dirname(dir) === dir) return; } }
This covers every current and future test without editing the 38 files. Check:
TMPDIR=<checkout>/tmp npm run test:e2e -- muse-supportexits 1 with that message andgit status --porcelain --ignoredshows nothing new.
They can land as two PRs: the guard is tests-only; the init change needs a real-CLI record.
Activity
SaulMoro commentedon Oct 23, 2026
Checked against multi-project workspaces (#913), from the parent and from the children, on
mainand with the fix above applied. The workspace isws/;--role devat the workspace,--role opselsewhere.init --scope projectrun inmainprintsmainwrites tows/(git repo)wswsconfigwsws/svc-a/src(svc-aa nested repo, a child)ws/svc-a/srcsvc-aconfig; pull delivers tosvc-a/ws/svc-aws/backend(plain folder inside the git workspace)ws/backendopsandws/now getsopsskillswspws/(plain folder workspace)pwspws/.teamaipwspws/svc-b/src(svc-ba repo child)pws/svc-b/srcsvc-bconfigpws/svc-bpws/docs(plain child folder)pws/docspws/docs/.teamai, a child ofpwspws/docsSo:
From a child. A child's
initnever touches the parent's config, before or after the fix. From a child's subdirectory, the fix makes init print and store the child's root, which is where pull already delivers.From the parent. The workspace still finds every child, before and after the fix (
Workspace with 1 child: svc-a,Workspace with 2 children: docs, svc-b).One more case the fix must cover. In a workspace that is a git repo, only repos are children (Proposal: a workspace mode for features that span several repos #913).
initfrom a plain folder inside it already reconfigures the whole workspace onmainwhile printing the folder. With the fix it prints the right root, but it still rewrites the parent's selection,--forceor not. Suggested addition: when the resolved root differs from the cwd and that root already has a project config, stop and say so instead of overwriting:Red test:
git init ws,init --role devinws, theninit --role ops --forceinws/backend. Expect exit 1 andws's role stilldev.test(isolation): reject temporary directories inside Git checkouts (#…
fix(init): anchor workspace onboarding to repository roots
fix(init): anchor workspace onboarding to repository roots (#1344)
SaulMoro commentedon Oct 24, 2026
Both halves are on
mainnow. #1344 makes init anchor the workspace at the repository root, as pull does, so it stores and prints the root and writes nothing into the subdirectory. #1349 makes the tests refuse aTMPDIRinside a Git checkout. Closing.