Smilewithoutfalling b3b3a0b03c fix(dry-run): thread { dryRun } through the loaders pull, push, status and list use (#866)
* fix(dry-run): thread { dryRun } through the loaders pull, push, status and list use

`--dry-run` is documented as previewing without making changes, and #837 made
that true for `tags subscribe`, `tags unsubscribe` and `roles set`. `pull`,
`push` and `status` still wrote: each runs its scope-detection block before any
dry-run guard, and that block called config loaders that were never given the
flag. A preview could therefore persist the legacy role migration, and in a git
repo adopt a pre-#546 partition or run the single-repo self-heal bootstrap.

The loaders already take LoadOptions — #853 threaded them through
`loadLocalConfigForScope` for contribute / session save / recall. These
commands simply did not supply the flag.

- pull.ts: both loaders take { dryRun: options.dryRun }.
- push.ts: autoDetectInit takes it.
- status.ts and list: { dryRun: true } unconditionally, because both are
  read-only and should never migrate, adopt a partition or bootstrap.

Callers that pass nothing behave as before, the same compatibility promise
#837 made. The one observable change is that the preview path logs, so
`status`/`list` now surface a "[dry-run] Would ..." line where a migration or
bootstrap is pending; the PR description asks for a decision on that label.

Verification: seven new command-level cases, each failing on unmodified main
with the identical test file (the project-scope three need a git project with
a pre-#546 partition name, which the existing user-scope fixture never
reaches); and the issue's own real-CLI reproduction, 6/6 — the control writes
the reported fields, this change writes nothing.

oxlint --deny-warnings and tsc --noEmit: rc=0 here and rc=0 on unmodified main.

Closes #850

* fix(pull,push): a --dry-run must leave a fresh self-mode clone alone

Resolves both P1 findings on this PR.

pull.ts:1891 - the previewed self-mode config reached lockScope(), whose
acquireLock() calls ensureDir() on the lock's parent. On a fresh clone the
partition does not exist yet, so the directory was created and stayed:
releaseLock removes the lock file, not its parent. The guard sits inside
lockScope(), the one choke point all three call sites share.

push.ts:731 - the same previewed config ran the whole self-mode setup before
pushCore reached its own dry-run guard at push.ts:1577: the sync-lock,
migrateSelfModeGitignore(), and the disposable knowledge worktree.

Both guards are deliberately narrow. A blanket early return before the
git-mode branch would also skip resetToCleanMaster/pullRepo (push.ts:934),
which a dry run performs on purpose so it can name the destination the real
command would use. Only writes that outlive the command are gated.

The preview still reads the uncommitted teamai.yaml that pushCore receives
as initialPendingTeamConfig; that block is now pendingSelfTeamConfig(), the
identical read, so the preview keeps describing the config edit it exists to
describe.

Fixture gap, also flagged: dry-run-load-path.test.ts already had a
fresh-self-mode-clone fixture, but only tags/roles ran against it. pull and
push ran at user scope, or on a project partition that already exists - never
on the one shape where acquireLock has something new to create. Two cases
added there; the unfixed tree fails them at
fs.existsSync(<HOME>/.teamai/projects) with "expected true to be false".

Not fixed here, and named in the PR description: pull --dry-run on a fresh
clone still creates an empty <HOME>/.teamai/locks/, via listPendingForInstall
in utils/pending-learnings.ts. That call is unchanged by this PR and the file
is outside its scope; the test declares and counts the entry, so anything
else appearing still fails.

* test(pull): pin the loader call shape the widened signature produces

Fixes the red Lint & Test on the previous head (all four matrix entries).

pull-scope-isolation.test.ts asserted the exact argument list of
loadLocalConfigForScope, which this change widens to carry LoadOptions:

  expect(loadLocalConfigForScope).toHaveBeenCalledWith('user');
    received ['user', undefined, { dryRun: undefined }]

The shape is not a free choice: recall.ts:464 and recall.ts:515 already pass
the same third argument, landed with #853, so pull.ts matches the merged
precedent. recall-scope-isolation.test.ts never asserts the argument list,
which is why the same change left it green.

The affected set is derived from the changed symbols rather than from the
topic: every test file that mentions loadLocalConfigForScope,
detectProjectConfig or autoDetectInit (76 files, 1221 tests).

* fix(update): a --dry-run asks the lock for its state instead of taking it

`acquireLock` is not a read. It `ensureDir`s the lock's parent, which on a
fresh self-mode clone is a `<getDataHome>` partition that does not exist yet —
and `releaseLock` removes the lock FILE, not that directory, so the directory
outlives the command. Any preview that calls it therefore writes, which is the
defect this PR is about (#866).

The new `options.dryRun` returns what the preview actually owes its caller: the
ANSWER the real run would get. `lockState` already separates `live` (a holder
is running) from `stale` and `missing`, and the real run reclaims either of the
latter and wins, so `acquireLock(path, { dryRun: true })` is exactly that
verdict — no mkdir, no lock file, no reclaim sentinel.

Nothing is recorded in `heldLockOwners`, which is what makes the preview safe
alongside the existing `releaseLock` calls: it returns at its first line when
it holds no owner token for the path, so a preview cannot delete a lock another
process owns.

No caller passes `dryRun` yet; this commit is the primitive only.

* fix(pull,push): acquire the preview's locks read-only, and stop hiding what it reports

Supersedes the guards added in cfd7c573. Those guards stopped the writes, and
E2E (fork-safe) caught what they cost — 6 failures in
push-sync-followups-823.test.ts, all of the same shape:

  expected '- Scanning local resources...\nNo new or modified resources to push'
  to contain '[rules] teamai-rule (modified)'

A preview that reports no changes for a tree with a deliberately edited team
rule is not a conservative preview; it is this PR's own defect with the sign
flipped.

pull.ts — `lockScope()` returned `true` outright for a dry run, asserting the
scope was uncontended. That is a fabricated fact: its callers read `true` as
"you hold the lock". It now acquires through the read-only primitive and
records the lock for release only when it really took one, so a scope with a
live holder is reported as contended and skipped, exactly as a real pull does.

push.ts — the self-mode branch returned early for a dry run. Two of the three
things it skipped are right to skip and one is not.

  - the sync-lock: read-only now, at both push.ts:784 (self) and push.ts:839
    (git mode).
  - `migrateSelfModeGitignore()`: still skipped. It rewrites a tracked file in
    the user's ACTIVE tree, which outlives the preview, and it is idempotent,
    so the next real push performs it.
  - the knowledge worktree: MUST run, and that is the correction. `pushCore`
    adds the active tree's `.teamai/{skills,rules}` as scan sources and diffs
    them against `localConfig.repo.localPath` (push.ts:1095-1112) — the clean
    worktree checkout. Outside the worktree those are the same path in self
    mode, so the diff is empty by construction: skipping the worktree does not
    report an edit early, it hides the edit. `withKnowledgeWorktree` already
    removes it in a `finally`, so it stays disposable.

Fixture — `push --dry-run` now performs its `git fetch` for real, which leaves
`app/.git/FETCH_HEAD` behind. Declared and counted next to the existing
`<getDataHome>/locks/` entry, so any OTHER new entry still fails the case.
2026-09-28 22:32:16 +08:00
2026-03-03 14:38:43 +08:00
2026-03-03 14:38:43 +08:00

teamai-cli

TeamAI — Make Every Team AI Native

Tencent%2Fteamai-cli | Trendshift

English | 中文 | 日本語 | 한국어 | ไทย

CI npm version npm downloads License: MIT

The shared foundation for how your team works, learns, and improves with AI.

TeamAI turns individual AI capabilities into shared team capabilities — across agents, machines, and team members.

Why TeamAI

Eight everyday scenarios, before and after TeamAI

Quick Start

Send this one line to your AI tool:

Install the teamai skill: https://github.com/Tencent/teamai-cli/tree/main/skills/teamai , load the teamai skill, then set up TeamAI for my team from scratch.

Once TeamAI is set up, just talk to the /teamai skill in your AI tool:

Set up a team from scratch

/teamai Help me set up TeamAI for my team from scratch

Join a team

/teamai Help me join my team's TeamAI, repo URL is https://github.com/your-org/your-repo

Share with the team

Skills, rules, MCP servers, and other agent resources can all be shared:

/teamai Share my xxx skill with the team

Open the dashboard

/teamai Open the TeamAI dashboard

Once a teammate is set up, they just open their agent and already have the team's full set of AI assets.

Command-line install

Install

npm install -g teamai-cli

Team admin / solo user

Create a shared-experience repo on your git host (GitHub, GitLab, GitCode, CNB, TGit, or a private Git service), grant write access to team members, then run teamai init https://github.com/your-org/your-repo.

No team repo yet? Start from a template pre-loaded with production-ready skills, rules, and review agents. Browse the teamai-hub org, click Fork, then teamai init against your new repo.

Team members

# Choose one, depending on where you want resources installed

# Project-scope init (default, resources installed under the project directory)
cd /path/to/my-project
teamai init https://github.com/your-org/your-repo

# Or, user-scope init (resources installed under ~/)
teamai init https://github.com/your-org/your-repo --scope user

Once initialized, every AI session automatically pulls the latest skills / rules and other Harness updates published by admins — no manual sync needed.

Product Overview

Three layers of capability, built on Git:

  • Team Execution — make every agent work the team's way: skills, rules, docs, env, agents, hooks, MCP, models.
  • Team Context (beta) — make every agent understand the team: learnings, codebase graph, teamwiki.
  • Team Improvement (beta) — make every execution improve the team: usage, sessions, dashboard.
Agent Team Execution Team Context (beta) Team Improvement (beta)
skillsrulesdocsenvagentshooksmcpmodels learningscodebaseteamwiki usagesessionsdashboard
Claude Code✓✓✓✓✓✓✓✓✓✓✓✓✓✓
Codex✓✓✓✓✓✓✓✓✓✓✓✓✓✓
Cursor✓✓✓✓✓✓✓—✓✓✓✓✓✓
GitHub Copilot CLI✓✓✓✓✓✓✓—✓✓✓✓✓✓
CodeBuddy✓✓✓✓✓✓✓✓✓✓✓✓✓✓
WorkBuddy✓✓✓✓—✓✓✓✓✓✓✓✓✓
OpenCode✓✓✓✓✓✓✓✓✓✓✓———
Pi Coding Agent✓✓✓——✓————————
OpenClaw✓✓✓✓————✓✓✓———
Hermes✓—✓✓————✓✓✓———
DeepSeek Harness✓—✓—————✓✓✓———
Qoder✓✓✓✓✓✓✓—✓✓✓✓✓✓
Qoder CN✓✓✓✓✓✓✓—✓✓✓✓✓✓
Kiro✓✓✓✓✓✓✓—✓✓✓✓✓✓
ZCode✓—✓—✓✓✓—✓✓✓✓✓✓
Oh My Pi✓✓✓✓✓✓✓—✓✓✓———

Learn More

Contributors

Thanks to everyone who has contributed to TeamAI!

Contributors

Made with contrib.rocks.

Contributing

Join the conversation, or open an issue or PR. See CONTRIBUTING.md for how to contribute.

License

MIT

S
Description
GitHub Trending: Tencent/teamai-cli
Readme MIT
25 MiB
Languages
TypeScript 99.7%
Python 0.2%
JavaScript 0.1%