* fix(KIND_DEFAULTS): permission_mode acceptEdits voor idea-kinds + PLAN_CHAT Spiegel van scrum4me-mcp PR #40. Symptoom: IDEA_GRILL job IDEA-047 werd 3x geclaimd, Claude liep telkens succesvol (exit 0 na 600-900s) maar deed nooit update_job_status('done'). Lease verliep, retry_count >= 2 → status FAILED met "agent did not complete job within 2 attempts". Root cause: KIND_DEFAULTS.permission_mode='plan' voor idea-kinds en PLAN_CHAT. In autonome batch-mode wacht plan-mode op een human "go" na elke planning-fase — geen mens in de loop in deze runner-context. Fix: - IDEA_GRILL.permission_mode: plan → acceptEdits - IDEA_MAKE_PLAN.permission_mode: plan → acceptEdits - PLAN_CHAT.permission_mode: plan → acceptEdits - PLAN_CHAT.allowed_tools krijgt mcp__scrum4me__update_job_status (ontbrak) De allowed_tools-lijsten doen de echte sandboxing (geen Bash, geen Edit voor IDEA_GRILL/PLAN_CHAT). Plan-mode's "veiligheid" wordt al door tool-allowlists geleverd; acceptEdits is hier puur om Claude door zijn eigen update_job_status loop te laten lopen zonder approval-wachttijd. Plus: docs/runbooks/{job-model-selection,worker-idempotency}.md tabellen bijgewerkt. last_updated note in job-model-selection.md. Verify: 587 tests in 78 files passed (incl. nieuwe lib/job-config tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: remove .claude/scheduled_tasks.lock per ongeluk meegecommit Lokale tooling-lock-file van de cowork-skills, hoort niet in de repo. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
211 lines
6.8 KiB
TypeScript
211 lines
6.8 KiB
TypeScript
// 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<string, JobConfig> = {
|
|
// 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',
|
|
],
|
|
},
|
|
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,
|
|
}
|
|
}
|