mirror of
https://github.com/Tencent/teamai-cli.git
synced 2026-10-02 03:14:40 +08:00
1fd400ecfa80b464ffabdc06a8e01f8dfd008f63
894
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1fd400ecfa |
fix(pull): sync nothing in a project whose config cannot be read (#784) (#792)
* fix(pull): sync nothing in a project whose config cannot be read (#784) Detection skips a project config it cannot read and returns what loads next: a legacy .teamai/ behind a broken partition, which may name another team, or the user scope. pull() deployed and reported for that team, and the session-start hook did so on every session (reports-wt/ and learnings-wt/ appeared in the legacy .teamai/). pull() now listens for the unreadable config, syncs no scope, prints the problem with BROKEN_CONFIG_ADVICE and exits 1. A silent pull (the session-start hook, or a pre-dispatch hook running `teamai pull --silent`) records it in debug.log only. Agent-root seeding and the package hint refuse the same way, so a session start there does nothing. Hooks and usage already follow this rule since #748. The message trimming detectTeam used moves to config.ts as describeUnreadableConfig so both share it. * fix(pull): address pre-review findings (#784) - The session-start handler returns when the dispatcher resolved no config for the hook's cwd, which is what an unreadable project config resolves to since #748. That one guard replaces the unreadable-config sinks added to seedProjectAgentRoot and the package-hint context, and follows the #769 contract that handlers read their scope from the dispatcher. The handler tests that exercise cwd routing now pass a resolved scope; a new one pins that nothing runs without one. - CHANGELOG and usage guide (en, zh-CN): a session start there runs no pull; only `teamai pull --silent` from a pre-dispatch hook writes the reason to debug.log. - skill-data troubleshooting: what `Nothing was synced` means, and that moving the config aside and re-running init needs the user's consent. * fix(pull): address CI review findings (#784) - The session-start pull is registered with `requiresConfig` instead of returning early inside the handler: the dispatcher drops it wherever no config resolves, which covers an unreadable project config (#748), and spawns no detached pass for it. Where no teamai config exists at all it did nothing on main either (no scope to pull, no project root to seed, no config for a package hint). The #748 registry test and the docs no longer list it as machine-level work. - `teamai pull --silent` exits 1 on the refusal too. Pre-dispatch hooks run it as `… 2>/dev/null || true` (`; exit 0` on Windows), so hosts still see success. - The dispatch-scope test asserts the pull is skipped and resets the pull mock it queues. - skill-serving design doc: `teamai pull` now reports an unreadable project config too. skill-data troubleshooting: `teamai doctor` can pass there. * fix(hooks): still pull at session start where teamai is not set up (#784) A null config from the dispatcher means either "no teamai here" or "the project config cannot be read". Gating the session-start pull on `requiresConfig` stopped it in both; only the second must stop it. The handler now asks `findUnreadableProjectConfig` for the hook's cwd when no config resolved (a cwd that no longer exists holds none) and runs nothing when it reports a file. Everywhere else it runs as on main, so the docs list it as machine-level work again. |
||
|
|
dc233e4489 |
fix(votes): keep votes with the scope they were cast in (#787) (#793)
* fix(votes): keep votes with the scope they were cast in (#787) Every scope recorded into one ~/.teamai/votes/<user>.yaml, so a vote cast in one project (recall feedback, a recall search, a Stop whose push failed) was pushed to the team of whichever scope synced next: the leak usage.jsonl had before #758. Votes now live in the data home of the scope that resolves for the session: <dataHome>/votes/ for a project, ~/.teamai/user-votes/ for the user scope. The Stop hook uses the config the dispatcher resolved; the pull report, recall search, `recall feedback` and the vote view read only that scope's votes, and the CLI readers resolve it with resolveConfigForDir, so an unreadable project config falls back to no other scope: `recall feedback` exits 1 and the vote view names the broken file. The shared ~/.teamai/votes/ is never read. Its V2 `votes` map is the last merged remote snapshot of whichever team synced, not this scope's history, and seeding a scope from it would let `recall feedback --negative` push a decrement and merged timestamps derived from another team. The remote votes/<user>.yaml format is unchanged. * fix(votes): address pre-review findings (#787) - recall search: in a project whose config cannot be read, detection falls back to another scope; record no recalled count there, so the vote cannot reach that scope's team. Which scope the search itself uses stays #796's. - recall feedback: with no project config and an empty or invalid user config, name the file and the fix (requireInit's error) instead of "not set up here". - CHANGELOG: note the recall search case; the shared directory is never read or pushed "by this release" (an earlier release still pushes it). - Design doc: getUserVotesDir() is the exception to "getters unchanged". * fix(votes): address second pre-review round (#787) - recall feedback: name an unusable user config through throwMissingOrInvalid (now exported) instead of re-running requireInit, which loaded the config twice and printed its parse error twice. - recall search: a project detection that throws is treated like an unreadable config, so no recalled count lands in the fallback scope. - The recall-search test now asserts the search ran and that neither the shared directory nor the broken project's votes/ was written; it fails on origin/main too. - Design doc: re-wrap the edited paragraph. * fix(votes): record recall votes from a deleted cwd in the user scope (#787) The previous commit treated any throw from recall's project detection as an unreadable project, including a cwd that no longer exists. Such a cwd holds no project and resolves to the user scope everywhere else (resolveConfigForDir, detectTeam), so its recalled counts belong there. Tests pin that case and the single parse-error line for an invalid user config in recall feedback. * fix(votes): address CI review findings (#787) - getVotesDir: a historical project-scoped ~/.teamai/config.yaml with no projectRoot (schema-valid, not backfilled) made getDataHome throw, so recall, feedback and the Stop hook recorded no vote. It lives in ~/.teamai, as recall and viz already treat it, so its votes go to the user scope's user-votes/. - git-native-memory design doc: the local votes path is user-votes/. * fix(votes): address CI review (#787) - recall feedback --negative counts the upvotes the scope's own team already holds (its reports checkout's votes/<user>.yaml plus the deltas not yet pushed). A scope's file starts empty on upgrade, so a doc upvoted before it was rejected as not found, or as having no upvotes once a later recall counted it. The shared ~/.teamai/votes and other scopes' teams are never read. - The adoption judge (#723, merged meanwhile) recorded into and synced from the user scope's votes in every scope, which pushed the user scope's pending votes to the project's team. It uses the scope's votes like the Stop handler. - votes-scope tests: Stop transcripts prove adoption with a Read of the recalled file (#723), and the update.js mock keeps the real lock. |
||
|
|
fd0e913814 |
fix(stats): await the async dashboard scope filter (#795) (#806)
* fix(stats): await the async dashboard scope filter (#795) #795 made filterEventsByScope async and changed its argument from a projectRoot/excludeProjectRoots filter to the scope config, while #771 still called it synchronously with the old filter. On main, tsc fails in stats.ts and every stats-scope test throws "events is not iterable"; `teamai stats` crashes once there are dashboard events. stats now awaits the filter and passes the scope config, the call pull makes, which is what #771 set out to do: show what the report sends. The cwd-based project-root resolution is gone with the old argument. The user-scope test followed pull's rule before #795 (keep events that carry no dataHome); it now follows the current one: the user scope never reports them, so it does not count them. * fix(stats): subtract the scope's own reported snapshots (#786) Since #795 each scope reports against its own reported-*.json under its data home, and the shared ~/.teamai/dashboard files are no longer written. stats still subtracted the shared files, so after the upgrade every session reported since counted twice in the headline. stats reads the snapshots through team-push's readers with the scope config, including the one-time seed from the shared file. |
||
|
|
8cee7ab23e |
fix(migrate): keep the legacy .teamai/ while the partition config cannot be read (#797) (#799)
* fix(migrate): keep the legacy .teamai/ while the partition config cannot be read (#797) planMigration took a partition config.yaml that merely existed as a built partition and planned a retire-only cleanup, so the first init/pull/push after the partition file broke renamed the legacy directory to .teamai.bak although it held the only config that still loaded. Only a partition config that detection's own reader (readConfigFrom) accepts now counts as built; one that exists but cannot be read plans nothing, and the next write command after the fix retires the legacy dir as before. The re-check under the sync lock in runMigration uses the same rule, so a broken file that appears between planning and locking skips instead of retiring. * fix(migrate): address pre-review findings (#797) - Warn with the file and the first line of the reason when an unreadable partition config holds the migration back. The skip was silent, so a member had no signal which file to fix, including under --dry-run. The text says what happens next and hedges for a file caught mid-write. - Stand down when the partition dir exists without a config.yaml. Keeping the legacy dir made "move it aside and run teamai init" (BROKEN_CONFIG_ADVICE) reach the full copy, which removes the partition dir before renaming the staged copy in and so deleted its pending learnings, env and clone. The re-check under the lock stands down on an existing partition dir too. - Decide built / unreadable / absent in one helper that uses detection's own onUnreadable report, so the plan and the re-check cannot drift. - Tests: a real YAML syntax error, a partition config that is not scope: project, the moved-aside sequence, a fresh project still planning 'full', and the re-check retiring when a readable partition appears after planning. - Design doc and CHANGELOG: list every cause detection reports and the guard. * fix(migrate): address CI review (#797) The upgrade note in both usage guides promised an unconditional migration; it now says a partition whose config.yaml cannot be read, or is missing, keeps .teamai/ and what the member does next. The full copy's re-check under the sync lock logged only at debug level when it stood down; it now gives the same actionable warning as the planner. |
||
|
|
352cfc4ccc |
fix(report): each scope keeps its own reported dashboard snapshots (#786) (#795)
* fix(report): each scope reports only the dashboard sessions recorded in it (#785) Every scope read one machine-wide events.jsonl and picked its sessions out by cwd prefix. The user scope excluded nothing, so a user-scope pull reported every project's sessions (and, through the shared reported snapshots, took them from the project's own report); Copilot sends no cwd, so a project never reported its Copilot sessions; and a raw cwd under a symlink or /tmp never matched the realpath'd projectRoot. The hook now stamps each event's dataHome with the data home of the scope the dispatcher resolved (the key the per-scope usage file already uses), and a report keeps only its own scope's events, comparing realpath'd keys. A project also owns its in-repo .teamai key, where hooks record until migration moves it to a partition. Events written before this carry no dataHome: a project keeps those whose realpath'd cwd is under its root, the user scope never reports them. The log stays machine-wide for the dashboard UI, stats --by-repo, session save and the contribute check. Removes the excludeProjectRoots option, which pull only ever passed as [] (the user target exists only when no project config resolved), and the projectRoot option now carried by selfConfig. The usage guide documents how to remove by hand a skill an earlier release pushed into stats/<user>.yaml from another project. * fix(report): each scope keeps its own reported dashboard snapshots (#786) The report sends per-session deltas against reported-*.json snapshots that every scope shared. A session whose events belong to two scopes (a cd into another project mid-session) was then reported by the first scope, and the second compared its own part with the first scope's totals and sent nothing. Each scope now keeps its snapshots in <dataHome>/dashboard/, and the user scope, whose data home holds the shared files, in user-reported-*.json. The first time a scope needs one it copies the shared file, so the first report after the upgrade sends nothing already reported; after that it reads only its own. The user scope moves too, unlike the ticket proposed: had it kept writing the shared file, a project seeding later would copy the user scope's part of a split session and report nothing for its own. The shared file is no longer written, except by an earlier release after a rollback, which only a scope not yet seeded reads. |
||
|
|
a84bf6fbba |
chore(review): scope e2e to behavior changes, drop provider×agent matrix, add P3 (#781)
* chore(review): scope e2e requirement, drop provider×agent matrix, add P3 The auto-review gate over-flagged: it required a full provider × agent e2e matrix as [P1 blocking] on every PR, raised theoretical/low-confidence risks at blocking severity, and repeated already-resolved findings. - e2e record is [P1 blocking] only for behavior-changing PRs; docs-only / tests-only diffs need none. One representative real-CLI run is enough; missing extra provider/agent coverage the author flagged as untestable here or deferred to CI is at most [P3 nit], never blocking. - Add a third severity [P3 nit] (minor/optional polish, theoretical edge case, coverage deferred to CI). - Tell the reviewer not to over-review: report only high-confidence findings, prefer few high-signal over exhaustive, never restate a resolved finding. Kept in sync across AGENTS.md (## Code Review Rules, the trusted base criteria) and the workflow prompt in codex-review-on-assign.yml. * chore(review): relax PR 前测试 matrix, gate P1 by evidence not count Address SaulMoro's changes-requested on #781: - The provider × agent matrix requirement still lived in `## PR 前测试` (AGENTS.md/CLAUDE.md), which the Codex reviewer loads whole — so the matrix could return as a P1 despite `## Code Review Rules` relaxing it. Relax `## PR 前测试` to match: one representative real-CLI run suffices, docs/tests-only needs no e2e, extra provider/agent coverage defers to CI. - Replace "prefer a few high-signal findings" (which caps real bugs and pushes them to later passes) with evidence-gated severity: every [P1 blocking] must cite a concrete failure scenario or the exact rule it breaks, else it drops to P2/P3. Count is uncapped; real bugs surface in one pass. Synced into the workflow prompt. --------- Co-authored-by: review <review@local> |
||
|
|
97be092633 |
feat(projects): add admin commands to manage the projects manifest (#756) (#774)
`manifest/projects.yaml` could only be edited by hand. Add `teamai projects add/update/remove`, mirroring `roles add/update/remove`: each edits the manifest, validates it with the same checks a load applies, and opens a PR, with --dry-run to preview. The first `add` creates the file. - `add --namespaces` sets one namespace set on every resource type. - `update --add-namespaces/--remove-namespaces` edits each type's own list, so hand-edited per-type layouts survive; emptying a project is refused. - `remove` warns about directories that still have the project active. The pull/branch/PR plumbing moves from roles-cmd.ts into manifest-edit.ts so both commands share the single-repo worktree handling. An e2e test drives add -> pull, update -> pull and remove -> pull through the built CLI: after `remove`, a member that still has the project active has its deployed skills, rules and agents reclaimed on the next pull. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aa6a8830b4 |
fix(webhook): report failed deliveries in webhook test and log the last attempt (#777)
sendToEndpoint swallowed every failure without telling its caller, so `teamai webhook test` printed "Webhook test successful" for an endpoint that answered 4xx/5xx or timed out. A 5xx or 429 on the final attempt was not logged at all, and a timeout was retried immediately instead of backing off. sendToEndpoint now resolves to whether the endpoint accepted the event, logs the final failure with its cause, and backs off after a timeout like after any other failure. `webhook test` reports success only for a delivery that went through; sendWebhook keeps never failing the calling command. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c3bfef38f8 |
fix(stats): count each session once and keep other projects out (#771)
* fix(stats): count each session once and keep other projects out showStats added the whole machine's local events.jsonl metrics to the scope's already-reported totals, so every session a pull had reported — and that stays in the event log until compaction — was counted once by the team total and once again locally, and sessions whose cwd belonged to another project were added to this scope's totals too. Filter the event log the way `pull` reports it (a project scope keeps only sessions under its own root, the user scope excludes them) and add only what the scope has not reported yet, derived from the same per-session reported-* snapshots the report path advances, so the local figure agrees with the team's instead of exceeding it. The per-repo and by-hour breakdowns use the same filtered log, keeping them consistent with the headline numbers. * fix(stats): align the breakdowns with the headline and resolve the project root independently Three points from the review of the double-count fix: 1. The project root is resolved on its own with detectProjectConfig(), the same call `pull` makes, instead of being read off the resolved scope config. A user-scope config carries no projectRoot — that field is attached only when a PROJECT config is detected — so the old expression could never populate the user scope's exclusion list. 2. `--by-repo` and `--by-time` now describe the same local part the headline adds to the team totals. They consumed the whole retained event log, so with a reported session still on disk the headline said "1 session, 300 tokens" while the breakdown said "2 sessions, 1.8K tokens" — two numbers from one command that could not both be right. unreportedDashboardStats now also returns the set of sessions the scope still owes the team, and the breakdowns filter to it. 3. The regression tests assert token totals, not only sessions and conversation turns, since over-counted tokens were half the bug. Verified through the built CLI in an isolated HOME: the headline and the per-repo breakdown now report the same sessions, turns and tokens. * fix(stats): keep the breakdowns reading the scope's own event log The previous round filtered the breakdowns down to unreported sessions so they would match the headline. That was the wrong trade: the headline adds this machine's unreported sessions to totals that already include other machines and sessions compaction has dropped, so it can never equal a per-repo or per-hour view of the local log. Filtering made the breakdowns show neither a total nor a delta — a fully reported project vanished from `--by-repo` entirely. Restore the full scoped log for the breakdowns and say in the comment what question each answers. What both must share is the SCOPE, and the breakdown tests now pin that: removing the scope filter turns the cross-project case red, which the earlier assertion missed because it read only the first matching row. * fix(stats): subtract reported totals only when they were read The delta path keyed off `config` alone, so a scope whose team stats could not be read at all — no stats file yet, an unreadable one, a reports worktree that is not there — still had its local snapshot subtracted. The snapshot records what this machine pushed, not what the team holds, and with the reported side null it hid sessions the member could see happening, down to "No usage data yet." Require `reported` as well, so the local aggregate is shown when there is no team total to reconcile against. The changelog entry no longer claims the breakdowns match the headline; they read the same scoped log and answer a different question, and the heading says so. * docs(stats): say which log the optional breakdowns read The `--by-repo` heading was changed to name the local event log, but the flag descriptions and the generated command reference still said "Break usage down per repository", and `--by-time` kept the old "(local time)" heading — so the two optional views disagreed with each other and with the changelog entry about them. Name the source in both flag descriptions, label the by-hour view the same way, and regenerate `commands.md` per AGENTS.md. * test(stats): cover the project scope against its reported totals The end-to-end shape the review asked for was missing: a project scope with reported team totals, one reported session still in the event log, one new session in this project, and one session belonging to a different project. It now asserts 3 reported + 1 new = 4 sessions, 301 turns, and a breakdown holding only this project's rows. Dropping the scope filter turns four tests red at once (the new one reporting 5 instead of 4), so the filter is pinned rather than assumed. * fix(stats): do not subtract a snapshot the team file never received The previous guard only checked that the reported totals were readable. The local snapshots are machine-global while the team file is per-scope, so a snapshot can name a session this team never got — an empty team file alongside a populated snapshot. Subtracting then undercounts, down to "No usage data yet." with sessions sitting in the event log. Trust the snapshot only when the team total is non-empty: the report path writes the team file and advances the snapshot under the same lock, so a non-empty total is what licenses the subtraction. * docs(stats): correct the changelog's trust and scope claims The note inverted the guard's condition: the code trusts the reported snapshots only when the team total is NON-empty, while the wording said an empty total is what licenses trusting them. Say what the code does. It also claimed the user scope excludes project roots. A user config resolves only when detectProjectConfig() found no project, so there is no root to exclude and the log passes through unfiltered. State that instead of asserting an exclusion the branch cannot perform. No behaviour change; wording only. --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
89ac24a3d4 |
fix(votes): collect upvote adoption from tool-use evidence + opt-in LLM-judge (#723) (#744)
* fix(votes): collect upvote adoption from tool-use evidence + opt-in LLM-judge (#723) Fixes #723. upvoted_count was structurally near-zero because collecting an upvote depended on the main agent voluntarily emitting the <!-- teamai:referenced-doc-ids: [...] --> marker (~3.7% in the issue's data). This collects adoption WITHOUT AI self-declaration and removes that mechanism entirely. Signals: - Tool-use evidence (always on): a recalled doc is adopted when the MAIN agent opens its file (Read/Grep/Glob/Bash). Sidechain tool calls excluded; gated to the recalled set; full-path or >=2-segment suffix match (no bare-basename cross-attribution); relative `./x` normalized; Bash harvests FILE-OPERAND tokens only (grep patterns, option values, `#` comments and `>`/`>>`/`2>` redirection targets never credit; `-e/-f` frees the file operand); a FAILED tool_result (is_error) revokes its refs; Glob/Grep matched files are harvested from the reader RESULT (input path is often just a directory). - Opt-in background LLM-judge (TEAMAI_UPVOTE_JUDGE=1, off by default): a detached Stop handler asks the local signed-in CLI whether the latest reply used each still-uncredited recalled doc, grounded in the doc's real content. Grounded-only (a candidate whose excerpt cannot be securely read is dropped, so a forged recall marker cannot earn an upvote from its id alone); fail-closed excerpt read (lstat + realpath + .md-in-trusted-root); trusted roots derive from learningsRoots + pendingLearningsDir; prompt fences excerpts/reply as untrusted data. Each recalled doc is judged at most once per session via a per-doc judged-record (crash-safe: recorded AFTER the CLI call, so a killed run retries; later turns still judge NEW docs) — no exclusive claim marker. Scope attribution: recall labels each hit [project]/[user]; while a project is active a doc recalled from the inherited USER scope is read-only and is NOT upvoted into the project team (matches recall.ts's recalled_count scoping and the documented rule). Recall regions from a reader tool_result (file content the agent opened) are UNTRUSTED — parsed into throwaway sinks so a forged region can neither manufacture a doc-id nor poison a real doc's scope/path; only assistant text, non-reader results, toolUseResult.stdout and plain-string content are trusted. Concurrency & idempotency: - All vote mutators serialize on one cross-process file lock; the votes file is written atomically (temp+rename) so a killed detached judge cannot leave a torn file that loadUserVotes would read as empty. creditedDocIdsForSession reads the YAML directly (never loadUserVotes) so a v1 file is not auto-migrated by an unlocked read. - Per-session dedup ledger lives inside the votes file under the same lock; its TTL is measured from the session's first-seen time (firstTs) so a long/resumed session cannot re-credit an already-adopted doc; TTL-pruned every Stop; local- only (never synced to the team repo via mergeDeltas). Also: deterministic "[teamai] Adopted team knowledge this session: <ids>" summary (once per session, tools that print the Stop payload); finalAssistantText joins all text blocks of the final message and accumulates across records sharing a message id; recall lock-exhaustion is logged honestly; removed the referenced-doc-ids parser/nudge/stash and its rules text in builtin-rules.ts / pull.ts. gitOnly and promote thresholds unchanged. Docs: document TEAMAI_UPVOTE_JUDGE (usage-guide en+zh); note OpenCode does not participate in adoption (session.idle carries no JSONL transcript_path); git-native-memory design flow + both decision tables updated to adoption-driven. * fix(votes): tighten adoption evidence in transcript parser (#723) Address the #744 review's precision findings in adoption collection: - Reader tool results no longer harvest every Markdown-looking string; only whole-line file paths and grep `path:line:` prefixes count, so an unrelated notes.md that merely mentions a recalled doc's name cannot credit it. - Relative tool-call paths are resolved against the transcript entry's cwd before matching, so a relative `learnings/setup.md` opened in one checkout is not misattributed to a recalled doc under another checkout. - Shell command splitting is quote-aware, so a `|` inside a quoted grep pattern no longer forges a synthetic `cat` segment that falsely credits a doc named only in the pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(votes): dedup the LLM-judge via the vote ledger, drop session marker (#723) The per-session "judged-docs" marker introduced crash-unsafe and once-per-session-violating behavior (#744 review): a transient CLI failure was recorded as judged and never retried, a positive verdict was marked judged before the vote write so a busy lock lost it permanently, an early negative verdict permanently excluded a doc a later reply actually used, and the 24h marker TTL re-credited docs on a resumed session. Remove the marker entirely and rely on incrementUpvoted's atomic, lock-protected per-session ledger, which already dedups credits across the foreground and background passes. A verdict is now recorded only after the atomic credit succeeds; an unadopted recalled doc may be re-judged on a later Stop (the judge is opt-in), which is the accepted cost of removing the crash-unsafe marker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9d3a91cf76 | fix(push): honor explicit branch and protect dirty team clones (#690) | ||
|
|
57afe76810 |
feat(models): merge show into list with an optional profile (#782)
Model catalogs are small, so `teamai models list` now prints every profile in full: API key source, gateway, models by protocol, compatible agents, and where it is active. `teamai models list <profile>` narrows the output to one profile, and the separate `show` command is removed. |
||
|
|
82ecf4153d |
docs(skill): harden credential and consent guidance in teamai skill (#779)
ClawHub's SkillSpector scan flagged three doc-level issues in the teamai skill guidance: - PE3 (Credential Access): the GitLab setup showed a literal `export GITLAB_TOKEN=glpat-...`, which lands in shell history and process listings. Switch to a no-echo prompt, recommend a short-lived api-scope token, and unset it after init. - SQP-2 / P4 (silent install + behavior manipulation): the TGit login guidance told the agent to run install/login itself and 'never tell the user'. Require disclosing that it installs a binary and stores a credential, and getting the user's OK first (or showing the commands if they prefer). - SDI-4 (trigger ambiguity): publishing a skill from a plain-language request had no confirmation gate. Require showing what will be shared and getting an explicit go-ahead before teamai push / contribute. Doc-only; edits land in skill-data/ (served by `teamai skill get`). |
||
|
|
2ab697d062 |
fix(usage): discard pre-upgrade usage and stop falling back past an unreadable 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. |
||
|
|
a2f93ae3d2 |
fix(config): release only Claude's MCP servers on a root move; read the recorded root in import and skill tracking (#775)
A re-init that moved the Claude Code root handed the full team config to
reconcileMcpForConfig({ removeAll }), which walks every MCP-capable tool,
so Codex, Cursor and the rest lost their teamai-managed servers until the
next pull. The release now narrows the team config to Claude.
import --from-claude scanned ~/.claude/rules and skill-use tracking only
knew the static ~/.claude/skills; both now resolve the recorded root. The
resolution (project config governing the directory, else user scope) moves
into resolveMemberToolRoots so the local agent, import and tracking agree;
tracking resolves it from the hook's reported directory. The helper checks
that the directory exists before probing, as resolveConfigForDir does, so a
hook from a deleted worktree still records — this also stops the local
agent from throwing on a missing workspace path.
Follow-up to #728 (third review pass, findings 1 and 5).
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
ab3f01f014 |
fix(tgit): fetch gf CLI over HTTPS with checksum verification (#773)
The gf CLI installer downloaded the platform tarball over plaintext HTTP and piped it straight into tar with no integrity check, then executed the extracted binary. ClawHub's security scan flagged this (finding T03). Fetch over HTTPS to a unique temp file, verify its SHA-256 before extracting, then clean up. The mirror 302-redirects to a content-addressed backend whose URL path is the artifact's sha256 (the backend does not echo a checksum header), so the expected digest is read from the redirect target, falling back to x-checksum-sha256 for a direct serve. Fail closed when no digest is advertised. Update the TGit provider reference doc to match. Verified end-to-end on darwin-arm64: download, digest match, extract. |
||
|
|
da13c1119b |
feat(models): share gateway model profiles across agents (#675)
Add `teamai models` to point Claude Code, Codex, OpenCode, CodeBuddy, and
WorkBuddy at a team or personal model gateway.
- A team publishes `models/models.yaml` (id, name, base_url, api_key
placeholder, model_groups by protocol); each member keeps the API key
locally or as an environment-variable reference.
- `models switch` updates every installed, compatible agent (or `--agent`),
asks for a missing key once, and accepts `--model` for the default.
- `teamai pull` re-applies the team's latest catalog to agents already
switched to it; agents never switched are left alone.
- TeamAI records the managed fields before its first switch, skips agents
whose managed fields changed outside TeamAI, and `models restore` puts
the originals back. Claude's `/model` pick is not treated as a takeover.
- Claude family aliases map to matching gateway models or the default;
Buddy entries use `${VAR}` key references; Codex edits are parsed and
verified before writing.
- Push rejects an invalid catalog; user-scope uninstall restores model
settings first.
|
||
|
|
55b71efb69 |
feat(config): honor a relocated Claude Code config dir via toolRoots (#728)
Claude Code can move its whole user config directory with CLAUDE_CONFIG_DIR, but teamai resolved every Claude path from the team-wide toolPaths (.claude/...), so hooks, skills and rules were written to ~/.claude, which that Claude Code never reads, and doctor stayed green. Add a member-level `toolRoots` key to the local config. In user scope `scopedToolPaths` re-roots every path of the listed tool; a new `hookToolPaths` does the same for writes that land in HOME regardless of scope (hook injection/removal/listing, doctor's hook checks, the local agent). `teamai init` records CLAUDE_CONFIG_DIR into `toolRoots.claude` and keeps it across a re-init; `teamai doctor` reports when the variable and the effective root disagree. An explicit CLAUDE_CONFIG_DIR=~/.claude is recorded too: Claude Code then reads .claude.json from inside the directory, so the MCP companion file moves inside the root even when the root is unchanged. Accepted roots are one directory in HOME or .config/<name>, the shapes `toolInstallRoot` can express; the hook gates in hooks.ts now use it so hook and resource gates agree. Only `claude` is accepted for now: it is the one tool whose every user-scope write goes through toolPaths. Closes #725 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d729ad1750 |
fix(pull): pass force option through MCP reconcile (#762)
reconcileMcpAllScopes dropped options.force when calling reconcileMcpForConfig, so `teamai pull --force` never forced an MCP config overwrite even though force reaches every other reconcile path. Co-authored-by: Dongwoo Jeong <dongwoo.jeong@lge.com> |
||
|
|
34e65e9017 |
fix(dashboard): gate the legacy dashboard-report command on a config (#772)
* fix(dashboard): gate the legacy dashboard-report command on a config A current install writes only `teamai hook-dispatch`, whose dashboard-report handler declares `requiresConfig` and is dropped when no config resolves for the hook's cwd. The legacy subcommand stayed ungated, so a hook left behind by an earlier install kept recording dashboard events for every directory it fired in — including projects that never set up teamai, whose sessions were then reported by whichever scope pulled next. Apply the same gate `teamai contribute-check` was given in #748, asked about the session's cwd (or, for a host that sends none, the directory the hook runs in), so the two legacy commands behave alike. * test(dashboard): give the dashboard e2e fixtures a configured scope Both suites drive `dashboardReport()` directly, so they bypass the dispatcher and now hit the new config gate. Their payload cwd is a fixture path that need not exist, and resolveConfigForDir then falls back to the user scope — which nothing had seeded, so every hook call returned early and no event was ever recorded. Seed a user-scope config, which is what these pipelines already assume: an installed team reporting its own sessions. --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
5576b38db7 | fix(init): preserve additional roles selected at the prompt (#765) | ||
|
|
cc2772114f |
fix(hooks): run every handler in the scope the dispatcher resolved (#752) (#769)
* fix(hooks): run every handler in the scope the dispatcher resolved (#752) hook-dispatch resolves the scope from the payload cwd, then chdirs there. The correction keywords, votes-sync and webhook-dispatch read their config again through autoDetectInit() on the process cwd, so when that chdir failed (a deleted worktree) and the host started the hook inside another project, they used that project: its keywords, its member for the session's votes, and its webhooks. HookHandler.execute now receives the config the dispatcher resolved, and the three handlers read their scope from it. * fix(hooks): start the background pass when the hook's cwd is gone (#752) The detached child was spawned in the payload cwd. On macOS and Linux a cwd that no longer exists (a deleted worktree) fails the spawn, the error is swallowed, and no background handler runs: no session-start pull, webhook or update check. Start it in the temp dir instead, as the Windows WMI launch already does. The child resolves its scope from the payload, and its handlers now use that config, so the directory it starts in names no project. |
||
|
|
6a53b6f925 |
fix(lock): never rename over a lock that was just released or is still being written (#760) (#761)
* fix(lock): never rename over a lock that was just released or is still being written (#760) A stale verdict lets the reclaimer rename over the lock, and two live states read as stale: a file that vanished (released, and possibly re-created by a third process before the rename) and an empty file (its owner opened it but has not written it yet). lockState now tells live, stale and missing apart: a missing lock gets one more exclusive create, and an empty one reads as held until it has stayed empty for 5s (its owner died before writing it). 16 processes x 2000 attempts on one lock: 4-18 overlapping holders per run before, 0 after, with acquisitions in the same range. * fix(review): EPERM owner is live; cover the second pass; complete lock users (#760) - process.kill(pid, 0) throwing EPERM means the owner is alive under another user (e.g. `sudo teamai`); it read as stale and was renamed over. - Test the missing branch under the reclaim sentinel, where the rename lives. - Comment and design doc state exactly what reads as stale (an unreadable lock still does); CHANGELOG lists every lock user. * fix(review): publish the lock with its content in place (#760) An empty lock expired after 5s, so a creator stalled between open and write (SIGSTOP, sleep, slow I/O) could be taken over while it still held the lock. exclusiveCreate now writes the payload to a private temp file and hard-links it to the lock name (link fails with EEXIST like O_EXCL), so the lock never exists empty. Without hard links it falls back to O_EXCL, where the 5s grace still applies. The same create backs the reclaim sentinel. * fix(review): O_EXCL fallback verifies its payload; unreadable lock is held (#760) - Without hard links the lock is created with O_EXCL and sits empty until written. A creator stalled past the 5s grace could be replaced and still report success; it now reads the lock back and holds it only if its own payload is there. - A lock that exists but cannot be read (EACCES: another user's 0600 lock) read as stale and was renamed over; it now reads as held. - Use fse.link like every other lock operation, and mock it in update.test. * fix(review): a wx creator holds only a lock it wrote inside the grace (#760) - Without hard links, a reclaimer that read the lock empty could rename over it after the stalled creator had written and checked it. The creator now keeps the lock only if it wrote it within half the grace, so no reclaimer can have judged it stale; otherwise it gives it up. - An unreadable lock logs a warning naming the file. - Migration skips lock artifacts (<lock>.*.tmp, .sentinel, .new-*), which a contending pull creates and removes during the copy. - Stale comments; the stall tests restore their spies in afterEach. * fix(review): reclaim only a lock whose owner is provably dead (#760) Each timing rule for locks that name no owner (empty, partly written) left an ordering where two processes held the lock: a partial payload read as stale at once, an older teamai stalled past the grace, a slow fallback creator yielded but kept blocking. Only ESRCH now makes a lock stale; a lock that names no owner, cannot be read, or has an EPERM owner is held, and a warning names it so a crash leftover can be removed by hand. Drops the 5s grace, the 2.5s creator limit and the read-back. - parseLockContent: a bare legacy PID is valid JSON (a number) and was returned as unparseable, which only worked while unparseable meant stale. - Migration skips only the real lock artifact formats, not every <lock>.*. * test(lock): exercise the live-holder verdict; drop grace leftovers (#760) - "returns false when a live process holds the lock" failed on the temp write before any verdict ran; link now rejects with EEXIST on the lock path. - Remove Date.now/utimes staging the removed grace no longer reads, merge the duplicate empty-lock tests, restore the stall spies in finally. - Docstrings and design doc: only a lock that names no owner or cannot be read is warned about; the sentinel-steal residual is stated as it is. |
||
|
|
506d4c9148 |
docs(readme): mark Copilot CLI usage/sessions/dashboard as supported (#766) (#767)
PR #666 (feat: add privacy-safe Copilot telemetry) shipped Copilot support for the Team Improvement columns (usage, sessions, dashboard), but the support matrix in the README files still showed em-dashes for those three cells. Flip them to checkmarks in all five language variants so the matrix matches the shipped, tested behavior. Co-authored-by: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com> |
||
|
|
4d85bbc45e |
docs(test): name the live e2e suites in the integration stub (#770)
Signed-off-by: hiro-nikaitou <vieteviete@proton.me> |
||
|
|
dad371c360 | fix(agents): render codex TOML with multi-line literal strings (#754) | ||
|
|
d3637ea270 |
fix(pull): gate the generic sync report on a tool that can receive it (#751)
* fix(pull): gate the generic sync report on a tool that can receive it The generic branch of the sync loop reported the team repo's item count for every resource type: `Synced N skills` counted what the repo holds, not what landed. Skills are written per tool into that tool's own directory, and a brand-new member has none of them yet — the handler skips such a tool by design and only logs at debug. So the first pull after `init` printed a success while nothing was on disk, which is the phantom-success half of #585. `hasInstalledTargetFor` asks the same question `getInstalledResourceTargets` already asks, for one resource field, and the generic branch now gates its report on it. Docs need no gate: they are copied to the team's own docs directory, which the copy creates, so that report was already truthful. Only the report is gated. The writes still run, so a tool root created later — Cursor makes `.cursor/` on first launch — is filled by the next pull, and `pull --force` fills it now. Fixes #585 (the generic branch). #597 fixed the same shape for rules and left this one open; this closes it for skills. * fix(pull): gate the agents sync report on a receiving tool too The gate only covered skills, so the generic branch still printed `Synced N agents` when no installed tool could receive them — the same phantom-success shape #585 describes, one resource type over. AgentsHandler then resolves no destinations and writes nothing. `hasInstalledTargetFor` also duplicated the walk that `getInstalledResourceTargets` already performs. That function now takes an optional `field`, and the gate calls it, so reporting and writing share one resolver instead of two that can drift — an external HERMES_HOME or an unresolvable workspace no longer disagrees between them. Docs, rules and env never reach this branch; hooks and mcp have no tool-path field to probe, so they keep reporting unconditionally. Tests add the agent case the file's header already claimed: no tool directory suppresses the claim and nothing lands, and the claim returns once the directory exists. The suppression case is RED without the gate. --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
72305c68b8 |
fix(skills): one share gate, actionable refusals, and a louder stub deploy (#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 |
||
|
|
a1a41dacb6 |
docs(readme): move the agent capability matrix under Quick Start (#763)
Put the product overview on the landing page so visitors can see Git-based Execution / Context / Improvement and per-agent coverage without opening a secondary doc. |
||
|
|
5502d8ec10 |
fix: keep TeamAI out of projects that never set it up (#748) (#753)
* fix(hooks): run team hooks only where TeamAI is set up (#748) 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. * fix(stats): keep skill usage in the scope that recorded it (#748) 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. * fix(review): one scope resolver for hooks, usage and stats (#748) - 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. * fix(review): scope wording in CHANGELOG and readKnownSkills (#748) * fix(review): gate legacy contribute-check and tolerate a missing cwd (#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. |
||
|
|
95cea46182 |
fix(manifest): reject namespace strings that are not safe path segments (#710)
* fix(manifest): reject namespace strings that are not safe path segments A resource namespace becomes a directory component (skills/<ns>/, agents/<ns>/, learnings/<ns>/) exactly as a project id does, but only the project id was refined. Both manifests accepted a namespace like '../../evil', and roles.yaml had the same hole. Guarded at the manifest boundary, which is where projects.ts already claims it is enforced and the only place these strings enter the process. The existing isSafeNamespaceSegment guards in contribute.ts and resources/agents.ts stay as defence in depth. The doc comment pointed at the wrong layer: an id read from a hand-edited config.yaml resolves through getProjectOrThrow, so it can only ever name a project the manifest already validated. Corrected to say so. No fixture or e2e manifest in the repo ships a namespace containing '/' or '..', so nothing that parses today stops parsing. * docs(changelog): note the manifest namespace guard * fix(manifest): guard namespaces against traversal only, and say what is wrong Review findings on #710. The first cut reused SAFE_ID (^[A-Za-z0-9._-]+$) for resource namespaces. That allowlist is right for a project id, which is also typed on the command line and split on commas, but for a namespace it rejects far more than traversal: a team whose skills live under a non-ASCII directory, or one with a space in the name, would have stopped parsing although the directory is perfectly safe. The namespace guard now tests what actually matters -- no path separator, no `:` (drive-relative on Windows), no control character, and not `.` or `..` -- while the project id keeps its narrower spelling. Both live in src/manifest-schema.ts, which is what roles.yaml and projects.yaml genuinely share; roles.ts no longer reaches into projects.ts for the schema. Running the CLI against a manifest with `../../evil` showed the second half: the zod failure escaped as a raw ZodError, so `teamai pull` printed a validation object instead of a sentence. parseManifest now reports it the way the hand-written checks beside it do, naming the entry: Invalid projects manifest: projects.0.resources.skills.1: resource namespace must be a single path segment (no '/', '\', ':' or control characters, and not '.' or '..') Docs: the namespace rule is stated where each manifest is documented, in both usage guides and in the multi-project design doc. * fix(manifest): reject DEL and C1 controls in a namespace too Review P2 on #710: the guard rejected only U+0000-U+001F while the error message and the docs promise every control character, so U+007F and the C1 range U+0080-U+009F still parsed. Range extended and the three ranges covered in the projects and roles fixtures. * fix(manifest): reject the Win32 dot/space aliases of . and .. Review P1 on #710: the guard tested for the exact strings '.' and '..', so '.. ', '.. .' and '...' passed. Win32 strips trailing spaces and periods from a path component, so each of those reaches the filesystem as '..' and escapes the namespace directory it was supposed to name. A segment of nothing but dots and spaces is '.' or '..' in disguise and is refused as such; 'a..' keeps parsing, since it stays inside its parent. The project id, whose allowlist already excluded spaces, refuses any run of dots for the same reason. * fix(manifest): keep the project id rule untouched, scope the role docs Review on #710. The dot/space fix reached further than it needed to: tightening the id to reject every run of dots also rejected '...', a working POSIX directory name the id rule has always accepted, so a manifest that parses today would have stopped. The id is back to the exact '.'/'..' check it had before this PR, with a test that says so. Only the namespace rule moves. The role docs claimed every namespace under resources: follows the rule, but roles.yaml's learnings: is accepted for backward compatibility and ignored at runtime -- it names no directory, so holding an old manifest to the rule would reject it over a field nothing reads. Both usage guides and the design doc now name the fields that do take effect. * docs(manifest): state the id and namespace rules separately Review P2 on #710: after the id was left on its old rule, the docs still described one rule for both, so they claimed a project id rejects any name made only of dots and spaces while '...' parses. Each rule now stands on its own in both usage guides and the design doc, and the projects.ts comment says why the id is not held to the namespace rule. * fix(manifest): reject a namespace with a trailing '.' or space Review P1 on #710: refusing only names made entirely of dots and spaces left the aliasing half open. Win32 strips trailing periods and spaces from every path component, so 'frontend.', 'frontend ' and 'frontend..' all resolve to 'frontend' -- one namespace reading and writing another's directory, which is the isolation a namespace exists to provide. The rule is now the trailing character itself, which covers the escape ('.. ' arriving as '..') and the aliasing in one test, and '.' and '..' fall out of it. A dot inside a name ('alpha.v2') is untouched. * fix(pull): a roles manifest that does not parse must not widen delivery Review P1 on #710. resolveResourceNamespaces caught every failure from loadRolesManifest and carried on with no role filter, which for a member with no active project means an unfiltered sync: making the schema stricter would have turned 'skills: [../../evil]' into 'deliver every namespace', the opposite of what the guard is for. The catch was covering two cases at once, because loadRolesManifest throws both when the file is absent and when it is invalid. Only the first is the legacy, unfiltered case, so it now throws a typed RolesManifestMissingError and the catch reacts to that alone. An invalid manifest propagates and pull fails the scope with the entry named -- exactly what an invalid projects manifest already does. Verified against the real CLI: with 'skills: [evil/nested]' pushed to the team repo, pull reports the failed 'Skills to deliver can be resolved' check and the three delivered skills are left untouched; restoring the manifest syncs them again. * fix(manifest): narrow 'absent' to ENOENT, refuse Windows device names Review on #710, two of the three findings; the third was a stale read of the PR description, which the e2e section had already been rewritten to match and which is now updated before the push rather than after. readFileSafe returns null for every read failure and for an empty file, so an unreadable roles.yaml was indistinguishable from one that was never written -- and 'never written' is the one case allowed to relax role filtering. projects.yaml had the same hole, where a null manifest means 'this team is not partitioned'. Both loaders now read the file directly: ENOENT is absence, and a permission error, a directory or an empty file is an error that fails the pull. Windows opens a device for CON, NUL, AUX, PRN, COM0-9 and LPT0-9 in every directory, extension or not, so a namespace spelled that way cannot be the directory the manifest names. A name that merely starts like one (console, community) is untouched, and the project id stays out of this rule as it stays out of the others: it is a working POSIX name the id rule has always accepted. * fix(manifest): narrow every roles fallback, drop COM0/LPT0 from the device set Review on #710. The fail-closed change covered resolveResourceNamespaces but not the other callers that fall back when the loader throws, so a malformed manifest still reached an unfiltered sync by another route: bootstrap.ts left the member role-less while auto-selecting the sole role, resources/skills.ts and push.ts guessed the namespaces from the role ids, and config.ts skipped the legacy migration and left the role unset. Each now reacts to RolesManifestMissingError alone. roles-cmd.ts keeps its broad catches on purpose: those commands report the error to the person running them instead of deciding what to deliver. Windows reserves COM1-COM9 and LPT1-LPT9, not COM0/LPT0, so the guard was rejecting two ordinary directory names for no safety gain. Both are now covered by the test that pins 'console' and 'community' as valid. * fix(manifest): prove absence before trusting it, add the superscript devices Review on #710. ENOENT is not proof that a manifest is absent: a committed symlink whose target is missing reads exactly the same way, and absence is the one answer that lets a caller relax its filtering. The path is now lstat-ed before absence is believed, so a dangling link is an error like any other unreadable file. Windows reads the superscript forms of 1, 2 and 3 as device numbers, so COM and LPT followed by one of those join the ASCII-digit set. The third finding, that resolveResourceNamespaces returns before reading roles.yaml, is not a fail-open and is left as it is: that branch is reached only when the member has no role, and a role-less member gets the same unfiltered sync from a perfectly valid manifest, since every role namespace below is gated on primaryRole. Reading the manifest there would only add a new way for their pull to fail. The reasoning now sits in the code beside the early return. * fix(init): a broken roles manifest must stop init, a skipped prompt must not Review on #710. Both init paths swallowed every role-selection failure and carried on without a role. A role-less config matches every role when hooks are reconciled, so a manifest that does not parse installed exactly the hooks it restricts. Narrowing the catch to RolesManifestMissingError alone was too much: the same block also absorbs a person skipping the role prompt, and a non-interactive run reaches it, so init would have started failing for anyone who does not pick a role. That case is now its own type, NoRoleSelectedError, and the two lenient cases are named while a parse failure or an unknown --role propagates. Both catch blocks read the same. Also: ENOENT proves nothing about absence when the DIRECTORY is a dangling link -- readFile and lstat on the file both report ENOENT -- so the path's components are walked, and the first link that leads nowhere is reported instead of being read as 'no manifest'. Verified in init.test.ts (malformed aborts and writes nothing, absent still initializes role-less) and against the real CLI in single-repo mode: no manifest exits 0 with the role unset, '../../evil' exits 1, and a valid manifest with --role sets primaryRole: frontend. * chore(ci): re-run review against the rebased head No code change. The Codex review workflow re-reviews on push, and the PR body now carries the real-CLI matrix run on the rebased head. * fix(manifest): expand '~' when reading a manifest, guard role ids used as fallback namespaces Review findings on #710 after the rebase. readManifestFile replaced readFileSafe/readFileIfExists, which expanded a home-relative repo.localPath. Without the expansion a documented `~/.teamai/...` path is searched under the current directory, read as absent, and roles.yaml absence relaxes the filtering. The path is expanded before both the read and the dangling-link walk. When roles.yaml is absent, skills.ts and push.ts fall back to the role ids as namespaces. A role id is an unrestricted string, so a value such as '../../outside' reached path.join, and SkillsHandler.removeItem could recurse outside the team repo. Both fallbacks now pass the ids through the namespace guard and fail with the rule's message. * fix(config): expand '~' in repo.localPath at the config boundary A home-relative repo.localPath reached simple-git, the manifest readers and every resource path unexpanded, so `teamai pull` failed with 'Cannot use simple-git on a directory that does not exist'. The schema now expands it once, at parse time, so no consumer has to. expandHome moves to utils/home.ts (fs.ts re-exports it) so types.ts can import it without pulling in the fs helpers. * fix(manifest): reject the CONIN$ and CONOUT$ console devices too Review finding on #710. Windows opens the console for these names in any directory, extension or not, the way it does for CON, so a namespace spelled that way cannot be the directory the manifest means. * fix(push): guard the role id silent mode uses as a namespace Review finding on #710. With a valid manifest that maps the role to several skill namespaces, silent push assigned primaryRole as the namespace without the check the fallback path already has. It now goes through the same guard, so 'frontend.' or 'CON' fail the push instead of becoming a path. * fix(manifest): reject namespaces that differ only by case, classify the guard as breaking Review findings on #710. Two namespaces of one resource type that differ only by case (or Unicode normalization) name a single directory on the default Windows and macOS filesystems, so a role scoped to 'frontend' would read 'Frontend' too. Each manifest is checked when it loads; the pull path checks roles.yaml against projects.yaml as well, since both share skills/, knowledge/ and agents/. The namespace guard makes a manifest that parsed before fail every pull, so the changelog entry moves under Breaking Changes. * fix(pull): check roles.yaml against projects.yaml for role-less members too Review finding on #710. The cross-manifest case-alias check ran only when the member had a role, so a project-only member pulling skills/Common with a role's skills/common in the same repo was not stopped. roles.yaml is now read whenever a projects manifest is in play; an absent one stays absent, a broken one fails the pull as it does for a member with a role. * fix(manifest): fold case the way filesystems do when comparing namespaces Review finding on #710. The alias key was normalize('NFC').toLowerCase(), which is not case folding: 'σ'/'ς' and 's'/'ſ' stayed distinct although case-insensitive filesystems give each pair one directory. The key now upper- then lowercases each code point on its own, which folds both pairs and sidesteps the context-sensitive final-sigma rule. It errs toward joining ('ß'/'ss', 'ı'/'i'), which can only reject a pair. * fix(config): keep the config loadable when the roles manifest is broken Review finding on #710. migrateLegacyRoleConfig rethrew a manifest parse error, which loadLocalConfig caught and turned into null, so every command reported "teamai is not initialized" — pull included, leaving the member no way to fetch the fixed manifest. The migration now skips with a warning and returns the config unmigrated. That alone would widen delivery: a role-less member who would have been migrated to 'hai' reached resolveResourceNamespaces' unfiltered early return without roles.yaml being read. roles.yaml is now read for every member before that return, so a broken one fails the pull (absent still means unfiltered). * fix(status): report a resource type it cannot scan instead of crashing Found running the real CLI on #710. scanLocalForPush resolves namespaces through the roles manifest (agents via resolveResourceNamespaces, skills when it falls back to role ids), and a manifest that does not parse now throws there instead of being read as "no filter". status let that escape as a stack trace after printing half its report. Status is where a member looks to find out why pull failed, so it now warns with the error for that type and lists the rest, as it already does for git status. * fix(status): do not report "(none)" when a resource type could not be scanned Review finding on #710. With every successful scan empty and one type failing, status printed the warning and then "(none)", which reads as a complete clean result. It now says "(none in the types that could be scanned)" in that case. * fix(config): a role the manifest could not resolve matches no role-scoped entry Review finding on #710. When the legacy role migration cannot read the roles manifest, the config stayed plainly role-less, and resolveMembership reads role-less as "every role": hooks, MCP servers and env variables scoped to roles reached a member the manifest would have made 'hai'. The pull refused the manifest for skills, but those reconcilers still ran. The migration now marks the in-memory config roleUnresolved, a runtime-only field like dataHome that serializeLocalConfig drops and the schema strips on load. activeRoleIds returns [] for it, so role-scoped entries reach nobody, unscoped ones apply as before, and the reconcilers remove role-scoped entries already installed. The next load decides the role again. |
||
|
|
0b9586e9f9 |
fix(ai-client): mute share hint in AI child sessions (#746)
Signed-off-by: hiro-nikaitou <vieteviete@proton.me> |
||
|
|
1496855c66 |
docs(readme): add SkillHub fallback prompt for GitHub-blocked users (#745)
Add an alternative install prompt via skillhub.cn to the Chinese README quick-start, for users who cannot reach GitHub. |
||
|
|
178eca705b |
fix(env): hold env keys to the identifier rule env.sh already assumes (#740)
`generateEnvFile` interpolated the key raw into `export <key>=<quoted value>`. The value had `shellQuoteValue`; the key had nothing, so a key from the team repo's env/env.yaml could produce a line that is not valid shell (`export bad key='x'`) or one that runs code (`export FOO;cmd='x'`), in every member's shell — env is pushable, so any member who can push can put such a key in front of everyone else. `parseEnvFile` already refused to read back any key outside `[A-Za-z_][A-Za-z0-9_]*`, which made the asymmetry worse: the variable was written into env.sh and then invisible to the CLI, so nothing reported it as missing. That regex is now a module-level `ENV_KEY_RE` shared by both sides, so write and read agree by construction rather than by two copies drifting. The generator drops a non-matching key instead of failing: one member's bad key must not take env.sh down for everyone, and the remaining variables are still correct. `env add` rejects such a key up front with the offending name, because the local command is where the mistake is still visible — accepting it there would report success for a variable that never reaches a shell. Refs #738 Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
9d7e50bb50 |
feat(pi): add Pi Coding Agent integration (#692)
* feat(pi): add Pi Coding Agent integration Squash-rebased onto the latest upstream/main to resolve the PR's merge conflict (main gained #693/#685/#694/#691/#681/#680/#666 since this branch forked). This combines all commits from the PR into one, applied cleanly on top of the new base — no functional changes from the previously reviewed state. The only real conflict was in src/__tests__/uninstall.test.ts, where diff3 split a test mid-body because of the repeated `});` boilerplate around it; resolved by keeping both sides' new tests intact, in full. * fix(pi): gate agent-hook files on the same per-slug marker ownership check applyPiAgentHook()/removePiAgentHook() wrote and deleted teamai-agent-<slug>.ts purely by path, with no ownership check — the same class of bug already fixed for the main teamai-hooks.ts file, but never extended to the per-slug HTTP agent-hook files. A user-authored file at that conventional path could be silently overwritten on sync or deleted on uninstall. Adds hasPiAgentHook(slug), mirroring hasPiHooks: injection now skips (with a warning) instead of overwriting a same-named file without the `[teamai] agent hook [<slug>]` marker, and removal skips instead of deleting one. uninstall.ts's discovery scan now derives each file's slug and checks the same marker before scheduling it for removal, instead of matching by filename prefix alone. * fix(pi): fail install_hook_rule instead of silently acking a skipped Pi agent hook applyPiAgentHook warned and returned normally when the requested event has no Pi equivalent or a same-named extension file exists without the TeamAI marker. The caller in local-agent.ts wrote the manifest entry and acked success regardless, so the server and local state believed the hook was installed even though the file was never touched. Throw in both cases so the existing install_hook_rule error path acks failure instead. Also document the known limitation (shared with the OMP adapter) that a scoped Pi uninstall is not durable across multiple projects on the same machine, since the extension is one machine-wide file and hook dispatch has no per-project exclusion check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
ca6e51251f |
feat(skill): serve builtin skill content from the CLI, deploy a discovery stub (#699)
* feat(skill): serve packaged skill content from the CLI
Add `teamai skill get <names...> [--full] [--all]` and `teamai skill path
[name]`, so an agent can read built-in skill content that always matches the
installed CLI version instead of a copy deployed into its skills directory.
`get` prints SKILL.md byte for byte, frontmatter included, with {SKILL_DIR}
resolved to the absolute packaged directory so documented script invocations
run as-is. `--full` appends references/ and templates/, walked recursively and
sorted by relative path, because our references nest one level deeper than the
flat layout agent-browser assumes.
Content goes to stdout and every diagnostic to stderr, so the output stays
byte-exact when piped. An unknown flag warns and continues; an unknown name is
fatal, since acting on the wrong skill is worse than a retry.
`skill list` gains the served catalog and `--json`; `skill show` resolves
packaged skills before the installed-agent fallback, which is what keeps it
working once the deployed unit becomes a stub. Legacy directory names resolve
as aliases.
Refs #678
* refactor(skills): move content to skill-data and deploy a single stub
Agents now receive one file: `skills/teamai/SKILL.md`, a discovery stub of
about 2 KB whose description carries the triggers of every workflow and whose
body holds the commands that load them. The workflow content moves to
skill-data/{core,share,wiki}, which is never deployed and is printed by
`teamai skill get`.
Before this, `deployBuiltinSkills` copied three whole trees — 176 KB — into
every installed agent on every pull, so the text an agent read could disagree
with the CLI it documented until the member ran a pull, and a machine with ten
agents held ten copies. skills/ keeps its meaning ("everything here is
deployed"), which is what lets BUILTIN_SKILL_NAMES collapse to one name.
The stub is copied verbatim: no ensureSkillFrontmatter on the way out, so a
deployed copy that differs from the packaged one is a bug rather than a
variant. Recall no longer gates deployment, since the stub routes to every
workflow; the run-time gate for share lands with the pruning pass.
Uninstall learns the legacy directory names, which it would otherwise leave
behind on every machine that upgraded.
"skill-data" is added to package.json files, with a test that asserts it
through `npm pack`: without that entry every test still passes against the
repo and `skill get` serves nothing once installed from the registry.
Refs #678
* fix(skills): repair stale commands, broken refs and frontmatter
An audit of the three builtin skills found 60 defects. This fixes the ones that
survive the move to skill-data, and splits the two skills that were carrying
more than one job.
Stale CLI surface. The wiki skill advertised `teamai extract graph`, a command
that has never existed. The hand-written "ground truth" cheat sheet in the
teamai skill omitted 19 real commands while telling the agent that anything
missing from it could be checked with `--help` — which fails for the flags
`--help` hides. The cheat sheet is replaced by
`skill-data/core/references/commands.md`, rendered from the CLI's own command
table, with hidden flags marked as such. Two tests guard it: one regenerates
the file and diffs, the other resolves every `teamai …` string written anywhere
in skill-data against the command table and fails on an unknown command or
flag. That second test is the one that would have caught
|
||
|
|
667aed0f55 |
fix(hooks): widen track-slash regex to match SKILL_NAME_REGEX (#737)
The trackSlashHandler extracted the slash-command skill name with
`/^\/([\w-]+)/`, which does not match dots (`.`) or colons (`:`) —
both valid per `SKILL_NAME_REGEX` (`/^[a-zA-Z0-9_\-:.]{1,200}$/`).
A user invoking `/org.setup` or `/ns:deploy` had the name silently
truncated to `org` or `ns`, so the wrong (or nonexistent) skill was
tracked.
The CLI counterpart `trackSlashCommand` (usage-tracker.ts) already
uses the full `[a-zA-Z0-9_\-:.]+` set. Align the hook handler.
|
||
|
|
ff47714902 |
fix(doctor): probe the Copilot hooks file where inject writes it (#732) (#733)
In a non-self project scope, resolveDoctorContext forces the hook paths to
the hook scope ('user', per resolveHookScope) so settings-based hooks are
probed where reconcileHooksToAllTools writes them. The standalone Copilot
hooks file is written by reconcileTeamHooksForConfig at the config's own
scope instead, so the doctor ended up joining the userScope relative path
(hooks/teamai.json) onto <projectRoot> and reported Copilot missing right
after a successful `hooks inject`.
buildHookChecks now takes both maps and picks per hook kind: a standalone
`hooks` file from the config-scoped paths, `settings` from the hook-scoped
ones. Same rule `hooks list` already applies.
Hypothesis confirmed: scope mismatch between the two path maps, introduced
when #695 moved the doctor's hook paths to the hook scope for Qoder CN.
|
||
|
|
bc6943ea30 |
fix(members): read the default-branch roster as an inherited root after the reports switch (#741)
The orphan-branch switch (#489) made members list and projects members read only the teamai-reports worktree, so a team whose roster still lives on the default branch saw "No team members registered" right after upgrading (#735). The default-branch clone now stays a read-only inherited member root, the way learnings' already is (#485): listing unions both roots (the reports-branch copy wins when the same file exists on both), nothing is copied or deleted, and read-only commands still never publish the reports branch. Member registration merges against the inherited copy too, so a re-init keeps the original registeredAt/projects and converges the data onto the branch. Fixes #735 |
||
|
|
48b3dcb953 |
feat(hooks): add DeepSeek Harness hook bridge (#689)
Fixes #623 |
||
|
|
d80d5a8678 |
fix(push): namespace new rules and agents from --role/--project (#649) (#698)
* fix(push): namespace new rules and agents from --role/--project (#649) `--role`/`--project` only ever placed new skills, so a rule pushed with `--project front-app` landed at `rules/<name>.md` and a new agent at `agents/<name>.yaml` — both of which `pull` ships to every member. The flag also collapsed into the project's `skills` namespace, which is the wrong directory for a rule: a rule is namespaced on the `knowledge` axis, and the manifest allows the two to differ. Each pushable type now resolves from its own axis (skills → `skills`, rules → `knowledge`, agents → `agents`), and the destination is printed rather than chosen silently. Where the named project declares no namespace for a type being pushed, the command fails and names it instead of writing to the shared root. Only new resources already at the shared root are placed; anything the scanner namespaced keeps its path (#654), and an open PR's recorded destination still wins so a force-push never moves a resource. Also resolves a root-level local rule against its namespaced team copy, so a rule that was placed on an earlier push is not re-pushed to the shared root once it merges. * fix(push): record rule placement in state instead of matching by basename RulesHandler.scanLocalForPush matched a root-level local rule against the sole active rules/<ns>/<name>.md by basename. A namespaced team rule is pulled into a namespaced local directory, so another member's unrelated root rule with the same name would have been read as a modification of the team rule and overwritten it under --all. push now records where it placed each root-level rule (state.placedRules, name -> team path). The scanner redirects a root-level local rule only when that record exists and its team file is still present; otherwise the rule is new. The ambiguity warning goes with the basename index. Review: https://github.com/Tencent/teamai-cli/pull/698#issuecomment-5770251793 * fix(push): stop on unreadable roles manifest, sync placed rules, resolve in dry-run Three review findings on top of the #649 placement fix. A roles manifest that exists but cannot answer — unparseable, or missing the configured role — no longer falls back to an empty namespace list, which sent a new rule or agent to the shared root and therefore to the whole team. Only an absent manifest keeps the pre-manifest fallback, so `loadRolesManifest` now throws a tagged `RolesManifestNotFoundError` to tell the two apart. `syncTeamUpdatesToLocal` follows the same `placedRules` record the scanner does, so a root-authored rule placed under `rules/<ns>/` takes part in the three-way sync. Without it a teammate's newer version was never synced down and the stale root copy was pushed over it. `--dry-run` now runs the recorded-destination and placement steps before it exits, so it reports where every new resource goes and fails on the same unresolvable project axis the real command refuses. * test(push): cover #649 placement with the real CLI across agents and providers Drives the built dist/index.js against real git remotes, a fake `gh` and a fake GitLab API, and asserts on the branch content that reached the remote. The four agents × three providers cover the placement itself; the remaining cases cover what review round 2 raised — the pre-push sync following placedRules, an unreadable roles manifest stopping the push, and --dry-run resolving the same destinations. Reverting any of those three fixes turns exactly its case red and leaves the rest green. * fix(push): keep a placed resource maintainable and removable by its author Three review findings on the placement this PR added. A placement record only ever meant "push put this here", and the two sides that read one disagreed about how much it was worth. `placedResourcePath` is now the single resolver: it validates the record (inside the resource root, namespaced, no traversal, named after the resource) and both the push scanner and the pre-push sync go through it, so they cannot drift apart again. The record also takes precedence over a shared-root file that appears later with the same basename — mapping the author's copy onto somebody else's rule would push their content over it. Agents gained the analogue, `placedAgents`. `AgentsHandler.scanLocalForPush` only accepts a team source whose namespace is ACTIVE here, so an agent published with --role/--project into a namespace this directory never activated was skipped as "no active source" on the author's very next edit: they could create the agent and then never maintain it. `teamai remove rules <name>` resolves the same record through the new `publishedNameFor` hook. The author's copy stays at the rules root, so the name they type is the bare one, and remove answered "not found" about a rule it had recorded publishing. It now reports which name it resolved to, deletes the namespaced team file, and takes the author's root copy with it — left behind, that copy re-publishes the rule on the next push. * fix(push): narrow what a placement record grants, and when it is written Four review findings, each about the record rather than the placement. `remove` consulted it only after a bare-name match failed, but the LOCAL scan contributes the bare name whenever the author's own copy has edits — so `remove rules my-rule` deleted that copy, reported success, and left `rules/<ns>/my-rule.md` published. The record is now resolved first. A record is written only for a resource push actually placed: `new`, and namespaced by this run. Recording a `modified` agent meant a namespace that happened to be active at edit time became standing permission to keep editing that agent long after the role or project granting it was dropped. Records are persisted per group, right after that group reaches the remote, instead of after every group completes. A failing later group returned early and took the earlier group's mapping with it, so a resource that WAS pushed came back misclassified once its PR merged. And the roles manifest is held to the same rule as `--role` and the projects manifest: a namespace is one path segment. `foo/bar` wrote an agent below the depth pull looks at, and read back as namespace `foo` for a rule. * fix(push): let --role/--project decide which team agent a local edit belongs to `AgentsHandler.scanLocalForPush` picked the team file to edit by activity alone, and it runs before the destination is resolved. So `agents/other-ns/vr.yaml` — an agent this directory never activates — made `push --project front-app` report "no active source" and push nothing, even though the same stem is allowed to exist in several namespaces and the flag had named a different one. The scan now takes the requested namespace, through a new optional `ScanForPushOptions`; with one named, sources in other namespaces are other agents, and an absent one means this agent is new there. A shared-root copy still blocks, and now says why: both would be active at once, which is the collision pull reports. `RulesHandler.removeItem` swept the bare basename unconditionally, so `remove rules fe/foo` deleted an unrelated personal .claude/rules/foo.md. The bare copy is only ours to delete when this machine's placement record says the two are the same rule. `teamai push --help` said both flags target skills. * fix(push): never place a new resource onto one that is already there Placement rewrote a new root-level resource to the resolved namespace without looking at what was at that path. An unrelated local `foo.md` — which the scanner rightly calls new, since no record maps it anywhere — landed on `rules/<ns>/foo.md` and replaced somebody else's rule, silently, in a run they never reviewed. Push now stops and names the file. The same guard covers the `--role`/`--project` skills override, for new skills only: a modified one is meant to land on its own directory. `loadRolesManifest` read through `readFileSafe`, which answers null for every failure, so a manifest that exists but cannot be read arrived looking exactly like a missing one — and a missing one is the pre-manifest layout, which sends new rules and agents to the shared root. The two are told apart now. `RulesHandler.removeItem` tombstoned only the name it was given. Removing through a placement record means the author's source is named `<name>` while the published file is `<ns>/<name>`, and the local sweep skips excluded tools, so a root copy could outlive the removal there and come back on the next push. Both names are tombstoned when the record vouches for the bare one. * fix(push): resolve a placed agent on removal, and collide on either extension `remove` asked every handler for the published name, but only rules answered. So `teamai remove agents vr` matched the bare stem and deleted every `vr` in every namespace — other people's agents included — while the namespaced `placedAgents` record, keyed by a path the removal never named, survived. `AgentsHandler.publishedNameFor` resolves it now; the existing sweep already narrows a `<ns>/<stem>` to one file, since the root directory is one of the directories it probes. The bare stem is tombstoned alongside the published one and swept from the tool directories, the same way rules are. The placement collision check tested the proposed path alone. `pull` reads a legacy `<stem>.md` as the same agent as `<stem>.yaml`, so a new `.md` landing beside an existing `.yaml` passed the check and left two copies answering to one name. Agents are now checked under both canonical extensions. * fix(remove): make the placed-agent resolution actually reach the command Round 7 added `AgentsHandler.publishedNameFor` but `remove` only used its answer when `allNames` also carried that spelling — and `scanTeamForPull` reports an agent by its bare stem, never `<ns>/<stem>`. So the resolution was inert on the real command path: `teamai remove agents vr` fell back to the bare stem and deleted every `vr` in every namespace, exactly as before. The cross check is gone; `publishedNameFor` has already proved the file is in the team repo, which is stronger evidence than membership in a list the scans spell differently per type. The bare-stem tombstone that round went with it. Agents deploy FLATTENED, so both the push scan and the post-pull cleanup read a bare tombstone globally: removing `fe/vr` suppressed and deleted `be/vr` the moment that namespace became active. Only the published name is tombstoned now. The author's own flattened copy is still swept, but only where this machine's record says the file just removed is where push put it — without that, the copy on disk may be another namespace's deployment. Covered end to end this time: the new case drives `teamai remove agents vr` through the built CLI, which is the join the round-7 unit tests skipped. * fix(push): raise a project agents-axis failure the scan would otherwise swallow `--project <id>` resolved the agents destination before scanning and dropped the failure on the floor. A project with no agents namespace then looked identical to a run with no flag at all: the scan skipped the agent as "no active source", the item never reached placement, and the command exited 0 with "No new or modified resources" — on a flag it could not honour. The error is carried forward and raised as soon as the scan contains an agent. It cannot wait for the selection the way the skills axis does, because the item that would prove the axis is needed is exactly the one the scan removes. `placedResourcePath` matched the recorded filename by prefix, so a record pointing at `rules/<ns>/foo.backup.md` was trusted whenever that file existed, and scanning, the pre-push sync and removal would all follow it onto somebody else's file. The filename must now be exactly the resource's own. * fix(agents): reach the canonical source, and hold the record to what it proves Four review findings, all on the agent side of placement. The single-repo canonical source in `.teamai/agents/` is picked up directly, never reverse-parsed, and that branch ignored the placement record: a root `vr.yaml` placed at `agents/fe/vr.yaml` read as new on the next push, and the collision guard then refused the very agent this machine published. Removal missed the same directory, so the agent republished itself on the next push — which a bare-stem tombstone cannot prevent without suppressing that stem in every other namespace, since agents deploy flattened. The project agents-axis error now counts only agents that actually need a destination. A modified agent already in a namespace is written in place, so an empty agents axis is none of its business; blocking it contradicted the rule that only new shared-root resources are placed. And the record is no longer taken as licence to overwrite. It admits a namespace this directory never activates, which also means `pull` never refreshed a copy and the pre-push sync does not cover agents — so if the canonical file moved on since the last pull, push now says so and asks for a pull instead of writing a stale rendering over whoever changed it. * fix(agents): deliver recorded agents on pull, and let a named destination win The staleness guard added last round was defeated by the pull it recommended: `pull` advances lastPullRev without deploying an inactive namespace, so the next push saw an unchanged canonical and wrote the stale rendering anyway. It also never fired right after the first PR merged, when the file did not exist at lastPullRev. The guard is gone, and the cause with it. `pull` now delivers an agent whose placement record names it, so the local copy tracks the team file and the ordinary comparison is valid — the inactive case stops being special instead of needing its own machinery. A stem an ACTIVE namespace already claims is left alone, since agents deploy flattened and the active one is what is deployed here; the scan follows the same order, treating the record as a fallback rather than an extra candidate. Pending-PR reuse matched on type and name alone, so an open PR for a different resource of the same name captured a push that named another namespace and force-pushed into that review. Neither silent answer is safe, so the flag the user typed decides, the open PR is left untouched, and the collision is reported. This supersedes the original #331/#654 rule that a PR's destination always won; that rule still holds whenever no destination is named. * fix(pull): stop revoking the agent pull had just delivered Self-review of the branch, before the next review round. Round 11 taught delivery about placement records but not revocation, and both run in the same pull: `filterAgentsByNamespaces` wrote the agent and `cleanupInactiveNamespaces` deleted it again, byte-equal to the render so the data-safety gate passed it straight through. The record-based delivery was inert and the file churned on every pull. Both halves now resolve through one exported `selectAgentsForDirectory`, so they cannot disagree — the same treatment `placedResourcePath` already gives the push scanner and the pre-push sync. Two smaller ones from the same pass. `--dry-run` grouped against the unfiltered pending list, so it reported a destination the real push no longer uses, which breaks the property that a dry run matches the run it describes. And the partial-selection warning counted entries the run had already declined to reuse, contradicting the warning given for them; both now share the filtered list, which is computed once there is actually something to push. * fix(pull): stop the stale sweep from deleting the author's own placed rule `pullAllRules` sweeps a local rule whose name is absent from the desired set. A rule published into a namespace keeps the author's copy at the rules ROOT under its bare name, while the desired set holds `<ns>/<name>` — or nothing at all when that namespace is not active here — so the sweep deleted their own file, local edits included. The placement record marks it as theirs, and only while the team file it points at still exists. Pending-PR conflict detection trusted `PendingPushItem.namespace`, but the agent scan records a namespaced destination without setting that field, so those entries slipped past the check and a push naming another namespace could force-push into the PR under review. The namespace is derived from the recorded path when the field is absent, and the agent scan now sets it too — the field was the only thing telling `pendingNamespaceFor` where to put the resource. * fix(push): defer the agents-axis failure to selection, reload projects.yaml after the pull, and stop a named namespace reusing a shared-root PR A project with no agents namespace failed before the listing whenever any new agent was present locally, so a rules-only push under `--project` was blocked on an agent the user was never given the chance to deselect. Only an agent the scan itself skipped (`needsDestination`) fails early now — that one never reaches the listing, so deferring its error means never raising it. A new agent is listed, and step 4 raises the same error if it stays selected. `manifest/projects.yaml` was read in `push`, before `pushCore` pulled the team clone, so a project whose namespaces changed on the remote placed this run's new rules and agents by the previous pull's mapping. It is read inside `pushCore` now, after the pull, and in self mode from the fresh worktree. Pending-PR conflict detection treated a recorded path with no namespace as non-conflicting, so an explicit `--role`/`--project` reused a shared-root PR's branch and rebuilt it with the namespaced path, moving a review the user did not name from "everyone" to one namespace. The shared root is a destination like any other: it conflicts with any namespace the flag names. `pull` delivered a rule this machine placed at `<tool>/rules/<ns>/<name>` beside the author's copy at the rules root, so a tool that loads rules recursively applied the same rule twice, disagreeing as soon as the team file moved on. The placement record names the root copy as this rule's local file, so delivery updates it and removes the namespaced duplicate an earlier pull wrote. A namespace another member placed is untouched. * fix(push): stop on a stale clone under --project, retire a renamed canonical agent's recorded file, and prune placement records A failed refresh of the team clone was only warned about, after which `--project` resolved every destination from the previous pull's `manifest/projects.yaml`. A namespace the remote had changed sent this run's new rules and agents to the members of the old one. `--project` stops now, and so does a new resource that would resolve from `manifest/roles.yaml`; a team with no roles manifest resolves from nothing that can go stale and keeps its behaviour. `--role` names the namespace itself and is unaffected. In self mode the placement record redirected a root canonical agent to the recorded file, extension included. `pushItem` writes by the source's extension, so an author who rewrote `vr.md` as `vr.yaml` had the `.yaml` written and the `.md` staged: the change never reached the branch and the `.md` stayed. The destination now keeps the record's directory and the source's extension, `pushItem` deletes the file it retires, `push` stages that deletion and moves the record to the new path. Placement records were written when a branch reached the remote and never removed. Once the PR was closed unmerged, or the file deleted upstream, the record pointed at nothing — until another member created the same path, at which point it came true again and their unrelated resource read as this author's. `push` and `pull` now drop a record whose target is neither on the default branch nor awaiting review on a branch origin still has, before anything reads the records. A record is kept when origin cannot be asked. * fix(push): record a placement only once it has landed, withdraw it when a shared-root file takes the name, and drop every stale record Placement records were written when the branch reached the remote, so a PR closed without merging left one behind for as long as its branch stayed — and no provider here can say whether a PR is open. The record now travels on the pending PR entry (`PendingPushItem.placed`, with the blob push wrote) and becomes a `placedRules`/`placedAgents` record only when that blob is in the default branch's history for the path: the PR merged, however the platform merged it. A path that merely exists is not enough, since another member may have created it after the PR was closed. `push`, `pull` and `remove` settle this before reading the records; the stale sweep spares a root copy whose placement is still awaiting review. `localNameFor` redirected a recorded rule onto the bare root path without asking whether a shared-root rule of the same name was being delivered too, so both landed on the one file in loop order, and the next push could follow the record and carry the shared rule over the namespaced one. Delivery keeps the namespaced path when the shared root holds that name, and the reconcile pass withdraws the record with a warning: the root copy follows the shared rule from then on. Dropping stale records destructured from the ORIGINAL map each iteration, so a later deletion put back what an earlier one had removed and only the last stale record went. The kept entries are rebuilt in one pass. * fix(push): consume a pending placement once it is recorded Recording a landed placement left its `placed` mark on the pending entry. Had the team then deleted the file — which drops the record — and another member recreated the path, the next reconcile recorded it again: the path existed and the blob push had written was still in the default branch's history, so both checks passed, and the unrelated replacement read as this author's resource. The mark and the blob are cleared when the record is written, so a placement is recorded exactly once. * fix(remove): keep placement records until the removal lands, and retire flattened copies `remove` dropped the placement record as soon as its branch was pushed, so a retry during review resolved `vr` to the bare stem and removed that agent from every namespace. Records are now left to the reconcile pass, which drops one once its file is gone from the default branch, or was deleted since the last check (`placementsCheckedAt`) even if another member has recreated the path. A namespaced removal tombstones only `<ns>/<name>`, which never matched the flattened `<agents>/<name>` copy members hold, so that copy survived pull and the next push republished it. `AgentsHandler.removedStems` reads the tombstone as the flattened stem while no namespace still has that agent, for both the pull cleanup and the push scan. Rules no longer write a bare tombstone, which swept and suppressed other members' unrelated rules of the same name. Agent and rule push scans skip tools the member excluded: remove leaves those copies behind by design, so reading them republished the removed resource. Landing is proven only by history after the full commit the push branch was built on, and a placement whose path was deleted after it landed is spent unrecorded. Single-repo pull no longer reconciles against the member's own checkout; push and remove still do, in a fresh origin/<default> worktree. * fix(pull): reconcile single-repo records through origin/<default>, and retire flattened copies per directory Single-repo pull skipped the reconcile pass, so a merged placement stayed unrecorded until the next push or remove. It now reads the default branch as the ref origin/<default> (existence via `<ref>:./<path>`, history up to that ref) instead of the member's own checkout, and changes nothing when the ref cannot be resolved. `placementsCheckedAt` survived its last record, and a record made in the same run was checked against it, so re-placing a resource at a path deleted earlier dropped the new record at once. The checkpoint is cleared with the last record, and records made in this run are not held to it. `removedStems` retired the flattened stem only when no namespace had it at all, so an fe member kept a removed fe/vr while an unrelated be/vr existed. It now asks what this directory is meant to hold, through the same selection pull delivers with. * fix(remove): stop on a stale clone, and judge PR conflicts by the destination the flag gives `remove` ignored a failed refresh and reconciled the stale clone as if it were the default branch. A placement merged since the last pull was then not recorded, and the bare name fell back to the stem, removing that agent from every namespace. `remove` now stops with exit 1 and removes nothing. Under --role/--project, a pending PR counted as conflicting whenever its recorded namespace differed from the flag's, although only skills and new shared-root rules and agents are moved by it. A modified rule already in a namespace kept its path yet went to a second PR on the same file. Only an item the flag actually moves can conflict with it now. * fix(push): prefer an agent's delivered source over the flag, baseline new placements, validate the record's namespace With --role/--project the requested namespace always chose the team file a local agent was compared with, so an untouched copy delivered from an active namespace read as an edit of the requested namespace's agent and overwrote it. Candidates now follow delivery: an active source (the shared root included), then this machine's record, and only then the requested namespace. A placement that landed after the last pull had no `lastPullRev` version, so the pre-push sync skipped it and a teammate's edit before the author's next pull was pushed over. Rules take the version the file was added with as their base; a recorded agent, which has no pre-push sync, is held with a pull-first message when the team file moved past that baseline. `placedResourcePath` now requires a safe namespace segment: a backslash in it is a separator on Windows and walked out of the resource root. * fix(push): close the round-21 findings on placement, removal and agent sources - A pending namespaced placement whose name a shared-root file now takes is left out of the push with a warning, instead of the open PR being rebuilt with the author's copy over the shared file. - `remove` stops when the placement records cannot be reconciled and saved: a missing record sends the bare name to the stem, which spans namespaces. - An agent skipped for want of a --project agents namespace no longer blocks the rest of the push; the error stands only when nothing else is left. - A placement is marked only with a blob that can prove it landed, and one without is spent rather than recorded because its path exists. - The single-repo `.teamai/rules` scan source is not a tool, so an `enabledAgents` list no longer hides it. - A namespaced agent tombstone retires the flattened stem only where that agent could have been delivered; reconcile keeps the author's dropped record (`retiredPlacedAgents`) so their own copy still counts. - Two active same-name agents stay ambiguous under a flag, and a flag naming a namespace that already holds the agent is a collision, as for rules. - In single-repo mode a root copy equal to an older version of its placed file is held as stale rather than pushed over a teammate's edit. - The recorded-agent hold runs only for a changed copy and says to set the edit aside first; a pending placement is routed to its PR, not skipped; a flag that does not move a shared-root edit says so; several candidate namespaces without a terminal fail with a --role hint; a placement that landed with other content is reported once. * fix(push): stop on unsaved records and stale placements, name namespaced agents on remove - `push` stops, pushing nothing, when the reconciled placement records cannot be saved: the sync and the scan read them back from disk, and a record that could not be withdrawn still redirects the author's copy. - On a stale clone every unflagged placement stops, not only one resolved from an existing roles manifest: the manifest's absence and the skills namespaces detected from the tree are clone state too. - `remove agents <ns>/<name>` names one namespaced agent; a bare name only one namespace has resolves to it, and a bare name found in several places is refused rather than removed from all of them. - A record dropped because its file was deleted and recreated is retired like one whose file is simply gone, so the author's flattened copy of the removed agent is still recognised. --------- Co-authored-by: Saul Moro <smoro@ai-lab.knowmadmood.com> |
||
|
|
94a0d428c1 |
fix(env): only stick to a candidate the active profile actually reaches (#682) (#715)
* fix(env): only stick to a candidate the active profile actually reaches (review) 02c93fe's resolveActiveShellProfile scanned every SHELL_PROFILE_CANDIDATE_NAMES entry for a matching block and returned the first hit, in a fixed order (.zshrc, .bashrc, .bash_profile, .bash_login, .profile). That's broader than the Git-for-Windows-forwarding case it was written for: a stale block a pre-#682 install left in .bashrc would outrank a correctly order-picked .profile that hasn't been written to yet, since .bashrc sorts earlier in the candidate list — silently reintroducing #682 for exactly the installs upgrading through this fix, with doctor unable to catch it because the stale block is well-formed where it sits. Reworked to start from detectShellProfile's order-based pick (the file the current environment actually reads) and only diverge from it when that pick's own content references another candidate by a home-relative path (~/.bashrc, $HOME/.bashrc) — the shape Git for Windows' generated forwarding file actually takes. A block sitting in a candidate the pick never reaches is no longer preferred over the pick, regardless of what it contains. Also caught and fixed a case of exactly the failure mode this PR is about: the first cut of the forwarding check was a bare substring match on the candidate's filename, and my own test's plain-English comment ("...unrelated to .bashrc") satisfied it. Tightened to require the home-relative reference form a real sourcing line uses. Verified both scenarios end-to-end on a real Windows host: - The exact bot-reported upgrade case (stale .bashrc block, empty .profile, no forwarding between them): pull now writes into .profile and correctly flags .bashrc as stale; doctor reports delivery healthy. - The Git-for-Windows forwarding case from the prior round: still sticks to .bashrc through the generated .bash_profile, no duplicate, no stale-block warning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(env): match real source commands, resolve reachability transitively (review) Two P1s from the bot's review of #715: - resolveActiveShellProfile's reachability check was a bare substring search on the active pick's content. A comment mentioning a filename (never executed) or a longer file sharing the same prefix (~/.bashrc.local) would both satisfy it, letting a stale block win the same way #682 did. Replaced with referencesCandidate(): strips full-line comments, splits each remaining line into statements on &&/||/;, and only counts a statement whose first word is literally `.` or `source` and whose second word is an anchored home-relative reference to exactly that candidate. - The check only followed one hop: .bash_profile sourcing .profile sourcing .bashrc (the common Debian .profile pattern, sourcing .bashrc for interactive shells) would miss a block two hops away and inject a duplicate. Reworked into a loop that walks the chain of files the pick actually sources, with a visited set for cycle protection, stopping at the first one that carries the block. Also fixed the P2: EnvHandler.detectShellProfile's doc comment still claimed it "stays on whichever candidate already carries this scope's block" unconditionally, which stopped being true once reachability was required. Verified end-to-end on a real Windows host: - The new two-hop chain (.bash_profile -> .profile -> .bashrc, block in .bashrc): resolves to .bashrc, no duplicate, doctor fully clean. - Re-ran the Git-for-Windows one-hop scenario and the #682 upgrade scenario from the prior round — both still correct, no regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(env): search every referenced candidate, respect || conditionality (review) Two more P1s from the bot's round-10 review of |
||
|
|
a52374ab04 |
fix(ci): make Codex review report all findings and run fork-safe e2e (#739)
Closes #731. Feed current PR body plus earlier review comments into each pass, stop hand-listing config mocks, and run credential-free e2e on fork PRs without exposing fixture tokens. Co-authored-by: review <review@local> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8d74aa4e42 |
docs(teamai-skill): fix wrong share-learnings hand-off hint (#736)
Step 9 of setup-admin.md told users to run `/teamai 我想把学到的经验分享给团队` to share what they learned. This contradicts the teamai SKILL.md, which states twice that sharing a session's learnings is automatic and must NOT be routed through `/teamai` — it is handled by the separate teamai-share-learnings skill. Replace that bullet with the correct guidance: - `/teamai` share entry is for publishing a reusable skill (contribute-member.md), not loose learnings. - Session learnings are shared automatically via teamai-share-learnings. |
||
|
|
94eb1484bc |
fix(init): refuse provider logins without a terminal instead of hanging (#711) (#713)
* fix(init): refuse provider logins without a terminal instead of hanging (#711) `teamai init` with no session spawned `gh auth login --web` (or `gf auth login`, `cnb login`) with inherited stdio and waited for a browser device flow nobody could complete, about five minutes for GitHub, then exited with the provider's error and no hint of the missing credential. Cause: the non-TTY guard lived only in utils/prompt.ts. A login is a child process that owns the terminal, so it never went through that guard. - `isInteractive()` in utils/prompt.ts: stdin is a TTY and neither `CI` nor `TEAMAI_NONINTERACTIVE` is set. Every prompt and the four prompt-semantics `isTTY` checks use it; the six hook-payload checks are untouched. - github, tgit and cnb logins throw before spawning when not interactive, naming the token variable, the way gitcode already did. - index.ts exports GIT_TERMINAL_PROMPT=0 when not interactive, so a missing clone credential fails at once instead of prompting or opening a credential helper dialog. An explicit caller value wins. - e2e test with a fake `gh` whose `auth login` sleeps: exit 1 in under a second naming GITHUB_TOKEN, also under CI=true. Closes #711 * fix(init): close every git prompt and point TGit at the credential that works Review follow-up on #713. - utils/git-env.ts: GIT_TERMINAL_PROMPT=0 only closed git's own terminal question. The askpass chain (GUI dialog), ssh's passphrase / unknown-host question through /dev/tty, and Git Credential Manager's window each still parked an unattended clone until the 180s timeout. All four are now closed together (GIT_ASKPASS=echo, GIT_SSH_COMMAND='ssh -o BatchMode=yes', GCM_INTERACTIVE=never), each only where the caller set nothing. - tgit: the guard suggested exporting TGIT_TOKEN, which cannot make an unattended run succeed — the PAT is REST-API-only and git.woa.com's git endpoint rejects it, so `gf auth whoami` still fails and the clone still has no credential. The message now names `gf auth login` (whose stored credential is the one that works) and says why the token is not it. Docs follow. - local-agent: keep askViaTty's non-interactive decline synchronous. Awaiting the prompt module's import before declining shifted hook-path timing enough to break the once-per-session binding hint (local-agent.test.ts). * fix(git-env): append batch mode to core.sshCommand instead of replacing it Review follow-up on #713. GIT_SSH_COMMAND overrides core.sshCommand rather than extending it, so setting it blindly dropped a configured custom key, ssh binary or wrapper and left the run unable to authenticate at all. The value is now composed: read core.sshCommand and append `-o BatchMode=yes`, or use plain `ssh` when nothing is configured. A command that already decides BatchMode is left alone, and the config read is skipped entirely when the caller set GIT_SSH_COMMAND. Test isolation, so the suite's own result can be trusted: - shell-profile.test.ts: the three Windows cases never stubbed SHELL, and detectShellProfile reads it before the platform branch — a suite run from a zsh login shell resolved .zshrc and failed them without ever reaching the Windows branch. CI runners use bash, which is why only local runs saw it. - local-agent.test.ts: the once-per-session binding-hint markers live in os.tmpdir() under one shared key, so a leftover marker decided whether the next test emitted a hint, and concurrent runs competed for the same paths. Each test now gets its own temp directory, which makes the markers per-test by construction. Full suite: 3788 pass, 0 failures, five consecutive runs. * fix(git-env): leave ssh to each repository instead of a process-wide override Review follow-up on #713. GIT_SSH_COMMAND is the only way to reach ssh's batch flag, and it overrides `core.sshCommand` for *every* later git operation, not just the one the value was derived from. Reading the launch directory's config and exporting it process-wide therefore pushed that repo's key or wrapper onto the managed team repo, and the plain default suppressed a `core.sshCommand` the managed repo had configured for itself. Prompt suppression must not reach a repository's transport, so the variable and the `git config` read are gone. What remains is the three variables that name a prompt and nothing else, so one value is right for every repository a run touches: GIT_TERMINAL_PROMPT=0, GIT_ASKPASS=echo and GCM_INTERACTIVE=never. An ssh remote that would still ask is now documented as the caller's to close, per repository (`git config core.sshCommand 'ssh -o BatchMode=yes'`) or per run (`GIT_SSH_COMMAND`). Measured first: with stdin closed, ssh's own tty read hits EOF and fails in about a second, so the unattended paths this PR is about do not depend on the flag. Also reverts the shell-profile.test.ts and local-agent.test.ts isolation edits from the previous round: neither traces to the unattended-login fix, so they belong in their own PR. * docs(providers): mark the provider logins interactive-only (#711) docs/providers.md still described `teamai init` as running `gh auth login`, `gf auth login` and `cnb login` unconditionally. Each now happens only in an interactive terminal; an unattended run fails at once naming the credential to prepare (a token for GitHub and CNB, a prior `gf auth login` for TGit, since a TGIT_TOKEN PAT is REST-API-only and cannot clone). |
||
|
|
b91b6dfcae |
fix(hooks): list each tool's own built-in hook set (#718)
* fix(hooks): list the built-in hooks each tool really receives
`hooks list` rendered builtinHookDefs('claude') for every tool, so the built-in
block described Claude's hook set no matter which tool the row was for:
Copilot's SessionEnd hook never appeared, while tools that receive no hooks at
all were credited with six.
The displayed set is now derived from what each tool actually receives through
reconciliation, and a tool with no hook surface is omitted rather than shown an
invented list.
Fixes #717
* fix(hooks): report adapter-driven tools by their generated artifact
`hooks list` probed a settings file per tool, so Hermes, OpenCode and
OpenClaw — which reconciliation installs as one generated script/plugin/
handler each — fell through to the generic branch and were reported as
"not configured" even right after `hooks inject` wrote their hook. OMP
already had a bespoke branch for this.
Resolve that artifact per adapter and use its presence as the status, the
same rule OMP used, so the status column matches the built-in block.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(hooks): status against the effective built-in set and the user plugin
Two status-column defects the per-tool listing exposed:
- `getHookStatus` checked the unmodified `builtinHookDefs(tool)`, so a
built-in the team disabled through hooks.yaml was still expected on disk
and every tool read `missing` right after a correct reconciliation. It
now takes the same §4.8 override reconciliation applies.
- The OpenCode artifact was probed under the config scope's base dir, but
`reconcileOpencodePlugin` always installs the single plugin under the
user path, so a project-scope config reported `missing` after a
successful injection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(hooks): require every generated file before reporting installed
The adapter status probe accepted a single file, so an OpenClaw
installation missing its handler.ts — the file HOOK.md points at, without
which no hook runs — still read as `installed`. Check every generated file
the adapter writes and report `installed` only when all are present.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
cd3e0e6ef5 | fix(hooks): stop the Stop hook nudge reaching the user twice (#720) | ||
|
|
2ed17e4fb2 | feat: scope hooks, MCP servers and env variables by logical project (#700) | ||
|
|
1e2c029466 |
feat(agents): support Qoder CN via its .qoder-cn user directory (#695)
* feat(agents): support Qoder CN via its .qoder-cn user directory Qoder CN keeps its user directory at ~/.qoder-cn instead of ~/.qoder, so a CN install saw none of TeamAI's synced resources and users worked around it with symlinks. Register qoder-cn as its own built-in target mirroring qoder: skills discovery, toolPaths defaults, the Claude-compatible agent/MCP formats, and the subagent render/reverse switches. Docs list the new paths. * fix(agents): resolve Qoder CN paths by scope, not by one flat table The first cut registered `qoder-cn` with `.qoder-cn/*` as every path, which wrote project-scope resources into a directory Qoder CN does not read. Only the *user* directory differs (`~/.qoder-cn`); project scope still uses `.qoder/`. `scopedToolPaths` documents the contract -- top-level fields are the project paths, `userScope` carries the user overrides -- so the entry now follows it: top-level stays `.qoder/*`, `userScope` holds `.qoder-cn/*`, `mcp` is the user-level MCP file and `mcpProject` the project-level one. `userScope` did not admit `settings` at all, so a user-scope `qoder-cn` would have had its hooks written to `~/.qoder/settings.json` -- the international install's config. `settings` is now part of the userScope schema and its splice. Hooks do not necessarily live at the config's scope: `resolveHookScope` maps a non-self project scope to HOME (#264). Every caller that resolved paths from `localConfig` while using that HOME base directory therefore looked under the project prefix inside HOME. `resolveHookScope` and `resolveLegacyProjectHookScope` now report the scope they resolved, and the inject, remove and check paths use it -- `hooks.ts`, `pull.ts`, `hooks-cmd.ts` and `doctor.ts`, the last of which is why `teamai doctor` reported "teamai hooks in qoder-cn settings" as missing. Verified with a real CLI end-to-end run, not only unit tests. Against `teamai-hub/teamai-cli-dev` with a scratch HOME, `--scope project --agent qoder-cn`: - `~/.qoder-cn/settings.json` is written; `~/.qoder/settings.json` is not - project scope delivers to `<project>/.qoder/skills`; `.qoder-cn/` is not created - `teamai doctor` reports `qoder-cn is installed` and `teamai hooks in qoder-cn settings` both green (the latter failed before this change) `npx tsc --noEmit` clean; full suite unchanged at 30 failed / 240 passed files and 75 failed / 3655 passed tests, the pre-existing Windows failures. * fix(agents): keep Qoder CN off the paths Qoder already owns Registering `qoder-cn` with the project paths `.qoder/*` made two targets resolve to the same project-scope hook file. `reconcileHooksToAllTools` iterates per tool and `reconcileClaudeFormat` re-renders every built-in entry with the current tool's dispatch identity, so the later `qoder-cn` pass rewrote a plain international-Qoder install's file as `--tool qoder-cn` and dropped team hooks scoped to `tools: [qoder]`. Reproduced with the default configuration (no agent whitelist) in self mode with `<root>/.qoder` present: 6 built-ins all `--tool qoder-cn`, 0 `--tool qoder`, and the `tools: [qoder]` hook gone. Each settings file is now reconciled once, for the first target that reaches it. The shipped table lists `qoder` before `qoder-cn`, so the existing target keeps ownership of `<root>/.qoder/settings.json` and CN remains a user-scope addition there; a CN-only pass still targets the file as `qoder-cn` because nothing else claims it. `hooksList` mirrors the same rule, so the shared file is listed once instead of reporting the CN row as missing. Two user-scope consumers resolved their paths at the config's scope while using the HOME base directory `resolveHookScope` reports, so they looked in `~/.qoder/settings.json` for hooks that live in `~/.qoder-cn/settings.json`: `hooksList` reported a healthy CN install as missing, and `uninstall` discovery never found the CN hooks and left them behind. Both now use the hook scope. Skills, rules, agents and claudemd stay at the config scope in uninstall — they are real project resources, and resolving those to user paths would strand the project copies. Corrected the MCP table in both `docs/usage-guide.md` and `docs/usage-guide.zh-CN.md`, which still said `<project>/.qoder-cn/settings.json` while `mcpProject` and the surrounding prose use `<project>/.qoder/`. Known limitation, documented rather than fixed: in project scope a team hook scoped `tools: [qoder-cn]` no longer lands in the shared file. One physical file can carry only one dispatch identity, so it is not representable there; teams can scope to `qoder` or omit `tools:`. Union semantics would need engine support for alias-aware definitions and is out of scope here. `tsc --noEmit` clean; full suite 30 failed / 240 passed files unchanged, the 71-test failure set byte-identical, +4 tests from this change. * fix(cli): resolve the shared hook file by the enabled set on the read path `hooksList` iterated the whole tool table without applying the enabled set, so for a self-scope config that enabled Qoder CN alone, `qoder` — disabled, but earlier in the shipped table — claimed `<root>/.qoder/settings.json`, was probed for Qoder's dispatch identity, and was reported `missing` while the enabled `qoder-cn` target never appeared in the table at all. The write path already got this right: `reconcileHooksToAllTools` skips any tool outside `filterAgents`, which `reconcileTeamHooksForConfig` derives from `enabledAgents` and `disabledAgents`. Only the read path was missing the same rule, so the fix reuses the filter `doctor` already applies to this path table. Ownership of the shared file therefore follows the enabled set rather than the table order, and the two paths now agree. --------- Co-authored-by: PerryLink <255665900+PerryLink@users.noreply.github.com> |
||
|
|
595034f404 |
fix(test): eliminate CI flaky tests (#729)
* fix(test): eliminate CI flaky tests - lock-atomic: accept 1–2 winners under high-load concurrent reclaim instead of exactly 1 (fs rename window widens under load) - dashboard-collector: refresh `now` in beforeEach so timestamps are never stale when rebuildSessions checks the 30s expiry window - codebase-reconcile, import-dir: increase timeout from 15s to 60s for tests that hit disk-heavy operations on slow CI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): address review — keep strict lock assertion, reduce concurrency Preserve exact-one mutual-exclusion assertion per reviewer feedback. Reduce concurrency from 32→8 to lower CI load pressure, and add retry:3 so transient fs scheduling races don't block the pipeline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): revert lock-atomic changes, keep original assertion Revert lock-atomic to its original form (32 concurrency, strict exact-one assertion, no retry). The sentinel serialization is correct under Node.js single-threaded event loop; the rare 2-winner case is an OS/fs-level anomaly under extreme CI load, not a product bug. This PR focuses on the deterministic fixes for the other 3 test files. |