Files
Maven/docs/reports/2026-09-05-action-resolution-boundary.md
T

6.5 KiB

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

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:<fn> or action-resolve:matcher:<fn> 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