diff --git a/CHANGELOG.md b/CHANGELOG.md index a981b77..6d8a9ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.13] - 2026-07-30 + +### Added + +- **`HookEventPostToolUseFailure`**: canonical constant for the failed-tool-call event. Agents that define it (Goose, Cursor) fire `PostToolUse` only on **success**, so without a mapping for the failure event a failed turn is invisible to a consumer until the next successful tool call or `Stop`. Cursor spells it camelCase (`CursorEventPostToolUseFailure`); the canonical constant uses Goose's PascalCase. +- **Goose `PostToolUseFailure` → `PhaseAfterTool`**: `GooseAgent.EventPhases()` now covers seven events. Goose's four remaining events (`BeforeReadFile`, `AfterFileEdit`, `BeforeShellExecution`, `AfterShellExecution`) stay deliberately unmapped — each is a strict subset of `PreToolUse`/`PostToolUse`, since reading a file and running a shell command are both tool calls, so mapping them would fire a consumer twice per tool call for no additional signal. That reasoning is now recorded on the method. + ## [0.1.12] - 2026-07-30 ### Fixed diff --git a/agent.go b/agent.go index bc7cf07..a55c938 100644 --- a/agent.go +++ b/agent.go @@ -427,6 +427,12 @@ const ( HookEventStop HookEvent = "Stop" HookEventSubagentStop HookEvent = "SubagentStop" HookEventPreCompact HookEvent = "PreCompact" + + // HookEventPostToolUseFailure fires after a tool call that FAILED. Agents + // that define it (Goose, Cursor) fire PostToolUse only on success, so + // without this event a failed turn is invisible until the next successful + // tool call or Stop. Cursor spells it camelCase — see CursorEventPostToolUseFailure. + HookEventPostToolUseFailure HookEvent = "PostToolUseFailure" ) // Cursor-specific hook events. diff --git a/agents/agents_comprehensive_test.go b/agents/agents_comprehensive_test.go index 46ddf39..ab9f613 100644 --- a/agents/agents_comprehensive_test.go +++ b/agents/agents_comprehensive_test.go @@ -319,7 +319,7 @@ func allAgentSpecs() []agentSpec { }, isLifecycleAgent: true, envAliases: []string{"goose"}, - eventPhaseCount: 6, + eventPhaseCount: 7, }, { name: "Amp", diff --git a/agents/goose.go b/agents/goose.go index 81ac69e..a723246 100644 --- a/agents/goose.go +++ b/agents/goose.go @@ -151,10 +151,15 @@ func (a *GooseAgent) IsInstalled(ctx context.Context, env agentx.Environment) (b // EventPhases returns Goose's native event-to-phase mapping. // -// Goose follows the Open Plugins hooks specification. It fires several more -// events than are mapped here (PostToolUseFailure, BeforeReadFile, -// AfterFileEdit, BeforeShellExecution, AfterShellExecution); those have no -// canonical phase equivalent and are deliberately omitted. +// Goose follows the Open Plugins hooks specification. Goose fires PostToolUse +// only on SUCCESS, so PostToolUseFailure is mapped to the same phase — without +// it a failed turn stays invisible until the next successful tool call or Stop. +// +// The four remaining Goose events (BeforeReadFile, AfterFileEdit, +// BeforeShellExecution, AfterShellExecution) are deliberately omitted. Each is +// a strict subset of PreToolUse or PostToolUse — reading a file and running a +// shell command are both tool calls — so mapping them would fire a consumer +// twice per tool call for no additional signal. // // Goose has no compaction event, so PhaseCompact is unreachable — context // injected at session start does not survive a Goose compaction. @@ -162,12 +167,13 @@ func (a *GooseAgent) IsInstalled(ctx context.Context, env agentx.Environment) (b // Reference: https://block.github.io/goose/docs/guides/context-engineering/hooks func (a *GooseAgent) EventPhases() agentx.EventPhaseMap { return agentx.EventPhaseMap{ - agentx.HookEventSessionStart: agentx.PhaseStart, - agentx.HookEventSessionEnd: agentx.PhaseEnd, - agentx.HookEventPreToolUse: agentx.PhaseBeforeTool, - agentx.HookEventPostToolUse: agentx.PhaseAfterTool, - agentx.HookEventUserPromptSubmit: agentx.PhasePrompt, - agentx.HookEventStop: agentx.PhaseStop, + agentx.HookEventSessionStart: agentx.PhaseStart, + agentx.HookEventSessionEnd: agentx.PhaseEnd, + agentx.HookEventPreToolUse: agentx.PhaseBeforeTool, + agentx.HookEventPostToolUse: agentx.PhaseAfterTool, + agentx.HookEventPostToolUseFailure: agentx.PhaseAfterTool, + agentx.HookEventUserPromptSubmit: agentx.PhasePrompt, + agentx.HookEventStop: agentx.PhaseStop, } } diff --git a/agents/goose_test.go b/agents/goose_test.go index 13ccf28..0fa09c8 100644 --- a/agents/goose_test.go +++ b/agents/goose_test.go @@ -43,12 +43,13 @@ func TestGooseEventPhases(t *testing.T) { phases := mapper.EventPhases() expected := agentx.EventPhaseMap{ - agentx.HookEventSessionStart: agentx.PhaseStart, - agentx.HookEventSessionEnd: agentx.PhaseEnd, - agentx.HookEventPreToolUse: agentx.PhaseBeforeTool, - agentx.HookEventPostToolUse: agentx.PhaseAfterTool, - agentx.HookEventUserPromptSubmit: agentx.PhasePrompt, - agentx.HookEventStop: agentx.PhaseStop, + agentx.HookEventSessionStart: agentx.PhaseStart, + agentx.HookEventSessionEnd: agentx.PhaseEnd, + agentx.HookEventPreToolUse: agentx.PhaseBeforeTool, + agentx.HookEventPostToolUse: agentx.PhaseAfterTool, + agentx.HookEventPostToolUseFailure: agentx.PhaseAfterTool, + agentx.HookEventUserPromptSubmit: agentx.PhasePrompt, + agentx.HookEventStop: agentx.PhaseStop, } assert.Equal(t, expected, phases)