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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion agents/agents_comprehensive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ func allAgentSpecs() []agentSpec {
},
isLifecycleAgent: true,
envAliases: []string{"goose"},
eventPhaseCount: 6,
eventPhaseCount: 7,
},
{
name: "Amp",
Expand Down
26 changes: 16 additions & 10 deletions agents/goose.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,23 +151,29 @@ 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.
//
// 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,
}
}

Expand Down
13 changes: 7 additions & 6 deletions agents/goose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading