Commit Graph
838 Commits
Author SHA1 Message Date
Jeff ff0d30c19c docs: slim README to a landing page and move product details out (#722)
Keep README as title, Why TeamAI, quick start, docs links, contributors, and contributing. Move architecture and capability details into product-overview, and the command table into the usage guide.
2026-09-22 16:37:56 +08:00
Jeffandreview 3735a4672d fix(learnings): refresh knowledge branch on pull, fix recall roots + single-branch checkout (#704, #705, #706) (#709)
#704: An independent teamai-learnings update never moves main's revision, so
the "Already synced" fast-return in pull skipped the learnings-branch refresh
and index rebuild — a teammate's contribution only surfaced after `pull --force`.
Hoist the learnings-sync + index-rebuild into a helper and run it on the
fast-return path too. It is read-only (pushIfCreated:false) and never publishes,
so it stays inside the caller's partition sync lock and does not reintroduce the
flush-outside-lock bug.

#705: contribute indexed pending-learnings, then published + deleted the pending
files without refreshing the index, so recall handed back a File path under the
now-emptied pending dir. Rebuild the index once more after a successful publish
so every published entry resolves to its durable worktree copy.

#706: A --single-branch clone's fetch refspec covers only the default branch, so
`fetch origin <branch>` moved only FETCH_HEAD and left origin/<branch> absent,
then `worktree add --track` failed and the knowledge worktree was never created.
Fetch the tracking ref with an explicit refspec, and check the worktree out with
`--no-track` (nothing relies on git upstream config; every sync references
origin/<branch> directly). Shared helper — reports branch verified not regressed.

Regression tests: real-CLI e2e for #704/#705 (learnings-sync-704-705) and a
real-git single-branch checkout test for #706 (git-kind-learnings). Both confirmed
to fail on the pre-fix code and pass after.

Co-authored-by: review <review@local>
2026-09-22 14:17:47 +08:00
dvd233 2c0ae96f6a fix(opencode): spawn hook dispatcher without windows popup (#688) 2026-09-21 23:10:54 +08:00
zdGrandeand林志达 04b6de2219 fix(hooks): run CodeBuddy's Windows hooks through cmd.exe (#637)
* fix(hooks): run CodeBuddy's Windows hooks through cmd.exe

bundled-runtime: CodeBuddy provides cmd.exe on Windows, so the /bin/sh shell gate is a false negative there. POSIX keeps the old check.

builtin-hooks: render cmd-syntax commands for codebuddy on win32 and write a teamai.cmd shim, so the PATHEXT lookup can resolve the CLI.

hooks: render the project gate in the host shell's syntax and recognise both renderings, so a platform switch cannot leave dead duplicates.

tests: win32 cases with a spied platform, so ubuntu CI covers them.

* fix(hooks): exit 0 on a project-gate miss and align the cmd wrapper path

hooks: the cmd gate `${gate} && (payload)` let a gate miss inherit findstr's
exit status — non-zero is surfaced as a hook error, and exit 2 blocks
UserPromptSubmit. Use `& if not errorlevel 1 (payload) else exit /b 0`: a miss
exits 0, a match still passes the payload's own status through.

builtin-hooks: the cmd wrapper searched `%USERPROFILE%\.teamai\bin` while the
writer uses getUserHome(), so HOME/USERPROFILE could diverge and `teamai` was
never found. Embed the resolved bin dir; drop TEAMAI_BIN_DIR_WIN.

tests: update the cmd-gate and codebuddy PATH assertions.

* fix: escape cmd project gate for CodeBuddy hooks on Windows

Cmd shell metacharacters in project roots break the team-hook
project gate; escape the root literal and match with findstr.
Sync tests and docs.

---------

Co-authored-by: 林志达 <linzhida@bosssoft.com.cn>
2026-09-21 23:09:45 +08:00
Hill Patel cc38871818 fix(env): detect the right shell profile file on Windows (#682) (#693) 2026-09-21 18:08:52 +08:00
zerolxy612 f4d09c2cc8 fix: redact prompt summaries before local persistence (#685) 2026-09-21 18:05:01 +08:00
Saul Moro a3366b9bf6 chore(git): ignore local copilot agent sync files (#694)
Ignore Copilot sync targets under .github/ (.github/hooks/teamai.json, .github/skills/, .github/instructions/*.instructions.md, .github/agents/, .github/copilot-instructions.md, and .github/mcp.json) as a best-effort ignore pattern while preserving other GitHub files.
2026-09-21 17:05:52 +08:00
yimi528 1f52967dbd fix(pid-monitor): resolve the process tree on Windows (#691)
`getParentPid()` and `getProcessComm()` read Linux /proc and fall back to
`ps -o`. Windows has neither: /proc does not exist, and the MSYS `ps` Git Bash
ships rejects `-o`. Both therefore returned undefined for every PID,
`resolveMonitorPid()` broke out of its walk on the first level, and the `best`
it returned was the hook's own parent — the launcher, not the AI tool.

`dashboard.ts` hands that value to `isProcessAlive()`, and a false answer
becomes a `process_exit` event, i.e. `status = 'stopped'`: on Windows the exit
signal was tied to the launcher's lifetime instead of the tool's.

Windows now reads the table from one Win32_Process query — the walk needs up to
five ancestors, so a per-pid PowerShell start would cost more than the whole
answer — and the walk itself moved into `walkToNonShell()`, leaving the POSIX
readers exactly as they were. `windowsPowerShell()` moves to
`src/utils/powershell.ts`, shared with the hook dispatcher that already needed
the same path resolution.
2026-09-21 11:28:44 +08:00
Carlos 71b3a5fab5 fix(env): warn when env.yaml has no top-level variables: key (#681)
* fix(env): warn when env.yaml has no top-level `variables:` key

`pullItem` parses env.yaml with a schema whose `variables` field carries
`.default([])`, and zod drops unknown keys without a word. A file that is a
bare `FOO: bar` mapping — or that misspells the key — therefore parsed as "no
variables", and `pullItem` returned at its length check. Every env variable
silently stopped being delivered and nothing in the output said why.

Report that one shape instead of tightening the schema: no `variables` key
plus at least one other top-level key produces a warning that names the keys
found and shows the expected `key`/`value` form.

`.strict()` was the obvious alternative and is deliberately not used. It turns
a valid `variables:` list that carries an extra top-level key into a hard parse
failure that stops env delivery for that team repo — an upgrade regression for
repos that never had a problem. Only the silent no-op is reported; every other
shape stays permissive.

Fixes #662

* fix(env): warn from pullForScope when env.yaml has no `variables:` key

pullForScope skips the env resource as soon as countEnvVars() reports 0
(src/pull.ts:828), and an env.yaml with no top-level `variables:` key answers
exactly that — so a warning raised inside pullItem never runs on a real pull,
which is what the review on #681 found.

The detection moves to a shape probe the orchestration layer can call before
it skips the resource: describeEnvYamlShapeProblem() names the top-level keys
that were found and states the shape `variables:` must have, and
describeEnvYamlShapeProblemAt() reads a file through it.

Only this one shape is reported, per #662: a valid `variables:` list that also
carries an extra top-level key keeps being delivered rather than starting to
fail to parse.

The handler-level tests cover the detection; pull-env-shape-warning.test.ts
drives pull() itself, so the wiring that makes the warning reachable is what
is under test.

* fix(env): also warn about a malformed env.yaml from the unchanged-rev fast path

The review pointed out that `pullForScope` raises the shape warning from the
Step 2 resource loop, and the "Already synced" branch returns before that loop
ever runs. A machine that recorded `lastPullRev` while the CLI still accepted a
bad shape therefore takes the fast path on every later pull and never sees the
warning — which is exactly the population the check exists for: the repo has
not moved, so the rev never changes and the warning can never fire.

The check now lives in `warnIfEnvYamlShapeIsWrong()` and is called from both
sites, so the two cannot drift apart. `resourceTypes` moves above the fast path
so that branch can tell whether this scope syncs env at all.

pull-env-shape-warning.test.ts gains a same-rev case: it pulls twice against the
same mocked rev and asserts both that the fast path was taken (the "Already
synced" success line) and that the warning still fired. Deleting the new call
fails that case alone — the other three stay green.
2026-09-21 10:45:01 +08:00
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