From b53d2c1f235579b0a0166e23801eb88a8e37fed3 Mon Sep 17 00:00:00 2001 From: zaridan <1617679+zaridan@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:02:49 -0700 Subject: [PATCH] feat(orchestration): starter recipes + getRecipes registry (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two built-in recipes to the token-free recipe director, alongside #9's implement_then_review: - single_worker_pr: one task, no track/deps → its own worktree → one PR. - repro_fix_verify: three tasks on one shared track, chained repro→fix→verify via dependsOn so the deps form a total order on the track (satisfying the coordinator's same-track ordering guard). Each spec instructs the worker to commit its artifact so the next task in the shared worktree sees it. Add getRecipes(): Recipe[] returning all three built-ins in display order (fresh array so callers can't mutate the registry) for the #11 picker to list. Tests cover both new recipes compiling to the right tasks/tracks/deps, the repro_fix_verify deps forming a strict total order, and getRecipes returning all three. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/lib/recipe-director-recipes.test.ts | 89 ++++++++++++++++++- .../src/lib/recipe-director-recipes.ts | 73 +++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/lib/recipe-director-recipes.test.ts b/src/renderer/src/lib/recipe-director-recipes.test.ts index e568ac1a8aa..bfdf2796827 100644 --- a/src/renderer/src/lib/recipe-director-recipes.test.ts +++ b/src/renderer/src/lib/recipe-director-recipes.test.ts @@ -1,5 +1,12 @@ import { describe, it, expect } from 'vitest' -import { compileRecipe, IMPLEMENT_THEN_REVIEW, type Recipe } from './recipe-director-recipes' +import { + compileRecipe, + getRecipes, + IMPLEMENT_THEN_REVIEW, + REPRO_FIX_VERIFY, + SINGLE_WORKER_PR, + type Recipe +} from './recipe-director-recipes' // Mirror the coordinator's track-hint contract (parseTrackFromSpec): a leading // `track: ` line on its own line. Kept local so this renderer test does not @@ -95,4 +102,84 @@ describe('compileRecipe', () => { } expect(() => compileRecipe(recipe)).toThrow(/duplicate task keys/) }) + + it('compiles single_worker_pr to one task with its own per-task track and no deps', () => { + const compiled = compileRecipe(SINGLE_WORKER_PR) + + expect(compiled.map((t) => t.key)).toEqual(['deliver']) + expect(compiled[0].dependsOn).toEqual([]) + // No explicit track → track defaults to the task key → its own worktree/PR. + expect(trackHintOf(compiled[0].spec)).toBe('deliver') + }) + + it('compiles repro_fix_verify to three same-track tasks chained repro→fix→verify', () => { + const compiled = compileRecipe(REPRO_FIX_VERIFY) + + expect(compiled.map((t) => t.key)).toEqual(['repro', 'fix', 'verify']) + + const [repro, fix, verify] = compiled + expect(repro.dependsOn).toEqual([]) + expect(fix.dependsOn).toEqual(['repro']) + expect(verify.dependsOn).toEqual(['fix']) + + // All three share one track → one worktree, one branch, one PR. + const tracks = compiled.map((t) => trackHintOf(t.spec)) + expect(tracks.every((t) => t !== null)).toBe(true) + expect(new Set(tracks).size).toBe(1) + }) + + it('repro_fix_verify deps form a total order on its single track', () => { + const compiled = compileRecipe(REPRO_FIX_VERIFY) + + // The coordinator refuses same-track tasks that are not totally ordered by + // deps. Verify the chain is a strict total order: each task (after the first) + // transitively depends on every earlier same-track task, with no ties. + const indexByKey = new Map(compiled.map((t, i) => [t.key, i])) + const depsByKey = new Map(compiled.map((t) => [t.key, t.dependsOn])) + + const dependsTransitively = (from: string, on: string): boolean => { + const stack = [...(depsByKey.get(from) ?? [])] + while (stack.length > 0) { + const next = stack.pop()! + if (next === on) { + return true + } + stack.push(...(depsByKey.get(next) ?? [])) + } + return false + } + + // For every ordered pair (earlier, later), the later one must depend on the + // earlier one — that is exactly what "totally ordered by deps" means. + for (let i = 0; i < compiled.length; i++) { + for (let j = i + 1; j < compiled.length; j++) { + const earlier = compiled[i].key + const later = compiled[j].key + expect(dependsTransitively(later, earlier)).toBe(true) + } + } + // Sanity: compile order matches dependency order. + expect(indexByKey.get('repro')).toBeLessThan(indexByKey.get('fix')!) + expect(indexByKey.get('fix')).toBeLessThan(indexByKey.get('verify')!) + }) +}) + +describe('getRecipes', () => { + it('returns all three built-in recipes by name', () => { + const names = getRecipes().map((r) => r.name) + expect(names).toEqual(['implement_then_review', 'single_worker_pr', 'repro_fix_verify']) + }) + + it('exposes a name and a non-empty description per recipe (picker shape)', () => { + for (const recipe of getRecipes()) { + expect(recipe.name.length).toBeGreaterThan(0) + expect(recipe.description.trim().length).toBeGreaterThan(0) + } + }) + + it('returns a fresh array so callers cannot mutate the registry', () => { + const first = getRecipes() + first.pop() + expect(getRecipes()).toHaveLength(3) + }) }) diff --git a/src/renderer/src/lib/recipe-director-recipes.ts b/src/renderer/src/lib/recipe-director-recipes.ts index b562bccb343..b4793b91d06 100644 --- a/src/renderer/src/lib/recipe-director-recipes.ts +++ b/src/renderer/src/lib/recipe-director-recipes.ts @@ -65,6 +65,79 @@ export const IMPLEMENT_THEN_REVIEW: Recipe = { ] } +/** The simplest recipe: a single worker does the whole job and opens a PR. No + * track or deps — its track defaults to the task key, so it gets its own + * worktree/branch and produces exactly one PR. */ +export const SINGLE_WORKER_PR: Recipe = { + name: 'single_worker_pr', + description: 'One worker does the whole job end to end on its own branch and opens a single PR.', + tasks: [ + { + key: 'deliver', + spec: + 'Carry out the requested change from start to finish. Make focused commits, ' + + 'keep the build and tests green, and open a PR for your branch. When done, ' + + 'report what you changed and anything a reviewer should scrutinize.' + } + ] +} + +// Why: repro → fix → verify all share ONE track so they run in the same worktree +// (one branch → one PR), and each builds on the previous one's committed artifact. +// The dependsOn chain (fix waits on repro, verify waits on fix) is a TOTAL order on +// the track — which the coordinator's same-track guard requires (it refuses +// same-track tasks not totally ordered by deps, since they would race one checkout). +const REPRO_FIX_VERIFY_TRACK = 'repro-fix-verify' + +/** Bug-fix workflow as a single-track dependency chain: reproduce with a failing + * test, fix until it passes, then independently verify. Each step commits its + * artifact so the next step (same worktree) sees it. */ +export const REPRO_FIX_VERIFY: Recipe = { + name: 'repro_fix_verify', + description: + 'Reproduce the bug with a failing test, fix it, then verify — one worktree, one PR, ' + + 'each step chained after the last so they share the same branch in order.', + tasks: [ + { + key: 'repro', + track: REPRO_FIX_VERIFY_TRACK, + spec: + 'Reproduce the reported bug by writing a failing test (or a minimal repro) that ' + + 'captures it. Commit the failing test so the next step sees it on this branch. ' + + 'Report exactly how the bug manifests and what the test asserts.' + }, + { + key: 'fix', + track: REPRO_FIX_VERIFY_TRACK, + dependsOn: ['repro'], + spec: + 'Make the failing test from the previous step pass with the smallest correct ' + + 'change. Keep the rest of the build and tests green. Commit the fix so the verify ' + + 'step sees it on this branch, and report what you changed and why.' + }, + { + key: 'verify', + track: REPRO_FIX_VERIFY_TRACK, + dependsOn: ['fix'], + spec: + 'Independently verify the fix on this branch: run the full test suite, confirm the ' + + 'previously failing test now passes, and check for regressions or missed edge ' + + 'cases. Commit any follow-up test or fixup, then confirm the PR is ready (or say ' + + 'why not).' + } + ] +} + +/** Every built-in recipe, keyed by name. The picker (#11) lists these; the launch + * path compiles the selected one. Order is the intended display order. */ +const BUILT_IN_RECIPES: Recipe[] = [IMPLEMENT_THEN_REVIEW, SINGLE_WORKER_PR, REPRO_FIX_VERIFY] + +/** All built-in recipes, in display order. Returns a fresh array so callers can + * sort/filter without mutating the registry. */ +export function getRecipes(): Recipe[] { + return [...BUILT_IN_RECIPES] +} + /** A recipe task lowered to the inputs `orchestration.taskCreate` needs, minus the * resolved dependency ids (those exist only after each create returns, so the * launch path resolves `dependsOn` keys → ids as it goes). */