feat: unified hooks management — team-declared hooks + built-in hooks as data (#19) - #65
Merged
Merged
Conversation
…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>
4 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #19.
What
Lowers the CLI's built-in operational hooks to a single
HookDefdata model and drives both them (A) and team-declared hooks from a newhooks/hooks.yaml(B) through onereconcileHooks()engine, delivered to every AI tool (Claude Code, CodeBuddy, Cursor, …) byteamai pull.Authoring stays zero-new-command for admins: edit
hooks/hooks.yaml+git push, same mental model asenv.yaml.How it's structured
src/builtin-hooks.ts— built-in hooks asHookDef[]; 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.jsonmanifest. 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 (teamActivegate). Kept the existinggetHookStatus()from feat(hooks): add hooks list command #62 and reimplemented it on the new model.src/resources/hooks.ts—HooksHandler+ ZodHooksYamlSchema+ parse/resolve; registered via static import (tree-shaking-safe).pull()—reconcileHooksAllScopes()runs outside the rev fast-path, so new built-in hooks andhooks.yamledits self-heal on every session start.teamai hooks listnow shows per-tool install status and the effective A+B audit;statuscounts team hooks;uninstallstrips team hooks via the manifest.builtin:overrides (disabled/ whitelistedtimeout); empty = no change.TEAMAI_HOOKS_DISABLEDkill-switch, command transparency on apply,sharing.hooks.autoApplyconsent gate (pull only hints; explicithooks injectapplies),sharing.hooks.requireTeamScriptswhitelist.Backward compatibility
sharing.hooksis optional in the inferred config type, so existingTeamaiConfigliterals/fixtures stay valid.[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-CLIhooks-team-e2e(spawns theteamaibinary through inject → list → edit → kill-switch → remove).🤖 Generated with Claude Code