feat(PBI-4/ST-004): publieke API + KIND_DEFAULTS + per-kind prompts

Voorbereidende wijzigingen voor de queue-loop-refactor (zie
docs/plans/queue-loop-extraction.md in Scrum4Me-repo). Maakt scrum4me-mcp
geschikt als gedeelde library voor de nieuwe scrum4me-docker runner.

- T-13: export getFullJobContext uit src/tools/wait-for-job.ts
- T-14: mapBudgetToEffort(budget) → --effort {medium,high,xhigh,max} mapping
  voor Claude CLI 2.1.x (heeft geen --thinking-budget). Comment in header
  documenteert dat max_turns audit-only is en de CLI-flag-mapping.
- T-15: KIND_DEFAULTS.allowed_tools van null → expliciete lijsten zonder
  wait_for_job/check_queue_empty/get_idea_context. Vangrail tegen recursieve
  claims. SPRINT_IMPLEMENTATION mist bewust job_heartbeat (runner doet
  lease-renewal).
- T-16: src/lib/idea-prompts.ts → src/lib/kind-prompts.ts. Nieuwe export
  getKindPromptText voor alle 5 kinds. Back-compat re-export
  getIdeaPromptText behouden zodat wait-for-job.ts:508 ongewijzigd werkt.
- T-17: nieuwe prompts src/prompts/task/implementation.md,
  sprint/implementation.md, plan-chat/chat.md. Idea-prompts (M12) ongewijzigd.

Tests: 334 passed (38 files). 27 nieuwe asserts: mapBudgetToEffort
grenswaarden (14), KIND_DEFAULTS.allowed_tools structurele checks (6),
kind-prompts loading + verboden-tool-mentions (13).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Madhura68 2026-05-08 17:15:21 +02:00
parent 2fbb36bdbe
commit 96f5b0dd03
9 changed files with 391 additions and 41 deletions

View file

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest' import { describe, it, expect } from 'vitest'
import { getKindDefault, resolveJobConfig } from '../src/lib/job-config.js' import { getKindDefault, resolveJobConfig, mapBudgetToEffort } from '../src/lib/job-config.js'
const KIND_EXPECTED = { const KIND_EXPECTED = {
IDEA_GRILL: { model: 'claude-sonnet-4-6', thinking_budget: 12000, permission_mode: 'plan', max_turns: 15 }, IDEA_GRILL: { model: 'claude-sonnet-4-6', thinking_budget: 12000, permission_mode: 'plan', max_turns: 15 },
@ -86,12 +86,81 @@ describe('resolveJobConfig — cascade', () => {
expect(cfg.permission_mode).toBe('acceptEdits') expect(cfg.permission_mode).toBe('acceptEdits')
}) })
it('max_turns en allowed_tools blijven kind-default ook met product- en job-overrides (geen V1-cascade)', () => { it('max_turns blijft kind-default ook met product- en job-overrides (geen V1-cascade)', () => {
const cfg = resolveJobConfig( const cfg = resolveJobConfig(
{ kind: 'IDEA_GRILL', requested_model: 'claude-haiku-4-5-20251001' }, { kind: 'IDEA_GRILL', requested_model: 'claude-haiku-4-5-20251001' },
{ preferred_model: 'claude-sonnet-4-6' }, { preferred_model: 'claude-sonnet-4-6' },
) )
expect(cfg.max_turns).toBe(15) expect(cfg.max_turns).toBe(15)
expect(cfg.allowed_tools).toEqual(['Read', 'Grep', 'Glob', 'WebSearch', 'AskUserQuestion']) })
})
describe('KIND_DEFAULTS.allowed_tools', () => {
it('TASK_IMPLEMENTATION bevat geen claim-tools', () => {
const cfg = getKindDefault('TASK_IMPLEMENTATION')
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__wait_for_job')
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__check_queue_empty')
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__get_idea_context')
})
it('TASK_IMPLEMENTATION bevat de essentiële task-tools', () => {
const cfg = getKindDefault('TASK_IMPLEMENTATION')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_task_status')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_job_status')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__verify_task_against_plan')
expect(cfg.allowed_tools).toContain('Bash')
expect(cfg.allowed_tools).toContain('Edit')
expect(cfg.allowed_tools).toContain('Write')
})
it('SPRINT_IMPLEMENTATION bevat sprint-specifieke tools maar GEEN job_heartbeat (runner doet die)', () => {
const cfg = getKindDefault('SPRINT_IMPLEMENTATION')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_task_execution')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__verify_sprint_task')
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__job_heartbeat')
})
it('IDEA_GRILL bevat update_idea_grill_md en geen wait_for_job', () => {
const cfg = getKindDefault('IDEA_GRILL')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_idea_grill_md')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__log_idea_decision')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_job_status')
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__wait_for_job')
})
it('IDEA_MAKE_PLAN bevat update_idea_plan_md en geen wait_for_job', () => {
const cfg = getKindDefault('IDEA_MAKE_PLAN')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_idea_plan_md')
expect(cfg.allowed_tools).toContain('mcp__scrum4me__log_idea_decision')
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__wait_for_job')
})
it('alle kinds hebben non-null allowed_tools', () => {
for (const kind of ['IDEA_GRILL', 'IDEA_MAKE_PLAN', 'PLAN_CHAT', 'TASK_IMPLEMENTATION', 'SPRINT_IMPLEMENTATION']) {
const cfg = getKindDefault(kind)
expect(cfg.allowed_tools).not.toBeNull()
expect(Array.isArray(cfg.allowed_tools)).toBe(true)
}
})
})
describe('mapBudgetToEffort', () => {
it.each([
[0, null],
[-1, null],
[1, 'medium'],
[3000, 'medium'],
[6000, 'medium'],
[6001, 'high'],
[9000, 'high'],
[12000, 'high'],
[12001, 'xhigh'],
[18000, 'xhigh'],
[24000, 'xhigh'],
[24001, 'max'],
[50000, 'max'],
[100000, 'max'],
])('budget %i → %s', (budget, expected) => {
expect(mapBudgetToEffort(budget)).toBe(expected)
}) })
}) })

View file

@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest'
import type { ClaudeJobKind } from '@prisma/client'
import { getKindPromptText, getIdeaPromptText } from '../src/lib/kind-prompts.js'
const KINDS: ClaudeJobKind[] = [
'IDEA_GRILL',
'IDEA_MAKE_PLAN',
'TASK_IMPLEMENTATION',
'SPRINT_IMPLEMENTATION',
'PLAN_CHAT',
]
describe('getKindPromptText', () => {
it.each(KINDS)('returnt non-empty content voor %s', (kind) => {
const text = getKindPromptText(kind)
expect(text.length).toBeGreaterThan(0)
})
it('TASK_IMPLEMENTATION-prompt verbiedt wait_for_job', () => {
const text = getKindPromptText('TASK_IMPLEMENTATION')
expect(text).toMatch(/GEEN.*wait_for_job/)
})
it('SPRINT_IMPLEMENTATION-prompt verbiedt job_heartbeat', () => {
const text = getKindPromptText('SPRINT_IMPLEMENTATION')
expect(text).toMatch(/GEEN.*job_heartbeat/)
})
it.each(['TASK_IMPLEMENTATION', 'SPRINT_IMPLEMENTATION', 'PLAN_CHAT'] as const)(
'%s-prompt noemt $PAYLOAD_PATH als variabele',
(kind) => {
const text = getKindPromptText(kind)
expect(text).toContain('$PAYLOAD_PATH')
},
)
})
describe('getIdeaPromptText (back-compat)', () => {
it('returnt content voor IDEA_GRILL', () => {
expect(getIdeaPromptText('IDEA_GRILL').length).toBeGreaterThan(0)
})
it('returnt content voor IDEA_MAKE_PLAN', () => {
expect(getIdeaPromptText('IDEA_MAKE_PLAN').length).toBeGreaterThan(0)
})
it('returnt empty string voor non-idea kind', () => {
expect(getIdeaPromptText('TASK_IMPLEMENTATION')).toBe('')
})
})

View file

@ -1,32 +0,0 @@
// Loader voor embedded idea-prompts (M12).
// De .md-bestanden in src/prompts/idea/ zijn een kopie van
// scrum4me/lib/idea-prompts/* — bewust dupliceren voor reproduceerbaarheid
// op elke worker (geen externe anthropic-skills-plugin-dependency).
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { ClaudeJobKind } from '@prisma/client'
let cached: { grill?: string; makePlan?: string } = {}
function loadPrompt(file: 'grill.md' | 'make-plan.md'): string {
const here = dirname(fileURLToPath(import.meta.url))
// src/lib/idea-prompts.ts → src/lib → src → src/prompts/idea/{file}
const path = join(here, '..', 'prompts', 'idea', file)
return readFileSync(path, 'utf8')
}
export function getIdeaPromptText(kind: ClaudeJobKind): string {
if (kind === 'IDEA_GRILL') {
if (!cached.grill) cached.grill = loadPrompt('grill.md')
return cached.grill
}
if (kind === 'IDEA_MAKE_PLAN') {
if (!cached.makePlan) cached.makePlan = loadPrompt('make-plan.md')
return cached.makePlan
}
// TASK_IMPLEMENTATION en future kinds: geen embedded prompt nodig.
return ''
}

View file

@ -1,10 +1,21 @@
// PBI-67: model + mode-selectie per ClaudeJob-kind. // PBI-67: model + mode-selectie per ClaudeJob-kind.
// //
// Sync met Scrum4Me/lib/job-config.ts — als je hier een veld aanpast,
// doe hetzelfde aan de webapp-kant. Bewust duplicate (geen gedeeld
// package) om de MCP-server eigenstandig te houden.
//
// Override-cascade (eerste match wint): // Override-cascade (eerste match wint):
// 1. task.requires_opus === true → forceer Opus // 1. task.requires_opus === true → forceer Opus
// 2. job.requested_* (snapshot bij enqueue) // 2. job.requested_* (snapshot bij enqueue)
// 3. product.preferred_* // 3. product.preferred_*
// 4. KIND_DEFAULTS hieronder // 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 = export type ClaudeModel =
| 'claude-opus-4-7' | 'claude-opus-4-7'
@ -38,20 +49,52 @@ export type TaskInput = {
requires_opus?: boolean | null 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> = { const KIND_DEFAULTS: Record<string, JobConfig> = {
IDEA_GRILL: { IDEA_GRILL: {
model: 'claude-sonnet-4-6', model: 'claude-sonnet-4-6',
thinking_budget: 12000, thinking_budget: 12000,
permission_mode: 'plan', permission_mode: 'plan',
max_turns: 15, max_turns: 15,
allowed_tools: ['Read', 'Grep', 'Glob', 'WebSearch', 'AskUserQuestion'], 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: { IDEA_MAKE_PLAN: {
model: 'claude-opus-4-7', model: 'claude-opus-4-7',
thinking_budget: 24000, thinking_budget: 24000,
permission_mode: 'plan', permission_mode: 'plan',
max_turns: 20, max_turns: 20,
allowed_tools: ['Read', 'Grep', 'Glob', 'WebSearch', 'AskUserQuestion', 'Write'], 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: { PLAN_CHAT: {
model: 'claude-sonnet-4-6', model: 'claude-sonnet-4-6',
@ -65,14 +108,20 @@ const KIND_DEFAULTS: Record<string, JobConfig> = {
thinking_budget: 6000, thinking_budget: 6000,
permission_mode: 'bypassPermissions', permission_mode: 'bypassPermissions',
max_turns: 50, max_turns: 50,
allowed_tools: null, allowed_tools: TASK_TOOLS,
}, },
SPRINT_IMPLEMENTATION: { SPRINT_IMPLEMENTATION: {
model: 'claude-sonnet-4-6', model: 'claude-sonnet-4-6',
thinking_budget: 6000, thinking_budget: 6000,
permission_mode: 'bypassPermissions', permission_mode: 'bypassPermissions',
max_turns: null, max_turns: null,
allowed_tools: 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',
],
}, },
} }
@ -118,3 +167,20 @@ export function resolveJobConfig(
allowed_tools: base.allowed_tools, 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/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'
}

48
src/lib/kind-prompts.ts Normal file
View file

@ -0,0 +1,48 @@
// Loader voor embedded prompts per ClaudeJob-kind.
//
// De .md-bestanden in src/prompts/<kind>/ worden bewust meegebakken zodat
// elke runner ze kan inlezen zonder externe plugin-dependency. De runner
// (scrum4me-docker/bin/run-one-job.ts) leest de juiste prompt via
// getKindPromptText() en geeft die door als `claude -p`-prompt.
//
// Variabele-vervanging gebeurt door de runner zelf (bv. $PAYLOAD_PATH).
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { ClaudeJobKind } from '@prisma/client'
const cache: Partial<Record<ClaudeJobKind, string>> = {}
function loadPrompt(rel: string): string {
const here = dirname(fileURLToPath(import.meta.url))
// src/lib/kind-prompts.ts → src/lib → src → src/prompts/<rel>
const path = join(here, '..', 'prompts', rel)
return readFileSync(path, 'utf8')
}
const KIND_TO_PROMPT_PATH: Partial<Record<ClaudeJobKind, string>> = {
IDEA_GRILL: 'idea/grill.md',
IDEA_MAKE_PLAN: 'idea/make-plan.md',
TASK_IMPLEMENTATION: 'task/implementation.md',
SPRINT_IMPLEMENTATION: 'sprint/implementation.md',
PLAN_CHAT: 'plan-chat/chat.md',
}
export function getKindPromptText(kind: ClaudeJobKind): string {
if (cache[kind]) return cache[kind]!
const rel = KIND_TO_PROMPT_PATH[kind]
if (!rel) return ''
const text = loadPrompt(rel)
cache[kind] = text
return text
}
// Back-compat re-export. wait-for-job.ts roept getIdeaPromptText aan voor
// de twee idea-kinds; behouden zodat we de bestaande call-site niet hoeven
// te wijzigen tot een aparte cleanup-pass.
export function getIdeaPromptText(kind: ClaudeJobKind): string {
if (kind !== 'IDEA_GRILL' && kind !== 'IDEA_MAKE_PLAN') return ''
return getKindPromptText(kind)
}

View file

@ -0,0 +1,16 @@
# PLAN_CHAT-prompt (placeholder)
> Deze prompt is een placeholder. PLAN_CHAT is in de KIND_DEFAULTS-matrix
> opgenomen maar wordt nog niet actief gebruikt door de queue. Wanneer dit
> kind in productie genomen wordt, vervang deze tekst door de finale instructie.
---
Je bent gestart voor een `PLAN_CHAT`-job. De payload staat in:
```
$PAYLOAD_PATH
```
Lees de payload en doe wat erin staat. Sluit af met
`mcp__scrum4me__update_job_status({ job_id, status: 'done' })`.

View file

@ -0,0 +1,77 @@
# SPRINT_IMPLEMENTATION-prompt
> Deze prompt wordt door `scrum4me-docker/bin/run-one-job.ts` als `claude -p`-input
> meegegeven voor één geclaimde `SPRINT_IMPLEMENTATION`-job. Eén job = de hele
> sprint-run sequentieel afhandelen.
---
Je bent gestart voor één geclaimde `SPRINT_IMPLEMENTATION`-job. De payload bevat
een **frozen scope-snapshot** met alle te verwerken taken:
```
$PAYLOAD_PATH
```
Lees die payload eerst. Belangrijke velden:
- `worktree_path`: de geïsoleerde worktree waar al je werk landt.
- `branch_name`: de feature-branch (bv. `feat/sprint-<id>`); bij PR-strategy
SPRINT zit alle werk in één branch.
- `task_executions[]`: ordered lijst van `SprintTaskExecution`-rijen. Verwerk in
`order`-volgorde. Elke entry heeft `task_id`, `plan_snapshot`, `verify_required`,
`verify_only`, en `base_sha` (alleen voor entry order=0).
- `pbis[]`, `stories[]`: context voor begrip; geen wijzigingen daarop.
- `sprint_run.id`: nodig voor `update_task_status` cascade-prop. Geef altijd
`sprint_run_id` mee aan `update_task_status`.
## Hard regels
- **GEEN** `mcp__scrum4me__wait_for_job` aanroepen. De runner heeft geclaimd.
- **GEEN** `mcp__scrum4me__job_heartbeat` aanroepen. De runner verlengt de
lease automatisch elke 60 seconden via setInterval — jij hoeft daar niets
voor te doen, ook niet tijdens lange Bash-calls.
- Werk uitsluitend in `worktree_path` op `branch_name`. Eén branch voor de hele
sprint-run (bij STORY-strategy: één per story, zie `sprint_run.pr_strategy`).
- Verwerk taken in de exacte `order`-volgorde uit `task_executions[]`.
## Workflow per task_execution
Voor elke entry in `task_executions[]` (in order-volgorde):
1. **Start**: `update_task_execution({ execution_id, status: 'RUNNING' })` en
`update_task_status({ task_id, status: 'in_progress', sprint_run_id })`.
2. **Lees** het `plan_snapshot` uit de execution + de bredere context uit
`task`/`story`/`pbi` in de payload.
3. **Implementeer** de taak in `worktree_path`. Commit per logische laag met
`git add -A && git commit`.
4. **Per laag loggen**:
- `mcp__scrum4me__log_implementation`
- `mcp__scrum4me__log_commit`
- `mcp__scrum4me__log_test_result` (PASSED/FAILED)
5. **Verify-gate** (als `verify_required === true`):
`mcp__scrum4me__verify_sprint_task({ execution_id })`. Bij DIVERGENT: stop de
sprint en sluit af met `update_job_status('failed')`.
6. **Afronden taak**:
- Bij ALIGNED/PARTIAL: `update_task_status({ task_id, status: 'done', sprint_run_id })`
en `update_task_execution({ execution_id, status: 'DONE' })`.
- Bij EMPTY (no-op): `update_task_execution({ execution_id, status: 'SKIPPED' })`
en `update_task_status({ task_id, status: 'done', sprint_run_id })`.
## Sprint afronden
Na de laatste `task_execution`:
- **Verify-gate run**: optioneel een algemene `npm run verify` op de hele worktree.
- **Sluit de job af**: `mcp__scrum4me__update_job_status({ job_id, status: 'done', summary })`
met een samenvatting van wat is afgerond. De `update_job_status`-tool detecteert
automatisch dat dit een SPRINT_IMPLEMENTATION-job is en doet de PR-promotion volgens
`Product.auto_pr` en `sprint_run.pr_strategy`.
Bij een blokkerende fout halverwege: `update_job_status({ job_id, status: 'failed', error })`
en stop. De runner zorgt voor lease-cleanup.
## Vragen aan de gebruiker
Voor blokkerende keuzes: `mcp__scrum4me__ask_user_question` + wacht op antwoord
met `mcp__scrum4me__get_question_answer`. Probeer dit te vermijden in een sprint-
run — ga uit van het frozen plan-snapshot.

View file

@ -0,0 +1,58 @@
# TASK_IMPLEMENTATION-prompt
> Deze prompt wordt door `scrum4me-docker/bin/run-one-job.ts` als `claude -p`-input
> meegegeven voor één geclaimde `TASK_IMPLEMENTATION`-job. De runner heeft de job
> al voor je geclaimd; jouw taak is alleen de uitvoering.
---
Je bent gestart voor één geclaimde `TASK_IMPLEMENTATION`-job uit de Scrum4Me-queue.
De volledige job-payload (inclusief task, story, pbi, sprint, product, config en
worktree_path) staat in:
```
$PAYLOAD_PATH
```
Lees die payload eerst met `Read $PAYLOAD_PATH`. Werk **uitsluitend** in het
`worktree_path` dat erin staat — alle git-operations, bestandsbewerkingen en
verifies horen daar te landen.
## Hard regels
- **GEEN** `mcp__scrum4me__wait_for_job` aanroepen. De runner heeft al voor je
geclaimd. Eén Claude-invocation = één job.
- **GEEN** `mcp__scrum4me__check_queue_empty`. Je sluit af na deze ene job.
- Werk in het toegewezen worktree-pad; geen edits in andere directories.
- Volg `task.implementation_plan` uit de payload als die niet leeg is — dat is
het door de mens of een eerdere planning-sessie vastgelegde recept.
## Workflow
1. **Status op in_progress**: `mcp__scrum4me__update_task_status({ task_id, status: 'in_progress' })`.
2. **Plan lezen**: Lees `task.implementation_plan` uit de payload + relevante
project-docs (`docs/specs/functional.md`, eventueel `docs/patterns/*.md`).
3. **Implementeer** de taak: lees → verander → test → commit per logische laag.
Gebruik `git add -A && git commit` per laag, **geen** `git push`.
4. **Logging per laag**:
- `mcp__scrum4me__log_implementation` met een korte beschrijving van wat je
gewijzigd hebt en waarom.
- `mcp__scrum4me__log_commit` met `commit_hash` en `commit_message` na elke
commit (haal hash uit `git rev-parse HEAD`).
- `mcp__scrum4me__log_test_result` met PASSED/FAILED en uitleg na elke
`npm test` of build-run.
5. **Verify-gate**: roep `mcp__scrum4me__verify_task_against_plan({ task_id })`
aan om de wijzigingen tegen het plan te toetsen.
6. **Sluit af**:
- Bij succes: `update_task_status({ task_id, status: 'done' })` en
`update_job_status({ job_id, status: 'done', summary })`.
- Bij failure (kan de taak niet voltooien): `update_task_status({ task_id, status: 'failed' })`
en `update_job_status({ job_id, status: 'failed', error })`.
- Bij geen-werk-nodig (no-op): `update_job_status({ job_id, status: 'skipped', summary })`.
## Vragen aan de gebruiker
Als je een blokkerende keuze tegenkomt waarvoor je input nodig hebt, gebruik
`mcp__scrum4me__ask_user_question` en wacht op het antwoord met
`mcp__scrum4me__get_question_answer`. Vraag **niet** voor zaken die je zelf
kunt afleiden uit het plan.

View file

@ -446,7 +446,7 @@ export async function tryClaimJob(
return rows.length > 0 ? rows[0].id : null return rows.length > 0 ? rows[0].id : null
} }
async function getFullJobContext(jobId: string) { export async function getFullJobContext(jobId: string) {
const job = await prisma.claudeJob.findUnique({ const job = await prisma.claudeJob.findUnique({
where: { id: jobId }, where: { id: jobId },
include: { include: {
@ -505,7 +505,7 @@ async function getFullJobContext(jobId: string) {
if (job.kind === 'IDEA_GRILL' || job.kind === 'IDEA_MAKE_PLAN') { if (job.kind === 'IDEA_GRILL' || job.kind === 'IDEA_MAKE_PLAN') {
if (!job.idea) return null if (!job.idea) return null
const { idea } = job const { idea } = job
const { getIdeaPromptText } = await import('../lib/idea-prompts.js') const { getIdeaPromptText } = await import('../lib/kind-prompts.js')
// Setup persistent product-worktrees for this idea-job (PBI-9). // Setup persistent product-worktrees for this idea-job (PBI-9).
// Primary product is gated by repo_url via resolveRepoRoot returning null. // Primary product is gated by repo_url via resolveRepoRoot returning null.