Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ result-*
.direnv
.envrc.local
.gitban/

# Python bytecode from the adapter/test imports
__pycache__/
*.pyc
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## Unreleased

### Fixed
- **Antigravity fired "Job's done!" then "Right away!" on every tool call.** The Python watcher decided a turn had ended from file mtime alone, so any tool that ran longer than the idle threshold (25s, or 8s in some local setups) was read as completion, and the write that came back when the tool finished was read as a new user prompt. It now parses `brain/<guid>/.system_generated/logs/transcript.jsonl` and takes the turn boundary from Antigravity's own records: `USER_INPUT` with `source: USER_EXPLICIT` is a prompt, a `PLANNER_RESPONSE` carrying `tool_calls` is work in flight, and one carrying prose and no tool calls is the final answer. Idle timeouts are switched off entirely for these sessions, so tool duration no longer matters. Replaying the 33 transcripts on a real machine turns 124 user prompts into exactly 124 acknowledgements. Sessions with no transcript (legacy `conversations/*.pb` and `*.db`) keep the mtime fallback, now at 45s.
- **Every Antigravity event fired twice.** Antigravity writes `transcript.jsonl` and a byte-identical `transcript_full.jsonl`, and the watcher matched `transcript*`. Only `transcript.jsonl` is parsed now. This was invisible under mtime detection and would have doubled every sound once records drove events.
- **Gemini CLI played the completion sound after every successful tool call.** `adapters/gemini.sh` and `gemini.ps1` mapped `AfterTool` with `exit_code == 0` to `Stop`. A tool succeeding is the middle of a turn; `AfterAgent` already reports completion. `AfterTool` now sounds only on failure.
- **`install.sh` never shipped the Antigravity Python watcher.** The one-liner install fetched `antigravity.sh` but not `antigravity-py.sh` or `antigravity-watcher.py`, and the local install copied `adapters/*.sh` only, so `antigravity-py.sh` exited on its own preflight check. Both paths now install all three, and `install.ps1` ships the watcher too.

### Added
- **Antigravity reaches parity with Claude Code on all five sounds.** Beyond acknowledge and complete: a shell command that exits non-zero plays `task.error`, the same as a failing Bash tool in Claude Code. Older builds record the code in an `exit_code` field and newer ones only state it in the record's prose, so both are read. `ERROR_MESSAGE` records also sound `task.error`, deduplicated so a rate-limited retry loop sounds once rather than nine times. `user.spam` needs nothing new: peon.sh derives it from prompt timing.
- **Antigravity announces when it is waiting for permission.** The watcher reads the CLI log for `Surfacing tool confirmation` and emits `PermissionRequest` (`input.required`), cleared on `Responding to tool confirmation`. Three things this needed on a real machine: `cli.log` is a symlink into `log/cli-<timestamp>.log` retargeted at every launch, so the symlink's own directory never sees a write; Antigravity holds the log open and flushes in batches, so filesystem events arrived minutes late or not at all and the log is polled once a second instead; and a flushed batch usually contains prompts already answered, so only a batch that ends still waiting sounds, exactly once. Offsets are keyed by resolved path, since the same file otherwise arrives under two names. The conversation to attribute a prompt to is seeded from the most recently modified transcript at startup, so a watcher started mid-session is not silent until the next thing you type.
- **Antigravity notifications name the workspace, and no longer claim to be Claude Code.** The watcher runs as a LaunchAgent, so its working directory is `/`, and it passed that as every event's `cwd`. peon.sh cannot derive a project name from `/` and falls back to labelling anything non-Codex `claude`, so every Antigravity banner was titled that. The workspace now comes from the conversation's own `conversations/<guid>.db`, which stores it as a `file://` URI in `trajectory_metadata_blob`, with `cache/last_conversations.json` as a fallback for a conversation whose database is not written yet. Reading the database rather than only the cache is what makes a resumed session resolve, since the cache holds only the newest conversation per workspace: across 46 real conversations it takes 37 of them, and the 9 it does not are `default-cli-project` sessions started outside any workspace, which record no path at all.
- **`peon status --verbose` lists Antigravity.** Antigravity has no settings file to register hooks in, so "installed" means the watcher daemon is set up (LaunchAgent or pidfile).

### Notes
- `resource.limit` stays unreachable from Antigravity. peon.sh only routes it from `PreCompact`, and Antigravity exposes no context-compaction event; rate limits are reported as `task.error` instead. Subagent start/stop is also not wired: peon.sh tracks subagents by session-id relationships the watcher does not model, and `INVOKE_SUBAGENT` appears once across 33 transcripts, which is not enough to verify the semantics.

## v2.37.0 (2026-08-26)

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ peon-ping works with any agentic IDE that supports hooks. Adapters translate IDE
| **Kilo CLI** | Adapter | `bash adapters/kilo.sh` / `powershell adapters/kilo.ps1` ([setup](#kilo-cli-setup)) |
| **Kiro** | Adapter | Add hook entries pointing to `adapters/kiro.sh` (or `.ps1`) ([setup](#kiro-setup)) |
| **Windsurf** | Adapter | Add hook entries pointing to `adapters/windsurf.sh` (or `.ps1`) ([setup](#windsurf-setup)) |
| **Google Antigravity** | Adapter | `bash adapters/antigravity.sh` / `powershell adapters/antigravity.ps1`. For headless / macOS LaunchAgent use, also see `bash adapters/antigravity-py.sh --install` (Python `watchdog` watcher with 25s idle threshold; requires `pip3 install watchdog`). The Python watcher supports legacy `conversations/*.pb` state plus newer `antigravity-cli` / `antigravity-ide` `conversations/*.db` and `brain/**/transcript*.jsonl` layouts. |
| **Google Antigravity** | Adapter | `bash adapters/antigravity.sh` / `powershell adapters/antigravity.ps1`. For headless / macOS LaunchAgent use, also see `bash adapters/antigravity-py.sh --install` (Python `watchdog` watcher; requires `pip3 install watchdog`). The Python watcher parses `brain/**/transcript.jsonl` for turn boundaries and `cli.log` for permission prompts, giving the same five sounds as Claude Code. Legacy `conversations/*.pb` and `*.db` sessions without a transcript fall back to a 45s idle timer (`ANTIGRAVITY_IDLE_SECONDS`). |
| **Kimi Code** | Adapter | `bash adapters/kimi.sh --install` / `powershell adapters/kimi.ps1 -Install` ([setup](#kimi-code-setup)) |
| **OpenClaw** | Adapter | Call `adapters/openclaw.sh <event>` (or `openclaw.ps1`) from your OpenClaw skill |
| **Rovo Dev CLI** | Adapter | Auto-registered by `install.sh` if `~/.rovodev` exists, or add hooks to `~/.rovodev/config.yml` manually ([setup](#rovo-dev-cli-setup)) |
Expand Down Expand Up @@ -1047,7 +1047,7 @@ A shell adapter for **Gemini CLI** with full [CESP v1.0](https://github.com/Peon

- `SessionStart` (startup) → Greeting sound (*"Ready to work?"*, *"Yes?"*)
- `AfterAgent` → Task completion sound (*"Work, work."*, *"Job's done!"*)
- `AfterTool` → Success = Task completion sound, Failure = Error sound (*"I can't do that."*)
- `AfterTool` → Success = Silent; `tool_response.error` = Error sound (*"I can't do that."*). Legacy top-level `exit_code`/`stderr` input is also supported.
- `Notification` → System notification

### Windsurf setup
Expand Down
4 changes: 2 additions & 2 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ peon-ping はフックをサポートする任意のエージェント型 IDE
| **Kilo CLI** | アダプター | `bash adapters/kilo.sh` / `powershell adapters/kilo.ps1`([セットアップ](#kilo-cli-セットアップ)) |
| **Kiro** | アダプター | `adapters/kiro.sh`(または `.ps1`)を指すフックエントリを追加([セットアップ](#kiro-セットアップ)) |
| **Windsurf** | アダプター | `adapters/windsurf.sh`(または `.ps1`)を指すフックエントリを追加([セットアップ](#windsurf-セットアップ)) |
| **Google Antigravity** | アダプター | `bash adapters/antigravity.sh` / `powershell adapters/antigravity.ps1`。ヘッドレス / macOS LaunchAgent 用途には `bash adapters/antigravity-py.sh --install`(Python `watchdog` ベース、25 秒のアイドル閾値、`pip3 install watchdog` が必要)もあります。Python ウォッチャーは旧来の `conversations/*.pb` 状態に加え、新しい `antigravity-cli` / `antigravity-ide` の `conversations/*.db` と `brain/**/transcript*.jsonl` レイアウトにも対応しています。 |
| **Google Antigravity** | アダプター | `bash adapters/antigravity.sh` / `powershell adapters/antigravity.ps1`。ヘッドレス / macOS LaunchAgent 用途には `bash adapters/antigravity-py.sh --install`(Python `watchdog` ベース、`pip3 install watchdog` が必要)もあります。Python ウォッチャーは `brain/**/transcript.jsonl` からターンの境界を、`cli.log` から許可要求を読み取り、Claude Code と同じ 5 種類の通知音を提供します。transcript がない旧来の `conversations/*.pb` と `*.db` セッションには 45 秒のアイドルタイマー(`ANTIGRAVITY_IDLE_SECONDS`)を使用します。 |
| **Kimi Code** | アダプター | `bash adapters/kimi.sh --install` / `powershell adapters/kimi.ps1 -Install`([セットアップ](#kimi-code-セットアップ)) |
| **OpenClaw** | アダプター | OpenClaw スキルから `adapters/openclaw.sh <event>`(または `openclaw.ps1`)を呼び出し |
| **Rovo Dev CLI** | アダプター | `~/.rovodev` が存在する場合 `install.sh` が自動登録、または `~/.rovodev/config.yml` にフックを手動追加([セットアップ](#rovo-dev-cli-セットアップ)) |
Expand Down Expand Up @@ -903,7 +903,7 @@ curl -fsSL https://raw.githubusercontent.com/PeonPing/peon-ping/main/adapters/ki

- `SessionStart`(startup)→ 挨拶サウンド(*"Ready to work?"*、*"Yes?"*)
- `AfterAgent` → タスク完了サウンド(*"Work, work."*、*"Job's done!"*)
- `AfterTool` → 成功 = タスク完了サウンド、失敗 = エラーサウンド(*"I can't do that."*)
- `AfterTool` → 成功 = 無音、`tool_response.error` = エラーサウンド(*"I can't do that."*)。従来のトップレベル `exit_code`/`stderr` 入力にも対応。
- `Notification` → システム通知

### Windsurf セットアップ
Expand Down
4 changes: 2 additions & 2 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ peon-ping 适用于任何支持钩子的代理式 IDE。适配器将 IDE 特定
| **Kilo CLI** | 适配器 | `bash adapters/kilo.sh` / `powershell adapters/kilo.ps1`([设置](#kilo-cli-设置)) |
| **Kiro** | 适配器 | 添加指向 `adapters/kiro.sh`(或 `.ps1`)的钩子条目([设置](#kiro-设置)) |
| **Windsurf** | 适配器 | 添加指向 `adapters/windsurf.sh`(或 `.ps1`)的钩子条目([设置](#windsurf-设置)) |
| **Google Antigravity** | 适配器 | `bash adapters/antigravity.sh` / `powershell adapters/antigravity.ps1`。无头 / macOS LaunchAgent 场景可用 `bash adapters/antigravity-py.sh --install`(基于 Python `watchdog` 的 25 秒空闲阈值守护进程,需要 `pip3 install watchdog`)。Python 守护进程支持旧版 `conversations/*.pb` 状态,以及较新的 `antigravity-cli` / `antigravity-ide` 的 `conversations/*.db` 和 `brain/**/transcript*.jsonl` 布局。 |
| **Google Antigravity** | 适配器 | `bash adapters/antigravity.sh` / `powershell adapters/antigravity.ps1`。无头 / macOS LaunchAgent 场景可用 `bash adapters/antigravity-py.sh --install`(基于 Python `watchdog` 的守护进程,需要 `pip3 install watchdog`)。该守护进程解析 `brain/**/transcript.jsonl` 判断回合边界,并从 `cli.log` 读取权限确认提示,可提供与 Claude Code 相同的五种提示音。没有 transcript 的旧版 `conversations/*.pb` 与 `*.db` 会话则回退到 45 秒空闲计时器(`ANTIGRAVITY_IDLE_SECONDS`)。 |
| **Kimi Code** | 适配器 | `bash adapters/kimi.sh --install` / `powershell adapters/kimi.ps1 -Install`([设置](#kimi-code-设置)) |
| **OpenClaw** | 适配器 | 调用 `adapters/openclaw.sh <event>`(或 `openclaw.ps1`),支持所有 CESP 分类和原生 Claude Code 事件名 |
| **Rovo Dev CLI** | 适配器 | 如果 `~/.rovodev` 存在,`install.sh` 会自动注册,或手动添加钩子到 `~/.rovodev/config.yml`([设置](#rovo-dev-cli-设置)) |
Expand Down Expand Up @@ -930,7 +930,7 @@ curl -fsSL https://raw.githubusercontent.com/PeonPing/peon-ping/main/adapters/ki

- `SessionStart`(startup)→ 问候音效(*"准备好了吗?"*、*"是的?"*)
- `AfterAgent` → 任务完成音效(*"干活,干活。"*、*"完成了!"*)
- `AfterTool`(成功)→ 任务完成音效;(失败)→ 错误音效(*"我做不到。"*)
- `AfterTool`(成功)→ 静音;`tool_response.error` → 错误音效(*"我做不到。"*)。也支持旧版顶层 `exit_code`/`stderr` 输入。
- `Notification` → 系统通知

### Windsurf 设置
Expand Down
94 changes: 67 additions & 27 deletions adapters/antigravity-py.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
#
# This adapter is designed for background / headless / LaunchAgent use.
# Unlike the shell-only antigravity.sh adapter, it uses a Python watcher
# with a 25s idle threshold that survives tool-call pauses without
# false-triggering, and pipes events through peon.sh for full config
# support (volume, pack rotation, notifications, etc.).
# that reads turn boundaries out of transcript.jsonl rather than guessing
# them from file mtime, so a long tool call is never mistaken for
# completion. Events are piped through peon.sh for full config support
# (volume, pack rotation, notifications, etc.).
#
# Requires: python3, pip-installed watchdog, peon-ping already installed
#
Expand Down Expand Up @@ -68,7 +69,9 @@ for arg in "$@"; do
echo ""
echo "Environment:"
echo " ANTIGRAVITY_NO_LAUNCHD=1 Force nohup+pidfile on macOS (skip LaunchAgent)"
echo " ANTIGRAVITY_IDLE_SECONDS Seconds of silence before emitting Stop (default: 25)"
echo " ANTIGRAVITY_IDLE_SECONDS Seconds of silence before emitting Stop for"
echo " legacy .pb/.db sessions (default: 45)."
echo " Ignored for sessions with a transcript.jsonl."
exit 0 ;;
esac
done
Expand Down Expand Up @@ -251,42 +254,47 @@ pipe_to_peon() {
local event="$1"
local session_id="$2"
local cwd="$3"
local tool_name="${4:-}"
local error="${5:-}"

_PE="$event" _PC="$cwd" _PS="$session_id" python3 -c "
# tool_name and error are only set for failures: peon.sh gates
# task.error on a named tool plus a non-empty error string.
_PE="$event" _PC="$cwd" _PS="$session_id" _PT="$tool_name" _PR="$error" python3 -c "
import json, os
print(json.dumps({
payload = {
'hook_event_name': os.environ['_PE'],
'notification_type': '',
'cwd': os.environ['_PC'],
'session_id': os.environ['_PS'],
'permission_mode': '',
'source': 'antigravity',
}))
}
if os.environ.get('_PT'):
payload['tool_name'] = os.environ['_PT']
payload['error'] = os.environ.get('_PR', '')
print(json.dumps(payload))
" | bash "$PEON_DIR/peon.sh" 2>/dev/null || true
}

# --- Test mode: skip main loop when sourced for testing ---
if [ "${PEON_ADAPTER_TEST:-0}" = "1" ]; then
return 0 2>/dev/null || exit 0
fi

# --- Start watching ---
info "${BOLD}peon-ping Antigravity adapter (Python watcher)${RESET}"
info "Watching: Antigravity conversation state directories"
info "Watcher: python3 + watchdog"
info "Press Ctrl+C to stop."
echo ""
# --- Route one JSON event line from the watcher ---
dispatch_event_line() {
local line="$1"
[ -z "$line" ] && return 0

# Start the Python watcher and read JSON events from its stdout.
# Each line is a JSON object with {event, session_id, cwd}.
# We pipe each one through peon.sh for sound playback.
python3 "$WATCHER_PY" --cwd "$PWD" 2>"$LOGFILE" | while IFS= read -r line; do
[ -z "$line" ] && continue
# One python call per line, not five: the watcher can emit a burst.
# Tab-delimited on a single line, because $() strips trailing newlines
# and a line-per-field read then hits EOF on the empty trailing fields.
local parsed
parsed=$(echo "$line" | python3 -c "
import json, sys
d = json.load(sys.stdin)
fields = [str(d.get(k, '')).replace('\t', ' ').replace('\n', ' ')
for k in ('event', 'session_id', 'cwd', 'tool_name', 'error')]
print('\t'.join(fields))
" 2>/dev/null) || return 0

# Parse event fields from JSON
event=$(echo "$line" | python3 -c "import json,sys; print(json.load(sys.stdin)['event'])" 2>/dev/null) || continue
session_id=$(echo "$line" | python3 -c "import json,sys; print(json.load(sys.stdin)['session_id'])" 2>/dev/null) || continue
event_cwd=$(echo "$line" | python3 -c "import json,sys; print(json.load(sys.stdin)['cwd'])" 2>/dev/null) || continue
local event session_id event_cwd tool_name tool_error
IFS=$'\t' read -r event session_id event_cwd tool_name tool_error <<< "$parsed"

case "$event" in
SessionStart)
Expand All @@ -301,8 +309,40 @@ python3 "$WATCHER_PY" --cwd "$PWD" 2>"$LOGFILE" | while IFS= read -r line; do
info "Agent completed: $session_id"
pipe_to_peon "Stop" "$session_id" "$event_cwd"
;;
PermissionRequest)
info "Awaiting confirmation: $session_id"
pipe_to_peon "PermissionRequest" "$session_id" "$event_cwd"
;;
PostToolUseFailure)
info "Agent error: $session_id"
pipe_to_peon "PostToolUseFailure" "$session_id" "$event_cwd" "$tool_name" "$tool_error"
;;
Notification)
info "Notification: $session_id"
pipe_to_peon "Notification" "$session_id" "$event_cwd"
;;
*)
warn "Unknown event: $event"
;;
esac
}

# --- Test mode: skip main loop when sourced for testing ---
if [ "${PEON_ADAPTER_TEST:-0}" = "1" ]; then
return 0 2>/dev/null || exit 0
fi

# --- Start watching ---
info "${BOLD}peon-ping Antigravity adapter (Python watcher)${RESET}"
info "Watching: Antigravity conversation state directories"
info "Watcher: python3 + watchdog"
info "Press Ctrl+C to stop."
echo ""

# Start the Python watcher and read JSON events from its stdout.
# The LaunchAgent points stdout and stderr at this same file, so append
# rather than truncate: two writers sharing an offset overwrite each other
# mid-line and the log becomes unreadable.
python3 "$WATCHER_PY" --cwd "$PWD" 2>>"$LOGFILE" | while IFS= read -r line; do
dispatch_event_line "$line"
done
Loading
Loading