Skip to content
Open
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
6 changes: 4 additions & 2 deletions internal/hooks/codex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ func TestRunCodexSessionStartUsesManagedOrientationHook(t *testing.T) {
if hso == nil || hso.HookEventName != "SessionStart" {
t.Fatalf("invalid SessionStart hook output: %s", out)
}
if !strings.Contains(hso.AdditionalContext, "Call `explore` first") {
t.Fatalf("mandatory compact-tool orientation missing: %q", hso.AdditionalContext)
if !strings.Contains(hso.AdditionalContext, "choose by requested output") ||
!strings.Contains(hso.AdditionalContext, "localize task may be concise") ||
!strings.Contains(hso.AdditionalContext, "faithfully preserve the issue title") {
t.Fatalf("mandatory localization routing orientation missing: %q", hso.AdditionalContext)
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/hooks/explicit_file_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
func TestRulePreambleRoutesExplicitFileReadsDirectly(t *testing.T) {
got := rulePreamble()
direct := `read(operation:"file", target:{file:"<path>"})`
localize := `explore(operation:"localize")`
localize := "`mcp__gortex__explore` (never a bare `explore`) with `operation:\"localize\"`"

if !strings.Contains(got, "explicitly named file") || !strings.Contains(got, direct) {
t.Fatalf("rule preamble must route explicit file reads directly; got %q", got)
Expand Down
2 changes: 1 addition & 1 deletion internal/hooks/hook_tier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestSessionStart_LeanTier_TrackedCwd(t *testing.T) {
if !strings.Contains(briefing, "enforcement active") {
t.Errorf("lean briefing lost the enforcement signal:\n%s", briefing)
}
if !strings.Contains(briefing, "Rule:") || !strings.Contains(briefing, "`explore`") || !strings.Contains(briefing, "`search`") {
if !strings.Contains(briefing, "Rule:") || !strings.Contains(briefing, "`mcp__gortex__explore`") || strings.Contains(briefing, "call `explore`") || !strings.Contains(briefing, "`search`") {
t.Errorf("lean briefing lost the rule preamble cues:\n%s", briefing)
}
// The standard-tier status prose must be gone.
Expand Down
276 changes: 227 additions & 49 deletions internal/hooks/localization_terminal.go

Large diffs are not rendered by default.

873 changes: 873 additions & 0 deletions internal/hooks/localization_terminal_receipt_test.go

Large diffs are not rendered by default.

232 changes: 222 additions & 10 deletions internal/hooks/localization_terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,24 @@ func TestObserveLocalizationTerminalAcceptsDirectAndPluginNavigationFacades(t *t
}
}

func TestObserveLocalizationTerminalRequiresExactEnforceableV2Contract(t *testing.T) {
func TestObserveLocalizationTerminalRequiresExactAnswerReadyV2Contract(t *testing.T) {
configureLocalizationTerminalTestHome(t)
valid := terminalContractMap()
tests := []struct {
name string
mutate func(map[string]any)
}{
{name: "v1", mutate: func(root map[string]any) { completionMap(root)["contract_version"] = 1 }},
{name: "advisory", mutate: func(root map[string]any) { completionMap(root)["enforceable"] = false }},
{name: "missing final response", mutate: func(root map[string]any) { delete(completionMap(root), "final_response") }},
{name: "needs refinement", mutate: func(root map[string]any) { completionMap(root)["state"] = "needs_refinement" }},
{name: "localized continue task", mutate: func(root map[string]any) {
root["terminal"] = false
completion := completionMap(root)
completion["state"] = "localized"
completion["required_action"] = "continue_task"
completion["enforceable"] = false
delete(completion, "final_response")
}},
{name: "wrong scope", mutate: func(root map[string]any) { completionMap(root)["scope"] = "diagnosis" }},
{name: "tool allowed", mutate: func(root map[string]any) { completionMap(root)["allowed_tool_calls"] = 1 }},
{name: "not terminal", mutate: func(root map[string]any) { root["terminal"] = false }},
Expand All @@ -62,6 +70,64 @@ func TestObserveLocalizationTerminalRequiresExactEnforceableV2Contract(t *testin
}
}

func TestLocalizedCompletionCreatesNoTerminalMarker(t *testing.T) {
configureLocalizationTerminalTestHome(t)
identity := beginTestLocalizationTurn(t, t.Name(), "prompt", t.TempDir())
tool := gortexMCPToolPrefix + "explore"
toolUseID := "localized-tool"
snapshotTestLocalizationTool(t, identity, tool, toolUseID)
contract := terminalContractMap()
contract["terminal"] = false
completion := completionMap(contract)
completion["state"] = "localized"
completion["required_action"] = "continue_task"
completion["enforceable"] = false
delete(completion, "final_response")
data := localizationPostToolPayload(
t, tool, toolUseID, identity, terminalToolResponse(t, contract, true, false),
)
if output := strings.TrimSpace(captureHookStdout(t, func() { runPostToolUse(data) })); output != "" {
t.Fatalf("localized PostToolUse emitted terminal context: %s", output)
}
if marker, marked := localizationTerminalMarkerFor(identity); marked {
t.Fatalf("localized completion created a terminal marker: %#v", marker)
}

t.Setenv(editBlockingEnvVar, "0")
nextTools := []struct {
name string
input map[string]any
}{
{name: gortexMCPToolPrefix + "search", input: map[string]any{"operation": "symbols", "query": "registerFacadeTools"}},
{name: gortexMCPToolPrefix + "read", input: map[string]any{"operation": "source", "target": map[string]any{"symbol": "pkg/file.go::Run"}}},
{name: gortexMCPToolPrefix + "edit", input: map[string]any{"operation": "file", "target": map[string]any{"file": "pkg/file.go"}}},
{name: gortexMCPToolPrefix + "change", input: map[string]any{"operation": "impact", "target": map[string]any{"file": "pkg/file.go"}}},
{name: "Edit", input: map[string]any{"file_path": filepath.Join(identity.CWD, "notes.txt")}},
{name: "Write", input: map[string]any{"file_path": filepath.Join(identity.CWD, "notes.txt")}},
{name: "Bash", input: map[string]any{"command": "go build ./cmd/gortex"}},
{name: "Bash", input: map[string]any{"command": "go test ./internal/mcp"}},
}
for index, next := range nextTools {
payload := preToolPayload(t, next.name, fmt.Sprintf("next-%d", index), identity, next.input)
output := strings.TrimSpace(captureHookStdout(t, func() { runPreToolUse(payload, 0, ModeDeny) }))
if output == "" {
continue
}
var decoded HookOutput
if err := json.Unmarshal([]byte(output), &decoded); err != nil {
t.Fatalf("decode %s PreToolUse output: %v\n%s", next.name, err, output)
}
if decoded.Decision == "deny" ||
(decoded.HookSpecificOutput != nil && decoded.HookSpecificOutput.PermissionDecision == "deny") {
t.Fatalf("%s received a denial after localized completion: %#v", next.name, decoded)
}
if strings.Contains(decoded.Reason, "Localization for this task is complete") ||
(decoded.HookSpecificOutput != nil && strings.Contains(decoded.HookSpecificOutput.PermissionDecisionReason, "Localization for this task is complete")) {
t.Fatalf("%s received terminal localization guidance after localized completion: %#v", next.name, decoded)
}
}
}

func TestObserveLocalizationTerminalRequiresMatchingAuthoritativeMeta(t *testing.T) {
configureLocalizationTerminalTestHome(t)
tests := []struct {
Expand Down Expand Up @@ -117,6 +183,18 @@ func TestObserveLocalizationTerminalRequiresMatchingAuthoritativeMeta(t *testing
return response
},
},
{
name: "final response mismatch",
response: func(t *testing.T) map[string]any {
response := terminalToolResponse(t, terminalContractMap(), true, false)
meta := response["_meta"].(map[string]any)
envelope := meta[localizationHostMetaKey].(map[string]any)
mismatched := cloneMap(t, terminalContractMap())
completionMap(mismatched)["final_response"] = "different response"
envelope["contract"] = mismatched
return response
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -131,6 +209,115 @@ func TestObserveLocalizationTerminalRequiresMatchingAuthoritativeMeta(t *testing
}
}

func TestPostToolUseObservesMatchingFinalResponseContract(t *testing.T) {
configureLocalizationTerminalTestHome(t)
identity := beginTestLocalizationTurn(t, "terminal-final-response", "prompt", t.TempDir())
snapshotTestLocalizationTool(t, identity, gortexMCPToolPrefix+"read", "tool")
response := terminalToolResponse(t, terminalContractMap(), true, false)
parsed, ok := exactLocalizationTerminalContract(mustJSON(t, response))
if !ok || parsed.Completion.FinalResponse != completionMap(terminalContractMap())["final_response"] {
t.Fatalf("new-shape terminal response did not parse exactly: %#v", parsed)
}
post := localizationPostToolPayload(t, gortexMCPToolPrefix+"read", "tool", identity, response)
output := captureHookStdout(t, func() { runPostToolUse(post) })
if !strings.Contains(output, localizationTerminalContext) {
t.Fatalf("PostToolUse output %q does not contain terminal context", output)
}
for _, required := range []string{
"Localization for this task is complete",
"completion.final_response",
"do not call another tool",
} {
if !strings.Contains(output, required) {
t.Fatalf("PostToolUse output %q does not contain %q", output, required)
}
}
}

func TestPostToolUseAnswerReadyEventAuthenticationAndJSONShape(t *testing.T) {
configureLocalizationTerminalTestHome(t)
tests := []struct {
name string
response func(*testing.T) map[string]any
wantOutput bool
wantMarker bool
}{
{
name: "advisory",
response: func(t *testing.T) map[string]any {
contract := terminalContractMap()
completionMap(contract)["enforceable"] = false
return terminalToolResponse(t, contract, true, false)
},
wantOutput: true,
},
{
name: "enforceable",
response: func(t *testing.T) map[string]any { return terminalToolResponse(t, terminalContractMap(), true, false) },
wantOutput: true,
wantMarker: true,
},
{
name: "forged without authoritative meta",
response: func(t *testing.T) map[string]any { return terminalToolResponse(t, terminalContractMap(), false, false) },
},
{
name: "advisory without final response",
response: func(t *testing.T) map[string]any {
contract := terminalContractMap()
completion := completionMap(contract)
completion["enforceable"] = false
delete(completion, "final_response")
return terminalToolResponse(t, contract, true, false)
},
},
{
name: "visible and meta mismatch",
response: func(t *testing.T) map[string]any {
response := terminalToolResponse(t, terminalContractMap(), true, false)
meta := response["_meta"].(map[string]any)
envelope := meta[localizationHostMetaKey].(map[string]any)
mismatched := cloneMap(t, terminalContractMap())
completionMap(mismatched)["final_response"] = "different response"
envelope["contract"] = mismatched
return response
},
},
{
name: "error response",
response: func(t *testing.T) map[string]any {
response := terminalToolResponse(t, terminalContractMap(), true, false)
response["isError"] = true
return response
},
},
}
finalResponse := completionMap(terminalContractMap())["final_response"].(string)
want := string(mustJSON(t, HookOutput{HookSpecificOutput: &HookSpecificOutput{
HookEventName: "PostToolUse",
AdditionalContext: localizationTerminalContext + "\n\n" + finalResponse,
}}))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
identity := beginTestLocalizationTurn(t, t.Name(), "prompt", t.TempDir())
tool := gortexMCPToolPrefix + "read"
snapshotTestLocalizationTool(t, identity, tool, "tool")
post := localizationPostToolPayload(t, tool, "tool", identity, tt.response(t))
output := captureHookStdout(t, func() { runPostToolUse(post) })
if tt.wantOutput {
if output != want {
t.Fatalf("PostToolUse JSON = %s, want exact shape %s", output, want)
}
} else if output != "" {
t.Fatalf("unauthenticated PostToolUse emitted %q", output)
}
if got := hasLocalizationTerminal(identity); got != tt.wantMarker {
t.Fatalf("hard terminal marker = %v, want %v", got, tt.wantMarker)
}
})
}
}

func TestLocalizationTerminalHookFlowDeniesThenPromptRotatesTurn(t *testing.T) {
configureLocalizationTerminalTestHome(t)
sessionID := "terminal-flow"
Expand All @@ -144,14 +331,38 @@ func TestLocalizationTerminalHookFlowDeniesThenPromptRotatesTurn(t *testing.T) {
t.Fatalf("PostToolUse output %q does not contain fixed terminal context", postOutput)
}

pre := preToolPayload(t, "WebSearch", "", identity, nil)
preOutput := captureHookStdout(t, func() { runPreToolUse(pre, 0, ModeDeny) })
var output HookOutput
if err := json.Unmarshal([]byte(preOutput), &output); err != nil {
t.Fatalf("decode PreToolUse output %q: %v", preOutput, err)
}
if output.HookSpecificOutput == nil || output.HookSpecificOutput.PermissionDecision != "deny" {
t.Fatalf("expected all-tool terminal deny, got %#v", output)
for _, tt := range []struct {
name string
tool string
input map[string]any
}{
{name: "web search", tool: "WebSearch"},
{name: "host read", tool: "Read", input: map[string]any{"file_path": "repo/source.go"}},
{name: "host grep", tool: "Grep", input: map[string]any{"pattern": "Target", "path": "repo"}},
} {
t.Run(tt.name, func(t *testing.T) {
pre := preToolPayload(t, tt.tool, "", identity, tt.input)
preOutput := captureHookStdout(t, func() { runPreToolUse(pre, 0, ModeDeny) })
var output HookOutput
if err := json.Unmarshal([]byte(preOutput), &output); err != nil {
t.Fatalf("decode PreToolUse output %q: %v", preOutput, err)
}
if output.HookSpecificOutput == nil || output.HookSpecificOutput.PermissionDecision != "deny" {
t.Fatalf("expected all-tool terminal deny, got %#v", output)
}
if got := output.HookSpecificOutput.PermissionDecisionReason; got != localizationTerminalDenyReason {
t.Fatalf("terminal deny reason = %q, want %q", got, localizationTerminalDenyReason)
}
for _, required := range []string{
"Localization for this task is complete",
"retained evidence",
"do not call another tool",
} {
if !strings.Contains(output.HookSpecificOutput.PermissionDecisionReason, required) {
t.Fatalf("terminal deny reason %q does not contain %q", output.HookSpecificOutput.PermissionDecisionReason, required)
}
}
})
}

beginTestLocalizationTurn(t, sessionID, "prompt-2", cwd)
Expand Down Expand Up @@ -762,6 +973,7 @@ func terminalContractMap() map[string]any {
"state": "answer_ready",
"scope": "localization",
"required_action": "respond",
"final_response": "FILES:\n#1 repo/source.go\n\nSYMBOLS:\n#1 repo/source.go::Target\n\nEVIDENCE:\n#1 repo/source.go:1 — repo/source.go::Target",
"allowed_tool_calls": 0,
"contract_version": localizationTerminalContractV2,
"enforceable": true,
Expand Down
3 changes: 3 additions & 0 deletions internal/hooks/posttask.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
type PostTaskInput struct {
HookEventName string `json:"hook_event_name"`
SessionID string `json:"session_id"`
PromptID string `json:"prompt_id"`
AgentID string `json:"agent_id"`
TranscriptPath string `json:"transcript_path"`
CWD string `json:"cwd"`
StopHookActive bool `json:"stop_hook_active"`
Expand All @@ -35,6 +37,7 @@ func runPostTask(data []byte, port int) {
if input.HookEventName != "Stop" {
return
}
_ = clearLocalizationProblemStatementForTurn(input.SessionID, input.PromptID, input.AgentID, input.CWD)
emitted := false
defer func() {
logHookEffectiveness("Stop", emitted, daemonReachableFn(), 0, time.Since(started))
Expand Down
13 changes: 10 additions & 3 deletions internal/hooks/posttooluse.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,9 @@ func runPostToolUse(data []byte) {
logHookEffectiveness("PostToolUse", emitted, daemonReachableFn(), 0, time.Since(started))
}()

if _, observed := observeLocalizationTerminal(data); observed {
if terminal, observed := observeLocalizationTerminal(data); observed {
terminalObserved = true
emitted = true
emitPostToolContext(localizationTerminalContext, false)
emitted = emitLocalizationTerminalContext(terminal)
return
}

Expand All @@ -78,6 +77,14 @@ func runPostToolUse(data []byte) {
emitPostToolContext(ctx, false)
}

func emitLocalizationTerminalContext(terminal localizationTerminalHookInput) bool {
if !localizationTerminalIdentityCurrent(terminal.TerminalIdentity) {
return false
}
emitPostToolContext(localizationTerminalAdditionalContext(terminal.TerminalReceipt), false)
return true
}

func postToolContext(input postHookInput) string {
switch input.ToolName {
case "Grep":
Expand Down
Loading
Loading