Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion src/renderer/src/lib/recipe-director-recipes.test.ts
Original file line number Diff line number Diff line change
@@ -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: <key>` line on its own line. Kept local so this renderer test does not
Expand Down Expand Up @@ -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)
})
})
73 changes: 73 additions & 0 deletions src/renderer/src/lib/recipe-director-recipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down