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:
parent
2fbb36bdbe
commit
96f5b0dd03
9 changed files with 391 additions and 41 deletions
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
48
__tests__/kind-prompts.test.ts
Normal file
48
__tests__/kind-prompts.test.ts
Normal 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('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -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 ''
|
|
||||||
}
|
|
||||||
|
|
@ -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
48
src/lib/kind-prompts.ts
Normal 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)
|
||||||
|
}
|
||||||
16
src/prompts/plan-chat/chat.md
Normal file
16
src/prompts/plan-chat/chat.md
Normal 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' })`.
|
||||||
77
src/prompts/sprint/implementation.md
Normal file
77
src/prompts/sprint/implementation.md
Normal 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.
|
||||||
58
src/prompts/task/implementation.md
Normal file
58
src/prompts/task/implementation.md
Normal 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.
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue