// Package router decides whether a task should run on a local Claude Code // session (current Phase 1 behaviour) or be delegated to the centralized // SecuAAS secutools AI platform (Phase 2 chantier E). // // The decision is driven entirely by the task's YAML frontmatter; no // network call is performed in Decide. package router import "strings" // Provider is the destination chosen for a task. type Provider string const ( // ProviderClaudeCode means dispatch to a local ccl-auto tmux session // running Claude Code. This is the Phase 1 behaviour. ProviderClaudeCode Provider = "claude-code" // ProviderAuto means delegate to secutools and let its smart_triage // router pick the actual backend (GPU > Claude > Gemini fallback chain). ProviderAuto Provider = "auto" // ProviderGPU pins delegation to the in-cluster vLLM GPU pool. ProviderGPU Provider = "gpu" // ProviderGemini pins delegation to Google Gemini via secutools. ProviderGemini Provider = "gemini" // ProviderClaudeAPI pins delegation to the Anthropic API (NOT Claude // Code locally — this means stateless API calls billed to the secutools // account). ProviderClaudeAPI Provider = "claude-api" ) // IsDelegated reports whether p means "submit to secutools" (i.e. not the // local Claude Code path). func (p Provider) IsDelegated() bool { switch p { case ProviderAuto, ProviderGPU, ProviderGemini, ProviderClaudeAPI: return true default: return false } } // Decision is the output of Decide: which provider to use, plus a short // human-readable reason for logging and the /api/delegated/status endpoint. type Decision struct { Provider Provider Reason string } // Task is the slice of frontmatter fields the router cares about. It is // intentionally narrower than the dispatcher's full TaskFrontmatter so that // router tests don't need to import the dispatcher. type Task struct { PreferredAI string // auto | claude-code | gpu | gemini | claude-api (case-insensitive) AllowDelegation bool // default false → backward-compatible (Claude Code) NeedsClaudeCode bool // legacy bypass — forces ProviderClaudeCode ComplexityHint string // low | medium | high (informational only for now) } // Decide returns the routing decision for the given task. // // Order of precedence: // 1. needs_claude_code: true → ProviderClaudeCode (legacy bypass, never delegate) // 2. preferred_ai: claude-code → ProviderClaudeCode (explicit) // 3. allow_delegation: false → ProviderClaudeCode (default safety net) // 4. preferred_ai parses to a delegated provider → that provider // 5. fallback → ProviderAuto (let secutools smart_triage decide) func Decide(t Task) Decision { if t.NeedsClaudeCode { return Decision{ProviderClaudeCode, "needs_claude_code=true"} } pref := strings.ToLower(strings.TrimSpace(t.PreferredAI)) if pref == string(ProviderClaudeCode) { return Decision{ProviderClaudeCode, "preferred_ai=claude-code"} } if !t.AllowDelegation { return Decision{ProviderClaudeCode, "allow_delegation=false (default)"} } switch Provider(pref) { case ProviderGPU, ProviderGemini, ProviderClaudeAPI: return Decision{Provider(pref), "preferred_ai=" + pref} case ProviderAuto, "": return Decision{ProviderAuto, "preferred_ai=auto (smart_triage)"} default: // Unknown value: fail safe to local Claude Code so a typo doesn't // silently route real work to GPU. return Decision{ProviderClaudeCode, "unknown preferred_ai=" + pref + " → fail-safe"} } }