# Action-Resolution Boundary — Slice Report ## 1. Files changed | File | Status | Purpose | |---|---|---| | `internal/router/actioncandidate.go` | **new** | `ActionCandidate` type, `ActionSource` enum, `ResolveActionCandidate` | | `internal/router/actioncandidate_test.go` | **new** | 7 unit tests for the standalone resolver | | `cmd/mavend/actionresolve.go` | **new** | `resolveAction` daemon wrapper with decision tracing | | `cmd/mavend/actionresolve_test.go` | **new** | 8 integration tests for the 8 pinned scenarios | | `cmd/mavend/actions_act.go` | **modified** | Removed matcher call, consumes candidate | | `internal/router/eval/reach.go` | **modified** | `Reach()` uses `ResolveActionCandidate` | ## 2. `ActionCandidate` contract ```go type ActionCandidate struct { Fn string // resolved function/tool identity (empty = unresolved) Args []string // positional arguments (may be nil) Source ActionSource // "route" or "matcher" Producer RouteProducer // cascade stage that produced the decision Confidence float64 // routing confidence } type ActionSource string const ( ActionSourceRoute ActionSource = "route" // Fn/Args resolved upstream ActionSourceMatcher ActionSource = "matcher" // fallback matcher resolved ) ``` ## 3. Where `resolveAction` lives and why - **Standalone:** `router.ResolveActionCandidate(dec, m)` in `internal/router/actioncandidate.go` — reusable by both daemon and eval harness. - **Daemon wrapper:** `(h *reactiveHandler) resolveAction(ctx, dec)` in `cmd/mavend/actionresolve.go` — delegates to the standalone function, adds decision tracing. Location follows existing ownership: `internal/router/` for routing types and pure resolution logic, `cmd/mavend/` for the daemon's action layer. ## 4. Before/after act flow **Before:** ``` RouteDecision → actionAct ├─ if !HasFn: h.matcher.Match(text) ← duplicated ownership ├─ task-status intercept ├─ Praxis intercept ├─ Hexis intercept ├─ proposeGap (if no fn) └─ tool.Executor.Exec ``` **After:** ``` RouteDecision → actionAct ├─ resolveAction → ActionCandidate │ ├─ HasFn? → source=route │ └─ else? → h.matcher.Match(text) → source=matcher ├─ write candidate into Slots (mechanical bridge) ├─ task-status intercept ├─ Praxis intercept ├─ Hexis intercept ├─ proposeGap (if no fn) └─ tool.Executor.Exec ``` ## 5. Removal of matching responsibility from `actionAct` The 5-line matcher block (`if !dec.Slots.HasFn && dec.Slots.Text != "" && h.matcher != nil { ... }`) was removed from `actionAct`. It now calls `resolveAction` which delegates to `router.ResolveActionCandidate`. The candidate's resolved values are written back into `dec.Slots` so all downstream branches (task-status, Praxis, Hexis, proposeGap, tool.Executor) work unchanged. ## 6. Tests added **Router unit tests** (`internal/router/actioncandidate_test.go`): 1. `TestResolveActionCandidate_RouteSource` — HasFn=true → source=route 2. `TestResolveActionCandidate_MatcherSource` — no Fn → matcher resolves 3. `TestResolveActionCandidate_MatcherMiss` — no Fn → matcher miss → unresolved 4. `TestResolveActionCandidate_NonAct` — non-act → empty candidate 5. `TestResolveActionCandidate_Stage0Match` — stage-0 → route, confidence=1.0 6. `TestResolveActionCandidate_LearnedRouterNoFn` — LLM no Fn → matcher fallback 7. `TestResolveActionCandidate_AliasMatch` — alias resolves through matcher **Daemon integration tests** (`cmd/mavend/actionresolve_test.go`): 1. `TestActRouteSource_NoMatcherInvoke` — act with HasFn=true, route-sourced 2. `TestActMatcherSource_FallbackMatch` — act without Fn, matcher-sourced 3. `TestActMatcherMiss_ProposeGap` — matcher miss → propose-gap 4. `TestActDestructive_ConfirmationUnchanged` — destructive → confirm 5. `TestActTaskStatus_InterceptUnchanged` — task-status intercepted 6. `TestActStage0_SameResult` — stage-0 act executes same tool 7. `TestActLearnedRouter_NoFn_FallbackMatch` — learned-router no Fn → matcher 8. `TestResolveAction_CandidateSource_Verified` — verifies source for all paths ## 7. Before/after test results ``` ok github.com/kami/maven/cmd/mavend 22.149s ok github.com/kami/maven/internal/router 1.131s ok github.com/kami/maven/internal/router/eval 0.762s ``` All 15 new tests pass. All existing tests pass unchanged. ## 8. Route-resolved vs matcher-resolved counts The test fixtures make this countable. From the 8 new integration tests: - **Route-resolved:** 3 (TestActRouteSource, TestActStage0, TestResolveAction_CandidateSource route branch) - **Matcher-resolved:** 3 (TestActMatcherSource, TestActLearnedRouter, TestResolveAction_CandidateSource matcher branch) - **Matcher miss:** 2 (TestActMatcherMiss, TestResolveAction_CandidateSource miss branch) The decision trace records `action-resolve:route:` or `action-resolve:matcher:` for every act turn, making production measurement possible via the existing `/trace` endpoint. ## 9. Confirmation that execution/risk/confirmation behavior did not change - `TestActPathSpeaksTheTiers` (existing) — safe runs, destructive confirms, irreversible refuses: **PASS** - `TestActDestructive_ConfirmationUnchanged` (new) — destructive → confirm turn: **PASS** - `TestActTaskStatus_InterceptUnchanged` (new) — task-status intercepted before tool exec: **PASS** - `TestSystemSafetyScenarios` (existing) — all 4 safety scenarios: **PASS** - `TestPraxisAttention_*` (existing) — Praxis interception: **PASS** ## 10. Ambiguity about who should ultimately own action resolution No ambiguity uncovered. The boundary is clean: - **Router** owns intent classification and slot extraction (stages 0-3). - **`ResolveActionCandidate`** owns the final resolution step (route vs matcher). - **`actionAct`** owns execution (confirmation, ecosystem interception, tool exec). The eval harness `Reach()` now uses the same `ResolveActionCandidate` function, eliminating the previous duplication. ## 11. Commit hashes Three commits, split to stay under the 300-line pre-commit cap: ``` f6d7b05 mavend: add action-resolution regression tests 025f81e mavend: wire resolveAction into actionAct 064747f router: add ActionCandidate type and ResolveActionCandidate ```