import { IssuesPort, ScmPort, CiPort, DeployPort, NotifyPort } from '@lonca/baron-core'; import { KnowledgeLoop } from '@lonca/baron-knowledge-loop'; /** * The primitive operations a recipe step may invoke, mapped 1:1 onto the issues/scm port methods. * Centralized so step `do:` values are not magic strings and the engine dispatch stays exhaustive. */ declare const RECIPE_OPS: { readonly issueCreate: "issue.create"; readonly issueGet: "issue.get"; readonly issueUpdate: "issue.update"; readonly issueTransition: "issue.transition"; readonly issueReconcile: "issue.reconcile"; readonly issueClassify: "issue.classify"; readonly issueBlock: "issue.block"; readonly issueUnblock: "issue.unblock"; readonly issueComment: "issue.comment"; readonly issueLink: "issue.link"; readonly issueAssign: "issue.assign"; readonly issueWhoami: "issue.whoami"; readonly issueIterations: "issue.iterations"; readonly issueSetIteration: "issue.set-iteration"; readonly issueQuery: "issue.query"; readonly issueTrace: "issue.trace"; readonly scmBranchCreate: "scm.branch.create"; readonly scmPrCreate: "scm.pr.create"; readonly scmPrThread: "scm.pr.thread"; readonly scmPrStatus: "scm.pr.status"; readonly scmPrFind: "scm.pr.find"; readonly scmPrReady: "scm.pr.ready"; readonly scmPrMerge: "scm.pr.merge"; readonly ciRunTrigger: "ci.run.trigger"; readonly ciRunCancel: "ci.run.cancel"; readonly deployDeployments: "deploy.deployments"; readonly notifySend: "notify.send"; readonly learningAppend: "learning.append"; readonly learningQuery: "learning.query"; readonly followupAppend: "followup.append"; readonly followupList: "followup.list"; }; type RecipeOp = (typeof RECIPE_OPS)[keyof typeof RECIPE_OPS]; declare function isRecipeOp(value: string): value is RecipeOp; declare const ASK_TYPES: readonly ["text", "confirm", "choice"]; type AskType = (typeof ASK_TYPES)[number]; /** A typed prompt for human input (decision #7); rendered per harness by the {@link RecipeAsker}. */ interface AskSpec { /** Context variable the answer is bound to. */ readonly as: string; readonly type: AskType; readonly message: string; /** Allowed values for `type: choice`. */ readonly choices?: readonly string[]; /** When true, a `text` ask may be skipped (yields undefined). */ readonly optional?: boolean; } interface AskStep { readonly ask: AskSpec; } /** * A declarative condition over the run context. Exactly ONE key; operands are interpolated before * evaluation. Deliberately not an expression language: four primitive tests cover the reference * guards (refuse-if-closed, refuse-containers, skip-when-PR-exists) without opening a parser * attack/maintenance surface. */ interface StepCondition { /** True when the interpolated value is present and not ''/false/null. */ readonly truthy?: string; /** True when the interpolated value is absent, '', false, or null. */ readonly falsy?: string; /** True when both interpolated operands are equal (string comparison). */ readonly equals?: readonly [string, string]; /** True when the interpolated operands differ (string comparison). */ readonly notEquals?: readonly [string, string]; } /** A guard: when the condition is false the run STOPS with the (interpolated) message. */ interface RequireStep { readonly require: StepCondition & { readonly message: string; }; /** * Enforce the guard only when this condition holds; otherwise the guard is skipped (no stop). * Lets a guard be conditional — e.g. "require takeover WHEN the item is already assigned". */ readonly when?: StepCondition; } interface DoStep { readonly do: RecipeOp; /** Step parameters; string values may contain `${path}` references into the run context. */ readonly with?: Record; /** Context variable the step result is bound to. */ readonly as?: string; /** Run the step only when the condition holds; otherwise it is skipped (its `as` stays unset). */ readonly when?: StepCondition; } interface MessageStep { readonly message: string; /** Emit the message only when the condition holds. */ readonly when?: StepCondition; } /** * Run nested steps once per element of a list. * * The grammar was `ask` / `do` / `require` / `message`, every one of them single-shot, so a workflow * that sweeps N items could not be expressed at all — which is why the one sweeping workflow Baron * ships (task-sync) lived as prose in a skill. On an install that sets `mutations.channel` to * `recipe-only` that prose cannot even run: each fix it prescribes is refused, and the refusal tells * the caller to find the recipe that covers it, which did not exist. Iteration is what closes that. * * Bindings made inside an iteration are scoped to it. Leaking them would mean the last element * silently wins, and a recipe reading `${pr.id}` after a loop would get whichever item happened to * be last — a bug that looks like data. */ interface ForEachStep { /** Interpolated reference to the list to walk. Anything but an array is an error, not an empty run. */ readonly for_each: string; /** Context name the current element is bound to, for the duration of one iteration. */ readonly as: string; readonly steps: readonly Step[]; /** * Accumulate one value per iteration into an array bound AFTER the loop — how a sweep reports what * it touched. Iterations where the expression resolves to nothing contribute nothing, so "the ones * that matched" falls out of the same mechanism rather than needing a second concept. */ readonly collect?: { readonly as: string; readonly from: string; /** * Collect only from iterations where this holds, evaluated in the ITERATION's scope so it can * read what the nested steps just bound. Without it a sweep cannot say "the ones that matched": * the value it wants to record (the item) and the value that decides whether it matched (what * the provider answered) are different expressions, and no conditional exists inside one. */ readonly when?: StepCondition; }; /** Skip the whole loop unless the condition holds. */ readonly when?: StepCondition; } type Step = AskStep | DoStep | ForEachStep | MessageStep | RequireStep; interface Recipe { readonly name: string; readonly description?: string; readonly steps: readonly Step[]; } /** A declared input a recipe gathers via an `ask` step — surfaced so a caller can collect them upfront. */ interface RecipeInput { readonly name: string; readonly message: string; readonly type: AskType; readonly optional: boolean; readonly choices?: readonly string[]; } /** The inputs a recipe's `ask` steps gather, in order — used to drive non-interactive runs. */ declare function recipeInputs(recipe: Recipe): RecipeInput[]; declare function isAskStep(step: Step): step is AskStep; declare function isDoStep(step: Step): step is DoStep; declare function isMessageStep(step: Step): step is MessageStep; declare function isForEachStep(step: Step): step is ForEachStep; declare function isRequireStep(step: Step): step is RequireStep; /** * Validate an untrusted object (typically `YAML.parse` of a recipe file) into a typed {@link Recipe}. * Throws {@link BaronError} (`RECIPE_PARSE`) with an actionable, pathed message on any violation. */ declare function parseRecipe(raw: unknown): Recipe; /** Parse a recipe from YAML text. */ declare function loadRecipe(yamlText: string): Recipe; type RecipeContext = Record; /** * Replace `${path}` references in a value against the run context. A string that is exactly a single * `${path}` yields the raw resolved value (preserving non-string types and `undefined`, so an * optional `parentId: ${parent}` becomes undefined rather than the literal "undefined"); strings * with embedded references are interpolated to text. Arrays/objects are walked recursively. */ declare function interpolate(value: unknown, context: RecipeContext): unknown; /** * The typed human-input surface a recipe's `ask` steps render through (decision #7). Each harness * provides its own: the CLI uses stdin prompts; tests use a scripted answerer. Kept separate from * the engine so recipes run without any terminal. */ interface RecipeAsker { /** Free-text answer; may resolve to undefined when the ask is optional. */ text(message: string, optional: boolean): Promise; confirm(message: string): Promise; /** A single choice from the allowed set. */ choice(message: string, choices: readonly string[]): Promise; /** Surface an informational line (recipe `message` steps). */ note(message: string): void; } /** * The run journal: what makes a recipe that failed halfway safe to run again. * * `ship` and `task-finish` mutate several providers in sequence. A failure in the middle leaves the * world half-changed, and a plain re-run repeats the steps that already succeeded — the concrete * case is a second pull request. So every run appends one line per event to * `.baron/runs/.jsonl`: the inputs it started with, each answer, and each `do` step with an * idempotency key and the result it bound. Resuming replays the completed steps from the journal * instead of executing them, and carries on from the first one that has no entry. * * Pure append. A journal is never rewritten, so a crash while writing loses at most the last line. */ /** Where a project's journals live, relative to its root. Gitignored: it holds a run's answers. */ declare const RUNS_DIR_REL = ".baron/runs"; declare const RUN_JOURNAL_EXT = ".jsonl"; declare const RUN_NOT_FOUND = "RUN_NOT_FOUND"; declare const RUN_RECIPE_CHANGED = "RUN_RECIPE_CHANGED"; declare const RUN_JOURNAL_CORRUPT = "RUN_JOURNAL_CORRUPT"; type JournalEntry = { readonly kind: 'start'; readonly at: string; readonly recipe: string; /** * How the harness that started the run referred to the recipe — a path, for the CLI — so it * can load the same one again. Absent when the name is the whole reference. */ readonly source?: string; /** {@link recipeFingerprint} of the recipe as parsed, so a changed recipe refuses to resume. */ readonly fingerprint: string; readonly inputs: Readonly>; } | { readonly kind: 'resume'; readonly at: string; } | { readonly kind: 'ask'; readonly at: string; readonly as: string; readonly value: unknown; } | { readonly kind: 'do'; readonly at: string; /** Where in the recipe: top-level index, with for_each iterations as `3[1]/0`. */ readonly path: string; readonly op: string; /** {@link stepKey}: the same step with the same parameters in the same run has the same key. */ readonly key: string; readonly as?: string; readonly result?: unknown; } | { readonly kind: 'note'; readonly at: string; readonly text: string; } | { readonly kind: 'error'; readonly at: string; readonly path?: string; readonly op?: string; readonly code?: string; readonly message: string; } | { readonly kind: 'end'; readonly at: string; readonly replayed: number; }; interface RunJournalStore { append(runId: string, entry: JournalEntry): void; /** Every entry of a run in order, or undefined when no such run was ever started. */ read(runId: string): readonly JournalEntry[] | undefined; } /** Short, sortable, safe in a file name: a time prefix (base 36) and eight random hex digits. */ declare function newRunId(now?: number): string; /** JSON with object keys sorted at every level, so equal parameters always serialize equally. */ declare function canonicalJson(value: unknown): string; /** Identifies the recipe's instructions, not its file: whitespace and comments do not count. */ declare function recipeFingerprint(recipe: Recipe): string; /** * The idempotency key of one `do` step in one run: the run, the step's position, its op and its * fully interpolated parameters. A resumed run recomputes it from the restored context, so a step * whose parameters would now differ (a re-answered ask, an upstream result that changed) gets a new * key and runs again rather than reusing a result produced under other conditions. */ declare function stepKey(runId: string, path: string, op: string, params: unknown): string; /** The path of a journal file. */ declare function runJournalPath(root: string, runId: string): string; /** The two file operations a journal needs, so a harness with its own file system can supply them. */ interface RunJournalFiles { read(path: string): string | undefined; append(path: string, text: string): void; } /** Journals under `/.baron/runs/`, one file per run. */ declare function createFileRunJournal(root: string, files?: RunJournalFiles): RunJournalStore; /** An in-memory journal, for tests and for a harness that keeps its own store. */ declare function createMemoryRunJournal(): RunJournalStore & { readonly runs: ReadonlyMap; }; interface RecipePorts { readonly issues?: IssuesPort; readonly scm?: ScmPort; readonly ci?: CiPort; readonly deploy?: DeployPort; readonly notify?: NotifyPort; readonly knowledge?: KnowledgeLoop; } /** Journal this run, and optionally continue one that stopped. */ interface RunJournalOptions { readonly id: string; readonly journal: RunJournalStore; /** How the caller referred to the recipe (a path, say), kept so a resume can load the same one. */ readonly source?: string | undefined; /** * Continue run `id` from its journal: inputs and answers are restored, every `do` step whose key * the journal holds is replayed from it rather than executed, and the recipe must be the one the * run started with (RUN_RECIPE_CHANGED otherwise). */ readonly resume?: boolean; } interface RunRecipeOptions { readonly ports: RecipePorts; readonly asker: RecipeAsker; /** Pre-seed context variables; an `ask` whose variable is already set is skipped. */ readonly inputs?: RecipeContext; /** Absent: the run leaves no journal and cannot be resumed (tests, embedded callers). */ readonly run?: RunJournalOptions; } interface RunRecipeResult { /** Final run context: seeded inputs + each `ask`/`do` step's bound variable. */ readonly context: RecipeContext; /** The journal this run wrote, when it wrote one — what `resume` takes. */ readonly runId?: string; /** How many `do` steps a resumed run took from the journal instead of executing. */ readonly replayed?: number; /** * Every `message` step the run emitted, in order. * * Collected here rather than left to the asker because the asker a caller supplies decides * whether anyone hears it, and the non-interactive one used by the MCP service discards notes * entirely — which silently threw away task-land's warning that it could not verify the checks, * on the one path every skill actually uses. A recipe's own words about what it just did belong * in its result. */ readonly notes: readonly string[]; } /** * Execute a recipe step by step against the injected ports, threading a context: `ask` steps gather * typed human input (skipped when pre-seeded), `do` steps call a primitive and bind its result, * `message` steps surface a line, `require` steps are engine-enforced guards (decision #19: the * rules live in the engine, not in agent judgement), and a `when:` condition skips a do/message * step. All workflow opinion lives in the recipe; this engine is pure mechanism (invariant #3) and * does no role/native translation (that stays in the ports, #4). */ declare function runRecipe(recipe: Recipe, options: RunRecipeOptions): Promise; /** The code a provider's own error (not a BaronError) is reported under when a step throws it. */ declare const RECIPE_STEP_FAILED = "RECIPE_STEP_FAILED"; /** The recipes Baron ships out of the box, runnable by name (no file path). */ declare const BUILTIN_RECIPE_NAMES: readonly ["task-new", "task-start", "task-move", "task-finish", "task-land", "task-sync-report", "task-reconcile", "ship"]; type BuiltinRecipeName = (typeof BUILTIN_RECIPE_NAMES)[number]; declare function isBuiltinRecipe(name: string): name is BuiltinRecipeName; /** Raw YAML of a built-in recipe (the canonical file — no inlined copy, so it can never drift). */ declare function loadBuiltinRecipeText(name: BuiltinRecipeName): string; declare function loadBuiltinRecipe(name: BuiltinRecipeName): Recipe; interface RecipeSummary { readonly name: string; readonly description?: string; readonly inputs: RecipeInput[]; } /** * Runs Baron's declarative recipes deterministically (the engine enforces order/rules, not the agent), * with inputs supplied upfront — so a workflow is one atomic, rule-enforced call. Resolves built-in * recipes by name plus any project recipes under `/.baron/recipes/*.yaml`. */ interface RecipeService { list(): RecipeSummary[]; /** * The full run result, not just its context: a recipe's `message` steps are how it explains what * it did and what it could not verify, and this service's asker deliberately has nowhere to print * them. Dropping them here is what silently swallowed task-land's "could not verify the checks" * warning on the MCP path. */ run(name: string, inputs: Record): Promise; /** * Continue a run that stopped: same recipe, inputs and answers restored from its journal, every * step it completed replayed rather than executed. `RUN_NOT_FOUND` for an id with no journal or * one that already finished; `RUN_RECIPE_CHANGED` when the recipe is no longer what it ran. */ resume(runId: string): Promise; } interface RecipeServiceOptions { /** * Where runs are journaled. A harness that owns a project root passes `createFileRunJournal(root)` * so a run outlives the process; the default keeps journals in memory, resumable only for as long * as this service lives — enough for tests and embedded callers, and it writes nothing to disk. */ readonly journal?: RunJournalStore; } /** * Resolve a recipe BY NAME: a built-in, else a project recipe under `.baron/recipes/`. * * Exported so the CLI and the MCP server resolve identically. They used not to — only the MCP path * could resolve a name, so a name that worked for an agent was `RECIPE_NOT_FOUND` on the command * line, and the README's quick start told people to run one. Two resolvers is one more than the * number of ways a name should mean something. */ declare function resolveRecipeByName(name: string, root: string): Recipe; declare function createRecipeService(ports: RecipePorts, root: string, options?: RecipeServiceOptions): RecipeService; export { ASK_TYPES, type AskSpec, type AskStep, type AskType, BUILTIN_RECIPE_NAMES, type BuiltinRecipeName, type DoStep, type ForEachStep, type JournalEntry, type MessageStep, RECIPE_OPS, RECIPE_STEP_FAILED, RUNS_DIR_REL, RUN_JOURNAL_CORRUPT, RUN_JOURNAL_EXT, RUN_NOT_FOUND, RUN_RECIPE_CHANGED, type Recipe, type RecipeAsker, type RecipeContext, type RecipeInput, type RecipeOp, type RecipePorts, type RecipeService, type RecipeServiceOptions, type RecipeSummary, type RequireStep, type RunJournalFiles, type RunJournalOptions, type RunJournalStore, type RunRecipeOptions, type RunRecipeResult, type Step, type StepCondition, canonicalJson, createFileRunJournal, createMemoryRunJournal, createRecipeService, interpolate, isAskStep, isBuiltinRecipe, isDoStep, isForEachStep, isMessageStep, isRecipeOp, isRequireStep, loadBuiltinRecipe, loadBuiltinRecipeText, loadRecipe, newRunId, parseRecipe, recipeFingerprint, recipeInputs, resolveRecipeByName, runJournalPath, runRecipe, stepKey };