Skip to content

feat: unified hooks management — team-declared hooks + built-in hooks as data (#19) - #65

Merged
jeff-r2026 merged 1 commit into
mainfrom
feature/unified-hooks
Jun 29, 2026
Merged

feat: unified hooks management — team-declared hooks + built-in hooks as data (#19)#65
jeff-r2026 merged 1 commit into
mainfrom
feature/unified-hooks

Conversation

@jeff-r2026

Copy link
Copy Markdown
Collaborator

Closes #19.

What

Lowers the CLI's built-in operational hooks to a single HookDef data model and drives both them (A) and team-declared hooks from a new hooks/hooks.yaml (B) through one reconcileHooks() engine, delivered to every AI tool (Claude Code, CodeBuddy, Cursor, …) by teamai pull.

Authoring stays zero-new-command for admins: edit hooks/hooks.yaml + git push, same mental model as env.yaml.

How it's structured

  • src/builtin-hooks.ts — built-in hooks as HookDef[]; output is byte-identical to the previous hardcoded version (golden fixtures pin it), so upgrading the CLI is a zero-diff reconcile.
  • src/hooks.ts — single reconcile engine (Claude + Cursor) + ~/.teamai/managed-hooks.json manifest. Team hooks are isolated by a [teamai:hook:<id>] marker (Cursor via the manifest). A built-in-only refresh (injectHooks / autoMigrate) never touches team hooks (teamActive gate). Kept the existing getHookStatus() from feat(hooks): add hooks list command #62 and reimplemented it on the new model.
  • src/resources/hooks.tsHooksHandler + Zod HooksYamlSchema + parse/resolve; registered via static import (tree-shaking-safe).
  • pull()reconcileHooksAllScopes() runs outside the rev fast-path, so new built-in hooks and hooks.yaml edits self-heal on every session start.
  • CLIteamai hooks list now shows per-tool install status and the effective A+B audit; status counts team hooks; uninstall strips team hooks via the manifest.
  • §4.8 — team builtin: overrides (disabled / whitelisted timeout); empty = no change.
  • §6 securityTEAMAI_HOOKS_DISABLED kill-switch, command transparency on apply, sharing.hooks.autoApply consent gate (pull only hints; explicit hooks inject applies), sharing.hooks.requireTeamScripts whitelist.
  • Docs: README + README.zh-CN.

Backward compatibility

  • Built-in hook disk output is byte-identical (golden test) → already-installed machines see a zero-diff reconcile after upgrade.
  • sharing.hooks is optional in the inferred config type, so existing TeamaiConfig literals/fixtures stay valid.
  • Marker namespaces ([teamai] vs [teamai:hook:]) keep A and B injection/cleanup from ever touching each other.

Tests

TDD throughout, with a golden byte-equality anchor kept green across the refactor. New: builtin-hooks, hooks-golden, hooks-team, hooks-handler, hooks-reconcile-scope, hooks-security, and a real-CLI hooks-team-e2e (spawns the teamai binary through inject → list → edit → kill-switch → remove).

  • Unit: 1564 passed · typecheck clean · build OK.
  • E2E: hooks suites green (golden 4, hooks-e2e 14, hooks-team-e2e 5).

🤖 Generated with Claude Code

…ta (#19)

Lower the CLI's built-in operational hooks to a single HookDef data model and
drive both them (A) and team-declared hooks from hooks/hooks.yaml (B) through
one reconcileHooks() engine, delivered to every AI tool by `teamai pull`.

- builtin-hooks.ts: built-in hooks as HookDef[]; byte-identical output (golden
  fixtures pin it) so upgrades are a zero-diff reconcile.
- hooks.ts: single reconcile engine (Claude + Cursor) + ~/.teamai/managed-hooks.json
  manifest; team hooks isolated via [teamai:hook:<id>] marker. Built-in-only
  refresh never touches team hooks (teamActive gate).
- resources/hooks.ts: HooksHandler + Zod HooksYamlSchema + parse/resolve; registered.
- pull(): reconcileHooksAllScopes() outside the rev fast-path → self-heal each session.
- CLI: `teamai hooks list`; status count; uninstall strips team hooks via manifest.
- §4.8 builtin overrides (disabled / timeout whitelist).
- §6 security: TEAMAI_HOOKS_DISABLED kill-switch, command transparency,
  sharing.hooks.autoApply consent gate, requireTeamScripts whitelist.
- Docs: README + README.zh-CN.

Tests: +115 hook-specific (golden, engine, team, handler, scope, security,
overrides) incl. a real-CLI hooks-team-e2e; full suite 1524 unit + e2e green.

Closes #19

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeff-r2026
jeff-r2026 merged commit a42b2e1 into main Jun 29, 2026
7 checks passed
@jeff-r2026
jeff-r2026 deleted the feature/unified-hooks branch June 29, 2026 08:07
jeff-r2026 added a commit that referenced this pull request Jun 30, 2026
Reconcile the issue-#1 HTTP init path onto main's unified-hooks (#65)
architecture: the old `injectHooksToAllTools(toolPaths, baseDir)` entry no
longer exists, so the HTTP consumer now injects hooks via the same
authoritative `reconcileTeamHooksForConfig` path the git init uses. Fixes the
tsc TS2304 (injectHooksToAllTools / resolveBaseDir not found) seen in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
jeff-r2026 added a commit that referenced this pull request Jun 30, 2026
Reconcile the issue-#1 HTTP init path onto main's unified-hooks (#65)
architecture: the old `injectHooksToAllTools(toolPaths, baseDir)` entry no
longer exists, so the HTTP consumer now injects hooks via the same
authoritative `reconcileTeamHooksForConfig` path the git init uses. Fixes the
tsc TS2304 (injectHooksToAllTools / resolveBaseDir not found) seen in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
jeff-r2026 added a commit that referenced this pull request Jun 30, 2026
Reconcile the issue-#1 HTTP init path onto main's unified-hooks (#65)
architecture: the old `injectHooksToAllTools(toolPaths, baseDir)` entry no
longer exists, so the HTTP consumer now injects hooks via the same
authoritative `reconcileTeamHooksForConfig` path the git init uses. Fixes the
tsc TS2304 (injectHooksToAllTools / resolveBaseDir not found) seen in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
jeff-r2026 added a commit that referenced this pull request Jun 30, 2026
)

* feat: HTTP team repo + hooks-based agent status reporting (issue #1)

实现 issue #1 评审通过的两套方案,共享同一套 skill 分发原语。

方案一(git-free HTTP 团队仓库 + skill 只读拉取):
- source-http.ts: GET /repo 物化(files[] 内联 + commands[] 走共享执行器),
  路径穿越防护,version 作为增量缓存 key
- init --http <url>: 只读消费者 onboarding(只需 API key,跳过 git/clone/member/reviewer)
- pull.ts: 抽象 refreshTeamRepo() 收口 git/http 两种刷新,其余管线原样复用
- read-only.ts: http kind 下 push/contribute/remove 明确拒绝

方案二(hooks 驱动的 agent 状态上报):
- machine-id.ts: 跨平台 machine_id(macOS ioreg / Windows reg / Linux machine-id)
  + local_agent_id 派生(install_path 仅本地哈希,不上报)
- status-report.ts: report/sync/ack 三接口 + 离线队列 + clawpro/local 来源区分;
  接口路径走可覆盖的内部映射(默认 iWiki 契约,TEAMAI_REPORT_PATHS 可覆盖)
- 挂到既有 hook dispatch: session-start→report+sync,prompt-submit→sync
- openclaw-hooks.ts: WorkBuddy(龙虾系)HOOK.md + handler.ts 注入适配器

共享地基:
- skill-command.ts: executeSkillCommand(fflate 解压 zip,含 SKILL.md 校验、
  路径穿越防护、SMH 直连下载),push/pull 两条路径共用
- api-key.ts: 统一 Bearer 凭证解析/保存(0600,不入 config/不上报)
- teamai login <key> 命令

测试: 本地实现三接口 mock HTTP 服务(进程内 helper + scripts/mock-teamai-server.mjs
独立可运行版),单测 + 集成端到端共 49 个新用例全部通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit e8e50532024bfccf83a1b968cfc7d5a0de4472e3)

* fix: align local-agent reporter with updated backend contract

后端契约调整(对齐 clawpro https://5hborhrw.cvmopenclaw.site):
1. 接口名去掉 v1:/api/v1/local-agent/* → /api/local-agent/*
2. ack 的 command id 从 path 移到 request body,类型 int:
   POST /api/local-agent/commands/ack,body { id: <int>, status, error }

- status-report.ts: EndpointMap.ack 由路径构造函数改为固定路径字符串;
  默认路径去 v1;ack body 带 int id。TEAMAI_REPORT_PATHS 覆盖保留。
- skill-command.ts: SkillCommand.id string → number。
- mock-server.ts / mock-teamai-server.mjs / 单测同步更新。

端到端验证(真实后端):report/sync → 200,ack 路由按 int body id 校验;
完整 install→ack 闭环(mock)通过。tsc 通过,vitest 1454 passed 无回归。

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit fe5d649f4f29a0e70996a011ee670bb32ec7ca27)

* fix: wire WorkBuddy via settings.json Claude hooks (not OpenClaw)

Real-device verification (WorkBuddy 5.2.0) shows WorkBuddy embeds the CodeBuddy
CLI engine and reads Claude-format hooks from ~/.workbuddy/settings.json:
SessionStart / UserPromptSubmit / PostToolUse all fire with PascalCase event
names and CLI-style tool names (tool_name="Bash"), identical to codebuddy.

MR 191 originally (wrongly) assumed WorkBuddy used the OpenClaw HOOK.md engine,
so teamai never wired it (0 workbuddy events on a real machine). Fix:

- types.ts: toolPaths.workbuddy now carries settings: '.workbuddy/settings.json'
  → routes through the Claude-format injection path like codebuddy.
- hooks.ts: drop workbuddy from OPENCLAW_TOOLS (kept for the still-unverified
  openclaw/qclaw/easyclaw/autoclaw variants).
- openclaw-hooks.ts: clarify it's no longer for WorkBuddy; default tool=openclaw.
- tests updated to assert workbuddy → settings.json hooks (--tool workbuddy),
  OpenClaw HOOK.md path only for openclaw.

Verified end-to-end on the real machine: `teamai hooks inject` writes the
workbuddy hook block (preserving claw/enabledPlugins/sandbox); real WorkBuddy
fires the hooks; `hook-dispatch --tool workbuddy` records tool=workbuddy in the
dashboard. tsc OK, vitest 110 files / 1466 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit d1ec907433177e3ac76ed81867fec928be647293)

* fix: teamai uninstall also removes OpenClaw HOOK.md hooks

MR 191 added injectOpenClawHooks but uninstall only removed hooks from tools
with a `settings` path, so OpenClaw-style HOOK.md/handler.ts dirs
(~/.<tool>/hooks/teamai-status-report) leaked on uninstall.

- uninstall.ts: discover + remove OpenClaw hook dirs for settings-less tools
  (mirrors the inject path); listed in the removal summary.
- Added regression test asserting the OpenClaw HOOK.md dir is removed.

Verified end-to-end: with workbuddy now settings-based, `uninstall` strips its
teamai hooks while preserving claw/enabledPlugins/sandbox; OpenClaw HOOK.md dirs
are removed (previously leaked); real-machine dry-run lists ~/.workbuddy/
settings.json. tsc OK, vitest 110 files / 1467 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 4a9a2361c55292547dd033ade98825ac3083811a)

* feat: tolerate missing /repo in HTTP init (reporting-only mode)

A clawpro-style backend may ship the status-reporter endpoints before the
team-repo /repo endpoint exists. Previously `teamai init --http <url>` hard-
failed on such endpoints (/repo → 404 or 200 HTML), so there was no first-class
way to configure endpoint+key for reporting.

- source-http.ts: fetchRepoSnapshot now distinguishes "/repo not live yet"
  (404 or non-JSON 200 body) by throwing RepoNotAvailableError; auth (401/403)
  and other errors stay hard failures.
- init.ts: on RepoNotAvailableError, init --http enters reporting-only mode —
  writes a minimal local teamai.yaml (default toolPaths), saves the http config
  (endpoint+key), injects hooks, and prints a clear notice. When /repo later
  comes online, a normal pull materializes skills with no re-init.
- pull.ts: refreshTeamRepo swallows RepoNotAvailableError (reporting-only) so
  every session doesn't error while /repo is absent.
- tests: RepoNotAvailableError classification (404 / non-JSON / 500).

Verified e2e against the real reporter-only backend: `teamai login <key>` +
`teamai init --http <url>` now succeeds (reporting-only), reporter report/sync
hit the backend (200, no offline queue), and pull no longer hard-fails.
tsc OK, vitest 110 files / 1470 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 75b03c633a731f302375c764df970a5b84dba00d)

* refactor: fold `teamai login` into `init --http --token` (remove login command)

Reduces command surface and puts endpoint + key in one place (addresses the
"why are login and endpoint separate" awkwardness). The `login` command was
only introduced in this unreleased HTTP/reporter feature set, so removing it is
not a breaking change for released users.

- index.ts: remove `login` command; add `--token <key>` to `init`.
- init.ts: initHttp persists --token via saveApiKey (0600) before resolving;
  still falls back to TEAMAI_API_TOKEN / existing apikey file.
- update "run `teamai login`" hints in source-http.ts / pull.ts / api-key.ts.

One-command setup now:
  teamai init --http <url> --token <key>

Verified e2e against the real reporter-only backend: single command saves the
key, configures the http endpoint (reporting-only), injects hooks; reporter
report/sync hit the backend (200). `login` no longer appears in --help.
tsc OK, vitest 110 files / 1470 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit e98b48f7fc63eddc755fa9ef04260ad9055c49d3)

* fix: skip git usage auto-report for HTTP consumers (no .git → noisy ERROR)

In HTTP team-repo mode the local team-repo path is not a git checkout, so
pull's Step 5 usage auto-report (reportUsageToTeam, git-based) failed every
session with `[ERROR] Auto-report skipped: fatal: not a git repository`. HTTP
consumers are read-only and have no git remote to report to, so skip the step
entirely when repo.kind === 'http'.

Verified: `teamai pull` in HTTP reporting-only mode now produces no Auto-report
error. tsc OK, vitest 110 files / 1470 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 388078d0c587c03602b93a5635e193961e84e592)

* feat: log skill-command execution in the reporter (observability)

Skill install/uninstall driven by `sync` commands previously ran silently —
on failure the reporter only ack'd `failed` with no local log, making it
impossible to see why a dispatched skill (e.g. fd-find) didn't install. Now:

- log.debug the number of commands returned by sync
- log.debug each command success
- log.error each command FAILURE with the underlying error message

No behavior change beyond logging. tsc OK, vitest 110 files / 1470 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 9bbabfcc0ef467b1addf605a29a2754632c455f4)

* chore: drop accidentally committed .teamai/domains.yaml (test artifact)

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 882698389411271ca359725b17e61907143c12ad)

* feat: surface skill download URL + server error body in reporter logs

A failed install previously logged only "download failed: HTTP 409",
giving no clue what was being fetched. Now downloadZip logs the signed
download_url before fetching and includes the server response body + URL
in the thrown error (which lands in debug.log and the ack error field).
The reporter also logs each sync command (incl. download_url) and renders
empty skill_version as "?" instead of a bare "@".

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 2a58ede7abf0e411ad61474adc0b65d21887f713)

* feat: log every reporter run + report/sync/ack outcome

Previously a successful report or sync produced no log line, so there was
no way to tell whether a SessionStart/UserPromptSubmit hook actually fired
or whether sync ran ("seems sync never triggered"). Now each invocation
logs run (agent/phase/id/endpoint) and the OK/FAILED outcome of report,
sync (with command count), and each ack.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 49ac63953c9d6e33e8f9998a2be86ea91da2e00e)

* feat: skip team-repo built-in skills in HTTP reporting-only mode

teamai-share-learnings and teamai-wiki both write to the team repo, so in
reporting-only HTTP mode (no /repo) they are non-functional. refreshTeamRepo
now reports reportingOnly, which pull threads into deployBuiltinSkills to
skip injecting them.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit d5c40475ba99078174ba553719b707e6a0d88ccc)

* fix: accept flat skill zips (SKILL.md at root), not just <slug>/SKILL.md

The clawpro/skillhub backend packages skills as a flat zip (SKILL.md +
_meta.json at the root, e.g. find-skills-skill), but installSkillZip required
a top-level <slug>/ directory and failed with "skill package missing
<slug>/SKILL.md". installSkillZip now resolves the SKILL.md location across
three layouts (nested-by-slug, flat root, nested-other-name) and installs the
contents into <skillsDir>/<slug>/.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit fbdf643c79c42613eb52fb7c383fddeb893b9b83)

* fix: use reconcileTeamHooksForConfig for HTTP init hook injection

Reconcile the issue-#1 HTTP init path onto main's unified-hooks (#65)
architecture: the old `injectHooksToAllTools(toolPaths, baseDir)` entry no
longer exists, so the HTTP consumer now injects hooks via the same
authoritative `reconcileTeamHooksForConfig` path the git init uses. Fixes the
tsc TS2304 (injectHooksToAllTools / resolveBaseDir not found) seen in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: document git-free HTTP team repo + agent status reporting

Add a "Read-only consumers (HTTP team repo, no git)" quick-start subsection
to both README.md and README.zh-CN.md, covering `init --http --token`,
reporting-only fallback, hooks-driven status reporting, and the local-only
hashing of install path / machine id.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: document the HTTP contract (endpoints, /repo schema, env knobs)

Add a collapsible "HTTP contract" block to both READMEs spelling out what a
--http backend must serve: the fixed GET /repo snapshot shape, the three
(overridable) local-agent reporter endpoints, the signed download_url +
accepted zip layouts, and the env vars that make paths/hosts/agents
configurable. Clarifies what is fixed vs configurable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(reporter): drop per-skill `source` tag and clawpro bookkeeping

The report payload no longer tags each skill `source: clawpro|local`. With
`source` gone, the entire clawpro-skills.json bookkeeping (getClawproSlugs /
recordClawproSlug / clawproRecordPath) had no remaining consumer, so it is
removed. `scanReportableSkills(skillsDir)` now just lists every skill found in
the agent's own skills dir. Tests updated accordingly.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@hsuchifeng hsuchifeng mentioned this pull request Jul 3, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 增加管理 hooks 功能

1 participant