Skip to content

feat: agent-detector process-tree detection + auto-wire hooks - #46

Closed
getappz wants to merge 8 commits into
feature/ponytail-l1-integrationfrom
feature/agent-detector-ponytail
Closed

feat: agent-detector process-tree detection + auto-wire hooks#46
getappz wants to merge 8 commits into
feature/ponytail-l1-integrationfrom
feature/agent-detector-ponytail

Conversation

@getappz

@getappz getappz commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces env-var-only agent detection with process-tree walking (agent-detector style). Auto-wires ponytail hooks via \init --agent X. Detects existing npm ponytail plugin and offers to uninstall.

Closes #43

Changes

  • \src/ponytail/detect.rs\ — 3-tier detection: process tree → standard env → tool env
  • ~60 known agent definitions (from dtcxzyw/agent-detector)
  • \sysinfo\ optional dep behind \process-tree\ feature flag
  • \init::wire_ponytail_hooks()\ — auto-wires ponytail hooks for Claude/Cursor/OpenCode
  • \init --agent X -y\ — auto-accepts prompts, runs uninstall
  • Existing plugin detection + confirmation prompt

Summary by CodeRabbit

  • New Features

    • Added automatic agent detection, including support for environment-based and process-based identification.
    • Introduced optional process-tree support for broader detection coverage.
    • Enhanced initialization with guided migration prompts and automatic configuration wiring for supported tools.
    • Added a --yes / -y option to skip prompts during init.
  • Bug Fixes

    • Improved platform detection handling and made hook output generation more consistent for Claude-style setups.

getappz added 6 commits July 7, 2026 18:45
Vendor agent-detector (MIT, dtcxzyw) detection logic into
src/ponytail/detect.rs. Three-tier detection:
1. Parent process tree walk (with process-tree feature)
2. AI_AGENT / AGENT standard env vars
3. Tool-specific env vars for ~60 known agents

Platform detection now uses process-tree first, falling back
to env vars. Maps agent name to output format (Claude/Codex/
Copilot/Fallback).

sysinfo optional dep behind 'process-tree' feature flag.
Detects agents even when launched via plain shell (no env vars).
Ponytail hooks now auto-wired by 'agentflare init --agent X'.
Skills already embedded via include_str! — no download needed.
wire_ponytail_hooks() adds ponytail SessionStart/SubagentStart
hooks + statusline to Claude Code, Cursor, and OpenCode settings.
Init now checks agent settings for existing ponytail npm plugin
hooks before auto-wiring agentflare's built-in ponytail hooks.
If detected, prints warning + uninstall instructions, skips wiring.
User re-runs init after disabling the old plugin.
agentflare init --agent X -y skips all confirmation prompts and
attempts to uninstall the existing ponytail npm plugin before
auto-wiring agentflare's built-in ponytail hooks.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e6f24b8-eecb-48b6-b0f0-a235d1f1273f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/agent-detector-ponytail

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/ponytail/detect.rs (1)

111-144: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Full-system process refresh on every detection call.

find_in_parent_tree allocates a new System and refreshes every process on the machine (ProcessesToUpdate::All, default ProcessRefreshKind) just to walk a handful of ancestor PIDs and read their names. Per the sysinfo docs, "Refreshing all processes and their tasks can be quite expensive", and here it's only needed for name()/parent() of a small ancestor chain. Consider using refresh_processes_specifics with a minimal ProcessRefreshKind (no CPU/memory/disk/tasks) to cut overhead, since detection presumably runs on every CLI invocation.

♻️ Proposed fix
-    let mut system = System::new();
-    system.refresh_processes(ProcessesToUpdate::All, true);
+    let mut system = System::new();
+    system.refresh_processes_specifics(
+        ProcessesToUpdate::All,
+        true,
+        ProcessRefreshKind::nothing(),
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ponytail/detect.rs` around lines 111 - 144, `find_in_parent_tree` is
doing a full `System::refresh_processes(ProcessesToUpdate::All, true)` on every
call even though it only needs ancestor `name()` and `parent()` values. Update
`find_in_parent_tree` to use a narrower sysinfo refresh path, such as
`refresh_processes_specifics`, with a minimal `ProcessRefreshKind` that avoids
CPU/memory/disk/tasks, while keeping the `current_pid`, `proc.name()`, and
`proc.parent()` lookup logic unchanged. This should preserve the `AGENTS`
matching and `is_cowork_override()` behavior but reduce detection overhead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/init.rs`:
- Around line 41-72: The confirmation flow in confirm/reconfigure logic has two
edge cases: in non-interactive stdin or EOF cases, empty input from read_line
should not be treated as approval when yes is false, and the opencode path
should not continue after a failed plugin uninstall. Update the confirmation
handling in the relevant init flow and the opencode uninstall branch so that
non-TTY/EOF input defaults to decline unless -y is set, and return false
immediately when opencode plugin uninstall fails instead of proceeding to wire
hooks.
- Around line 307-315: The already_wired guard in the ponytail wiring flow is
too broad because it treats any SessionStart containing "ponytail" as already
wired, which wrongly matches the npm plugin and skips agentflare wiring. Update
the check in wire_ponytail_claude_code to distinguish agentflare from the npm
ponytail plugin by aligning it with the has_existing_ponytail_claude logic, i.e.
only consider it already wired when the agentflare-specific hooks are present
and the session does not just contain the npm ponytail marker.
- Around line 345-379: Add an idempotency guard to the cursor wiring path in the
same way as wire_ponytail_claude_code and wire_ponytail_opencode so repeated
init --agent cursor runs do not append duplicate hooks. Update the cursor hook
setup logic in init.rs to detect when the ponytail commands are already present
in .cursor/hooks.json before writing sessionStart and beforeSubmitPrompt
entries, and skip re-adding them if they exist.

In `@src/ponytail/detect.rs`:
- Line 28: The Antigravity agent mapping in detect logic uses the wrong process
name, so update the AgentEntry for the "antigravity" symbol to match the actual
CLI binary. Replace the current process_names entry with the correct binary name
used by Antigravity, and keep the env_vars mapping unchanged so process
detection in detect.rs identifies the right agent instead of the unrelated one.

---

Nitpick comments:
In `@src/ponytail/detect.rs`:
- Around line 111-144: `find_in_parent_tree` is doing a full
`System::refresh_processes(ProcessesToUpdate::All, true)` on every call even
though it only needs ancestor `name()` and `parent()` values. Update
`find_in_parent_tree` to use a narrower sysinfo refresh path, such as
`refresh_processes_specifics`, with a minimal `ProcessRefreshKind` that avoids
CPU/memory/disk/tasks, while keeping the `current_pid`, `proc.name()`, and
`proc.parent()` lookup logic unchanged. This should preserve the `AGENTS`
matching and `is_cowork_override()` behavior but reduce detection overhead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2bf398f0-66b8-4ce5-8768-7a80d3dbea82

📥 Commits

Reviewing files that changed from the base of the PR and between 5c325a1 and a619a2a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • Cargo.toml
  • src/init.rs
  • src/main.rs
  • src/ponytail/detect.rs
  • src/ponytail/mod.rs
  • src/ponytail/platform.rs

Comment thread src/init.rs
Comment on lines +41 to +72
if !yes {
print!(" Uninstall ponytail plugin? [Y/n] ");
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
match input.trim().to_lowercase().as_str() {
"y" | "yes" | "" => {}
_ => {
println!(" Skipped. Re-run: agentflare init --agent {agent}");
return false;
}
}
}

match agent {
"opencode" => {
println!(" Running: opencode plugin uninstall ponytail@ponytail");
match std::process::Command::new("opencode")
.args(["plugin", "uninstall", "ponytail@ponytail"])
.output()
{
Ok(out) => {
if out.status.success() {
println!(" ok ponytail plugin uninstalled");
} else {
let stderr = String::from_utf8_lossy(&out.stderr);
println!(" fail {stderr}");
}
}
Err(e) => println!(" fail could not run opencode: {e}"),
}
true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Two edge cases in the confirmation flow.

  • Non-interactive stdin (EOF/pipe) with yes == false: read_line returns Ok(0) leaving input empty, which matches the "" arm and is treated as "yes" — silently proceeding to uninstall/rewire in CI or piped contexts. Consider treating a non-TTY/EOF as a decline unless -y is given.
  • opencode branch: if opencode plugin uninstall fails (Line 64-69), the function still returns true, so hooks are wired despite the conflicting plugin remaining. Consider returning false on uninstall failure.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/init.rs` around lines 41 - 72, The confirmation flow in
confirm/reconfigure logic has two edge cases: in non-interactive stdin or EOF
cases, empty input from read_line should not be treated as approval when yes is
false, and the opencode path should not continue after a failed plugin
uninstall. Update the confirmation handling in the relevant init flow and the
opencode uninstall branch so that non-TTY/EOF input defaults to decline unless
-y is set, and return false immediately when opencode plugin uninstall fails
instead of proceeding to wire hooks.

Comment thread src/init.rs
Comment on lines +307 to +315
let already_wired = settings
.get("hooks")
.and_then(|h| h.get("SessionStart"))
.map(|v| v.to_string().contains("ponytail"))
.unwrap_or(false);
if already_wired {
println!(" skip ponytail hooks already wired in ~/.claude/settings.json");
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

already_wired conflates the npm ponytail plugin with agentflare, skipping the wiring it was asked to do.

This check only looks for the substring "ponytail" in SessionStart, but has_existing_ponytail_claude() (Line 86-94) uses ponytail && !agentflare to detect the npm plugin. In the migration flow, confirm_ponytail_migration first detects the npm plugin, the user confirms, and Claude uninstall is manual (deferred). When wire_ponytail_claude_code then runs, the npm plugin's "ponytail" string is still present, so already_wired is true and agentflare's hooks are never wired. Align this guard with the !contains("agentflare") logic so agentflare's own hooks are distinguished from the npm plugin's.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/init.rs` around lines 307 - 315, The already_wired guard in the ponytail
wiring flow is too broad because it treats any SessionStart containing
"ponytail" as already wired, which wrongly matches the npm plugin and skips
agentflare wiring. Update the check in wire_ponytail_claude_code to distinguish
agentflare from the npm ponytail plugin by aligning it with the
has_existing_ponytail_claude logic, i.e. only consider it already wired when the
agentflare-specific hooks are present and the session does not just contain the
npm ponytail marker.

Comment thread src/init.rs
Comment on lines +345 to +379
fn wire_ponytail_cursor() {
let path = cwd().join(".cursor").join("hooks.json");
let bin = agentflare_binary();

let mut content: Value = fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| json!({ "version": 1, "hooks": {} }));
if !content.is_object() {
content = json!({ "version": 1, "hooks": {} });
}

let hooks = content.as_object_mut().unwrap()
.entry("hooks").or_insert_with(|| json!({}));
let hooks_obj = hooks.as_object_mut().unwrap();

hooks_obj.entry("sessionStart").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({
"command": format!("\"{bin}\" ponytail hook session-start"),
"type": "command",
"timeout": 30
}));
hooks_obj.entry("beforeSubmitPrompt").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({
"command": format!("\"{bin}\" ponytail hook prompt-submit"),
"type": "command",
"timeout": 10
}));

if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
match fs::write(&path, serde_json::to_string_pretty(&content).unwrap() + "\n") {
Ok(_) => println!(" ok ponytail hooks wired in .cursor/hooks.json"),
Err(e) => println!(" fail writing .cursor/hooks.json: {e}"),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing idempotency check — cursor hooks get duplicated on every re-run.

Unlike wire_ponytail_claude_code and wire_ponytail_opencode, this function has no already_wired guard. Re-running init --agent cursor appends another sessionStart / beforeSubmitPrompt entry each time, accumulating duplicate ponytail commands in .cursor/hooks.json.

Proposed guard
     let hooks = content.as_object_mut().unwrap()
         .entry("hooks").or_insert_with(|| json!({}));
     let hooks_obj = hooks.as_object_mut().unwrap();
 
+    let already_wired = hooks_obj
+        .get("sessionStart")
+        .map(|v| v.to_string().contains("ponytail"))
+        .unwrap_or(false);
+    if already_wired {
+        println!("  skip  ponytail hooks already wired in .cursor/hooks.json");
+        return;
+    }
+
     hooks_obj.entry("sessionStart").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn wire_ponytail_cursor() {
let path = cwd().join(".cursor").join("hooks.json");
let bin = agentflare_binary();
let mut content: Value = fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| json!({ "version": 1, "hooks": {} }));
if !content.is_object() {
content = json!({ "version": 1, "hooks": {} });
}
let hooks = content.as_object_mut().unwrap()
.entry("hooks").or_insert_with(|| json!({}));
let hooks_obj = hooks.as_object_mut().unwrap();
hooks_obj.entry("sessionStart").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({
"command": format!("\"{bin}\" ponytail hook session-start"),
"type": "command",
"timeout": 30
}));
hooks_obj.entry("beforeSubmitPrompt").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({
"command": format!("\"{bin}\" ponytail hook prompt-submit"),
"type": "command",
"timeout": 10
}));
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
match fs::write(&path, serde_json::to_string_pretty(&content).unwrap() + "\n") {
Ok(_) => println!(" ok ponytail hooks wired in .cursor/hooks.json"),
Err(e) => println!(" fail writing .cursor/hooks.json: {e}"),
}
}
let already_wired = hooks_obj
.get("sessionStart")
.map(|v| v.to_string().contains("ponytail"))
.unwrap_or(false);
if already_wired {
println!(" skip ponytail hooks already wired in .cursor/hooks.json");
return;
}
hooks_obj.entry("sessionStart").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/init.rs` around lines 345 - 379, Add an idempotency guard to the cursor
wiring path in the same way as wire_ponytail_claude_code and
wire_ponytail_opencode so repeated init --agent cursor runs do not append
duplicate hooks. Update the cursor hook setup logic in init.rs to detect when
the ponytail commands are already present in .cursor/hooks.json before writing
sessionStart and beforeSubmitPrompt entries, and skip re-adding them if they
exist.

Comment thread src/ponytail/detect.rs
AgentEntry { name: "cursor-cli", process_names: &[], env_vars: &["CURSOR_AGENT", "CURSOR_EXTENSION_HOST_ROLE"] },
AgentEntry { name: "gemini", process_names: &["gemini"], env_vars: &["GEMINI_CLI"] },
AgentEntry { name: "codex", process_names: &["codex"], env_vars: &["CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID"] },
AgentEntry { name: "antigravity", process_names: &["amp"], env_vars: &["ANTIGRAVITY_AGENT"] },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## File context\n'
git ls-files src/ponytail/detect.rs
wc -l src/ponytail/detect.rs
cat -n src/ponytail/detect.rs | sed -n '1,120p'

printf '\n## Search for antigravity/amp references\n'
rg -n "antigravity|ANTIGRAVITY_AGENT|process_names: &\\[\"amp\"\\]|\\bamp\\b|\\bagy\\b" -S .

Repository: getappz/agentflare

Length of output: 2225


🌐 Web query:

Google Antigravity CLI binary name agy amp process name

💡 Result:

The Google Antigravity CLI binary is named agy [1][2][3]. On Windows systems, this binary typically appears as agy.exe, while on Unix-like systems (Linux/macOS), it is named agy [4]. Regarding your mention of amp, this refers to a separate, unrelated technology known as Accelerated Mobile Pages (AMP) [5]. In the context of AMP development, there is a development tool binary also named amp (often used to start local dev servers), which is unrelated to the Google Antigravity platform [6][5]. Do not confuse these; the Antigravity CLI (agy) is Google's terminal-based coding agent that replaced the deprecated Gemini CLI [3][7], whereas AMP is an open-source web framework [5].

Citations:


Use agy for Antigravity, not amp. The Antigravity CLI binary is agy; amp is unrelated, so this entry will miss the real agent and can misclassify the wrong process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ponytail/detect.rs` at line 28, The Antigravity agent mapping in detect
logic uses the wrong process name, so update the AgentEntry for the
"antigravity" symbol to match the actual CLI binary. Replace the current
process_names entry with the correct binary name used by Antigravity, and keep
the env_vars mapping unchanged so process detection in detect.rs identifies the
right agent instead of the unrelated one.

getappz added 2 commits July 7, 2026 20:41
wire_ponytail_opencode() was writing invalid keys (hooks,
statusLine) to opencode.jsonc. OpenCode hooks are handled
by the ponytail npm plugin — config doesn't support them.
@getappz
getappz deleted the branch feature/ponytail-l1-integration July 7, 2026 15:23
@getappz getappz closed this Jul 7, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 7, 2026
@getappz
getappz deleted the feature/agent-detector-ponytail branch July 7, 2026 15:27
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant