diff --git a/__tests__/job-config.test.ts b/__tests__/job-config.test.ts index 3a7af58..bef0de1 100644 --- a/__tests__/job-config.test.ts +++ b/__tests__/job-config.test.ts @@ -1,5 +1,5 @@ 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 = { 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') }) - 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( { kind: 'IDEA_GRILL', requested_model: 'claude-haiku-4-5-20251001' }, { preferred_model: 'claude-sonnet-4-6' }, ) 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) }) }) diff --git a/__tests__/kind-prompts.test.ts b/__tests__/kind-prompts.test.ts new file mode 100644 index 0000000..6dbb9d2 --- /dev/null +++ b/__tests__/kind-prompts.test.ts @@ -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('') + }) +}) diff --git a/src/lib/idea-prompts.ts b/src/lib/idea-prompts.ts deleted file mode 100644 index bcc8873..0000000 --- a/src/lib/idea-prompts.ts +++ /dev/null @@ -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 '' -} diff --git a/src/lib/job-config.ts b/src/lib/job-config.ts index c615de1..1c77915 100644 --- a/src/lib/job-config.ts +++ b/src/lib/job-config.ts @@ -1,10 +1,21 @@ // 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): // 1. task.requires_opus === true → forceer Opus // 2. job.requested_* (snapshot bij enqueue) // 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' @@ -38,20 +49,52 @@ 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_GRILL: { model: 'claude-sonnet-4-6', thinking_budget: 12000, permission_mode: 'plan', 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: { model: 'claude-opus-4-7', thinking_budget: 24000, permission_mode: 'plan', 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: { model: 'claude-sonnet-4-6', @@ -65,14 +108,20 @@ const KIND_DEFAULTS: Record = { thinking_budget: 6000, permission_mode: 'bypassPermissions', max_turns: 50, - allowed_tools: null, + allowed_tools: TASK_TOOLS, }, SPRINT_IMPLEMENTATION: { model: 'claude-sonnet-4-6', thinking_budget: 6000, permission_mode: 'bypassPermissions', 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, } } + +// 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' +} diff --git a/src/lib/kind-prompts.ts b/src/lib/kind-prompts.ts new file mode 100644 index 0000000..f7e03c1 --- /dev/null +++ b/src/lib/kind-prompts.ts @@ -0,0 +1,48 @@ +// Loader voor embedded prompts per ClaudeJob-kind. +// +// De .md-bestanden in src/prompts// 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> = {} + +function loadPrompt(rel: string): string { + const here = dirname(fileURLToPath(import.meta.url)) + // src/lib/kind-prompts.ts → src/lib → src → src/prompts/ + const path = join(here, '..', 'prompts', rel) + return readFileSync(path, 'utf8') +} + +const KIND_TO_PROMPT_PATH: Partial> = { + 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) +} diff --git a/src/prompts/plan-chat/chat.md b/src/prompts/plan-chat/chat.md new file mode 100644 index 0000000..224d51e --- /dev/null +++ b/src/prompts/plan-chat/chat.md @@ -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' })`. diff --git a/src/prompts/sprint/implementation.md b/src/prompts/sprint/implementation.md new file mode 100644 index 0000000..9089f8a --- /dev/null +++ b/src/prompts/sprint/implementation.md @@ -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-`); 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. diff --git a/src/prompts/task/implementation.md b/src/prompts/task/implementation.md new file mode 100644 index 0000000..fa408ee --- /dev/null +++ b/src/prompts/task/implementation.md @@ -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. diff --git a/src/tools/wait-for-job.ts b/src/tools/wait-for-job.ts index 99a8090..c8af6f4 100644 --- a/src/tools/wait-for-job.ts +++ b/src/tools/wait-for-job.ts @@ -446,7 +446,7 @@ export async function tryClaimJob( 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({ where: { id: jobId }, include: { @@ -505,7 +505,7 @@ async function getFullJobContext(jobId: string) { if (job.kind === 'IDEA_GRILL' || job.kind === 'IDEA_MAKE_PLAN') { if (!job.idea) return null 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). // Primary product is gated by repo_url via resolveRepoRoot returning null.