Skip to content

fix(init): refuse to initialize single-repo team URL as standalone team repo (#1500) - #1509

Merged
SaulMoro merged 3 commits into
Tencent:mainfrom
hobostay:fix/init-single-repo-refusal-1500
Oct 28, 2026
Merged

SaulMoro merged 3 commits into
Tencent:mainfrom
hobostay:fix/init-single-repo-refusal-1500

Conversation

@hobostay

Copy link
Copy Markdown
Contributor

Summary

A single-repo team keeps its knowledge in the business repository under .teamai/ with mode: self in .teamai/teamai.yaml. When a member mistakenly runs teamai init <url> against that business repository's URL (instead of teamai init . / teamai init --self), init previously cloned it as a separate-mode team repo, failed to find a root teamai.yaml, generated a default standalone skeleton (teamai.yaml, skills/.gitkeep, rules/.gitkeep, docs/.gitkeep, env/.gitkeep, members/.gitkeep), and pushed it to the business repo's main branch.

This PR addresses #1500 (Part 2):

  • Detects single-repo team repositories in isSingleRepoTeam by checking whether the clone's default branch holds .teamai/teamai.yaml declaring mode: self while lacking a root teamai.yaml.
  • Refuses to initialize single-repo team URLs as standalone team repos in teamai init, immediately removing the clone, logging ✖ <url> is a single-repo team: its config is .teamai/teamai.yaml. Clone the repository and run \teamai init --self` in it.`, and exiting 1 before any user configuration, file write, or git push occurs.
  • Preserves existing behavior for empty repositories (gets default skeleton) and repositories with a root teamai.yaml (standalone team repo).
  • Documents the fix in CHANGELOG.md.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

  • npm run typecheck passes cleanly (0 errors)
  • npm run lint passes with 0 warnings and 0 errors across 1397 files
  • npm run build succeeds cleanly
  • Added unit tests in src/__tests__/init.test.ts:
    • isSingleRepoTeam verification across root teamai.yaml present, .teamai/teamai.yaml with mode: self, without mode: self, missing files, and malformed YAML.
    • init refusal path: verifies clone removal, error message, exit 1, and no git commit or push.
  • Added built-CLI E2E test in src/__tests__/e2e/init-single-repo-refusal.test.ts:
    • A1: verifies teamai init <url> against a bare single-repo repository exits 1 with the exact error message, leaves remote main branch commit untouched, creates no local config in ~/.teamai/config.yaml, and removes the cloned ~/.teamai/team-repo.
    • A2: verifies empty repository is not refused as a single-repo team.
    • A3: verifies standalone repository with root teamai.yaml is not refused.

Real CLI Verification

Executed built CLI (dist/index.js) against an isolated bare repository configured with .teamai/teamai.yaml (mode: self) on its main branch:

$ node dist/index.js init https://git.example.invalid/team/biz.git --provider git --agent claude --scope user --force
ℹ Initializing teamai...
ℹ Scope: user
ℹ   config    → ...\.teamai\config.yaml
ℹ   resources → .../.claude/skills, ...
ℹ Provider: git (--provider; auto-detection skipped)
ℹ Clone path: ...\.teamai\team-repo
- Checking Git identity...
√ Using Git identity Qiaochu-Hu
- Cloning team repo...
√ Team repo cloned
✖ https://git.example.invalid/team/biz.git is a single-repo team: its config is .teamai/teamai.yaml. Clone the repository and run `teamai init --self` in it.

Remote repository's default branch commit SHA remained unchanged, and no local config was created.

Related Issues

Part of #1500 (Part 2: init with a single-repo team URL)

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Preserve existing clones when refusing initialization — src/init.ts:2109. This guard also runs after the same-repository reuse path, not just after a fresh clone. If an existing ~/.teamai/team-repo contains .teamai/teamai.yaml with mode: self, no root config, and uncommitted or untracked work, rerunning init <url> deletes that work without confirmation. Remove the directory only when this invocation created it; leave reused clones intact.

The PR description includes a sufficient representative built-CLI verification record. No additional provider × agent coverage is required.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

@hobostay Thanks. With the built CLI, a fresh init <url> against a single-repo team now exits 1 with the issue's message in both user and workspace scope. The business repo's main is untouched and no config is left. An empty repo still gets the skeleton, a repo with a root teamai.yaml is unchanged, and following the message (init --self in a clone) sets the member up. One thing blocks approval:

  • [P1 blocking] The refusal deletes a clone it didn't create. This is the CI review's finding, and it reproduces. When ~/.teamai/team-repo is already a clone of the same URL, init takes the "Repo already exists … using existing clone" path, and the refusal then removes the whole directory. In our run that lost an untracked notes.md, an unpushed commit and an uncommitted edit, and ~/.teamai was left with only debug.log. Removing the directory only when this run cloned it, and otherwise refusing without touching it, keeps the fix. A test for the reuse path would cover it.

Non-blocking:

  • [P2 non-blocking] The mode: self check now exists three times. isSingleRepoTeam repeats readSelfModeMarker (src/bootstrap.ts:39) and holdsSelfKnowledge (src/migrate.ts). Exporting and reusing readSelfModeMarker would keep one definition.
  • [P2 non-blocking] init --force <single-repo url> from another team still moves the old config to config.yaml.previous and deletes its clone before refusing, so the member is left with no working install. Main does the same, so it isn't a regression of this PR.
  • [P3 nit] A workspace-scope refusal leaves an empty ~/.teamai/projects/<slug>/. The body's record shows only the run after the fix. One unit test asserts the internal remove call rather than the result.

The CI review accepted the body's test record, and we agree. tsc, lint and init.test.ts (81 tests, also with a symlinked TMPDIR) pass. The new e2e file passes 3/3 on this head, and A1 fails on main. CI e2e was still running when we checked. Not run here: the gitlab and github providers, and Windows.

@SaulMoro
SaulMoro self-requested a review October 28, 2026 08:37
@github-actions

Copy link
Copy Markdown

No new findings in the specified diff.

  • Resolved: The earlier clone-deletion finding is fixed: cleanup now runs only when this invocation created the clone. Regression tests cover preserving a reused clone and its uncommitted work.
  • Testing: The PR description includes a sufficient representative built-CLI verification record. No additional provider × agent coverage is required.

Review was read-only; no PR code was run, built, or installed.

@hobostay

Copy link
Copy Markdown
Contributor Author

@SaulMoro Thank you for the detailed and helpful review!

Both items have been addressed:

  1. [P1 blocking] Clone preservation on reuse:

    • In src/init.ts, clonedThisRun now records whether the clone directory existed before init cloned it (const clonedThisRun = !await pathExists(localPath);).
    • If the repository was an existing reused clone,
      emove(localPath) is skipped upon refusal, preserving all untracked files, unpushed commits, and working tree edits.
    • Added unit test: preserves an existing reused clone when refusing a single-repo team in src/tests/init.test.ts.
    • Added E2E test: A1 (preservation): preserves an existing reused clone and its files when refusing a single-repo team in src/tests/e2e/init-single-repo-refusal.test.ts.
  2. [P2 non-blocking] Deduplicating mode: self check:

    • Exported
      eadSelfModeMarker from src/bootstrap.ts.
    • Reused
      eadSelfModeMarker(repoDir) in isSingleRepoTeam within src/init.ts, eliminating duplicate YAML parsing logic.

All unit tests (82/82), E2E refusal tests (4/4),
pm run typecheck,
pm run lint, and
pm run build pass cleanly.

@github-actions

Copy link
Copy Markdown

No new findings in the specified diff.

  • Resolved: The earlier clone-deletion finding is fixed at src/init.ts:2106: cleanup only removes clones created during this invocation. Regression tests cover preserving a reused clone and its uncommitted work.
  • Testing: The PR description documents a sufficient representative built-CLI verification record. No additional provider × agent coverage is required.

Review was read-only; no PR code was run, built, or installed.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

@hobostay Thanks, approving b2eddd4a.

  • Reused clone. With the built CLI, an existing clone of the same URL holding an untracked notes.md, an unpushed commit and an uncommitted edit keeps all three after init <url> refuses with exit 1. The business repo's main is unchanged. A fresh clone is still removed, and no config is left.
  • One mode: self reader. isSingleRepoTeam now reuses readSelfModeMarker, with the same behavior and no import cycle.
  • Checks. On a merge with main (after the oxlint change in #1414), lint, tsc and init.test.ts (82 tests) pass. The PR's e2e file passes 4/4, including the new preservation test. CI is green at this head.

The CI review reported no new findings and called the clone deletion resolved. We agree.

Optional, for later:

  • [P3 nit] The new unit test queues mockResolvedValueOnce(true) on the fs-extra mock, but init reads pathExists from utils/fs.js. Those lines do nothing, and the as any can go with them.
  • [P3 nit] The body still says the refusal removes the clone "immediately", which now holds only for a clone this run created.
  • Not a regression. init --force from another team's install to a single-repo URL still moves the old config aside before refusing. Main does the same.

@SaulMoro
SaulMoro merged commit 564b1a3 into Tencent:main Oct 28, 2026
18 checks passed
Walternater pushed a commit to Walternater/teamai-cli that referenced this pull request Nov 2, 2026
…e team repo (Tencent#1509)

teamai init <url> against a repository whose default branch holds .teamai/teamai.yaml with mode: self and no root teamai.yaml now exits 1 with a message pointing to teamai init --self, instead of writing a root teamai.yaml and pushing a skeleton to the business repository's main. A clone this run created is removed; a reused clone is left untouched.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants