Multi-agent orchestration harness for AI coding agents (claude-code, codex, cursor, opencode, pi). Async-await TypeScript API; lifecycle hooks; auto-todos with verify-loop.
npm install @taskflow-corp/cliimport { taskflow } from '@taskflow-corp/cli';
await taskflow('hello').run(async ({ phase, session }) => {
await phase('greet', async () => {
await session('say-hi', { with: 'claude-code', task: 'Print hello world' });
});
});Shadcn-style distribution: drop any harness into any project with a single command. You never need to install the package locally — npx @taskflow-corp/cli@latest <command> works from any directory and always pulls the newest version.
# 1. Bare name — built-in @taskflow registry
# resolves against TASKFLOW_REGISTRY_URL (default https://taskflow.sh/r/{name}.json)
npx @taskflow-corp/cli@latest add example-hello
# 2. Namespaced — @ns/name, looked up in taskflow.json `registries` map
npx @taskflow-corp/cli@latest add @acme/e2e-video-tests
npx @taskflow-corp/cli@latest add @acme/e2e-video-tests@^1.2.0 # optional semver tail
# 3. Raw URL — any server returning a valid registry-item.json
npx @taskflow-corp/cli@latest add https://example.com/r/harness.json
npx @taskflow-corp/cli@latest add https://raw.githubusercontent.com/you/repo/main/r/x.json
# 4. Local file — absolute, relative, or ~/-prefixed
npx @taskflow-corp/cli@latest add ./my-harness.json
npx @taskflow-corp/cli@latest add /abs/path/harness.json
npx @taskflow-corp/cli@latest add ~/shared/harness.json
# 5. Bare GitHub shortcut (degit-style) — defaults to github.com
npx @taskflow-corp/cli@latest add user/repo
npx @taskflow-corp/cli@latest add user/repo/path/to/item.json # subpath
npx @taskflow-corp/cli@latest add user/repo/path/to/item.json#v1.2.0 # branch | tag | sha
# 6. Explicit host shortcut — github: / gitlab: / bitbucket:
npx @taskflow-corp/cli@latest add github:user/repo/items/foo.json#main
npx @taskflow-corp/cli@latest add gitlab:user/repo/items/foo.json#v1
npx @taskflow-corp/cli@latest add bitbucket:user/repo/items/foo.json
# 7. Fully qualified (Terraform grammar, for private SSH, integrity pinning, etc.)
# Format: <type>::<url>[//<subpath>][?ref=<ref>&sha256=<hex>&depth=<n>]
npx @taskflow-corp/cli@latest add git::https://host/org/repo.git//items/foo.json?ref=v1
npx @taskflow-corp/cli@latest add git::ssh://git@github.com/you/priv.git//items/foo.json?ref=main
npx @taskflow-corp/cli@latest add https::https://example.com/bundle.json?sha256=abc123
npx @taskflow-corp/cli@latest add file::./local/harness.jsonDetection order (first match wins, mirrors shadcn's resolver):
- ends with
.jsonand not a URL → local file git::/https::/file::prefix → fully qualifiedgithub:/gitlab:/bitbucket:prefix → host shortcut- parses as a URL → raw URL
@ns/name→ namespace lookupuser/repo[/subpath][#ref]→ bare GitHub shortcut- otherwise → bare name (built-in
@taskflowregistry)
Every form resolves to: fetch → validate (Zod) → write files → patch .agents/taskflow/config.ts (ts-morph AST merge) → merge .env.local → upsert taskflow.lock.
npx @taskflow-corp/cli@latest add example-hello @acme/video-tests ./local.jsonregistryDependencies from each item are resolved transitively (BFS + Kahn topo sort with cycle tolerance), so one command can pull a whole dependency graph.
| Command | Purpose |
|---|---|
taskflow init |
Scaffold taskflow.json + .agents/taskflow/config.ts + harness/rules dirs (auto-invoked by add on first use) |
taskflow add <source...> |
Install one or more harnesses |
taskflow view <source> |
Print the resolved registry-item JSON (no write, no install) |
taskflow list |
List installed harnesses from taskflow.lock |
taskflow search <query> |
Fuzzy-match local registries + auto-discover taskflow harnesses on GitHub |
taskflow update [name...] |
Re-resolve; rewrite files + lockfile (--all implied if no names) |
taskflow remove <name> |
Delete installed files + lockfile entry |
taskflow apply <preset> |
add --overwrite alias (shadcn-style re-skin) |
taskflow build [input] |
Publisher: inline source file contents, emit r/*.json + r/registry.json |
taskflow mcp |
Start MCP server over stdio (tools: list_harnesses, search, install) |
taskflow run <harness.ts> |
Execute an installed harness (TUI if TTY else JSONL) |
taskflow watch <harness.ts> |
Alias for run |
taskflow plan <harness.ts> |
Static AST preview — no LLM calls |
| Flag | Default | Effect |
|---|---|---|
-y, --yes |
false |
Skip all confirmation prompts. On existing-file conflicts, skips the file (does NOT auto-overwrite — use --overwrite for that). |
-o, --overwrite |
false |
Replace existing files without prompt. Orthogonal to --yes. |
--dry-run |
false |
Resolve + validate, print what would change, do not write. |
--diff |
false |
Like --dry-run with a diff. Implies --dry-run. |
--view |
false |
Resolve and print the registry-item JSON to stdout. Do not write, do not preflight. |
-p, --path <dir> |
harnessDir from taskflow.json |
Override install directory for this run. |
-c, --cwd <dir> |
process.cwd() |
Run as if invoked from <dir>. |
-s, --silent |
false |
Mute all @clack/prompts output. On conflicts, skips like --yes. |
--frozen |
false |
CI mode: error if resolved items don't match taskflow.lock. |
--skip-adapter-check |
false |
Skip the requiredAdapters preflight check. |
# From any repo on your machine:
cd /path/to/some/project
# First add auto-scaffolds taskflow.json + .agents/taskflow/config.ts + dirs
npx @taskflow-corp/cli@latest add <your-source> -y --skip-adapter-check
# See what was installed
npx @taskflow-corp/cli@latest list
# Run it
npx @taskflow-corp/cli@latest run .agents/taskflow/harness/<name>.tsproject/
├── taskflow.json # config + registries map
├── taskflow.lock # content-addressed install manifest
├── .env.local # auto-loaded; ${VAR} expansion for registries
└── .agents/
└── taskflow/
├── config.ts # hooks, plugins, scope
├── harness/<name>.ts # installed harness files
├── harness/plugins/<name>.ts # installed plugins
├── harness/utils/<name>.ts # installed utilities
├── harness/examples/<name>.ts
└── rules/<name>.md # installed rules files
taskflow.json:
{name}placeholder is mandatory;{style}is reserved for future use.${VAR_NAME}(braces required, no$VARform) interpolates fromprocess.env..envand.env.localare auto-loaded before resolution.- Missing env vars fail pre-flight with a clear
RegistryMissingEnvironmentVariablesError. - HTTP 401/403/404/410 map to specific typed errors with actionable hints.
- Author
registry/registry.json:{ "$schema": "https://taskflow.sh/schema/registry.json", "name": "@yourname", "homepage": "https://yourname.dev", "items": [ { "$schema": "https://taskflow.sh/schema/registry-item.json", "name": "my-harness", "type": "taskflow:harness", "files": [ { "path": "items/my-harness.ts", "type": "taskflow:harness" } ] } ] } - Write the source
.tsfiles referenced byfiles[].path. - Run
npx @taskflow-corp/cli@latest build -c ./registry --output ./registry/r. - Host the emitted
r/directory (GitHub Pages, S3, your CDN). - Consumers then:
npx @taskflow-corp/cli@latest add https://<your-host>/r/my-harness.json
See registry/ in this repo for a worked example.
Typing taskflow add <user>/<repo> with no subpath and no registry-item.json at the repo root now triggers auto-discovery: the CLI searches the target repo for files that look like taskflow harnesses (i.e. import @taskflow-corp/cli / taskflow-cli / taskflowjs / taskflow-sdk and top-level-call taskflow(...).run(...)), then prompts a multi-select so you pick exactly which harnesses to install.
$ npx @taskflow-corp/cli@latest add AbhiShake1/taskflow
◇ Discovered 4 taskflow harnesses in AbhiShake1/taskflow:
◇ Select harnesses to install: (space to toggle, enter to confirm)
◼ examples/ui-plan.ts (matched: import from '@taskflow-corp/cli')
◻ examples/ui-execute.ts
◼ examples/ui-harness-trio/index.ts
◻ harness/self-evolve.ts
◇ Installing 2 harnesses…
✔ .agents/taskflow/harness/ui-plan.ts
✔ .agents/taskflow/harness/index.ts
✔ Done. 2 written, 0 skipped, 0 overwritten.Rules:
- 0 matches → error with a clear message ("no taskflow harnesses found in
<repo>"). - 1 match → auto-install, no prompt.
- >1 matches → interactive
@clack/promptsmultiselect. Default nothing selected; pick what you want. --yeswith >1 matches → errors ("multiple harnesses found; re-run without--yes, or pass the full path e.g.user/repo/path/file.tsto target one file"). Auto-discovery never silently installs everything just because--yeswas passed.
If the target repo ships a registry-item.json at HEAD on the default branch, the original Tier 2 shortcut behaviour wins (fetch the registry item directly) and discovery is skipped. If you type the full path (user/repo/sub/path.ts), the tarball-fetch path is used and discovery is also skipped.
How it works. The CLI hits a small Cloudflare Pages Function (/api/discover) which calls the official GitHub Code Search REST API. Results are cached 10 minutes in Workers KV. Sub-second end-to-end, no headless browsers, no cookie juggling — just a plain server-to-server fetch. Set TASKFLOW_DISCOVER_URL to point the CLI at a private proxy (useful for enterprise mirrors or local wrangler pages dev):
TASKFLOW_DISCOVER_URL=https://my-proxy.example.com/api/discover \
npx @taskflow-corp/cli@latest add AbhiShake1/taskflowtaskflow search uses discovery too. Results from a taskflow search <query> now fold GitHub-wide discovery hits in alongside the configured-registry fuzzy-matches, so you can grep the whole GitHub index for a harness by keyword without targeting a specific repo.
Full design notes for discovery live in docs/add-command-plan.md §15.
| Type | Default destination | Notes |
|---|---|---|
taskflow:harness |
<harnessDir>/<basename> |
Main .ts the user runs |
taskflow:plugin |
<harnessDir>/plugins/<basename> |
Imported from config.ts |
taskflow:utils |
<harnessDir>/utils/<basename> |
Shared TS |
taskflow:example |
<harnessDir>/examples/<basename> |
Sample invocation |
taskflow:rules |
<rulesDir>/<basename> or target |
Markdown rules |
taskflow:config-patch |
Merged into config.ts via ts-morph AST |
Not a file |
taskflow:file |
target (required) |
Arbitrary path incl. ~/ |
npx @taskflow-corp/cli@latest mcpExposes three tools over stdio:
list_harnesses— return installed harnesses fromtaskflow.locksearch— fuzzy-match local registries + GitHub-wide auto-discoveryinstall— delegate torunAdd
Wire it into your MCP client config and the model can discover and install harnesses autonomously.
# Reproducible — errors on lockfile drift
npx @taskflow-corp/cli@latest add <source> --yes --silent --frozenFull design notes: docs/add-command-plan.md.
import { defineConfig } from '@taskflow-corp/cli/config';
export default defineConfig({
events: {
afterTaskDone: async (ctx, { spec, result }) => {
// post-task hook
},
},
todos: { autoExtract: true, maxRetries: 3 },
});Sessions with a zod schema return typed results. claude-code uses native tool-use. codex (on gpt-5 / gpt-5.4 models) uses --output-schema; gpt-5-codex variants fall back to prompt-engineered JSON pending openai/codex#4181. Other adapters (cursor, opencode, pi) use prompt-engineered JSON. Override codex behavior with HARNESS_CODEX_SCHEMA=0|1.
import { z } from 'zod';
const result = await session('summary', {
with: 'codex:gpt-5.4',
task: 'Summarize the repo',
schema: z.object({ title: z.string(), bullets: z.array(z.string()) }),
});
// result is typed: { title: string; bullets: string[] }A plugin contributes hooks, a ctx.plugins.<name> namespace, and optional config fragments:
import type { Plugin } from '@taskflow-corp/cli/core';
export const myPlugin: Plugin = () => ({
name: 'my-plugin',
events: {
afterTaskDone: async (ctx, { spec }) => { /* ... */ },
},
ctx: () => ({ hello: () => 'world' }),
});To get typed access to ctx.plugins.myPlugin.hello() in downstream hooks, module-augment the plugin namespace registry:
declare module '@taskflow-corp/cli/core' {
interface PluginNamespaces {
'my-plugin': { hello: () => string };
}
}Single-file harnesses live in examples/. Install the SDK globally first (npm i -g @taskflow-corp/sdk), then run any file directly with tsx:
examples/ui-plan.ts— scans a project, injects missingdata-testids, emits a YAML test plan.examples/ui-execute.ts— consumes the YAML and generates a standalone Playwright project.examples/ui-execute.test.ts— unit tests for the YAML → Playwright codegen.examples/ui-content.ts— stitches media into a video, generates narratives, and publishes to YouTube / Facebook / Instagram / LinkedIn / Twitter / Medium / Reddit (stubs — swap in real OAuth before production).
Each is ~400–600 LOC with zero build step: tsx examples/ui-plan.ts.
| Var | Purpose |
|---|---|
ANTHROPIC_API_KEY |
Required for claude-code sessions and pi:anthropic/*. |
OPENAI_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, GEMINI_API_KEY |
Required per-session for the respective providers. |
HARNESS_PI_BIN |
Override the pi binary name (default pi). Use omp with @oh-my-pi/pi-coding-agent. |
HARNESS_CODEX_SCHEMA |
0 forces prompt-engineered JSON for codex; 1 forces native --output-schema. |
HARNESS_ADAPTER_OVERRIDE=mock |
Swap every agent for the mock adapter — smoke runs with zero token cost. |
HARNESS_NO_TTY=1 |
Force headless JSONL output even when a TTY is attached. |
HARNESS_RUNS_DIR=... |
Override the runs archive directory (default data/runs). |
HARNESS_REAL_TESTS=1 |
Enable integration tests that make real LLM calls (default-skipped). |
See .claude/skills/taskflow/SKILL.md for the authoring guide.
{ "$schema": "https://taskflow.sh/schema/taskflow.json", "version": "1", "registries": { // simple form — string URL template "@acme": "https://registry.acme.com/r/{name}.json", // advanced form — per-registry headers and query params "@private": { "url": "https://api.corp.com/taskflow/{name}.json", "headers": { "Authorization": "Bearer ${TASKFLOW_TOKEN}" }, "params": { "v": "latest" } } } }