mirror of
https://github.com/Tencent/teamai-cli.git
synced 2026-10-02 03:14:40 +08:00
main
979
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f7d37042cd |
fix(env): make the shell-profile block load on Windows (#680)
* fix(env): make the shell-profile block load on Windows The block written into the user's shell profile interpolated teamaiHome as a native path, so on Windows it emitted [ -f C:\Users\me\.teamai/env.sh ] && source C:\Users\me\.teamai/env.sh A POSIX shell reads those backslashes as escapes: the `[ -f ... ]` test fails, `source` never runs, and the env vars are never loaded. Nothing reports it either — the block is present, so `doctor` still passes. Normalise `\` to `/` and quote via the existing shellQuoteValue. Both are unconditional rather than gated on path.sep or on whitespace, so the emitted block is byte-identical on every platform and the Windows form stays assertable from the Linux/macOS runners, which are the only ones CI has. The three new assertions cover the Windows path and a home directory with a space; removing the fix fails exactly seven tests. Fixes #661 * fix(env): normalise only Windows-form paths in the shell-profile block The block is sourced by a POSIX shell even on Windows, so a native home such as C:\Users\me\.teamai has to reach it with forward slashes, and the path is now quoted unconditionally — a home directory containing a space breaks the same `[ -f ... ]` test on every platform (#661). Review on #680 found that replacing every backslash also corrupts a POSIX home carrying a literal one, where a backslash is an ordinary filename character rather than a separator: /home/a\b/.teamai became /home/a/b/.teamai. The rewrite now keys off the path's own shape — a drive letter or a UNC prefix — and leaves every other path untouched. Keying off the shape rather than path.sep keeps the Windows form assertable from the Linux/macOS CI runners, which have no Windows job: the new cases pin the Windows output, a POSIX home carrying a backslash, and a Windows home containing a space. The five verbatim block assertions move to the same helper the source uses, which also makes them pass on a Windows host instead of only on CI. |
||
|
|
a7770e3b95 |
feat: add privacy-safe Copilot telemetry (#666)
* test: define Copilot telemetry contract * feat:copilot-privacy-telemetry * fix:read-real-copilot-usage * fix:lock-down-copilot-telemetry * fix: address Copilot telemetry review * fix(copilot): keep paths out of session IDs The background dispatcher can inject a cwd-bearing fallback before the collector runs. Keep that fallback path-free for Copilot and reject path-like IDs at persistence. Preserve other providers' existing IDs. Refs #666 * fix(copilot): reject stale shutdown totals A resumed session can retain an older shutdown record. When no new record arrives after SessionEnd, leave tokens absent so the existing missing-token fallback applies. Keep SessionStart when PID lookup fails. Refs #666 * fix(copilot): distinguish resumed shutdowns --------- Co-authored-by: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com> |
||
|
|
c091e54694 | fix(spawn): hide the remaining Windows console windows on git child launches (#687) | ||
|
|
83141be8f9 | feat(webhook): add webhook integration for team notifications (#665) | ||
|
|
73012087e2 | docs(windows): add Windows hook wiring guide (#660) | ||
|
|
f04d0055d5 | fix(spawn): hide Windows console windows on the remaining child launches (#679) | ||
|
|
52525a9402 |
feat(doctor): extend the delivery check to rules, agents, MCP and env (#669)
* refactor(doctor): resolve delivery destinations through one handler seam `doctor` asked "can this tool receive skills" through `skillsReachTool`, which had to invent a skill name (`__teamai_probe__`) because `skillTargetForTool` fused two questions: whether a tool receives skills at all, and where a given skill lands. Only a comment said the invented name could not affect the first. Split the gate from the path. `skillsDirForTool` answers the gate on its own — OpenClaw's workspace, Hermes' home, Copilot's enabledAgents, else the tool root — and `skillTargetForTool` is that directory plus the skill name, with Codex's shared-directory redirect on top since only that one is per-skill. Add `ResourceHandler.deliveryTargets`, the read-only seam #624 asks for: where an item lands for each tool that receives it, `null` for a resource with no per-tool file destination. `SkillsHandler` implements it, and its `pullItem` now walks the same resolved targets, so the write path and the check cannot answer differently. `buildDeliveryChecks` consumes the seam through the handler registry instead of importing `SkillsHandler` directly; check names, failure buckets and fix text are unchanged. Also points the inactive-skill cleanup at the same gate. It probed the tool root and then swept `<base>/<skills path>`, which for OpenClaw is a directory delivery never writes to — the real workspace copy was never pruned. * feat(doctor): check that rules reached each tool in its own format A rule changes both its filename and its bytes per tool: `.md` verbatim for Claude, `.mdc` with derived `globs`/`alwaysApply` for Cursor-compatible tools, `.instructions.md` with `applyTo` for Copilot. Nothing exposed where one lands, so `doctor` could not ask — the extension table lived inside `pullItem`. `RulesHandler.deliveryTargets` answers it, and `pullItem` now walks the targets it returns rather than rebuilding the gate chain, so the check and the write path resolve the same paths. `resolveDesiredRules` joins `resolveDesiredSkills` in pull.ts: the namespace convention and the tag channel are stated once, and the check reads them rather than restating them. The check reports two buckets per tool: a rule that never arrived, and one that arrived without the frontmatter its tool reads — a `.mdc` without `alwaysApply` is inert, which no write-time gate can see because the write succeeded. The fix names the destination directory, since the filename is not the rule's name. A legacy `.md` left beside a correct `.mdc` is deliberately not reported: it is inert leftover that `pullAllRules` already sweeps, not a delivery failure. * feat(doctor): check that agents reached each tool they target Agents break the items × tools shape the skills check assumes: a spec carries `targets:`, so the desired set is a relation, and each tool renders its own format, so the filename comes from the render and not from the agent's name. `AgentsHandler.deliveryTargets` is therefore the only thing that can say where an agent lands, and `pullItem` now walks the same resolution — the YAML and legacy paths merge into one loop instead of two gate chains. `resolveDesiredAgents` joins its skills and rules siblings in pull.ts, so the namespace filter and its stem-collision throw are stated once; `doctor` reports that throw as a failing check rather than stack-tracing, as it already does for skills. Two bugs surfaced while unifying the paths: - `renderedForTool` decided "legacy" from `item.legacy` alone while `pullItem` also accepted a non-`.yaml` source. An item built without the flag was therefore parsed as a spec by the cleanup and copied verbatim by the pull. `isLegacyAgent` now answers it in one place. - The parse-failure warning was Chinese, which the repo forbids in production code, and went through `console.warn` rather than the logger. Also adds a check for an agent that renders for no installed tool at all: the file is in the team repo, `pull` names the reason once, and nothing afterwards says it is still reaching nobody. * feat(doctor): check that MCP servers reached each tool's own config An MCP server is an entry inside a tool's native config, not a file of its own, so this check takes the shape of the hook check rather than of the delivery seam: it asks which servers the reconcile would want for a tool, then whether that tool's config carries them. The desired-set pass moves out of `reconcileMcpForConfig` into `desiredMcpForTarget`, unchanged — the `tools:` and `roles:` filters, the transport and policy gates, the `requires:` PATH check and the placeholder resolution all stay in one place, and the reconcile now calls it. A second copy of those filters is precisely how a server skipped once for an unresolved variable gets reported as delivered forever after. That skip reason is the point. A server dropped for `unresolved variable(s)` prints one line during a pull and is never mentioned again, so the member sees "MCP does not work" and goes looking at MCP. The check now names the server, the variable and `env/env.yaml` — including that its top-level key must be `variables:`, since a plain `KEY: value` mapping parses as no variables at all and silently skips every injection (#662). A server the member excluded on purpose is not reported; an unparseable tool config is, because the write path abandons the injection there too. * feat(doctor): check that env variables reach a shell, not just a marker The env check asserted that `# [teamai:env:start]` appeared somewhere in the profile. That is true of a block that cannot load and of a run that delivered nothing, so both failures passed and surfaced three layers away as MCP servers skipped for `unresolved variable(s)`, with nothing pointing back at env. It now asks the three questions the marker stands in for: - Does `env.yaml` declare anything? A file with content that parses to zero variables is the shorthand `KEY: value` form, which zod strips to an empty list — the pull then writes nothing and logs nothing (#662). - Did every declared variable reach `env.sh`? - Would the injected block load it? The block is built with the platform separator, so on Windows it carries backslashes; a POSIX shell reads an unquoted `\` as an escape, the `[ -f ... ]` test fails, `&&` short-circuits and `source` never runs, silently (#661). Whitespace in the path needs quotes for the same reason. Neither underlying bug is fixed here — #661 and #662 own those. This is the row missing from the issue's table: env had no check that looks at the payload, which is why both of them reach `All checks passed!`. The check is still emitted when there is nothing to deliver, passing, since `doctor --json` consumers cannot tell an absent entry from a passing one. * feat(doctor): let the caller pick the stage instead of flagging each check The post-pull pass re-runs the registry under a 5s all-or-nothing budget that covers building it as well as running it. Skills and docs cost a stat per item; rules cost a read per rule per tool and agents parse every spec. Adding those to the pass would spend the budget on the expensive checks and lose the cheap ones — and going over means the member gets no check at all. `buildChecks(ctx, stage)` takes 'pull' or 'doctor' and does not build the two expensive registries for 'pull'. The stage is a property of the caller, not of a check, so it is an argument rather than a third optional flag on `Check` beside `source` and `reportedByPull` — which the issue flags as the point where that object stops reading. Skipping is at build time, not a filter over the result: the cost is in building the registry, so filtering afterwards would save nothing. * docs(doctor): describe the rules, agents, MCP and env delivery checks * test(doctor): cover the delivery checks through the built CLI * refactor(doctor): move the delivery checks out of the command file Review findings, all three from the repo's own standards. `doctor.ts` had grown to 959 lines, most of it domain logic: where a rule lands for Cursor, which tools an agent's spec targets, whether a shell block would load. CONTRIBUTING says commands in `src/*.ts` stay thin and the heavy lifting lives elsewhere. The checks move to `doctor-delivery.ts`, and `doctor.ts` is back to being the registry that runs them — smaller now than before this branch. The three per-tool builders repeated one shape: walk items × targets, bucket the failures by tool, remember the directory, format a check. `walkDelivery` holds that walk and takes a `classify` callback for the part that genuinely differs; `describeProblems` formats the buckets in the caller's label order, so the same broken machine reads the same way twice rather than in the order its failures happened. `envDeliveryProblems` had its own copy of the `$SHELL` → `.zshrc`/`.bashrc` choice, a second spelling of what `EnvHandler.detectShellProfile` already decides — the exact failure this branch exists to prevent, one layer down: it would check `.bashrc` while the pull wrote `.zshrc` and call a correct install broken. That method is now public and the check calls it. No check name, failure bucket or fix string changes. * refactor(doctor): drop the unused null return from deliveryTargets AGENTS.md's review rules reject unused flexibility, and this was some. The seam returned `DeliveryTarget[] | null`, where `null` meant "this resource has no per-tool file destination" and `[]` meant "no installed tool receives it here". The single caller wrote `?? []` and treated them alike, so the distinction only cost a branch nobody took. The default is `[]` now, and the comment carries the meaning the type was trying to. * refactor(doctor): drop the imports the delivery move left behind Moving the checks into `doctor-delivery.ts` left nine imports in `doctor.ts` with no remaining user: `fs`, `expandHome`, `listFilesRecursive`, `TEAMAI_ENV_END`, `getMcpSharing`, `usesCursorMdcRules`, `usesCopilotInstructions`, `splitFrontmatter` and the `ResourceItem` type. `tsc --noEmit` stays green either way because `noUnusedLocals` is off, so CI could not have caught these. They make `doctor.ts` look like it still reaches into frontmatter parsing and MCP sharing config, which is the impression the move existed to remove. * fix(doctor): report unreachable agents from the tools, not from the renders `Every team agent reaches a tool` was gated on `byTool.size > 0`, using successful deliveries as the proxy for "some tool was there to receive an agent". It is the wrong proxy for exactly the case it exists to catch: when every agent is malformed or targets tools that are not installed, no agent renders anywhere, `byTool` is empty, no check is built at all, and `doctor` reports success on a machine where nothing arrived. The gate is now the installed tools themselves. `AgentsHandler.agentToolDirs` answers that on its own — the tool-path, exclusion and install gates without asking any agent to render — and `resolveRenders` and the inactive-agent cleanup, which both carried their own copy of that loop, now go through it. * fix(doctor): compare MCP entries with the team definition, not their names The check asked whether the desired server name was a key in the tool's config. Reconciliation never overwrites an entry teamai does not own, so the one case the write path deliberately skips — a server of your own under a team name — satisfied the check: the key is there, the team's server is not, and every later pull skips it again without a word. `installedMcpEntries` replaces `installedMcpServerNames` and returns the entries in the rendered form `desiredMcpForTarget` produces, so the check compares values. Structurally, via `isDeepStrictEqual`: key order in a JSON config is not meaning, and a tool that rewrites its own file should not read as a failure. Codex stores a TOML block rather than a JSON value, so `codexBlockIn` extracts the block by the same regex `spliceCodexBlock` writes with, trimmed to the single trailing newline `renderCodexBlock` emits. A stale entry and a foreign one are reported alike, as `not the team's definition` — both mean the tool is not running what the team declared — and the fix says that a pull leaves an entry teamai does not own alone, so only `--force` replaces it. * fix(doctor): compare env.sh assignments with their values, not their keys `export KEY=` as a substring is true of the value env.yaml declares and of the one it replaced. A rotated credential that never reached `env.sh` — the pull that would rewrite it skips a scope whose team repo has not changed — passed the check while every shell and every MCP server kept exporting the old value, which is the failure this check exists to name. Each declared variable is now compared against the line `generateEnvFile` would write for it, the injection's own rendering rather than a second copy of its quoting, and a key present with a different value is reported as stale rather than as missing. Neither value is printed: these are credentials, and the key is the whole diagnosis. The e2e fixture delivered an MCP entry and an `env.sh` that were not what teamai writes; it now carries the rendered forms, and covers a foreign server under a team name and a stale `env.sh` through the built CLI. * docs(doctor): say what the delivery checks compare, not just that they check The MCP and env paragraphs described a name lookup and a key lookup. Both now compare values, and the MCP one reports a server of your own holding a team name — which only `teamai pull --force` replaces — so the guide and the changelog have to say so. Both language versions. * feat(doctor): compare a delivered agent with its render, not its existence The check asked only whether something readable sat at the destination, which is the same class of gap the three review findings were: an agent rendered from an older spec passes while the tool runs instructions the team replaced. A plain pull syncs a scope only when its team repo changed, so the copy can sit there indefinitely. `DeliveryTarget` carries the bytes `pullItem` writes, which `resolveRenders` already had in hand and threw away at the seam, and the agents check compares them. It is the same equality the inactive-agent cleanup already uses to decide a deployed copy is the team's. Absent `content` means the handler renders nothing — a skill is a directory tree — and only existence is judged, so skills and rules are unchanged. `walkDelivery` passes the target to `classify` rather than its two fields. The fixtures delivered the literal string `rendered`, which the new comparison correctly rejects: the unit tests now deliver through the handler's own seam, and the e2e fixture carries each tool's render byte for byte. * fix(agents): leave a member's same-stem file alone beside a legacy .md Routing the legacy `.md` path through `resolveRenders` also gave it the stale-sibling sweep, which the old `pullLegacyMd` never ran. A team agent named `helper` then deleted a `helper.toml`, `helper.json` or `helper.agent.md` the member wrote, with no ownership or content check. Only a rendered spec can leave a sibling behind: its extension follows the tool's format and changes when `targets` does. A legacy `.md` is copied verbatim to one extension for every tool, so anything else on the stem is not ours. * fix(doctor): compare a delivered rule with its render, not its key names The check read the delivered file for the presence of `alwaysApply` or a nonempty `applyTo`. A `.mdc` whose `globs` no longer match the team rule's `paths:` passes that while Cursor applies it to the wrong files, and so does a body that drifted from the team `.md`. `RulesHandler.deliveryTargets` now carries the bytes `pullItem` writes, the way the agents handler does, and the check compares against them. That makes the render the single spelling of the mapping rather than a contract `doctor` restates in terms of the keys it happens to know about. * fix(doctor): keep the reason an mcp.yaml yielded no servers `parseTeamMcpServers` answers `[]` to an absent file and to one that does not parse alike. That is right for a pull, which can only skip the run, but it left `doctor` unable to tell a team with no MCP from a team whose every server reaches no tool: the desired set was empty, no per-tool check was emitted, and `doctor --json` reported ok: true. `readMcpYaml` returns the parse failure with its reason and the check reports it. `parseMcpYaml` keeps its old shape on top of it, so the pull path is unchanged. * fix(doctor): tell a parse failure from a deliberately empty env.yaml `parseEnvYaml` answers `[]` to four different files: absent, empty, `variables: []`, and the shorthand `KEY: value` mapping whose unknown top-level key zod drops (#662). The check equated zero variables with the shorthand form, so an intentional `variables: []` was reported as malformed. `readEnvYaml` returns the reason instead of the count, so the shorthand form and invalid YAML are both named while an empty configuration fails nothing. * docs(doctor): say what the rules, MCP and env checks compare after the review The guides and the changelog entry describe what each check compares, and three of them now compare something else: a delivered rule against its render rather than its frontmatter keys, an unparsable `mcp.yaml` as its own failing check, and an explicit `variables: []` as an empty configuration rather than a malformed file. The e2e suite covers all four cases through the built CLI. * fix(doctor): check the two rule destinations that are not a file per tool `deliveryTargets` covers what `pullItem` writes under `toolPath.rules`. `pullAllRules` delivers two more things it cannot see, and both fail silently: OpenCode does not auto-scan a rules directory. Every `.md` can be there byte for byte and be inert, because `opencode.json` no longer lists the glob the pull owns — and `Rules delivered to opencode` passes throughout. Hermes has no rules directory at all: its rules are the contents of a managed block in SOUL.md. A deleted or stale block is a tool reading the wrong rules with nothing on disk to show for it. Both take the shape of the hook and MCP checks — one destination, not one per tool. `opencodeInstructionsTarget` and `hermesRulesText` are the single spelling each, so the check reads the answer the pull writes rather than deriving a second one. * fix(doctor): match a multiline env value instead of calling it stale A YAML block scalar is a legal env value, and `generateEnvFile` single-quotes it into an export spanning several physical lines. The check split env.sh on newlines and compared each line with a whole generated export, so such a value could never match: a correct pull was reported as a stale value on every run. `parseEnvFile` is the generator's inverse — it reads the assignments back, including the `'\''` encoding of an embedded quote — and the check compares values rather than lines. * docs(doctor): describe the two rule activation checks and the env inverse Two checks are new and one comparison changed, so the guides and the changelog entry describing them change with it. The e2e suite covers both through the built CLI: OpenCode rules delivered byte for byte while the glob is gone, and a multiline env value that the old line scan called stale. --------- Co-authored-by: Saul Moro <saul.moro@darstelecom.es> |
||
|
|
bd857725d8 | fix: anchor project-relative docs destinations (#652) | ||
|
|
f7205ed2a3 |
fix(omp): report the extension status in hooks list (#648)
Address the non-blocking review finding from #645: hooks list showed OMP as 'not configured' even with the teamai extension installed, because OMP has no settings/hooks path for the generic status check to parse. Add an OMP-specific row that reports the extension file itself (installed / missing), and correct the marker comment in omp-hooks.ts (doctor does not check extensions; uninstall and hooks list do). |
||
|
|
378c3a6392 |
docs: fix CONTRIBUTING branch baseline and dogfood setup (#479) (#626)
Point feature branches at origin/main (not master), prefer worktrees, and document required public-hub init/pull dogfooding for contributors. |
||
|
|
6e47059a8d | fix(skills): mirror deleted files during push (#651) | ||
|
|
9dcd23e3bb | fix(push): preserve namespaced rule destinations (#654) | ||
|
|
437ab2b177 | fix(pull): include root-level claudemd files (#653) | ||
|
|
26eb0df20f |
fix(hooks): resolve Git Bash by absolute path on Windows (#639)
* fix(hooks): resolve Git Bash by absolute path on Windows On Windows, CreateProcess resolves a bare `bash` to %SystemRoot%\System32\bash.exe (the WSL launcher) before any PATH entry, so every rendered `bash -lc "teamai hook-dispatch ..."` hook ran inside WSL instead of Git Bash. There the user's npm-global `teamai`/Node 20 are typically absent, and the trailing `2>/dev/null || true` silently swallowed the failure, so hooks appeared to succeed but did nothing. ZCode already sidesteps this via a `cmd /c` fallback and WorkBuddy/CodeBuddy use a PATH-prefix wrapper, but shell-string tools had no protection. On win32 we now emit the resolved Git Bash path (quoted, forward slashes so it stays JSON-safe and tolerates the "Program Files" space), discovered from the standard install locations first, then the HKLM GitForWindows InstallPath. When Git truly is not found we fall back to bare `bash` (the old form was already inert there). POSIX keeps the bare `bash`, so the golden fixtures stay byte-identical. The golden comparison is skipped on win32 since the rendered path is machine-specific. * fix(hooks): render Copilot powershell field with the call operator on Windows getDispatchCommand() now prefixes the launcher with a quoted Git Bash path on Windows, which COPILOT_BUILTIN_COMMAND_RE did not recognize — the raw bash command landed verbatim in the powershell field, where PowerShell cannot parse a quoted executable without the call operator (&). Accept the quoted launcher in the regex and render it back with & in front and ; exit 0 in place of || true; the bare-bash form keeps the existing POSIX output byte-for-byte. Add a Windows rendering test that asserts both fields exactly (stringContaining could not catch this). * test(hooks): skip only machine-dependent golden cases on Windows The suite-level skipIf(win32) also dropped codebuddy and workbuddy, whose wrapper dispatch commands stay machine-independent — restore their compatibility coverage by skipping only the getDispatchCommand cases (claude, claude-internal, cursor) whose rendered launcher differs per machine on Windows. * docs: sync usage guides with the Windows Git Bash hook resolution Document the absolute-path Git Bash dispatch on Windows in both languages, and stop presenting the WSL-bash sidestep as a ZCode-only perk now that every shell-string target avoids the same trap. * fix(hooks): also probe %ProgramW6432% for 64-bit Git Bash on Windows A 32-bit Node process on 64-bit Windows resolves %ProgramFiles% to the x86 tree, so a standard 64-bit Git install stayed undiscovered and hooks degraded to bare bash (the WSL launcher). Cover it with a test. |
||
|
|
610ec142dd |
fix(update): short-circuit checkForUpdate when cache is valid but no update was found (#676)
The cache guard at line 422 required `state.availableUpdate` to be truthy,
so it never fired when the last check found no newer version (the common
case — `availableUpdate` is persisted as `null`). Every Stop hook therefore
spawned `npm view` and made a registry round-trip, defeating the 12 h TTL
entirely.
Split the guard: when the cache is valid and `availableUpdate` is null,
return `{ available: false }` immediately instead of falling through to
`fetchLatestVersion()`.
Closes #671
|
||
|
|
4e334c5389 |
fix(ci): fail closed when rejection checks fail (#674)
* fix(ci): fail closed when rejection checks fail * docs(ci): document fail-closed rejection checks |
||
|
|
e10bbcbb23 |
feat(copilot): sync MCP servers (#628)
* feat(copilot): sync MCP servers * fix(copilot): detect MCP-only installations * fix(copilot): detect project MCP config * fix(copilot): preserve bare project MCP maps * fix(copilot): honor local-agent MCP paths --------- Co-authored-by: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com> |
||
|
|
c3a5d8e01a | docs(readme): replace product tagline with shared-capability narrative (#658) | ||
|
|
c6367b96da |
docs(readme): center the header block and simplify the logo (#657)
* docs(readme): center the header block and simplify the logo Center the title, language switcher, status badges and the Trendshift badge so the whole header lines up with the logo. Replace the wordmark banner with a compact rounded app-icon style logo. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(readme): use the Tencent brand blue in the logo Co-authored-by: Cursor <cursoragent@cursor.com> * docs(readme): move the Trendshift badge above the language switcher Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: jeffyxu <jeffyxu@tencent.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
810109b8c2 |
docs(teamai-skill): drive GitHub login from the assistant, not the user (#655)
* docs(teamai-skill): drive GitHub login from the assistant, not the user
The teamai skill's GitHub login step was a bare `gh auth login`, unlike the
TGit/CNB steps that spell out "you run the login; the user only approves in the
browser." That asymmetry made the assistant hand the raw command to the user
("run this with the ! prefix"), which non-technical users can't do.
Rewrite the GitHub step in join-member.md and setup-admin.md to match TGit/CNB:
the assistant runs the login (or lets `teamai init` auto-start `gh auth login
--web`, which it already does), relays the device code + URL, and the user's
only action is approving in the browser. Add the headless GITHUB_TOKEN escape
hatch for parity.
* docs(teamai-skill): make 'log in first' the GitHub step, not init auto-trigger
Drop the 'let teamai init auto-start gh auth login' path as the primary route.
The GitHub step now mirrors TGit: the assistant runs `gh auth login --web`
first, as an explicit step, and the user only approves in the browser.
|
||
|
|
3d5f490daf |
feat(omp): run lifecycle hooks via the OMP extension runner (#645)
OMP has no settings.json hook list — it auto-loads TS extensions from ~/.omp/agent/extensions. Add the omp-hooks.ts adapter (mirroring the OpenCode plugin): a single generated extension forwarding OMP's session_start / session_stop / before_agent_start / tool_result events to `teamai hook-dispatch --tool omp`, gated on the session cwd, never returning session_stop continuation fields, and with no matcher pass (OMP tool ids are lowercase; it has no Skill / TodoWrite tool). Wire it into the inject and reconcile loops, discover + remove it on uninstall, flip the README hooks column to ✓, and document the hook-capable list and the OMP section. Part of #550; stacked on #643. |
||
|
|
97a02777ac | feat(omp): add first-class Oh My Pi (OMP) resource sync (#643) | ||
|
|
e30c1ad06f |
docs(review): spell out P1/P2 severity meaning in review findings (#646)
The Codex review posts findings tagged bare 'P1'/'P2', but the review rules never defined what those codes mean, so first-time readers cannot tell a blocking issue from a suggestion. Add a Code Review Rule requiring each finding's severity to be spelled out inline (keeping the P marker): [P1 blocking] must-fix before merge, [P2 non-blocking] suggestion, in the PR author's language. Co-authored-by: review <review@local> |
||
|
|
d4d2b8923f |
feat(pull): run team scripts.postPull after a pull (#633)
teamai.yaml may declare `scripts.postPull: { path }`: a Node entrypoint
the CLI runs once a pull has fully finished (resources, hooks, MCP and
reports done), for team-owned deployment the CLI knows nothing about.
The path must resolve inside the clone, symlinks included. The script
runs as a child of the pull process itself - no supervisor, no second
escape: on the session-start path that process already left the host's
job object (see hook-dispatch-cli.ts), so the script survives the host
by construction and inherits the child's hidden console. How the pull
launches it depends on who triggered the pull:
- a headless (hook) pull waits for it under a budget
(POST_PULL_BUDGET_SEC, fitted under the pull handler's own
PULL_TIMEOUT_MS by a guard test), so its outcome line lands before
the handler deadline can exit the process. On expiry the script is
orphaned, never killed - killing a deploy mid-flight strands the
machine it was updating, while leaving it running costs nothing -
the next pull reconciles. The budget is exported to the script
(TEAMAI_POSTPULL_TIMEOUT_SEC) so its inner npm/git step can cut
itself off with a clean error line instead of being cut down.
- an interactive pull shares the user's terminal, has no deadline to
fit and no job object to escape: the script is launched
fire-and-forget with that terminal attached. The launch shape is
picked where the caller is known (GlobalOptions.interactive).
A bad path, a missing file or a failed spawn is a log line in
debug.log (launched / exited / timed out), never a failed pull.
Also folds in the path-safety fix that the missing-script case needs:
resolveReal() realpath'd existing paths but fell back to the lexical
form for missing ones, so a containment check under a symlinked prefix
compared one resolved side with one unresolved side - on macOS
(/var -> /private/var tmpdirs) a legitimate path was rejected as "Path
traversal detected", and a missing leaf reached through an escaping
symlink slipped past the guard. Missing paths now resolve through
their nearest existing ancestor and re-append the rest; rejections
name the compared (resolved) paths.
Co-authored-by: flowjzh <flowjzh@users.noreply.github.com>
|
||
|
|
f0fd890208 |
docs(readme): collapse the command-line install path by default (#644)
Wrap Install / Team admin / Team members in a <details> block (collapsed by default) so Quick Start leads with the /teamai flow. The usage-guide link stays outside the fold since it's useful to everyone. Applied to en/zh-CN/ja/ko/th; content unchanged, only wrapped. Co-authored-by: review <review@local> |
||
|
|
1ea599adf2 |
feat(dashboard): unify workspace views, themes and localization (#604)
* feat(dashboard): unify workspace views, themes and localization * fix(dashboard): address review, decouple cache metric, fix session attribution Review fixes (@laolaoPlayer): - #1 self-mode workspace read the wrong KB: route dashboard workspace discovery through the exported readConfigFrom so the self-mode repo rebind is applied; log the real /api/context error instead of swallowing. - #3 workspace membership frozen at startup: recompute on a short TTL and route unmatched sessions to a dedicated "unassigned" bucket instead of silently inflating User scope. - #4 add workspaceEvents unit tests and a ?workspace= scoping e2e (project scope + linked worktree + unassigned). - #5 delete the drifted, unreferenced demos/dashboard/ prototype. - #6 harden report i18n: tag every report title with data-i18n, drop the brittle regex special-cases, add a guard test that every data-i18n label has a translation. Cost/metric accuracy: - feat(pricing): modelAliases config maps gateway model aliases (e.g. ep-qxst1hw4) to known Claude models so cost estimation works behind a gateway; falls back to the built-in table when unset. - fix(trends): decouple cache-read share from pricing — derive it from the session's own transcript tokens so it shows even when the model can't be priced. Session-data fixes: - fix(collector): drop UserPromptSubmit events that are purely injected content (task-notifications, system-reminders, interrupt markers) so they no longer inflate the prompt count or appear as prompts. - fix(collector): dedupe cross-tool duplicate events at the readEvents boundary — a host (e.g. Cursor) that also loads claude's hooks double-fires every event; collapse the pair, keep the specific host tool. Fixes Cursor sessions being labelled claude and turn counts doubling. UI: drop the "All local workspaces" option, the sidebar accent dot and the bottom "TeamAI Dashboard" text; remove the low-signal "Usage & sessions" panel row and the "Active duration" / "Session success" trend cards; rename "团队执行" to "团队执行环境" (zh only). |
||
|
|
c89aef4f75 |
docs(readme): make quick-start prompts one-click copyable (#642)
Move each /teamai prompt (and the bootstrap line) into its own fenced code block so GitHub renders a copy button — blockquotes and inline code have none. Labels become bold headings above each block. Applied to en/zh-CN/ja/ko/th; prompt text unchanged. Co-authored-by: review <review@local> |
||
|
|
9e8f2abf07 |
fix: prevent infinite nudge loop when model declares empty referenced-doc-ids (#614)
* fix: treat empty referenced-doc-ids [] as a valid declaration so the Stop nudge does not loop extractReferencedDocIds matched `[]` but produced no ids, so `declared.length === 0` was indistinguishable from "no declaration". A model that correctly reported "nothing used" was nudged again on every Stop (Claude Code re-runs the model on additionalContext, up to 8 times per turn), for every turn of the session. Expose hasReferencedDocIdsDeclaration from the transcript parser and gate the nudge on it. Nudge cadence per tool and the A/B telemetry fields are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: do not count a placeholder-only referenced-doc-ids list as a declaration A marker whose list has no valid id (e.g. `[<id1>, <id2>, ...]` copied from the rule template) previously set hasReferencedDocIdsDeclaration and suppressed the nudge. Only an explicitly empty list or one with at least one valid id now counts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: shenshijia <shenshijia@kuaishou.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a599fbfd02 |
docs(readme): add /teamai quick start to all README languages (#641)
Replace the top of Quick Start with a one-line bootstrap prompt (load the teamai skill from its repo URL, then set up a team) followed by the four /teamai flows: set up from scratch, join, share a skill, open the dashboard. Applied to en/zh-CN/ja/ko/th; the /teamai lines, URLs, and commands stay verbatim while the surrounding prose is localized per file. |
||
|
|
701e472b0b |
feat(doctor): run the checks after a pull, and check what actually landed (#625)
* feat(pull): report failing checks at the end of an interactive pull Every line a pull prints reports what it did; none reported what is on disk. That gap is the shape of #574, #525, #342 and friends: "Synced N skills" and the tool receives nothing. An explicit `teamai pull` now re-runs the doctor registry and prints only the checks that failed, with the fix each one already carries. The SessionStart hook path (`pull({ silent: true })`) and `--dry-run` run no checks at all, so session startup is unchanged. Checks now declare `source: 'local' | 'provider'`. The post-pull pass runs the local ones only: the pull just used the provider successfully, so re-probing `gh auth status` would add a subprocess to every sync and prove nothing new. `teamai doctor` still runs the full registry. For #598. * feat(doctor): fail when an enabled tool is not installed buildHookChecks skipped any tool whose settings directory was missing — the same silent skip #574 reports in pull, reproduced inside doctor. With `claude` installed and `codex` not, the report was all green while codex received nothing. A tool listed in `enabledAgents` is the user's own claim that they use it, so it now yields a failing `<tool> is installed` check with a fix that points at `teamai uninstall --agent <tool>`. Without `enabledAgents` the team's tool list is aspirational and an absent tool stays silent, so no existing install grows a new red line. For #598. * refactor(pull): extract resolveDesiredSkills from pullForScope pullForScope computed the desired skill set — role namespaces union the subscribed tags, minus the exclusions — and dropped it when the run ended. The delivery check needs the same set, and re-deriving it there would put that policy in a second place that drifts on its own. The block moves to an exported, read-only resolveDesiredSkills(), called from where it stood. roleContext stays an explicit argument: pullForScope already holds one, and null means "no roles configured", not "not looked up yet". No behaviour change. For #598. * feat(doctor): check that the desired skills actually landed on disk Every other check verifies plumbing — provider CLI, clone, config, hooks, env. None verified the payload, which is what #574, #525, #342 and #372 are actually about: the run reports success and the agent finds nothing. `Skills delivered to <tool>` compares the desired set (role namespaces union subscribed tags, minus exclusions) against what is on disk for each installed tool, and names the skills that are missing. It catches what a write-time gate cannot: per-tool skips, and drift after a correct pull — a directory deleted by hand, a tool reinstalled, a role changed. Destination resolution moves into skillTargetForTool(), so pull writes and doctor checks the same paths, including Codex's shared .agents/skills directory. A tool that is not installed is asked for nothing; enabledAgents covers that case with its own check. For #598. * feat(doctor): report a skill that landed but stays invisible A copy can arrive intact and still never be discovered: SKILL.md deleted, frontmatter that does not parse, or a `name` that does not match its directory (#372's class). The write succeeded, so no write-time gate has anything to report. The delivery check now separates the two causes — "not delivered" from "delivered but unreadable" — and the fix says which one `teamai pull` can repair and which one needs the team repo fixed. For #598. * fix(doctor): check every enabled tool, not only the ones with hooks Hanging "is this tool installed" off the hook registry made it invisible for exactly the tools most likely to be declared and absent: OpenCode and CodeBuddy ship skills and no hook configuration, so `buildHookChecks` returned before the question was ever asked. Found driving the real CLI: `enabledAgents: [claude, opencode]` with no OpenCode root printed nothing. The check moves to its own builder over ctx.toolPaths, and probes a resource path rather than the settings path — resources land under resolveToolBaseDir (the project root in project scope), which is the root a pull would have to write into. For #598. * fix(doctor): survive a team repo it cannot resolve a desired set from Two findings from the review pass. A repo with the same skill in two active namespaces makes scanRoleAwareSkills throw. pullForScope catches it and the post-pull pass catches it, but `teamai doctor` called buildChecks unguarded: the command whose job is explaining bad state stack-traced on it. It now reports a failing "Skills to deliver can be resolved" check carrying the collision. `copilot is installed` could never fail — isToolInstalledForConfig counts Copilot as installed as soon as enabledAgents names it — so that dead check is gone. Copilot's delivery check still reports what did not arrive. Also: pull and doctor now share formatCheckResult instead of two copies of the same glyphs, the timeout message interpolates its constant, and the DesiredSkills block no longer sits between skillSafeToRemove's doc comment and its function. For #598. * docs: document the post-pull checks and the delivery check Both guides, both languages, same positions: the manual-pull block, the doctor section, the exclusion and tag-subscription paragraphs, and the packages-section one-liner that enumerated what doctor checks. No README change: the `teamai doctor` row still describes it, and touching it would cost five synchronized translations. For #598. * feat(doctor): check the team docs bundle landed Docs are the one payload with a single destination instead of one per tool, so the check is a tree comparison rather than a per-tool loop: every non-dot file under the team repo's `docs/` against `sharing.docs.localDir`, with the same filter the copy uses. The destination resolution moves out of DocsHandler.pullItem into resolveDocsDestination(), so pull writes and doctor checks the same directory — including the project-scope rule that a `~/` prefix means the project root, not HOME. Found while validating it: a doc deleted by hand is not restored by the next pull, because the rev fast-path skips the scope. The check is what makes that visible. For #598. * fix(doctor): stop telling people to run the pull they just ran The delivery fixes said "Run `teamai pull`" — printed at the end of a `teamai pull`, and wrong besides: a scope whose team repo has not moved is skipped by the revision fast-path, so a plain pull cannot restore a resource deleted after a correct sync, which is the main case these checks exist to catch. Both fixes now say `teamai pull --force` and why. The underlying gap — that a plain pull does not heal drift — is filed as its own issue. For #598. * fix(pull): do not repeat, at the end of a pull, what the pull already said #621 landed a "Contributed learnings are published" check on the same registry this pass now runs. A pull with a stuck queue therefore said the same thing twice, and contradicted itself doing it: pullForScope warns "run `teamai doctor` for what to check", then the post-pull block answers with the check's own fix, "Run `teamai pull` to publish them" — the pull that had just run. The warning is the better of the two and has to stay: it carries the push error, which the check cannot learn without attempting a push of its own, and `doctor` is a read-only diagnostic. Rewording the fix is no good either, because in `teamai doctor` — where the queue publish runs before the revision fast-path, so a plain pull really is the retry — that advice is correct. So `Check` gains `reportedByPull`, and the post-pull pass skips a check whose topic this run reported. Evidence, not a declaration: pullForScope returns before the publish step when the team repo fails to refresh, and swallows a publish throw into a debug line. On both paths the pull says nothing about the queue, so suppressing the check unconditionally would leave a stuck queue reported by nobody. The topic is a union rather than a boolean for the same reason: the pull proves what it reported by naming it, so a second tagged check cannot be silenced by the first one's evidence. For #598. * fix(doctor): cap the delivery fix's name list, as the docs one already does `nameList` was written for the docs check and used only there, while the delivery check — the one most likely to have a long list, since a fresh machine is missing every skill at once — joined its names unbounded. A member with forty desired skills got all forty pasted into one fix line. Both now go through the helper, which moves above its first caller. Also drops a stray blank line that a rebase left between `skillSafeToRemove`'s docstring and the function, detaching the two. For #598. * fix(skills): stop reporting a Codex conflict nobody can act on `resolveSkillDestination` warns when a skill exists in both `.agents/skills` and `.codex/skills`, unless it can prove the two are identical. That proof needs the team copy, so the check is written as `sourcePath && ...` — and without a sourcePath the guard short-circuits into the warning instead of past it. The read-only callers are the ones that omit it. `teamai remove skills` already did on main; this branch added `buildDeliveryChecks`, so the warning now fires once per skill on every `teamai doctor` and at the end of every pull, for copies the write path silently reconciles. Omitting sourcePath now returns the shared destination before the reconciliation branch, which is what the function's own docstring already promised. The write path is untouched: with a source, an unprovable pair still warns. For #598. * fix(doctor): own the post-pull evidence, bound the whole pass, report both ways Three things a review of the post-pull pass turned up. The set of what a run already said was a module-level `const` cleared at the top of `pull()`, and the topic it held was a union with one member: two pieces of machinery where one value does. `pull()` now owns the set and passes it down. It is a required parameter of `pullForScope` rather than a field on its optional `policy`, because a call site that forgot it would stop recording silently, which is the failure the mechanism exists to prevent. `PullReportedTopic` is gone and `Check.reportedByPull` is a plain string. The 5s budget wrapped `runChecks` only, while the I/O is in `buildChecks`: the delivery checks stat every desired skill for every tool as the registry is built. Both are inside it now. And the pass no longer goes quiet when it gives up — silence after spending the whole budget is the same "reported success, nothing happened" shape these checks exist to catch, so it says one line and points at `teamai doctor`. The reason stays on the debug channel. `<tool> is installed` only pushed a check when it already failed, so an installed tool had no entry at all. `doctor --json` is consumed by hooks and CI, where a missing entry cannot be told apart from one that passed, and no other check in the registry behaves that way. It now reports both ways. Docs in both languages and the CHANGELOG follow, including a note that the checks at the end of a pull cover the scope resolved from the current directory. For #598. * fix(doctor): judge a tool where the sync writes, and keep off a busy clone Three findings from the Codex review. A pull that found a scope's lock held by another process drops that scope from every stage that reads the shared clone, because the other process may have it on a transient branch. The post-pull checks resolve their own context from that same clone and ran anyway, so a diagnostic could report a failure about someone else's work in progress. They now stay out entirely when any scope was contended; `teamai doctor` runs them once the other process is done. `<tool> is installed` probed the tool root while skill delivery asks `skillTargetForTool`, which sends OpenClaw to its workspace directory, Hermes to its home, and Copilot through `enabledAgents`. A `~/.openclaw` with no workspace therefore passed the check while delivery skipped the tool and its delivery check vanished — "reported success, received nothing" inside the command written to catch it. The probe is now `skillsReachTool`, which asks that same resolver; a tool that configures no skills path keeps the generic one, having no such resolver to ask. `Team docs delivered` called `pathExists`, which follows symlinks and says yes to a directory, so a name occupied by something other than the document passed while the document was no more readable than a missing one. It now requires a file. Reading each one would cost more than the job needs on a bundle of hundreds of documents, so this stats rather than reads, and the guides no longer claim the docs check does everything the skills check does. For #598. |
||
|
|
bea46d110e |
fix(zcode): launch hooks via hidden wscript VBS — no console flash, bounded wait (#596)
Windows ZCode hook entries launch through wscript.exe running a hidden teamai-hook-dispatch.vbs written next to config.json, eliminating the console-window flash on every hook run. The launcher spools STDIN to a temp file to preserve the payload contract and runs the dispatch with a per-event bounded wait (SessionStart 180s, Stop/UserPromptSubmit 60s, PostToolUse 30s). POSIX entries keep bash -lc. Identity fields are salvaged when a multi-byte payload degrades at the ANSI-codepage spool step, and getHookStatus requires the launcher on win32. |
||
|
|
da174c91c8 |
feat(copilot): deliver instructions and context (#627)
* feat(copilot): deliver instructions and context * docs: sync Copilot support matrix * fix(copilot): refresh managed instructions on upgrade * fix(copilot): reconcile custom-home resources * test(copilot): cover default-home skill deployment * fix(copilot): preserve culture on read failure --------- Co-authored-by: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com> |
||
|
|
7683c54064 |
ci(codex-review): pin codex-action to v1.11 and cap review job runtime (#640)
openai/codex-action@v1 now resolves to v1.12+, which rewrote the codex-exec runner to wait on the whole process tree's stdio. The backgrounded Responses API proxy keeps those descriptors open, so the step never sees stdio close and hangs until timeout — the review is produced (final-message is printed) but the job idles for hours and the Post-comment step never runs, so the review is thrown away. Pin to v1.11, the last release before the regression (identical final-message output contract), and add timeout-minutes: 15 to the review job so any future hang is capped instead of idling for the default 6h. Refs: openai/codex-action#151, openai/codex#9269 |
||
|
|
5750279486 |
feat(skills): add teamai onboarding skill (#572) (#605)
* feat(skills): add teamai onboarding skill (#572) Publish a standard SKILL.md-format `teamai` skill that interactively guides users (including those unfamiliar with Git) through setting up, joining, managing, and contributing to a team AI repo. - Manual-trigger only (/teamai); progressive-disclosure menu, no action on bare invoke - Scenario references: admin setup, member onboarding, daily management, contribute, uninstall, plus a shared troubleshooting guide - Replies in the user's language; agent-specific hook caveats (Codex trust-gate, Cursor, CodeBuddy/WorkBuddy, ChatGPT App) - Privacy note: no third-party data reporting; only the team repo Supersedes #517. * feat(skills): address review feedback on teamai skill (#572) - Point 'contribute learnings' to the dedicated teamai-share-learnings skill; refocus contribute-member.md on publishing a reusable skill - Spell out daily management as publish/update skills, rules, MCP, env; add MCP, projects, and dashboard sections to manage-admin.md - Add a dashboard menu entry / cheat-sheet line (teamai dashboard) - Don't restrict install to one agent: set up all installed AI tools by default and report which agents were configured (new global rule 9) - Add projects to the cheat sheet (manage several projects from one repo) * docs(skills): address teamai skill review — TGit provider, member access, tighter description (#572) - SKILL.md: trim description to <100 tokens; keep the "invoke only on explicit /teamai, never auto-trigger" constraint. - setup-admin.md: add Tencent TGit (工蜂) as a first-class, top-listed platform when a request to git.woa.com returns the `x-env: tgit` header. Covered in 2a/2b probes, the 2c create-repo table, and a new `gf auth login` step (teamai auto-installs the gf CLI; no GITLAB_URL needed). - setup-admin.md: new Step 7 — admin must grant each member read/write access on the platform before handoff, or their init/pull/push fails (teamai has no permission model of its own). Renumber later steps. - join-member.md: add the matching git.woa.com → `gf auth login` entry. * docs(skills): fix member contribute hints — auto-share is automatic, any member can publish a skill (#572) The member-facing "what's next" hints were wrong on two counts: - Sharing session learnings is NOT a manual `/teamai` menu choice — it is auto-triggered by the Stop hook (contributeCheckHandler) at session end and gated by the admin's team-sharing toggle (sharing.contributeHint.enabled, on by default). Reworded the menu, routing note, join-member wrap-up, and contribute-member to say so, and stop offering a `/teamai` line for it. - Any member (not just admins) can publish a skill, by asking in plain language ("share this xxx skill with my team"). Added that as the member menu row / routing entry and stated it in contribute-member. Also documented the admin on/off toggle + resolution order in manage-admin (verified against isContributeHintEnabled; contribute-hint-toggle tests pass). * docs(skills): TGit needs no manual login — init auto-installs gf and prompts (#572) The 工蜂 login instructions were wrong: they told the user to run `gf auth login` in the agent. In fact the tgit provider does it all inside `teamai init` — ensureInstalled() auto-downloads the gf CLI and authenticate() launches the interactive login mid-init when the user isn't authorized yet. - setup-admin Step 3 / 2c note / Step 5 caveat: drop the manual `gf auth login` command; state that init installs gf and handles the login on its own (user only approves in browser / iOA when prompted). - join-member Step 3: same fix for the member side. - Keep TGIT_TOKEN only as the headless/CI escape hatch. * docs(skills): document optional gf pre-install using teamai's own method (#572) Add an optional path for the agent to install the gf CLI before init, using the exact same source, install dir, and verification teamai uses internally (gf-cli.ts) — not a hand-rolled URL: - source: http://mirrors.tencent.com/repository/generic/gongfeng-cli/.../gf-<os>-<arch>.tar.gz - dir: ${TEAMAI_HOME:-~/.teamai}/gf ; verify: test -x <dir>/gf/bin/gf - darwin|linux × x64|arm64 only Login stays automatic (init launches it); this only lets you pre-fetch gf. join-member points to the same commands rather than duplicating the script. Verified end-to-end on darwin/arm64 (download + extract + test -x all pass). * docs(skills): agent installs gf + logs in; prefer auto-creating the TGit repo (#572) Per review: stop describing gf install/login as something teamai/init does automatically. The agent drives it. - setup-admin Step 3 (TGit): the agent installs gf (teamai's own download + test -x verify) AND runs `gf … auth login` (verified: binary + auth login subcommand exist); user only approves the browser/iOA prompt. Drop the "init auto-installs/auto-logs-in" wording (2a note, 2c note, Step 5 caveat). - Repo creation: prefer letting `teamai init` create the TGit repo via the API (init offers the create prompt and calls provider.createRepo); only fall back to git.woa.com/projects/new when the group/namespace is missing or the user lacks create permission. - join-member: member side also has the agent install gf + log in, reusing the setup-admin commands. * docs(skills): agent runs gf install AND login itself, user only approves (#572) Per review: never tell the user to run a gf command. The agent runs both the install and `gf … auth login`; the user's only action is approving the login in the browser / iOA. - setup-admin Step 3 (TGit): reword to "YOU run gf install and login"; describe gf auth login's real interactive flow (iOA / browser device code / token — verified via `gf auth login --help`); confirm with `gf auth whoami`. - join-member: same — agent runs both, user only approves the URL. --------- Co-authored-by: review <review@local> |
||
|
|
470d229ff8 |
fix(codex-review): let maintainer-authorized fork PRs re-review on push (#636)
The auto re-review added in #631 never worked for fork PRs. Our gate job correctly authorizes a re-review when the PR carries a maintainer assignee, but codex-action then runs its OWN write-access check against the triggering actor — which on a fork PR's `synchronize` is the PR author (read access). So every fork auto re-review failed with: Actor '<author>' is not permitted to run this action ... Detected 'read'. Pass `allow-users: "*"` to disable codex-action's actor check. This does NOT widen who can trigger a review: our `gate` job is the real, stricter authorization door (an un-authorized PR never reaches this job at all), and codex-action's check is redundant with it while being wrong for the fork push case. All secret-protecting layers are unchanged: gate authorization, trusted base checkout, PR code read as diff data only (never built/installed/ run), persist-credentials: false, and Codex staying :read-only. |
||
|
|
b9a2b926eb | fix(cache): reject malformed GC integer options (#634) | ||
|
|
63aa2d1c5b |
ci(codex-review): assign once, then auto re-review on push (#631)
Previously a maintainer had to re-assign the PR after every push to get an
updated review. Now assigning once authorizes the PR, and later pushes
re-review automatically.
- Add synchronize + ready_for_review to the trigger.
- The gate no longer keys off who triggered the run (a push's actor is the
fork author, who would always fail the check). Instead:
* assigned -> the assigner must be a maintainer (unchanged intent)
* synchronize / -> the PR must already carry a maintainer assignee,
ready_for_review i.e. it was authorized by an earlier assign
- Skip re-review on pushes to a draft PR; wait for ready_for_review.
Security is unchanged: an un-authorized fork PR (no maintainer assignee)
never runs on its own pushes, the workflow still comes from the base branch,
PR code is still read-only diff data, and Codex stays read-only.
Gate logic verified locally with a 10-case truth table covering authorized/
unauthorized assigns, re-review with/without a maintainer assignee, and the
draft skip.
|
||
|
|
c60ade9cbd | feat(copilot): sync custom agents (#629) | ||
|
|
1f79b2d45e |
feat(contribute): make the pending queue the write path, and say when it is stuck (#621)
`pending-learnings/` was a durable queue that only ever caught a failed push, and nothing mentioned it. A member without push rights queued notes forever while being told each time that the next pull would retry, and in single-repo mode a rejected push lost the note outright. Contributing is now: write to the queue, index it, publish from the queue. - A queue entry is dropped only once its content is confirmed on origin, in every mode. Single-repo mode goes through the same queue, which is what stops it losing notes. - One function knows the destination, so contribute runs no git command itself. - The queue is indexed ahead of the published roots, so a contribution is recallable the moment it is written, online or not, and a queued edit of a published learning is the copy recall serves. - `teamai pull` reports what it published and warns, with the reason, when anything is still queued. - `teamai doctor` carries a check for the queue with an actionable fix, on the existing registry, so `doctor --json` gets it for free. Both stay silent when the queue is empty. Three things a review of the queue's edges turned up, fixed here: - A queue entry nobody can read was skipped on every run, so the warning said "1 learning is not published" forever with no reason. It now names the file. - The hidden-entry filter split paths on the platform separator, while the directory walk always joins with `/`, so on Windows a hidden entry inside a namespace was published. - The publisher's docstring claimed it stops at the first failure; every entry rides in one commit. Closes #615 |
||
|
|
67b6f080a9 |
fix(report): scope usage by path on Windows too (#630)
* fix(report): scope usage by path on Windows too `filterEventsByScope` decides which sessions go to a project team's repo and which stay in the user scope. Both sides of that comparison are native paths: `projectRoot` is stored as `path.resolve(cwd)` at init time, and an event's `cwd` is whatever the AI tool put in its hook payload. The check was written against POSIX separators — `root + '/'` as the prefix — so on Windows only a session started in the project root itself ever matched. The two directions fail together. The project repo gets `projectRoot: C:\p` and keeps only sessions whose cwd is exactly that, dropping every session started in `C:\p\packages\api` or any other subdirectory. The user repo is handed the same root in `excludeProjectRoots` to subtract those sessions, and misses the same ones — so they land in the user scope's report instead. The usage guide promises reporting stays isolated in project mode; on Windows it did not. Separators are now unified before comparing, and trailing ones dropped, which leaves POSIX paths byte-identical. The new tests pass Windows paths as plain strings, so the ubuntu CI exercises the case that was never covered. * fix(report): fold case on Windows scope paths, leave POSIX ones alone Unifying the separators was not enough. A Windows path is case-insensitive, and the two sides of the comparison come from different places: `projectRoot` is `path.resolve(cwd)` recorded once at init time, while an event's `cwd` is whatever the AI tool wrote into its hook payload. They can disagree on the case of the drive letter or of any directory along the way, and every such disagreement leaks the same way as the separator bug did — a project session dropped from the team's report and counted in the user scope instead. Folding case unconditionally would have broken POSIX, though, in the other direction: POSIX paths are case-sensitive, and `\` is a legal character in a POSIX filename, so `/work/a\b` and `/work/a/b` are two different directories that the previous `scopeKey` collapsed onto one key. A session under a backslash-named sibling was silently pulled into an unrelated scope. The root now decides which set of rules applies to both sides of the comparison. A drive-letter or UNC root folds separators and case; anything else is compared byte for byte, trailing slashes aside. Driving it from the root rather than testing each path on its own keeps a Windows root matching a cwd the tool reported with forward slashes, and guarantees a POSIX root never has a backslash rewritten underneath it. Five assertions cover it: drive-letter and directory casing in both directions, a UNC root, POSIX case sensitivity, and a backslash filename that must neither match nor escape a sibling root. |
||
|
|
09d0472c23 |
ci(codex-review): review fork PRs too, via gated pull_request_target (#622)
The pull_request trigger never receives secrets on fork PRs, so the Codex review silently no-op'd for external contributions (the majority of PRs). Switch to pull_request_target so fork PRs can access the API key, and add the safeguards that trigger requires: - A maintainer gate job: only a user with write/admin/maintain permission can trigger the review; unauthorized assigns skip it. This stops strangers from burning the API budget or exercising the token. - Check out the trusted BASE commit, never the PR head. The PR changes are exposed to Codex only through git diff — read as passive data, never built, installed, or executed (npm install alone would run a fork's pre/postinstall hooks). - Review rules come from the base AGENTS.md, so a PR cannot rewrite its own criteria. - persist-credentials: false keeps the repo token off disk. Codex stays read-only throughout. |
||
|
|
6ed25152de |
fix(hooks): survive the Windows job object (#608)
The detached child that runs the session-start pull inherited the hook's job object: `detached: true` only adds DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP, and leaving a job needs CREATE_BREAKAWAY_FROM_JOB, which node never passes. A host that terminates that job when the hook's direct child exits (WorkBuddy/CodeBuddy do) killed the pull about a second into every session: the machine never synced, and nothing was logged, because the pull died before its first line. spawnBackground now creates that child through the WMI service instead - outside any job - with the console hidden at creation (Win32_ProcessStartup.ShowWindow = 0) and the creating PowerShell itself hidden (CREATE_NO_WINDOW). WMI has no STDIN pipe, so the hook payload travels as a temp file named on the command line. A refused WMI call falls back to the plain detached spawn, and says so in debug.log: a silent fallback here is exactly how the bug this path exists for looked in the field. Two things that path must not assume: PowerShell is resolved by absolute path (a hook inherits its host's PATH - measured: WorkBuddy's leaves a bare `powershell.exe` unresolvable, which quietly degraded every escape to an in-job spawn), and the escape runs while the foreground pass does, so a hook pays ~0.3s at most instead of blocking in front of it. Give the pull its own budget too: a handler timeout does not merely stop us waiting - index.ts process.exit(0)s as soon as the dispatch pass settles, so the shared 15s truncated cold pulls (fetch + submodules + reconcile, measured at 10-25s) mid-flight. Co-authored-by: flowjzh <flowjzh@users.noreply.github.com> |
||
|
|
d4c57a0961 |
feat(learnings): write learnings to the teamai-learnings branch (#616)
* refactor(branch): extract the orphan-branch worktree engine `reports-branch.ts` managed the `teamai-reports` orphan branch: cold-start creation across two git versions, stale-worktree repair, a non-blocking lock, commit, and push with fetch+rebase retry. Learnings need the same machinery on their own branch (#485), and must not share the branch, the worktree or the lock. The engine moves to `branch-worktree.ts`, parameterised by a spec of three names plus an init commit message. Reports become one instance of it and keep every exported name, so no caller changes. `reports-branch.ts` keeps what is not a side branch: `EmptyRepoError` and `withKnowledgeWorktree`. - A publish returns `PublishResult` instead of a boolean. A caller holding the only durable copy of its data must be able to tell "landed on origin" from "another writer holds the lock", "nothing to commit" and "rejected". Reports map it back to today's boolean, so their behaviour is unchanged. - A publish confirms the ref actually moved before reporting success, and separates "nothing to commit" from "nothing to deliver": an earlier attempt may have committed the content and failed to push it. A successful push updates the remote-tracking ref locally, so the check is a local rev-list. Anything unreadable counts as not landed, which costs one extra push instead of losing data. - `getWorktreeDir` and `getBusinessRoot` come out of the reports-specific helpers; `getReportsDir` is now one call to the first. - `usesReportsBranch` was a one-line forward; its eleven call sites use `usesBranchWorktree`, which says what it means now that two branches use it. - A side branch's `.gitignore` lists every worktree directory, so no worktree can nest-track another. - `ensureReportsDir` had no callers and duplicated `ensureReportsWorktree`. Refs #485 * feat(learnings): write learnings to teamai-learnings, read them from every root Contributing no longer touches the default branch, so a member whose team protects `main` can contribute. Everything the team wrote before the switch stays readable exactly where it is: nothing is copied, deleted or migrated. **One accessor.** Eleven call sites built the learnings path by hand, from a different base each time, so a forgotten one did not fail — it read an empty directory and recall quietly returned less. `learningsRoots(localConfig)` now answers with a write root and an ordered read list, and a test fails on a new hand-built path. Two call sites have no config to resolve (the `viz --repo` flag and CI) and opt out in place, with the reason on the line. **Every root is read.** `buildIndex` takes the whole list. For one relative path the first root wins, deduplicated while collecting: that path is also the id votes are counted by, so two entries would double-count votes and then have one silently dropped by recall's dedup. The clone's `learnings/` is always the last root, which is what keeps the pre-switch corpus searchable. **One publish path.** `teamai contribute` writes into the `teamai-learnings` worktree and pushes, for independent clones and single-repo installs alike. Single-repo mode stops opening a pull request per contribution, so the disposable knowledge worktree and the machine-wide cache copy that stood in for it are gone — and with them the cross-project leak of writing one project's learnings into a cache every project shares. What cannot be published stays in the durable queue outside the clone and is retried by the next `teamai pull`. **Maintenance.** Pruning, promotion and confidence write-backs used to mutate a checkout nothing pushes: the result reached no teammate, and a realign could undo it. Promotion was worse — the `promoted_to` mark it wrote was discarded, so the same learning was promoted again on the next run and paid for another model call. They now write to the write root, publish what they changed, and refuse to prune an inherited learning instead of deleting a file that comes back. Fixes that fall out of reading the same code: - A recall that had to rebuild the index passed no project namespaces, so every project-private learning vanished from the rebuilt index while a pull-built one had them. - Recall printed the absolute path an entry had when it was indexed; a worktree that is removed and rebuilt leaves that dangling, and the agent reading the output got a dead pointer. - `pull`'s mirror deletes whatever its source does not have, so it now takes every published root except the mirror itself — pointed at one root it would have stopped propagating upstream deletions (#458). - The learnings count in `pull` deduplicates across roots, the way the index does. - Contributing self-heals the single-repo `.gitignore` first, the way `pull` and `push` already do, so the new worktree never shows up in the user's own `git status`. - Recall says why a scope was skipped instead of swallowing the reason: an invalid projects manifest reported "No learnings available" and nothing else. Real-git coverage against a bare origin whose `update` hook refuses the default branch: the learning lands on the side branch with `main` untouched, a refused branch leaves the note recoverable, a commit whose push failed is delivered on the next attempt, two members publishing at once both land, one member reads the other's learning after a refresh, and the inherited corpus is still there. Refs #485 * docs: document the branch layout and the minimum Git permissions (#486) A team that turns on branch protection needs to know what still works and what access its members actually need. The docs said learnings live on the default branch, and the providers guide said the push target is hardcoded to `master`. - The data-layout table covers knowledge, learnings, reports and machine-local data, and says for each how it is written and whether it needs write access to the default branch. EN and ZH match row for row. - A minimum-permissions section in the usage guide and in the providers guide: what a member needs, what they do not, and that `provider: git` still cannot open a pull request for you while `contribute` never needs one. - The directory trees show `learnings-wt/` and `pending-learnings/`. - The admin checklist says `init` commits an empty `learnings/` and that contributions do not go there. - `docs/providers.md` no longer says the push target is hardcoded: it is resolved by `getDefaultBranch()`. The same stale claim in the docstring of `pushRepoDirectly` goes with it. - The share-learnings skill names the branch instead of "directly to master". - All five READMEs name the branch in the `teamai contribute` row. - Both design documents describe the new split. Closes #486 * fix(contribute): keep a contribution findable when nothing can be published Splitting this change in two left a gap the full version did not have. Before learnings moved to their own branch the note was written into the clone, so it was indexed no matter what git did. Now a contribution is written into the branch worktree — and when that worktree cannot be created at all, the durable copy was kept but never indexed, so the member could not recall what they had just written. - The queue is a learnings root for the index, in `contribute` and in `pull`. - The durable copy is written before the index is rebuilt, not after, or there would be nothing to index. The queue stays the fallback here; making it the write path is #615. Refs #485 * fix(learnings): do not call a successful push a failure, and cap how long it can take A review pass against edge cases found three ways this change could report or behave worse than what it replaced. **A successful push read as a failure.** The publish confirmed delivery by checking that HEAD was no longer ahead of `origin/<branch>`. That ref is only updated through the remote's FETCH refspec, so in a clone made with `--single-branch` — what CI checkouts and many business repos are — pushing a side branch leaves no tracking ref behind and the check said "not landed". The result was five pushes of the same content and a reported failure although the data was on origin, and for reports a `save-session` that printed a push timeout for a write that had worked. A push that resolves is a push the remote accepted; git exits non-zero when it refuses one. The tracking ref is now only consulted to answer "does this worktree still owe origin a commit?", where an unreadable answer means push and find out. **No cap on how long publishing can take.** `contribute` used to give its push ten seconds. That cap was lost, so an unreachable origin or a credential prompt could block the CLI through five push and rebase rounds. Both publish paths carry it again; timing out is safe because the durable copy stays. **A warning aimed at a backend with no branch.** Maintenance on an HTTP team repo has nothing to publish, and said so as a failure. It stays quiet. Also from the same pass: - `teamai pull` publishes the queue for every repo kind. It sat inside the team-repo refresh, which returns early for single-repo and HTTP, so the promise contribute makes — "will retry on the next pull" — was only true for independent clones. - The auto-migration skips `learnings-wt/` along with the other worktrees, driven by the shared list so the next worktree is covered without anyone remembering this file. `pending-learnings/` deliberately still travels: it is work the member has already done. - `recall`'s own index rebuild reads the queue, so a contribution that could not be published does not vanish when anything invalidates the index. - The learnings mirror carries every visible file in an active namespace again, not only Markdown. Refs #485 |
||
|
|
0c585098a2 |
ci: add Codex PR review on assign (#618)
Add a GitHub Actions workflow that runs an automated Codex review when a pull request is assigned. Codex reads the PR diff in read-only mode, follows the new AGENTS.md "Code Review Rules" section, and posts its findings as a PR comment in the PR author's language. The review flags bugs, rule violations, and PRs whose description lacks a test plan or an end-to-end record. PR title/body/diff are treated as untrusted data to guard against prompt injection. The Responses API endpoint and review model are optional, configurable via secret/variable and fall back to Codex defaults when unset. Co-authored-by: review <review@local> |
||
|
|
a06400e430 |
fix(local-agent): only prompt project binding for CodeBuddy/WorkBuddy (#613)
The ClawPro project-binding prompt fired for every host that runs the teamai hook — Claude, Cursor, Codex included — even though ClawPro project binding only backs CodeBuddy/WorkBuddy. Those other users got a "[ClawPro项目 绑定提示]" choice list injected into their session with no way to act on it, which is the poor UX being reported. Gate the whole prompt (both the SessionStart TTY prompt via ensureWorkspaceBinding and the UserPromptSubmit hint via emitBindingHint) on the current tool being a buddy agent, at the single `reportAndSyncLocalAgent` entry point. Non-buddy tools now short-circuit before resolving the workspace, so they fork no git process and never fetch /projects/mine. Reuses `modelAgentKind` for the check so tool-name variants like `codebuddy-internal` still match (a raw Set would miss them). Tests: existing bind-hint cases retargeted to a buddy agent so they keep their discriminating power; added a parametrized case asserting claude and cursor emit no hint and skip the project fetch. Full suite green (3307). Verified end-to-end against a mock backend with the built CLI: codebuddy/workbuddy/codebuddy-internal inject the hint; claude/cursor/codex produce no output. |
||
|
|
0c059b2da6 |
docs: add README rule to CLAUDE.md and AGENTS.md (#607)
Keep README changes minimal, and when a change is needed all language variants (README.md and every README.*.md) must be updated in sync. |
||
|
|
d3f8634488 |
fix(doctor): remove duplicate LocalConfig import breaking the build (#606)
src/doctor.ts imported LocalConfig twice — once in the top type-only import (line 5) and once in the grouped import block from './types.js'. TypeScript rejected this with TS2300 (Duplicate identifier 'LocalConfig'), so `tsc --noEmit` failed and the main CI has been red since #599. The two imports landed cleanly as a merge (no textual conflict) but collided at the type level, so each PR's own branch build was green. Drop the LocalConfig binding from line 5 and keep it in the grouped block alongside TeamaiConfig, matching the surrounding style. |
||
|
|
3d4353f45e |
feat(remove): add --force to skip the confirmation prompt (#594)
`askConfirmation` returns false when stdin is not a TTY, and `teamai remove` had no flag to skip it, so every scripted run printed "Cancelled" and exited 0. The command could not be used from a script, and it had no end-to-end test, which is how both defects in #576 survived. `--force` is spelled and described the same way as `teamai uninstall --force`, and the prompt keeps the same guard shape so the two stay refactorable together. The command's action was also dropping its `cmdOpts` argument, so the option is wired through the way `uninstall` does it. The new end-to-end test drives the built CLI against a bare repository and asserts the whole contract. The deployed copy goes immediately, and the deletion plus its tombstone are published as a branch for review. `checkoutMaster` returns the clone to the default branch, so the clone's own working tree keeps the file until that branch merges. Fixes #591 |
||
|
|
0982976baf |
feat(doctor): export the check registry and add --json (#599)
The checks that catch "reported success, nothing on disk" lived inside doctor() as a local array, so nothing else could run them, and the only machine-readable result was the exit code #569 added. Extract resolveDoctorContext() and buildChecks(), which render nothing, and add --json: one object on stdout, every log line on stderr, exit code unchanged. Human output is byte-for-byte what it was. |
||
|
|
295cea0414 |
ci: add informational code-erosion (slop metrics) workflow (#588)
* ci: add informational code-erosion (slop metrics) workflow
Report SlopCodeBench verbosity/erosion metrics on every PR using the
official scb-check tool, pinned to 0.2.0 (the first release with
TypeScript support; 0.1.3 is Python-only).
The workflow is informational and never blocks a merge: scb-check's exit
code is swallowed, and the numbers are posted as a deduplicated PR comment
with the run's job summary as a fallback (so fork PRs, whose token is
read-only, still surface the report). Tests are excluded via scb-check.toml
so metrics reflect the product surface.
On TypeScript the ast-grep verbosity rule component is Python-only and
contributes 0, so verbosity reflects clone + wrapper detection only;
erosion is fully faithful. This caveat is documented in the bilingual
docs/ci-code-erosion.{md,zh-CN.md}.
* ci(code-erosion): add independent TS verbosity rule layer
scb-check only runs its ast-grep rules on Python files, so on this
TypeScript repo its verbosity rule component is always 0. This adds a
standalone ast-grep pass with a small, hand-ported rule set to fill that
gap, reported as a separate "Rule hits (TS verbosity layer)" section in
the same non-blocking PR comment.
Only purely structural rules are ported. Rules that hinge on truthiness or
type semantics (len==0, ==True, redundant template strings) were tried and
deliberately dropped: they are false positives in TypeScript, where
arr.length>0 is idiomatic and x!==true is not equivalent to x===false
(TS has undefined). Ported rules verified against src/ for false positives:
unnecessary-else-after-return, empty-catch-block, redundant-ternary-same,
if-return-boolean-literal, return-ternary-boolean-literal,
duplicated-if-condition, self-assignment.
Rules use severity: hint and the scan step has `|| true`, so the layer
never blocks CI. Bilingual docs updated with the honest scope: this is an
extra signal, not a reproduction of the paper's verbosity number.
* ci(code-erosion): slim down the PR comment, defer detail to docs
The comment carried long inline explanations (verbosity footnote, rule-layer
paragraph). Move the prose to docs/ci-code-erosion.md and keep the comment to
numbers plus a one-line pointer. Also replace the ambiguous "(informational)"
tag with plain "never blocks the merge".
* ci(code-erosion): drop the two repeated doc links in the comment
The top line already points to docs/ci-code-erosion.md; the verbosity
footnote and rule-layer note repeated the same link. Keep one pointer.
|