Files
oh-my-pi/docs/tools/recipe.md
T
2026-05-11 00:38:35 +02:00

12 KiB

recipe

Run a task exposed by a detected project task runner.

Source

  • Entry: packages/coding-agent/src/tools/recipe/index.ts
  • Model-facing prompt: packages/coding-agent/src/prompts/tools/recipe.md
  • Key collaborators:
    • packages/coding-agent/src/tools/recipe/runner.ts — op parsing, task resolution, prompt model.
    • packages/coding-agent/src/tools/recipe/render.ts — shell-style call/result rendering.
    • packages/coding-agent/src/tools/recipe/runners/index.ts — runner registration order.
    • packages/coding-agent/src/tools/recipe/runners/just.ts — detect just recipes from justfiles.
    • packages/coding-agent/src/tools/recipe/runners/pkg.ts — detect package.json scripts and workspaces.
    • packages/coding-agent/src/tools/recipe/runners/cargo.ts — detect Cargo run/test targets.
    • packages/coding-agent/src/tools/recipe/runners/make.ts — parse make targets from makefiles.
    • packages/coding-agent/src/tools/recipe/runners/task.ts — detect Taskfile tasks via task --list-all.
    • packages/coding-agent/src/tools/bash.ts — actual command execution, truncation, cwd/env handling.

Inputs

Field Type Required Description
op string Yes Single string containing the task selector plus trailing arguments. The first whitespace-delimited token selects the task; the remainder is appended verbatim to the resolved runner command. Examples from schema/prompt: test, build --release, pkg-a/test, crate/bin/server, pkg:test --watch.

op grammar

op          := S* head (S+ tail)?
head        := explicit-runner / implicit-task
explicit-runner := runner-id ":" task-token
implicit-task   := task-token
runner-id   := detected runner id (`just` | `pkg` | `cargo` | `make` | `task`)
task-token  := first non-whitespace token; may contain `/`
tail        := remaining characters after the first whitespace run

Resolution rules from resolveRunnerAndTask():

  • Leading whitespace is ignored; an empty op throws ToolError with the available task list.
  • Only the first token is parsed structurally. Everything after the first whitespace run becomes tail and is appended to the command unchanged.
  • If head contains : and the prefix matches a detected runner id, the suffix must exactly match a task in that runner.
  • Otherwise head is treated as a task name and matched across all detected runners.
  • If exactly one runner has that task, it is used.
  • If multiple runners have that task, the call is rejected and the error tells the model to use <runner-id>:<task>.
  • Namespaced task names generated by runners use /, not :. / is part of the task name, not a parser separator.

Outputs

  • Delegates directly to BashTool.execute() and returns the same AgentToolResult<BashToolDetails> shape.
  • Success path: one text content block containing merged command output (result.output from bash execution, or (no output)), plus any timeout clamp notice appended after a blank line.
  • Recipe does not return separate stdout, stderr, or exitCode fields. stdout/stderr are already merged into the text block by bash execution; exitCode is only observed indirectly (success requires 0, non-zero becomes an error).
  • Error path: throws ToolError; for non-zero exits the message is the merged output followed by Command exited with code <n>.
  • details may include:
    • timeoutSeconds: effective timeout used by bash.
    • requestedTimeoutSeconds: only when bash clamped a requested timeout; recipe never sets one itself.
    • meta: output truncation metadata from bash execution.
    • async: defined by bash background execution paths, but recipe does not expose an async input.
  • When bash output is truncated, the full text is stored in an artifact and referenced via bash truncation metadata.
  • Call/result rendering in the TUI uses bash shell rendering with a resolved title, command preview, and optional task cwd.

Flow

  1. RecipeTool.createIf() in packages/coding-agent/src/tools/recipe/index.ts checks session.settings.get("recipe.enabled"); disabled returns null.
  2. It probes every runner in RUNNERS from packages/coding-agent/src/tools/recipe/runners/index.ts with Promise.all(...) in this order: just, pkg, cargo, make, task.
  3. Each runner returns either null or a DetectedRunner { id, label, commandPrefix, tasks }; runners with zero tasks are discarded.
  4. If no runners remain, the tool is not registered.
  5. Constructor stores detected runners, instantiates BashTool, renders the model-facing description by passing buildPromptModel(runners) into packages/coding-agent/src/prompts/tools/recipe.md, and builds shell renderers from createRecipeToolRenderer().
  6. On execution, RecipeTool.execute() calls resolveCommand(op, this.#runners).
  7. resolveCommand() in packages/coding-agent/src/tools/recipe/runner.ts:
    1. parseOp() trims only leading whitespace, extracts the first non-whitespace token as head, and keeps the remainder as tail.
    2. resolveRunnerAndTask() resolves head either as runnerId:taskName or as an unqualified task name.
    3. It throws ToolError for empty ops, missing explicit tasks, ambiguous task names, or unknown tasks; all error variants include the available task list.
    4. It builds the final shell command with buildCommand(commandPrefix, commandName, tail), joining non-empty parts with spaces.
    5. If the task defines cwd, that relative path is returned alongside the command.
  8. RecipeTool.execute() forwards { command, cwd } into BashTool.execute(); recipe does not pass timeout, env, async, or pty options.
  9. BashTool.execute() resolves internal URLs, validates/normalizes cwd against session.cwd, clamps timeout, applies bash interception rules, runs the command, and formats the final result.

Modes / Variants

  • Tool enablement:
    • Disabled by recipe.enabled setting: tool is absent.
    • Enabled but no detected tasks: tool is absent.
  • Task selection:
    • Unqualified task name: succeeds only when exactly one detected runner owns that task.
    • Explicit runner-qualified task: <runner-id>:<task>.
  • Runner detection paths:
    • just: requires just on PATH, a justfile, and successful just --dump --dump-format=json.
    • pkg: requires a readable root package.json; picks a package manager command from lockfiles or bun availability; discovers root scripts and workspace package scripts.
    • cargo: requires cargo on PATH, Cargo.toml, and successful cargo metadata --no-deps --format-version=1.
    • make: requires make on PATH and a makefile; parses targets statically.
    • task: requires task on PATH, a Taskfile, and successful task --list-all --json.
  • Execution path:
    • Always the synchronous bash call surface from recipe inputs.
    • Bash may still auto-background long-running work if bash.autoBackground.enabled and session async job support are enabled.

Side Effects

  • Filesystem
    • Reads manifests from the session cwd during detection: justfiles, package.json, workspace package.json files, Cargo.toml, makefiles, Taskfile.yml / Taskfile.yaml.
    • Command execution runs in session.cwd or a task-specific relative cwd resolved under it.
    • Bash may allocate output artifacts for truncated command output.
  • Subprocesses / native bindings
    • Detection may spawn just --dump --dump-format=json, cargo metadata --no-deps --format-version=1, and task --list-all --json.
    • Execution spawns the resolved shell command through BashTool / executeBash().
  • Session state (transcript, memory, jobs, checkpoints, registries)
    • Tool availability depends on session settings.
    • Constructor prompt text is specialized to detected runners/tasks.
    • Bash execution may create async job records and output artifacts if bash auto-background triggers.
  • User-visible prompts / interactive UI
    • The model-facing tool description lists detected runners and up to 20 tasks per runner.
    • TUI rendering shows a shell-style preview using the resolved title/command/cwd.
  • Background work / cancellation
    • Detection is parallelized across runners.
    • Runtime command execution honors the passed abort signal through BashTool.

Limits & Caps

  • Prompt task listing is capped at PROMPT_TASK_LIMIT = 20 per runner in packages/coding-agent/src/tools/recipe/runner.ts; this affects the rendered tool description, not execution.
  • Recipe itself defines no timeout input; delegated bash execution therefore uses bash's default timeout = 300 seconds from packages/coding-agent/src/tools/bash.ts.
  • Bash clamps timeouts to the configured bash range (clampTimeout("bash", ...) in packages/coding-agent/src/tools/bash.ts), but recipe cannot request a custom value.
  • pkg workspace discovery normalizes workspace globs to .../package.json and sorts matched package files lexicographically before task generation.
  • cargo deduplicates generated task names with a Set, so duplicate targets collapse to one recipe task.

Errors

  • Detection failures in runner modules are mostly soft-failed:
    • Missing binaries, missing manifests, parse failures, or non-zero probe exits usually return null and log with logger.debug(...).
    • Result: the affected runner disappears instead of surfacing an error to the model.
  • Invocation failures are hard errors from resolveRunnerAndTask():
    • Empty op.
    • Explicit runner prefix with missing/empty task.
    • Ambiguous unqualified task name across runners.
    • Unknown task name.
  • Execution failures come from BashTool.execute():
    • Invalid cwd.
    • Bash interceptor blocks.
    • Aborts/timeouts.
    • Non-zero exit codes.
    • Missing exit status.
  • All resolveRunnerAndTask() errors include the current available task list to help the model retry.

Notes

  • RecipeTool sets concurrency = "exclusive"; calls do not run concurrently with other exclusive tools.
  • Tool registration is all-or-nothing per runner: a detected runner with zero tasks is dropped.
  • Runner ids are fixed string literals from the runner modules: just, pkg, cargo, make, task.
  • buildPromptModel() includes each task's rendered command (commandPrefix + commandName) and relative cwd when present; the prompt therefore exposes the exact shell form recipe will run.
  • pkg task names:
    • Root package.json scripts keep bare names like test.
    • Workspace scripts are always namespaced as <package-name-or-dir>/<script> and set cwd to that package directory.
    • Script names are shell-quoted into commandName, so a task like build becomes bun run 'build' / npm run 'build' / similar.
  • pkg command prefix selection prefers lockfiles in this order: bun.lock/bun.lockb, pnpm-lock.yaml, yarn.lock, package-lock.json/npm-shrinkwrap.json; otherwise it falls back to bun run if bun exists, else npm run.
  • cargo task names are generated from metadata targets:
    • Single-package manifests: bin/<name>, example/<name>, test/<name>.
    • Multi-package workspaces: <package>/bin/<name>, <package>/example/<name>, <package>/test/<name>.
    • Each task overrides commandPrefix to the full cargo run ... --bin|--example or cargo test ... --test prefix, and commandName to the quoted target name.
  • make target parsing is static text parsing, not make -qp output:
    • Recognizes makefiles named Makefile, makefile, GNUmakefile.
    • Uses .PHONY lines to decide whether to include undocumented file targets; without any .PHONY, all parsed targets are exposed.
    • If .PHONY exists, documented non-phony targets are kept with (file target) appended to doc.
  • just detection ignores private recipes and preserves declared parameter names only for prompt display; execution still accepts arbitrary tail text.
  • task detection uses desc first, then summary, for task documentation.
  • Recipe has no env input of its own. Commands inherit whatever environment BashTool supplies for normal bash execution in the session.