Files
Maven/docs/reports/2026-09-05-action-resolution-boundary.md
T
claude 025f81e961 mavend: wire resolveAction into actionAct
Daemon half of the action-resolution boundary:

- Add resolveAction wrapper: delegates to ResolveActionCandidate,
  records outcome in the decision trace (action-resolve:route/matcher)
- Refactor actionAct: remove matcher call, consume candidate, write
  resolved values back into Slots for downstream branches
- Add 8 integration tests pinning all required scenarios:
  route-sourced, matcher-sourced, matcher miss, destructive confirm,
  task-status intercept, stage-0, learned-router, alias match

All existing tests pass. Execution/risk/confirmation unchanged.
2026-09-05 21:50:33 +04:00

6.3 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 hash

Pending.