fix: keep TeamAI out of projects that never set it up (#748) - #753
Merged
jeff-r2026 merged 6 commits intoSep 23, 2026
Merged
jeff-r2026 merged 6 commits into
jeff-r2026 merged 6 commits into
Conversation
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.
# Conflicts: # src/init.ts
Collaborator
Author
|
Update since the last push: After the merge, #750 was closed in favour of this PR. Its author suggested keeping |
|
Findings
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.
Merged
3 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
The legacy
teamai contribute-checkcommand, which hooks of older installs still call, resolves the same way.requiresConfigstays 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.
origin/main)BCA2CAAA2init --scope user, leftover skillIdentical for Claude, Codex, CodeBuddy and OpenCode.
Red → green tests:
Test plan
npx tsc --noEmit,npx vitest run(4302 passed, 1 skipped),npm run test:e2e(225 passed, 26 skipped),npm run buildgitprovider: Claude, Codex, CodeBuddy, OpenCode (table above)teamai init --scope userwith leftover usage (gitprovider)githubprovider (Claude, throwaway private repo): project-b no nudge, no session file;teamai-reportshas project-a's skill, not project-b'sgitlabprovider: run separately by the author, passedMerge Danger
Door: two-way, except two local data drops:
~/.teamai/usage.jsonl(its events name no project);Blast Radius: telemetry
A config that fails to parse now withholds team prompts instead of running all of them (
teamai doctorreports it). Stats already pushed are not rewritten.Not in this PR → #752 (dashboard events per scope,
pullon an unreadable project config, growth in scopes that never report, retroactive cleanup).