Skip to content

[bug] project init from a repository subdirectory stores the subdirectory while pull delivers to the repo root #1338

Closed

Description

@SaulMoro

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: main at 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 pull

Actual

ℹ 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:

  1. init anchors where pull anchors. resolveInitScope gets the git toplevel when the cwd is inside a repository (both call sites, init and initHttp):

     ({ 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 init a repo, run init --scope project --agent muse from repo/packages/web, then pull. Expect Scope: workspace (<repo>), repo/.agents/skills/teamai/SKILL.md, and no packages/web/.agents. It fails on main because init prints the subdirectory.

    Behavior change to name in the PR: when HOME is itself a git repository (a dotfiles repo), init from 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.

  2. Tests fail closed on an in-tree TMPDIR. A vitest globalSetup in vitest.config.ts and vitest.e2e.config.ts walks up from os.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-support exits 1 with that message and git status --porcelain --ignored shows nothing new.

They can land as two PRs: the guard is tests-only; the init change needs a real-CLI record.

Activity

  1. SaulMoro commented on Oct 23, 2026

    @SaulMoro
    CollaboratorAuthor

    Checked against multi-project workspaces (#913), from the parent and from the children, on main and with the fix above applied. The workspace is ws/; --role dev at the workspace, --role ops elsewhere.

    init --scope project run in main prints main writes to with the fix prints with the fix writes to
    ws/ (git repo) ws ws config ws same
    ws/svc-a/src (svc-a a nested repo, a child) ws/svc-a/src svc-a config; pull delivers to svc-a/ ws/svc-a same
    ws/backend (plain folder inside the git workspace) ws/backend the workspace's own config: its role becomes ops and ws/ now gets ops skills ws same overwrite
    pws/ (plain folder workspace) pws pws/.teamai pws same
    pws/svc-b/src (svc-b a repo child) pws/svc-b/src svc-b config pws/svc-b same
    pws/docs (plain child folder) pws/docs pws/docs/.teamai, a child of pws pws/docs same

    So:

    • From a child. A child's init never 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). init from a plain folder inside it already reconfigures the whole workspace on main while printing the folder. With the fix it prints the right root, but it still rewrites the parent's selection, --force or 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:

      ✖ ws/backend is inside the repository ws, which already has a workspace config (role dev).
        A plain folder inside a repository cannot have its own config; only repositories are children of a workspace.
        Run `teamai init` in ws to change that config, or make ws/backend a repository of its own.
      

      Red test: git init ws, init --role dev in ws, then init --role ops --force in ws/backend. Expect exit 1 and ws's role still dev.

  2. SaulMoro commented on Oct 24, 2026

    @SaulMoro
    CollaboratorAuthor

    This was generated by AI during triage.

    Both halves are on main now. #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 a TMPDIR inside a Git checkout. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions