first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
// This file defines the process contract between pigo and a user hook command:
// what pigo writes to the hook's stdin (HookInput), what pigo reads back from
// its stdout (HookOutput), and the internal merged decision the dispatcher
// produces (HookDecision). It also defines the exit-code semantics parsing.
//
// The wire contract follows Claude Code: exit 0 = allow, exit 2 = block (with
// stderr as the reason), any other non-zero = execution failure. On exit 0 a
// well-formed JSON stdout is parsed as HookOutput; a non-JSON stdout is a no-op.
package hooks
import (
"encoding/json"
"strings"
)
// HookInput is the JSON payload pigo writes to a hook command's stdin. It
// carries only observable, non-secret fields (FR-17) — never API keys or
// credentials. Per-event fields are omitempty so a payload only contains the
// fields relevant to its event type.
type HookInput struct {
EventType string `json:"event_type"`
SessionID string `json:"session_id,omitempty"`
ProjectDir string `json:"project_dir,omitempty"`
ToolName string `json:"tool_name,omitempty"` // Pre/PostToolUse
ToolInput json.RawMessage `json:"tool_input,omitempty"` // Pre/PostToolUse
ToolResponse json.RawMessage `json:"tool_response,omitempty"` // PostToolUse
Prompt string `json:"prompt,omitempty"` // UserPromptSubmit
StopReason string `json:"stop_reason,omitempty"` // Stop/SessionEnd
Source string `json:"source,omitempty"` // SessionStart (startup/resume)
Trigger string `json:"trigger,omitempty"` // PreCompact (manual/auto)
Message string `json:"message,omitempty"` // Notification
}
// HookOutput is the optional JSON a hook may print to stdout to influence the
// agent. A non-JSON stdout on exit 0 is treated as an empty HookOutput (no
// operation). Decision "block" is equivalent to exiting with code 2.
type HookOutput struct {
Decision string `json:"decision,omitempty"` // "block" | "approve" | ""
Reason string `json:"reason,omitempty"`
AdditionalContext string `json:"additionalContext,omitempty"`
Continue *bool `json:"continue,omitempty"`
UpdatedInput json.RawMessage `json:"updatedInput,omitempty"` // PreToolUse: rewrite tool args
}
// blocks reports whether this output requests a block. A decision of "block"
// blocks; an explicit continue=false also blocks. "approve" and the empty
// decision allow.
func (o HookOutput) blocks() bool {
if strings.EqualFold(o.Decision, "block") {
return true
}
if o.Continue != nil && !*o.Continue {
return true
}
return false
}
// HookDecision is the dispatcher's merged result after running all matched
// hooks for one event. Block is set if any hook blocked; Reason accumulates the
// blocking reasons; AdditionalContext accumulates injected context in order;
// UpdatedInput holds the last-provided rewrite (last writer wins, §5.4).
type HookDecision struct {
Block bool
Reason string
AdditionalContext string
UpdatedInput json.RawMessage
}
// parseHookOutput parses a hook's stdout into a HookOutput. On exit 0 a
// non-JSON body is a no-op (returns the zero value, ok=false). Empty/whitespace
// stdout is also a no-op. A valid JSON object is parsed and ok is true.
func parseHookOutput(stdout []byte) (HookOutput, bool) {
trimmed := strings.TrimSpace(string(stdout))
if trimmed == "" {
return HookOutput{}, false
}
var out HookOutput
if err := json.Unmarshal([]byte(trimmed), &out); err != nil {
return HookOutput{}, false
}
return out, true
}