Skip to content

fix: keep TeamAI out of projects that never set it up (#748) - #753

Merged
jeff-r2026 merged 6 commits into
Tencent:mainfrom
SaulMoro:fix/748-scope-teamai-to-initialized-projects
Sep 23, 2026
Merged

jeff-r2026 merged 6 commits into
Tencent:mainfrom
SaulMoro:fix/748-scope-teamai-to-initialized-projects

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #748. Supersedes #750.

Summary

Hooks live in HOME, so they fire in every project. Each hook run now resolves its scope once and does nothing team-related where TeamAI is not set up.

hook-dispatch (any cwd)
└─ resolveConfigForDir(cwd ?? process.cwd())                  config.ts
   ├─ project config             → team handlers · usage → <partition>/usage.jsonl
   ├─ user config                → team handlers · usage → ~/.teamai/usage.jsonl
   ├─ project config unreadable  → machine-level only · nothing recorded
   ├─ cwd no longer exists       → user config, else none
   └─ none                       → machine-level only (update, pull, local agent, package pending hint)

The legacy teamai contribute-check command, which hooks of older installs still call, resolves the same way.

 filterHandlersForConfig(registry, null)
-  → every handler
+  → drops `requiresConfig` handlers (share nudge, TodoWrite nudge, dashboard, track, webhook, …)

 reportUsageToTeam(scope)
-  read ~/.teamai/usage.jsonl          ← every project's skills
+  read <dataHome>/usage.jsonl         ← this scope only; truncate only it

 init --scope user   (first user scope on the machine)
+  discard ~/.teamai/usage.jsonl       ← events that name no project

requiresConfig stays opt-in; an exact allow-list test fails when a new handler runs without config and is not on it.

Evidence

E2E with the real CLI, isolated HOME, local bare team repos, the same session script per agent. project-a and project-c are initialized against two different teams; project-b has no TeamAI.

Before (origin/main) After
project-b Stop nudge shown (Claude, OpenCode) · stashed (Codex, CodeBuddy) none
project-b TodoWrite nudge shown none
project-b local traces dashboard, sessions, known-skills none
team-c reported skills B C A2 C
team-a reported skills A A A2
first init --scope user, leftover skill pushed not pushed

Identical for Claude, Codex, CodeBuddy and OpenCode.

Red → green tests:

hook-dispatch-scope      session without config leaves no dashboard/usage/session file
                         host without cwd (OpenClaw) records into the project it runs in
                         unreadable project config records nothing, not even in user scope
                         cwd that no longer exists falls back to user scope, no crash
contribute-check-e2e     legacy `teamai contribute-check` silent without config
git-kind-reports         A and C on different teams: each report has only its own skill,
                         A's truncation keeps C's events            (real git, bare origins)
hook-handlers            only machine-level handlers survive without config (allow-list)
                         Stop share nudge silent without config (was: pinned as "keeps hinting")
                         TodoWrite nudge silent without config
usage-tracking           legacy track / track --stdin / track-slash record nothing without config
http-repo-integration    first user-scope init empties leftover usage; re-init keeps it

Test plan

  • npx tsc --noEmit, npx vitest run (4302 passed, 1 skipped), npm run test:e2e (225 passed, 26 skipped), npm run build
  • E2E, git provider: Claude, Codex, CodeBuddy, OpenCode (table above)
  • E2E, first teamai init --scope user with leftover usage (git provider)
  • Smoke, github provider (Claude, throwaway private repo): project-b no nudge, no session file; teamai-reports has project-a's skill, not project-b's
  • Smoke, gitlab provider: run separately by the author, passed

Merge Danger

Door: two-way, except two local data drops:

  • first user-scope init deletes ~/.teamai/usage.jsonl (its events name no project);
  • on project-only machines, events already in the old shared file are never reported.

Blast Radius: telemetry

A config that fails to parse now withholds team prompts instead of running all of them (teamai doctor reports it). Stats already pushed are not rewritten.

Not in this PR → #752 (dashboard events per scope, pull on an unreadable project config, growth in scopes that never report, retroactive cleanup).

Hooks of a project-scope install live in HOME, so they fire in every
project on the machine. With no config for the hook's cwd they ran anyway:
the Stop share nudge (even with recall off), the TodoWrite recall nudge,
and local capture of sessions and skill usage that another project's
report later pushed to its team.

Handlers that need a team now declare requiresConfig; the dispatcher drops
them when neither a project nor a user config resolves. Only machine-level
work runs there: update check, session-start pull, local agent, package
pending hint. The legacy track paths skip recording the same way.
Every scope appended to one ~/.teamai/usage.jsonl, so whichever project
pulled next reported every project's skills to its own team.

Usage now goes to <dataHome>/usage.jsonl of the scope that resolves for
the session's directory (detectProjectConfig(cwd) ?? loadLocalConfig(),
as the dispatcher does). Each report reads and truncates only its own
file; teamai stats shows the current scope. A machine's first user scope
starts with an empty file: what it held cannot be attributed.
- resolveConfigForDir (config.ts) is the single resolution the dispatcher,
  the usage writers and readers, and teamai stats use. A host that sends no
  cwd (OpenClaw) resolves from the process cwd, and an unreadable project
  config yields null instead of falling back to the user scope.
- readUsageEvents / truncateUsageAfterReport require a scope; no path reads
  the old shared file by default. readKnownSkills reads its scope's file.
- Real-dispatch tests: no-config session leaves no trace, cwd-less host,
  unreadable project config.
- Docs: machine-level handler list, data-directory-layout note.
…encent#748)

The hidden 'teamai contribute-check' command, still called by hooks of
older installs, had no config gate, so it nudged in projects without
teamai. It now resolves the scope like the dispatcher and skips when
none resolves.

resolveConfigForDir no longer throws for a directory that does not
exist (simple-git refuses it); a hook naming a deleted worktree falls
back to the user scope.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Update since the last push:

454be75  fix(review)  legacy `teamai contribute-check` gated like the dispatcher
                      a hook whose cwd no longer exists falls back to user scope (it used to throw)
b39e679  merge main   #710 conflicted only on imports in src/init.ts; both kept

After the merge, tsc, vitest run (4302 passed) and test:e2e (225 passed) are green, and the Claude end-to-end run gives the same table as the PR body.

#750 was closed in favour of this PR. Its author suggested keeping cwd on UsageEvent for later attribution by directory. I left it out because nothing reads it yet, and per-scope files already attribute usage by scope. It fits better with the per-scope dashboard work in #752.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/config.ts:318 returns a lower-priority legacy project config even after the authoritative partition config was reported unreadable. detectProjectConfig() explicitly continues from a broken partition to <workspace>/.teamai/config.yaml; because project is checked before unreadable, hooks and usage can run against the wrong team. Return null whenever any higher-priority config triggered the unreadable callback.
  • [P1 blocking] src/usage-tracker.ts:23 reinterprets the existing shared ~/.teamai/usage.jsonl as user-scope data without migrating or discarding it for machines that already have a user config. Previous releases stored usage from every project there, so the next user-scope report can upload historical project activity to the user-scope team. saveUserScopeConfig() only cleans the file during first-time user initialization and does not protect upgrades.

The PR description includes a sufficient test plan and real-CLI E2E records.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 23, 2026
…onfig is skipped

Codex review of 5793758:

- A project-location config that is not `scope: project` (or omits
  `scope`, which defaults to user) was skipped without a word, so the gate
  read past it to the user config. It is now reported as unusable, unless
  it is the user config itself, as when running from HOME.
- The legacy `contribute-check` changed into the payload cwd and, if that
  failed, asked the gate about the directory the process started in. It
  now passes the payload cwd to the gate (`detectTeam(cwd)`), and a cwd
  that no longer exists holds no project config, so only the user config
  is asked, as Tencent#753 does.
@jeff-r2026
jeff-r2026 merged commit 5502d8e into Tencent:main Sep 23, 2026
13 checks passed
@SaulMoro
SaulMoro deleted the fix/748-scope-teamai-to-initialized-projects branch September 23, 2026 12:56
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 23, 2026
Conflicts resolved as planned before Tencent#753 landed:

- hook-handlers.ts: Tencent#753's local `contributeHintAllowed` is dropped; the
  handlers use skill-content's, with the payload cwd.
- hook-handlers.test.ts: both config mocks kept, and this branch's tests.
- contribute-check-e2e: the fixture keeps recall on, which the gate needs.
- docs and manage-admin: both sentences combined; skill-serving already
  places `contributeHintAllowed` in src/skill-content.ts.
- contribute-check.ts merged cleanly: Tencent#753's `resolveConfigForDir` check
  and this branch's gate run in turn on the same payload cwd.
jeff-r2026 pushed a commit that referenced this pull request Sep 23, 2026
…eploy (#747)

* fix(skills): one share gate, actionable refusals, and a louder stub deploy

Follow-ups from the review of #699:

- The Stop-hook reminder and `teamai skill get share` ask one gate
  (`shareGate`, through `contributeHintAllowed`). The hook skipped the
  unreadable-project-config check, and the legacy `teamai contribute-check`
  command, still called by hooks written before the dispatcher, checked
  nothing, so both nudged towards a command that refused.
- The gate reads only a config load failure as "cannot be loaded"; any other
  fault propagates (the hook withholds the reminder and logs it at debug).
- A `config` refusal says what failed (the file and position for a parse
  error) instead of pointing at `teamai doctor`, which cannot see a broken
  config. `skill show` now refuses through the same helper, so its hint moves
  from stdout to stderr like `skill get` and `skill path`.
- `pull` warns when the discovery stub cannot be deployed (it was an empty
  catch on the fast path and a debug line on a full sync), and so does the
  legacy prune.
- Error text no longer claims a reason was logged when none was: an empty
  config is named as empty, and `init` points at ~/.teamai/debug.log, where
  every path that deploys nothing now records why.
- `core` routes a bare `/teamai` right after a friction reminder to `share`,
  as the stub already said.
- The command drift guard rejects an unknown subcommand inside a group
  (`teamai skill gett core` passed before).
- The contribute-check e2e asserts the reminder's real text again; the usage
  guides (EN, zh-CN) and the design doc cover the config refusal, the gate and
  the reminder routing.

* test(learnings): retry temp-dir cleanup that races a detached git gc

A push into the bare origin can leave `git gc --auto` writing to
objects/pack after the test returns; the single rmdir in afterEach then
fails with ENOTEMPTY (seen on CI, Node 22 ubuntu, #747).

* fix(skills): gate skill show before its lookups, name the failing field

Review of #747:

- `skill show share` under a broken project config searched the user
  config's team repo and agents, which detection falls back to, and printed
  a `share` found there. It now asks the gate first and refuses on a config
  block before any lookup. With an empty user config it refuses instead of
  ending in a stack trace.
- A config that parses but fails validation reported the Zod JSON dump,
  whose first line is `[`, so the refusal said `config.yaml: [.`. Every
  config loader now reports each issue as `field: reason` on one line.
- The docs and skills that describe the share reminder or the refusal say
  it is withheld on a read-only source and while the config cannot be
  loaded, and that a validation failure names the field: product-overview
  and usage-guide (EN, zh-CN), designs/skill-serving.md, core/SKILL.md,
  contribute-member, setup-admin, join-member and manage-admin.

* fix(skills): no share reminder where teamai is not set up

`contributeHintAllowed` fell open with no config at all, so a caller other
than the dispatcher (the legacy `teamai contribute-check`) still nudged in
projects that never set up teamai, which have no team to share with
(#748). It now returns false there. Serving the skill stays fail-open.

* fix(contribute-check): gate the legacy reminder on the session's cwd

`teamai contribute-check --stdin` asked the share gate about the directory
the hook process started in, while the session analysis used the payload
cwd. Started outside the project, it could read the user config and nudge
where `teamai skill get share` refuses (a project config that does not
load). It now moves to the payload cwd first, as hook-dispatch does.

* fix(pull): keep a debug.log record when the stub cannot be deployed

The previous commit turned both deploy catches into `log.warn`, which is
muted in silent mode and never reaches debug.log, and a SessionStart pull
runs detached with its output discarded. So the automatic pull, the one
that deploys the stub for most members, lost the only persistent record
it had. Both catches now warn and write the same line to debug.log.

* fix(skills): skill show and list never answer for the fallback team

Known issues left by #747:

- `skill show <name>` and `skill list` on a config that exists but does
  not load ended in a Node stack trace, and under a broken project config
  they searched the user config detection falls back to: another team's
  repo and agents. Both now ask `detectTeam`, the one place that tells
  "this team", "no team" and "cannot tell, and why" apart (`shareGate` is
  built on it). Without a usable team, `show` answers from the package
  alone and `list` prints only the packaged catalog; both say what failed
  on stderr and exit 1.
- A teamai.yaml that exists but fails validation was reported as "not
  found. Check your repo path". It is now named as invalid, empty or
  unreadable, like the local config.

* fix(skills): the gate reads the session's directory, and no project config is skipped

Codex review of 5793758:

- A project-location config that is not `scope: project` (or omits
  `scope`, which defaults to user) was skipped without a word, so the gate
  read past it to the user config. It is now reported as unusable, unless
  it is the user config itself, as when running from HOME.
- The legacy `contribute-check` changed into the payload cwd and, if that
  failed, asked the gate about the directory the process started in. It
  now passes the payload cwd to the gate (`detectTeam(cwd)`), and a cwd
  that no longer exists holds no project config, so only the user config
  is asked, as #753 does.

* fix(logger): record warnings in debug.log

`log.warn` wrote to the console only and was muted in silent mode, so a
detached SessionStart pull, whose output is discarded, lost every warning:
the stub deploy failure and the legacy prune among them. Warnings now reach
debug.log like debug and error lines. `warnStubNotDeployed` drops the
second `log.debug` call, which printed the line twice under --verbose.

* fix(skills): the dispatcher gate reads the payload cwd; a symlink is not HOME

Codex review of b0583f5:

- The dispatcher's `contribute-check` and `pending-hint` handlers asked
  the gate about the process's directory, trusting hook-dispatch's
  `chdir`; when that failed, the launcher's config decided. They now pass
  `resolveHookCwd(stdin)`, as the legacy command does.
- The HOME exception for a non-project scope compared the config file's
  real path, so a project config symlinked to ~/.teamai/config.yaml passed
  for the user config. It is now decided by the project's location: its
  root is HOME.

* fix(skills): only a missing cwd falls back to the user config; load it once

Codex review of 15a5b5d:

- `detectTeam` read any failure to see the payload cwd as "deleted", so a
  cwd it could not open (no permission, a path through a file) fell back
  to the user config and could allow the reminder. Only ENOENT does now;
  anything else is `unusable` and withholds it.
- `skill show share` and `skill list` loaded the config twice, through the
  gate and then the team lookup, and reported a broken one twice. Both
  detect the team once and hand it to the gate.

* fix(logger): a file-only record instead of persisting every warning

b0583f5 made every `log.warn` append to debug.log, wider than the two
failures it was for, and it wrote unrelated subprocess errors to disk.
`log.warn` is console-only again; `log.persist` writes one line to
debug.log and never to the console. The stub deploy catches and the
legacy prune catch use both, so a detached SessionStart pull keeps the
record and --verbose prints it once.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 23, 2026
…nreadable config (Tencent#748)

Follow-up to Tencent#753, from its review.

- resolveConfigForDir returns null when any project config was reported
  unreadable, even if a lower-priority one (a legacy .teamai/ behind a broken
  partition) loads: that one may name another team.
- The user scope's usage.jsonl is the old shared file. Tencent#753 only emptied it on
  a machine's first user-scope init, so a machine that already had a user
  scope reported every project's pre-upgrade usage to it. The first access
  after the upgrade now discards what an earlier release left there and
  writes ~/.teamai/usage-per-scope. One process discards, under acquireLock;
  concurrent hooks wait for the marker, so none deletes what another recorded.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 23, 2026
…nreadable config (Tencent#748)

Follow-up to Tencent#753, from its review.

- resolveConfigForDir returns null when any project config was reported
  unreadable, even if a lower-priority one (a legacy .teamai/ behind a broken
  partition) loads: that one may name another team.
- The user scope's usage.jsonl is the old shared file. Tencent#753 only emptied it on
  a machine's first user-scope init, so a machine that already had a user
  scope reported every project's pre-upgrade usage to it. The first access
  after the upgrade now discards what an earlier release left there and
  writes ~/.teamai/usage-per-scope. One process discards, under acquireLock;
  concurrent hooks wait for the marker, so none deletes what another recorded.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 24, 2026
…nreadable config (Tencent#748)

Follow-up to Tencent#753, from its review.

- resolveConfigForDir returns null when any project config was reported
  unreadable, even if a lower-priority one (a legacy .teamai/ behind a broken
  partition) loads: that one may name another team.
- The user scope's usage.jsonl is the old shared file. Tencent#753 only emptied it on
  a machine's first user-scope init, so a machine that already had a user
  scope reported every project's pre-upgrade usage to it. The first access
  after the upgrade now discards what an earlier release left there and
  writes ~/.teamai/usage-per-scope. One process discards, under acquireLock;
  concurrent hooks wait for the marker, so none deletes what another recorded.
jeff-r2026 pushed a commit that referenced this pull request Sep 24, 2026
…nreadable config (#748) (#758)

* fix(usage): discard pre-upgrade usage and stop falling back past an unreadable config (#748)

Follow-up to #753, from its review.

- resolveConfigForDir returns null when any project config was reported
  unreadable, even if a lower-priority one (a legacy .teamai/ behind a broken
  partition) loads: that one may name another team.
- The user scope's usage.jsonl is the old shared file. #753 only emptied it on
  a machine's first user-scope init, so a machine that already had a user
  scope reported every project's pre-upgrade usage to it. The first access
  after the upgrade now discards what an earlier release left there and
  writes ~/.teamai/usage-per-scope. One process discards, under acquireLock;
  concurrent hooks wait for the marker, so none deletes what another recorded.

* fix(review): parse the empty usage file in its test; scope the fallback wording to team hooks (#748)

- "handles empty file" wrote no marker, so the discard removed the file and
  the read passed on a missing file. A first read now settles the file as
  the scope's own, and the test asserts the file survives.
- The session-start pull still resolves its project on its own, so the
  "never falls back to a lower-priority config" rule is stated for team
  hooks and skill usage only (CHANGELOG, usage guide en/zh-CN).

* fix(usage): keep the user scope's usage in its own file, safe across a rollback (#748)

The usage-per-scope marker could not tell a pre-upgrade event from one an
earlier release appends after a rollback, so a reinstall reported those to
the user-scope team. The user scope now records in ~/.teamai/user-usage.jsonl,
which no earlier release writes; ~/.teamai/usage.jsonl is removed, never read.
Drops the marker, its lock and the bounded wait.

* fix(usage): a failed removal of the shared usage file does not stop the user scope (#748)

Also names the user scope's own file where comments and the design diagram
still described every scope's usage as <dataHome>/usage.jsonl.

* docs(changelog): drop the claim that teamai doctor reports an unreadable project config

resolveDoctorContext falls back past an unreadable project config the way
detection does, so doctor diagnoses the config it falls back to and says
nothing about the broken one (#752).

* fix(usage): leave the shared usage file in place instead of removing it on every access (#748)

getUsagePath deleted ~/.teamai/usage.jsonl on every user-scope call,
including each hook append and the read-only `teamai stats`. The user scope
never reads that file, which is what keeps its events off the team; the
delete added a side effect to a path getter and a failure path to guard.
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.

[bug] TeamAI hooks reach projects that never set it up: Stop nudge, and skill usage pushed to another team's stats

2 participants