Skip to content

Latest commit

 

History

History

README.md

@generata/core

Composable multi-agent pipelines on top of the Claude Code CLI.

npx @generata/core init @generata/starter ~/Projects/hello-generata
# or: pnpm dlx @generata/core init @generata/starter ~/Projects/hello-generata

Public API

import {
  defineAgent,
  defineWorkflow,
  defineConfig,
  runWorkflow,
  runAgent,
  runScript,
  worktree,
} from "@generata/core";

defineAgent / defineWorkflow / defineConfig author agents, workflows, and a project config. runWorkflow / runAgent drive them from your own TypeScript without going through the CLI, and runScript wraps a driver script with SIGINT/exit-code handling. worktree(...) declares git-worktree isolation:

defineWorkflow({
  isolation: worktree({ sharedPaths: ["state.md"] }),
  // ...
});

Agent options

filesystemAccess

Optional boolean. full-permission agents (worker, planner) implicitly receive Read, Glob, and Grep as baseline tools so they can inspect the workspace before writing. Set filesystemAccess: false to omit that baseline - useful for write-or-exec-only agents that should not be allowed to read arbitrary files. Omitting the field keeps the baseline (the default).

The flag is a no-op on read-only agents (filesystem read is the defining characteristic of that permission level); setting it to false on a read-only agent is a parse error.

import { defineAgent } from "@generata/core";

export default defineAgent({
  name: "shell-runner",
  type: "worker",
  permissions: "full",
  filesystemAccess: false,
  modelTier: "light",
  description: "Runs a single shell command without reading source.",
  tools: ["bash"],
  prompt: ({ command }) => `Run: ${command}`,
});

Running workflows from code

@generata/core exposes runWorkflow and runAgent so you can drive any workflow or agent from your own TypeScript without going through the CLI. This is the primitive for loops, batch jobs, or wrapping generata in a larger script.

Basic call

import { runWorkflow } from "@generata/core";
import reviewNote from "./workflows/review-note.js";

const result = await runWorkflow(reviewNote, { file: "notes/aardvark.md" });
console.log(result.output);
if (!result.success) process.exit(1);

Looping over a folder

import { glob } from "node:fs/promises";
import { runWorkflow } from "@generata/core";
import reviewNote from "./workflows/review-note.js";

for await (const file of glob("notes/*.md")) {
  const result = await runWorkflow(reviewNote, { file });
  if (!result.success) {
    console.error(`Failed on ${file}:`, result.steps.at(-1)?.output);
    continue;
  }
  console.log(`Reviewed ${file}: ${result.output}`);
}

Sharing a single worktree across many runs

When a workflow declares isolation: worktree(...), every runWorkflow call by default sets up and tears down its own worktree. If you want one shared worktree across an iteration, set it up yourself, pass isolation: "none" to disable per-run setup, and point cwd at the worktree path:

import { runWorkflow } from "@generata/core";
import processItem from "./workflows/process-item.js";

// Replace with whatever produces a worktree path - your own helper, an
// existing checkout, etc. The point is: one path, many iterations.
const worktreePath = await setupSharedWorktree();

for (const item of items) {
  await runWorkflow(processItem, { id: item.id }, { isolation: "none", cwd: worktreePath });
}

Run output

A programmatic run prints two lines to stderr at start so you know what's running and where the log lives:

workflow: notes/review-note (3 steps)
Full log: file:///abs/path/to/.generata/logs/workflow/review-note-<runId>.log

(For runAgent the first line is agent: <name> [<type>].) The Full log: line fires whenever a prompt log is being written - both CLI and programmatic, with or without onEvent. The header line is suppressed when you wire onEvent, since you're driving display yourself (the CLI does this via consoleSink's richer workflow-start / agent-welcome lines).

Prompt logs land at <logsDir>/<kind>/<caller>-<name>-<runId>.log (controlled by logPrompts: true in your config, the default), where <caller> is the basename of the script that called runWorkflow / runAgent. The prefix makes it easy to tell apart logs from different scripts that drive the same workflow. CLI runs and explicit promptLogFile overrides skip the prefix.

Subscribing to progress events

Programmatic runs are otherwise silent on the console. Pass onEvent to receive structured events (workflow-start, step-start, step-done, etc.):

await runWorkflow(
  processItem,
  { id: "42" },
  {
    onEvent: (e) => {
      if (e.type === "step-done") process.stderr.write(".");
    },
  },
);

The driver skeleton: runScript

runScript wraps a driver script's main function with the canonical lifecycle contract, so every script doesn't re-implement it:

import { runScript, runWorkflow } from "@generata/core";
import longRun from "./workflows/long-run.js";

await runScript(async ({ signal }) => {
  await runWorkflow(longRun, { input }, { signal });
});

What it does:

  • First ^C prints ^C - aborting... and aborts the signal you thread into runWorkflow / runAgent calls; a second ^C falls through to Node's default hard-kill.
  • An AbortError escaping your fn prints <script> cancelled and exits 130.
  • Any other error prints <script> failed: <message> and exits 1 - so inside the fn you just throw instead of hand-rolling console.error + process.exit.
  • <script> is derived from the calling file's basename (audit.ts -> audit), the same convention prompt-log prefixes use.
  • Exit codes are set via process.exitCode, not process.exit(), so stdio flushes naturally.

Cancellation via AbortSignal

If you need custom wiring (your own signal source, embedding in a larger process), skip runScript and pass any AbortSignal directly:

const ac = new AbortController();
process.once("SIGINT", () => ac.abort());
await runWorkflow(longRun, { input }, { signal: ac.signal });

Error contract

runWorkflow throws for:

  • GenerataPrecheckError - workflow misconfigured (introspect err.issues for diagnostics).
  • AbortError - caller cancelled via AbortSignal.
  • Infra errors (cannot spawn claude, etc.).

It returns success: false for:

  • A step that hits its maxRetries limit. Inspect result.steps.at(-1).output and result.steps.at(-1).metrics.error for diagnostics.

It returns halted: true, success: false for:

  • A worker that emits --halt "<reason>" via the emit bin. result.haltReason carries the reason; this is a clean structured stop, not a failure.

Looking to expose workflows over HTTP? See @generata/serve.

CLI

Command Purpose
generata <name> Run a workflow (shorthand for workflow <name>)
generata init <template> [dest] Scaffold a new project from a template
generata add <template> Merge a template into the current project
generata agent <name> Run a single agent
generata workflow <name> Run a workflow (alias: run)
generata validate [--all] Static-check workflow definitions
generata metrics [today|week] Show metrics summary
generata commands sync Regenerate .claude/commands/ from workflows
generata worktree prune Remove orphan generata/wt-* worktrees and branches
generata help [topic] Show help (topics: agents, workflows, env, templates, bins)

Workflow flags: --worktree forces git-worktree isolation for the run; --local forces it off (mutually exclusive).

Config reference

Every field accepted by defineConfig (i.e. the GlobalConfig schema):

Field Type Default Description
modelTiers.heavy/standard/light string (required) Model IDs for each cost tier
workDir string (required) Root directory for worktree isolation
agentsDir string "agents" Directory scanned for agent definition files
metricsDir string "metrics" Directory where per-run metrics JSON files are written
logsDir string "logs" Directory where prompt logs are written (when logPrompts is true)
logPrompts boolean true Write full prompt/response logs for every run
verboseOutput boolean false Stream each agent's raw Claude output to the console
showPricing boolean false Print token cost breakdown after each run
showWeeklyMetrics boolean true Print a weekly usage summary on CLI startup
notifications boolean true Send OS/Telegram notifications on workflow completion
maxCriticRetries number 3 How many times a critic agent may loop before the step fails
telegram { botToken, chatId } - Telegram bot credentials for completion notifications (requires notifications: true)
serve unknown - Passed through to @generata/serve; core does not validate the shape

Template specifiers

generata init accepts:

  • @generata/<alias> - resolves via the built-in catalog
  • <owner>/<repo> - resolves to https://github.com/<owner>/<repo>.git
  • git@... / https://....git - any git URL
  • ./path / /abs/path - a local directory containing generata.template.json

Init flags

  • --yes - non-interactive; required env values default to manifest examples
  • --skip-preflight - skip required-bin checks
  • --skip-install - skip the package-manager install step (useful offline)

Add flags

  • --force - overwrite conflicting files
  • --dry-run - print what would be written
  • --into <subdir> - merge into a subdirectory rather than the project root

Development (in the generata monorepo)

pnpm install                            # links the workspace
pnpm --filter @generata/core build      # rebuild dist/ after engine changes
node --test --import tsx packages/core/src/**/*.test.ts

The exports map uses a development condition that points at src/ so workspace dev runs through the TypeScript source via tsx; published consumers see the compiled dist/ output.

See CHANGELOG.md for release notes.