Skip to content

feat(pi): add Pi Coding Agent integration - #692

Merged
jeff-r2026 merged 6 commits into
Tencent:mainfrom
kongdayan:feat/617-pi-agent-hooks
Sep 23, 2026
Merged

jeff-r2026 merged 6 commits into
Tencent:mainfrom
kongdayan:feat/617-pi-agent-hooks

Conversation

@kongdayan

@kongdayan kongdayan commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of #617.

This PR adds first-class Pi Coding Agent support for the Phase 1 surface requested in Tencent/teamai-cli#617:

  • register Pi as a supported agent and detect its install roots;
  • sync Pi skills, rules, and AGENTS.md-based instructions using Pi's user/project paths;
  • generate a TeamAI-managed Pi TypeScript extension that bridges built-in lifecycle events to teamai hook-dispatch;
  • add Pi-aware doctor, hooks list, uninstall, and local-agent handling;
  • update the README variants and bilingual usage guide.

Pi MCP and sub-agent formats are outside the Phase 1 surface described in #617.

Pi hook support boundary

Pi supports TeamAI's built-in lifecycle bridge only in this PR. Custom team hooks and built-in hook overrides from hooks/hooks.yaml are not applied to Pi; reconciliation prints an explicit warning when either is present. README capability tables and both usage guides document this boundary.

Full Pi support for hooks/hooks.yaml, including per-project ownership and cross-scope enable/disable behavior, requires a shared adapter design and will be handled in a follow-up PR.

Pi hook mapping

TeamAI event Pi event
session-start session_start
prompt-submit before_agent_start
pre-tool-use payload capture tool_execution_start
post-tool-use tool_execution_end
stop agent_settled

The generated extension invokes teamai hook-dispatch <event> --tool pi and forwards the active cwd and event payload. Hook failures are swallowed so a missing or failing TeamAI process does not block Pi.

Scope and extension ownership — single-copy policy

Pi auto-loads both ~/.pi/agent/extensions and <project>/.pi/extensions, and dedups by absolute path — a copy in each root would dispatch every lifecycle event twice. TeamAI writes exactly one copy, ever, in the user agent dir, and never in a project — the same single-copy policy the OMP and OpenCode adapters already use:

  • lifecycle extension: ~/.pi/agent/extensions/teamai-hooks.ts only, for every scope (user, project, and self mode);
  • Pi has no settings file for init/self mode to commit, so a fresh clone still needs one teamai init/pull run on that machine before Pi hooks are active there;
  • a TeamAI-marked project copy left by an earlier revision is removed the next time that project is reconciled;
  • extension removal (hooks remove, uninstall) is gated on the [teamai] marker, so a same-named file without it is never touched;
  • any targeted removal — teamai hooks remove, or a scoped teamai uninstall --agent pi from any scope — deletes the single global extension outright, the same single-file removal semantics as OMP. hook-dispatch has no per-project exclusion check for any tool (the cwd forwarded on STDIN only lets downstream handlers resolve project context, it doesn't gate whether Pi is excluded there), so a single shared file genuinely has no way to be scoped to one project — see "Why removal changed mid-review" below.

teamai hooks list and teamai doctor always inspect the global lifecycle extension.

Why installation changed mid-review: an earlier revision had self mode write and commit its own project copy so a fresh clone could self-bootstrap. That reintroduced the double-dispatch problem this section opens with whenever a global copy also existed on the same machine, and made scoped uninstall/hooks remove ownership ambiguous — two different review passes flagged opposite sides of that same trade-off. The OMP adapter (src/omp-hooks.ts) already made the other choice for the identical dual-root problem: never write a project copy, and accept that self mode doesn't get zero-touch bootstrap on a fresh clone. That design shipped and passed review cleanly, so Pi now follows it instead of carrying its own resolution to the same problem.

Why removal changed mid-review (a separate axis from the above): the previous revision had scoped uninstall/hooks remove preserve the global extension so other projects wouldn't lose their hooks. That preservation was well-intentioned but not actually load-bearing: since hook-dispatch never checks per-project exclusion for any tool, the "preserved" extension kept firing session-start/stop/post-tool-use hooks for the very project that had just uninstalled Pi — the scoping it was trying to provide was never enforceable in the first place. OMP's uninstall already makes the honest choice for the identical single-global-file constraint: any targeted removal deletes the one global copy outright, accepting that it affects every project on the machine rather than pretending to scope something unscopable. Pi's removal logic now matches.

Cross-cutting fixes (uninstall.ts / hooks.ts, shared by every agent)

These three landed together because they're all instances of the same class of bug: code that decided whether a different tool still "owns" a shared resource by checking whether some file merely existed, instead of checking whether that tool was actually enabled and installed.

  • retainedInstructionFiles (and the sibling "other tools still have resources" check) now require the other tool to be enabled and actually installed, via isAgentExcluded + isToolInstalledForConfig against a tool-specific probe path (skills/rules/settings — never claudemd, since that's exactly the ambiguous shared path in question). toolPaths always carries every built-in tool's default entry regardless of what a project actually selected, and Hermes/WorkBuddy default to the same project AGENTS.md as Pi, so a Pi-only project could never get uninstall --agent pi to clean its AGENTS.md block — those never-installed tools always "claimed" it.
  • Pi's uninstall discovery now also scans for teamai-agent-*.ts files, mirroring OpenCode's existing fallback. Previously it only checked for the main teamai-hooks.ts; a Pi-only server-pushed agent hook with no lifecycle extension installed was invisible to uninstall and left on disk forever.
  • The "Pi supports built-in lifecycle hooks only" warnings are now gated on Pi actually being installed (the same check reconcilePiExtension already used before writing anything), instead of firing whenever a team defines a Pi-targeted hook regardless of whether the teammate running the command uses Pi at all.

Other review fixes

  • The generated extension's child process listens for a stdin error event before writing, so a missing or short-lived teamai binary raises an async EPIPE that is swallowed instead of crashing Pi (fixed in both the lifecycle extension and HTTP-source agent hooks).
  • injectPiHooks() now applies the same [teamai]-marker ownership check removePiHooks() already had: it skips with a warning instead of overwriting a same-named file that isn't TeamAI-managed, so a user-authored extension at the conventional path is never clobbered by init/pull.
  • Extended that same per-file ownership check to the per-slug HTTP agent-hook files (teamai-agent-<slug>.ts): applyPiAgentHook()/removePiAgentHook() previously wrote/deleted these purely by path with no marker check at all. New hasPiAgentHook(slug) mirrors hasPiHooks(); uninstall.ts's discovery scan now derives each file's slug and checks its marker before scheduling removal, instead of matching by filename prefix alone.
  • Documented that Pi profile overrides (PI_CODING_AGENT_DIR / PI_CONFIG_DIR), which relocate the agent directory, are out of scope for this adapter — the same limitation the OMP adapter already documents and ships with.
  • Clarified that tool_execution_start only caches the tool's input; post-tool-use at tool_execution_end forwards that cached input as tool_input — there's no separate result/output field, matching OMP's post-tool-use payload shape (nothing in hook-dispatch's handler registry reads a result field for any adapter).
  • HTTP-source Pi agent hooks pass their configured timeout through to the generated extension.
  • PostToolUse agent hooks correlate tool input by toolCallId and delete cached input on both matching and non-matching matcher paths.
  • Windows lifecycle dispatch uses shell execution so npm .cmd shims can run.

Validation

  • npm run build
  • npx tsc --noEmit
  • npx vitest run — 270 test files / 3756 tests passed (branch rebased onto the latest main, which added ~100 unrelated tests of its own)
  • Unit regression: Pi ignores custom team hooks and built-in overrides and emits explicit warnings — and now, a companion test confirms those warnings are suppressed entirely when Pi isn't installed.
  • Unit regression: every scope (user, project, and self-mode-style bases) reconciles to the single global extension; a project run only ever cleans up a TeamAI-marked legacy copy and never creates a new one.
  • Unit regression: any removal pass (scoped project uninstall --agent pi, or hooks remove from any scope) deletes the single global extension outright, mirroring OMP; a scoped pass separately cleans up a legacy project copy first.
  • Unit regression: uninstall, removePiHooks, and now injectPiHooks are all gated on the [teamai] marker — a same-named file without it (global or a legacy project copy) is left untouched on removal, and never overwritten on injection.
  • Unit regression: per-slug agent-hook files are gated the same way — applyPiAgentHook/removePiAgentHook and uninstall's discovery scan all skip a same-named teamai-agent-<slug>.ts that lacks its own [teamai] agent hook [<slug>] marker, both on write and on removal.
  • Unit regression: targeted Pi uninstall preserves shared AGENTS.md when another tool sharing that path is genuinely installed, and now separately removes it when that other tool is only present as a toolPaths default and was never actually installed.
  • Unit regression: uninstall --agent pi at user scope now discovers and removes a server-pushed teamai-agent-*.ts file even when the main lifecycle extension is absent.
  • Unit regression: an asynchronous EPIPE from the lifecycle extension's or an HTTP agent hook's child stdin resolves cleanly instead of throwing.
  • Unit regression: matcher-scoped PostToolUse cleanup deletes cached tool input before returning on a mismatch.
  • Unit regression: HTTP-source timeout 45 seconds renders as 45000 ms; the default renders as 10000 ms.
  • Real GitLab-hosted team repository resource sync in a temporary HOME (SSH clone of git@gitlab.surbanajurong.com:ai-hub/sjg-teamai.git): user scope and project scope pulled skills/rules, injected Pi hooks, and doctor --json passed.
  • Real CLI unsupported-team-hooks run:
    node dist/index.js hooks inject with one Pi-targeted custom hook plus one built-in override emitted both skip warnings; the custom command was absent from the generated extension and the built-in stop bridge remained present.
  • Real CLI ownership run:
    injection removed a seeded TeamAI-marked legacy project copy; project teamai hooks remove deleted the global extension; after reinjection, project teamai uninstall --force --agent pi removed both the legacy project copy and the global extension (updated from an earlier record of this same run, back when scoped removal still preserved the global copy).
  • Real Pi CLI 0.85.1, single-copy dispatch + EPIPE check: with a temporary HOME containing only the global extension, and a teamai shim on PATH that exits immediately without reading stdin (the exact EPIPE trigger), pi --offline --approve --no-session --no-tools --model openai/nonexistent -p global-extension-smoke ran to completion with the expected No API key found for openai result; the shim's log recorded exactly one hook-dispatch session-start --tool pi call — confirming single dispatch (no project copy present to double-fire) and that the EPIPE fix holds against a real Pi runtime, not just the unit harness.
  • Self mode's "no project copy" behavior is enforced structurally, not just by test: initSelfRepo's commit skeleton and reconcilePiExtension no longer contain any code path that writes to <project>/.pi/extensions/ for any scope — the only project-path operation left is removePiProjectHooks (delete-only, legacy cleanup).
  • Real multi-agent CLI run, git provider, covering the cross-cutting fixes across Claude/Codex/CodeBuddy/OpenCode alongside Pi: node dist/index.js init . --self --agent claude --agent codex --agent codebuddy --agent opencode --agent pi --force against a real local git-provider remote (an actual git push/git-upload-pack round trip, not a mock) installed all five cleanly; teamai doctor passed for all five. Seeding a shared AGENTS.md with a TeamAI block (simulating Hermes/WorkBuddy's default toolPaths entry pointing at the same file, with neither actually installed) and running teamai uninstall --agent pi --force removed AGENTS.md entirely and left Claude/Codex/CodeBuddy/OpenCode's settings.json/hooks.json/skills completely untouched; a follow-up doctor showed the remaining four agents still fully healthy. This is the scenario the retention-check fix targets, exercised end-to-end through the compiled CLI.
  • Real CLI check of the OMP-aligned removal change: a fresh self-mode init with claude + pi (same local git-provider remote) created the global extension; a project-scoped teamai uninstall --agent pi --force then removed it entirely (confirmed by direct filesystem check, not just command output), while .claude/settings.json and .claude/skills were untouched.

The self-hosted GitLab resource pipeline was verified through the actual repository clone and local provider configuration. Real GitHub- or GitLab-hosted network verification (live API/auth against github.com/an actual GitLab instance) was not run because no credentials were available in this sandboxed environment — the multi-agent run above used a real local git-provider remote instead, which exercises the same generic-provider code path and the same shared uninstall/hooks logic this round's fixes changed.

@kongdayan

kongdayan commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex plz review

@jeff-r2026 jeff-r2026 self-assigned this Sep 21, 2026
@jeff-r2026
jeff-r2026 marked this pull request as ready for review September 21, 2026 03:04
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] src/hooks.ts:1285 writes a project extension in self mode without removing or suppressing an existing global ~/.pi/agent/extensions/teamai-hooks.ts. A user who previously initialized Pi in user/non-self project scope will therefore load both extensions and dispatch every lifecycle event twice—the exact duplication this code claims to prevent. Add cross-scope cleanup or runtime deduplication, plus a test where the global extension already exists.

  • [P1 blocking] src/local-agent.ts:2750 drops the resolved HTTP-hook timeout; src/pi-hooks.ts:148 instead hard-codes 15 seconds. Consequently the documented default of 10 seconds and explicit backend values such as 30/45 seconds are ignored, potentially killing valid hooks early or blocking Pi longer than configured. Pass timeout through and render it into the extension.

  • [P1 blocking] The PR description lacks the required real-CLI E2E record. “Real Pi CLI runtime verification with a temporary HOME” provides no Pi version, commands, scenarios, or observed results, and the description itself still lists the final E2E/de-duplication matrix as a draft review point. Document the executed matrix, including user scope, project scope, self mode, existing-global-extension deduplication, hooks, and uninstall.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] The PR description lacks the required reproducible end-to-end test record. “Real Pi CLI runtime verification with a temporary HOME” does not document commands, tested scenarios, agents/providers, or observed results; add a concrete test plan and real-CLI output/results before merge.
  • [P1 blocking] Project/self-mode uninstall deletes the global Pi extension as well as the project copy. discoverToolResources always adds ~/.pi/agent/extensions/teamai-hooks.ts, so uninstalling Pi from one self-mode repository disables hooks for user scope and other repositories. Only include the global file when uninstalling the scope that owns it. src/uninstall.ts:289
  • [P1 blocking] Generated PostToolUse agent hooks send event.args, but Pi’s implementation already captures arguments during tool_execution_start because the end event does not carry them. Consequently HTTP-distributed post-tool hooks receive {} as tool_input. Track arguments by toolCallId, as the main extension does. src/pi-hooks.ts:158

@kongdayan
kongdayan marked this pull request as draft September 21, 2026 03:59
@kongdayan
kongdayan marked this pull request as ready for review September 21, 2026 04:00
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/uninstall.ts:291 always adds both the global and project Pi extensions to the removal plan. In self mode, teamai uninstall --agent pi therefore deletes ~/.pi/agent/extensions/teamai-hooks.ts, breaking Pi hooks for unrelated user/project installations and contradicting the PR’s stated preservation behavior. Only include the extension belonging to the active scope.
  • [P1 blocking] src/pi-hooks.ts:63 invokes teamai.cmd directly through spawn() on Windows without shell: true or cmd.exe /c. Windows cannot directly execute .cmd shims this way, so the error is silently swallowed and every generated Pi lifecycle hook becomes a no-op.
  • [P1 blocking] src/pi-hooks.ts:158 builds PostToolUse agent-hook payloads from event.args, although the main adapter explicitly caches arguments during tool_execution_start and retrieves them by toolCallId during tool_execution_end. Consequently, server-pushed Pi PostToolUse hooks receive an empty tool_input; this generator needs equivalent start/end correlation.
  • [P1 blocking] src/types.ts:417 makes Pi share the project-root AGENTS.md with tools such as Hermes/WorkBuddy. Targeted teamai uninstall --agent pi treats that shared file as Pi-owned and removes TeamAI’s instruction blocks even when those other agents remain enabled. Shared instruction paths need reference-aware removal.

The PR description includes a substantive test plan and real-CLI/e2e records, so no testing-description finding is needed.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] src/hooks.ts:1426 — The Pi branch bypasses the normal reconciliation path and never consumes teamDefs. Consequently, hooks declared in hooks/hooks.yaml are not installed for Pi, despite the README marking hooks as supported and hooks list displaying those definitions. The generated Pi extension must include/reconcile applicable team hooks, not only built-in hook-dispatch events.
  • [P1 blocking] src/hooks.ts:1280 — In self mode, injection may retain and refresh the global Pi extension while adding a project copy, but removal deletes only the project copy. Because Pi loads both locations, teamai hooks remove and project-scoped uninstall --agent pi leave the global extension active in that project, so lifecycle hooks continue firing after removal.
  • [P2 non-blocking] src/pi-hooks.ts:155 — For matcher-scoped PostToolUse agent hooks, a non-matching tool returns before toolInputs.delete(event.toolCallId). Each non-matching invocation therefore leaves its captured arguments in the map for the lifetime of the Pi process. Ensure cleanup occurs before returning or in a finally block.

The PR description includes a detailed test plan and real-CLI/end-to-end verification record, so it satisfies the testing-documentation requirement.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Self-mode Pi clones lose automatic synchronizationsrc/hooks.ts:1297 installs only the global ~/.pi/agent/extensions/teamai-hooks.ts and removes any project copy. Since Pi has no settings file for init to commit, a fresh clone receives no session-start hook and cannot self-bootstrap. The PR’s e2e claim that .pi/extensions/teamai-hooks.ts was created also contradicts the final code and tests, so the current-head e2e record is insufficient and must be rerun or corrected.

  • [P1 blocking] Hook spawn failures can crash Pi instead of being swallowedsrc/pi-hooks.ts:72 writes to child.stdin without an error listener. If teamai is missing or exits before consuming stdin, the pipe can emit an asynchronous EPIPE outside the surrounding try/catch. Add a no-op/error handler before calling end; the generated HTTP agent-hook code at src/pi-hooks.ts:182 has the same issue.

  • [P1 blocking] Uninstall may delete an extension TeamAI does not ownsrc/uninstall.ts:298 schedules any file named teamai-hooks.ts for deletion based solely on existence. Unlike legacy cleanup, it does not verify the [teamai] hooks extension marker, so an unrelated or user-replaced extension at that path is deleted. Gate removal using hasPiHooks() or equivalent marker validation.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] injectPiHooks() unconditionally overwrites ~/.pi/agent/extensions/teamai-hooks.ts, even when the existing file lacks TeamAI’s ownership marker. This can destroy a user-authored extension during any init/pull. The removal path explicitly preserves unmarked files, so injection should apply the same ownership check or fail on collision. src/pi-hooks.ts:125
  • [P1 blocking] All Pi paths are hard-coded under ~/.pi/agent, ignoring PI_CODING_AGENT_DIR / PI_CONFIG_DIR, which the codebase already recognizes as relocating the agent directory. In such configurations, skills, rules, hooks, doctor, and uninstall operate on an unused default location. Centralize Pi root resolution and use it consistently. src/pi-hooks.ts:37
  • [P2 non-blocking] The usage guide says tool_execution_start is mapped to a hook-dispatch event, but the generated lifecycle extension only caches its payload and dispatches on tool_execution_end. Clarify that it is capture-only or implement the documented dispatch. docs/usage-guide.md:1469

The PR description includes a detailed test plan and multiple real-CLI/Pi end-to-end records, so it satisfies the testing-documentation requirement. I did not run any code or tests, as requested.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Targeted Pi uninstall never removes shared AGENTS.md blocks when Pi is the only enabled agent. retainedInstructionFiles considers every configured tool rather than enabled/non-excluded tools. Since default Hermes and WorkBuddy configurations also point to project AGENTS.md, they always cause the file to be retained. Filter this calculation using enabledAgents/disabledAgents and actual installation state. src/uninstall.ts:515

  • [P1 blocking] Pi-only HTTP hook installations can be missed by uninstall. Pi discovery checks only teamai-hooks.ts, unlike OpenCode’s explicit scan for teamai-agent-* files. If the lifecycle extension/resources are absent but server-pushed Pi hooks remain, isPlanEmpty() returns true and uninstall --agent pi leaves those extensions installed. Include managed Pi agent-hook files or manifest records in discovery. src/uninstall.ts:289

  • [P2 non-blocking] Uninstalled Pi produces misleading hook warnings. Custom-hook and override warnings are emitted before checking whether Pi exists. Because Pi is now in default toolPaths, users without Pi will receive these warnings on every reconciliation whenever their team defines such hooks. Gate warnings on Pi installation/selection. src/hooks.ts:1437

  • [P1 blocking] Required pre-PR E2E coverage is incomplete. The description includes real Pi and GitLab checks, but no real-CLI verification records for the required Claude, Codex, CodeBuddy, and OpenCode agents or the git and github providers. The repository’s PR test requirements require all of that coverage.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/hooks.ts:1300 preserves the global Pi extension during project-scoped removal, but the generated extension dispatches unconditionally and hook-dispatch never checks whether Pi is excluded in that project. Consequently, teamai uninstall --agent pi still leaves Pi firing TeamAI hooks for that project. Add runtime agent/scope gating before dispatching.
  • [P1 blocking] src/types.ts:410 makes project resources depend on <project>/.pi existing. The shared resource writers use isToolInstalledForConfig(), which checks that project directory rather than the actual global Pi installation at ~/.pi; regular project initialization does not seed tool directories. A normal globally installed Pi therefore receives the hook but silently misses project skills, rules, and AGENTS.md until the user manually creates .pi.
  • [P1 blocking] src/pi-hooks.ts:185 builds PostToolUse payloads with only the cached input and omits the completed event’s result/output. Server-pushed hooks receive the full post-tool payload on existing adapters and may inspect the result; this also contradicts the usage guide’s claim that Pi combines the cached input “with the result.” Forward the result using the expected tool_response field.

The PR description includes a test plan and real-CLI/Pi end-to-end records, so no testing-description finding is needed.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] src/uninstall.ts:304 adds the global ~/.pi/agent/extensions/teamai-hooks.ts to every Pi uninstall plan, including project-scoped uninstall --agent pi. This disables hooks for all other TeamAI projects on the machine. It also directly contradicts the PR description’s claimed project-scope preservation and e2e result; the committed test at src/__tests__/uninstall.test.ts:251 instead explicitly expects global deletion. Project uninstall should remove only the legacy project copy, reserving global-extension removal for user scope or explicit hooks remove.

The PR description otherwise contains a detailed test plan and real-CLI/e2e records, so it does not lack testing documentation.

@kongdayan

Copy link
Copy Markdown
Contributor Author

Responding to the latest finding:

[P1 blocking] src/uninstall.ts:304 adds the global ~/.pi/agent/extensions/teamai-hooks.ts to every Pi uninstall plan, including project-scoped uninstall --agent pi. This disables hooks for all other TeamAI projects on the machine. It also directly contradicts the PR description's claimed project-scope preservation and e2e result; the committed test at src/__tests__/uninstall.test.ts:251 instead explicitly expects global deletion. Project uninstall should remove only the legacy project copy, reserving global-extension removal for user scope or explicit hooks remove.

This is intentional, not a regression, and it's the direct reversal of the previous review pass on this same PR (comment above this one), which said:

[P1 blocking] src/hooks.ts:1300 preserves the global Pi extension during project-scoped removal, but the generated extension dispatches unconditionally and hook-dispatch never checks whether Pi is excluded in that project. Consequently, teamai uninstall --agent pi still leaves Pi firing TeamAI hooks for that project. Add runtime agent/scope gating before dispatching.

Those two findings ask for opposite things against the same constraint: Pi ships one global extension file, and hook-dispatch has no per-project exclusion check for any adapter — cwd on STDIN lets downstream handlers resolve project context, it does not gate whether a tool is excluded for that project. Given that, there are only two honest options:

  1. Preserve the global file on scoped removal → other projects keep working, but the "uninstalled" project's hooks keep firing anyway (exactly what the prior finding flagged).
  2. Delete the global file on any targeted removal → the "uninstalled" project's hooks actually stop, at the cost of affecting every other project on the machine (what this finding is now flagging).

There's no third option without adding real per-project runtime gating to hook-dispatch itself — new infrastructure no adapter has today, and out of scope for this PR.

We went with (2) deliberately, because it's not a novel choice: it's exactly what the OMP adapter already does for the identical single-global-file constraint. discoverToolResources's omp branch (src/uninstall.ts) and removeOmpHooks() have no scope conditional at all — any targeted uninstall --agent omp deletes the one global ~/.omp/agent/extensions/teamai-hooks.ts outright, machine-wide. That design already shipped and passed review. Pi's removal path now mirrors it exactly, rather than carrying a bespoke "preserve for other projects" guarantee that, per the prior finding, was never actually enforceable anyway.

On "contradicts the PR description's claimed project-scope preservation": the current PR description doesn't claim that as present-tense behavior — the only place "preserve" appears is a "why removal changed mid-review" paragraph explicitly describing the old, now-replaced behavior and why it was dropped. The committed test at uninstall.test.ts:251 (project-scope Pi uninstall also removes the global extension) matches the current code intentionally, not as an oversight.

Not changing this for now — happy to revisit if there's a concrete case for building real per-project dispatch gating instead.

Squash-rebased onto the latest upstream/main to resolve the PR's merge
conflict (main gained Tencent#693/Tencent#685/Tencent#694/Tencent#691/Tencent#681/Tencent#680/Tencent#666 since this branch
forked). This combines all commits from the PR into one, applied cleanly on
top of the new base — no functional changes from the previously reviewed
state.

The only real conflict was in src/__tests__/uninstall.test.ts, where diff3
split a test mid-body because of the repeated `});` boilerplate around it;
resolved by keeping both sides' new tests intact, in full.
@kongdayan
kongdayan force-pushed the feat/617-pi-agent-hooks branch from 9f971c6 to 44bd402 Compare September 21, 2026 10:25
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/uninstall.ts:301 — A project-scoped uninstall --agent pi schedules deletion of the machine-global extension. This disables Pi hooks for every other project/user scope; when another project later recreates it, hooks resume in the project that explicitly uninstalled Pi because dispatch does not honor disabledAgents. Add project-aware dispatch gating or shared ownership tracking instead of deleting the global file.
  • [P1 blocking] src/local-agent.ts:3363 — Pi HTTP agent hooks are now included in removeAllAgentHooks(), which executeRemoval() invokes for every uninstall, including uninstall --agent claude. Thus uninstalling an unrelated agent deletes all manifest-tracked Pi hooks and clears their records. Targeted uninstall must filter cleanup to the selected agent.
  • [P1 blocking] src/pi-hooks.ts:214 and src/uninstall.ts:308teamai-agent-<slug>.ts files are overwritten and deleted solely by filename, without checking the [teamai] marker. A user-authored extension using that filename pattern can be destroyed during synchronization or uninstall. Apply the same ownership check used for teamai-hooks.ts.

The PR description includes a detailed test plan and multiple real-CLI/e2e records, so its testing documentation is sufficient.

…check

applyPiAgentHook()/removePiAgentHook() wrote and deleted teamai-agent-<slug>.ts
purely by path, with no ownership check — the same class of bug already
fixed for the main teamai-hooks.ts file, but never extended to the per-slug
HTTP agent-hook files. A user-authored file at that conventional path could
be silently overwritten on sync or deleted on uninstall.

Adds hasPiAgentHook(slug), mirroring hasPiHooks: injection now skips (with a
warning) instead of overwriting a same-named file without the
`[teamai] agent hook [<slug>]` marker, and removal skips instead of
deleting one. uninstall.ts's discovery scan now derives each file's slug and
checks the same marker before scheduling it for removal, instead of
matching by filename prefix alone.
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Scoped uninstall is neither isolated nor durable. src/uninstall.ts:302 includes the global ~/.pi/agent/extensions/teamai-hooks.ts in every project-scoped Pi removal. This immediately disables Pi hooks for all other projects and user scope. Later, another enabled project’s reconciliation recreates the global file at src/hooks.ts:1312, causing hooks to fire again in the project that explicitly uninstalled Pi because dispatch has no exclusion check. Implement cwd/config-based gating or another per-project ownership mechanism rather than deleting/recreating a shared global file.

  • [P1 blocking] A skipped Pi agent-hook installation is recorded as successful. src/pi-hooks.ts:228 returns normally when a same-named user-owned extension exists, but src/local-agent.ts:2755 unconditionally writes the hook into the manifest and the command is acknowledged as successful. The server and local state therefore claim the hook is installed although the file was untouched. Return an installation result or throw, and only update the manifest/ack success after an actual installation.

  • [P1 blocking] Required provider E2E coverage is missing. The PR description explicitly says GitHub- and GitLab-provider verification was not run. The repository’s mandatory pre-PR matrix requires real-CLI validation for git, gitlab, and github; cloning a GitLab-hosted repository through the generic git provider does not exercise the GitLab provider implementation.

@jeff-r2026

Copy link
Copy Markdown
Collaborator

This PR currently has merge conflicts with main. Please rebase and resolve them so review can continue.

kongdayan and others added 4 commits September 23, 2026 12:06
…Pi agent hook

applyPiAgentHook warned and returned normally when the requested event has no
Pi equivalent or a same-named extension file exists without the TeamAI
marker. The caller in local-agent.ts wrote the manifest entry and acked
success regardless, so the server and local state believed the hook was
installed even though the file was never touched. Throw in both cases so the
existing install_hook_rule error path acks failure instead.

Also document the known limitation (shared with the OMP adapter) that a
scoped Pi uninstall is not durable across multiple projects on the same
machine, since the extension is one machine-wide file and hook dispatch has
no per-project exclusion check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Conflicts:
- README.* (5 languages): upstream relocated the agent-capability table
  and "Git providers" line out of the READMEs into the new
  docs/product-overview.md; took upstream's side and added the Pi row
  (with its footnote) to product-overview.md / .zh-CN.md instead, since
  the "Learn More" link to Product Overview was already in place.
- src/hooks-cmd.ts (hooksList): combined our tool === 'pi' branch with
  upstream's new isAgentExcluded() skip — excluded agents are now
  filtered before the Pi-specific branch runs.
- src/hooks-cmd.ts (hooksRemove): combined upstream's fix (resolve
  scopedToolPaths at the hooks' effective scope, not the raw
  unscoped toolPaths) with our scope/installedBaseDir options, which
  Pi's self-mode reconciliation needs.

Also registered `pi` in ADAPTER_BUILTIN_HOOKS (src/builtin-hooks.ts),
mirroring OMP's four keys — a gap the merge surfaced: upstream's new
`teamai hooks list` "Built-in hooks (A)" section (Tencent#717) made
HookListRow.builtinDefs required, and the Pi row (added before that
field existed) had no entry, which manifested as a type error rather
than a textual conflict.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…idge)

upstream/main added a DeepSeek Harness (dsh) adapter alongside Pi in the
same reconcile/discovery/removal code paths, causing conflicts in
src/hooks.ts and src/uninstall.ts. Kept both adapters' blocks side by
side everywhere (reconcileHooksToAllTools's tool switch, ToolResources/
RemovalPlan fields, discovery, isPlanEmpty, plan printing, execution).

One spot needed care rather than a mechanical union: the removal-plan
merge loop's claudeMdFiles handling. Our branch already carries a fix
(from an earlier review round) that skips a shared instruction file
still owned by another active, enabled tool — upstream's version of
that line predates the fix and is a plain push. Kept our
retainedInstructionFiles-aware version, not upstream's, to avoid
reintroducing that bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
upstream/main's Tencent#732 fix (probe Copilot hooks via hookToolPaths instead
of toolPaths) landed right where our Pi hook-check branch is: both add
code before the shared hookPath resolution in buildHookChecks. Pi's
branch already returns via `continue` before reaching that point, so
kept both blocks in sequence with no logic overlap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/uninstall.ts:321 makes every project-scoped uninstall --agent pi delete the machine-global Pi extension. This disables hooks for unrelated projects, while a later pull elsewhere recreates it and re-enables hooks in the project that explicitly uninstalled Pi. Documenting this non-durable behavior does not make targeted uninstall safe.
  • [P1 blocking] src/local-agent.ts:3375 adds Pi to removeAllAgentHooks(), which src/uninstall.ts:891 invokes for every uninstall. Therefore uninstall --agent claude removes all manifest-tracked Pi hooks and clears their records. Targeted uninstall must filter agent-hook cleanup to the selected tool.
  • [P1 blocking] src/pi-hooks.ts:5 never creates a project extension, while Pi is added to the self-mode agent choices. A fresh clone on another machine therefore has no committed hook capable of triggering self-bootstrap, contradicting the documented “Clone = initialized” behavior.
  • [P1 blocking] src/pi-hooks.ts:43 hard-codes ~/.pi/agent, and src/types.ts:451 similarly hard-codes resource paths, ignoring PI_CODING_AGENT_DIR / PI_CONFIG_DIR. Users with relocated Pi profiles receive hooks, skills, rules, doctor checks, and uninstall operations in an unused directory.
  • [P1 blocking] src/pi-hooks.ts:190 forwards only cached input for server-pushed PostToolUse hooks and drops the completed tool event’s result/output. Such hooks receive a materially incomplete payload compared with normal post-tool hooks and cannot inspect command results.
  • [P1 blocking] The PR description explicitly says real github- and gitlab-provider verification was not run. The repository requires real-CLI coverage for git, gitlab, and github; cloning a GitLab-hosted repository through the generic git provider does not test the GitLab provider.

Earlier marker-ownership, timeout propagation, argument correlation, matcher cleanup, EPIPE handling, warning gating, and shared-AGENTS.md findings are resolved in the current diff. I did not run or build PR code.

@jeff-r2026
jeff-r2026 merged commit 9d7e50b into Tencent:main Sep 23, 2026
11 checks passed
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.

2 participants