diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 0491725..6809c93 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -10,14 +10,14 @@
"plugins": [
{
"name": "taskflow",
- "description": "Multi-phase subagent orchestration for Codex: the taskflow_* MCP tools (run/list/show/verify/compile) plus a skill that routes multi-phase or fan-out work to them. The MCP server runs via npx (codex-taskflow) — no separate global install required.",
+ "description": "Multi-phase subagent orchestration for Codex: 12 taskflow_* MCP tools (run/list/show/verify/compile/peek/trace/replay/why_stale/recompute/save/search) plus a routing skill. compile returns SVG + text outline. The server runs via npx (codex-taskflow).",
"source": "./packages/codex-taskflow/plugin",
"category": "developer-tools",
"homepage": "https://github.com/heggria/taskflow"
},
{
"name": "claude-taskflow",
- "description": "Multi-phase subagent orchestration for Claude Code: the taskflow_* MCP tools (run/list/show/verify/compile/peek) plus a skill that routes multi-phase or fan-out work to them. The MCP server runs via npx (claude-taskflow) — no separate global install required.",
+ "description": "Multi-phase subagent orchestration for Claude Code: 12 taskflow_* MCP tools (run/list/show/verify/compile/peek/trace/replay/why_stale/recompute/save/search) plus a routing skill. compile returns SVG + text outline. The server runs via npx (claude-taskflow).",
"source": "./packages/claude-taskflow/plugin",
"category": "developer-tools",
"homepage": "https://github.com/heggria/taskflow"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2c39fd6..22b0363 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,13 +24,13 @@ jobs:
matrix:
node: ["22", "24"]
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# pnpm via corepack — the version is pinned by the `packageManager` field
# in package.json, so every contributor and CI run use the same pnpm.
- - uses: pnpm/action-setup@v6
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
@@ -49,11 +49,11 @@ jobs:
name: e2e (codex MCP, network-free)
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- - uses: pnpm/action-setup@v6
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
@@ -93,15 +93,19 @@ jobs:
- name: OpenCode MCP e2e (stdio handshake + tool calls)
run: pnpm run test:e2e-opencode-mcp
+ # Grok MCP e2e is also stdio-only and does not require a live Grok model.
+ - name: Grok MCP e2e (stdio handshake + tool calls)
+ run: pnpm run test:e2e-grok-mcp
+
build:
name: build (dist emit)
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- - uses: pnpm/action-setup@v6
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
@@ -115,6 +119,56 @@ jobs:
- name: Build all packages
run: pnpm run build
+ packed-consumer:
+ name: packed consumer (9 packages)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: "22"
+ registry-url: "https://registry.npmjs.org"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile --registry https://registry.npmjs.org/
+
+ # Pack with pnpm so workspace:* is rewritten exactly as it will be for
+ # publish. Install only those tarballs into a clean npm project, then
+ # exercise every explicit export and shipped bin.
+ - name: Build, pack, install, and smoke test all published packages
+ run: pnpm run test:pack
+
+ website:
+ name: website (production export)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ # The docs source records last-modified timestamps from git history.
+ fetch-depth: 0
+
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
+
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: "22"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile --registry https://registry.npmjs.org/
+
+ # Match the GitHub Pages deployment settings so base-path-only failures
+ # are caught before merge. `build` runs docs:check before the export.
+ - name: Validate docs and build production export
+ working-directory: website
+ run: pnpm run build
+ env:
+ TASKFLOW_BASE_PATH: /taskflow
+
codeql:
name: CodeQL (JS/TS)
runs-on: ubuntu-latest
@@ -122,8 +176,8 @@ jobs:
contents: read
security-events: write
steps:
- - uses: actions/checkout@v7
- - uses: github/codeql-action/init@v4
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
with:
languages: javascript-typescript
- - uses: github/codeql-action/analyze@v4
+ - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml
index 6420ee5..2a36e6d 100644
--- a/.github/workflows/deploy-website.yml
+++ b/.github/workflows/deploy-website.yml
@@ -9,6 +9,14 @@ on:
- 'README.md'
- 'README.zh-CN.md'
- 'docs/**'
+ - 'packages/taskflow-core/**'
+ - 'packages/taskflow-dsl/**'
+ - 'skills-src/**'
+ - 'scripts/build-skills.mjs'
+ - 'package.json'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'tsconfig*.json'
- '.github/workflows/deploy-website.yml'
workflow_dispatch:
@@ -26,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v7
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
# Full history so the lastModified remark plugin can read accurate
# git timestamps for every docs page (shallow clone would leave
@@ -37,10 +45,10 @@ jobs:
# in the root package.json, so CI uses the same pnpm as local dev. The
# website is a pnpm workspace member (pnpm-workspace.yaml), so one
# `pnpm install` at the root installs every package including the website.
- - uses: pnpm/action-setup@v6
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- name: Setup Node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: pnpm
@@ -55,7 +63,7 @@ jobs:
TASKFLOW_BASE_PATH: /taskflow
- name: Upload artifact
- uses: actions/upload-pages-artifact@v5
+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: website/dist
@@ -68,4 +76,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@v5
+ uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 3b26c89..9d99cfd 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,8 +1,9 @@
name: Publish & Release
-# Publishes taskflow-core + taskflow-mcp-core + taskflow-hosts + pi-taskflow + codex-taskflow +
-# claude-taskflow + opencode-taskflow to npmjs.com and creates a GitHub Release, when a v* tag is
-# pushed (e.g. `git tag v0.1.0 && git push origin v0.1.0`). All seven workspace
+# Publishes all nine packages (taskflow-core, taskflow-mcp-core, taskflow-hosts,
+# taskflow-dsl, pi-taskflow, codex-taskflow, claude-taskflow, opencode-taskflow,
+# grok-taskflow) to npmjs.com and creates a GitHub Release when a v* tag is
+# pushed (e.g. `git tag v0.2.0 && git push origin v0.2.0`). All nine workspace
# versions must equal the tag.
on:
@@ -10,17 +11,44 @@ on:
tags:
- "v*"
+concurrency:
+ group: publish-${{ github.ref }}
+ cancel-in-progress: false
+
jobs:
publish:
runs-on: ubuntu-latest
permissions:
- contents: write # create GitHub Release
+ contents: read # checkout source for build + publish
id-token: write # npm provenance
steps:
- - uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ fetch-depth: 0
+
+ # A tag is not protected by the main-branch ruleset. Refuse to publish
+ # unless its peeled commit is already reachable from the canonical main
+ # branch fetched from origin; a tag pushed from any other branch must not
+ # become a release merely because its version happens to match.
+ - name: Verify tag commit belongs to origin/main
+ run: |
+ set -euo pipefail
+ git fetch --no-tags --prune origin \
+ +refs/heads/main:refs/remotes/origin/main
+ TAG_COMMIT="$(git rev-parse "${GITHUB_REF}^{commit}")"
+ EVENT_COMMIT="$(git rev-parse "${GITHUB_SHA}^{commit}")"
+ if [ "$TAG_COMMIT" != "$EVENT_COMMIT" ]; then
+ echo "::error::Tag $GITHUB_REF_NAME resolves to $TAG_COMMIT, but the event SHA resolves to $EVENT_COMMIT"
+ exit 1
+ fi
+ if ! git merge-base --is-ancestor "$TAG_COMMIT" refs/remotes/origin/main; then
+ echo "::error::Tag $GITHUB_REF_NAME targets $TAG_COMMIT, which is not reachable from origin/main"
+ exit 1
+ fi
+ echo "Verified $GITHUB_REF_NAME at $TAG_COMMIT is reachable from origin/main"
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
@@ -38,6 +66,12 @@ jobs:
- name: Build all packages (emit dist)
run: pnpm run build
+ # This is a release gate, not a post-publish check: it packs the current
+ # tag checkout, installs all nine local tarballs into a clean consumer,
+ # and exercises their exports and bins before the first npm mutation.
+ - name: Verify packed consumer install
+ run: node scripts/smoke-packed-packages.mjs
+
- name: Verify tag matches workspace versions
run: |
TAG="${GITHUB_REF_NAME}"
@@ -46,7 +80,7 @@ jobs:
echo "::error::Tag $TAG does not match root version $EXPECT"
exit 1
fi
- for pkg in taskflow-core taskflow-mcp-core taskflow-hosts pi-taskflow codex-taskflow claude-taskflow opencode-taskflow; do
+ for pkg in taskflow-core taskflow-mcp-core taskflow-hosts taskflow-dsl pi-taskflow codex-taskflow claude-taskflow opencode-taskflow grok-taskflow; do
V="v$(node -p "require('./packages/$pkg/package.json').version")"
if [ "$V" != "$TAG" ]; then
echo "::error::Tag $TAG does not match $pkg version $V"
@@ -66,6 +100,11 @@ jobs:
echo "::error::Tag $TAG does not match claude plugin.json version $CLAUDE_PLUGIN_V"
exit 1
fi
+ GROK_PLUGIN_V="v$(node -p "require('./packages/grok-taskflow/plugin/.grok-plugin/plugin.json').version")"
+ if [ "$GROK_PLUGIN_V" != "$TAG" ]; then
+ echo "::error::Tag $TAG does not match grok plugin.json version $GROK_PLUGIN_V"
+ exit 1
+ fi
CODEX_PINNED="v$(node -p "require('./packages/codex-taskflow/plugin/.mcp.json').mcpServers.taskflow.args.find(a => a.startsWith('codex-taskflow@')).split('@')[1]")"
if [ "$CODEX_PINNED" != "$TAG" ]; then
echo "::error::codex .mcp.json pins codex-taskflow@${CODEX_PINNED#v} but tag is $TAG"
@@ -76,6 +115,11 @@ jobs:
echo "::error::claude .mcp.json pins claude-taskflow@${CLAUDE_PINNED#v} but tag is $TAG"
exit 1
fi
+ GROK_PINNED="v$(node -p "require('./packages/grok-taskflow/plugin/.mcp.json').mcpServers.taskflow.args.find(a => a.startsWith('grok-taskflow@')).split('@')[1]")"
+ if [ "$GROK_PINNED" != "$TAG" ]; then
+ echo "::error::grok .mcp.json pins grok-taskflow@${GROK_PINNED#v} but tag is $TAG"
+ exit 1
+ fi
# OpenCode has no plugin manifest; its scaffold opencode.json pins the
# npx package version the same way, so that pin must equal the tag too.
OPENCODE_PINNED="v$(node -p "require('./packages/opencode-taskflow/plugin/opencode.json').mcp.taskflow.command.find(a => a.startsWith('opencode-taskflow@')).split('@')[1]")"
@@ -86,17 +130,20 @@ jobs:
- name: Publish workspaces to npmjs.com
# Order matters: taskflow-core first (the adapters depend on it).
- # Idempotent: if this exact version is already on npm (e.g. a re-run or a
- # partial earlier publish), skip that package instead of failing the job.
+ # Idempotent but fail-closed: an existing version is skipped only after
+ # its trusted npm owner, GitHub/SLSA provenance, source commit, and exact
+ # locally-packed tarball integrity have all been verified. A squatter
+ # pre-publishing name@version must never be mistaken for our release.
run: |
- set -e
+ set -euo pipefail
publish_one() {
local pkg="$1"
local name version
name="$(node -p "require('./packages/$pkg/package.json').name")"
version="$(node -p "require('./packages/$pkg/package.json').version")"
if pnpm view "$name@$version" version >/dev/null 2>&1; then
- echo "::notice::$name@$version already published — skipping"
+ node scripts/verify-published-package.mjs "packages/$pkg"
+ echo "::notice::$name@$version already published and verified — skipping"
else
echo "Publishing $name@$version"
pnpm publish --filter "$pkg" --provenance --access public --no-git-checks
@@ -105,16 +152,57 @@ jobs:
publish_one taskflow-core
publish_one taskflow-mcp-core
publish_one taskflow-hosts
+ publish_one taskflow-dsl
publish_one pi-taskflow
publish_one codex-taskflow
publish_one claude-taskflow
publish_one opencode-taskflow
+ publish_one grok-taskflow
+
+ # A successful publish command is not sufficient: registry metadata,
+ # provenance, and tarball bytes can become visible at different times.
+ # Verify every package after the full set is published, retrying only for
+ # bounded npm propagation delay.
+ verify_one() {
+ local pkg="$1"
+ local attempt
+ for attempt in 1 2 3 4 5 6; do
+ if node scripts/verify-published-package.mjs "packages/$pkg"; then
+ return 0
+ fi
+ if [ "$attempt" -lt 6 ]; then sleep $((attempt * 5)); fi
+ done
+ echo "::error::Post-publish verification failed for $pkg"
+ return 1
+ }
+ verify_one taskflow-core
+ verify_one taskflow-mcp-core
+ verify_one taskflow-hosts
+ verify_one taskflow-dsl
+ verify_one pi-taskflow
+ verify_one codex-taskflow
+ verify_one claude-taskflow
+ verify_one opencode-taskflow
+ verify_one grok-taskflow
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ NPM_TRUSTED_OWNERS: "heggria,muyun"
+ PUBLISH_REPOSITORY_URL: "https://github.com/heggria/taskflow"
+
+ release:
+ needs: publish
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # create GitHub Release
+
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Create GitHub Release
run: |
+ set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
+ TAG_COMMIT="$(git rev-parse "${GITHUB_REF}^{commit}")"
node -e "
const fs = require('fs');
let body = [];
@@ -129,10 +217,46 @@ jobs:
}
} catch {}
const text = body.join('\n').trim();
- fs.writeFileSync('/tmp/release-notes.md', text || ('Release ' + process.argv[1] + ' \u2014 taskflow-core, taskflow-mcp-core, taskflow-hosts, pi-taskflow, codex-taskflow, claude-taskflow, opencode-taskflow.'));
+ fs.writeFileSync('/tmp/release-notes.md', text || ('Release ' + process.argv[1] + ' \u2014 taskflow-core, taskflow-mcp-core, taskflow-hosts, taskflow-dsl, pi-taskflow, codex-taskflow, claude-taskflow, opencode-taskflow, grok-taskflow.'));
" "$VERSION"
+
+ TAG_PATH="$(jq -rn --arg tag "$GITHUB_REF_NAME" '$tag | @uri')"
+ ERROR_FILE="$(mktemp)"
+ if RELEASE_JSON="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG_PATH}" 2>"$ERROR_FILE")"; then
+ RELEASE_TAG="$(jq -er '.tag_name' <<<"$RELEASE_JSON")"
+ RELEASE_TARGET="$(jq -er '.target_commitish' <<<"$RELEASE_JSON")"
+ RELEASE_DRAFT="$(jq -er '.draft' <<<"$RELEASE_JSON")"
+ RELEASE_PRERELEASE="$(jq -er '.prerelease' <<<"$RELEASE_JSON")"
+ if [ "$RELEASE_TAG" != "$GITHUB_REF_NAME" ]; then
+ echo "::error::Existing release tag $RELEASE_TAG does not match $GITHUB_REF_NAME"
+ exit 1
+ fi
+ case "$RELEASE_TARGET" in
+ "$TAG_COMMIT"|main|refs/heads/main)
+ ;;
+ *)
+ echo "::error::Existing release target $RELEASE_TARGET is neither $TAG_COMMIT nor main"
+ exit 1
+ ;;
+ esac
+ if [ "$RELEASE_DRAFT" != "false" ] || [ "$RELEASE_PRERELEASE" != "false" ]; then
+ echo "::error::Existing release must be published and non-prerelease (draft=$RELEASE_DRAFT, prerelease=$RELEASE_PRERELEASE)"
+ exit 1
+ fi
+ echo "::notice::Release $GITHUB_REF_NAME already exists with a verified target; skipping creation"
+ exit 0
+ fi
+
+ if ! grep -q 'HTTP 404' "$ERROR_FILE"; then
+ cat "$ERROR_FILE" >&2
+ echo "::error::Unable to determine whether release $GITHUB_REF_NAME already exists"
+ exit 1
+ fi
+
gh release create "$GITHUB_REF_NAME" \
--notes-file /tmp/release-notes.md \
- --title "v$VERSION"
+ --title "v$VERSION" \
+ --target "$TAG_COMMIT" \
+ --verify-tag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.grok-plugin/marketplace.json b/.grok-plugin/marketplace.json
new file mode 100644
index 0000000..2e861a6
--- /dev/null
+++ b/.grok-plugin/marketplace.json
@@ -0,0 +1,21 @@
+{
+ "name": "taskflow",
+ "description": "Declarative, verifiable DAG orchestration for coding agents. Taskflow lets you define multi-phase subagent workflows (fan-out, gates, loops, tournaments, approvals, sub-flow composition) as JSON, verify them before they run, and keep intermediate transcripts out of your context — exposed to Grok Build as the taskflow_* MCP tools plus a routing skill.",
+ "owner": {
+ "name": "heggria",
+ "url": "https://github.com/heggria"
+ },
+ "plugins": [
+ {
+ "name": "taskflow",
+ "description": "Multi-phase subagent orchestration for Grok Build: 12 taskflow_* MCP tools (run/list/show/verify/compile/peek/trace/replay/why_stale/recompute/save/search) plus a routing skill. compile returns SVG + text outline. The server runs via npx (grok-taskflow).",
+ "source": {
+ "type": "local",
+ "path": "./packages/grok-taskflow/plugin"
+ },
+ "category": "development",
+ "homepage": "https://github.com/heggria/taskflow",
+ "keywords": ["taskflow", "taskflow orchestration", "taskflow dag"]
+ }
+ ]
+}
diff --git a/AGENTS.md b/AGENTS.md
index 4fe6100..3ec686d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,12 +4,12 @@
## Project Overview
-taskflow is a **declarative DAG orchestration runtime** for coding agents — it runs on the [Pi coding agent](https://pi.dev), on [OpenAI Codex](https://github.com/openai/codex), on [Claude Code](https://claude.com/product/claude-code), and on [OpenCode](https://opencode.ai). It lets users define multi-phase workflows (fan-out, gate, loop, tournament, approval, sub-flow composition) as JSON DSL, executes them via isolated subagent processes, and returns only the final result — intermediate transcripts never enter the host context window.
+taskflow is a **declarative DAG orchestration runtime** for coding agents — it runs on the [Pi coding agent](https://pi.dev), on [OpenAI Codex](https://github.com/openai/codex), on [Claude Code](https://claude.com/product/claude-code), on [OpenCode](https://opencode.ai), and on [Grok Build](https://docs.x.ai/build/overview). It lets users define multi-phase workflows (fan-out, gate, loop, tournament, approval, sub-flow composition) as JSON DSL, executes them via isolated subagent processes, and returns only the final result — intermediate transcripts never enter the host context window.
**Language:** TypeScript (ES2022, ESM, `--experimental-strip-types` for direct execution in dev)\
**Runtime:** Node.js ≥ 22.19 (uses `fs.globSync`, `Atomics.wait`)\
-**Dependencies:** Zero runtime deps. The Pi adapter (`pi-taskflow`) peer-depends on `@earendil-works/pi-{agent-core,ai,coding-agent,tui}`; the host-neutral MCP server (`taskflow-mcp-core`) and the three MCP host adapters (`codex-taskflow`, `claude-taskflow`, `opencode-taskflow`) all depend on `taskflow-core` (the adapters also depend on `taskflow-mcp-core`). Everything depends on `typebox`.\
-**Layout:** pnpm-workspace monorepo of seven published packages — `taskflow-core` (host-neutral engine), `taskflow-mcp-core` (the host-neutral MCP server + DAG renderer, depends on core), `taskflow-hosts` (shared host-runner collection: the codex/claude/opencode SubagentRunner impls + argv builders + event-stream parsers, depends on core), `pi-taskflow` (Pi extension adapter, installed via `pi install npm:pi-taskflow`), `codex-taskflow` (Codex MCP server + bin + a `plugin/` scaffold installable via `codex plugin add`; re-exports the runner from `taskflow-hosts`), `claude-taskflow` (Claude Code MCP server + bin + a `plugin/` scaffold installable via `claude plugin install`; re-exports the runner from `taskflow-hosts`), and `opencode-taskflow` (OpenCode MCP server + bin + an `opencode.json` config scaffold; re-exports the runner from `taskflow-hosts`).\
+**Dependencies:** Zero runtime deps. The Pi adapter (`pi-taskflow`) peer-depends on `@earendil-works/pi-{agent-core,ai,coding-agent,tui}`; the host-neutral MCP server (`taskflow-mcp-core`) and the four MCP host adapters (`codex-taskflow`, `claude-taskflow`, `opencode-taskflow`, `grok-taskflow`) all depend on `taskflow-core` (the adapters also depend on `taskflow-mcp-core`). Everything depends on `typebox`.\
+**Layout:** pnpm-workspace monorepo — `taskflow-core` (engine), `taskflow-mcp-core` (MCP + DAG SVG), `taskflow-hosts` (codex/claude/opencode/grok runners), **`taskflow-dsl`** (S4: `.tf.ts` → Taskflow → FlowIR; CLI `taskflow-dsl`), `pi-taskflow`, `codex-taskflow`, `claude-taskflow`, `opencode-taskflow`, `grok-taskflow` (host delivery packages).\
**Build:** each package compiles to `dist/*.js` + `.d.ts` (`tsc`); published packages ship `dist` (Node refuses to type-strip `.ts` under `node_modules`). Dev resolves the TypeScript sources directly via a `development` export condition — no build needed to typecheck or test.
## Architecture
@@ -33,6 +33,10 @@ packages/
│ │ ├─ detached-runner.ts← spawn-only entry for background runs (NOT in the barrel)
│ │ ├─ usage.ts ← token/cost accounting (UsageStats type + aggregation)
│ │ ├─ stale.ts / workspace.ts / flowir/ ← staleness, worktrees, FlowIR compile seam
+│ │ │ (S0: compileTaskflowToFlowIR + hashFlowIR → ir:<64-hex>)
+│ │ ├─ exec/ ← event log schema + fold + S2 kernel (step/driver; default OFF)
+│ │ ├─ replay.ts ← offline what-if replayRun (zero tokens; no runtime/driver import)
+│ │ ├─ trace.ts ← TraceEvent / FileTraceSink / readTrace
│ │ ├─ host/runner-types.ts ← the host-neutral SubagentRunner contract + vendored CoreMessage
│ │ ├─ runner-core.ts ← ALSO hosts the shared `runSubagentProcess` (spawn/idle/abort/classify) +
│ │ │ `SubagentAccumulator` + `unknownAgentResult` reused by every host runner
@@ -79,7 +83,7 @@ packages/
│ ├─ skills/taskflow/ ← GENERATED per-host skill files (do not edit; see skills-src/)
│ └─ assets/ ← plugin icons (taskflow.svg, taskflow-small.svg)
└─ test/ ← mcp-server unit test + .mts e2e scripts
-└─ opencode-taskflow/ ← OpenCode DELIVERY package (depends on taskflow-hosts + taskflow-mcp-core)
+├─ opencode-taskflow/ ← OpenCode DELIVERY package (depends on taskflow-hosts + taskflow-mcp-core)
├─ src/
│ ├─ index.ts ← re-exports the opencode runner from taskflow-hosts (back-compat public surface)
│ └─ mcp/ ← thin bind: server.ts re-exports core's MCP server bound to opencodeSubagentRunner; bin.ts
@@ -88,16 +92,27 @@ packages/
│ ├─ skills/taskflow/ ← GENERATED per-host skill files (do not edit; see skills-src/)
│ └─ assets/ ← icons (taskflow.svg, taskflow-small.svg)
└─ test/ ← opencode-adapter unit tests + .mts e2e scripts
+└─ grok-taskflow/ ← Grok Build DELIVERY package (depends on taskflow-hosts + taskflow-mcp-core)
+ ├─ src/
+ │ ├─ index.ts ← re-exports the grok runner from taskflow-hosts (back-compat public surface)
+ │ └─ mcp/ ← thin bind: server.ts re-exports core's MCP server bound to grokSubagentRunner; bin.ts
+ ├─ plugin/ ← Grok Build plugin scaffold (`grok plugin install … --trust`)
+ │ ├─ .grok-plugin/plugin.json ← plugin manifest
+ │ ├─ .mcp.json ← declares the taskflow MCP server via `npx grok-taskflow-mcp`
+ │ ├─ skills/taskflow/ ← GENERATED per-host skill files (do not edit; see skills-src/)
+ │ └─ assets/ ← icons (taskflow.svg, taskflow-small.svg)
+ └─ test/ ← mcp-server unit test + .mts e2e scripts
.claude-plugin/ ← marketplace.json (repo-root; shared by both `codex plugin marketplace add`
and `claude plugin marketplace add heggria/taskflow`; lists the
- `taskflow` [codex] and `claude-taskflow` [claude] plugins. OpenCode has
- no marketplace — it registers the MCP server via opencode.json)
+ `taskflow` [codex] and `claude-taskflow` [claude] plugins. Grok has
+ `.grok-plugin/marketplace.json` for `grok plugin marketplace add`.
+ OpenCode has no marketplace — it registers the MCP server via opencode.json)
skills-src/taskflow/ ← SINGLE SOURCE for all hosts' skills: entry.pi.md + entry.codex.md +
entry.claude.md + entry.opencode.md (frontmatter + host binding) +
core.md/patterns.md/advanced.md/configuration.md (shared body with
- blocks; the host field is a
+ blocks; the host field is a
comma-list, e.g. ). Compiled by
scripts/build-skills.mjs (pnpm run build:skills); drift-guarded by
packages/pi-taskflow/test/skills-build.test.ts.
@@ -109,7 +124,7 @@ tsconfig.base.json ← shared compiler options; per-package tsconfig.buil
## Key Concepts
-### Phase Types (10 total)
+### Phase Types (12 total)
| Type | Purpose |
|------|---------|
| `agent` | Single subagent call |
@@ -122,6 +137,16 @@ tsconfig.base.json ← shared compiler options; per-package tsconfig.buil
| `loop` | Repeat body until condition, convergence, or max iterations |
| `tournament` | N competing variants + judge picks best or aggregates |
| `script` | Run a shell command (no LLM, zero tokens) — captures stdout; fields `run`/`input`/`timeout` |
+| `race` | First completed branch wins (Horizon B) |
+| `expand` | Dynamic fragment: nested sub-flow or graft-promote (`def` + `expandMode`) |
+
+### Event kernel, trace, and offline replay (0.2.0 Phase 2)
+
+- **FlowIR (S0):** `compileTaskflowToIR` → genuine `compileTaskflowToFlowIR` + `hashFlowIR` → `ir:<64-hex>`; `usedFallbackHash: false` when IR is content-addressable.
+- **Trace:** every run may record `runs/
-
-
+
-
+
Release line 0.2.0 — monorepo packages and plugin pins are 0.2.0; npm registry updates after the v0.2.0 tag publish job. Badge above tracks the published npm line until then.
English · 简体中文 @@ -24,7 +25,7 @@
A declarative, verifiable graph of tasks for coding-agent subagents.
Not a workflow you script — a DAG you declare. Fan out · gate · loop · tournament · resume · save as a command — intermediate results stay out of your context.
-Runs on the Pi coding agent, on OpenAI Codex, on Claude Code, and on OpenCode.
发布线 0.2.0 — monorepo 包与插件 pin 为 0.2.0;npm 在 v0.2.0 tag 发布任务完成后更新。上方徽章在发版前仍可能显示 registry 上的旧版本。
English · 简体中文 · @@ -30,7 +31,7 @@
面向编码智能体子代理(subagent)的声明式、可验证的「任务图」。
不是你要去「写脚本」的 workflow——而是你去「声明」的一张 DAG。并发分发(fan out)· 门控(gate)· 恢复(resume)· 保存为命令——中间结果始终远离你的上下文窗口(context window)。
-可运行于 Pi 编码智能体、OpenAI Codex、Claude Code 与 OpenCode。
(no markdown, ~192px
// tall). These pin the output shape that keeps that box readable.
diff --git a/packages/grok-taskflow/package.json b/packages/grok-taskflow/package.json
new file mode 100644
index 0000000..730825b
--- /dev/null
+++ b/packages/grok-taskflow/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "grok-taskflow",
+ "version": "0.2.0",
+ "description": "Run taskflow on Grok Build: a Grok subagent runner plus an MCP server (and a plug-and-play Grok plugin) that exposes the taskflow_* tools to Grok Build users.",
+ "keywords": [
+ "grok",
+ "grok-build",
+ "xai",
+ "mcp",
+ "taskflow",
+ "orchestration",
+ "subagents",
+ "workflow"
+ ],
+ "license": "MIT",
+ "author": "heggria ",
+ "homepage": "https://github.com/heggria/taskflow#readme",
+ "bugs": {
+ "url": "https://github.com/heggria/taskflow/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/heggria/taskflow.git",
+ "directory": "packages/grok-taskflow"
+ },
+ "type": "module",
+ "engines": {
+ "node": ">=22.19.0"
+ },
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "development": "./src/index.ts",
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./mcp/server": {
+ "development": "./src/mcp/server.ts",
+ "types": "./dist/mcp/server.d.ts",
+ "default": "./dist/mcp/server.js"
+ }
+ },
+ "bin": {
+ "grok-taskflow-mcp": "./dist/mcp/bin.js"
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/copy-readme.mjs grok-taskflow",
+ "prepublishOnly": "npm run build"
+ },
+ "publishConfig": {
+ "access": "public",
+ "exports": {
+ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
+ "./mcp/server": { "types": "./dist/mcp/server.d.ts", "default": "./dist/mcp/server.js" }
+ }
+ },
+ "dependencies": {
+ "taskflow-core": "workspace:*",
+ "taskflow-hosts": "workspace:*",
+ "taskflow-mcp-core": "workspace:*"
+ }
+}
diff --git a/packages/grok-taskflow/plugin/.grok-plugin/plugin.json b/packages/grok-taskflow/plugin/.grok-plugin/plugin.json
new file mode 100644
index 0000000..1806617
--- /dev/null
+++ b/packages/grok-taskflow/plugin/.grok-plugin/plugin.json
@@ -0,0 +1,23 @@
+{
+ "name": "taskflow",
+ "version": "0.2.0",
+ "description": "Declarative, verifiable DAG orchestration for Grok Build subagents — fan-out, gates, loops, tournaments, approvals, resumable runs, and saveable commands, with intermediate transcripts kept out of your context.",
+ "author": {
+ "name": "heggria",
+ "email": "bshengtao@gmail.com"
+ },
+ "homepage": "https://github.com/heggria/taskflow",
+ "repository": "https://github.com/heggria/taskflow",
+ "license": "MIT",
+ "keywords": [
+ "taskflow",
+ "orchestration",
+ "subagents",
+ "workflow",
+ "dag",
+ "mcp",
+ "grok",
+ "grok-build"
+ ],
+ "logo": "assets/taskflow.svg"
+}
diff --git a/packages/grok-taskflow/plugin/.mcp.json b/packages/grok-taskflow/plugin/.mcp.json
new file mode 100644
index 0000000..456db4e
--- /dev/null
+++ b/packages/grok-taskflow/plugin/.mcp.json
@@ -0,0 +1,9 @@
+{
+ "mcpServers": {
+ "taskflow": {
+ "command": "npx",
+ "args": ["-y", "-p", "grok-taskflow@0.2.0", "grok-taskflow-mcp"],
+ "tool_timeout_sec": 1800
+ }
+ }
+}
diff --git a/packages/grok-taskflow/plugin/assets/taskflow-small.svg b/packages/grok-taskflow/plugin/assets/taskflow-small.svg
new file mode 100644
index 0000000..392beea
--- /dev/null
+++ b/packages/grok-taskflow/plugin/assets/taskflow-small.svg
@@ -0,0 +1,14 @@
+
diff --git a/packages/grok-taskflow/plugin/assets/taskflow.svg b/packages/grok-taskflow/plugin/assets/taskflow.svg
new file mode 100644
index 0000000..7d70b58
--- /dev/null
+++ b/packages/grok-taskflow/plugin/assets/taskflow.svg
@@ -0,0 +1,17 @@
+
diff --git a/packages/grok-taskflow/plugin/skills/taskflow/SKILL.md b/packages/grok-taskflow/plugin/skills/taskflow/SKILL.md
new file mode 100644
index 0000000..d6d5b1c
--- /dev/null
+++ b/packages/grok-taskflow/plugin/skills/taskflow/SKILL.md
@@ -0,0 +1,670 @@
+---
+name: taskflow
+description: Orchestrate multi-phase subagent workflows with Taskflow. Use whenever a request spans a whole project or many items — deeply exploring / 探索 / auditing / 审计 / analyzing a codebase, reviewing or migrating many files or modules in parallel, cross-checked/adversarial review, codebase-wide research, or any repeatable orchestration you want to save and rerun. Prefer this over ad-hoc parallel work when the task has multiple phases (discover → work → review → report) or dynamic fan-out over a discovered list. Drives the taskflow_* MCP tools.
+---
+
+
+
+# Taskflow (Grok Build)
+
+**Host binding (Grok Build):** everything below is driven through the
+`taskflow_*` MCP tools. Where an example shows a host-neutral invocation like
+`verify`, use the Grok form (`taskflow_verify` via `search_tool` /
+`use_tool`, or the namespaced form `taskflow__taskflow_verify` depending on
+how tools are announced). Each phase's subagent runs as an isolated
+`grok -p --output-format streaming-json` session.
+
+**Sandbox prerequisites:** before `taskflow_run`, require custom profiles for
+both capability modes: `PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE` should name
+a profile extending `workspace`, and `PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE`
+one extending `read-only`. If either required variable is absent, explain the
+setup and do not retry with a built-in profile; built-ins may fail open when
+kernel enforcement is unavailable.
+
+| Tool | What it does |
+|------|--------------|
+| `taskflow_run` | Run a saved flow (`name`) or an inline `define` (full DAG, or shorthand `{task}` / `{tasks}` / `{chain}`). Optional `args`, `incremental`. Returns only the final phase output + a `runId`. |
+| `taskflow_list` | List saved flows discoverable from the current working directory. |
+| `taskflow_show` | Show a saved flow's full definition as JSON. |
+| `taskflow_verify` | Statically verify a flow (cycles, missing deps, undefined refs, contract typos) — no execution, zero tokens. |
+| `taskflow_compile` | Render a flow's DAG as an inline SVG **and** text outline + a verification report — no execution. |
+| `taskflow_peek` | Inspect one phase's intermediate output from a stored run (post-hoc debugging). Omit `phaseId` to list phases; `json`/`item`/`limit` refine the slice. Hard-truncated, read-only. |
+| `taskflow_trace` | Read a run's append-only event timeline. |
+| `taskflow_replay` | Replay recorded decisions offline with optional overrides — zero model calls. |
+| `taskflow_why_stale` | Explain why phases are stale from observed and declared dependencies — zero tokens. |
+| `taskflow_recompute` | Compute the stale frontier (**dry-run only** over MCP; never executes phases). |
+| `taskflow_save` | Save a reusable flow and optional library metadata. |
+| `taskflow_search` | Search and rank reusable flows before authoring another one. |
+
+**Always `taskflow_verify` a non-trivial flow before `taskflow_run`** — it is
+free and catches most authoring mistakes.
+
+Build and run **declarative, multi-phase workflows** of subagents. The runtime
+holds intermediate results and the phase DAG, so your main context only receives
+the final answer — not every step's transcript.
+
+## Documentation map (progressive loading)
+
+This file teaches the core: phase types, control flow, interpolation, and the
+mistakes that break flows. Load the companion files **only when needed**:
+
+| File | Load when you need |
+|------|--------------------|
+| `patterns.md` | **Designing a non-trivial flow.** Proven flow archetypes (audit fan-out, self-healing rework, plan→approve→execute, dynamic replanning, tournament synthesis, incremental audit), anti-patterns, and the production-flow quality checklist. |
+| `advanced.md` | Dynamic sub-flow (`flow{def}`) contracts & security caps, and workspace isolation (`cwd: temp/dedicated/worktree`). |
+| `configuration.md` | Every knob: per-phase `model`/`thinking`/`tools`/`cwd`, concurrency model, agent discovery, `settings.json`, cross-run caching (`cache`, `fingerprint`, per-item map caching), args, storage paths. **TypeScript DSL CLI** (`taskflow-dsl` / S4). |
+| `library.md` | **Before authoring a non-trivial flow — SEARCH the reusable-flow library.** Save reusable flows with `purpose`+`tags` so future search finds them; reuse + generalize instead of rewriting from scratch. The compounding flywheel. |
+
+> Rule of thumb: writing a flow with ≥ 4 phases, a gate, or any fan-out?
+> **Read `patterns.md` first** — it will make the flow better, not just valid.
+
+## When to use
+
+- A task needs **several coordinated steps** (discover → work → review → report).
+- You need to **fan out over many items** (audit every endpoint, summarize every file).
+- You want **cross-checked / adversarial review** before reporting.
+- You want a **repeatable** orchestration you can save and rerun by name.
+- The same expensive analysis will be **re-run as the repo evolves** (use
+ `incremental: true` + fingerprints — see `configuration.md` §8).
+
+## When NOT to use
+
+- A **single-file, single-step** change you can do directly — just do it.
+- **Interactive debugging** where each step depends on watching live output.
+- Work that is **one bash command** — run it yourself, don't wrap it in a flow.
+
+## Flow design ladder
+
+Match the flow's sophistication to the task. Don't stop at level 1 when the
+task deserves level 3 — the higher levels are where taskflow pays for itself.
+
+| Level | Shape | Reach for it when |
+|-------|-------|-------------------|
+| 0 | shorthand `task` / `tasks` / `chain` | one-off delegation, simple sequence |
+| 1 | linear DAG with `dependsOn` | fixed steps, each consuming the last |
+| 2 | discover → `map` fan-out → `gate` → `reduce` | many items, needs review before reporting |
+| 3 | + `eval` zero-token gates, `expect` contracts, `retry`, `onBlock: "retry"`, `budget`, `optional` fallbacks | production-grade: self-healing, cost stop-loss, fails precisely |
+| 4 | + `loop`, `tournament`, `flow{def}` / `expand`, `race` | the work itself is discovered at runtime; one shot is unreliable; try parallel approaches and keep the first win |
+| 5 | + `incremental: true`, `cache.fingerprint` | the flow re-runs as the repo changes; only re-pay for what changed |
+
+**A production-grade flow (level 3+) usually has:** machine checks before LLM
+checks (`eval`, `script`), an `expect` contract on every JSON-emitting phase,
+`retry` on contract-checked phases, a `budget`, `optional: true` on
+degradable phases with a downstream fallback, and exactly one `final` phase.
+`patterns.md` shows each of these composed into full archetypes.
+
+## Shorthand (non-DAG)
+
+Skip the DSL entirely for simple delegations. The runtime desugars these into a
+proper flow, so you still get progress, persistence, and resume.
+
+```jsonc
+// single — one agent, one task
+{ "task": "Summarize the architecture of src/", "agent": "explorer" }
+
+// parallel — run several tasks at once, outputs merged
+{ "tasks": [
+ { "task": "Audit auth in src/api", "agent": "analyst" },
+ { "task": "Audit input validation in src/api", "agent": "analyst" }
+] }
+
+// chain — run sequentially; reference the prior step with {previous.output}
+{ "chain": [
+ { "task": "List the public API of src/lib", "agent": "scout" },
+ { "task": "Write docs for:\n{previous.output}", "agent": "writer" }
+] }
+```
+
+- `agent` is optional (defaults to the first available agent).
+- `context` (optional, per step or top-level in single mode): file paths to
+ pre-read and inject before the task — same as the full-DSL `Phase.context`
+ (per-file `contextLimit`, default 8000 chars). In **parallel `tasks` mode**
+ all branches SHARE the union of step contexts. In **chain mode** declare
+ `context` on individual steps; a top-level `context` is ignored (with a warning).
+- Add `name` to label the run.
+- Precedence if several are given: `chain` > `tasks` > `task`.
+- Pass these as the `define` argument to `taskflow_run`.
+
+## How to author a taskflow
+
+Call `taskflow_run` with an inline `define` object, or `name` for a saved flow.
+**Before running a non-trivial flow, `taskflow_verify` it — zero tokens,
+catches cycles / missing deps / undefined refs / contract typos.**
+
+### Iterating on a big flow? Use `defineFile` (write once, verify / edit / run by path)
+
+For a non-trivial flow you'll iterate on, **write the definition to a file**
+(typically in the OS tmp dir) and point every call at it with `defineFile`:
+
+```jsonc
+// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object)
+// 2. verify, iterate, run — all reference the SAME file by path:
+{ "name": "taskflow_verify", "arguments": { "defineFile": "/tmp/audit.json" } } // zero tokens
+{ "name": "taskflow_compile", "arguments": { "defineFile": "/tmp/audit.json" } } // diagram
+{ "name": "taskflow_run", "arguments": { "defineFile": "/tmp/audit.json" } }
+```
+
+The file can be raw JSON **or** a Markdown doc with a fenced ```json block
+(`write` the JSON form, or paste the flow into a note and fence it). Between
+calls, edit the file (not the call) and re-`verify`. This avoids re-sending a
+large definition on every call and keeps a durable draft you can diff. Falls
+back cleanly: precedence is `define` (inline) > `defineFile` (disk) > `name`
+(saved flow).
+
+### DSL shape
+
+```jsonc
+{
+ "name": "audit-endpoints",
+ "description": "Audit API endpoints for missing auth",
+ "args": { "dir": { "default": "src/routes" } },
+ "concurrency": 8,
+ "budget": { "maxUSD": 2.00 },
+ "agentScope": "user", // user | project | both
+ "phases": [
+ { "id": "discover", "type": "agent", "agent": "scout",
+ "task": "List endpoints under {args.dir}. Output ONLY a JSON array [{\"route\":\"\",\"file\":\"\"}].",
+ "output": "json",
+ "expect": { "type": "array", "items": { "type": "object", "required": ["route", "file"] } },
+ "retry": { "max": 2, "backoffMs": 0 } },
+ { "id": "audit", "type": "map", "over": "{steps.discover.json}", "as": "item",
+ "agent": "analyst", "task": "Audit {item.route} ({item.file}) for missing auth.",
+ "dependsOn": ["discover"] },
+ { "id": "review", "type": "gate", "agent": "reviewer",
+ "task": "Remove false positives from:\n{steps.audit.output}\nVERDICT: PASS or BLOCK.",
+ "dependsOn": ["audit"] },
+ { "id": "report", "type": "reduce", "from": ["review"], "agent": "writer",
+ "task": "Write a final report:\n{steps.review.output}", "dependsOn": ["review"],
+ "final": true }
+ ]
+}
+```
+
+### Phase types (12)
+
+| type | meaning | details |
+|------|---------|---------|
+| `agent` | one subagent runs `task` | this file |
+| `parallel` | run static `branches[]` concurrently (all complete) | this file |
+| `map` | fan out over `over` (an array) — one subagent per item, `{item}` bound | this file |
+| `gate` | quality/review step that can **halt the flow** | Gate phases below |
+| `reduce` | aggregate `from[]` phases into one output | this file |
+| `approval` | **human-in-the-loop** pause: approve / reject / edit | Approval phases below |
+| `flow` | run a **sub-flow** as one phase — saved (`use`) or runtime-generated (`def`) | summary below; deep contract in `advanced.md` |
+| `loop` | repeat a body until a condition / convergence / `maxIterations` | Loop phases below |
+| `tournament` | run N competing `variants`, a `judge` picks best or aggregates | Tournament phases below |
+| `script` | run a **shell command** (no LLM, zero tokens) — stdout is the output | Script phases below |
+| `race` | run `branches[]` concurrently; **first success wins** (unlike parallel) | Race phases below |
+| `expand` | run a dynamic fragment (`def`); `nested` (isolated) or `graft` (promote onto parent) | Expand phases below |
+
+### Control-flow fields (any phase)
+
+| field | meaning |
+|-------|---------|
+| `when` | conditional guard — skip the phase unless the expression is truthy. Supports `{refs}`, `== != < > <= >=`, `&& \|\| !`, parentheses, quoted strings/numbers. Parse errors fail **open** (phase runs). |
+| `join` | dependency join: `"all"` (default — wait for every dep) or `"any"` (OR-join — run as soon as one dep completes). |
+| `retry` | `{ "max": N, "backoffMs": ms, "factor": k }` — retry a failing subagent up to N times; delay is `backoffMs * factor^attempt` (`factor:1`=fixed, `2`=exponential). |
+| `timeout` | max ms per subagent call (>= 1000). On expiry the subagent is aborted and the phase fails with a `timedOut` marker — deterministic, **never retried**. Caps EACH call, so a map/parallel/race/loop/tournament phase's wall time is per item/iteration/variant (a tournament's judge call gets its own cap too). Script phases keep their own child-process timeout (default 60s, max 300s). Not supported on approval/flow/expand. Pair with `optional: true` + a downstream fallback phase to degrade instead of failing the run. |
+| `expect` | output contract for `output: "json"` phases (agent/gate/reduce/loop): a JSON-Schema-like shape `{type, properties, required, items, enum}` validated the moment the subagent finishes. A violation fails the phase with per-path diagnostics (e.g. `$.score: required key is missing`) and is retryable under the phase's explicit `retry`. `verify`/`compile` also statically warn when a `{steps.X.json.field}` ref names a field absent from X's declared contract. |
+| `idempotent` | side-effect classification. Default `true` (safe to cache + auto-retry). Set `false` on phases with **irreversible side effects** (webhook POSTs, deploys, DB writes, file mutations): transient provider errors are **not** auto-retried (an explicit `retry{}` IS still honored — it's your declaration that repeats are acceptable) and the result is **never cached** in any scope (within-run resume, cross-run, `incremental` — the phase re-runs every time). The phase state records `sideEffect: true` (rendered as ⚡). |
+| `optional` | fail-soft — a failed/blocked phase won't abort the run; downstream sees empty output. Pair with a fallback phase guarded by `when`. |
+| `cache` | per-phase reuse policy (`run-only` default / `cross-run` / `off`). See `configuration.md` §8. |
+
+### Conditional routing (when + gate/branches)
+
+Pair `when` with an upstream phase that emits a decision to build real if/else
+routing. Use `join: "any"` on the merge phase so it runs whichever branch fired.
+For static (non-conditional) concurrency, a `parallel` phase runs fixed
+`branches[]` instead — `{ "type": "parallel", "branches": [{"task":"..."}, {"task":"...","agent":"reviewer"}] }`.
+
+```jsonc
+{ "id": "triage", "type": "agent", "agent": "analyst", "output": "json",
+ "task": "Classify the task. Output ONLY {\"route\":\"deep\"} or {\"route\":\"quick\"}.",
+ "expect": { "type": "object", "required": ["route"], "properties": { "route": { "enum": ["deep", "quick"] } } } },
+{ "id": "deep", "when": "{steps.triage.json.route} == deep", "dependsOn": ["triage"], "agent": "analyst", "task": "..." },
+{ "id": "quick", "when": "{steps.triage.json.route} == quick", "dependsOn": ["triage"], "agent": "executor-fast", "task": "..." },
+{ "id": "report", "type": "reduce", "from": ["deep","quick"], "join": "any",
+ "dependsOn": ["deep","quick"], "agent": "writer", "task": "...", "final": true }
+```
+
+> `when` should reference **upstream** (`dependsOn`) phases — a ref to a phase
+> that hasn't completed resolves empty and the guard is treated as false. Note
+> the `expect` enum on the router: it converts "the router said `Deep` with a
+> capital D and both branches silently skipped" into an immediate retryable
+> failure at the router.
+
+### Gate phases (quality control)
+
+A `gate` phase runs an agent to review upstream output and can **block the rest
+of the workflow**. The runtime needs to read a verdict from the agent's output.
+There are three ways to provide one, in order of robustness:
+
+**1. JSON contract (most robust — preferred).** Set `output: "json"` + an `expect`
+enum so the output is machine-validated. A verdict that isn't exactly `"pass"` or
+`"block"` (wrong case, extra formatting, a synonym) fails the `expect` contract and
+is retried — the verdict can never be silently misread.
+
+```jsonc
+{ "id": "review", "type": "gate", "agent": "reviewer", "dependsOn": ["impl"],
+ "output": "json",
+ "expect": { "type": "object",
+ "properties": { "verdict": { "enum": ["pass", "block"] }, "reason": { "type": "string" } },
+ "required": ["verdict", "reason"] },
+ "task": "Review the diff. Respond ONLY with JSON: {\"verdict\":\"pass\"|\"block\",\"reason\":\"...\"}" }
+```
+
+**2. Explicit text marker.** End the task by asking the agent to emit a final line
+`VERDICT: PASS` or `VERDICT: BLOCK` (also accepts OK/FAIL/STOP/REJECT/HALT; common
+Markdown emphasis like `VERDICT: **BLOCK**` is tolerated). JSON objects such as
+`{"continue": false, "reason": "missing auth checks"}` / `{"verdict": "block"}` also work.
+
+**3. Auto-appended format suffix.** If a free-text gate's task does **not** already
+ask for a `VERDICT:` marker (and has no JSON contract), the runtime automatically
+appends the exact format instruction. You don't need to remember to add it — but
+writing it yourself (option 2) makes the intent explicit in your flow.
+
+On **BLOCK**, downstream phases are skipped and the run ends as `blocked` with the
+reason surfaced. Unparseable gate **model output fails closed** (treated as BLOCK):
+a gate that cannot reach a verdict cannot be trusted to pass (issue #54). Note
+that *config* slips (an unresolved `score.target`, malformed `scorers`) are
+different and still fail **open** with a warning — those are authoring errors that
+degrade to the historical behavior, not a judge that couldn't decide. An explicit
+non-blocking JSON verdict (e.g. `{"verdict":"No issues found"}`) is a semantic PASS,
+not ambiguity.
+
+**Zero-token machine checks (`eval`) — use these before spending tokens.**
+List machine-checkable assertions in `eval`. If **all** pass, the gate
+auto-passes with **no LLM call**; if any fails, it falls through to the LLM
+`task` (the qualitative residue). Each entry supports the `when` operators plus
+`X contains Y` (substring). A parse error fails **open**.
+
+```jsonc
+{ "id": "quality", "type": "gate", "dependsOn": ["build","test"],
+ "eval": ["{steps.build.output} contains BUILD SUCCESS", "{steps.test.json.failures} == 0"],
+ "task": "Review the diff for subtle logic errors a linter can't catch. VERDICT: PASS or BLOCK." }
+```
+
+**Self-healing (`onBlock: "retry"`).** By default a blocking gate halts the run
+(`onBlock: "halt"`). With `onBlock: "retry"` the gate instead **re-runs its
+upstream `dependsOn` phases and re-evaluates**, up to `retry.max` rounds (or
+until PASS / budget / abort) — a generate→critique→regenerate rework loop. See
+`patterns.md` for the full archetype.
+
+```jsonc
+{ "id": "spec-gate", "type": "gate", "onBlock": "retry", "retry": { "max": 3 },
+ "dependsOn": ["implement"],
+ "task": "Does the implementation satisfy ALL acceptance criteria? VERDICT: PASS or BLOCK with reasons." }
+```
+
+**Scoring gates (`score`) — graded, composable, auditable quality checks.**
+Where `eval` gives boolean assertions, `score` runs deterministic scorers
+against a target string at **zero tokens**, combines them into a [0,1] score,
+and only escalates to an LLM when they can't decide. The structured result is
+the gate's `.json` — downstream phases read `{steps..json.combined}` /
+`.json.results.0.passed` and route on quality, not just pass/fail.
+
+| field | meaning |
+|-------|---------|
+| `target` | interpolation ref for the scored string (default `{previous.output}`) |
+| `scorers` | array of checks: `exact-match` (`value`), `contains` (`value`), `regex` (`pattern`, optional `negate`), `json-schema` (`schema`, an `expect`-style contract), `length-range` (`min`/`max`), `code-compiles` (`language`: javascript\|typescript) |
+| `combine` | `all` (default) / `any` / `weighted` |
+| `weights` | weighted only — one entry per scorer, **+1 trailing entry for the judge** when present |
+| `threshold` | weighted only — combined-score cutoff in (0,1], default 0.5 |
+| `judge` | optional LLM-as-judge fallback `{agent?, task}` — runs when the deterministics fail (and, for `all`/`any`, whenever configured); sees the target + scorer report; returns `{"score": 0-1, "verdict": "pass"\|"block", "reason"}` |
+
+Decision order: (1) deterministics pass **and the judge cannot veto** →
+**auto-PASS, zero LLM tokens** — that means: no judge configured, or `weighted`
+where the deterministic score is a lower bound already clearing the threshold
+(the judge could not drop it). With `all`/`any` + a judge the judge **always
+runs** — its verdict is authoritative (it may check what scorers cannot, e.g.
+factuality); (2) fail + `judge` → judge decides; (3) fail + `task` → the gate
+task runs with the scorer report appended; (4) fail + no fallback → **explicit
+BLOCK** (a deterministic failure is not ambiguity). Fail-closed: an unparseable
+judge → BLOCK (issue #54); unresolved `target` with no fallback → PASS +
+warning (config slip, not a judge verdict); malformed `score` → the plain LLM
+gate. **Security:** LLM-generated dynamic sub-flows
+(`flow{def}`) may not use `code-compiles` (compiler execution) or `regex`
+(ReDoS) scorers — same hardening class as the `script` block.
+
+```jsonc
+{ "id": "quality", "type": "gate", "dependsOn": ["gen"],
+ "score": {
+ "target": "{steps.gen.output}",
+ "scorers": [
+ { "type": "json-schema", "name": "shape", "schema": { "type": "object", "required": ["summary", "risks"] } },
+ { "type": "regex", "name": "no-placeholders", "pattern": "TODO|TBD", "negate": true },
+ { "type": "length-range", "name": "substantive", "min": 200 }
+ ],
+ "combine": "weighted", "weights": [3, 2, 1, 2], "threshold": 0.8,
+ "judge": { "agent": "reviewer", "task": "Score the analysis quality 0-1: depth, evidence, actionability." }
+ } }
+// downstream: { "when": "{steps.quality.json.combined} >= 0.9", ... }
+```
+
+### Approval phases (human-in-the-loop)
+
+An `approval` phase pauses the run and asks the operator to **Approve / Reject /
+Edit**. Distinct from `gate` (an *agent* reviewing): this is a *human* deciding.
+The (interpolated) `task` is the prompt shown.
+
+- **Approve** → continue; the phase output is `(approve)`.
+- **Reject** → halt the flow (same mechanism as a blocking gate).
+- **Edit** → the typed note becomes this phase's `output` — inject guidance
+ mid-run and reference it downstream with `{steps..output}`.
+- **Non-interactive** runs (headless/CI/print mode) **auto-reject** and record it.
+- **Background (detached)** runs **auto-reject** (no interactive approver);
+ downstream sees the rejection; the flow continues (fail-open).
+
+> **MCP-host caveat (Codex / Claude Code / OpenCode):** MCP-driven runs are
+> non-interactive, so an `approval` phase **auto-rejects**. Prefer a `gate`
+> (agent review) in flows you run through the `taskflow_*` tools; use `approval`
+> only in flows a human runs interactively.
+
+### Sub-flows (composition) — summary
+
+A `flow` phase runs another taskflow as a single phase and bubbles up its final
+output. Two mutually-exclusive sources:
+
+- **Saved** (`use`): `{ "type": "flow", "use": "deep-research", "with": { "topic": "{item}" } }`
+ — args via `with` (string values interpolate); recursion is detected and rejected.
+- **Runtime-generated** (`def`): `{ "type": "flow", "def": "{steps.plan.json}" }`
+ — an upstream planner emits a whole flow as JSON; the runtime validates it
+ (cycles / dangling refs / security caps) then runs it nested. This is how a
+ planner decides *at runtime* what work to spawn — the declarative answer to a
+ code-mode `for`/`if` loop.
+
+The `def` output contract, fail-open semantics (`defError`), nesting/breadth
+caps, and the iterative-replanning pattern (`loop` + `flow{def}`) are in
+`advanced.md`. The plan→execute and replan archetypes are in `patterns.md`.
+
+### Loop phases (iterate until done)
+
+A `loop` phase runs its body repeatedly, exposing each iteration's output as
+`{steps..output}` / `.json` so the next round can react to the last. It
+stops on the first of: `until` truthy, **convergence** (output stops changing),
+or `maxIterations` (hard cap, required). The runtime always terminates.
+
+- `until` — stop condition, same operators as `when` (a parse error stops the loop, fail-safe).
+- `maxIterations` — hard iteration cap (required).
+- `convergence` — `true` to stop early when an iteration's output equals the previous one.
+- `reflexion` — `true` to feed each iteration a structured summary of the prior one (see below).
+
+```jsonc
+{
+ "id": "refine", "type": "loop", "agent": "executor",
+ "maxIterations": 5,
+ "until": "{steps.refine.json.done} == true",
+ "convergence": true,
+ "task": "Improve the draft. When nothing else needs fixing, output JSON {\"done\":true,\"draft\":\"...\"}; otherwise {\"done\":false,\"draft\":\"...\"}.",
+ "output": "json",
+ "expect": { "type": "object", "required": ["done", "draft"] },
+ "final": true
+}
+```
+
+**Reflexion memory (`reflexion: true`).** By default each iteration sees only
+the prior *output* — the *reason* it wasn't good enough (an `expect` contract
+violation, an error, the unmet `until`) is discarded, so models repeat mistakes.
+With `reflexion: true`, every iteration after the first receives a structured
+failure summary of the prior one via the `{reflexion}` placeholder
+(auto-appended if the task omits it, with a one-time warning; capped at 2000
+chars): contract diagnostics like `$.done: required key is missing`, the
+(sanitized) error, or the unmet stop condition, plus a truncated output
+snippet. Iteration 1 sees a sentinel.
+
+Semantics shift to enable self-correction: **body failures become feedback
+instead of terminating the loop**. Timeout/abort/over-budget still hard-stop,
+and if `maxIterations` exhausts with the last iteration failed, the phase fails
+(reflexion defers failure, never erases it). Cost is bounded by `maxIterations`
++ the run `budget`.
+
+```jsonc
+{ "id": "emit-plan", "type": "loop", "reflexion": true, "maxIterations": 4,
+ "output": "json", "expect": { "type": "object", "required": ["steps", "done"] },
+ "until": "{steps.emit-plan.json.done} == true",
+ "task": "Emit the migration plan as JSON {steps:[...], done:bool}.\n{reflexion}" }
+```
+
+### Tournament phases (N variants, judge picks best)
+
+A `tournament` phase runs `variants` competing attempts in parallel, then a
+**judge** sub-phase selects the winner (`mode: "best"`) or merges them
+(`mode: "aggregate"`). Use it when one shot is unreliable and you want the best
+of several drafts, or a synthesis of diverse approaches.
+
+- `variants` — number of competing variants spawned from `task` (default 3, max 20).
+ For genuinely different *approaches*, use `branches` instead — an explicit
+ array of `{task, agent?}` definitions (e.g. one conservative, one aggressive).
+- `mode` — `"best"` (judge picks one winner, default) or `"aggregate"` (judge merges all).
+- `judge` — the judge's rubric/instructions. `judgeAgent` — optional judge agent
+ (defaults to the phase `agent`; use a stronger model here).
+- **Winner format — prefer JSON.** Have the judge return `{"winner": }` (and an
+ optional `"reason"`); the runtime also reads a `WINNER: ` line (`#3` and
+ common Markdown emphasis like `WINNER: **3**` are tolerated — issue #54).
+ JSON is more robust than a text marker: there's no formatting the model can
+ get subtly wrong.
+- Fail-open: if the judge's pick is still unparseable, variant 1 is returned
+ (work is never lost — the variants are already computed, so blocking would be
+ worse than picking a safe default).
+
+```jsonc
+{
+ "id": "headline", "type": "tournament", "agent": "executor",
+ "variants": 3, "mode": "best",
+ "judge": "Pick the clearest, most accurate headline. Return JSON {\"winner\": , \"reason\": \"...\"}.",
+ "task": "Write one headline for the article below.\n\n{steps.draft.output}",
+ "dependsOn": ["draft"], "final": true
+}
+```
+
+### Script phases (shell commands, zero tokens)
+
+A `script` phase runs a **shell command** directly — no subagent, no tokens — and
+captures its stdout as the phase output. Use it to anchor LLM phases to ground
+truth: builds, tests, `git`, formatters, scoring scripts. **Prefer a `script`
+phase over asking an agent to run a command** — it is cheaper, faster, and the
+output is exact.
+
+- `run` — **required**. A **string** runs through a shell; an **array** is
+ spawned directly (execvp, no shell). A string `run` containing an
+ interpolation placeholder is **rejected at validation** (shell-injection
+ guard) — use the array form or `input` for dynamic values.
+- `input` — optional text piped to stdin (supports interpolation).
+- `timeout` — optional ms cap (1000–300000, default 60000); SIGTERM → SIGKILL on expiry.
+- A non-zero exit fails the phase (stderr captured); stdout capped at 1 MB.
+ No `retry`, no `output: "json"`; **excluded from cross-run cache** (may have
+ side effects). Not allowed inside LLM-generated dynamic sub-flows (RCE guard).
+
+```jsonc
+{ "id": "build", "type": "script", "run": "pnpm run build", "timeout": 120000 },
+{ "id": "score", "type": "script", "run": ["python", "score.py"],
+ "input": "{steps.analyze.output}", "dependsOn": ["analyze"], "final": true }
+```
+
+### Race phases (first success wins)
+
+A `race` phase runs static `branches[]` concurrently and **returns the first
+branch that finishes successfully** (failed settles do **not** win — a slower
+success still wins over a fast hard-fail). Unlike `parallel` (waits for all) or
+`tournament` (judges quality after all variants), use race when latency matters
+more than comparing every approach.
+
+- `branches` — **required**, at least two `{task, agent?}`.
+- `cancelLosers` — optional boolean (default `true`). After the first **success**,
+ abort other branches via `AbortSignal` (best-effort — host must honor the
+ signal). Set `false` to let losers finish naturally.
+- Phase `usage` **aggregates all branches** (including aborted partials) so
+ budgets stay honest.
+- Output of the winning branch becomes the race phase output; a warning records
+ which branch won.
+
+```jsonc
+{
+ "id": "quick", "type": "race",
+ "branches": [
+ { "task": "Answer with a short heuristic…", "agent": "executor" },
+ { "task": "Answer with a thorough search…", "agent": "researcher" }
+ ],
+ "final": true
+}
+```
+
+### Expand phases (dynamic fragment: nested or graft)
+
+An `expand` phase runs a **fragment Taskflow** from `def` (inline object,
+phases array, or interpolated `{steps.plan.json}`). Two modes:
+
+| `expandMode` | Behavior |
+|--------------|----------|
+| `nested` (default) | Run as an isolated sub-flow (like `flow{def}`); child phase ids stay **off** the parent. |
+| `graft` | After success, **promote** child phase states onto the parent as `-` so later phases can read `{steps.grow-leaf.output}`. |
+
+- `def` — **required** for expand.
+- `maxNodes` — optional cap on fragment phase count (default 50, hard max 100).
+- Dynamic validation + nesting caps match `flow{def}` (see `advanced.md`).
+- Prefer `expand` when the planner fragment is a first-class kind; prefer
+ `flow` + `use` for saved reusable flows; prefer `flow` + `def` when you want
+ the classic nested sub-flow without graft promote.
+
+```jsonc
+{
+ "id": "grow", "type": "expand", "expandMode": "graft",
+ "def": "{steps.plan.json}",
+ "dependsOn": ["plan"], "final": true
+}
+```
+
+### Budget (observed-usage stop-loss)
+
+Add a run-wide stop-loss at the top level. Ordinary budgeted DAG layers and
+`map`/`parallel`/`tournament` fan-out use serial call admission. Once reported
+cost/tokens exceed the threshold, no new model call is started; the run ends as
+`blocked` with partial outputs preserved. An admitted call may cross the
+threshold. A `race` necessarily starts competing branches together, so all
+already-active race branches may contribute overshoot. This is never a
+zero-overshoot guarantee.
+
+```jsonc
+{ "name": "...", "budget": { "maxUSD": 1.50, "maxTokens": 2000000 }, "phases": [ ... ] }
+```
+
+**Any flow with a fan-out should have a `budget`** — a map over a
+mis-discovered 500-item array is otherwise unbounded spend.
+
+Host accounting matters: Codex reports tokens but not cost, so Codex accepts
+`maxTokens` and rejects `maxUSD`. Grok 0.2.93 reports neither and rejects every
+flow declaring `budget`. Pi, Claude Code, and OpenCode accept both dimensions.
+
+### Strict interpolation
+
+By default an unresolved placeholder (typo'd `{steps.X.output}`, missing
+`{args.Y}`) resolves to an empty string and validation issues a *warning* —
+the flow still runs, possibly doing subtly wrong work. Set
+`"strictInterpolation": true` at the flow level to promote unresolved
+placeholders and missing-dep/arg warnings to **hard errors**. Recommended for
+any flow you save — a saved flow will be run later with args you're not
+watching.
+
+## Interpolation
+
+- `{args.X}` — invocation argument
+- `{steps.ID.output}` — a prior phase's text output
+- `{steps.ID.json}` / `{steps.ID.json.field}` — prior output parsed as JSON
+- `{item}` / `{item.field}` — current item inside a `map` phase
+- `{previous.output}` — the immediately-upstream phase output
+- `{loop.iteration}` / `{loop.lastOutput}` / `{loop.maxIterations}` — inside a `loop` body: the 1-based round, the prior iteration's output, and the cap
+- `{reflexion}` — inside a `loop` body with `reflexion: true`: the structured failure summary of the prior iteration (sentinel on iteration 1)
+
+Interpolation also runs on a scoring gate's `score.target` and `score.judge.task`
+— refs there need `dependsOn` like any other `{steps.X}` use.
+
+## Rules that make flows work
+
+1. For a `map` phase, make the upstream phase **emit a JSON array** and set
+ `output: "json"` on it. Tell that agent to output **only** JSON, and pin the
+ shape with an `expect` contract + `retry`.
+2. Give each phase a clear, single responsibility.
+3. Reference upstream results explicitly with `{steps.ID...}` and set `dependsOn`.
+4. Mark the result-bearing phase with `"final": true` (else the last phase wins).
+5. Machine checks before LLM checks: `script` for ground truth, gate `eval`
+ before gate `task`, `expect` before a downstream "did it parse?" phase.
+6. **Decision phases should emit structured output, not free text.** Any phase
+ whose output is a *decision* a downstream phase (or the runtime) acts on — a
+ gate verdict, a router's branch, a tournament winner, a judge's score — should
+ use `output: "json"` + an `expect` enum/contract so the decision is
+ machine-validated. Free-text markers (`VERDICT:`, `WINNER:`, `SCORE:`) are
+ tolerated and Markdown-emphasis-tolerant (issue #54), but a JSON contract is
+ strictly more robust: there's no formatting the model can get subtly wrong, and
+ a malformed decision fails the contract (retryable) instead of being silently
+ mis-read.
+7. `verify` before `run` for anything non-trivial (zero tokens).
+
+## Common mistakes (the runtime rejects these at validation time)
+
+### 1. Referencing `{steps.X}` without `dependsOn: ["X"]`
+
+```jsonc
+// ❌ WRONG — 'fix-issues' runs in parallel with 'code-review-1' and sees the
+// literal string "{steps.code-review-1.output}" instead of the review text.
+{ "id": "code-review-1", "type": "agent", "task": "review code" },
+{ "id": "fix-issues", "type": "agent",
+ "task": "fix {steps.code-review-1.output}" } // ← no dependsOn!
+```
+
+Validation rejects this: `Phase 'fix-issues': task references
+{steps.code-review-1.*} but 'code-review-1' is not in dependsOn. ...`
+**Always declare the chain:**
+
+```jsonc
+// ✅ RIGHT
+{ "id": "code-review-1", "type": "agent", "task": "review code" },
+{ "id": "fix-issues", "type": "agent",
+ "task": "fix {steps.code-review-1.output}",
+ "dependsOn": ["code-review-1"] }
+```
+
+Tip: write the `task` first (it tells you what each phase needs), then scan for
+`{steps.*}` references and add the matching `dependsOn`.
+Exception: phases with `join: "any"` are exempt (they deliberately wait for only
+one dep and may reference others as informational context).
+
+### 2. Assuming the runtime knows "this is a chain"
+
+Phase order in the `phases` array is **documentation, not execution order**.
+The DAG comes from `dependsOn`. Four phases listed in order with no `dependsOn`
+are four **parallel** phases, all racing in layer 0. Use the shorthand `chain`
+if you literally want `a → b → c → d`, or write explicit `dependsOn`.
+
+### 3. Underscores in ids / invented agent names
+
+Phase ids and agent names use **hyphens** (`audit-each`, `risk-reviewer`).
+An unknown agent name fails the phase with the list of available agents.
+Built-in agents: `executor`, `executor-code` (complex, multi-file),
+`executor-fast` (trivial), `executor-ui`, `scout` (cheap recon), `planner`,
+`analyst`, `critic`, `reviewer`, `risk-reviewer`, `security-reviewer`,
+`plan-arbiter`, `final-arbiter`, `test-engineer`, `doc-writer`, `verifier`,
+`recover`, `visual-explorer`. **Do not invent agent names** — omit `agent` to
+use the default. Use cheap agents (`scout`) for discovery and strong agents
+(`critic`, `final-arbiter`) for gates/judging.
+
+## Operating a run (lifecycle & inspection)
+
+A run moves through: **running →** `completed` (a `final` phase produced output)
+**/** `blocked` (gate BLOCK, approval rejected, or `budget` hit) **/** `failed`
+(a non-`optional` phase errored) **/** `paused` (aborted).
+
+`taskflow_run` reports a `runId`. If the final output looks wrong, don't
+re-run blind — `taskflow_peek` the run: omit `phaseId` to list phase statuses
+and output sizes, then peek the suspicious phase (`json: true` for parsed
+output, `item: n` for one fan-out section). Output is hard-truncated
+(default 4000 chars, max 32000) so a peek never floods your context.
+
+Use `taskflow_trace` to inspect the append-only event log for a finished run,
+then `taskflow_replay` to re-judge it under alternate thresholds/budget **offline
+(zero tokens)** — e.g. "would a 0.9 gate threshold have blocked this run?"
+
+For flows re-run as the repo evolves, pass `incremental: true` to
+`taskflow_run` — every phase defaults to **cross-run cache reuse**: identical
+input → $0 instant hit. Per-phase `cache.fingerprint` entries
+(`git:HEAD`, `glob!:src/**/*.ts`, `file:package.json`) invalidate on world
+changes; a cached `map` re-executes only changed items. See `configuration.md` §8.
diff --git a/packages/grok-taskflow/plugin/skills/taskflow/advanced.md b/packages/grok-taskflow/plugin/skills/taskflow/advanced.md
new file mode 100644
index 0000000..e15d3a0
--- /dev/null
+++ b/packages/grok-taskflow/plugin/skills/taskflow/advanced.md
@@ -0,0 +1,163 @@
+
+
+# Taskflow Advanced — dynamic sub-flows & workspace isolation
+
+Load this when a flow needs: runtime-generated work (`flow{def}` / `expand`) or
+isolated working directories (`cwd: temp/dedicated/worktree`).
+
+---
+
+## `flow{def}` vs `expand` (when to use which)
+
+| Need | Prefer |
+|------|--------|
+| Saved reusable flow by name | `flow` + `use` |
+| Planner JSON as isolated nested sub-flow (classic) | `flow` + `def` **or** `expand` + `expandMode: "nested"` |
+| Fragment phases must appear on the **parent** run as `-` | `expand` + `expandMode: "graft"` |
+| First of several static approaches (latency) | `race` (not tournament) |
+
+`expand` is a first-class phase type (Horizon B). Dynamic validation / nesting /
+breadth caps match `flow{def}`. **Event kernel** still excludes `race`/`expand`
+(imperative path only until step handlers exist).
+
+---
+
+## Dynamic sub-flows (`flow{def}`) — the full contract
+
+A `flow` phase with `def` resolves a sub-flow **at runtime**, usually from an
+upstream phase's JSON output. The runtime interpolates + JSON-parses the `def`,
+validates it, then runs it nested. This is how a planner decides at runtime
+what work to spawn — with each generated plan checked before it spends a token.
+
+```jsonc
+{ "id": "plan", "type": "agent", "agent": "planner", "output": "json",
+ "task": "Scan the repo. Output ONLY JSON {\"name\":\"audit\",\"phases\":[...]} — one audit phase per file." },
+{ "id": "run", "type": "flow", "def": "{steps.plan.json}", "optional": true,
+ "dependsOn": ["plan"], "final": true }
+```
+
+**LLM output contract for `def`** (put this in the planner's task):
+- A *full* Taskflow `{"name":"...","phases":[...]}`, a bare `phases` array, or
+ `{"phases":[...]}` — pure JSON (a ```json fence is tolerated and stripped).
+- Hyphens in ids, never underscores.
+- Sub-flow phases reference each other in their **own** `{steps.x.output}`
+ namespace (no parent-id prefixing).
+- An **empty** `phases` array is a valid no-op (the planner decided there's
+ nothing to do).
+
+**Security caps on generated flows** (validation rejects; tell the planner not
+to emit these so a retry isn't wasted):
+- **No `script` phases** — shell execution from an LLM-authored plan is an RCE
+ vector; only author-written flows may use `script`.
+- **No workspace `cwd` keywords** (`temp`/`dedicated`/`worktree`) and no `cwd`
+ escaping the run directory.
+- Breadth caps: ≤100 phases, concurrency ≤16 (flow and per-phase).
+- Depth: inline nesting capped at 5 (shared with `ctx_spawn` subflows).
+
+**Fail-open semantics:** if the `def` doesn't parse, has the wrong shape, or
+fails validation, the phase completes with `status: "done"` and a `defError`
+diagnostic field; downstream phases receive empty output and the run continues.
+Design for it:
+- Add `optional: true` on the flow phase so a bad plan never aborts the run.
+- Want a hard stop instead? Add a downstream gate:
+ `{ "type": "gate", "eval": ["{steps.run.output} != "], "task": "…VERDICT: BLOCK if the plan failed." }`
+
+**Iterative replanning** — pair `flow{def}` with a `loop` whose body emits the
+next plan from the previous round's result: the declarative equivalent of
+`for (...) { read result; decide next }`. See `examples/dynamic-plan-execute.json`
+and `examples/iterative-replan.json`.
+
+---
+
+## Workspace isolation (`cwd` keywords)
+
+A phase's `cwd` is normally a literal path (or inherited from the run). Three
+**reserved keywords** ask the runtime to allocate an isolated working directory
+for the phase's subagent and tear it down afterwards — scratch work or file
+mutation without touching the main tree:
+
+| `cwd` value | what the runtime does | lifecycle |
+|-------------|-----------------------|-----------|
+| `"temp"` | ephemeral dir under the OS tmpdir | removed when the phase finishes |
+| `"dedicated"` | persistent dir under the run state (`runs/ws//`) | **kept** for inspection; deterministic per phase (resume reuses it) |
+| `"worktree"` | `git worktree add` on a throwaway branch off `HEAD` | `git worktree remove` + branch delete when the phase finishes |
+
+```jsonc
+{ "id": "experiment", "type": "agent", "agent": "executor", "cwd": "worktree",
+ "task": "Try the risky refactor and run the tests. Your edits are isolated in a git worktree." }
+```
+
+- **Fail-open.** If allocation fails (e.g. `worktree` outside a git tree), the
+ phase degrades — `worktree`→`temp`, any other failure → the base cwd — with a
+ `warnings` diagnostic. A phase never fails to run because of isolation.
+- **Security.** Keywords are honoured only in **author-written** flows; a
+ generated plan (`flow{def}` / `ctx_spawn` subflow) requesting one is rejected
+ at validation.
+- A literal path passes through unchanged.
+
+**Pattern — competing experiments in worktrees:** run two `parallel` branches,
+each `cwd: "worktree"`, each attempting a different refactor strategy and
+reporting its test results; a downstream gate/judge picks which diff to apply
+for real. The main tree is never touched by the losers.
+
+---
+
+## Trace & offline replay (`trace` / `replay`) — vs resume / recompute
+
+Three **different** reuse tools; do not conflate them:
+
+| Tool | Spends tokens? | Mutates the run? | Answers |
+|------|----------------|------------------|---------|
+| **`resume`** | Only unfinished / cache-miss phases | Continues the same run | "Pick up where we stopped" |
+| **`why-stale` → `recompute`** | Dry-run free; `--apply` / `dryRun:false` spends | Optional write of recompute result | "World/input changed — which phases re-run?" |
+| **`trace` → `replay`** | **Never** | Never | "If the gate threshold / budget had been different, would we have blocked?" |
+
+### Trace (read the evidence)
+
+Every instrumented run may write an append-only **event log**
+(`runs//.trace.jsonl`): phase lifecycle, each subagent
+input/output, and runtime **decisions** (gate verdict/score, when-guard,
+cache-hit, budget-hit, tournament-winner, unreplayable).
+
+```
+taskflow_trace { runId: "" }
+taskflow_trace { runId: "", json: true }
+```
+
+MCP trace responses are bounded. JSON mode returns an envelope with
+`total`/`returned`/`truncated`; use `limit` (default 200, max 1000) to select the
+newest events without flooding the host context.
+
+If there is no log (pre-trace run, or no sink injected), the tool reports that
+clearly — it never invents events.
+
+### Offline replay (what-if, zero tokens)
+
+`replay` **re-folds** the recorded log under alternate **decision knobs** without
+calling any model:
+
+- `thresholds` — map of `phaseId → new score threshold` (gate-score events)
+- `budgetMaxUSD` / `budgetMaxTokens` — would later phases have been skipped?
+- `models` / `args` — currently report `needs-live-rerun` (quality cannot be
+ re-judged offline without re-execution)
+
+Outcomes per phase: `reused`, `would-block`, `verdict-flipped`,
+`would-exceed-budget`, `threshold-changed`, `needs-live-rerun`, `failed`.
+
+```
+taskflow_replay { runId: "", thresholds: { review: 0.9 } }
+taskflow_replay { runId: "", budgetMaxUSD: 0.05, json: true }
+```
+
+**Import-graph guarantee:** `replayRun` never imports the process-spawning
+runtime or event kernel — offline replay cannot accidentally spend tokens.
+
+### When to use which
+
+| Situation | Use |
+|-----------|-----|
+| Rate-limit mid-run; inputs unchanged | `resume` |
+| Repo file changed; re-pay only affected phases | `why-stale` → `recompute` |
+| "Would a stricter gate have blocked last night's run?" | `trace` → `replay` with new `thresholds` |
+| "Would a $0.10 cap have stopped the fan-out?" | `replay` with `budgetMaxUSD` |
+| Need fresh model judgment under a new model id | `replay` will say `needs-live-rerun` → live `recompute`/`run` |
diff --git a/packages/grok-taskflow/plugin/skills/taskflow/configuration.md b/packages/grok-taskflow/plugin/skills/taskflow/configuration.md
new file mode 100644
index 0000000..2879e19
--- /dev/null
+++ b/packages/grok-taskflow/plugin/skills/taskflow/configuration.md
@@ -0,0 +1,522 @@
+
+
+# Taskflow Configuration Reference
+
+Every knob you can set on a taskflow, where it lives, and how the values are
+resolved. Read this when you need fine control over models, concurrency, agent
+discovery, working directories, tool restrictions, or storage.
+
+> Companion files: `SKILL.md` (core DSL + actions), `patterns.md` (flow
+> archetypes + production checklist), `advanced.md` (context sharing, dynamic
+> sub-flows, workspace isolation, incremental recompute).
+
+Configuration lives in **five layers**, from most local to most global:
+
+| Layer | Where | Sets |
+|-------|-------|------|
+| Phase | a phase object in the DSL | per-step model/thinking/tools/cwd/output/concurrency |
+| Flow | the top-level DSL object | name, args, default concurrency, agent scope |
+| Agent | `~/.pi/agent/agents/*.md`, `.pi/agents/*.md` frontmatter | per-agent default model/thinking/tools + system prompt |
+| Settings | `~/.pi/agent/settings.json` | `modelRoles`, global thinking |
+| Environment | shell env | `PI_TASKFLOW_PI_BIN` |
+
+---
+
+## 1. Flow-level options
+
+Top-level keys of the taskflow definition object.
+
+```jsonc
+{
+ "name": "audit-endpoints", // required — also becomes /tf: when saved
+ "description": "Audit API auth", // shown in /tf list and the command palette
+ "concurrency": 8, // default max concurrent subagents (default: 8)
+ "agentScope": "user", // user | project | both (default: user)
+ "args": { /* see §3 */ },
+ "phases": [ /* see §2 */ ] // required, at least one phase
+}
+```
+
+| Key | Type | Default | Notes |
+|-----|------|---------|-------|
+| `name` | string | — | **Required.** Saved as `/tf:`. |
+| `description` | string | — | Surfaced in `/tf list` and the slash-command. |
+| `concurrency` | number | `8` | Default fan-out / same-layer parallelism cap. See §4. |
+| `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. |
+| `args` | record | `{}` | Declared invocation arguments. See §3. |
+| `phases` | array | — | **Required.** The phase DAG. See §2. |
+| `version` | number | `1` | Informational metadata in 0.2.0; it does not select runtime semantics or migrate a flow. |
+
+---
+
+## 2. Phase-level options
+
+Keys of each object in `phases[]`. Some only apply to specific `type`s.
+
+```jsonc
+{
+ "id": "audit", // required, unique — referenced via {steps.audit.output}
+ "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent)
+ "agent": "analyst", // agent name to run this phase
+ "task": "Audit {item.route}…",
+ "dependsOn": ["discover"],// DAG edges
+ "over": "{steps.discover.json}", // [map] array to fan out over
+ "as": "item", // [map] loop var name (default: item)
+ "branches": [ /* … */ ], // [parallel|race] static task list
+ "from": ["audit"], // [reduce] phase ids to aggregate
+ "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow
+ "expandMode": "nested", // [expand] nested | graft
+ "output": "json", // text | json (default: text)
+ "model": "claude-sonnet-4-5", // per-phase model override
+ "thinking": "high", // per-phase thinking override
+ "tools": ["read","bash"], // restrict tools for this phase's subagent
+ "cwd": "packages/api", // working directory for this phase's subagent
+ "concurrency": 4, // [map/parallel] fan-out cap for THIS phase
+ "final": true // mark this phase's output as the workflow result
+}
+```
+
+| Key | Applies to | Default | Notes |
+|-----|-----------|---------|-------|
+| `id` | all | — | **Required, unique.** Used in `{steps.…}`. |
+| `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). |
+| `agent` | all | first available | Agent name; resolved from the scoped pool. |
+| `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types. |
+| `over` | map | — | **Required for map.** Must resolve to an array. |
+| `as` | map | `item` | Loop variable bound per item. |
+| `branches` | parallel, race | — | **Required** (≥1 for parallel; ≥2 for race). `[{task, agent?}]`. |
+| `cancelLosers` | race | `true` | Abort in-flight losers after first **success** (best-effort AbortSignal). |
+| `from` | reduce | — | **Required for reduce.** Phase ids whose outputs are aggregated. |
+| `def` | expand, flow | — | **Required for expand.** Fragment Taskflow / phases array / `{steps.X.json}`. |
+| `expandMode` | expand | `nested` | `nested` = isolated sub-flow; `graft` = promote children as `-`. |
+| `maxNodes` | expand | `50` | Cap fragment phase count (1..100). |
+| `run` | script | — | **Required for script.** Shell command: a string (runs in a shell) or an array (direct exec, no shell). A string with an interpolation placeholder is rejected (injection guard). |
+| `input` | script | — | Text piped to the command's stdin; supports interpolation. |
+| `timeout` | script | `60000` | Max run time in ms (1000–300000). On timeout: SIGTERM → SIGKILL, phase fails. |
+| `dependsOn` | all | `[]` | DAG edges. `from` also implies a dependency. |
+| `output` | all | `text` | `json` parses output so `{steps.id.json}` / map `over` work. |
+| `model` | all | agent/global | Per-phase model override. See §5. |
+| `thinking` | all | agent/global | Per-phase thinking level. See §5. |
+| `tools` | all | agent default | Whitelist of tools for the subagent. See §5. |
+| `cwd` | all | flow cwd | Run this phase's subagent in a different directory. |
+| `concurrency` | map, parallel | flow concurrency | Fan-out cap for this phase only. See §4. |
+| `context` | all | — | File paths / `{steps.X}` refs to **pre-read and inject** before the task. See §2.1. |
+| `contextLimit` | all | `8000` | Max characters read **per file** in `context`. See §2.1. |
+| `cache` | all | `run-only` | Per-phase cache policy (`scope`/`ttl`/`fingerprint`). See §11. |
+| `final` | all | last phase | Exactly one phase may be `final`; its output is returned. |
+
+> Gate-only control fields (`eval`, `onBlock`, score), the loop/tournament control
+> fields (`until`/`maxIterations`/`convergence`, `variants`/`judge`/`judgeAgent`/`mode`),
+> the script fields (`run`/`input`/`timeout`), race/expand fields above, and the
+> cross-phase contract fields (`expect`, `timeout`, `optional`, `strictInterpolation`)
+> are documented in `SKILL.md` next to their phase types. `shareContext` and the
+> workspace `cwd` keywords (`temp`/`dedicated`/`worktree`) are in `advanced.md`.
+
+---
+
+## 2.1 Context pre-reading (`context` / `contextLimit`)
+
+Instead of making a subagent *discover* files by exploring (an O(N²) turn-cost
+spiral), you can **pre-read** known files and inject their contents ahead of the
+task prompt. List file paths and/or `{steps.X}` refs in `context`; the runtime
+resolves interpolated refs first, then reads each file and prepends labeled
+blocks to the task.
+
+```jsonc
+{
+ "id": "review",
+ "type": "agent",
+ "agent": "reviewer",
+ "context": ["src/auth.ts", "src/middleware.ts", "{steps.spec.output}"],
+ "contextLimit": 12000,
+ "task": "Review the auth flow against the spec above. VERDICT: PASS or BLOCK.",
+ "dependsOn": ["spec"]
+}
+```
+
+**Behavior & limits (all enforced in the runtime):**
+
+| Aspect | Rule |
+|--------|------|
+| Resolution order | interpolate `{steps.X}` / `{args.X}` refs **first**, then read file paths. |
+| Per-file cap | `contextLimit` characters per file (default **8000**); longer files are truncated with a marker. |
+| Total cap | the combined injected block is hard-capped at **200,000 chars**; overflow is truncated with a notice. |
+| Unreadable file | skipped with a `console.warn` (never aborts the phase). |
+| JSON-looking entry | a value that looks like a JSON blob (not a path) is diagnosed and skipped, not read as a file. |
+
+Use `context` for **known, bounded** inputs (a handful of source files, an
+upstream phase's output). For large/unknown exploration, let the agent use its
+`read`/`grep` tools instead — pre-reading hundreds of files just hits the total
+cap.
+
+---
+
+## 3. Declaring & passing arguments
+
+Declare arguments on the flow, then reference them with `{args.X}`.
+
+```jsonc
+"args": {
+ "dir": { "default": "src", "description": "Directory to scan" },
+ "depth": { "default": 2 },
+ "token": { "required": true, "description": "API token" }
+}
+```
+
+| Field | Notes |
+|-------|-------|
+| `default` | Used when the caller omits the arg. |
+| `description` | Documentation only. |
+| `required` | ⚠️ Declared but **not enforced** at runtime — treat as documentation for now. |
+
+**Resolution:** for each declared arg, the provided value wins, else its
+`default`. Any extra provided keys are also passed through (so undeclared args
+still reach `{args.X}`).
+
+**Passing args:**
+
+Via the MCP tool: `taskflow_run` with `{ "name": "audit-endpoints", "args": { "dir": "packages/api" } }`.
+
+---
+
+## 4. Concurrency model
+
+There are **two independent concurrency limits**:
+
+1. **Same-layer parallelism** — phases with no dependency between them sit in the
+ same topological layer and run concurrently, bounded by **`flow.concurrency`**
+ (default `8`).
+2. **Fan-out within a `map`/`parallel` phase** — bounded by
+ **`phase.concurrency ?? flow.concurrency ?? 8`**.
+
+```jsonc
+{
+ "concurrency": 6, // ≤6 sibling phases run at once
+ "phases": [
+ { "id": "scan", "type": "map", "over": "{steps.list.json}",
+ "concurrency": 3, // …but this map only fans out 3 at a time
+ "task": "…", "dependsOn": ["list"] }
+ ]
+}
+```
+
+Set a low `phase.concurrency` to protect rate-limited models or heavy bash work;
+keep `flow.concurrency` higher to let independent phases overlap.
+
+---
+
+## 5. Model, thinking & tools resolution
+
+For any phase, the effective value is resolved in this **precedence order**
+(first defined wins):
+
+| Setting | Precedence (high → low) |
+|---------|-------------------------|
+| **model** | `phase.model` → agent frontmatter `model` (resolved via `modelRoles`) → pi default |
+| **thinking** | `phase.thinking` → agent frontmatter `thinking` → `settings` global thinking → pi default |
+| **tools** | `phase.tools` → agent frontmatter `tools` → host default capability policy |
+
+Notes:
+- `tools` expresses the requested capability set, but enforcement is
+ host-specific. It is a literal whitelist on Pi; Codex maps it to an OS
+ sandbox profile, while the other hosts use their own permission contracts.
+ Omit it to request the host's default capability policy.
+- Each phase runs as an isolated `grok -p --output-format streaming-json`
+ session. Unresolved `{{placeholder}}`s, multi-segment openrouter paths, and
+ pi thinking suffixes (`:xhigh`) are dropped so Grok falls back to its
+ configured default. Effective thinking maps to `--reasoning-effort` (`off`
+ → `none`). Read-only phases require
+ `PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE` to name a custom profile extending
+ `read-only`, plus a known-good `--tools
+ read_file,grep,list_dir` allowlist plus independent mutator/MCP deny rules and
+ no subagents; web tools are omitted because Grok 0.2.93 can fail open on those
+ allowlist ids. `--always-approve` then applies only to surviving tools. Grok
+ mutating/no-whitelist phases fail closed unless
+ `PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE` names a custom profile configured
+ in `~/.grok/sandbox.toml`; built-in profiles are rejected because Grok may
+ warn and continue unsandboxed when enforcement is unavailable. The custom
+ profile then runs with `--always-approve`. A `max_turns_reached` event fails
+ the phase rather than returning partial text. Grok 0.2.93 reports no usage, so
+ its MCP adapter rejects every flow with `budget` rather than pretending to
+ enforce an unobservable threshold.
+ Children inherit only platform/proxy/CA and Grok/xAI/Taskflow-Grok variables;
+ unrelated secrets are removed.
+
+For Codex, OpenCode, or Grok, an operator can intentionally pass additional
+task-specific environment variables by listing their names in the
+comma-separated `PI_TASKFLOW_CHILD_ENV_ALLOW` setting.
+- The agent's markdown body becomes the subagent's appended system prompt.
+
+---
+
+## 6. Agent discovery & scope
+
+`flow.agentScope` controls which agent directories are loaded:
+
+| Scope | Loads from |
+|-------|-----------|
+| `user` (default) | `~/.pi/agent/agents/*.md` |
+| `project` | nearest `.pi/agents/*.md` found walking up from cwd |
+| `both` | user **then** project (project overrides on name collision) |
+
+- Agents are `.md` files with frontmatter `name` + `description` (required), plus
+ optional `model`, `thinking`, `tools`. The body is the system prompt.
+- Reference agents in phases by their `name`. An unknown name fails that phase
+ with the list of available agents.
+- If a phase omits `agent`, the **first discovered agent** is used.
+
+---
+
+## 7. settings.json
+
+Taskflow shares the subagent settings file at `~/.pi/agent/settings.json`:
+
+```jsonc
+{
+ "modelRoles": {
+ "fast": "openrouter/deepseek/deepseek-v4-flash",
+ "strong": "openrouter/xiaomi/mimo-v2.5-pro"
+ },
+ "subagents": {
+ "globalThinking": "medium" // fallback thinking for all subagents
+ },
+ "defaultThinkingLevel": "low" // used if subagents.globalThinking is absent
+}
+```
+
+- `modelRoles` — maps `{{role}}` references in agent frontmatter to actual model identifiers.
+- `subagents.globalThinking` (or top-level `defaultThinkingLevel`) — global
+ thinking fallback.
+
+---
+
+## 8. Cross-run caching (`cache`)
+
+By default every phase is **`run-only`**: completed phases are reused only when
+you *resume the same run* (the historical behavior). Opt a phase into the
+persistent **cross-run** memoization store to reuse an identical-input result
+from *any prior run* — instant, zero tokens. See `docs/rfc-cross-run-memoization.md`
+for the design.
+
+```jsonc
+{
+ "id": "summarize-deps",
+ "type": "agent",
+ "agent": "writer",
+ "task": "Summarize the dependency tree of this repo.",
+ "cache": {
+ "scope": "cross-run",
+ "ttl": "6h",
+ "fingerprint": ["git:HEAD", "file:package-lock.json"]
+ }
+}
+```
+
+### `scope`
+
+| Value | Meaning |
+|-------|---------|
+| `run-only` (default) | Reuse only within a resumed run — exactly the historical behavior. |
+| `cross-run` | Reuse an identical-input result from **any** prior run (the persistent store). |
+| `off` | Never reuse, even within a run (force re-execution every time). |
+
+### Flow-wide opt-in: `incremental`
+
+Rather than annotating every phase with `cache: { "scope": "cross-run" }`, set
+`incremental: true` at the **flow** level (or pass `incremental: true` as the
+`run` tool argument) to default *every* phase to cross-run reuse:
+
+```jsonc
+{
+ "name": "audit",
+ "incremental": true, // ← every phase defaults to scope:"cross-run"
+ "phases": [ /* ... */ ]
+}
+```
+
+Precedence: the invocation `incremental` argument wins over the flow's
+`incremental` field, which is in turn overridden by any **per-phase** `cache`
+setting. The cross-run-blocked phase types (`gate`/`approval`/`loop`/
+`tournament`/`script`/`race`/`expand`) and all per-phase soundness fallbacks still apply. The default
+remains `run-only` (each run starts fresh unless something opts in), because
+cross-run reuse silently persists outputs and can serve stale results for phases
+whose agents read files at runtime.
+
+### `ttl` (cross-run only)
+
+Max age before a cross-run hit is treated as a miss: e.g. `"30m"`, `"6h"`, `"7d"`.
+Omit for no time bound. A hit older than the TTL re-executes the phase. Cross-run cache entries are hard-evicted after 90 days regardless of per-entry TTL. This ceiling is not configurable.
+
+### `fingerprint` (cross-run only)
+
+The cache key is normally `phaseId + agent + model + interpolated-task`. A
+fingerprint folds **“did the world change?”** signals into that key, so an
+external change becomes a cache **miss** even when the task text is identical.
+Each entry is one of:
+
+| Entry | Becomes a miss when… | Resolves to |
+|-------|----------------------|-------------|
+| `git:HEAD` / `git:` | the commit moves | the resolved SHA (30s timeout → ``; no git → ``) |
+| `glob:` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) |
+| `glob!:` | the **contents** of matching files change | content hashes (capped at 5000 matches) |
+| `file:` | that file's content changes | sha256 of the file (>10 MB or missing → ``/``) |
+| `env:` | the env var changes | the env value |
+
+### What is cached, and when
+
+- Only phases whose **`status` is `done`** and that **were not themselves a cache
+ hit** are written to the store (no re-storing a value just read).
+- The store is keyed by the full input hash + fingerprint, tagged with
+ `flowName`/`phaseId`/`runId`/`model` for inspection and LRU eviction.
+- Cross-run reuse is **safe by construction**: a different agent, model, task, or
+ fingerprint produces a different key, so stale results are never served.
+
+> **When to use it:** expensive, deterministic phases whose inputs rarely change
+> (dependency summaries, doc generation, repeated audits of the same tree). For
+> phases that *should* re-run every time (anything reading live external state
+> without a fingerprint), leave the default `run-only` or set `off`.
+
+---
+
+## 9. Environment variables
+
+| Variable | Effect |
+|----------|--------|
+| `PI_TASKFLOW_PI_BIN` | Override the `pi` binary used to spawn subagents. Used by tests and unusual launch setups (e.g. `PI_TASKFLOW_PI_BIN=pi`). Normally auto-detected. |
+| `PI_TASKFLOW_CODEX_BIN` | Override the `codex` binary used to spawn Codex subagents. |
+| `PI_TASKFLOW_CHILD_ENV_ALLOW` | Comma-separated names of extra task-specific environment variables to pass intentionally to Codex/OpenCode/Grok children. Unlisted application secrets are removed. |
+| `PI_TASKFLOW_CLAUDE_BIN` | Override the `claude` binary used to spawn Claude Code subagents. |
+| `PI_TASKFLOW_CLAUDE_UNSAFE_BYPASS=1` | Explicitly allow trusted Claude phases requesting known mutating tools to use narrow `--tools` + `bypassPermissions`; unknown names always fail closed. |
+| `PI_TASKFLOW_OPENCODE_BIN` | Override the `opencode` binary used to spawn OpenCode subagents. |
+| `PI_TASKFLOW_OPENCODE_MODEL` | Override the default OpenCode model for OpenCode executor e2e tests (e.g. `opencode/deepseek-v4-flash-free`). |
+| `PI_TASKFLOW_OPENCODE_UNSAFE_AUTO=1` | Explicitly permit trusted OpenCode mutating/default phases to use unsandboxed `--auto`; otherwise they fail before spawn. All OpenCode children still use `--pure`. |
+| `PI_TASKFLOW_GROK_BIN` | Override the `grok` binary used to spawn Grok Build subagents. |
+| `PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE` | Required for Grok mutating/no-whitelist phases. Must name a custom profile from `~/.grok/sandbox.toml`; built-in profiles are rejected because they may fail open on unsupported hosts. |
+| `PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE` | Required for Grok read-only phases. Must name a custom profile extending `read-only`; built-in names are rejected so hooks/plugins remain kernel-contained if the host cannot enforce a built-in profile. |
+
+---
+
+## 10. Storage & file locations
+
+| What | Path | Commit? |
+|------|------|---------|
+| User-scoped flow | `~/.pi/agent/taskflows/.json` | personal |
+| Project-scoped flow | `/taskflows/.json` | ✅ commit to share |
+| Run state (resume) | `/taskflows/runs//.json` | ❌ gitignore |
+
+- `action: "save"` takes `scope: "project"` (default) or `"user"`.
+- Project flows override user flows on a name collision.
+- Add `.pi/taskflows/runs/` to `.gitignore`.
+
+---
+
+## 11. Quick recipes
+
+**Pin a strong model only for the review gate:**
+```jsonc
+{ "id": "review", "type": "gate", "agent": "reviewer",
+ "model": "claude-opus-4", "thinking": "high",
+ "task": "…\nVERDICT:", "dependsOn": ["audit"] }
+```
+
+**Sandbox a phase to read-only in a subdirectory:**
+```jsonc
+{ "id": "scan", "type": "agent", "agent": "scout",
+ "cwd": "packages/api", "tools": ["read", "grep", "ls"],
+ "task": "List route files. Output ONLY a JSON array.", "output": "json" }
+```
+
+**Throttle a rate-limited fan-out:**
+```jsonc
+{ "id": "summarize", "type": "map", "over": "{steps.scan.json}",
+ "concurrency": 2, "agent": "writer",
+ "task": "Summarize {item.file}.", "dependsOn": ["scan"] }
+```
+
+**Project-only agents:**
+```jsonc
+{ "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] }
+```
+
+---
+
+## 9. TypeScript DSL CLI (`taskflow-dsl` / S4)
+
+Author flows as **`.tf.ts`** (compile-time runes), then run the emitted JSON
+through existing `taskflow_*` tools. JSON remains first-class (escape hatch).
+
+```bash
+# From a monorepo checkout (dev):
+node --conditions=development --experimental-strip-types \
+ packages/taskflow-dsl/src/cli.ts new audit
+# edit audit.tf.ts
+node --conditions=development --experimental-strip-types \
+ packages/taskflow-dsl/src/cli.ts check audit.tf.ts
+# Fast rune/static-only pass (skip the default full tsc Program check):
+node --conditions=development --experimental-strip-types \
+ packages/taskflow-dsl/src/cli.ts check audit.tf.ts --no-typecheck
+node --conditions=development --experimental-strip-types \
+ packages/taskflow-dsl/src/cli.ts build audit.tf.ts --emit both
+# → audit.taskflow.json (+ audit.flowir.json)
+# Then: taskflow_verify / taskflow_run with defineFile=audit.taskflow.json
+```
+
+| Command | Purpose |
+|---------|---------|
+| `new [name]` | ≤5-line hello skeleton (`.tf.ts` or `--json-escape` JSON) |
+| `check ` | Erase + `validateTaskflow` + tsc (use `--no-typecheck` for a faster static-only pass) |
+| `build ` | Erase → Taskflow JSON; optional FlowIR hash (`--emit taskflow\|flowir\|both`) |
+| `decompile ` | Taskflow JSON → readable `.tf.ts` (semantic, not literal) |
+
+Output commands are create-only by default: pass `--force` to replace an
+existing regular file. Outputs are `--cwd`-contained, reject destination
+symlinks, and commit atomically; `--emit both` preflights both destinations.
+
+**Authoring notes (kinds ↔ runes)**
+
+Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow
+JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry).
+
+| JSON `type` | DSL rune(s) | Notes |
+|-------------|-------------|--------|
+| `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` |
+| `parallel` | `parallel([agent…])` | waits for all branches |
+| `map` | `map(source, item => agent…)` | `over` + `as` |
+| `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` |
+| `reduce` | `reduce([…], () => agent…)` | `from` |
+| `approval` | `approval({ request })` | |
+| `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def |
+| `loop` | `loop({ task, until?, … })` | |
+| `tournament` | `tournament({ branches/variants, judge, … })` | |
+| `script` | `script(run, opts?)` | string or argv array |
+| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted |
+| `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` |
+
+- `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`.
+- `race([...])` does **not** support array destructure — bind as one phase: `const winner = race([...])`.
+- Unbuilt `.tf.ts` must **not** be executed as a Node program (runes throw `TFDSL_ERASE_ONLY`).
+- Modular erase: new kind → `packages/taskflow-dsl/src/build/erase/kinds/.ts` + registry entry (see `docs/internal/modularization-0.2.0.md`).
+
+Design docs: `docs/rfc-0.2.0-s4-mvp.md`, `docs/rfc-0.2.0-dsl-phases-horizon.md`.
+
+---
+
+## Caveats (declared but not yet enforced / partial)
+
+These keys validate but the runtime does **not** fully act on them yet — don't
+rely on them for behavior:
+
+- `arg.required` — documents intent for tooling, but missing required args are
+ not rejected at run time in 0.2.0. Use strict interpolation and verify/run
+ argument validation at your integration boundary when absence must block.
+- `flow.version` — informational only; it does not select runtime semantics.
+- **Event kernel** (`eventKernel` / `PI_TASKFLOW_EVENT_KERNEL=1`) — opt-in; does
+ **not** run `race`/`expand`; score gates, `retry`, `expect`, reflexion,
+ cross-run cache, and Shared Context Tree force the **imperative** path.
+- **`taskflow-dsl decompile`** — generates safe, readable TypeScript whose
+ rebuilt Taskflow/FlowIR is semantically equivalent for supported constructs;
+ dependencies are emitted before consumers even when input JSON is out of
+ order;
+ it does **not** reproduce original variable names, formatting, comments, or
+ source spelling. Unsupported/lossy constructs fail rather than silently
+ promising a literal round-trip.
diff --git a/packages/grok-taskflow/plugin/skills/taskflow/library.md b/packages/grok-taskflow/plugin/skills/taskflow/library.md
new file mode 100644
index 0000000..9c3cebc
--- /dev/null
+++ b/packages/grok-taskflow/plugin/skills/taskflow/library.md
@@ -0,0 +1,105 @@
+
+
+# Library: reusable flows & the search-before-author loop
+
+taskflow saves flows you'll reuse into a **library** with metadata, so you can
+`search` it before authoring a new flow — the **reuse flywheel**. The more you
+save (with a good `purpose` + `tags`), the better future search gets.
+
+> Full design: `docs/rfc-library-reuse.md`. This file is the agent-facing
+> "when and how" guide.
+
+**Host binding:** use the `taskflow_search`, `taskflow_save`, `taskflow_list`,
+`taskflow_show` tools.
+
+## Before authoring a non-trivial flow: SEARCH first
+
+Any time you're about to write a flow with ≥3 phases, fan-out, or a gate,
+**search the library first**. It costs nothing and often finds a starter you
+can adapt.
+
+```jsonc
+{ "name": "taskflow_search", "arguments": { "query": "audit API endpoints for missing auth", "limit": 5 } }
+```
+
+Read the results and the `→ reuseHint`:
+
+| score | reuseHint | what to do |
+|-------|-----------|------------|
+| **≥ 0.8** | "直接复用" / direct reuse | Run by name (skip authoring). |
+| **0.5 – 0.8** | "copy + 泛化" / copy & generalize | `show` it, copy, **generalize** (see checklist), save as a new version. |
+| **< 0.5** or no matches | "从头编写" / write fresh | Author from scratch — then **save** it if reusable. |
+
+`searchMode` tells you how the ranking was produced: `structural` (keyword +
+phase-signature; no embedding backend configured), `semantic` (cosine over
+embeddings), or `mixed` (some flows had vectors, some didn't). `structural` is
+weaker on paraphrase — if it missed something obvious, try `structureOnly:
+false` or a different phrasing.
+
+## After a successful novel flow: SAVE it (if reusable)
+
+When you finish a flow you expect to use again, save it **with a `purpose` and
+2–4 `tags`**. These two fields are what search matches on — a flow saved
+without them is nearly invisible to future search.
+
+```jsonc
+{ "name": "taskflow_save",
+ "arguments": { "name": "audit-endpoints", "definition": { "phases": [ ... ] },
+ "purpose": "Audit a directory of API endpoints for missing auth checks",
+ "tags": ["audit", "security", "auth", "fan-out"] } }
+```
+
+`save` auto-derives structural metadata (phase signature, a `generality` score
+in 0–1) and writes a sidecar `.meta.json` next to the flow file. You don't
+compute any of that — just give `purpose` + `tags`.
+
+## The generalization checklist (apply on every reuse)
+
+When you copy + generalize a flow, make it **more reusable than the version you
+found**. Each item raises the auto-derived `generality` score and broadens
+future search recall:
+
+- [ ] Hardcoded file/dir paths → `{args.X}` (with a `default`).
+- [ ] Specific entity words ("endpoint", "route") → broaden in the discover
+ prompt so the flow works for the whole class.
+- [ ] Thresholds / counts → `{args.X}` with sensible defaults.
+- [ ] Add `budget` / `retry` / `expect` if missing (production-grade knobs).
+- [ ] Update `purpose` to reflect the wider scope.
+
+Then save it back (version auto-bumps). Over time the library compounds: every
+reuse leaves a more general flow behind.
+
+## reuseCount & `reusedFromSearch`
+
+Each saved flow has a `reuseCount`. It goes up by 1 **only when** a run was
+chosen because of a prior search — set the `reusedFromSearch: true` flag on the
+run. Direct run-by-name does **not** bump it (that's intentional: `reuseCount`
+measures "found-via-search reuse", the high-quality signal for later auto-prune).
+
+```jsonc
+{ "name": "taskflow_run",
+ "arguments": { "name": "audit-endpoints", "args": { "dir": "src/api" }, "reusedFromSearch": true } }
+```
+
+## Judicious reuse — not every task needs the library
+
+Skip search + save for:
+- **One-off tasks** (a quick fix, a throwaway analysis) — `generality < 0.3`
+ and obviously won't recur.
+- **Trivial flows** (1–2 phases, no fan-out) — overhead isn't worth it.
+
+The library pays off for *patterns* that recur across projects or sessions:
+audits, migrations, reviews, fan-out summarization, plan→approve→execute, etc.
+
+## Configuration (embedding backend — optional, Phase 2)
+
+Search works with **zero config** (structural mode). For smarter paraphrase
+recall, configure an embedding backend in `~/.pi/agent/settings.json`:
+
+```jsonc
+{ "taskflow": { "library": { "enabled": true, "scope": "both" },
+ "embedder": { "kind": "http", "url": "http://127.0.0.1:8123/v1/embeddings", "model": "qwen3-embedding-0.6b" } } }
+```
+
+Without `embedder`, or if the embedder fails, search **degrades gracefully** to
+structural mode — it never breaks. (See `docs/rfc-library-reuse.md` §4.)
diff --git a/packages/grok-taskflow/plugin/skills/taskflow/patterns.md b/packages/grok-taskflow/plugin/skills/taskflow/patterns.md
new file mode 100644
index 0000000..48f11cd
--- /dev/null
+++ b/packages/grok-taskflow/plugin/skills/taskflow/patterns.md
@@ -0,0 +1,348 @@
+
+
+# Taskflow Patterns — proven archetypes & the production checklist
+
+Read this when designing a flow with ≥ 4 phases, a gate, or any fan-out.
+Each archetype below is a complete, runnable shape distilled from real runs.
+Copy the closest one and adapt — don't design from a blank page.
+
+---
+
+## The production-flow checklist
+
+Before running a flow you designed, check it against this list. Every item is
+cheap to add and each one has prevented a real failure class:
+
+- [ ] **`verify` first.** Run the static verifier (pi: `action: "verify"` /
+ Codex / Claude Code / OpenCode: `taskflow_verify`) — zero tokens,
+ catches cycles / missing deps / ref typos / contract mismatches.
+- [ ] **Every JSON-emitting phase has `expect` + `retry`.** "Output ONLY JSON"
+ in the task text is a request; `expect` is enforcement. Without it, a
+ malformed router output silently skips both branches.
+- [ ] **Machine checks before LLM checks.** `script` phases for build/test
+ ground truth; gate `eval` assertions before the gate's LLM `task`. A
+ token spent verifying what a shell command can verify is a wasted token.
+- [ ] **`budget` on any flow with a fan-out.** A map over a mis-discovered
+ 500-item array is unbounded spend without one.
+- [ ] **`optional: true` + a fallback for degradable phases.** An enrichment
+ phase that times out shouldn't sink the run — pair `timeout` +
+ `optional` with a downstream `when`-guarded fallback.
+- [ ] **Exactly one `final: true`** on the result-bearing phase.
+- [ ] **`strictInterpolation: true` on any flow you save.** Saved flows run
+ later with args you're not watching; unresolved placeholders must be
+ errors, not empty strings.
+- [ ] **Discovery phases are cheap and sandboxed.** `agent: "scout"`,
+ `tools: ["read","grep","ls"]`, low thinking. Don't pay executor prices
+ for `ls`.
+- [ ] **The reviewer is not the producer.** A gate reviewing phase X uses a
+ different agent (ideally a different model) than X. Self-review passes
+ ~everything.
+- [ ] **Re-runnable flows are `incremental: true`** with `fingerprint` entries
+ on phases that read the world (`git:HEAD`, `glob!:src/**/*.ts`). See
+ `advanced.md`.
+
+---
+
+## Archetype 1: Audit fan-out (discover → map → gate → reduce)
+
+The workhorse. Use for: audit every endpoint / migrate every file /
+summarize every module.
+
+```jsonc
+{
+ "name": "audit-endpoints",
+ "strictInterpolation": true,
+ "budget": { "maxUSD": 3.00 },
+ "phases": [
+ { "id": "discover", "type": "agent", "agent": "scout",
+ "tools": ["read", "grep", "ls"],
+ "task": "List every HTTP endpoint under src/routes. Output ONLY a JSON array [{\"route\":\"...\",\"file\":\"...\"}]. No prose.",
+ "output": "json",
+ "expect": { "type": "array", "items": { "type": "object", "required": ["route", "file"] } },
+ "retry": { "max": 2, "backoffMs": 0 } },
+
+ { "id": "audit", "type": "map", "over": "{steps.discover.json}", "as": "item",
+ "agent": "analyst", "concurrency": 4,
+ "task": "Audit {item.route} in {item.file} for missing auth. Report: SEVERITY (high/med/low/none), evidence (file:line), fix.",
+ "dependsOn": ["discover"] },
+
+ { "id": "screen", "type": "gate", "agent": "reviewer",
+ "task": "Cross-check the findings below. Delete false positives (cite why). If ANY confirmed HIGH remains, end with VERDICT: BLOCK and list them; else VERDICT: PASS.\n\n{steps.audit.output}",
+ "dependsOn": ["audit"] },
+
+ { "id": "report", "type": "reduce", "from": ["screen"], "agent": "doc-writer",
+ "task": "Write a prioritized remediation report from:\n{steps.screen.output}",
+ "dependsOn": ["screen"], "final": true }
+ ]
+}
+```
+
+Why each piece: `expect`+`retry` on discover means a chatty scout gets a second
+chance instead of feeding garbage to the map; `concurrency: 4` protects rate
+limits; the gate is a *different* agent than the auditor; `budget` limits the
+fan-out.
+
+**Variant — per-item caching for repeated audits:** add
+`"cache": { "scope": "cross-run" }` to the map phase. On the next run, only
+items whose task text changed re-execute; the rest are $0 cache hits.
+(Details: `configuration.md` §8.)
+
+---
+
+## Archetype 2: Self-healing implement→verify→rework
+
+Use for: implement against acceptance criteria, fix-until-green.
+The gate re-runs its upstream on BLOCK — a generate→critique→regenerate loop
+without you writing a loop.
+
+```jsonc
+{
+ "name": "implement-verified",
+ "budget": { "maxUSD": 5.00 },
+ "phases": [
+ { "id": "implement", "type": "agent", "agent": "executor-code",
+ "task": "Implement the feature per the spec in docs/spec.md. Run nothing; just edit." },
+
+ { "id": "build-test", "type": "script",
+ "run": "npx tsc --noEmit && pnpm test 2>&1 | tail -20",
+ "timeout": 180000, "dependsOn": ["implement"] },
+
+ { "id": "spec-gate", "type": "gate", "agent": "reviewer",
+ "onBlock": "retry", "retry": { "max": 3 },
+ "eval": ["{steps.build-test.output} contains pass"],
+ "task": "Build/test output:\n{steps.build-test.output}\n\nDoes the implementation satisfy ALL acceptance criteria in docs/spec.md? VERDICT: PASS, or VERDICT: BLOCK with a precise list of what to fix.",
+ "dependsOn": ["implement", "build-test"] },
+
+ { "id": "summary", "type": "agent", "agent": "doc-writer",
+ "task": "Summarize what was implemented and the verification result:\n{steps.spec-gate.output}",
+ "dependsOn": ["spec-gate"], "final": true }
+ ]
+}
+```
+
+Key mechanics: on BLOCK, `spec-gate` re-runs **both** its `dependsOn` upstreams
+(`implement` gets the blocker's reasons via re-interpolation, `build-test`
+re-verifies), up to 3 rounds. The `eval` line means a green build+test skips
+the LLM review entirely on the happy path.
+
+**Verification phases: force structured output.** LLMs are bad at
+*summarizing* shell output (234 tests read as 230) but good at *copying*
+structured data. If a verification step must go through an agent (not a
+`script`), demand `key=value` lines:
+
+```
+Report EXACTLY in this format (one key=value per line, no prose):
+typecheck=PASS|FAIL
+tests_total=N
+tests_fail=N
+If any field is missing, you failed the task — re-run and re-read.
+```
+
+Prefer a `script` phase whenever the check is a command — exact, free, fast.
+
+---
+
+## Archetype 3: Plan → human approval → execute
+
+Use for: anything expensive or destructive where a human should see the plan
+before the spend. The approval's **Edit** option injects mid-run guidance.
+
+> **MCP-host caveat (Codex / Claude Code / OpenCode):** approval phases auto-reject in
+> MCP-driven (non-interactive) runs. This archetype only works when a human
+> runs the flow interactively; for tool-driven runs, replace the approval with
+> a strict `gate`.
+
+```jsonc
+{
+ "name": "guarded-migration",
+ "phases": [
+ { "id": "plan", "type": "agent", "agent": "planner",
+ "task": "Plan the migration of src/legacy/* to the new API. List each file, the change, and the risk." },
+
+ { "id": "checkpoint", "type": "approval",
+ "task": "Migration plan:\n\n{steps.plan.output}\n\nApprove to execute, reject to abort, or edit to add constraints.",
+ "dependsOn": ["plan"] },
+
+ { "id": "execute", "type": "agent", "agent": "executor-code",
+ "task": "Execute the migration plan:\n{steps.plan.output}\n\nOperator guidance (if any): {steps.checkpoint.output}",
+ "dependsOn": ["checkpoint"] },
+
+ { "id": "verify", "type": "script", "run": "npx tsc --noEmit && pnpm test 2>&1 | tail -5",
+ "timeout": 180000, "dependsOn": ["execute"], "final": true }
+ ]
+}
+```
+
+Note `{steps.checkpoint.output}` — on Edit it carries the operator's note; on
+Approve it's `(approve)`. Don't use approval in detached/headless runs (it
+auto-rejects there — by design).
+
+---
+
+## Archetype 4: Dynamic plan → execute (`flow{def}`)
+
+Use when the *work itself* must be discovered at runtime — the planner emits a
+whole sub-flow as JSON and the runtime validates + runs it. The declarative
+answer to "loop over whatever we find".
+
+```jsonc
+{
+ "name": "dynamic-audit",
+ "budget": { "maxUSD": 4.00 },
+ "phases": [
+ { "id": "plan", "type": "agent", "agent": "planner", "output": "json",
+ "task": "Scan this repo. Output ONLY a JSON taskflow {\"name\":\"sub\",\"phases\":[...]} with one 'agent' phase per module that needs auditing (agent: \"analyst\"), plus a final 'reduce' phase (agent: \"doc-writer\", from: [all audit ids], final: true). Use hyphens in ids. No script phases, no cwd fields.",
+ "expect": { "type": "object", "required": ["name", "phases"] },
+ "retry": { "max": 2, "backoffMs": 0 } },
+
+ { "id": "run-plan", "type": "flow", "def": "{steps.plan.json}",
+ "optional": true, "dependsOn": ["plan"] },
+
+ { "id": "deliver", "type": "agent", "agent": "doc-writer",
+ "task": "Final result (empty means the plan failed validation — say so):\n{steps.run-plan.output}",
+ "dependsOn": ["run-plan"], "final": true }
+ ]
+}
+```
+
+Critical details (full contract in `advanced.md`): a bad plan **fails open**
+(`defError` diagnostic, empty output downstream) — `optional: true` + the
+`deliver` phase turn that into a graceful report instead of a dead run. The
+planner prompt must forbid what validation will reject anyway (`script`
+phases, workspace `cwd` keywords) so the plan doesn't waste a retry.
+
+**Iterative replanning:** wrap a plan-emitting body in a `loop` so round N's
+plan reacts to round N−1's result — see `examples/iterative-replan.json`.
+
+---
+
+## Archetype 5: Tournament for one-shot-unreliable work
+
+Use when quality varies run-to-run (naming, copywriting, tricky refactor
+strategy, root-cause hypotheses). Branches > variants when you want genuinely
+different *approaches* judged against each other.
+
+```jsonc
+{
+ "id": "strategy", "type": "tournament", "mode": "best",
+ "judgeAgent": "final-arbiter",
+ "judge": "Judge on: correctness under concurrent access, blast radius, migration cost. Quote evidence. Return JSON {\"winner\": , \"reason\": \"...\"}.",
+ "branches": [
+ { "task": "Design the cache-invalidation fix with a conservative approach: minimal diff, no schema change.", "agent": "analyst" },
+ { "task": "Design the fix assuming we can change the schema: optimal correctness.", "agent": "analyst" },
+ { "task": "Design the fix as an adversary: what will break each obvious approach? Then propose the one that survives.", "agent": "critic" }
+ ],
+ "dependsOn": ["context"], "final": true
+}
+```
+
+Give the judge a **rubric with named criteria**, a stronger model
+(`judgeAgent`), and a structured winner output (`{"winner": }` JSON,
+or an exact `WINNER: ` terminator). `mode: "aggregate"`
+instead merges all variants — good for research synthesis, bad for decisions.
+
+---
+
+## Archetype 7: Race for latency (first approach wins)
+
+When several strategies can answer the same question and you care about **time
+to first good answer** more than comparing quality, use `race` (not
+`tournament`). Branches start together; the first successful completion becomes
+the phase output.
+
+```jsonc
+{
+ "name": "quick-answer",
+ "budget": { "maxUSD": 0.5 },
+ "phases": [
+ {
+ "id": "answer", "type": "race",
+ "branches": [
+ { "task": "Answer from local heuristics only: {args.q}", "agent": "executor" },
+ { "task": "Answer after a short web/docs look: {args.q}", "agent": "researcher" }
+ ],
+ "final": true
+ }
+ ]
+}
+```
+
+**Prefer tournament when** you need a judge to pick the *best* draft after all
+variants finish. **Prefer parallel when** you need *every* branch's output
+downstream (then `reduce`).
+
+## Archetype 8: Expand graft (planner fragment on the parent DAG)
+
+Planner emits a fragment; `expand` with `expandMode: "graft"` runs it and
+promotes child phase states as `-` so a later phase can read
+them. Use `nested` (or classic `flow{def}`) when you only need the fragment's
+**final** output and do not want child ids on the parent.
+
+```jsonc
+{
+ "name": "plan-graft",
+ "phases": [
+ {
+ "id": "plan", "type": "agent", "agent": "planner", "output": "json",
+ "task": "Emit a mini-flow JSON: {name, phases:[{id,type,agent,task,final?}…]} for the audit.",
+ "expect": { "type": "object", "required": ["phases"] }
+ },
+ {
+ "id": "grow", "type": "expand", "expandMode": "graft",
+ "def": "{steps.plan.json}", "dependsOn": ["plan"]
+ },
+ {
+ "id": "wrap", "type": "agent", "agent": "writer",
+ "task": "Summarize grafted work. Child outputs may appear as steps.grow-* in the run state.",
+ "dependsOn": ["grow"], "final": true
+ }
+ ]
+}
+```
+
+## Archetype 6: Incremental repo-watch audit (cross-run)
+
+Use for flows you'll re-run as the repo evolves. First run pays full price;
+subsequent runs re-pay only for what changed.
+
+```jsonc
+{
+ "name": "security-sweep",
+ "incremental": true,
+ "budget": { "maxUSD": 3.00 },
+ "phases": [
+ { "id": "discover", "type": "agent", "agent": "scout", "output": "json",
+ "task": "List all files handling user input. Output ONLY a JSON array of paths.",
+ "expect": { "type": "array" },
+ "cache": { "scope": "cross-run", "fingerprint": ["glob!:src/**/*.ts"] } },
+ { "id": "audit", "type": "map", "over": "{steps.discover.json}",
+ "agent": "security-reviewer", "task": "Audit {item} for injection/authz issues.",
+ "cache": { "scope": "cross-run" },
+ "dependsOn": ["discover"] },
+ { "id": "report", "type": "reduce", "from": ["audit"], "agent": "doc-writer",
+ "task": "Prioritized findings report:\n{steps.audit.output}",
+ "dependsOn": ["audit"], "final": true }
+ ]
+}
+```
+
+The `glob!:` fingerprint makes `discover` a cache miss only when file
+*contents* change; the map's per-item cache means one changed file re-audits
+one item.
+
+---
+
+## Anti-patterns (seen in real flows)
+
+| Anti-pattern | Why it fails | Fix |
+|--------------|--------------|-----|
+| One mega-phase doing discover+audit+report | No parallelism, no caching granularity, one failure loses everything | Split along the archetype-1 shape |
+| Gate whose task doesn't demand a `VERDICT:` terminator | Ambiguous model output fails closed → gate blocks on a model that forgot the verdict | Use `output:"json"` + `expect` enum (preferred), or end the task with the exact `VERDICT: PASS\|BLOCK` instruction (auto-appended if you omit it) |
+| Router phase without `expect` enum | `"Deep"` vs `"deep"` → both `when` branches skip, `join:"any"` reduce gets nothing | `expect: { properties: { route: { enum: [...] } } }` + `retry` |
+| Agent phase that just runs a shell command | Tokens spent, output paraphrased inaccurately | `script` phase |
+| Same agent produces and reviews | Self-review passes everything | Different agent (ideally model) for the gate |
+| Fan-out with no `budget` and no `concurrency` cap | Unbounded spend + rate-limit storms | `budget` + `phase.concurrency` |
+| `dependsOn` declared but output never referenced | The downstream agent doesn't see the upstream's work — dependency ≠ data flow | Interpolate `{steps.X.output}` into the task (or `context`) |
+| Saving a flow without `strictInterpolation` | Later invocations with wrong args silently run on empty strings | `strictInterpolation: true` before saving |
+| `map` over `{steps.X.output}` (text, not json) | `over` must resolve to an array | `output: "json"` upstream + `over: "{steps.X.json}"` |
+| Deep `chain` where steps don't need each other's output | Serialized latency for no reason | `tasks` (parallel) or a DAG with real edges only |
diff --git a/packages/grok-taskflow/src/index.ts b/packages/grok-taskflow/src/index.ts
new file mode 100644
index 0000000..e7d45a4
--- /dev/null
+++ b/packages/grok-taskflow/src/index.ts
@@ -0,0 +1,13 @@
+/**
+ * grok-taskflow public entry.
+ *
+ * The grok runner lives in `taskflow-hosts`. This package re-exports it so
+ * `import { grokSubagentRunner, runGrokAgentTask, buildGrokArgs, … } from
+ * "grok-taskflow"` works. New code should prefer `taskflow-hosts` /
+ * `taskflow-hosts/grok`; this re-export is the delivery-package public surface.
+ *
+ * The delivery surface (MCP server + bin + Grok plugin scaffold) ships from
+ * this package — see `./mcp/server.ts`, `./mcp/bin.ts`, and `./plugin/`.
+ */
+
+export * from "taskflow-hosts/grok";
diff --git a/packages/grok-taskflow/src/mcp/bin.ts b/packages/grok-taskflow/src/mcp/bin.ts
new file mode 100644
index 0000000..1f40621
--- /dev/null
+++ b/packages/grok-taskflow/src/mcp/bin.ts
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+/**
+ * Executable entry for the taskflow MCP server, grok-bound (the
+ * `grok-taskflow-mcp` bin).
+ *
+ * Register with Grok Build:
+ * npm install -g grok-taskflow
+ * grok mcp add taskflow -- grok-taskflow-mcp
+ *
+ * Or install the plugin scaffold (skills + MCP via npx):
+ * grok plugin install ./packages/grok-taskflow/plugin --trust
+ * # or: grok plugin install --trust
+ *
+ * From a checkout of this repo (after `pnpm run build`):
+ * grok mcp add taskflow -- node /abs/path/to/packages/grok-taskflow/dist/mcp/bin.js
+ *
+ * Grok then launches this as a stdio MCP server and the taskflow_* tools
+ * become available. The server discovers saved flows + agents from its launch
+ * cwd, and runs each subagent as a grok -p session.
+ */
+
+import { startMcpServer } from "./server.ts";
+
+startMcpServer(process.cwd())
+ .then(() => process.exit(0))
+ .catch((e) => {
+ // Never write non-JSON to stdout (it would corrupt the MCP stream); log to
+ // stderr and exit non-zero so the client sees the transport drop.
+ process.stderr.write(`taskflow mcp server fatal: ${e instanceof Error ? e.stack ?? e.message : String(e)}\n`);
+ process.exit(1);
+ });
diff --git a/packages/grok-taskflow/src/mcp/server.ts b/packages/grok-taskflow/src/mcp/server.ts
new file mode 100644
index 0000000..fbdff32
--- /dev/null
+++ b/packages/grok-taskflow/src/mcp/server.ts
@@ -0,0 +1,33 @@
+/**
+ * The Grok Build binding of the host-neutral MCP server (taskflow-mcp-core/server).
+ *
+ * The protocol layer, tool schemas, and handlers all live in core; this shim
+ * only closes the loop for Grok: every subagent a flow spawns is itself a
+ * `grok -p` process (via grokSubagentRunner). Kept as a module (not just
+ * bin.ts) so tests and embedders get the same pre-bound surface the bin runs.
+ */
+
+import {
+ makeMcpHandlers as coreMakeMcpHandlers,
+ makeToolHandlers as coreMakeToolHandlers,
+ startMcpServer as coreStartMcpServer,
+} from "taskflow-mcp-core/server";
+import type { RpcContext, RpcHandler } from "taskflow-mcp-core/jsonrpc";
+import { grokSubagentRunner } from "taskflow-hosts";
+
+/** Per-call tool handlers with grok subagent execution bound in. */
+export function makeToolHandlers(
+ cwd: string,
+): Record, context?: RpcContext) => Promise> {
+ return coreMakeToolHandlers(cwd, grokSubagentRunner);
+}
+
+/** Full MCP method dispatch table (protocol + tools), grok-bound. */
+export function makeMcpHandlers(cwd: string): Record {
+ return coreMakeMcpHandlers(cwd, grokSubagentRunner);
+}
+
+/** Start the stdio MCP server. Resolves when the client disconnects. */
+export function startMcpServer(cwd: string = process.cwd()): Promise {
+ return coreStartMcpServer(grokSubagentRunner, cwd);
+}
diff --git a/packages/grok-taskflow/test/e2e-grok-mcp.mts b/packages/grok-taskflow/test/e2e-grok-mcp.mts
new file mode 100644
index 0000000..b63899d
--- /dev/null
+++ b/packages/grok-taskflow/test/e2e-grok-mcp.mts
@@ -0,0 +1,72 @@
+/**
+ * Lightweight e2e: spawn the grok-bound MCP server over stdio and exercise
+ * initialize + tools/list + taskflow_verify. No live Grok CLI required.
+ *
+ * node --conditions=development --experimental-strip-types packages/grok-taskflow/test/e2e-grok-mcp.mts
+ */
+
+import { spawn } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+import assert from "node:assert/strict";
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const bin = path.join(here, "..", "src", "mcp", "bin.ts");
+
+function rpc(child: ReturnType, msg: object): Promise {
+ return new Promise((resolve, reject) => {
+ let buf = "";
+ const onData = (d: Buffer) => {
+ buf += d.toString();
+ const i = buf.indexOf("\n");
+ if (i < 0) return;
+ child.stdout?.off("data", onData);
+ try {
+ resolve(JSON.parse(buf.slice(0, i)));
+ } catch (e) {
+ reject(e);
+ }
+ };
+ child.stdout?.on("data", onData);
+ child.stdin?.write(JSON.stringify(msg) + "\n");
+ });
+}
+
+const child = spawn(
+ process.execPath,
+ ["--conditions=development", "--experimental-strip-types", bin],
+ { stdio: ["pipe", "pipe", "inherit"], cwd: path.join(here, "..", "..", "..") },
+);
+
+try {
+ const init = await rpc(child, {
+ jsonrpc: "2.0",
+ id: 1,
+ method: "initialize",
+ params: { protocolVersion: "2025-06-18", capabilities: {} },
+ });
+ assert.equal(init.result.serverInfo.name, "taskflow");
+
+ const list = await rpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
+ const names = list.result.tools.map((t: any) => t.name);
+ assert.ok(names.includes("taskflow_verify"));
+
+ const verify = await rpc(child, {
+ jsonrpc: "2.0",
+ id: 3,
+ method: "tools/call",
+ params: {
+ name: "taskflow_verify",
+ arguments: {
+ define: {
+ name: "e2e",
+ phases: [{ id: "a", type: "agent", agent: "executor", task: "hi", final: true }],
+ },
+ },
+ },
+ });
+ assert.match(verify.result.content[0].text, /PASSED/);
+ console.log("e2e-grok-mcp: ok");
+} finally {
+ child.kill();
+}
diff --git a/packages/grok-taskflow/test/e2e-grok.mts b/packages/grok-taskflow/test/e2e-grok.mts
new file mode 100644
index 0000000..baab69c
--- /dev/null
+++ b/packages/grok-taskflow/test/e2e-grok.mts
@@ -0,0 +1,95 @@
+/**
+ * Live Grok executor E2E. Requires an authenticated `grok` CLI.
+ *
+ * Exercises the real taskflow-hosts runner, including Grok 0.2.93's tool
+ * allowlist parser. The read-only policy must neither trigger the known
+ * "unmappable -> full toolset" fallback nor permit a workspace write.
+ */
+
+import assert from "node:assert/strict";
+import { randomUUID } from "node:crypto";
+import { existsSync } from "node:fs";
+import { mkdtemp, rm } from "node:fs/promises";
+import { homedir, tmpdir } from "node:os";
+import { join } from "node:path";
+import {
+ GROK_MUTATING_SANDBOX_PROFILE_ENV,
+ GROK_READONLY_SANDBOX_PROFILE_ENV,
+ runGrokAgentTask,
+} from "taskflow-hosts/grok";
+import type { AgentConfig } from "taskflow-core";
+
+assert.ok(
+ process.env[GROK_MUTATING_SANDBOX_PROFILE_ENV]?.trim(),
+ `live mutating probe requires ${GROK_MUTATING_SANDBOX_PROFILE_ENV} to name a custom Grok sandbox profile`,
+);
+assert.ok(
+ process.env[GROK_READONLY_SANDBOX_PROFILE_ENV]?.trim(),
+ `live read-only probe requires ${GROK_READONLY_SANDBOX_PROFILE_ENV} to name a custom Grok sandbox profile`,
+);
+
+// Grok's read-only sandbox intentionally permits /tmp for session internals.
+// Keeping the test workspace there proves the independent tool/MCP deny rules
+// also prevent mutation when the OS sandbox's temp exemption applies.
+const readOnlyCwd = await mkdtemp(join(tmpdir(), "taskflow-grok-read-only-"));
+const readOnlyMarker = join(readOnlyCwd, "MUST_NOT_EXIST.txt");
+const mutatingCwd = await mkdtemp(join(homedir(), ".taskflow-grok-workspace-"));
+const workspaceMarker = join(mutatingCwd, "WORKSPACE_WRITE_OK.txt");
+const outsideMarker = join(homedir(), `.taskflow-grok-outside-${randomUUID()}.txt`);
+const agents: AgentConfig[] = [
+ {
+ name: "read-only-probe",
+ description: "live read-only permission probe",
+ tools: ["read", "grep", "glob", "web_search", "web_fetch"],
+ thinking: "low",
+ systemPrompt: "Follow the task exactly. Do not claim a tool succeeded unless it actually did.",
+ source: "project",
+ filePath: "(e2e)",
+ },
+ {
+ name: "mutating-probe",
+ description: "live workspace sandbox probe",
+ tools: ["write", "bash"],
+ thinking: "low",
+ systemPrompt: "Follow the task exactly. Attempt each requested write and report real tool outcomes.",
+ source: "project",
+ filePath: "(e2e)",
+ },
+];
+
+try {
+ const readOnlyResult = await runGrokAgentTask(
+ readOnlyCwd,
+ agents,
+ "read-only-probe",
+ `Try to create the file ${readOnlyMarker} using any available tool. Then report whether it exists.`,
+ { idleTimeoutMs: 90_000 },
+ );
+ assert.equal(readOnlyResult.exitCode, 0, readOnlyResult.stderr || readOnlyResult.errorMessage);
+ assert.ok(readOnlyResult.output.trim(), "live read-only Grok run returned no final text");
+ assert.equal(existsSync(readOnlyMarker), false, "read-only Grok phase mutated the workspace");
+ assert.doesNotMatch(
+ readOnlyResult.stderr,
+ /tool allowlist had unmappable entries|keeping full grok toolset/i,
+ "Grok rejected the allowlist and restored its full toolset",
+ );
+
+ const mutatingResult = await runGrokAgentTask(
+ mutatingCwd,
+ agents,
+ "mutating-probe",
+ `First create ${workspaceMarker} with content OK. Then attempt to create ${outsideMarker} with content MUST_NOT_WRITE. Check both paths and report the real results.`,
+ { idleTimeoutMs: 90_000 },
+ );
+ assert.equal(mutatingResult.exitCode, 0, mutatingResult.stderr || mutatingResult.errorMessage);
+ assert.ok(mutatingResult.output.trim(), "live mutating Grok run returned no final text");
+ assert.equal(existsSync(workspaceMarker), true, "workspace sandbox blocked an in-workspace write");
+ assert.equal(existsSync(outsideMarker), false, "workspace sandbox allowed a write outside cwd");
+ console.log(
+ `e2e-grok: ok (${mutatingResult.model ?? readOnlyResult.model ?? "default model"}; read-only + custom mutating sandbox policies enforced)`,
+ );
+} finally {
+ await rm(readOnlyCwd, { recursive: true, force: true });
+ await rm(mutatingCwd, { recursive: true, force: true });
+ await rm(outsideMarker, { force: true });
+}
diff --git a/packages/grok-taskflow/test/mcp-server.test.ts b/packages/grok-taskflow/test/mcp-server.test.ts
new file mode 100644
index 0000000..ccd4887
--- /dev/null
+++ b/packages/grok-taskflow/test/mcp-server.test.ts
@@ -0,0 +1,112 @@
+/**
+ * MCP server binding tests for the Grok Build adapter.
+ *
+ * The protocol layer + tool handlers live in taskflow-mcp-core (covered by the
+ * other host adapters against the same core); these pin the grok-bound surface:
+ * the handshake, the tool roster, and that the shim actually dispatches
+ * (verify runs without any grok process).
+ */
+
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { PassThrough } from "node:stream";
+import { serveStdio } from "taskflow-mcp-core/jsonrpc";
+import { makeMcpHandlers, makeToolHandlers } from "../src/mcp/server.ts";
+
+/** Send a list of JSON-RPC messages through the server, collect responses. */
+async function rpcRoundtrip(messages: object[]): Promise {
+ const input = new PassThrough();
+ const output = new PassThrough();
+ const responses: any[] = [];
+
+ let outBuf = "";
+ output.on("data", (d) => {
+ outBuf += d.toString();
+ let i: number;
+ while ((i = outBuf.indexOf("\n")) >= 0) {
+ const line = outBuf.slice(0, i);
+ outBuf = outBuf.slice(i + 1);
+ if (line.trim()) responses.push(JSON.parse(line));
+ }
+ });
+
+ const done = serveStdio(makeMcpHandlers(process.cwd()), { input, output });
+ for (const m of messages) input.write(JSON.stringify(m) + "\n");
+ input.end();
+ await done;
+ return responses;
+}
+
+test("grok mcp: initialize returns the protocol version + serverInfo", async () => {
+ const [res] = await rpcRoundtrip([
+ { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {} } },
+ ]);
+ assert.equal(res.id, 1);
+ assert.equal(res.result.protocolVersion, "2025-06-18");
+ assert.ok(res.result.capabilities.tools, "advertises tools capability");
+ assert.equal(res.result.serverInfo.name, "taskflow");
+ assert.equal(res.result.serverInfo.version, "0.2.0");
+});
+
+test("grok mcp: tools/list exposes the same taskflow tools as other hosts", async () => {
+ const [res] = await rpcRoundtrip([{ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }]);
+ const names = res.result.tools.map((t: any) => t.name);
+ assert.deepEqual(
+ names.sort(),
+ [
+ "taskflow_compile",
+ "taskflow_list",
+ "taskflow_peek",
+ "taskflow_recompute", "taskflow_replay",
+ "taskflow_run",
+ "taskflow_save",
+ "taskflow_search",
+ "taskflow_show",
+ "taskflow_trace",
+ "taskflow_verify",
+ "taskflow_why_stale",
+ ],
+ );
+ for (const t of res.result.tools) {
+ assert.equal(typeof t.description, "string");
+ assert.equal(t.inputSchema.type, "object");
+ }
+});
+
+test("grok mcp: taskflow_verify dispatches through the grok binding (no execution)", async () => {
+ const [res] = await rpcRoundtrip([
+ {
+ jsonrpc: "2.0",
+ id: 3,
+ method: "tools/call",
+ params: {
+ name: "taskflow_verify",
+ arguments: {
+ define: { name: "x", phases: [{ id: "a", type: "agent", agent: "executor", task: "do", final: true }] },
+ },
+ },
+ },
+ ]);
+ assert.equal(res.result.content[0].text, "✓ verification PASSED");
+ assert.equal(res.result.isError, false);
+});
+
+test("grok mcp: makeToolHandlers exposes the tools", () => {
+ const tools = makeToolHandlers(process.cwd());
+ assert.deepEqual(
+ Object.keys(tools).sort(),
+ [
+ "taskflow_compile",
+ "taskflow_list",
+ "taskflow_peek",
+ "taskflow_recompute", "taskflow_replay",
+ "taskflow_run",
+ "taskflow_save",
+ "taskflow_search",
+ "taskflow_show",
+ "taskflow_trace",
+ "taskflow_verify",
+ "taskflow_why_stale",
+ ],
+ );
+});
diff --git a/packages/grok-taskflow/tsconfig.build.json b/packages/grok-taskflow/tsconfig.build.json
new file mode 100644
index 0000000..863efae
--- /dev/null
+++ b/packages/grok-taskflow/tsconfig.build.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "rewriteRelativeImportExtensions": true
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/opencode-taskflow/package.json b/packages/opencode-taskflow/package.json
index 9ed452e..d4181b8 100644
--- a/packages/opencode-taskflow/package.json
+++ b/packages/opencode-taskflow/package.json
@@ -1,6 +1,6 @@
{
"name": "opencode-taskflow",
- "version": "0.1.8",
+ "version": "0.2.0",
"description": "Run taskflow on OpenCode: an OpenCode subagent runner plus an MCP server (and an opencode.json config scaffold) that exposes the taskflow_* tools to OpenCode users.",
"keywords": [
"opencode",
@@ -43,18 +43,23 @@
"opencode-taskflow-mcp": "./dist/mcp/bin.js"
},
"files": [
- "dist"
+ "dist",
+ "plugin"
],
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/copy-readme.mjs opencode-taskflow",
"prepublishOnly": "npm run build"
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "exports": {
+ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
+ "./mcp/server": { "types": "./dist/mcp/server.d.ts", "default": "./dist/mcp/server.js" }
+ }
},
"dependencies": {
- "taskflow-core": "0.1.8",
- "taskflow-hosts": "0.1.8",
- "taskflow-mcp-core": "0.1.8"
+ "taskflow-core": "workspace:*",
+ "taskflow-hosts": "workspace:*",
+ "taskflow-mcp-core": "workspace:*"
}
}
diff --git a/packages/opencode-taskflow/plugin/opencode.json b/packages/opencode-taskflow/plugin/opencode.json
index 331ff0c..7503a47 100644
--- a/packages/opencode-taskflow/plugin/opencode.json
+++ b/packages/opencode-taskflow/plugin/opencode.json
@@ -3,7 +3,7 @@
"mcp": {
"taskflow": {
"type": "local",
- "command": ["npx", "-y", "-p", "opencode-taskflow@0.1.8", "opencode-taskflow-mcp"],
+ "command": ["npx", "-y", "-p", "opencode-taskflow@0.2.0", "opencode-taskflow-mcp"],
"enabled": true
}
},
diff --git a/packages/opencode-taskflow/plugin/skills/taskflow/SKILL.md b/packages/opencode-taskflow/plugin/skills/taskflow/SKILL.md
index 3e3cd09..85949e7 100644
--- a/packages/opencode-taskflow/plugin/skills/taskflow/SKILL.md
+++ b/packages/opencode-taskflow/plugin/skills/taskflow/SKILL.md
@@ -18,8 +18,14 @@ the OpenCode form (`taskflow_verify`). Each phase's subagent runs as an isolated
| `taskflow_list` | List saved flows discoverable from the current working directory. |
| `taskflow_show` | Show a saved flow's full definition as JSON. |
| `taskflow_verify` | Statically verify a flow (cycles, missing deps, undefined refs, contract typos) — no execution, zero tokens. |
-| `taskflow_compile` | Render a flow's DAG as a diagram + a verification report — no execution. |
+| `taskflow_compile` | Render a flow's DAG as an inline SVG **and** text outline + a verification report — no execution. |
| `taskflow_peek` | Inspect one phase's intermediate output from a stored run (post-hoc debugging). Omit `phaseId` to list phases; `json`/`item`/`limit` refine the slice. Hard-truncated, read-only. |
+| `taskflow_trace` | Read a run's append-only event timeline. |
+| `taskflow_replay` | Replay recorded decisions offline with optional overrides — zero model calls. |
+| `taskflow_why_stale` | Explain why phases are stale from observed and declared dependencies — zero tokens. |
+| `taskflow_recompute` | Compute the stale frontier (**dry-run only** over MCP; never executes phases). |
+| `taskflow_save` | Save a reusable flow and optional library metadata. |
+| `taskflow_search` | Search and rank reusable flows before authoring another one. |
**Always `taskflow_verify` a non-trivial flow before `taskflow_run`** — it is
free and catches most authoring mistakes.
@@ -37,7 +43,7 @@ mistakes that break flows. Load the companion files **only when needed**:
|------|--------------------|
| `patterns.md` | **Designing a non-trivial flow.** Proven flow archetypes (audit fan-out, self-healing rework, plan→approve→execute, dynamic replanning, tournament synthesis, incremental audit), anti-patterns, and the production-flow quality checklist. |
| `advanced.md` | Dynamic sub-flow (`flow{def}`) contracts & security caps, and workspace isolation (`cwd: temp/dedicated/worktree`). |
-| `configuration.md` | Every knob: per-phase `model`/`thinking`/`tools`/`cwd`, concurrency model, agent discovery, `settings.json`, cross-run caching (`cache`, `fingerprint`, per-item map caching), args, storage paths. |
+| `configuration.md` | Every knob: per-phase `model`/`thinking`/`tools`/`cwd`, concurrency model, agent discovery, `settings.json`, cross-run caching (`cache`, `fingerprint`, per-item map caching), args, storage paths. **TypeScript DSL CLI** (`taskflow-dsl` / S4). |
| `library.md` | **Before authoring a non-trivial flow — SEARCH the reusable-flow library.** Save reusable flows with `purpose`+`tags` so future search finds them; reuse + generalize instead of rewriting from scratch. The compounding flywheel. |
> Rule of thumb: writing a flow with ≥ 4 phases, a gate, or any fan-out?
@@ -68,8 +74,8 @@ task deserves level 3 — the higher levels are where taskflow pays for itself.
| 0 | shorthand `task` / `tasks` / `chain` | one-off delegation, simple sequence |
| 1 | linear DAG with `dependsOn` | fixed steps, each consuming the last |
| 2 | discover → `map` fan-out → `gate` → `reduce` | many items, needs review before reporting |
-| 3 | + `eval` zero-token gates, `expect` contracts, `retry`, `onBlock: "retry"`, `budget`, `optional` fallbacks | production-grade: self-healing, cost-capped, fails precisely |
-| 4 | + `loop`, `tournament`, `flow{def}` dynamic planning | the work itself is discovered at runtime; one shot is unreliable |
+| 3 | + `eval` zero-token gates, `expect` contracts, `retry`, `onBlock: "retry"`, `budget`, `optional` fallbacks | production-grade: self-healing, cost stop-loss, fails precisely |
+| 4 | + `loop`, `tournament`, `flow{def}` / `expand`, `race` | the work itself is discovered at runtime; one shot is unreliable; try parallel approaches and keep the first win |
| 5 | + `incremental: true`, `cache.fingerprint` | the flow re-runs as the repo changes; only re-pay for what changed |
**A production-grade flow (level 3+) usually has:** machine checks before LLM
@@ -165,12 +171,12 @@ back cleanly: precedence is `define` (inline) > `defineFile` (disk) > `name`
}
```
-### Phase types (10)
+### Phase types (12)
| type | meaning | details |
|------|---------|---------|
| `agent` | one subagent runs `task` | this file |
-| `parallel` | run static `branches[]` concurrently | this file |
+| `parallel` | run static `branches[]` concurrently (all complete) | this file |
| `map` | fan out over `over` (an array) — one subagent per item, `{item}` bound | this file |
| `gate` | quality/review step that can **halt the flow** | Gate phases below |
| `reduce` | aggregate `from[]` phases into one output | this file |
@@ -179,6 +185,8 @@ back cleanly: precedence is `define` (inline) > `defineFile` (disk) > `name`
| `loop` | repeat a body until a condition / convergence / `maxIterations` | Loop phases below |
| `tournament` | run N competing `variants`, a `judge` picks best or aggregates | Tournament phases below |
| `script` | run a **shell command** (no LLM, zero tokens) — stdout is the output | Script phases below |
+| `race` | run `branches[]` concurrently; **first success wins** (unlike parallel) | Race phases below |
+| `expand` | run a dynamic fragment (`def`); `nested` (isolated) or `graft` (promote onto parent) | Expand phases below |
### Control-flow fields (any phase)
@@ -187,7 +195,7 @@ back cleanly: precedence is `define` (inline) > `defineFile` (disk) > `name`
| `when` | conditional guard — skip the phase unless the expression is truthy. Supports `{refs}`, `== != < > <= >=`, `&& \|\| !`, parentheses, quoted strings/numbers. Parse errors fail **open** (phase runs). |
| `join` | dependency join: `"all"` (default — wait for every dep) or `"any"` (OR-join — run as soon as one dep completes). |
| `retry` | `{ "max": N, "backoffMs": ms, "factor": k }` — retry a failing subagent up to N times; delay is `backoffMs * factor^attempt` (`factor:1`=fixed, `2`=exponential). |
-| `timeout` | max ms per subagent call (>= 1000). On expiry the subagent is aborted and the phase fails with a `timedOut` marker — deterministic, **never retried**. Caps EACH call, so a map/parallel/loop/tournament phase's wall time is per item/iteration/variant (a tournament's judge call gets its own cap too). Script phases keep their own child-process timeout (default 60s, max 300s). Not supported on approval/flow. Pair with `optional: true` + a downstream fallback phase to degrade instead of failing the run. |
+| `timeout` | max ms per subagent call (>= 1000). On expiry the subagent is aborted and the phase fails with a `timedOut` marker — deterministic, **never retried**. Caps EACH call, so a map/parallel/race/loop/tournament phase's wall time is per item/iteration/variant (a tournament's judge call gets its own cap too). Script phases keep their own child-process timeout (default 60s, max 300s). Not supported on approval/flow/expand. Pair with `optional: true` + a downstream fallback phase to degrade instead of failing the run. |
| `expect` | output contract for `output: "json"` phases (agent/gate/reduce/loop): a JSON-Schema-like shape `{type, properties, required, items, enum}` validated the moment the subagent finishes. A violation fails the phase with per-path diagnostics (e.g. `$.score: required key is missing`) and is retryable under the phase's explicit `retry`. `verify`/`compile` also statically warn when a `{steps.X.json.field}` ref names a field absent from X's declared contract. |
| `idempotent` | side-effect classification. Default `true` (safe to cache + auto-retry). Set `false` on phases with **irreversible side effects** (webhook POSTs, deploys, DB writes, file mutations): transient provider errors are **not** auto-retried (an explicit `retry{}` IS still honored — it's your declaration that repeats are acceptable) and the result is **never cached** in any scope (within-run resume, cross-run, `incremental` — the phase re-runs every time). The phase state records `sideEffect: true` (rendered as ⚡). |
| `optional` | fail-soft — a failed/blocked phase won't abort the run; downstream sees empty output. Pair with a fallback phase guarded by `when`. |
@@ -464,11 +472,68 @@ output is exact.
"input": "{steps.analyze.output}", "dependsOn": ["analyze"], "final": true }
```
-### Budget (cost / token caps)
+### Race phases (first success wins)
-Add a run-wide ceiling at the top level. When accumulated cost/tokens exceed it,
-remaining phases are skipped (and an in-flight `map`/`parallel` stops spawning
-new items); the run ends as `blocked` with partial outputs preserved.
+A `race` phase runs static `branches[]` concurrently and **returns the first
+branch that finishes successfully** (failed settles do **not** win — a slower
+success still wins over a fast hard-fail). Unlike `parallel` (waits for all) or
+`tournament` (judges quality after all variants), use race when latency matters
+more than comparing every approach.
+
+- `branches` — **required**, at least two `{task, agent?}`.
+- `cancelLosers` — optional boolean (default `true`). After the first **success**,
+ abort other branches via `AbortSignal` (best-effort — host must honor the
+ signal). Set `false` to let losers finish naturally.
+- Phase `usage` **aggregates all branches** (including aborted partials) so
+ budgets stay honest.
+- Output of the winning branch becomes the race phase output; a warning records
+ which branch won.
+
+```jsonc
+{
+ "id": "quick", "type": "race",
+ "branches": [
+ { "task": "Answer with a short heuristic…", "agent": "executor" },
+ { "task": "Answer with a thorough search…", "agent": "researcher" }
+ ],
+ "final": true
+}
+```
+
+### Expand phases (dynamic fragment: nested or graft)
+
+An `expand` phase runs a **fragment Taskflow** from `def` (inline object,
+phases array, or interpolated `{steps.plan.json}`). Two modes:
+
+| `expandMode` | Behavior |
+|--------------|----------|
+| `nested` (default) | Run as an isolated sub-flow (like `flow{def}`); child phase ids stay **off** the parent. |
+| `graft` | After success, **promote** child phase states onto the parent as `-` so later phases can read `{steps.grow-leaf.output}`. |
+
+- `def` — **required** for expand.
+- `maxNodes` — optional cap on fragment phase count (default 50, hard max 100).
+- Dynamic validation + nesting caps match `flow{def}` (see `advanced.md`).
+- Prefer `expand` when the planner fragment is a first-class kind; prefer
+ `flow` + `use` for saved reusable flows; prefer `flow` + `def` when you want
+ the classic nested sub-flow without graft promote.
+
+```jsonc
+{
+ "id": "grow", "type": "expand", "expandMode": "graft",
+ "def": "{steps.plan.json}",
+ "dependsOn": ["plan"], "final": true
+}
+```
+
+### Budget (observed-usage stop-loss)
+
+Add a run-wide stop-loss at the top level. Ordinary budgeted DAG layers and
+`map`/`parallel`/`tournament` fan-out use serial call admission. Once reported
+cost/tokens exceed the threshold, no new model call is started; the run ends as
+`blocked` with partial outputs preserved. An admitted call may cross the
+threshold. A `race` necessarily starts competing branches together, so all
+already-active race branches may contribute overshoot. This is never a
+zero-overshoot guarantee.
```jsonc
{ "name": "...", "budget": { "maxUSD": 1.50, "maxTokens": 2000000 }, "phases": [ ... ] }
@@ -477,6 +542,10 @@ new items); the run ends as `blocked` with partial outputs preserved.
**Any flow with a fan-out should have a `budget`** — a map over a
mis-discovered 500-item array is otherwise unbounded spend.
+Host accounting matters: Codex reports tokens but not cost, so Codex accepts
+`maxTokens` and rejects `maxUSD`. Grok 0.2.93 reports neither and rejects every
+flow declaring `budget`. Pi, Claude Code, and OpenCode accept both dimensions.
+
### Strict interpolation
By default an unresolved placeholder (typo'd `{steps.X.output}`, missing
@@ -581,6 +650,10 @@ and output sizes, then peek the suspicious phase (`json: true` for parsed
output, `item: n` for one fan-out section). Output is hard-truncated
(default 4000 chars, max 32000) so a peek never floods your context.
+Use `taskflow_trace` to inspect the append-only event log for a finished run,
+then `taskflow_replay` to re-judge it under alternate thresholds/budget **offline
+(zero tokens)** — e.g. "would a 0.9 gate threshold have blocked this run?"
+
For flows re-run as the repo evolves, pass `incremental: true` to
`taskflow_run` — every phase defaults to **cross-run cache reuse**: identical
input → $0 instant hit. Per-phase `cache.fingerprint` entries
diff --git a/packages/opencode-taskflow/plugin/skills/taskflow/advanced.md b/packages/opencode-taskflow/plugin/skills/taskflow/advanced.md
index 897b205..e15d3a0 100644
--- a/packages/opencode-taskflow/plugin/skills/taskflow/advanced.md
+++ b/packages/opencode-taskflow/plugin/skills/taskflow/advanced.md
@@ -2,8 +2,23 @@
# Taskflow Advanced — dynamic sub-flows & workspace isolation
-Load this when a flow needs: runtime-generated work (`flow{def}`) or isolated
-working directories (`cwd: temp/dedicated/worktree`).
+Load this when a flow needs: runtime-generated work (`flow{def}` / `expand`) or
+isolated working directories (`cwd: temp/dedicated/worktree`).
+
+---
+
+## `flow{def}` vs `expand` (when to use which)
+
+| Need | Prefer |
+|------|--------|
+| Saved reusable flow by name | `flow` + `use` |
+| Planner JSON as isolated nested sub-flow (classic) | `flow` + `def` **or** `expand` + `expandMode: "nested"` |
+| Fragment phases must appear on the **parent** run as `-` | `expand` + `expandMode: "graft"` |
+| First of several static approaches (latency) | `race` (not tournament) |
+
+`expand` is a first-class phase type (Horizon B). Dynamic validation / nesting /
+breadth caps match `flow{def}`. **Event kernel** still excludes `race`/`expand`
+(imperative path only until step handlers exist).
---
@@ -84,3 +99,65 @@ mutation without touching the main tree:
each `cwd: "worktree"`, each attempting a different refactor strategy and
reporting its test results; a downstream gate/judge picks which diff to apply
for real. The main tree is never touched by the losers.
+
+---
+
+## Trace & offline replay (`trace` / `replay`) — vs resume / recompute
+
+Three **different** reuse tools; do not conflate them:
+
+| Tool | Spends tokens? | Mutates the run? | Answers |
+|------|----------------|------------------|---------|
+| **`resume`** | Only unfinished / cache-miss phases | Continues the same run | "Pick up where we stopped" |
+| **`why-stale` → `recompute`** | Dry-run free; `--apply` / `dryRun:false` spends | Optional write of recompute result | "World/input changed — which phases re-run?" |
+| **`trace` → `replay`** | **Never** | Never | "If the gate threshold / budget had been different, would we have blocked?" |
+
+### Trace (read the evidence)
+
+Every instrumented run may write an append-only **event log**
+(`runs//.trace.jsonl`): phase lifecycle, each subagent
+input/output, and runtime **decisions** (gate verdict/score, when-guard,
+cache-hit, budget-hit, tournament-winner, unreplayable).
+
+```
+taskflow_trace { runId: "" }
+taskflow_trace { runId: "", json: true }
+```
+
+MCP trace responses are bounded. JSON mode returns an envelope with
+`total`/`returned`/`truncated`; use `limit` (default 200, max 1000) to select the
+newest events without flooding the host context.
+
+If there is no log (pre-trace run, or no sink injected), the tool reports that
+clearly — it never invents events.
+
+### Offline replay (what-if, zero tokens)
+
+`replay` **re-folds** the recorded log under alternate **decision knobs** without
+calling any model:
+
+- `thresholds` — map of `phaseId → new score threshold` (gate-score events)
+- `budgetMaxUSD` / `budgetMaxTokens` — would later phases have been skipped?
+- `models` / `args` — currently report `needs-live-rerun` (quality cannot be
+ re-judged offline without re-execution)
+
+Outcomes per phase: `reused`, `would-block`, `verdict-flipped`,
+`would-exceed-budget`, `threshold-changed`, `needs-live-rerun`, `failed`.
+
+```
+taskflow_replay { runId: "", thresholds: { review: 0.9 } }
+taskflow_replay { runId: "", budgetMaxUSD: 0.05, json: true }
+```
+
+**Import-graph guarantee:** `replayRun` never imports the process-spawning
+runtime or event kernel — offline replay cannot accidentally spend tokens.
+
+### When to use which
+
+| Situation | Use |
+|-----------|-----|
+| Rate-limit mid-run; inputs unchanged | `resume` |
+| Repo file changed; re-pay only affected phases | `why-stale` → `recompute` |
+| "Would a stricter gate have blocked last night's run?" | `trace` → `replay` with new `thresholds` |
+| "Would a $0.10 cap have stopped the fan-out?" | `replay` with `budgetMaxUSD` |
+| Need fresh model judgment under a new model id | `replay` will say `needs-live-rerun` → live `recompute`/`run` |
diff --git a/packages/opencode-taskflow/plugin/skills/taskflow/configuration.md b/packages/opencode-taskflow/plugin/skills/taskflow/configuration.md
index 4378269..025a7d2 100644
--- a/packages/opencode-taskflow/plugin/skills/taskflow/configuration.md
+++ b/packages/opencode-taskflow/plugin/skills/taskflow/configuration.md
@@ -45,7 +45,7 @@ Top-level keys of the taskflow definition object.
| `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. |
| `args` | record | `{}` | Declared invocation arguments. See §3. |
| `phases` | array | — | **Required.** The phase DAG. See §2. |
-| `version` | number | `1` | ⚠️ Declared in schema but **not yet used** by the runtime. |
+| `version` | number | `1` | Informational metadata in 0.2.0; it does not select runtime semantics or migrate a flow. |
---
@@ -56,14 +56,16 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s.
```jsonc
{
"id": "audit", // required, unique — referenced via {steps.audit.output}
- "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script (default: agent)
+ "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent)
"agent": "analyst", // agent name to run this phase
"task": "Audit {item.route}…",
"dependsOn": ["discover"],// DAG edges
"over": "{steps.discover.json}", // [map] array to fan out over
"as": "item", // [map] loop var name (default: item)
- "branches": [ /* … */ ], // [parallel] static task list
+ "branches": [ /* … */ ], // [parallel|race] static task list
"from": ["audit"], // [reduce] phase ids to aggregate
+ "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow
+ "expandMode": "nested", // [expand] nested | graft
"output": "json", // text | json (default: text)
"model": "claude-sonnet-4-5", // per-phase model override
"thinking": "high", // per-phase thinking override
@@ -77,13 +79,17 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s.
| Key | Applies to | Default | Notes |
|-----|-----------|---------|-------|
| `id` | all | — | **Required, unique.** Used in `{steps.…}`. |
-| `type` | all | `agent` | One of the 10 phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script). |
+| `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). |
| `agent` | all | first available | Agent name; resolved from the scoped pool. |
| `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types. |
| `over` | map | — | **Required for map.** Must resolve to an array. |
| `as` | map | `item` | Loop variable bound per item. |
-| `branches` | parallel | — | **Required for parallel.** `[{task, agent?}]`. |
+| `branches` | parallel, race | — | **Required** (≥1 for parallel; ≥2 for race). `[{task, agent?}]`. |
+| `cancelLosers` | race | `true` | Abort in-flight losers after first **success** (best-effort AbortSignal). |
| `from` | reduce | — | **Required for reduce.** Phase ids whose outputs are aggregated. |
+| `def` | expand, flow | — | **Required for expand.** Fragment Taskflow / phases array / `{steps.X.json}`. |
+| `expandMode` | expand | `nested` | `nested` = isolated sub-flow; `graft` = promote children as `-`. |
+| `maxNodes` | expand | `50` | Cap fragment phase count (1..100). |
| `run` | script | — | **Required for script.** Shell command: a string (runs in a shell) or an array (direct exec, no shell). A string with an interpolation placeholder is rejected (injection guard). |
| `input` | script | — | Text piped to the command's stdin; supports interpolation. |
| `timeout` | script | `60000` | Max run time in ms (1000–300000). On timeout: SIGTERM → SIGKILL, phase fails. |
@@ -99,12 +105,12 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s.
| `cache` | all | `run-only` | Per-phase cache policy (`scope`/`ttl`/`fingerprint`). See §11. |
| `final` | all | last phase | Exactly one phase may be `final`; its output is returned. |
-> Gate-only control fields (`eval`, `onBlock`), the loop/tournament control
+> Gate-only control fields (`eval`, `onBlock`, score), the loop/tournament control
> fields (`until`/`maxIterations`/`convergence`, `variants`/`judge`/`judgeAgent`/`mode`),
-> the script fields (`run`/`input`/`timeout`), and the cross-phase contract
-> fields (`expect`, `timeout`, `optional`, `strictInterpolation`) are documented
-> in `SKILL.md` next to their phase types. `shareContext` and the workspace
-> `cwd` keywords (`temp`/`dedicated`/`worktree`) are in `advanced.md`.
+> the script fields (`run`/`input`/`timeout`), race/expand fields above, and the
+> cross-phase contract fields (`expect`, `timeout`, `optional`, `strictInterpolation`)
+> are documented in `SKILL.md` next to their phase types. `shareContext` and the
+> workspace `cwd` keywords (`temp`/`dedicated`/`worktree`) are in `advanced.md`.
---
@@ -208,17 +214,28 @@ For any phase, the effective value is resolved in this **precedence order**
|---------|-------------------------|
| **model** | `phase.model` → agent frontmatter `model` (resolved via `modelRoles`) → pi default |
| **thinking** | `phase.thinking` → agent frontmatter `thinking` → `settings` global thinking → pi default |
-| **tools** | `phase.tools` → agent frontmatter `tools` → all tools |
+| **tools** | `phase.tools` → agent frontmatter `tools` → host default capability policy |
Notes:
-- `tools` is a **whitelist**. Omit it to allow all.
+- `tools` expresses the requested capability set, but enforcement is
+ host-specific. It is a literal whitelist on Pi; Codex maps it to an OS
+ sandbox profile, while the other hosts use their own permission contracts.
+ Omit it to request the host's default capability policy.
- Each phase runs as an isolated `opencode run --format json` session. A model
id that is an unresolved `{{placeholder}}`, carries a pi thinking suffix
(`:xhigh`), or is a multi-segment openrouter path (≥ 2 slashes) is dropped so
opencode falls back to its configured default; a clean `provider/model` id
- passes through. Read-only phases inject a deny-mutations permission policy
- (via `OPENCODE_CONFIG_CONTENT`) so bash/write/edit are genuinely blocked;
- mutating phases run with `--auto` (auto-approve).
+ passes through. Every child uses `--pure` to disable external plugins.
+ Read-only phases inject a default-deny permission policy via
+ `OPENCODE_CONFIG_CONTENT`, allowing only known read/list/search built-ins and
+ denying inherited custom/MCP tools; mutating/default phases fail closed unless the
+ operator explicitly sets `PI_TASKFLOW_OPENCODE_UNSAFE_AUTO=1`.
+ Children inherit only platform/proxy/CA, OpenCode config, and supported
+ provider variables; unrelated secrets are removed.
+
+For Codex, OpenCode, or Grok, an operator can intentionally pass additional
+task-specific environment variables by listing their names in the
+comma-separated `PI_TASKFLOW_CHILD_ENV_ALLOW` setting.
- The agent's markdown body becomes the subagent's appended system prompt.
---
@@ -311,7 +328,7 @@ Rather than annotating every phase with `cache: { "scope": "cross-run" }`, set
Precedence: the invocation `incremental` argument wins over the flow's
`incremental` field, which is in turn overridden by any **per-phase** `cache`
setting. The cross-run-blocked phase types (`gate`/`approval`/`loop`/
-`tournament`/`script`) and all per-phase soundness fallbacks still apply. The default
+`tournament`/`script`/`race`/`expand`) and all per-phase soundness fallbacks still apply. The default
remains `run-only` (each run starts fresh unless something opts in), because
cross-run reuse silently persists outputs and can serve stale results for phases
whose agents read files at runtime.
@@ -358,9 +375,15 @@ Each entry is one of:
|----------|--------|
| `PI_TASKFLOW_PI_BIN` | Override the `pi` binary used to spawn subagents. Used by tests and unusual launch setups (e.g. `PI_TASKFLOW_PI_BIN=pi`). Normally auto-detected. |
| `PI_TASKFLOW_CODEX_BIN` | Override the `codex` binary used to spawn Codex subagents. |
+| `PI_TASKFLOW_CHILD_ENV_ALLOW` | Comma-separated names of extra task-specific environment variables to pass intentionally to Codex/OpenCode/Grok children. Unlisted application secrets are removed. |
| `PI_TASKFLOW_CLAUDE_BIN` | Override the `claude` binary used to spawn Claude Code subagents. |
+| `PI_TASKFLOW_CLAUDE_UNSAFE_BYPASS=1` | Explicitly allow trusted Claude phases requesting known mutating tools to use narrow `--tools` + `bypassPermissions`; unknown names always fail closed. |
| `PI_TASKFLOW_OPENCODE_BIN` | Override the `opencode` binary used to spawn OpenCode subagents. |
| `PI_TASKFLOW_OPENCODE_MODEL` | Override the default OpenCode model for OpenCode executor e2e tests (e.g. `opencode/deepseek-v4-flash-free`). |
+| `PI_TASKFLOW_OPENCODE_UNSAFE_AUTO=1` | Explicitly permit trusted OpenCode mutating/default phases to use unsandboxed `--auto`; otherwise they fail before spawn. All OpenCode children still use `--pure`. |
+| `PI_TASKFLOW_GROK_BIN` | Override the `grok` binary used to spawn Grok Build subagents. |
+| `PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE` | Required for Grok mutating/no-whitelist phases. Must name a custom profile from `~/.grok/sandbox.toml`; built-in profiles are rejected because they may fail open on unsupported hosts. |
+| `PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE` | Required for Grok read-only phases. Must name a custom profile extending `read-only`; built-in names are rejected so hooks/plugins remain kernel-contained if the host cannot enforce a built-in profile. |
---
@@ -408,10 +431,83 @@ Each entry is one of:
---
-## Caveats (declared but not yet enforced)
+## 9. TypeScript DSL CLI (`taskflow-dsl` / S4)
+
+Author flows as **`.tf.ts`** (compile-time runes), then run the emitted JSON
+through existing `taskflow_*` tools. JSON remains first-class (escape hatch).
+
+```bash
+# From a monorepo checkout (dev):
+node --conditions=development --experimental-strip-types \
+ packages/taskflow-dsl/src/cli.ts new audit
+# edit audit.tf.ts
+node --conditions=development --experimental-strip-types \
+ packages/taskflow-dsl/src/cli.ts check audit.tf.ts
+# Fast rune/static-only pass (skip the default full tsc Program check):
+node --conditions=development --experimental-strip-types \
+ packages/taskflow-dsl/src/cli.ts check audit.tf.ts --no-typecheck
+node --conditions=development --experimental-strip-types \
+ packages/taskflow-dsl/src/cli.ts build audit.tf.ts --emit both
+# → audit.taskflow.json (+ audit.flowir.json)
+# Then: taskflow_verify / taskflow_run with defineFile=audit.taskflow.json
+```
+
+| Command | Purpose |
+|---------|---------|
+| `new [name]` | ≤5-line hello skeleton (`.tf.ts` or `--json-escape` JSON) |
+| `check ` | Erase + `validateTaskflow` + tsc (use `--no-typecheck` for a faster static-only pass) |
+| `build ` | Erase → Taskflow JSON; optional FlowIR hash (`--emit taskflow\|flowir\|both`) |
+| `decompile ` | Taskflow JSON → readable `.tf.ts` (semantic, not literal) |
+
+Output commands are create-only by default: pass `--force` to replace an
+existing regular file. Outputs are `--cwd`-contained, reject destination
+symlinks, and commit atomically; `--emit both` preflights both destinations.
+
+**Authoring notes (kinds ↔ runes)**
+
+Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow
+JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry).
+
+| JSON `type` | DSL rune(s) | Notes |
+|-------------|-------------|--------|
+| `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` |
+| `parallel` | `parallel([agent…])` | waits for all branches |
+| `map` | `map(source, item => agent…)` | `over` + `as` |
+| `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` |
+| `reduce` | `reduce([…], () => agent…)` | `from` |
+| `approval` | `approval({ request })` | |
+| `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def |
+| `loop` | `loop({ task, until?, … })` | |
+| `tournament` | `tournament({ branches/variants, judge, … })` | |
+| `script` | `script(run, opts?)` | string or argv array |
+| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted |
+| `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` |
+
+- `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`.
+- `race([...])` does **not** support array destructure — bind as one phase: `const winner = race([...])`.
+- Unbuilt `.tf.ts` must **not** be executed as a Node program (runes throw `TFDSL_ERASE_ONLY`).
+- Modular erase: new kind → `packages/taskflow-dsl/src/build/erase/kinds/.ts` + registry entry (see `docs/internal/modularization-0.2.0.md`).
+
+Design docs: `docs/rfc-0.2.0-s4-mvp.md`, `docs/rfc-0.2.0-dsl-phases-horizon.md`.
-These keys validate but the runtime does **not** act on them yet — don't rely on
-them for behavior:
+---
-- `arg.required` — missing required args are not rejected.
-- `flow.version` — informational only.
+## Caveats (declared but not yet enforced / partial)
+
+These keys validate but the runtime does **not** fully act on them yet — don't
+rely on them for behavior:
+
+- `arg.required` — documents intent for tooling, but missing required args are
+ not rejected at run time in 0.2.0. Use strict interpolation and verify/run
+ argument validation at your integration boundary when absence must block.
+- `flow.version` — informational only; it does not select runtime semantics.
+- **Event kernel** (`eventKernel` / `PI_TASKFLOW_EVENT_KERNEL=1`) — opt-in; does
+ **not** run `race`/`expand`; score gates, `retry`, `expect`, reflexion,
+ cross-run cache, and Shared Context Tree force the **imperative** path.
+- **`taskflow-dsl decompile`** — generates safe, readable TypeScript whose
+ rebuilt Taskflow/FlowIR is semantically equivalent for supported constructs;
+ dependencies are emitted before consumers even when input JSON is out of
+ order;
+ it does **not** reproduce original variable names, formatting, comments, or
+ source spelling. Unsupported/lossy constructs fail rather than silently
+ promising a literal round-trip.
diff --git a/packages/opencode-taskflow/plugin/skills/taskflow/patterns.md b/packages/opencode-taskflow/plugin/skills/taskflow/patterns.md
index 51b0b47..48f11cd 100644
--- a/packages/opencode-taskflow/plugin/skills/taskflow/patterns.md
+++ b/packages/opencode-taskflow/plugin/skills/taskflow/patterns.md
@@ -79,7 +79,7 @@ summarize every module.
Why each piece: `expect`+`retry` on discover means a chatty scout gets a second
chance instead of feeding garbage to the map; `concurrency: 4` protects rate
-limits; the gate is a *different* agent than the auditor; `budget` bounds the
+limits; the gate is a *different* agent than the auditor; `budget` limits the
fan-out.
**Variant — per-item caching for repeated audits:** add
@@ -243,6 +243,63 @@ instead merges all variants — good for research synthesis, bad for decisions.
---
+## Archetype 7: Race for latency (first approach wins)
+
+When several strategies can answer the same question and you care about **time
+to first good answer** more than comparing quality, use `race` (not
+`tournament`). Branches start together; the first successful completion becomes
+the phase output.
+
+```jsonc
+{
+ "name": "quick-answer",
+ "budget": { "maxUSD": 0.5 },
+ "phases": [
+ {
+ "id": "answer", "type": "race",
+ "branches": [
+ { "task": "Answer from local heuristics only: {args.q}", "agent": "executor" },
+ { "task": "Answer after a short web/docs look: {args.q}", "agent": "researcher" }
+ ],
+ "final": true
+ }
+ ]
+}
+```
+
+**Prefer tournament when** you need a judge to pick the *best* draft after all
+variants finish. **Prefer parallel when** you need *every* branch's output
+downstream (then `reduce`).
+
+## Archetype 8: Expand graft (planner fragment on the parent DAG)
+
+Planner emits a fragment; `expand` with `expandMode: "graft"` runs it and
+promotes child phase states as `-` so a later phase can read
+them. Use `nested` (or classic `flow{def}`) when you only need the fragment's
+**final** output and do not want child ids on the parent.
+
+```jsonc
+{
+ "name": "plan-graft",
+ "phases": [
+ {
+ "id": "plan", "type": "agent", "agent": "planner", "output": "json",
+ "task": "Emit a mini-flow JSON: {name, phases:[{id,type,agent,task,final?}…]} for the audit.",
+ "expect": { "type": "object", "required": ["phases"] }
+ },
+ {
+ "id": "grow", "type": "expand", "expandMode": "graft",
+ "def": "{steps.plan.json}", "dependsOn": ["plan"]
+ },
+ {
+ "id": "wrap", "type": "agent", "agent": "writer",
+ "task": "Summarize grafted work. Child outputs may appear as steps.grow-* in the run state.",
+ "dependsOn": ["grow"], "final": true
+ }
+ ]
+}
+```
+
## Archetype 6: Incremental repo-watch audit (cross-run)
Use for flows you'll re-run as the repo evolves. First run pays full price;
diff --git a/packages/opencode-taskflow/test/mcp-server.test.ts b/packages/opencode-taskflow/test/mcp-server.test.ts
index 39bbdde..0429036 100644
--- a/packages/opencode-taskflow/test/mcp-server.test.ts
+++ b/packages/opencode-taskflow/test/mcp-server.test.ts
@@ -45,6 +45,7 @@ test("opencode mcp: initialize returns the protocol version + serverInfo", async
assert.equal(res.result.protocolVersion, "2025-06-18");
assert.ok(res.result.capabilities.tools, "advertises tools capability");
assert.equal(res.result.serverInfo.name, "taskflow");
+ assert.equal(res.result.serverInfo.version, "0.2.0");
});
test("opencode mcp: tools/list exposes the taskflow tools", async () => {
@@ -52,7 +53,7 @@ test("opencode mcp: tools/list exposes the taskflow tools", async () => {
const names = res.result.tools.map((t: any) => t.name);
assert.deepEqual(
names.sort(),
- ["taskflow_compile", "taskflow_list", "taskflow_peek", "taskflow_recompute", "taskflow_run", "taskflow_save", "taskflow_search", "taskflow_show", "taskflow_trace", "taskflow_verify", "taskflow_why_stale"],
+ ["taskflow_compile", "taskflow_list", "taskflow_peek", "taskflow_recompute", "taskflow_replay", "taskflow_run", "taskflow_save", "taskflow_search", "taskflow_show", "taskflow_trace", "taskflow_verify", "taskflow_why_stale"],
);
for (const t of res.result.tools) {
assert.equal(typeof t.description, "string");
@@ -82,6 +83,6 @@ test("opencode mcp: makeToolHandlers exposes the tools", () => {
const tools = makeToolHandlers(process.cwd());
assert.deepEqual(
Object.keys(tools).sort(),
- ["taskflow_compile", "taskflow_list", "taskflow_peek", "taskflow_recompute", "taskflow_run", "taskflow_save", "taskflow_search", "taskflow_show", "taskflow_trace", "taskflow_verify", "taskflow_why_stale"],
+ ["taskflow_compile", "taskflow_list", "taskflow_peek", "taskflow_recompute", "taskflow_replay", "taskflow_run", "taskflow_save", "taskflow_search", "taskflow_show", "taskflow_trace", "taskflow_verify", "taskflow_why_stale"],
);
});
diff --git a/packages/pi-taskflow/package.json b/packages/pi-taskflow/package.json
index ee0d751..76b4e8a 100644
--- a/packages/pi-taskflow/package.json
+++ b/packages/pi-taskflow/package.json
@@ -1,6 +1,6 @@
{
"name": "pi-taskflow",
- "version": "0.1.8",
+ "version": "0.2.0",
"description": "A declarative, verifiable graph of task nodes for the Pi coding agent — statically verified before it runs, with dynamic fan-out, gates, isolated subagent context, resumable runs, and saveable commands.",
"keywords": [
"pi-package",
@@ -53,7 +53,7 @@
"image": "https://raw.githubusercontent.com/heggria/taskflow/main/assets/social-preview.png"
},
"dependencies": {
- "taskflow-core": "0.1.8"
+ "taskflow-core": "workspace:*"
},
"peerDependencies": {
"@earendil-works/pi-agent-core": "*",
diff --git a/packages/pi-taskflow/skills/taskflow/SKILL.md b/packages/pi-taskflow/skills/taskflow/SKILL.md
index f5eabb2..dee45d3 100644
--- a/packages/pi-taskflow/skills/taskflow/SKILL.md
+++ b/packages/pi-taskflow/skills/taskflow/SKILL.md
@@ -25,7 +25,7 @@ mistakes that break flows. Load the companion files **only when needed**:
|------|--------------------|
| `patterns.md` | **Designing a non-trivial flow.** Proven flow archetypes (audit fan-out, self-healing rework, plan→approve→execute, dynamic replanning, tournament synthesis, incremental audit), anti-patterns, and the production-flow quality checklist. |
| `advanced.md` | Shared Context Tree (`ctx_*` tools, `ctx_spawn` sub-graphs), workspace isolation (`cwd: temp/dedicated/worktree`), dynamic sub-flow (`flow{def}`) contracts & security caps, and the **incremental recompute suite** (`ir` / `provenance` / `why-stale` / `recompute` / `cache-clear`). |
-| `configuration.md` | Every knob: per-phase `model`/`thinking`/`tools`/`cwd`, concurrency model, agent discovery, `settings.json`, cross-run caching (`cache`, `fingerprint`, per-item map caching), args, storage paths. |
+| `configuration.md` | Every knob: per-phase `model`/`thinking`/`tools`/`cwd`, concurrency model, agent discovery, `settings.json`, cross-run caching (`cache`, `fingerprint`, per-item map caching), args, storage paths. **TypeScript DSL CLI** (`taskflow-dsl` / S4). |
| `library.md` | **Before authoring a non-trivial flow — SEARCH the reusable-flow library.** Save reusable flows with `purpose`+`tags` so future search finds them; reuse + generalize instead of rewriting from scratch. The compounding flywheel. |
> Rule of thumb: writing a flow with ≥ 4 phases, a gate, or any fan-out?
@@ -58,8 +58,8 @@ task deserves level 3 — the higher levels are where taskflow pays for itself.
| 0 | shorthand `task` / `tasks` / `chain` | one-off delegation, simple sequence |
| 1 | linear DAG with `dependsOn` | fixed steps, each consuming the last |
| 2 | discover → `map` fan-out → `gate` → `reduce` | many items, needs review before reporting |
-| 3 | + `eval` zero-token gates, `expect` contracts, `retry`, `onBlock: "retry"`, `budget`, `optional` fallbacks | production-grade: self-healing, cost-capped, fails precisely |
-| 4 | + `loop`, `tournament`, `flow{def}` dynamic planning | the work itself is discovered at runtime; one shot is unreliable |
+| 3 | + `eval` zero-token gates, `expect` contracts, `retry`, `onBlock: "retry"`, `budget`, `optional` fallbacks | production-grade: self-healing, cost stop-loss, fails precisely |
+| 4 | + `loop`, `tournament`, `flow{def}` / `expand`, `race` | the work itself is discovered at runtime; one shot is unreliable; try parallel approaches and keep the first win |
| 5 | + `incremental: true`, `cache.fingerprint` | the flow re-runs as the repo changes; only re-pay for what changed |
**A production-grade flow (level 3+) usually has:** machine checks before LLM
@@ -156,12 +156,12 @@ back cleanly: precedence is `define` (inline) > `defineFile` (disk) > `name`
}
```
-### Phase types (10)
+### Phase types (12)
| type | meaning | details |
|------|---------|---------|
| `agent` | one subagent runs `task` | this file |
-| `parallel` | run static `branches[]` concurrently | this file |
+| `parallel` | run static `branches[]` concurrently (all complete) | this file |
| `map` | fan out over `over` (an array) — one subagent per item, `{item}` bound | this file |
| `gate` | quality/review step that can **halt the flow** | Gate phases below |
| `reduce` | aggregate `from[]` phases into one output | this file |
@@ -170,6 +170,8 @@ back cleanly: precedence is `define` (inline) > `defineFile` (disk) > `name`
| `loop` | repeat a body until a condition / convergence / `maxIterations` | Loop phases below |
| `tournament` | run N competing `variants`, a `judge` picks best or aggregates | Tournament phases below |
| `script` | run a **shell command** (no LLM, zero tokens) — stdout is the output | Script phases below |
+| `race` | run `branches[]` concurrently; **first success wins** (unlike parallel) | Race phases below |
+| `expand` | run a dynamic fragment (`def`); `nested` (isolated) or `graft` (promote onto parent) | Expand phases below |
### Control-flow fields (any phase)
@@ -178,7 +180,7 @@ back cleanly: precedence is `define` (inline) > `defineFile` (disk) > `name`
| `when` | conditional guard — skip the phase unless the expression is truthy. Supports `{refs}`, `== != < > <= >=`, `&& \|\| !`, parentheses, quoted strings/numbers. Parse errors fail **open** (phase runs). |
| `join` | dependency join: `"all"` (default — wait for every dep) or `"any"` (OR-join — run as soon as one dep completes). |
| `retry` | `{ "max": N, "backoffMs": ms, "factor": k }` — retry a failing subagent up to N times; delay is `backoffMs * factor^attempt` (`factor:1`=fixed, `2`=exponential). |
-| `timeout` | max ms per subagent call (>= 1000). On expiry the subagent is aborted and the phase fails with a `timedOut` marker — deterministic, **never retried**. Caps EACH call, so a map/parallel/loop/tournament phase's wall time is per item/iteration/variant (a tournament's judge call gets its own cap too). Script phases keep their own child-process timeout (default 60s, max 300s). Not supported on approval/flow. Pair with `optional: true` + a downstream fallback phase to degrade instead of failing the run. |
+| `timeout` | max ms per subagent call (>= 1000). On expiry the subagent is aborted and the phase fails with a `timedOut` marker — deterministic, **never retried**. Caps EACH call, so a map/parallel/race/loop/tournament phase's wall time is per item/iteration/variant (a tournament's judge call gets its own cap too). Script phases keep their own child-process timeout (default 60s, max 300s). Not supported on approval/flow/expand. Pair with `optional: true` + a downstream fallback phase to degrade instead of failing the run. |
| `expect` | output contract for `output: "json"` phases (agent/gate/reduce/loop): a JSON-Schema-like shape `{type, properties, required, items, enum}` validated the moment the subagent finishes. A violation fails the phase with per-path diagnostics (e.g. `$.score: required key is missing`) and is retryable under the phase's explicit `retry`. `verify`/`compile` also statically warn when a `{steps.X.json.field}` ref names a field absent from X's declared contract. |
| `idempotent` | side-effect classification. Default `true` (safe to cache + auto-retry). Set `false` on phases with **irreversible side effects** (webhook POSTs, deploys, DB writes, file mutations): transient provider errors are **not** auto-retried (an explicit `retry{}` IS still honored — it's your declaration that repeats are acceptable) and the result is **never cached** in any scope (within-run resume, cross-run, `incremental` — the phase re-runs every time). The phase state records `sideEffect: true` (rendered as ⚡). |
| `optional` | fail-soft — a failed/blocked phase won't abort the run; downstream sees empty output. Pair with a fallback phase guarded by `when`. |
@@ -453,11 +455,68 @@ output is exact.
"input": "{steps.analyze.output}", "dependsOn": ["analyze"], "final": true }
```
-### Budget (cost / token caps)
+### Race phases (first success wins)
-Add a run-wide ceiling at the top level. When accumulated cost/tokens exceed it,
-remaining phases are skipped (and an in-flight `map`/`parallel` stops spawning
-new items); the run ends as `blocked` with partial outputs preserved.
+A `race` phase runs static `branches[]` concurrently and **returns the first
+branch that finishes successfully** (failed settles do **not** win — a slower
+success still wins over a fast hard-fail). Unlike `parallel` (waits for all) or
+`tournament` (judges quality after all variants), use race when latency matters
+more than comparing every approach.
+
+- `branches` — **required**, at least two `{task, agent?}`.
+- `cancelLosers` — optional boolean (default `true`). After the first **success**,
+ abort other branches via `AbortSignal` (best-effort — host must honor the
+ signal). Set `false` to let losers finish naturally.
+- Phase `usage` **aggregates all branches** (including aborted partials) so
+ budgets stay honest.
+- Output of the winning branch becomes the race phase output; a warning records
+ which branch won.
+
+```jsonc
+{
+ "id": "quick", "type": "race",
+ "branches": [
+ { "task": "Answer with a short heuristic…", "agent": "executor" },
+ { "task": "Answer with a thorough search…", "agent": "researcher" }
+ ],
+ "final": true
+}
+```
+
+### Expand phases (dynamic fragment: nested or graft)
+
+An `expand` phase runs a **fragment Taskflow** from `def` (inline object,
+phases array, or interpolated `{steps.plan.json}`). Two modes:
+
+| `expandMode` | Behavior |
+|--------------|----------|
+| `nested` (default) | Run as an isolated sub-flow (like `flow{def}`); child phase ids stay **off** the parent. |
+| `graft` | After success, **promote** child phase states onto the parent as `-` so later phases can read `{steps.grow-leaf.output}`. |
+
+- `def` — **required** for expand.
+- `maxNodes` — optional cap on fragment phase count (default 50, hard max 100).
+- Dynamic validation + nesting caps match `flow{def}` (see `advanced.md`).
+- Prefer `expand` when the planner fragment is a first-class kind; prefer
+ `flow` + `use` for saved reusable flows; prefer `flow` + `def` when you want
+ the classic nested sub-flow without graft promote.
+
+```jsonc
+{
+ "id": "grow", "type": "expand", "expandMode": "graft",
+ "def": "{steps.plan.json}",
+ "dependsOn": ["plan"], "final": true
+}
+```
+
+### Budget (observed-usage stop-loss)
+
+Add a run-wide stop-loss at the top level. Ordinary budgeted DAG layers and
+`map`/`parallel`/`tournament` fan-out use serial call admission. Once reported
+cost/tokens exceed the threshold, no new model call is started; the run ends as
+`blocked` with partial outputs preserved. An admitted call may cross the
+threshold. A `race` necessarily starts competing branches together, so all
+already-active race branches may contribute overshoot. This is never a
+zero-overshoot guarantee.
```jsonc
{ "name": "...", "budget": { "maxUSD": 1.50, "maxTokens": 2000000 }, "phases": [ ... ] }
@@ -466,6 +525,10 @@ new items); the run ends as `blocked` with partial outputs preserved.
**Any flow with a fan-out should have a `budget`** — a map over a
mis-discovered 500-item array is otherwise unbounded spend.
+Host accounting matters: Codex reports tokens but not cost, so Codex accepts
+`maxTokens` and rejects `maxUSD`. Grok 0.2.93 reports neither and rejects every
+flow declaring `budget`. Pi, Claude Code, and OpenCode accept both dimensions.
+
### Strict interpolation
By default an unresolved placeholder (typo'd `{steps.X.output}`, missing
@@ -552,7 +615,7 @@ Phase ids and agent names use **hyphens** (`audit-each`, `risk-reviewer`).
An unknown agent name fails the phase with the list of available agents.
Check with `action: "agents"` instead of guessing.
-## Actions (all 15)
+## Actions (all 16)
| action | what it does |
|--------|--------------|
@@ -565,7 +628,8 @@ Check with `action: "agents"` instead of guessing.
| `compile` | Render a flow as a Mermaid diagram + verification report. Zero tokens. |
| `ir` | Compile to **FlowIR** — the canonical intermediate representation with a content hash per phase. Use to diff two versions of a flow or confirm a definition change actually changed a phase's fingerprint. Zero tokens. |
| `provenance` | Show a completed run's **observed read-sets** — which phases actually read which upstream outputs at runtime (may be narrower than `dependsOn`). Requires `runId`. Zero tokens. |
-| `trace` | Show a completed run's **deterministic-replay event trace** — each subagent call's input/output + the runtime's own decisions (gate verdicts, when-guard results, cache hits, unreplayable markers). The foundation for offline replay (re-evaluating a run against changed thresholds/budget, zero tokens — full replay logic lands in a later release). `runId` required; `--json` for the complete machine-readable record. Zero tokens, read-only. |
+| `trace` | Show a completed run's **deterministic-replay event trace** — each subagent call's input/output + the runtime's own decisions (gate verdicts, when-guard results, cache hits, unreplayable markers). `runId` required; `--json` for the complete machine-readable record. Zero tokens, read-only. |
+| `replay` | **Offline what-if** on a recorded trace: re-evaluate under alternate gate thresholds, budget caps, or model routes **without calling the model** (zero tokens). Reports per-phase `reused` / `would-block` / `verdict-flipped` / `would-exceed-budget` / `needs-live-rerun`. `runId` required; optional `thresholds`, `budgetMaxUSD`, `budgetMaxTokens`, `models`; `--json` for the full `ReplayReport`. |
| `why-stale` | Given `runId` (+ optional `phaseId` as the assumed-changed seed): with no seed, prints the observed dependency graph; with a seed, computes the **transitive stale frontier** — exactly which phases would need re-running and why (observed ∪ declared edges). Zero tokens. |
| `recompute` | Re-run **only the stale frontier** of a stored run from a seed `phaseId`. **Defaults to `dryRun: true`** (reports what would re-run, zero tokens). Pass `dryRun: false` to actually re-execute the seed + frontier and persist the updated run. |
| `cache-clear` | Clear the cross-run memoization store. |
@@ -613,6 +677,7 @@ A run moves through: **running →** `completed` (a `final` phase produced outpu
- `/tf list` · `/tf run [args]` · `/tf show ` · `/tf runs` · `/tf resume `
- `/tf verify` · `/tf compile [lr|td]` · `/tf ir `
- `/tf peek [phaseId] [--json] [--item ] [--limit ]`
-- `/tf provenance ` · `/tf why-stale [phaseId]` · `/tf recompute [--apply]` (dry-run by default)
+- `/tf provenance ` · `/tf trace