Skip to content

feat(init): activate every project with --project all - #518

Open
Yi-111-a wants to merge 5 commits into
Tencent:mainfrom
Yi-111-a:init-project-all
Open

Yi-111-a wants to merge 5 commits into
Tencent:mainfrom
Yi-111-a:init-project-all

Conversation

@Yi-111-a

@Yi-111-a Yi-111-a commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements teamai init <repo> --project all from #509: the value expands to every id declared in manifest/projects.yaml and that snapshot is persisted into LocalConfig.projects, so a monorepo's onboarding keeps one line instead of a project list that has to be edited whenever the manifest changes.

Type of Change

  • New feature (non-breaking change that adds functionality)

Existing invocations are untouched: no flag → no projects, as before; an explicit id list → the same resolution as before.

Design framing (the note you asked for)

--project all is an explicit operator choice with snapshot semantics, not auto-activation:

  • The multi-project design deliberately never auto-activates a project, and nothing here changes that. A lone project is still not selected on its own; the active set changes only through an explicit flag or by re-running init.
  • It is a snapshot, not a live alias: config.yaml stores the resolved id list, which is consistent with the point in your reply that re-running init --project all is what re-resolves against the current manifest. Adding a project to the manifest later does not silently change any directory's active set.
  • The expansion feeds the same LocalConfig.projects as an explicit id list, so everything downstream is unchanged by construction: the member-roster append, pull's resource/learnings namespaces, recall scope, and contribute's landing point all see a plain list of real project ids.

The code comment says this out loud, so the next reader does not mistake the selector for an auto-activation back door.

Behaviour

all resolves inside resolveActiveProjects (src/init.ts), which already loads the manifest, validates ids and dedupes; the expansion is listProjectIds(manifest).

--project manifest result
all 3 projects [backend, frontend, gateway] — the manifest's own order
all 1 project that one project
all projects: [] no projects, plus a warning, exit 0
all no manifest error, exit 1 (the existing "no projects manifest" message)
all,backend any error — all already covers every declared project
backend 3 projects unchanged

Two deliberate calls, both easy to change if you disagree:

  • A mixed list is rejected. all subsumes the rest, so the other ids are redundant at best and a typo at worst — and silently accepting them would disarm the existing unknown-id check for exactly the invocation most likely to be scripted. The error names the offending list.
  • The reserved-selector collision. A project whose id is literally all is shadowed by the selector. It is never silently dropped — the expansion still covers it, and it is still selectable alone through teamai projects set all, which takes plain ids — but --project <...> can no longer express "only that project". A warning names the situation when the manifest is found to contain it. That is the unavoidable cost of reserving the word; the alternative (silently changing the meaning of all depending on the manifest) is worse for scripts.

all is matched case-sensitively, like every project id, so a project named All is unaffected.

Why a flag and not a new command

Per AGENTS.md's 奥卡姆剃刀: this extends the existing --project option, adds no command and no new state. teamai projects set --all is left out, as you asked.

Test Plan

  • npx tsc --noEmit passes
  • npx vitest run — no new failures vs main. The suite still exits non-zero on Windows because of a standing Windows-only failure pool (the CI matrix has no Windows runner); the set comparison below is the meaningful signal, not the exit code.
  • Added/updated tests for the change
  • src/__tests__/e2e/init-project-all.test.ts — the real-CLI leg for the command (not CI-gated, like every other file under e2e/): build, run init <repo> --project all against an offline team repo, read the ids back out of .teamai/config.yaml. On main it fails with Unknown project "all". Available projects: gamma, alpha, billing while its explicit-id control case passes, so the failing case is the new selector and not the harness.
# Step Result
1 npx tsc --noEmit clean
2 npm run build clean
3 npx vitest run src/__tests__/init-projects.test.ts 13 passed (new); all 13 fail on main, so each one is a genuine before/after
4 npx vitest run full suite, main vs this branch no new failures — see below
5 Real CLI on the built dist/index.js, 9 scenarios all as expected — see below
6 resolveActiveProjects exported, like the neighbouring resolveInitScope / resolveInitRepo makes the new behaviour unit-testable without a CLI harness

Step 4 — full suite, before vs after

The suite has a large, noisy Windows-only failure pool, so a comparison by count is meaningless here (details below). Comparing test sets instead:

run 1 run 2 run 3 stable (in every run)
this branch 83 failed 109 failed 110 failed 78
main (same tree, src/init.ts + src/index.ts reverted, new test file kept) 123 failed 122 failed 120
  • Tests that fail in all three branch runs but in neither baseline run: 0. No new failure is introduced.
  • All 13 new tests fail on main (13/13) and pass on the branch.
  • 29 tests fail in both baseline runs but not in all three branch runs (pull-exclude, pull-learnings-deletion, pull-tombstone, remove, rules, scope, issue-82-84-94-regression). These are not improvements — they are the flaky pool: they all share HOME / temp state, and they flip between runs on the unmodified tree too (e.g. run 1 of the branch had 3 failures that run 2 did not). This is why the table above reports sets, not counts.
  • The remainder are standing Windows-only failures. The repo's CI matrix is ubuntu-latest + macos-latest only, so nothing here is visible upstream; none of them are touched by this change and all of them are present on main.

Step 5 — real CLI

Nine scenarios against npm run build output. Each is a genuine
teamai init <url> --scope project --force --agent claude [--project …] in a
fresh directory, and config.yaml is read back afterwards:

############ 1. --project all on a 3-project manifest ############
### all-many   --project all   exit=0   config=yes
        - backend
        - frontend
        - gateway

############ 2. regression: an explicit id is unchanged ############
### one-explicit   --project backend   exit=0   config=yes
        - backend

############ 3. --project all on a one-project manifest ############
### all-one   --project all   exit=0   config=yes
        - solo

############ 4. no --project flag at all ############
### no-flag   --project <omitted>   exit=0   config=yes

############ 5. --project all on a manifest with zero projects ############
### all-none   --project all   exit=0   config=yes
      msg: --project "all" was given but manifest/projects.yaml declares no projects; nothing was activated.

############ 6. --project all on a repo with no manifest ############
### all-nomf   --project all   exit=1   config=MISSING
      msg: --project given but no projects manifest (manifest/projects.yaml) exists in the team repo.

############ 7. --project all where the manifest declares a project named 'all' ############
### all-shadow   --project all   exit=0   config=yes
        - all
        - backend
      msg: manifest/projects.yaml declares a project with the id "all", which is the reserved --project
           selector: every project is activated (that one included). To activate only it, run
           `teamai projects set all`.

############ 8. mixed list is rejected ############
### mixed   --project all,backend   exit=1   config=MISSING
      msg: --project "all" already covers every declared project; drop the other ids (got: all, backend).

############ 9. unknown id is still rejected ############
### unknown   --project bakcend   exit=1   config=MISSING
      msg: Unknown project "bakcend". Available projects: backend, frontend, gateway

The run used a local bare repo reached through a fake https:// URL rewritten by git's GIT_CONFIG_* override, so the real clone / identity / member-push path executed without touching the network or any developer's ~/.gitconfig.

Related Issues

Closes #509

Notes for Reviewers

One premise in the issue note is out of date. It says the teamai projects command family "isn't on main yet". It is: src/index.ts:298 registers projects with list / set [ids...] / members <id>, and set is wired to projectsSet (src/projects-cmd.ts:76). So projects set --all is blocked only by the missing flag, not by a missing command. I have still left it out of this PR as you asked — flagging it only so that call is made on current facts. This is also why the new warning points at teamai projects set all: that command exists today, and it is the escape hatch for the collision case.

Docs touched (bilingual kept in sync per AGENTS.md): docs/usage-guide.md and docs/usage-guide.zh-CN.md (the --project flag table and the multi-project key points), the --project help text in src/index.ts, and docs/designs/multi-project-management.md (the all snapshot under "Entry point", plus projects set --all added to the explicit out-of-scope list).

`--project all` expands to every id manifest/projects.yaml declares and
persists that snapshot, so a monorepo keeps one line in its onboarding docs
instead of a project list that has to be edited whenever the manifest
changes. `all` is reserved: a mixed list (`all,backend`) is rejected as
redundant or a typo, and a project whose id is literally `all` is still
covered by the expansion but selected on its own through
`teamai projects set all`, which takes plain ids.

It stays an explicit operator choice — activate everything, project-private
learnings included — rather than the auto-activation the multi-project design
rules out. Snapshot semantics also keep the existing rule that re-running
`init` is what re-resolves the active set.

Scope is `init` only; `teamai projects set --all` is deliberately not added.

Docs: the usage guide (en + zh-CN) flag table and multi-project section, the
CLI help text, and the multi-project design note.
The unit cases in `init-projects.test.ts` drive `resolveActiveProjects`
directly. This adds the end-to-end leg for the user-facing command: build the
CLI, run `init <repo> --project all` against a real team repo, and read the
ids back out of `.teamai/config.yaml`.

Offline, without weakening the test: the remote is a synthetic HTTPS URL that
git's `url.<base>.insteadOf` rewrites to a local bare repo. init rejects plain
HTTP outright, and a plain-HTTP static server (the first attempt) never gets
past the URL validator. The rewrite target is a filesystem path rather than
`file:///C:/…`, which on Git for Windows parses as `/C:/…` and fails the
clone.

Two cases: `all` expands to every manifest id in manifest order (the fixture
is deliberately not alphabetical, so an accidental sort is caught), and an
explicit id still replaces the previous selection instead of merging with it.

On `main` the first case fails with `Unknown project "all". Available
projects: gamma, alpha, billing` while the second passes — the case that
fails is the new selector, not the harness.
@jeff-r2026
jeff-r2026 self-requested a review September 14, 2026 11:11
@jeff-r2026

Copy link
Copy Markdown
Collaborator

Local review of c79f87438c03a37121d9e18778499f7de7223af0: I reproduced a case that should be addressed before merging.

[P2] Re-running init --project all does not pick up newly added remote projects when the existing clone is reused.

Reproduction using the built CLI and an isolated local bare Git remote:

  1. Initialize with init <repo> --scope project --role common --project all --force. The manifest contains gamma, alpha, and billing; all three are saved to .teamai/config.yaml.
  2. Add a fourth project, delta, to the remote manifest and push the commit.
  3. Run the same init command again, ensuring the cached clone is recognized as the same repository.
  4. The CLI reports using existing clone and exits successfully, but the saved project list remains [gamma, alpha, billing]; delta is missing.

The existing-clone path in src/init.ts does not refresh the repository before resolveActiveProjects reads its manifest. Consequently, the new selector expands the stale local manifest. The clone-reuse behavior predates this PR, but it prevents this feature from fulfilling the documented promise that re-running init re-resolves the current project set.

The current E2E does not cover this path: its Git URL rewrite makes the detected remote a local filesystem path, so the second init treats it as a different repository and replaces the clone under --force. For the reproduction, I restored the cached origin to the synthetic HTTPS URL, removed the rewrite after initial setup, and retained a local push URL. This exercises clone reuse without requiring a network fetch.

Please refresh the reused clone before resolving the project snapshot, and add a real-CLI regression covering a remote manifest change between two initializations.

Validation on macOS:

  • npm run build: passed.
  • npx tsc --noEmit: passed.
  • Existing init-projects.test.ts: 13 passed.
  • Existing real-CLI init-project-all.test.ts: 2 passed.
  • Added remote-update / clone-reuse scenario: reproduced the missing delta in two independent runs; initial activation and explicit-ID control cases passed in both runs.

I did not run the full Agent / Provider matrix.

Re-running init --project all against an existing clone previously expanded the stale local manifest. pullRepo before resolveActiveProjects so newly added remote projects are picked up.
… all

Regression for PR Tencent#518 review: re-init with an existing clone must pick up projects added to the remote between runs.
@Yi-111-a

Copy link
Copy Markdown
Contributor Author

Addressed in 09627a5.. (follow-up commit on this branch):

  1. Refresh on clone reuse. After using existing clone, init now calls pullRepo before resolveActiveProjects, so --project all expands against the current remote manifest rather than a stale local snapshot. A failed refresh exits with a clear error (or use --force to replace the clone).

  2. E2E regression. init-project-all.test.ts now covers a remote manifest change between two inits while keeping the existing clone (restore synthetic HTTPS origin so remotesMatch reuses it; insteadOf still bridges pull to the local bare remote). Expectation: newly added delta appears in .teamai/config.yaml after the second --project all.

CI watching.

@jeff-r2026 jeff-r2026 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.

I reviewed the latest head (2d9c3362) and reproduced two issues locally. I recommend addressing these before merging.

Validation on macOS:

  • npm run build: passed.
  • npx tsc --noEmit: passed.
  • npx vitest run src/__tests__/init-projects.test.ts: 13 passed.
  • npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/init-project-all.test.ts: 2 passed, 1 failed at line 188.
  • Additional real-CLI regression check for preserving a tracked local edit: passed against origin/main (b9cd9ea6), failed against this PR. The affected init invocation used an explicit project ID and omitted --force.

The regression harness used isolated temporary HOME/project directories and a local bare remote. HTTPS transport was redirected locally; a test-only Git wrapper returned the synthetic origin for remote get-url so Git's insteadOf expansion would not accidentally take the different-repository/reclone path. Pull, fetch, and reset ran through real Git. No live team repository was modified.

Comment thread src/init.ts Outdated
// (re-running init after a new project is added would otherwise keep the
// old project list — see PR #518 review).
try {
const pullResult = await pullRepo(localPath);

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.

[P1] Avoid the destructive pull fallback during init

pullRepo() is not just a refresh: when git pull --ff-only fails, it can fetch and run reset --hard origin/<branch> on a dedicated clone. This new unconditional call therefore changes ordinary init behavior, including explicit project IDs and invocations without --force.

Local reproduction: initialize a project-scoped clone with --project billing; advance the remote manifest; append an uncommitted edit to the local manifest; remove only the fixture's local config to exercise init without --force while retaining the clone; initialize again with --project billing. This PR prints realigning discards 0 local commit(s) and 1 uncommitted change(s), deletes the local edit, and exits successfully. The same scenario on main preserves the edit.

Please use a non-destructive refresh here and stop with an actionable error if the clone cannot be fast-forwarded safely. Add a regression asserting that tracked local edits survive a failed refresh; retaining the existing pullRepo() hard-reset fallback makes reinitialization capable of losing work.

expect(first.code, first.output).toBe(0);
expect(readProjects(projectRoot)).toEqual(['gamma', 'alpha', 'billing']);

const teamRepo = path.join(home, '.teamai', 'team-repo');

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.

[P2] Resolve the clone path from the project-scoped config

This test initializes with --scope project, but looks for the clone under the user-scoped home/.teamai/team-repo. Running the submitted E2E file fails at the next assertion (expected false to be true), before it can exercise the new refresh behavior. Read repo.localPath from the generated config instead of assuming a user-scope location.

Please also verify that the repaired offline fixture actually reaches using existing clone: locally, fixing the path alone still took the replacement branch because git remote get-url expands the insteadOf URL to the local filesystem path. Restoring the raw synthetic origin URL did not prevent that expansion.

The default Vitest configuration excludes src/__tests__/e2e/**, so the green default CI run does not establish that this added test passes. Please rerun the explicit E2E command after fixing the fixture and update the validation results.

Address jeff-r2026 review on Tencent#518:
- init clone-reuse path uses pullRepoFastForward (ff-only only; never reset --hard)
- e2e reads repo.localPath from project config; git wrapper keeps remotesMatch on synthetic origin
- regression: tracked local edits survive a failed refresh
@Yi-111-a

Copy link
Copy Markdown
Contributor Author

Addressed the latest review on dab812b:

  1. [P1] Non-destructive init refresh. Clone-reuse now calls new pullRepoFastForward() (git pull --ff-only only). Init never takes the pullRepo() hard-reset fallback. If ff-only fails, init exits with an actionable error and leaves the local clone unchanged (suggests fixing divergence or re-running with --force).

  2. [P2] E2E fixture. The refresh regression reads repo.localPath from the project-scoped config (not ~/.teamai/team-repo). A test-only git wrapper returns the synthetic HTTPS origin for remote get-url so insteadOf expansion does not force the replace/reclone path; pull/fetch still use real git. Added a regression that a tracked local manifest edit survives a failed refresh (no realigning discards).

Validation:

  • npm run build: passed
  • npx tsc --noEmit: passed
  • npx vitest run src/__tests__/git.test.ts src/__tests__/init-projects.test.ts: 69 passed
  • npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/init-project-all.test.ts: 4 passed

@jeff-r2026 jeff-r2026 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.

Rechecked dab812b2 locally. The P1 data-loss issue is fixed: the independent real-CLI reproduction now exits nonzero and preserves the tracked edit, including when retried with --force. Build, typecheck, and all 13 selector unit tests passed.

The E2E clone-location and origin-rewrite fixes are in place, but the submitted E2E file still reports 3 passed / 1 failed on macOS because the new path assertion compares /var/... with /private/var/.... In a local test copy, changing only that assertion to use fs.realpathSync on both sides made all 4 E2E cases pass, including remote manifest refresh and preservation of local edits. Please make that small fixture correction before merging.

One remaining recovery-message nit: init still suggests --force will replace this clone after refresh failure, but a matching origin always reuses the clone regardless of that flag. I verified that retrying the dirty-clone reproduction with --force fails again and preserves the edit. Please remove that replacement suggestion from this error (and the helper documentation), keeping the manual recovery advice. There is no need to add destructive replacement behavior to resolve the wording.

const teamRepo = readClonePath(projectRoot);
expect(fs.existsSync(path.join(teamRepo, '.git'))).toBe(true);
// Project scope stores the clone under the project dataHome, not ~/.teamai.
expect(path.resolve(teamRepo)).toBe(

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.

[P2] Canonicalize filesystem aliases before comparing clone paths

path.resolve() only normalizes path syntax; it does not resolve symlinks. On macOS, this test receives /private/var/folders/.../project/.teamai/team-repo from the CLI while the temporary fixture path is /var/folders/.../project/.teamai/team-repo. These identify the same directory, but this assertion fails before the remote-update scenario runs.

Reproduced with npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/init-project-all.test.ts --retry 0: 3 passed, 1 failed. Changing just this assertion in a local copy to the following produces 4 passing tests:

expect(fs.realpathSync(teamRepo)).toBe(
  fs.realpathSync(path.join(projectRoot, '.teamai', 'team-repo')),
);

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] support --project all for multi-project workspaces / superprojects

2 participants