979 Commits
Author SHA1 Message Date
Carlos 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.
2026-09-21 10:44:12 +08:00
Ben YounesandBen Younes 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>
2026-09-21 10:43:05 +08:00
Carlos c091e54694 fix(spawn): hide the remaining Windows console windows on git child launches (#687) 2026-09-21 09:58:40 +08:00
Jokebear 83141be8f9 feat(webhook): add webhook integration for team notifications (#665) 2026-09-20 23:59:33 +08:00
BeastAyyG 73012087e2 docs(windows): add Windows hook wiring guide (#660) 2026-09-20 23:57:05 +08:00
CarlosWonMore f04d0055d5 fix(spawn): hide Windows console windows on the remaining child launches (#679) 2026-09-20 23:50:35 +08:00
Saul MoroandSaul Moro 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>
2026-09-20 23:29:01 +08:00
dvd233 bd857725d8 fix: anchor project-relative docs destinations (#652) 2026-09-20 23:28:16 +08:00
pablo 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).
2026-09-20 23:27:24 +08:00
Frank_zhu 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.
2026-09-20 21:58:51 +08:00
dvd233 6e47059a8d fix(skills): mirror deleted files during push (#651) 2026-09-20 21:36:13 +08:00
dvd233 9dcd23e3bb fix(push): preserve namespaced rule destinations (#654) 2026-09-20 21:28:01 +08:00
dvd233 437ab2b177 fix(pull): include root-level claudemd files (#653) 2026-09-20 21:27:21 +08:00
BuXiuDN 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.
2026-09-20 19:31:16 +08:00
Leo Camus 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
2026-09-20 19:28:43 +08:00
Ruifeng Xue 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
2026-09-20 12:26:48 +08:00
Ben YounesandBen Younes 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>
2026-09-19 22:32:15 +08:00
jeff c3a5d8e01a docs(readme): replace product tagline with shared-capability narrative (#658) 2026-09-19 16:50:28 +08:00
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>
2026-09-19 14:52:33 +08:00
jeff 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.
2026-09-19 10:27:59 +08:00
pablo 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.
2026-09-18 23:54:50 +08:00
pablo 97a02777ac feat(omp): add first-class Oh My Pi (OMP) resource sync (#643) 2026-09-18 22:30:56 +08:00
jeffandreview 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>
2026-09-18 21:39:21 +08:00
flowjzhandflowjzh 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>
2026-09-18 21:30:06 +08:00
jeffandreview 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>
2026-09-18 21:24:31 +08:00
jeff 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).
2026-09-18 21:15:17 +08:00
jeffandreview 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>
2026-09-18 21:09:27 +08:00
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>
2026-09-18 20:59:55 +08:00
jeff 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.
2026-09-18 20:57:52 +08:00
Saul Moro 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.
2026-09-18 20:52:36 +08:00
titto 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.
2026-09-18 20:36:24 +08:00
Ben YounesandBen Younes 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>
2026-09-18 20:18:15 +08:00
jeff 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
2026-09-18 20:15:58 +08:00
jeffandreview 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>
2026-09-18 19:55:18 +08:00
jeff 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.
2026-09-18 15:32:38 +08:00
Ruifeng Xue b9a2b926eb fix(cache): reject malformed GC integer options (#634) 2026-09-18 15:06:32 +08:00
jeff 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.
2026-09-18 11:46:08 +08:00
Ben Younes c60ade9cbd feat(copilot): sync custom agents (#629) 2026-09-18 11:36:00 +08:00
Saul Moro 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
2026-09-18 11:35:20 +08:00
Leo Camus 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.
2026-09-18 11:30:04 +08:00
jeff 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.
2026-09-18 10:34:34 +08:00
flowjzhandflowjzh 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>
2026-09-17 20:55:21 +08:00
Saul Moro 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
2026-09-17 20:53:11 +08:00
jeffandreview 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>
2026-09-17 20:31:34 +08:00
jeff 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.
2026-09-17 20:03:27 +08:00
jeff 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.
2026-09-17 14:14:40 +08:00
jeff 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.
2026-09-17 12:42:26 +08:00
Saul Moro 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
2026-09-17 12:08:00 +08:00
Saul Moro 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.
2026-09-17 11:55:06 +08:00
jeff 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.
2026-09-17 11:25:06 +08:00