// PBI-67: model + mode-selectie per ClaudeJob-kind. // // Sync with scrum4me-mcp/src/lib/job-config.ts — als je hier een veld // aanpast, doe hetzelfde aan de MCP-kant. Dit is bewust een duplicate // (geen gedeeld package) om de MCP-server eigenstandig te houden. // // Override-cascade (eerste match wint): // 1. task.requires_opus === true → forceer Opus // 2. job.requested_* (snapshot bij enqueue, ingevuld door deze module) // 3. product.preferred_* // 4. KIND_DEFAULTS hieronder // // CLI-flag-mapping (Claude CLI 2.1.x): // - thinking_budget (number) → mapBudgetToEffort() → --effort {low,medium,high,xhigh,max} // (de CLI heeft geen --thinking-budget flag — alleen --effort) // - max_turns blijft audit-only: de CLI heeft géén --max-turns flag. // De waarde wordt gesnapshot voor cost-attribution maar niet doorgegeven. // - allowed_tools → --allowedTools (komma-gescheiden lijst) export type ClaudeModel = | 'claude-opus-4-7' | 'claude-sonnet-4-6' | 'claude-haiku-4-5-20251001' export type PermissionMode = 'plan' | 'default' | 'acceptEdits' | 'bypassPermissions' export type JobConfig = { model: ClaudeModel thinking_budget: number permission_mode: PermissionMode max_turns: number | null allowed_tools: string[] | null } export type JobInput = { kind: string requested_model?: string | null requested_thinking_budget?: number | null requested_permission_mode?: string | null } export type ProductInput = { preferred_model?: string | null thinking_budget_default?: number | null preferred_permission_mode?: string | null } export type TaskInput = { requires_opus?: boolean | null } // Tool-allowlists per kind. Bewust géén `wait_for_job`, `check_queue_empty` // of `get_idea_context` — de runner (scrum4me-docker/bin/run-one-job.ts) // claimt voor Claude. Vangrail tegen recursieve claims binnen één invocation. const TASK_TOOLS = [ 'Read', 'Edit', 'Write', 'Bash', 'Grep', 'Glob', 'mcp__scrum4me__get_claude_context', 'mcp__scrum4me__update_task_status', 'mcp__scrum4me__update_task_plan', 'mcp__scrum4me__log_implementation', 'mcp__scrum4me__log_test_result', 'mcp__scrum4me__log_commit', 'mcp__scrum4me__verify_task_against_plan', 'mcp__scrum4me__update_job_status', 'mcp__scrum4me__ask_user_question', 'mcp__scrum4me__get_question_answer', 'mcp__scrum4me__list_open_questions', 'mcp__scrum4me__cancel_question', 'mcp__scrum4me__worker_heartbeat', ] const KIND_DEFAULTS: Record = { // Idea-kinds en PLAN_CHAT draaien in `acceptEdits` (niet `plan`): // `plan`-mode wacht op human-approval na elke planning-fase, wat in een // autonome runner-context betekent dat Claude geen `update_job_status` // aanroept en de job na lease-expiry FAILED'd. De `allowed_tools`-lijst // doet de echte sandboxing (geen Bash, geen Edit, alleen Read/Grep/etc). IDEA_GRILL: { model: 'claude-sonnet-4-6', thinking_budget: 12000, permission_mode: 'acceptEdits', max_turns: 15, allowed_tools: [ 'Read', 'Grep', 'Glob', 'WebSearch', 'AskUserQuestion', 'mcp__scrum4me__update_idea_grill_md', 'mcp__scrum4me__log_idea_decision', 'mcp__scrum4me__update_job_status', 'mcp__scrum4me__ask_user_question', 'mcp__scrum4me__get_question_answer', ], }, IDEA_MAKE_PLAN: { model: 'claude-opus-4-7', thinking_budget: 24000, permission_mode: 'acceptEdits', max_turns: 20, allowed_tools: [ 'Read', 'Grep', 'Glob', 'WebSearch', 'AskUserQuestion', 'Write', 'mcp__scrum4me__update_idea_plan_md', 'mcp__scrum4me__log_idea_decision', 'mcp__scrum4me__update_job_status', ], }, IDEA_REVIEW_PLAN: { model: 'claude-opus-4-7', thinking_budget: 6000, permission_mode: 'acceptEdits', max_turns: 1, allowed_tools: [ 'Read', 'Write', 'Grep', 'Glob', 'mcp__scrum4me__update_idea_plan_reviewed', 'mcp__scrum4me__log_idea_decision', 'mcp__scrum4me__update_job_status', 'mcp__scrum4me__ask_user_question', ], }, PLAN_CHAT: { model: 'claude-sonnet-4-6', thinking_budget: 6000, permission_mode: 'acceptEdits', max_turns: 5, allowed_tools: [ 'Read', 'Grep', 'AskUserQuestion', 'mcp__scrum4me__update_job_status', ], }, TASK_IMPLEMENTATION: { model: 'claude-sonnet-4-6', thinking_budget: 6000, permission_mode: 'bypassPermissions', max_turns: 50, allowed_tools: TASK_TOOLS, }, SPRINT_IMPLEMENTATION: { model: 'claude-sonnet-4-6', thinking_budget: 6000, permission_mode: 'bypassPermissions', max_turns: null, // Geen `mcp__scrum4me__job_heartbeat` — de runner verlengt de lease // automatisch via setInterval (zie scrum4me-docker/bin/run-one-job.ts). allowed_tools: [ ...TASK_TOOLS, 'mcp__scrum4me__update_task_execution', 'mcp__scrum4me__verify_sprint_task', ], }, } const FALLBACK: JobConfig = { model: 'claude-sonnet-4-6', thinking_budget: 6000, permission_mode: 'default', max_turns: 50, allowed_tools: null, } export function getKindDefault(kind: string): JobConfig { return KIND_DEFAULTS[kind] ?? FALLBACK } // max_turns en allowed_tools blijven kind-default (geen product/task override // in V1 — als de behoefte ontstaat, voeg analoge velden toe aan Product/Task). export function resolveJobConfig( job: JobInput, product: ProductInput, task?: TaskInput, ): JobConfig { const base = getKindDefault(job.kind) const model = ( task?.requires_opus ? 'claude-opus-4-7' : job.requested_model ?? product.preferred_model ?? base.model ) as ClaudeModel const thinking_budget = job.requested_thinking_budget ?? product.thinking_budget_default ?? base.thinking_budget const permission_mode = (job.requested_permission_mode ?? product.preferred_permission_mode ?? base.permission_mode) as PermissionMode return { model, thinking_budget, permission_mode, max_turns: base.max_turns, allowed_tools: base.allowed_tools, } } // Map numeriek thinking_budget naar de Claude CLI 2.1.x --effort flag. // Returns null als de flag niet meegegeven moet worden (budget = 0). // // Mapping (sync met scrum4me-mcp/src/lib/job-config.ts): // 0 → null (geen --effort flag) // 1..6000 → "medium" // 6001..12000 → "high" // 12001..24000→ "xhigh" // >24000 → "max" export function mapBudgetToEffort(budget: number): string | null { if (budget <= 0) return null if (budget <= 6000) return 'medium' if (budget <= 12000) return 'high' if (budget <= 24000) return 'xhigh' return 'max' } // Snapshot-velden voor ClaudeJob.requested_*. Bij elke enqueue laden we // product (voor preferred_*) en optioneel task (voor requires_opus), draaien // de resolver, en schrijven het resultaat als auditspoor in de job-rij. export type ClaudeJobSnapshotFields = { requested_model: string requested_thinking_budget: number requested_permission_mode: string } export function snapshotFromConfig(cfg: JobConfig): ClaudeJobSnapshotFields { return { requested_model: cfg.model, requested_thinking_budget: cfg.thinking_budget, requested_permission_mode: cfg.permission_mode, } }