Skip to content

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

Merged
jeff-r2026 merged 13 commits into
Tencent:mainfrom
SaulMoro:fix/skill-review-followups
Sep 23, 2026
Merged

jeff-r2026 merged 13 commits into
Tencent:mainfrom
SaulMoro:fix/skill-review-followups

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-ups from the review of #699.

Summary

One place decides which team a directory belongs to, and one gate on top of it decides whether share can run. Every path that serves share, shows or lists skills, or nudges towards share asks them.

+detectTeam(cwd?)           team | none | unusable(detail)   project config first, never the fallback
 shareGate
-  own config loading        project config, http, recall
+  detectTeam                + http, recall
 teamai skill get | path | list share
   resolveServableSkill → shareGate
 teamai skill show <name>
-  autoDetectInit → team repo → agents → resolveServableSkill
+  detectTeam               no usable team: the package alone, say why
+  team repo → agents → (served result)
 teamai skill list
-  autoDetectInit           rethrows a broken config
+  detectTeam               no usable team: catalog only, say why, exit 1
 Stop hook, dispatcher
-  contributeHintAllowed    http, recall
+  contributeHintAllowed(payload cwd)    shareGate + hint switch, silent with no config
 Stop hook, legacy `teamai contribute-check --stdin`
-  (no gate)
+  contributeHintAllowed(payload cwd)

detectTeam checks the project config before detection, which skips a broken project file and falls back to the user config: another team's repo, recall and source. A project-location config that is not scope: project counts as broken too, unless the project's root is HOME, where that file is the user config. A symlink to the user config does not qualify. Both hooks hand the gate the payload cwd, so the directory the process started in is never read. A cwd that no longer exists (ENOENT) holds no project config, so only the user config is asked there, as in #753; one that cannot be opened for any other reason withholds the reminder. detectTeam reads only a failed config load as unusable; any other error propagates, and the hook then withholds the reminder and logs the error at debug.

With no config at all, serving stays fail-open (teamai skill get share works on a fresh install), but the reminder is silent. A project that never set up teamai has no team to share with (#748). #753, now merged, filters the dispatcher's team handlers for the same case and checks resolveConfigForDir first in the legacy command; the gate here covers contributeHintAllowed itself on both paths.

What a member sees:

 skill get share, project config does not parse
-  Run `teamai doctor` to see what is wrong with it, then try again.
+  <file>: <parse error> at line 2, column 1. Fix the file, or move it aside and run `teamai init` …

 skill get share, project config fails validation
-  Run `teamai doctor` to see what is wrong with it, then try again.
+  <file>: repo: Expected object, received number; username: Required. Fix the file, …

 skill get share, project config without `scope: project`
-  served from the user config (another team's recall)
+  <file>: it is scope: user, but a config inside a project must be scope: project. Fix the file, …

 skill show share, broken project config, user scope holds a `share`
-  skill: share … Source : [local-only]   (exit 0)
+  ✖ share is not available: … <file>: <error>   (exit 1)

 skill show core | skill list, config exists but does not load
-  Node stack trace, or the user config's team under a broken project config
+  the packaged card or catalog, then ✖ … could not be loaded … <file>: <error>   (exit 1)

 any command, teamai.yaml fails validation
-  Team config (teamai.yaml) not found. Check your repo path.
+  The team config at <file> could not be read: it is not a valid team config (the error is printed above). …

 legacy Stop hook, process started outside the project, project config broken
-  reminder (read the user config)
+  silent

 Stop hook, no teamai config at all
-  reminder
+  silent

 pull, stub deploy throws, or the legacy prune cannot finish
-  (silent), or a debug line only
+  ⚠ The built-in teamai skill was not deployed: <error>   (and one line in ~/.teamai/debug.log, which a detached SessionStart pull keeps)

 init, stub reached no tool
-  … (see the lines above) … or `teamai doctor` to see it.
+  … The reason is printed above or recorded in ~/.teamai/debug.log …

 any command, empty ~/.teamai/config.yaml
-  could not be read (the reason is logged above)
+  could not be read: it is empty.

teamai doctor does not report a broken project config, and it calls a broken user config "not initialized", so the refusal no longer sends people there. Every config loader reports a validation failure as field: reason on one line instead of Zod's JSON dump, whose first line is [.

Smaller changes:

skill-data/core/SKILL.md        bare /teamai right after a friction reminder loads share
skill-commands-exist.test.ts    fails on an unknown subcommand (`teamai skill gett core`)
skill show                      refuses through the same helper as get and path (hint on stderr)
builtin-skills.ts               the legacy prune warns when it cannot finish
contribute-check-e2e.test.ts    asserts the reminder's real text again
git-kind-learnings.test.ts      retries temp-dir cleanup; a detached git gc raced it (ENOTEMPTY on CI)

Docs and skills say the reminder is withheld on a read-only source, while the config cannot be loaded, and where teamai is not set up, and that a validation failure names the field. The read-only half was already true before this PR; those lines had only named recall.

docs/        usage-guide, product-overview (EN, zh-CN), designs/skill-serving.md
skill-data/  core/SKILL.md, core/references/contribute-member.md,
             setup/references/{setup-admin,join-member,manage-admin}.md

Left out on purpose: the Chinese trigger phrases the stub lost in #699. A test and the repo rule forbid Chinese in skills/ and skill-data/, so maintainers should decide that in its own issue.

Evidence

Real CLI, per provider, on 84a461d (after merging main with #753). npm run build passed. Each leg used an isolated HOME and Claude, Codex, CodeBuddy and OpenCode. After pull, each agent's teamai/SKILL.md is byte-identical to the packaged stub. Each row sets its state, then runs skill get share, skill show share, and a Stop payload through hook-dispatch stop and the legacy contribute-check --stdin. From the broken-config rows on, the user scope also holds a member's own share skill.

                          git / github / gitlab (identical in all three)
                          get  show  dispatcher  legacy
recall on                 0    0     reminder    reminder
recall off                1    1     silent      silent
empty user config         1    1     silent      silent
broken project config     1    1     silent      silent
invalid project config    1    1     silent      silent
project config, no scope  1    1     silent      silent
project config symlinked  1    1     silent      silent
  to the user config
project config removed    0    0     reminder    reminder
no config at all          0    0     silent      silent

The same legs also check:

legacy hook, process cwd outside the broken project    silent
legacy hook, launched in a broken project, payload cwd deleted    reminder (user config decides; launcher never read)
dispatcher, launched in a broken project, payload cwd deleted     reminder (user config decides; launcher never read)
legacy hook and dispatcher, payload cwd through a file (ENOTDIR)  silent (unknown, not absent)
skill show share, empty user config or broken project config      the refusal once (one config load)
skill list, broken project config                      ✖ not listed: <file>: <error>   exit 1
skill show core, broken project config                 card, then ✖ could not be loaded   exit 1
skill show core, empty user config                     card, then ✖ … it is empty   exit 1, no stack trace
skill get share, teamai.yaml `team: 42`                … it is not a valid team config …   exit 1
  • git: a local bare remote, with provider: git in teamai.yaml. init reads a file:// URL as owner/repo and picks GitHub, so this leg writes the local config by hand.
  • github: teamai init https://github.com/<fixture> into a private throwaway repo, read only.
  • gitlab: teamai init https://gitlab.com/<fixture> into a private throwaway repo, read only, with a PAT (teamai's GitLab client sends PRIVATE-TOKEN). Same script, run in a separate environment; every row matched.

The debug.log record for a failed stub deploy is covered by logger and pull-skip-sync. The legs do not force a deploy failure.

Before (d51f412, same script, git leg):

broken project config     get 1  show 0   prints "skill: share … [local-only]" from the user scope
invalid project config    get 1  show 0   refusal detail: "<file>: [. Fix the file, …"
empty user config (show)  Node stack trace

Tests. These fail on the commit named and pass now:

on ca6e512
  skill-recall-gate        config refusal names the file and the error
  hook-handlers            broken project config, good user config: no reminder
  contribute-check-e2e     legacy command, empty config: no reminder
  pull-skip-sync           stub deploy throws: warning on fast path and full sync
  config-not-initialized   empty config: "it is empty"
  init                     no tool reached: names debug.log, no "lines above"
  skill-commands-exist     `teamai skill gett core`: unknown subcommand
on d51f412
  skill-show               broken project config: refuses share, never prints the user-scope repo path
  config-not-initialized   project config failing validation: "repo: Expected object, received number", one line
  config-not-initialized   user config failing validation: log names "username: Expected string", one line
on 88829dd
  hook-handlers            no config at all: no reminder, session never scored
  contribute-check-e2e     legacy command, no config: silent (#748)
  contribute-check-e2e     legacy command started outside a broken project: silent
  pull-skip-sync           stub deploy throws: the line also reaches debug.log
  config-not-initialized   invalid teamai.yaml: "not a valid team config", not "not found"
  skill-list-uninitialized broken config: catalog, error, exit 1; unreadable project config: fallback team never read
  skill-show               broken project config: fallback team never searched; empty config: packaged card, no throw
  skill-serving-cli        unreadable config: skill list and skill show name the file, line and column
on 5793758
  config-not-initialized   project config without scope: project is named; the user config from HOME is not
  contribute-check-e2e     legacy launched in a broken project, payload cwd deleted: the launcher is never read
on b0583f5
  hook-handlers            dispatcher gate asks about the payload cwd, not the process's
  config-not-initialized   project config symlinked to the user config is named
on 15a5b5d
  hook-handlers            payload cwd through a file (ENOTDIR): reminder withheld
  skill-show, skill-list-uninitialized   one config load per command
  logger                   persist writes debug.log only, never the console; warn stays console-only
  pull-skip-sync           stub deploy failure also persists to debug.log

New tests also cover a multi-line parse error, a fault past the config load, and teamai recall enabel still passing the guard.

npm run build        ok
npx tsc --noEmit     clean
npx vitest run       4328 passed, 1 skipped
npm run test:e2e     228 passed, 26 skipped, no retries

Merge Danger

Door: two-way

The PR changes messages, a gate, docs and tests. It deletes and migrates nothing.

Blast Radius: reminders

  • Some teams stop seeing the share reminder: those with an unreadable project config, those on the legacy hook with recall off or a broken config, and every directory without a teamai config. In all but the last, share refuses anyway.
  • The skill show refusal hint moves from stdout to stderr.
  • Under an unreadable config, skill show and skill list exit 1 and show only packaged skills, even when a member's own skill exists on the machine.
  • Both the dispatcher handlers and the legacy contribute-check pass the payload cwd to the gate instead of trusting a chdir.
  • A project-location config without scope: project, or symlinked to the user config, now blocks share and the reminder, and skill show / skill list do not search its fallback team. init and the self bootstrap always write scope: project, so only a hand-written or foreign file is affected.
  • log.persist is new: one line to ~/.teamai/debug.log, never the console. Only the stub deploy catches and the legacy prune catch use it.
  • main with fix: keep TeamAI out of projects that never set it up (#748) #753 is merged in (6573bd0). fix: keep TeamAI out of projects that never set it up (#748) #753's local contributeHintAllowed is dropped in favour of the one in src/skill-content.ts, and the legacy command runs fix: keep TeamAI out of projects that never set it up (#748) #753's resolveConfigForDir check and then this gate, both on the payload cwd.

…eploy

Follow-ups from the review of Tencent#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.
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, Tencent#747).
@jeff-r2026 jeff-r2026 self-assigned this Sep 23, 2026
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] skill show share can bypass the new gate under a broken project config. autoDetectInit() falls back to the user config, and locateSkill() searches that user team repo and installed agents before calling resolveServableSkill(). If either contains a skill named share, the command exposes that path instead of refusing for the broken project config. Check the project-config gate before these lookups. src/skill-cmd.ts:220
  • [P1 blocking] The PR description does not provide the required complete real-CLI verification record. It documents only a local bare remote with provider: git; the repository instructions also require real-CLI coverage for gitlab and github, and the generic npm run test:e2e result does not establish that matrix. It also does not explicitly record npm run build as passed.
  • [P1 blocking] The behavior change is not synchronized across all affected docs and skills. Several files still state that the reminder appears automatically once recall is enabled, without the newly added invalid-config gate—for example docs/product-overview.md:124, docs/product-overview.zh-CN.md:124, skill-data/setup/references/join-member.md:140, and skill-data/setup/references/manage-admin.md:122. This violates the repository requirement to update every affected bilingual document and skill-data/ workflow.
  • [P2 non-blocking] Truncating every unreadable project-config error to its first line can discard the actionable part of schema-validation errors. Multiline Zod errors commonly begin with only [; the resulting refusal becomes effectively <path>: [. Fix the file…, while findUnreadableProjectConfig() does not print the omitted details elsewhere. Preserve the first meaningful validation issue rather than blindly taking line one. src/skill-content.ts:114

Review of Tencent#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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressed in 88829dd:

  • skill show share bypass (P1): fixed. skillShow asks the gate before autoDetectInit and refuses on a config block before any team-repo or agent lookup. On d51f412 the real CLI printed a user-scope share (exit 0) under a broken project config. Now it refuses with the file and the error (exit 1). Recall and read-only blocks still fall through, so a member's own share still shows. Test: skill-show › "refuses share under a broken project config before searching the user config it falls back to".
  • Verification record (P1): the body now has the real-CLI matrix for git and github, with four agents each, plus npm run build, and a before run on d51f412. gitlab is not run yet: teamai's GitLab client sends PRIVATE-TOKEN and needs a PAT. I will add that leg here.
  • Doc sync (P1): fixed in the four files you named, plus three more a grep found: skill-data/core/SKILL.md, core/references/contribute-member.md and setup/references/setup-admin.md. The usage guides and designs/skill-serving.md now also say a validation failure names the field.
  • Zod first line (P2): fixed at the source. Every config loader reports a validation failure as field: reason on one line. The refusal went from <file>: [. Fix the file… to <file>: repo: Expected object, received number; username: Required. Fix the file…, and the "error is printed above" line for a user config no longer points at a JSON dump.

Left as known issues in Merge Danger: skill show <non-share> on an unreadable user config still ends in a stack trace, and skill list under a broken project config still lists the fallback team's skill names.

@SaulMoro

Copy link
Copy Markdown
Collaborator Author

GitLab leg done: same matrix script against a private throwaway gitlab.com fixture with a PAT, four agents. Every row matches the git and github legs, so the table in the body now covers all three providers. npm run test:e2e on 88829dd: 225 passed, 26 skipped.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] The legacy teamai contribute-check --stdin gate ignores the hook payload’s cwd. contributeHintAllowed() resolves configuration from process.cwd(), while the session analysis correctly uses stdinData.cwd. If the legacy hook process starts outside the active project, it can allow a reminder using the user config even though teamai skill get share in the project refuses because its project config is broken. Pass the payload CWD into the gate/config detection. src/contribute-check.ts:716
  • [P1 blocking] The PR description still lacks the required verification record. It does not explicitly report npm run build, and explicitly states that the required real-CLI github and gitlab provider rows were not run.
  • [P2 non-blocking] Deployment failures during automatic SessionStart pulls remain silent. The detached hook process discards stderr, while log.warn neither writes to debug.log nor emits in CLI silent mode; replacing the previous debug logging therefore removes the only persistent diagnostic. Preserve a debug/error-log record in both deployment catch paths. src/pull.ts:1023

Resolved

  • The earlier skill show share project-config bypass is fixed.
  • The affected documentation and skill-data/ guidance are now synchronized.
  • Zod validation failures now retain meaningful field-level details instead of being truncated to [.

Tests were not run, per the review-only instruction.

`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
(Tencent#748). It now returns false there. Serving the skill stays fail-open.
`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.
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.
Known issues left by Tencent#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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressed the second review in the commits after 88829dd:

  • Legacy gate ignores the payload cwd (P1): fixed in d43c4e7. teamai contribute-check --stdin changes into the payload cwd before asking the gate, as hook-dispatch already does. The e2e test "asks the gate about the payload cwd, not the directory the hook process started in" starts the process in HOME, where the user config has recall on, while the session ran in a project whose config does not parse. It printed the reminder before the fix and is silent now. The real-CLI legs check the same case.
  • Verification record (P1): the body was updated at 11:21 and 11:23 UTC with npm run build, and with git, github and gitlab rows. This review seems to have read the body from before that. The body now also has the rows for the commits below.
  • Silent SessionStart deploy failures (P2): fixed in aa3b0cd. Both catch paths go through warnStubNotDeployed, which warns and writes the same line to ~/.teamai/debug.log, since log.debug persists even in silent mode. pull-skip-sync asserts it on the fast path and on a full sync.

Also in this push:

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] A project config with scope: user—or with scope omitted, which defaults to user—still bypasses the new gate. readConfigFrom() silently returns null without invoking onUnreadable, so detectTeam() falls through to the user config and can expose another team’s share skill or emit its reminder. Treat an existing config in a project location with a non-project scope as unusable. src/config.ts:435
  • [P1 blocking] The legacy hook continues after process.chdir(stdinData.cwd) fails, so contributeHintAllowed() evaluates the launcher’s unrelated working directory. A valid user/project config there can permit a reminder even though the payload project cannot be checked. Withhold the reminder when the payload CWD cannot be entered, or pass the CWD directly into the detector. src/contribute-check.ts:718
  • [P2 non-blocking] Changing the legacy-prune exception from log.debug to log.warn removes its persistent diagnostic during detached SessionStart pulls: hook stderr is discarded and warnings are not written to debug.log. Keep a debug-log write alongside the warning. src/builtin-skills.ts:448
  • [P2 non-blocking] warnStubNotDeployed() sends the identical message through both log.warn and log.debug; with --verbose, both methods print, so every failure appears twice. Use a file-only logging path or avoid the second console emission in verbose mode. src/pull.ts:740

Resolved

  • The previously reported malformed/invalid project-config bypass in skill show share is fixed for parse and schema-validation failures.
  • The PR description now records npm run build and real-CLI verification across git, github, and gitlab.
  • The previously identified documentation and skill-data/ invalid-config wording is synchronized.
  • Zod errors now retain field-level details.
  • The legacy hook now uses a valid payload CWD before gating.
  • Failed stub deployments now reach debug.log.

Tests were not run, per the review-only instruction.

…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.
`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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressed the third review in 79e4432 and b0583f5:

  • Project config that is not scope: project (P1): fixed in 79e4432. readConfigFrom reports such a file to the unreadable sink instead of skipping it silently, so detectTeam treats it as unusable and never reads past it to the user config. The one exception is the user config itself, as when running from HOME, compared by real path. config-not-initialized covers both. On the real CLI, skill get share now refuses with <file>: it is scope: user, but a config inside a project must be scope: project.
  • Legacy hook after a failed chdir (P1): fixed in 79e4432, the second option you offered. The chdir is gone, and contributeHintAllowed(stdinData.cwd) hands the payload cwd to detectTeam, so the directory the process started in is never read. A cwd that no longer exists holds no project config, so only the user config is asked there, which matches fix: keep TeamAI out of projects that never set it up (#748) #753's resolveConfigForDir. The e2e test "never asks the gate about the launcher directory, even when the payload cwd no longer exists" launches in a broken project, sends a deleted cwd, and gets the user config's reminder.
  • Legacy prune warning lost in detached pulls (P2), and the doubled line under --verbose (P2): fixed together in b0583f5. log.warn now records to debug.log like debug and error do, so every warning in a detached SessionStart pull leaves a trace, the prune among them. warnStubNotDeployed drops its second log.debug.

Left for after #753: the dispatcher's contribute-check and pending-hint handlers still call contributeHintAllowed() after hook-dispatch's own chdir. #753 rewrites hook-handlers.ts, so the cwd should be threaded through there once it lands.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] The dispatcher’s new reminder gate still ignores the hook payload’s cwd. It calls contributeHintAllowed() without an argument and relies on process.chdir; when that chdir fails, the gate evaluates the launcher’s unrelated directory and may emit a reminder despite an unusable payload project. Pass resolveHookCwd(stdin) directly, as the legacy command now does. src/hook-handlers.ts:254
  • [P1 blocking] A project config symlinked to ~/.teamai/config.yaml bypasses the non-project-scope gate. isUserConfigFile() compares resolved file targets, so the project-local symlink is mistaken for running from HOME; detection silently falls through to the user team and can serve its share. Determine the HOME exception from the project location, not the config file’s realpath. src/config.ts:475
  • [P2 non-blocking] The temp-directory retry and command-validator expansion are unrelated drive-by changes, contrary to the repository’s surgical-change rule. Move this independent test hardening to separate PRs. src/__tests__/git-kind-learnings.test.ts:34 src/__tests__/skill-commands-exist.test.ts:90

Resolved

  • The ordinary missing/scope: user project-config cases are now blocked.
  • The legacy hook uses the payload CWD and handles deleted payload directories consistently.
  • Stub deployment and legacy-prune warnings now receive a persistent diagnostic without duplicate verbose output.
  • Documentation, skill guidance, actionable validation errors, and the required build/provider/e2e evidence are present.

Tests were not run, per the review-only instruction.

…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.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Addressed the fourth review in 15a5b5d:

  • Dispatcher gate ignores the payload cwd (P1): fixed. The contribute-check and pending-hint handlers now call contributeHintAllowed(resolveHookCwd(stdin)), the same as the legacy command. The new hook-handlers test makes the unreadable-config mock answer "broken" only when it is asked without a cwd, so it fails if the gate falls back to the process's directory. It fails before the fix and passes now. On the real CLI, hook-dispatch itself still fails on a payload cwd that no longer exists, before any handler runs (Cannot use simple-git on a directory that does not exist). The result is silent, and fix: keep TeamAI out of projects that never set it up (#748) #753 fixes that part.
  • Project config symlinked to the user config (P1): fixed. The HOME exception is now decided by where the project is (isUserTeamaiDir: the data dir is <projectRoot>/.teamai and the project root is HOME, compared by real path), not by the file's target. config-not-initialized covers the symlink, and the HOME case still passes. The real-CLI legs have a "project config symlinked to user" row: skill get and skill show refuse, and both hooks stay silent.
  • Drive-by test changes (P2): kept, and here is why. The temp-dir retry in git-kind-learnings fixes an ENOTEMPTY this PR's own CI run hit (Node 22 ubuntu), so without it the PR cannot go green reliably. The unknown-subcommand check in skill-commands-exist is one of the feat(skill): serve builtin skill content from the CLI, deploy a discovery stub #699 review follow-ups this PR exists for (see the Summary). If maintainers still prefer them split out, I'll move both into their own PR.

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.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] pathExists(cwd) treats every filesystem error as “missing,” so an inaccessible or non-directory hook cwd falls back to requireInit() and may allow a reminder using an unrelated user config. Only ENOENT should trigger the deleted-directory fallback; other failures should withhold the reminder. src/skill-content.ts:110
  • [P2 non-blocking] skill show share loads the configuration twice—once through resolveServableSkill() and again through detectTeam()—so invalid user/team configs emit duplicate errors and repeat detection work. Human-readable skill list has the same issue through skillCatalog() followed by detectTeam(). src/skill-cmd.ts:52 src/skill-cmd.ts:147
  • [P2 non-blocking] Persisting every log.warn globally is broader than the stub-deployment diagnostic being fixed and now writes unrelated raw subprocess errors and paths to disk. Prefer an explicit file-only diagnostic for the affected deployment/prune paths. src/utils/logger.ts:145
  • [P2 non-blocking] The temp-directory retry and command-validator expansion remain unrelated drive-by changes, contrary to the surgical-change rule. Move them to separate PRs. src/__tests__/git-kind-learnings.test.ts:34 src/__tests__/skill-commands-exist.test.ts:90

Resolved

  • The dispatcher now gates using the payload cwd.
  • Project configs symlinked to the user config no longer bypass the scope gate.
  • Earlier config, documentation, hook, and persistent-diagnostic findings are addressed.
  • The PR description now contains sufficient build, test, real-CLI, provider, and agent coverage.

Tests were not run, per the review-only instruction.

…t 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.
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

Copy link
Copy Markdown
Collaborator Author

Merged main now that #753 has landed (6573bd0), and addressed the fifth review in aaaf0ac and 84a461d:

  • Merge with fix: keep TeamAI out of projects that never set it up (#748) #753: fix: keep TeamAI out of projects that never set it up (#748) #753's local contributeHintAllowed is dropped. Both hooks use the one in src/skill-content.ts and pass it the payload cwd. In the legacy command, fix: keep TeamAI out of projects that never set it up (#748) #753's resolveConfigForDir check and this gate run one after the other on the same cwd. The docs combine both PRs' sentences. With fix: keep TeamAI out of projects that never set it up (#748) #753 in, hook-dispatch no longer fails on a deleted payload cwd, so the real-CLI row "dispatcher, launched in a broken project, payload cwd deleted" now gets the user config's reminder, where before it was silent.
  • Only ENOENT is "deleted" (P1): fixed in aaaf0ac. detectTeam uses stat. ENOENT falls back to the user config, and any other failure (no permission, a path through a file) is unusable, which withholds the reminder. The hook-handlers test uses an ENOTDIR cwd. The real-CLI legs check both hooks with that cwd, and both are silent.
  • Config loaded twice (P2): fixed in aaaf0ac. skill show and skill list detect the team once and pass it to the gate (resolveServableSkill(name, roots, team), skillCatalog(roots, team)), so a broken config is reported once. The tests count one load, and the real CLI prints the refusal once.
  • Every warning persisted (P2): fixed in 84a461d. log.warn is console-only again. The new log.persist writes one line to debug.log and never to the console. Only the two stub deploy catches and the legacy prune catch use it.
  • Drive-by test changes (P2): kept, for the reasons in the previous reply. The retry fixes an ENOTEMPTY this PR's own CI hit, and the subcommand check is one of the feat(skill): serve builtin skill content from the CLI, deploy a discovery stub #699 review follow-ups this PR collects. Both are test-only, and moving them out would leave this PR's CI flaky until the other one lands.

@jeff-r2026
jeff-r2026 merged commit 72305c6 into Tencent:main Sep 23, 2026
10 of 11 checks passed
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.

2 participants