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//.trace.jsonl` via `RuntimeDeps.trace` (`FileTraceSink`). Decisions include gate/when/cache/budget/tournament/unreplayable. +- **Event kernel (S2 complete, default OFF):** set `RuntimeDeps.eventKernel: true` or `PI_TASKFLOW_EVENT_KERNEL=1`. Core kinds run on `exec/driver` when enabled **unless** the flow uses unsupported advanced features (score gates, `onBlock:retry`, reflexion, retry, expect, cross-run cache, shareContext) — those fall back to the imperative path. **`race` / `expand` stay on the imperative path** (excluded from `EVENT_KERNEL_PHASE_TYPES` until step handlers exist). Budget, join/optional deps, dynamic `flow{def}` hardening, and agent timeouts are enforced on the kernel path. +- **Offline replay (S3, zero tokens):** `replayRun(events, overrides)` in `replay.ts` — **must not** import `runtime` / `exec/driver` / `exec/step` (guarded by `replay-import-lint.test.ts`). Surfaces: pi `action=replay` + `/tf replay`; MCP `taskflow_replay`. Distinct from **resume** / **recompute** (those re-execute live phases). +- **MCP roster (12):** `taskflow_run|list|show|verify|compile|peek|trace|replay|why_stale|recompute|save|search`. ### Control Flow Fields - `when` — conditional guard (expression must be truthy) @@ -142,7 +167,7 @@ tsconfig.base.json ← shared compiler options; per-package tsconfig.buil ## Development Commands ```bash -pnpm install # links the seven workspace packages +pnpm install # links the nine workspace packages (+ website) pnpm run typecheck # tsc --noEmit across all packages (resolves taskflow-core to src via the dev condition) pnpm test # full unit suite (node --experimental-strip-types --test) pnpm run test:hosts # taskflow-hosts tests only @@ -150,7 +175,8 @@ pnpm run test:pi # pi-adapter tests only pnpm run test:codex # codex-adapter tests only pnpm run test:claude # claude-adapter tests only pnpm run test:opencode # opencode-adapter tests only -pnpm run build # emit dist/*.js + .d.ts for all seven packages +pnpm run test:grok # grok-adapter tests only +pnpm run build # emit dist/*.js + .d.ts for all nine packages pnpm run test:e2e-codex # codex executor e2e (needs live codex + model access) pnpm run test:e2e-codex-mcp # codex MCP stdio e2e (src) pnpm run test:e2e-codex-mcp-full # codex MCP comprehensive e2e against the built dist (runs build first) @@ -158,6 +184,7 @@ pnpm run test:e2e-claude # claude executor e2e (needs live claude + mod pnpm run test:e2e-claude-mcp # claude MCP stdio e2e (src; no live claude needed) pnpm run test:e2e-opencode # opencode executor e2e (needs live opencode; uses a free model by default) pnpm run test:e2e-opencode-mcp # opencode MCP stdio e2e (src; no live opencode needed) +pnpm run test:e2e-grok-mcp # grok MCP stdio e2e (src; no live grok needed) # pi e2e suites are run directly (they use .mts so the unit glob skips them): # node --conditions=development --experimental-strip-types packages/pi-taskflow/test/e2e.mts ``` @@ -210,7 +237,8 @@ pnpm run test:e2e-opencode-mcp # opencode MCP stdio e2e (src; no live opencod - **New test files**: name them `.test.ts` in the owning package's `test/` dir — each `test:*` script globs `packages//test/*.test.ts`, so they're picked up automatically (no manual list to update). E2E scripts use the `.mts` extension specifically so the glob excludes them (they need a live `pi`/`codex`). ### File Structure Rules -- **Source**: `.ts` source lives in `packages//src/`. Host-neutral logic goes in `taskflow-core`; host **runner** code (the `SubagentRunner` impl, argv builder, event-stream parser for codex/claude/opencode) goes in `taskflow-hosts`; host **delivery** code (the MCP server/bin + plugin scaffold) goes in the `codex-taskflow` / `claude-taskflow` / `opencode-taskflow` packages; the pi adapter (which peer-depends the pi SDK) stays in `pi-taskflow`. `taskflow-core` must never import a host SDK (`@earendil-works/*`). +- **Source**: `.ts` source lives in `packages//src/`. Host-neutral logic goes in `taskflow-core`; host **runner** code (the `SubagentRunner` impl, argv builder, event-stream parser for codex/claude/opencode/grok) goes in `taskflow-hosts`; host **delivery** code (the MCP server/bin + plugin scaffold) goes in the `codex-taskflow` / `claude-taskflow` / `opencode-taskflow` / `grok-taskflow` packages; the pi adapter (which peer-depends the pi SDK) stays in `pi-taskflow`. `taskflow-core` must never import a host SDK (`@earendil-works/*`). +- **No monolith growth**: prefer cohesive folders (`runtime/phases/.ts`, `build/erase/*`) over lengthening `runtime.ts` / fat erase pipelines. See `docs/internal/modularization-0.2.0.md`. - **Imports**: adapters import the engine via the bare specifier `taskflow-core` (never a relative path into `../taskflow-core/src`). The MCP server lives in the separate `taskflow-mcp-core` package — host adapters import it via `taskflow-mcp-core/server` / `taskflow-mcp-core/jsonrpc`. `detached-runner.ts` is spawn-only — reference it by `taskflow-core/detached-runner.js`, never via the barrel. `runSubagentProcess` (in `runner-core.ts`, re-exported from the `taskflow-core` barrel) is the shared spawn+classify helper every host runner delegates to. - **Tests**: `.test.ts` in the owning package's `test/`. Named `.test.ts` or `.test.ts`. - **Agents**: built-in agent `.md` files in `packages/taskflow-core/src/agents/` (copied to `dist/agents` at build). @@ -221,9 +249,11 @@ pnpm run test:e2e-opencode-mcp # opencode MCP stdio e2e (src; no live opencod ### Adding a New Phase Type 1. Add the type string to `PHASE_TYPES` in `schema.ts`. 2. Add per-type validation in `validateTaskflow()`. -3. Add the execution branch in `executePhase()` in `runtime.ts`. -4. Add tests in `packages/taskflow-core/test/runtime-branches.test.ts` (or a new file). -5. Update the skill sources in `skills-src/taskflow/` (never the generated files) and run `node scripts/build-skills.mjs`. +3. **Implement the kind in `runtime/phases/.ts`** (pure-ish helpers); wire with a one-liner from `executePhaseInner` in `runtime.ts`. Do **not** paste a large block into the monolith. +4. If event-kernel support is needed: handler in `exec/step-kinds.ts` (or keep excluded from `EVENT_KERNEL_PHASE_TYPES` until then). +5. Add tests in `packages/taskflow-core/test/` (prefer a focused `.test.ts`). +6. DSL: emitter under `taskflow-dsl/src/build/erase/` (not a growing dump in one file). +7. Update skill sources in `skills-src/taskflow/` and run `node scripts/build-skills.mjs`. ### Adding a New Condition Operator 1. Add the token to `OPS` in `interpolate.ts`. @@ -261,14 +291,15 @@ All engine files live in `packages/taskflow-core/src/`; the pi entry lives in `p | File | Responsibility | |------|----------------| -| `runtime.ts` | Core orchestration: `executeTaskflow()`, `executePhase()`, all 10 phase types | +| `runtime.ts` | Core orchestration: `executeTaskflow()`, `executePhase()`, all 12 phase types (peel kinds into `runtime/phases/`) | | `schema.ts` | DSL types, validation, desugar, topo sort, cycle detection | | `runner-core.ts` | Host-neutral runner helpers: failure classification, NDJSON accumulator, error sanitization, `mapWithConcurrencyLimit`, AND `runSubagentProcess` (the shared spawn/idle/abort/classify block every host runner delegates to) + `unknownAgentResult` | -| `taskflow-mcp-core/src/mcp/server.ts` | Host-neutral MCP server: the `taskflow_*` tool schemas + handlers, parameterized by a `SubagentRunner` (codex/claude/opencode adapters bind their runner + a thin bin) | +| `taskflow-mcp-core/src/mcp/server.ts` | Host-neutral MCP server: the `taskflow_*` tool schemas + handlers, parameterized by a `SubagentRunner` (codex/claude/opencode/grok adapters bind their runner + a thin bin) | | `pi-taskflow/src/runner.ts` | Pi subagent spawn (`pi --mode json`), idle watchdog; re-exports the core helpers | | `taskflow-hosts/src/codex-runner.ts` | Codex subagent spawn (`codex exec --json`); `codexSubagentRunner` + `buildCodexArgs` | | `taskflow-hosts/src/claude-runner.ts` | Claude Code subagent spawn (`claude -p --output-format stream-json`); `claudeSubagentRunner` + `buildClaudeArgs` + permission mapping | | `taskflow-hosts/src/opencode-runner.ts` | OpenCode subagent spawn (`opencode run --format json`); `opencodeSubagentRunner` + `buildOpencodeArgs` + model resolution + permission mapping | +| `taskflow-hosts/src/grok-runner.ts` | Grok Build subagent spawn (`grok -p --output-format streaming-json`); `grokSubagentRunner` + `buildGrokArgs` + permission mapping | | `store.ts` | Persistence, file locks, index, cleanup, atomic writes | | `interpolate.ts` | Template resolution, condition parser, safeParse, coerceArray | | `cache.ts` | Fingerprint resolution (git/glob/file/env), CacheStore | diff --git a/CHANGELOG.md b/CHANGELOG.md index 8612b90..c4f57fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,135 @@ All notable changes to taskflow are documented here. This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. +## [0.2.0] — Unreleased + +### Added +- **S4 TypeScript DSL (`taskflow-dsl`):** compile-time `.tf.ts` runes erase to Taskflow JSON via TypeScript AST (`typescript` package, not ts-morph); CLI `new` / `check` / `build` / `decompile`; modular `erase/kinds/*` registry; parity tests (map + `json` + templates). Hosts still run JSON only (no MCP auto-build of `.tf.ts`). +- **Horizon B engine kinds (`race`, `expand`):** `PHASE_TYPES` is **12**. Imperative runtime + FlowIR 1:1; `race` = **first-success** wins (failed settles do not win); cooperative loser usage is aggregated; non-cooperative losers and **parent abort** are bounded by a cancellation grace (gate is woken on parent abort so the race cannot hang forever); `expand` = nested or graft-promote (`def`, `expandMode`, `maxNodes`) with template rewrite on graft. DSL runes + skills + website phase docs + examples. **`cancelLosers` (default true)** aborts losers via best-effort `AbortSignal` after the first **success**. Event kernel still covers the original **10** kinds (excludes `race`/`expand`); advanced features continue to force imperative fallback; **nested** `flow{def}/use` re-checks kernel admission (fail-closed if child has race/expand or unsupported features). +- **S5 plan:** `docs/internal/s5-kernel-default-on-plan.md` — parity harness first, then feature gaps, then default ON + flagship recompute metric. +- **Claim-vs-impl alignment:** `docs/internal/claim-vs-impl-0.2.0.md` ledger; S4 RFCs toolchain corrected; architecture S4 ✅ / S5 next; skills + README phase/test/package counts; website homepage **12** phases / **5** hosts; reference page **TypeScript DSL** (en/zh). +- **Docs complete for Phase 2 surfaces:** website FlowIR docs (`ir:<64-hex>`, `usedFallbackHash: false`); new concepts **Deterministic Replay** (en/zh) + resume disambiguation; host MCP guides + Grok website + `reference/commands` list all 12 tools including `taskflow_replay`; README en/zh Commands tables; skills `advanced.md` trace/replay vs recompute; `AGENTS.md` exec/trace/replay + kernel flag; architecture RFC status table S0–S5 + internal FlowIR RFC superseded note. +- **Grok Build host.** New `grok-taskflow` delivery package + `taskflow-hosts` `grokSubagentRunner` (`grok -p --output-format streaming-json`). Plugin scaffold (`.grok-plugin/plugin.json` + `.mcp.json` + skills), repo marketplace index (`.grok-plugin/marketplace.json`), docs (`docs/grok-mcp.md`, website en/zh guides). Install: `grok plugin install … --trust` or local bin for dogfood. +- **0.2.0 Phase 2 / S0–S3 foundations (event-sourced kernel path):** + - `flowir/compile.ts` (`compileTaskflowToFlowIR`) — genuine Taskflow→canonical FlowIR compiler; `compileTaskflowToIR` now content-addresses with `hashFlowIR` (`ir:<64-hex>`) and sets `usedFallbackHash: false`. + - `exec/fold.ts` — pure `foldEvents(log) →` per-phase snapshot (S1 differential building block). + - `replay.ts` — implements `replayRun(log, overrides)` (threshold / budget / model / args knobs; zero tokens; no import of runtime/driver). + - **S1 decision coverage:** runtime emits `gate-score` / `gate-verdict`, `when-guard`, `cache-hit`, `tournament-winner`, `budget-hit`, plus synthetic phase lifecycle for budget/dep skips. Fold differential tests pin fold ↔ RunState agreement. + - **S2 event kernel (strangler, default OFF, kernel-eligible kinds = PHASE_TYPES minus `race`/`expand`):** `exec/step.ts` + `step-kinds.ts` + `exec/driver.ts` when `RuntimeDeps.eventKernel` or `PI_TASKFLOW_EVENT_KERNEL=1`. Imperative remains default. Hardening: budget enforcement + `budget-hit` events; deps/`join`/`optional` parity; gate eval fail-safe; `steps.*.json` population; agent timeout; script stdout cap; `flow{def}` dynamic validation + nesting cap + recursion stack; feature fall-back for score/retry/expect/reflexion/onBlock/cross-run cache/shareContext. + - **S1 hard gate + import lint:** fold(log) rebuilds phase statuses matching RunState after a captured run (kill-9 oracle); `replay-import-lint` keeps `replay.ts` off the runtime/driver/step import graph. + - **North-star slogan:** `compiled · resumable · incremental · replayable-for-what-if` (drops Qwik "not replayable" collision with deterministic replay). + - **S3 replay surface:** `taskflow_replay` MCP tool; pi `action=replay` and `/tf replay [--threshold phase=n] [--budget-usd n]`; golden trace fixture under `test/fixtures/`. + +### Fixed +- **Budget wording now matches the runtime contract.** User-facing docs, skills, + and FlowIR comments describe `budget` as an observed-usage stop-loss: after a + threshold is observed, no new call starts, while calls already in flight may + overshoot. Ordinary DAG layers and map/parallel/tournament fan-out use serial + admission, limiting new-call overshoot to one call; `race` necessarily admits + its competing branches together, so its already-active branches may all + contribute overshoot. Host limits are explicit: Codex rejects `maxUSD` but + accepts `maxTokens`; Grok rejects every budget because it reports no usage. +- **DSL erase closes remaining dynamic-field gaps.** `map` callback aliases must + match explicit `as`, tournament task/branch templates contribute DAG edges, + and templated `approval.request` / `script.input` preserve placeholders and + dependencies. Invalid or malformed `tsconfig.json` now fails `check` instead + of being silently ignored; decompile topology preserves implicit final output. +- **TypeScript DSL decompile now orders dependencies before consumers.** Valid + Taskflow JSON may list phases in any order; generated rune bindings now use a + stable topological order, so forward-reference gates/reducers rebuild instead + of producing unusable source with a successful CLI exit. +- **MCP cancellation is now real and end-to-end.** The dependency-free stdio + transport dispatches requests concurrently, handles + `notifications/cancelled`, and propagates a per-request `AbortSignal` through + `taskflow_run` into the runtime and active host subprocess. A cancelled tool + call returns JSON-RPC `-32800` instead of leaving hidden background work. + Input disconnect aborts active requests and notifications; duplicate request + ids abort the original controller instead of overwriting it. Completed host + subprocesses remove their abort listeners, avoiding long-DAG listener leaks. + Transport shutdown is grace-bounded and suppresses late writes; explicit + cancellation also races non-cooperative handlers and observes any late + rejection, so neither a hung promise nor an unhandled rejection can wedge the + MCP process. Asynchronous stdio `error` events (including output `EPIPE`) use + the same bounded teardown, abort active work, suppress late responses, and + remove their transport listeners after settling. +- **Grok thinking overrides now work.** Phase → agent → global thinking is + mapped to `grok --reasoning-effort` (`off` → `none`). +- **Grok budgets no longer fail open.** Grok 0.2.93 streaming JSON contains no + token/cost usage, so the Grok MCP adapter explicitly rejects flows declaring + `budget` rather than silently reporting zero and ignoring the ceiling. The + runtime capability check applies at every execution boundary, including + inline object/string flows, saved flows, and nested/graft `expand` fragments. +- **Script phases without `input` now close stdin immediately.** Commands that + read until EOF (for example `cat`) no longer wait for the phase timeout when + no input payload was configured. +- **Codex thinking overrides now reach the host CLI.** Phase → agent → global + thinking is mapped to Codex `model_reasoning_effort`, including the supported + aliases, instead of silently inheriting an unrelated user-level setting. +- **Thinking configuration is validated instead of silently ignored.** Invalid + values are rejected at the Taskflow boundary so a typo cannot fall back to a + host's global reasoning configuration. + +### Security +- **Host children no longer inherit ambient authority.** Codex subagents are + ephemeral, ignore parent user config/rules, and clear unrelated MCP servers; + OpenCode read-only phases use a default-deny policy covering custom/MCP tools; + Codex, OpenCode, and Grok receive filtered provider/runtime environments. + Operators can explicitly pass named task variables with + `PI_TASKFLOW_CHILD_ENV_ALLOW`. +- **Published surfaces are verified as consumers receive them.** Packed + manifests strip source-only `development` exports, declaration imports are + typechecked, internal dependencies are exact, and the OpenCode tarball now + includes its config/skill/assets scaffold. Publish reruns verify all nine + registry artifacts after mutation, not only versions that existed before it. +- **MCP trace responses are bounded.** Human and JSON trace views cap event + counts and oversized strings; JSON reports total/returned/truncated instead + of flooding the host context with an unbounded transcript. +- **DSL output writes are contained, no-clobber, and atomic.** `build`, + `decompile`, and `new` reject symlink escapes, preserve existing files unless + `--force` is explicit, and commit fsynced same-directory temporary files + atomically. `--emit both` preflights both destinations before writing either. +- **Grok read-only phases are kernel-enforced and defence-in-depth.** They now + require `PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE` to name a custom profile + extending `read-only`, plus a known-good file-read allowlist, independent + mutator/MCP deny rules, and disabled subagents. `web_search` / `web_fetch` are + omitted from the Grok 0.2.93 allowlist because that CLI version can label + them unmappable and restore the full toolset. A live executor E2E verifies a + write attempt is blocked even when the workspace is under `/tmp`. +- **Grok mutating/default phases require a fail-closed custom sandbox.** Built-in + profiles can warn and continue unsandboxed when kernel enforcement is + unavailable, which is unsafe with `--always-approve`. Mutating phases now + require `PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE` to name a configured + custom profile; built-in names are rejected. `max_turns_reached` also fails + the phase instead of accepting a partial answer. +- **OpenCode subprocess permissions fail closed.** Every OpenCode child now + uses `--pure` so external plugins cannot bypass the tool permission policy. + Mutating/default-capable phases are rejected unless the operator explicitly + sets `PI_TASKFLOW_OPENCODE_UNSAFE_AUTO=1`; only then is `--auto` added. +- **Release reruns no longer blindly trust an existing npm version.** Before + skipping, the publish workflow verifies a trusted npm owner, SLSA provenance + from this repository/workflow/tag/commit, and exact locally-packed tarball + integrity. A preclaimed `name@version` now fails the release. Every + third-party action across CI, Pages, and publish/release workflows is pinned + to the official major tag's full commit SHA; npm publish has only + `contents: read` and `id-token: write`, while GitHub Release creation is + isolated in a dependent job with only `contents: write`. +- **Claude permission handling no longer defaults to an unsandboxed permission + bypass.** Host policy now uses explicit least-privilege execution modes and + fails closed when the requested tool capability cannot be represented + safely. Claude Code >=2.1.169 `--safe-mode` disables non-managed + customizations, `--tools` restricts built-ins, only that same set is + pre-approved, and disk settings/non-managed hooks are disabled (managed + policy hooks may still run). Explicit lists remain narrow even after the + unsafe opt-in; unknown tool names always fail closed. + The Claude child also receives a filtered environment that retains + platform/proxy/CA and supported provider settings while dropping unrelated + application secrets. +- **Published artifacts are consumer-tested before npm is mutated.** CI and + the tag workflow pack all nine packages with pnpm, install the exact local + tarballs into a clean npm project, reject leaked `workspace:*` ranges, import + every explicit public entry point, and exercise all five shipped bins. The + same gate therefore protects first publication as well as release reruns. + ## [0.1.8] — 2026-07-09 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb19d95..243f554 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,17 +35,20 @@ I review issues and PRs ~weekly. If you need a faster turnaround, mention why in ## Architecture -See [`AGENTS.md`](./AGENTS.md) for the full layout and conventions. `taskflow` is a pnpm-workspace monorepo of seven published packages: +See [`AGENTS.md`](./AGENTS.md) for the full layout and conventions. `taskflow` is a pnpm-workspace monorepo of **nine** published packages: | Package / directory | What | |---------------------|------| | `packages/taskflow-core/` | Host-neutral engine: runtime, schema, agents, store, cache, verify, compile, context-store (zero host-SDK deps) | | `packages/taskflow-core/src/agents/` | 18 built-in agent definitions (`.md` with YAML frontmatter) | | `packages/taskflow-mcp-core/` | Host-neutral MCP server: stdio JSON-RPC + `taskflow_*` tools + DAG SVG/outline renderer (depends on taskflow-core) | +| `packages/taskflow-hosts/` | Shared host runners (codex/claude/opencode/grok) + argv builders + event-stream parsers | +| `packages/taskflow-dsl/` | TypeScript DSL CLI — erase `.tf.ts` → Taskflow JSON / FlowIR | | `packages/pi-taskflow/` | Pi extension adapter (`taskflow` tool + `/tf` commands, TUI) + `skills/` | -| `packages/codex-taskflow/` | Codex subagent runner + MCP bin + Codex plugin | -| `packages/claude-taskflow/` | Claude Code subagent runner + MCP bin + Claude Code plugin | -| `packages/opencode-taskflow/` | OpenCode subagent runner + MCP bin + opencode.json scaffold | +| `packages/codex-taskflow/` | Codex delivery package + MCP bin + Codex plugin | +| `packages/claude-taskflow/` | Claude Code delivery package + MCP bin + Claude Code plugin | +| `packages/opencode-taskflow/` | OpenCode delivery package + MCP bin + opencode.json scaffold | +| `packages/grok-taskflow/` | Grok Build delivery package + MCP bin + Grok plugin | | `examples/` | Runnable flow definitions (`.json`) | | `docs/` | Design docs, RFCs, dogfooding reports | diff --git a/DECISIONS.md b/DECISIONS.md index de820d0..b9a66a7 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -2,7 +2,7 @@ > Status: **living document**. Records the structural decisions behind the > multi-host layout (taskflow-core / taskflow-mcp-core / taskflow-hosts / pi-taskflow / -> codex-taskflow / claude-taskflow / opencode-taskflow), the trade-offs that +> codex-taskflow / claude-taskflow / opencode-taskflow / grok-taskflow), the trade-offs that > were considered, and the direction to take as the host count grows. > > Written as the output of the PR #26 (claude + opencode hosts) architecture @@ -14,8 +14,7 @@ ## The invariant that must never break **The engine (`taskflow-core/src/runtime.ts`) is host-agnostic.** It speaks -only to the `SubagentRunner` contract (`runTask → RunResult`). Adding 4 hosts -(codex, claude, opencode, + pi) changed **zero** lines of engine code. Any +only to the `SubagentRunner` contract (`runTask → RunResult`). Adding hosts (codex, claude, opencode, grok, + pi) changed **zero** lines of engine code. Any future restructuring MUST preserve this seam — it is the single reason a host SDK breaking change cannot force an engine release. @@ -30,19 +29,20 @@ bug waiting to happen (the original 3-way copy-paste already diverged on ## Decision A — taskflow-hosts: a shared host-runner package (DONE) ### Status -**Implemented.** The three host runners (codex / claude / opencode) now live -in a single `taskflow-hosts` package. The three legacy delivery packages -(`codex-taskflow` / `claude-taskflow` / `opencode-taskflow`) keep their npm +**Implemented.** Non-pi host runners (codex / claude / opencode / **grok**) live +in a single `taskflow-hosts` package. Delivery packages +(`codex-taskflow` / `claude-taskflow` / `opencode-taskflow` / `grok-taskflow`) keep their npm names, install paths, version pins, and plugin scaffolds, and import their runner from `taskflow-hosts`; each also re-exports the runner so its existing -public surface (`import ... from "codex-taskflow"`) is unchanged. A 4th host -now lands as one `-runner.ts` in `taskflow-hosts`, not a whole new -runner-owning package. +public surface (`import ... from "codex-taskflow"`) is unchanged. A new host +lands as one `-runner.ts` in `taskflow-hosts`, not a whole new +runner-owning package. **Grok Build** shipped this way (`grok-runner.ts` + +`grok-taskflow` delivery + `.grok-plugin` scaffold). ### Context -codex-taskflow, claude-taskflow, and opencode-taskflow are each published as a +codex-taskflow, claude-taskflow, opencode-taskflow, and grok-taskflow are each published as a **separate npm package**. This was reasonable at 1 host (codex) and tolerable -at 3. The review's concern: at ~10–15 hosts this becomes the dominant +at 3–4 delivery adapters. The review's concern: at ~10–15 hosts this becomes the dominant maintenance cost. ### The tension @@ -51,17 +51,17 @@ host ecosystem's delivery artifact (`codex plugin add taskflow@taskflow`, `npm i -g codex-taskflow`, an MCP server a user points their client at). But there is *no* reason their release cadence is independent — adapters almost never change except when `taskflow-core`'s contract changes or a host CLI -changes its flags. Today all seven packages are **lockstep versioned** at the +changes its flags. Today all **nine** packages are **lockstep versioned** at the same number, which makes a per-package semver meaningless: a codex flag fix forces a new version of the untouched core engine. ### Cost projection at N hosts -| | 3 hosts (now) | 12 hosts (projected) | +| | 5 hosts (now: pi + 4 MCP) | 12 hosts (projected) | |---|---|---| -| npm publishes per release | 7 | 7 | -| `package.json` to keep version-pinned | 7 | 7 | -| README/CHANGELOG package rows | 7 | 7 | -| npm names consumed (`*-taskflow`) | 3 | 3 (new hosts live inside `taskflow-hosts`) | +| npm publishes per release | 8 | 8 (+ thin delivery only if install brand requires it) | +| `package.json` to keep version-pinned | 8 | ~8 | +| README/CHANGELOG package rows | 8 | ~8 | +| npm names consumed (`*-taskflow` delivery) | 4 | 4+ only when a host needs its own install brand | ### Decision **Keep the three existing packages for backward compatibility** (their names @@ -75,12 +75,13 @@ taskflow-hosts ├─ codex-runner.ts ← lives here directly ├─ claude-runner.ts ← lives here directly ├─ opencode-runner.ts ← lives here directly +├─ grok-runner.ts ← lives here directly (Grok Build) ├─ .ts ← lives here directly ├─ test/ ← all host arg-contract + parser tests -└─ index.ts ← barrel re-exporting the three runners +└─ index.ts ← barrel re-exporting the host runners ``` -- Current publishes: `core → mcp → hosts → pi/codex/claude/opencode` (7 publishes). Future hosts ship inside `taskflow-hosts`, so the publish count stops growing. +- Current publishes: `core → mcp → hosts → pi/codex/claude/opencode/grok` (8 publishes). Future *runners* ship inside `taskflow-hosts`; new delivery packages only when the host needs a branded install (`grok plugin install`, `codex plugin add`, …). - Future hosts ship in `taskflow-hosts` and are discovered via `npx -p taskflow-hosts -mcp` or static import. - The three legacy packages can later become thin re-exports of @@ -89,15 +90,17 @@ taskflow-hosts ### Trigger to act ✅ Done at 3 hosts (the moment adoption is lowest, so the migration is -cheapest). `taskflow-hosts` now exists; future hosts go here. +cheapest). `taskflow-hosts` now exists; **Grok Build was added as the 4th +non-pi runner without a second process-lifecycle copy** — future hosts go here. ### What we explicitly reject - **A unified `HostConfig` interface / generic command-builder.** Each host's argv genuinely differs (codex pastes the prompt; claude uses - `--append-system-prompt`; opencode uses `provider/model` ids where codex/claude - use flat ids; permission models are sandbox vs `--allowedTools` vs `--auto`). + `--append-system-prompt`; opencode uses `provider/model` ids where codex/claude/grok + use flat ids; permission models are sandbox vs `--allowedTools` vs `--auto` vs + Grok's `--tools` + `--always-approve`). Forcing these into one interface produces an abstraction with a per-host - parameter for every flag — more complex than the three ~15-line builders it + parameter for every flag — more complex than the short pure builders it replaces. Instead: each host owns a **pure, exported `buildXxxArgs`** builder (extracted in this PR) that is independently unit-tested. Shared *shape*, not shared *code*. @@ -107,7 +110,7 @@ cheapest). `taskflow-hosts` now exists; future hosts go here. ## Decision B — host CLI contracts are locked by unit tests, not e2e ### Context -The executor e2e suites (`e2e-codex.mts`, `e2e-claude.mts`, `e2e-opencode.mts`) +The executor e2e suites (`e2e-codex.mts`, `e2e-claude.mts`, `e2e-opencode.mts`, plus `e2e-grok-mcp.mts` for MCP) spawn a **live** host CLI and need auth + spend tokens, so they never run in CI. That left each host's argv construction (the `--json` / `--format json` / `--output-format stream-json` flags, the permission→flag mapping, the @@ -141,7 +144,7 @@ CI-checked. A flag rename trips a unit test, not a user. ### Context Cross-host debugging ("why did my flow fail?") previously had only `taskflow_peek` (phase output) and a 64KB-capped raw stderr per child. With 4 -hosts, each child's stderr has a different CLI prefix (`codex exec`, `claude +hosts, each child's stderr has a different CLI prefix (`codex exec`, `grok -p`, `claude -p`, `opencode run`), making it hard to tell which agent/bin produced a given error blob. @@ -171,11 +174,11 @@ error blob. comma host list; the skill body is shared. Do not per-host the skill body. - **MCP server is its own package (`taskflow-mcp-core`)** — a pure presentation layer over core. Pi users never pull MCP code. This boundary is correct. -- **Lockstep versioning is kept for now** (all seven packages share a version). +- **Lockstep versioning is kept for now** (all nine packages share a version). It is crude but it is *less* work than tracking which subset of packages need - a given bump, and at 7 packages the cost is still low. `taskflow-hosts` now - exists; the next revisit is when core genuinely needs to move on its own - cadence independent of host CLI flag churn. + a given bump, and at 9 packages the cost is still low. `taskflow-hosts` and + `taskflow-dsl` exist; the next revisit is when core genuinely needs to move + on its own cadence independent of host CLI flag churn. --- diff --git a/README.md b/README.md index 2de0f88..ba2450f 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,14 @@ npm version npm downloads MIT license - zero runtime dependencies CI status - 1140 tests + 1500+ tests dogfooded - runs on Pi, Codex, Claude Code, and OpenCode + runs on Pi, Codex, Claude Code, OpenCode, and Grok Build

+

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.

+Runs on the Pi coding agent, on OpenAI Codex, on Claude Code, on OpenCode, and on Grok Build.

@@ -42,13 +43,20 @@ claude plugin install claude-taskflow@taskflow # OpenCode — add the MCP server to opencode.json (see the OpenCode guide) opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build (published MCP package) +# First define custom taskflow-workspace/taskflow-readonly profiles extending +# workspace/read-only respectively in ~/.grok/sandbox.toml, then: +export PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE=taskflow-workspace +export PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE=taskflow-readonly +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp ``` --- **A `workflow` flows. A `taskflow` is a *graph*.** Other orchestrators let the model *script* the work — imperative code that flows step by step, with the graph hidden inside control flow. `taskflow` does the opposite: you **declare** the work as a graph of discrete, named **task** nodes connected by `dependsOn` edges — and the runtime *verifies that graph before it spends a single token.* -You already know your agent's built-in subagent shorthand — `task` / `tasks` / `chain`. `taskflow` speaks the *same* shorthand — so your existing delegations instantly become **tracked, resumable, and saveable by name** (on Pi, a saved flow becomes a one-word `/tf:` command; on Codex, Claude Code, and OpenCode you run it by name through `taskflow_run`). When you outgrow the shorthand, the full DSL gives you a real DAG: dynamic fan-out over dozens of items, conditional routing, quality gates, human approvals, retries, loops, tournaments, and a hard spend ceiling. +You already know your agent's built-in subagent shorthand — `task` / `tasks` / `chain`. `taskflow` speaks the *same* shorthand — so your existing delegations instantly become **tracked, resumable, and saveable by name** (on Pi, a saved flow becomes a one-word `/tf:` command; on Codex, Claude Code, OpenCode, and Grok Build you run it by name through `taskflow_run`). When you outgrow the shorthand, the full DSL gives you a real DAG: dynamic fan-out over dozens of items, conditional routing, quality gates, human approvals, retries, loops, tournaments, and an observed-usage budget stop-loss. And the whole time, **only the final phase reaches your conversation.** Every intermediate transcript stays in the runtime, never your context window. @@ -91,7 +99,7 @@ Here's the wall you hit with raw subagents: you describe a multi-step plan in pr | **Conditional routing** | ✗ | **`when` guards + `join: any` OR-joins** | | **Fault tolerance** | ✗ | **per-phase `retry` + auto-retry on transient errors** | | **Human-in-the-loop** | ✗ | **`approval` phases (approve / reject / edit)** | -| **Cost control** | ✗ | **run-wide `budget` (USD / token caps)** | +| **Cost control** | ✗ | **run-wide observed-usage `budget` stop-losses (USD / tokens)** | | **Composition** | ✗ | **`flow` phases run saved *or runtime-generated* sub-flows** | | **Iterative loops** | ✗ | **`loop` phases — repeat until condition, convergence, or cap** | | **Competitive selection** | ✗ | **`tournament` phases — N variants + judge** | @@ -122,9 +130,9 @@ We chose the **verifiable** side on purpose. The expressivity you give up is rea The Pi ecosystem now has **20+ delegation, workflow, and orchestration extensions** — each great at what it's for. Here's an honest map of where `pi-taskflow` sits (verified against each package's latest npm release, June 2026). For the full breakdown — every package, strengths *and* weaknesses — see [`docs/internal/PI-ECOSYSTEM.md`](./docs/internal/PI-ECOSYSTEM.md). For the broader, non-Pi landscape (LangGraph, Temporal, CrewAI, Mastra…) see [`docs/internal/COMPETITORS.md`](./docs/internal/COMPETITORS.md). -| Extension | Model | Custom DSL | DAG | Dynamic fan-out | Cross-session resume | Quality gate | Human approval | Save as command | Zero deps | +| Extension | Model | Custom DSL | DAG | Dynamic fan-out | Cross-session resume | Quality gate | Human approval | Save as command | Zero runtime deps | |---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| **taskflow** | **declarative multi-phase taskflows** | **✓** | **✓** | **✓ `map`** | **✓ phase-hash** | **✓** | **✓** | **✓ `/tf:`** | **✓** | +| **taskflow** | **declarative multi-phase taskflows** | **✓** | **✓** | **✓ `map`** | **✓ phase-hash** | **✓** | **✓** | **✓ `/tf:`** | **✕ (1 + peers)** | | [`@pi-agents/orchid`](https://www.npmjs.com/package/@pi-agents/orchid) | opinionated 9-phase pipeline + Ralph loop | fixed | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ (2) | | [`pi-crew`](https://www.npmjs.com/package/pi-crew) | role teams + git worktrees + async | partial | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✕ (7) | | [`ultimate-pi`](https://www.npmjs.com/package/ultimate-pi) | governed plan→execute→review harness | YAML contracts | ✓ (plan-time) | ✕ | ✓ | ✓ (3-tier) | ✓ | ✓ | ✕ (16) | @@ -139,14 +147,14 @@ The Pi ecosystem now has **20+ delegation, workflow, and orchestration extension **How to choose:** -- **`@pi-agents/orchid`** is the most feature-complete orchestrator in the ecosystem (DAG + worktrees + Ralph loop + agent mailbox) — but its DSL is a *fixed* 9-phase pipeline, it carries runtime deps + jiti, and it's beta. Reach for `taskflow` when you want to **define your own graph** (not adopt an opinionated one) with **zero dependencies** and a one-command install. -- **`pi-crew` / `ultimate-pi`** go heavier — worktree isolation, durable async teams, multi-tier governance. If you want lightweight, declarative, and zero-dependency, that's this project. +- **`@pi-agents/orchid`** is the most feature-complete orchestrator in the ecosystem (DAG + worktrees + Ralph loop + agent mailbox) — but its DSL is a *fixed* 9-phase pipeline, it carries runtime deps + jiti, and it's beta. Reach for `taskflow` when you want to **define your own graph** (not adopt an opinionated one) with **no host-SDK coupling** and a one-command install. +- **`pi-crew` / `ultimate-pi`** go heavier — worktree isolation, durable async teams, multi-tier governance. If you want a lightweight declarative engine with no host-SDK coupling, that's this project. - **`@zhushanwen/pi-workflow`** is the closest in spirit and also zero-dep, but it's the **imperative** side of the split above: you author workflows as **JavaScript scripts** the model writes and runs. `taskflow`'s **declarative JSON DAG** is the verifiable side — statically checkable, visualizable, safe to LLM-generate, and resumable at phase granularity rather than call-cache dedup. - **`@fiale-plus/pi-rogue-orchestration`** has a real **loop-until-done** (goal-driven iteration). `taskflow` now ships its own `loop` phase (v0.0.13+) plus `tournament` for competitive selection — and unlike rogue-orchestration, `taskflow` has a full DAG with gates, compositional sub-flows, and cross-session resume. For raw "keep going until the goal is met" with minimal structure, rogue-orchestration is still lighter; for structured, branching pipelines, `taskflow` covers the same ground and more. - **`pi-subagents` / `@gotgenes/pi-subagents`** are the mature picks for ad-hoc "use reviewer on this diff" delegation and background jobs. `taskflow` is for when those delegations need to become a *repeatable, resumable pipeline*. - **`pi-pipeline` / `pi-agent-flow`** ship *opinionated, fixed* flows. `taskflow` ships an *empty canvas*: you (or the model) declare the graph that fits the job. -> The honest one-liner: **`pi-taskflow` is the only Pi extension that gives you a *declarative, verifiable, resumable* DAG of task nodes — saved as a one-word `/tf:` command, with zero runtime dependencies and context isolation by design** (and the same engine runs on Codex via the `taskflow_*` MCP tools). Where code-mode workflows let the model *script* the work, `taskflow` lets it *declare a graph the runtime can prove correct before running.* Recently shipped from the roadmap: the Shared Context Tree (blackboard + supervision) and worktree isolation (see [`docs/internal/STRATEGY.md`](./docs/internal/STRATEGY.md)). +> The honest one-liner: **`pi-taskflow` gives you a *declarative, verifiable, resumable* DAG of task nodes — saved as a one-word `/tf:` command, with context isolation by design** (and the same engine runs on MCP hosts). The engine avoids host-SDK coupling; `typebox` is a peer dependency, the TypeScript DSL includes the compiler, and delivery packages depend on the internal taskflow packages. ## 30-second start @@ -193,7 +201,7 @@ claude plugin marketplace add heggria/taskflow claude plugin install claude-taskflow@taskflow ``` -The plugin's MCP server runs via `npx` (a version-pinned `claude-taskflow`), so there's nothing else to install globally and the plugin version binds the exact code that runs. Each phase's subagent then runs as an isolated `claude -p` session. Just ask Claude Code to run a multi-phase or fan-out job and it calls the tools. See the [Claude Code guide](./docs/claude-mcp.md). +The plugin's MCP server runs via `npx` (a version-pinned `claude-taskflow`), so there's nothing else to install globally and the plugin version binds the exact code that runs. Each phase's subagent then runs as an isolated `claude -p` session. **Claude Code 2.1.169+ is required** for the safe-mode isolation contract. Just ask Claude Code to run a multi-phase or fan-out job and it calls the tools. See the [Claude Code guide](./docs/claude-mcp.md). ### On OpenCode @@ -219,6 +227,36 @@ opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp The server runs via `npx` (a version-pinned `opencode-taskflow`), and each phase's subagent runs as an isolated `opencode run` session. OpenCode also auto-discovers the bundled routing skill (`**/SKILL.md`). Then just ask OpenCode to run a multi-phase or fan-out job and it calls the tools. See the [OpenCode guide](./docs/opencode-mcp.md). +### On Grok Build + +The published path is the MCP package: + +```toml +# ~/.grok/sandbox.toml +[profiles.taskflow-workspace] +extends = "workspace" + +[profiles.taskflow-readonly] +extends = "read-only" +``` + +```bash +export PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE=taskflow-workspace +export PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE=taskflow-readonly +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +``` + +A plugin scaffold is also available from a monorepo checkout: + +```bash +pnpm --filter grok-taskflow build +grok plugin install ./packages/grok-taskflow/plugin --trust +grok plugin enable taskflow +grok mcp add taskflow -- node "$(pwd)/packages/grok-taskflow/dist/mcp/bin.js" +``` + +A public Grok plugin marketplace/source is not published yet; do not substitute a placeholder source. Each phase's subagent runs as an isolated `grok -p --output-format streaming-json` session. Mutating or omitted-tool phases require the custom profile above because Grok's built-in profiles may fail open when kernel enforcement is unavailable. Grok 0.2.93 does not report usage, so it rejects every flow that declares `budget`. Codex reports tokens but not cost, so it accepts `maxTokens` and rejects `maxUSD`; Pi, Claude Code, and OpenCode can enforce both dimensions as observed-usage stop-losses. See the [Grok Build guide](./docs/grok-mcp.md). + ### The shorthand (same shape as the built-in tool) ```jsonc @@ -307,7 +345,7 @@ The shorthand is your onramp. The DSL is where `taskflow` earns its keep — dyn The intermediate summaries never enter your context. The runtime owns them; you get the report. **Save it once → `/tf:summarize-files dir=src` forever.** -### Route, gate, retry, approve, and cap the spend +### Route, gate, retry, approve, and stop runaway spend ```jsonc { @@ -331,7 +369,7 @@ The intermediate summaries never enter your context. The runtime owns them; you - **`when`** routes to `deep` *or* `quick` from the triage JSON — the other branch is skipped. - **`join: "any"`** lets `approve` fire the moment whichever branch ran completes (an OR-join). -- **`retry`** re-runs a flaky patch with backoff; **`budget`** halts the whole run if it gets too expensive. +- **`retry`** re-runs a flaky patch with backoff; **`budget`** stops admitting new calls after reported usage crosses the threshold. A call already in flight may overshoot it. - **`approval`** pauses for a human (approve / reject / edit) before the final `ship`. No scripting. No JavaScript `eval`. Just data the runtime executes — safe enough to run LLM-generated definitions directly. @@ -404,6 +442,8 @@ See [Tournament phases](#tournament-tournament) for the full reference. | `loop` | **iterate a task until done** — re-run a body until a condition, convergence, or a cap | `task`, `until` | | `tournament` | **N variants compete**, a judge picks the best (or aggregates) | `task` \| `branches` | | `script` | run a **shell command** — no LLM, zero tokens — capturing stdout as the phase output | `run` | +| `race` | **first successful** branch wins (optional `cancelLosers` abort) | `branches` (≥2) | +| `expand` | run a dynamic fragment (`nested` or `graft` promote) | `def` (+ `expandMode?`) | ### Common phase fields @@ -433,6 +473,10 @@ Flow-level keys: `name`, `description`, `args`, `concurrency` (default 8), `agen ### Shared Context Tree (blackboard + supervision) +> **Host scope in 0.2.0:** `ctx_read` / `ctx_write` / `ctx_report` / +> `ctx_spawn` tool injection is available through `pi-taskflow`. The Codex, +> Claude, OpenCode, and Grok runners do not inject these tools yet. + By default subagents are fully isolated — they share nothing and only return a final string. Opt a phase in with `shareContext: true` (or `contextSharing: true` flow-wide) to give its subagent four extra tools backed by a per-run, file-based @@ -638,7 +682,7 @@ Condition grammar (for `when`): `== != < > <= >=`, `&& || !`, parentheses, quote ## Commands -Saved flows become CLI shortcuts. **These `/tf` commands are Pi-only** (they run in the Pi session). On Codex, Claude Code, and OpenCode, use the `taskflow_*` MCP tools instead — `taskflow_list` / `taskflow_show` / `taskflow_run` (by `name`) / `taskflow_verify` / `taskflow_compile` / `taskflow_peek`. +Saved flows become CLI shortcuts. **These `/tf` commands are Pi-only** (they run in the Pi session). On Codex, Claude Code, OpenCode, and Grok Build, use the `taskflow_*` MCP tools instead — full set: `taskflow_run` / `list` / `show` / `verify` / `compile` / `peek` / `trace` / `replay` / `why_stale` / `recompute` (dry-run) / `save` / `search`. | Command | What it does | |---|---| @@ -646,14 +690,19 @@ Saved flows become CLI shortcuts. **These `/tf` commands are Pi-only** (they run | `/tf run [args]` | Run a saved flow (e.g. `/tf run summarize-files dir=src`) | | `/tf show ` | Print a flow's definition | | `/tf compile [lr\|td]` | **Render the flow as a Mermaid diagram + verification overlay** — 0 tokens, no LLM; paste into a README/issue/PR | +| `/tf ir ` | Compile to **FlowIR** + content hash (`ir:<64-hex>`) — 0 tokens | | `/tf runs` | Browse recent run history (interactive TUI — **live auto-refreshes** while any run is active) | | `/tf resume ` | Continue a paused/failed run — cached phases skip automatically | | `/tf peek [phaseId]` | Inspect a phase's intermediate output (the debugging escape hatch) | +| `/tf provenance ` | Show observed read-sets for a completed run | | `/tf trace [--json]` | Show a run's **deterministic-replay event trace** (each subagent call + runtime decisions) | +| `/tf replay [--threshold phase=n] [--budget-usd n] [--json]` | **Offline what-if** re-judge of thresholds/budget from a recorded trace (zero tokens) | +| `/tf why-stale [phaseId]` | Explain the stale frontier (observed ∪ declared deps) | +| `/tf recompute [--apply]` | Dry-run (default) or apply minimal recompute of the stale frontier | | `/tf init` | **Interactively map model roles** to your enabled models (writes `~/.pi/agent/settings.json`) | | `/tf: [args]` | Shortcut — runs the flow in one tap | -Tool actions (used by the model on Pi): `run` (inline `define` or saved `name`), `save`, `resume`, `list`, `agents`, `init`, `verify`, `compile`, `ir`, `provenance`, `trace`, `why-stale`, `recompute`, `cache-clear`, `search`. On Codex, Claude Code, and OpenCode the exposed MCP tools are `taskflow_run` / `taskflow_list` / `taskflow_show` / `taskflow_verify` / `taskflow_compile` / `taskflow_peek` / `taskflow_trace` / `taskflow_why_stale` / `taskflow_recompute` (dry-run only) / `taskflow_save` / `taskflow_search`. +Tool actions (used by the model on Pi): `run` (inline `define` or saved `name`), `save`, `resume`, `list`, `agents`, `init`, `verify`, `compile`, `ir`, `provenance`, `trace`, `replay`, `why-stale`, `recompute`, `cache-clear`, `search`. On Codex, Claude Code, OpenCode, and Grok Build the exposed MCP tools are `taskflow_run` / `taskflow_list` / `taskflow_show` / `taskflow_verify` / `taskflow_compile` / `taskflow_peek` / `taskflow_trace` / `taskflow_replay` / `taskflow_why_stale` / `taskflow_recompute` (dry-run only) / `taskflow_save` / `taskflow_search`. ## Background (detached) execution @@ -867,12 +916,12 @@ Copy one into `.pi/taskflows/.json` (or `~/.pi/agent/taskflows/`) and it r
-**0 runtime dependencies** · **1140 tests** · **10 phase types** · **shared context tree** · **cross-session resume** · **cross-run memoization** · **per-item map caching** · **incremental recompute** · **FlowIR compile seam** · **detached execution** · **`compile` Mermaid renderer** · **~9k LOC runtime** +**Node.js ≥ 22.19.0** · **1500+ tests / 100 test files** · **12 phase types** · **shared context tree** · **cross-session resume** · **cross-run memoization** · **per-item map caching** · **incremental recompute** · **FlowIR compile seam** · **detached execution** · **MCP compile: SVG + text** · **Pi compile: Mermaid**
-- **Zero runtime dependencies.** No `dependencies` field — the runtime is built entirely on Node built-ins (`fs` / `path` / `os` / `child_process` / `crypto`). The file lock is `fs.openSync("wx")`, not a third-party library. -- **1140 tests across 70 test files** covering concurrency, atomic file locking (8-process race regressions), path-traversal hardening, cross-session resume, cross-run cache freshness (flow/thinking/tools key isolation, fingerprint invalidation, TTL/LRU eviction), backward-compatible cache-key migration (4-tier legacy fallback), per-phase structural sub-fingerprint (v3:phasefp — editing one phase invalidates only it and its dependents), per-item map caching (one changed item re-executes, N−1 cache hits), the `incremental` flag (run-wide cross-run default), reuse reporting, the FlowIR compile seam (determinism, declared-plane synthesis), incremental recompute (early-cutoff propagation, partial cascade strictly < full, observed ∪ declared union frontier), gate verdicts, budget caps, retry/backoff, approval flows, loop termination, tournament judging, sub-flow composition, the shared context tree (blackboard reuse, supervision spawn, subflow validation/nesting), workspace isolation (temp/dedicated/worktree lifecycle, fail-open degrade, dynamic-flow rejection), dynamic sub-flow security hardening, detached execution (PID persistence, stale detection, crash→failed, resume after failure), live run-history refresh, callback isolation, the idle watchdog, model-role init config, parseModelFromLabel with parenthesized-model-name regression, multi-fence `safeParse` recovery, host argv-contract locking (codex/claude/opencode `buildXxxArgs`), the `compile` Mermaid renderer (id-collision disambiguation, markdown-injection hardening, and full verify-overlay category coverage), plus the library Phase 1 metadata/search/store layer (phaseSignature, generality, CJK text scoring, staleness detection, sidecar persistence, A1 ghost-flow guard). +- **Accurate dependency boundary.** The MCP protocol implementation has no MCP SDK dependency and uses Node built-ins. `taskflow-core` has no direct `dependencies` but peers on `typebox`; `taskflow-dsl` depends on TypeScript; host delivery packages depend on the internal core/runner/MCP packages. All packages require Node.js ≥ 22.19.0. +- **1500+ tests across 100 test files** covering concurrency, persistence, security, resume/cache, all 12 phase kinds, FlowIR/replay, the TypeScript DSL, and host argv/MCP contracts. - **Hardened by design.** Path-traversal defense (lexical + `realpath` containment check), runId validation, HTML/error sanitization, atomic writes, stale-lock stealing via `rename`, and an idle watchdog that kills wedged subagents (SIGTERM → SIGKILL after 5 minutes of silence). Dynamic sub-flows additionally get breadth caps, `cwd` containment, budget clamping, nesting depth caps, and prototype-pollution defense. - **Dogfooded.** Every new feature has to survive the project's own `self-improve` taskflow before it ships. @@ -897,7 +946,10 @@ Our `self-improve` flow is a 10-phase DAG — it audits the codebase, patches de ## Status & limits -**v0.1.7** (current release) — **file loaders now report *why* a file failed with the parse position** (line/column) instead of a merged "not found or unparseable" message — `defineFile`, saved flows, run records, and library sidecars all distinguish *missing* from *malformed*, so a stray bare newline in a hand-authored flow is diagnosable in seconds; `safeParse` stays lenient for LLM output. Also fixes a pi-taskflow hint that re-printed every session. **Gate safety hardening (issue #54)**: a shared emphasis-tolerant marker factory now covers **all three decision markers** — `VERDICT`, `WINNER`, and `SCORE` — so Markdown-wrapped tokens (`VERDICT: **BLOCK**`, `WINNER: __3__`, `SCORE: `0.8``) are never silently mis-read (a genuine BLOCK no longer becomes PASS; a judge's pick no longer silently reverts to variant 1); **unparseable gate *model output now fails closed* (BLOCK)** instead of rubber-stamping PASS — a gate that cannot reach a verdict cannot be trusted to pass, while *config* slips (unresolved `score.target`, malformed `scorers`) stay fail-open with a warning; and free-text gates whose task omits a `VERDICT:` instruction now get the exact format suffix **auto-appended**. For the most robust decision phases, use `output: "json"` + `expect` to machine-validate the output (now the documented default for gate verdicts, tournament winners, and router branches). **v0.1.6** added **library Phase 1** (search-before-author + reusable-flow sidecar metadata), the **`defineFile`** parameter (verify/compile/run a flow from a path on disk), and **JSONC comment support** in flow definition files (`//` and `/* */` comments + trailing commas, parsed by the new zero-dependency `parseJsonc`). **v0.1.5** added **Claude Code and OpenCode as hosts**, **extracted the MCP server into its own `taskflow-mcp-core` package**, and **de-duplicated the three host runners** into a shared `runSubagentProcess`. See [CHANGELOG](./CHANGELOG.md) for the full history. Baseline: **multi-host monorepo of seven packages** — the host-neutral `taskflow-core` engine, the host-neutral `taskflow-mcp-core` MCP server, the shared host-runner `taskflow-hosts`, plus `pi-taskflow` (Pi adapter), `codex-taskflow`, `claude-taskflow`, and `opencode-taskflow` (the three delivery packages re-export their runners from `taskflow-hosts` and each ships an MCP bin + plugin/config), all sharing the host-neutral MCP server in `taskflow-mcp-core`. **Library Phase 1**: save flows with `purpose`+`tags` via `taskflow_save` (MCP) or `action=save` (Pi), search them with structural + CJK-aware keyword scoring via `taskflow_search`/`action=search`, and track `reuseCount` via `reusedFromSearch`. **`defineFile`**: pass a `defineFile` path (or `{defineFile, name}`) to `action=run` (Pi) or `taskflow_run`/`taskflow_verify`/`taskflow_compile` (MCP) instead of an inline `define`, and the engine reads the flow from disk — pair it with JSONC comments to annotate saved flows. **JSONC**: flow-definition `.json` files may now carry `//` and `/* */` comments and trailing commas (parsed by `parseJsonc`, re-exported from the `taskflow-core` barrel); LLM-output parsing via `safeParse` stays strict. **Shared Context Tree**: opt-in (`shareContext` / `contextSharing`) blackboard + supervision tools (`ctx_read`/`ctx_write` horizontal reuse, `ctx_report`/`ctx_spawn` vertical supervision); `ctx_spawn` accepts a flat task **or** a dependency-bearing `subflow` (a runtime-validated nested DAG), depth-capped on a unified nesting counter with budget accounting. **Workspace isolation**: a phase's `cwd` accepts reserved keywords `temp`/`dedicated`/`worktree` — the runtime allocates an isolated dir (or a git worktree on a throwaway branch) and tears it down after the phase, fail-open, rejected in LLM-authored sub-flows. **Detached execution**: runs can execute in the background, detached from the Pi session. Prior: loop-until-done (`loop`), tournament (best-of-N with a judge), cross-run memoization (content-addressed cache with git/file/glob/env fingerprints and TTL), interactive `/tf init`, configurable built-in agents, 18 built-in agents with 6 model roles. Full control-flow & reliability layer (`when` guards, `join: any`, `retry`/backoff, `approval`, `flow` composition, `budget` caps, `onBlock: "retry"`, `eval` machine gates, idle watchdog) on top of the DSL + DAG runtime (`agent`/`parallel`/`map`/`gate`/`reduce`). Inline + saved flows, cross-session resume, live progress, and isolated context. A run executes as one streaming tool call. +**Compatibility baseline from v0.1.8:** interpolation placeholders in phase +`cwd` are rejected; the release dependency/security sweep is also retained. + +**v0.2.0** (this monorepo release line — npm after `v0.2.0` tag) — adds the `taskflow-dsl` TypeScript frontend, Grok Build delivery package, 12 phase kinds with `race`/`expand`, FlowIR content hashes, event-kernel trace/fold, and offline replay. **v0.1.7** — **file loaders now report *why* a file failed with the parse position** (line/column) instead of a merged "not found or unparseable" message — `defineFile`, saved flows, run records, and library sidecars all distinguish *missing* from *malformed*, so a stray bare newline in a hand-authored flow is diagnosable in seconds; `safeParse` stays lenient for LLM output. Also fixes a pi-taskflow hint that re-printed every session. **Gate safety hardening (issue #54)**: a shared emphasis-tolerant marker factory now covers **all three decision markers** — `VERDICT`, `WINNER`, and `SCORE` — so Markdown-wrapped tokens (`VERDICT: **BLOCK**`, `WINNER: __3__`, `SCORE: `0.8``) are never silently mis-read (a genuine BLOCK no longer becomes PASS; a judge's pick no longer silently reverts to variant 1); **unparseable gate *model output now fails closed* (BLOCK)** instead of rubber-stamping PASS — a gate that cannot reach a verdict cannot be trusted to pass, while *config* slips (unresolved `score.target`, malformed `scorers`) stay fail-open with a warning; and free-text gates whose task omits a `VERDICT:` instruction now get the exact format suffix **auto-appended**. For the most robust decision phases, use `output: "json"` + `expect` to machine-validate the output (now the documented default for gate verdicts, tournament winners, and router branches). **v0.1.6** added **library Phase 1** (search-before-author + reusable-flow sidecar metadata), the **`defineFile`** parameter (verify/compile/run a flow from a path on disk), and **JSONC comment support** in flow definition files (`//` and `/* */` comments + trailing commas, parsed by the new zero-dependency `parseJsonc`). **v0.1.5** added **Claude Code and OpenCode as hosts**, **extracted the MCP server into its own `taskflow-mcp-core` package**, and **de-duplicated the three host runners** into a shared `runSubagentProcess`. See [CHANGELOG](./CHANGELOG.md) for the full history. Baseline: **multi-host monorepo of nine packages** — the host-neutral `taskflow-core` engine, the host-neutral `taskflow-mcp-core` MCP server, the shared host-runner `taskflow-hosts`, the `taskflow-dsl` compiler, plus `pi-taskflow` (Pi adapter), `codex-taskflow`, `claude-taskflow`, `opencode-taskflow`, and `grok-taskflow` (the four delivery packages re-export their runners from `taskflow-hosts` and each ships an MCP bin + plugin/config), all sharing the host-neutral MCP server in `taskflow-mcp-core`. **Library Phase 1**: save flows with `purpose`+`tags` via `taskflow_save` (MCP) or `action=save` (Pi), search them with structural + CJK-aware keyword scoring via `taskflow_search`/`action=search`, and track `reuseCount` via `reusedFromSearch`. **`defineFile`**: pass a `defineFile` path (or `{defineFile, name}`) to `action=run` (Pi) or `taskflow_run`/`taskflow_verify`/`taskflow_compile` (MCP) instead of an inline `define`, and the engine reads the flow from disk — pair it with JSONC comments to annotate saved flows. **JSONC**: flow-definition `.json` files may now carry `//` and `/* */` comments and trailing commas (parsed by `parseJsonc`, re-exported from the `taskflow-core` barrel); LLM-output parsing via `safeParse` stays strict. **Shared Context Tree**: opt-in (`shareContext` / `contextSharing`) blackboard + supervision tools (`ctx_read`/`ctx_write` horizontal reuse, `ctx_report`/`ctx_spawn` vertical supervision); `ctx_spawn` accepts a flat task **or** a dependency-bearing `subflow` (a runtime-validated nested DAG), depth-capped on a unified nesting counter with budget accounting. **Workspace isolation**: a phase's `cwd` accepts reserved keywords `temp`/`dedicated`/`worktree` — the runtime allocates an isolated dir (or a git worktree on a throwaway branch) and tears it down after the phase, fail-open, rejected in LLM-authored sub-flows. **Detached execution**: runs can execute in the background, detached from the Pi session. Prior: loop-until-done (`loop`), tournament (best-of-N with a judge), cross-run memoization (content-addressed cache with git/file/glob/env fingerprints and TTL), interactive `/tf init`, configurable built-in agents, 18 built-in agents with 6 model roles. Full control-flow & reliability layer (`when` guards, `join: any`, `retry`/backoff, `approval`, `flow` composition, `budget` caps, `onBlock: "retry"`, `eval` machine gates, idle watchdog) on top of the DSL + DAG runtime (`agent`/`parallel`/`map`/`gate`/`reduce`). Inline + saved flows, cross-session resume, live progress, and isolated context. A run executes as one streaming tool call. Known boundaries (tracked, bounded — no surprises mid-flow): @@ -906,31 +958,34 @@ Known boundaries (tracked, bounded — no surprises mid-flow): - **No `output: "file"`.** Outputs are text/JSON only — write files via an agent's `write` tool call. - **`map` fans out over a JSON array from a string `over`.** The `over` field is a string that either interpolates to a JSON array (e.g. `{steps.ID.json}`) or is a literal JSON-array string. Wrap a plain text list in a single-agent `output: "json"` phase first, or pass `JSON.stringify([...])` for a fixed list. (A raw literal array is rejected — emit it from a phase and reference that.) - **The DAG must be acyclic.** Cycles are rejected at validation. -- **Cross-run cache excludes `gate`, `approval`, `loop`, `tournament`, and `script`.** These must produce a fresh result each run (a `script` phase may also have side effects). +- **Cross-run cache excludes `gate`, `approval`, `loop`, `tournament`, `script`, `race`, and `expand`.** These must produce a fresh result each run. - **Approval auto-rejects in detached mode.** This is a safety invariant — approval gates are never silently bypassed. ## Development -`taskflow` is a pnpm-workspace monorepo of seven published packages: +`taskflow` is a pnpm-workspace monorepo of nine packages (eight host/core + `taskflow-dsl`): | Package | Role | |---------|------| | [`taskflow-core`](./packages/taskflow-core) | Host-neutral orchestration engine (zero host-SDK deps; only `typebox`) — runtime, DSL, cache, verify | | [`taskflow-mcp-core`](./packages/taskflow-mcp-core) | Host-neutral MCP server (stdio JSON-RPC + `taskflow_*` tools + DAG renderer); depends on core | -| [`taskflow-hosts`](./packages/taskflow-hosts) | Shared host-runner collection — the codex/claude/opencode `SubagentRunner` impls + their argv builders + event-stream parsers; depends on core | +| [`taskflow-hosts`](./packages/taskflow-hosts) | Shared host-runner collection — the codex/claude/opencode/grok `SubagentRunner` impls + their argv builders + event-stream parsers; depends on core | +| [`taskflow-dsl`](./packages/taskflow-dsl) | TypeScript DSL CLI/package — erases `.tf.ts` to Taskflow JSON and optional FlowIR; depends on core | | [`pi-taskflow`](./packages/pi-taskflow) | Pi extension adapter — `taskflow` tool + `/tf` commands (what `pi install npm:pi-taskflow` gives you) | | [`codex-taskflow`](./packages/codex-taskflow) | Codex MCP server + bin + [Codex plugin](./packages/codex-taskflow/plugin) (re-exports the runner from `taskflow-hosts`) ([guide](./docs/codex-mcp.md)) | | [`claude-taskflow`](./packages/claude-taskflow) | Claude Code MCP server + bin + [Claude Code plugin](./packages/claude-taskflow/plugin) (re-exports the runner from `taskflow-hosts`) ([guide](./docs/claude-mcp.md)) | | [`opencode-taskflow`](./packages/opencode-taskflow) | OpenCode MCP server + bin + [OpenCode config scaffold](./packages/opencode-taskflow/plugin) (re-exports the runner from `taskflow-hosts`) ([guide](./docs/opencode-mcp.md)) | +| [`grok-taskflow`](./packages/grok-taskflow) | Grok Build MCP server + bin + [Grok plugin](./packages/grok-taskflow/plugin) (re-exports the runner from `taskflow-hosts`) ([guide](./docs/grok-mcp.md)) | ```bash pnpm install pnpm run typecheck # tsc --noEmit across all packages (no build needed) pnpm test # unit tests — no network, no process spawning -pnpm run test:hosts # host-runner tests only (also: test:pi, test:codex, test:claude, test:opencode) -pnpm run build # emit dist/*.js + .d.ts for all seven packages +pnpm run test:hosts # host-runner tests only (also: test:pi, test:codex, test:claude, test:opencode, test:grok) +pnpm run build # emit dist/*.js + .d.ts for all nine packages pnpm run test:e2e-codex # codex executor e2e (needs `codex` + model access) pnpm run test:e2e-codex-mcp # codex MCP server e2e +pnpm run test:e2e-grok-mcp # grok MCP server e2e (no live model required) ``` The pi end-to-end suites spawn live `pi` subagents and are run directly (they use diff --git a/README.zh-CN.md b/README.zh-CN.md index 68fbd2b..83a93b0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -6,13 +6,14 @@ npm version npm downloads MIT license - zero runtime dependencies CI status - 1140 tests + 1500+ tests dogfooded - runs on Pi, Codex, Claude Code, and OpenCode + runs on Pi, Codex, Claude Code, OpenCode, and Grok Build

+

发布线 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 CodexClaude CodeOpenCode

+可运行于 Pi 编码智能体、OpenAI CodexClaude CodeOpenCodeGrok Build

@@ -48,13 +49,20 @@ claude plugin install claude-taskflow@taskflow # OpenCode — 向 opencode.json 添加 MCP server(见 OpenCode 指南) opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build(已发布 MCP 包) +# 先在 ~/.grok/sandbox.toml 分别定义 taskflow-workspace/taskflow-readonly, +# 并分别继承 workspace/read-only,然后: +export PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE=taskflow-workspace +export PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE=taskflow-readonly +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp ``` --- **`workflow` 是在「流动」,而 `taskflow` 是一张「图」。** 其他编排框架让模型去「写脚本」——命令式的代码逐步流动,而那张图藏在控制流里。`taskflow` 恰恰相反:你把工作**声明**为一张由离散、具名的**任务(task)节点**、通过 `dependsOn` 边连接而成的图——而运行时会在花掉一个 token 之前,*先验证这张图。* -你已经熟悉内置子代理(subagent)工具的 `task` / `tasks` / `chain` 了。`taskflow` 使用**完全相同的简写语法**——所以你现有的委托立刻就能变成**可追踪、可恢复、可按名保存**的流程(在 Pi 上,已保存的流程会变成一条 `/tf:` 命令;在 Codex、Claude Code、OpenCode 上,用 `taskflow_run` 按名运行)。当你超越简写语法时,完整的 DSL 为你提供真正的 DAG:针对数十个项目的动态并发分发、条件路由、质量门控、人工审批、重试,以及硬性费用上限。 +你已经熟悉内置子代理(subagent)工具的 `task` / `tasks` / `chain` 了。`taskflow` 使用**完全相同的简写语法**——所以你现有的委托立刻就能变成**可追踪、可恢复、可按名保存**的流程(在 Pi 上,已保存的流程会变成一条 `/tf:` 命令;在 Codex、Claude Code、OpenCode、Grok Build 上,用 `taskflow_run` 按名运行)。当你超越简写语法时,完整的 DSL 为你提供真正的 DAG:针对数十个项目的动态并发分发、条件路由、质量门控、人工审批、重试,以及硬性费用上限。 而且自始至终,**只有最终阶段(final phase)才会进入你的对话。** 每一个中间转录都留在运行时中,永远不会进入你的上下文窗口。 @@ -91,7 +99,7 @@ opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp | **拓扑结构** | 链式 / 平面并行 | **带分层并发 + 路由的 DAG** | | **中间结果** | 在你的上下文窗口中 | **在运行时中——不在你的上下文里** | | **规模** | 少量任务 | **动态 `map` 并发分发,覆盖数十个项目** | -| **可复用** | 每次重新描述 | **按名保存(Pi 上为 `/tf:`;Codex、Claude Code、OpenCode 上用 `taskflow_run` 按名运行)** | +| **可复用** | 每次重新描述 | **按名保存(Pi 上为 `/tf:`;Codex、Claude Code、OpenCode、Grok Build 上用 `taskflow_run` 按名运行)** | | **可恢复** | ✗ | **✓ 跨会话(cross-session)——已缓存的阶段自动跳过** | | **质量门控** | ✗ | **`gate` 阶段,在 `VERDICT: BLOCK` 时停止** | | **条件路由** | ✗ | **`when` 守卫 + `join: any` 或连接(OR-join)** | @@ -124,13 +132,13 @@ opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp ## 与其他 Pi 扩展的对比 -> 本节为 **Pi 专属** ——它将 `pi-taskflow` 与 Pi 生态中的其他包对比。如果你在 Codex、Claude Code 或 OpenCode 上,可直接跳到[阶段类型](#阶段类型);引擎与 DSL 完全相同。 +> 本节为 **Pi 专属** ——它将 `pi-taskflow` 与 Pi 生态中的其他包对比。如果你在 Codex、Claude Code、OpenCode 或 Grok Build 上,可直接跳到[阶段类型](#阶段类型);引擎与 DSL 完全相同。 Pi 生态现在有 **20 多个委托、工作流和编排扩展**——每个在各自领域都很出色。以下是一份诚实的定位图(已对照每个包截至 2026 年 6 月的最新 npm 发布版核实)。完整的对比——每个包的优缺点——请参见 [`PI-ECOSYSTEM.md`](./docs/internal/PI-ECOSYSTEM.md)。更广泛的非 Pi 生态对比(LangGraph、Temporal、CrewAI、Mastra……)请参见 [`COMPETITORS.md`](./docs/internal/COMPETITORS.md)。 -| 扩展 | 模型 | 自定义 DSL | DAG | 动态并发分发 | 跨会话恢复 | 质量门控 | 人工审批 | 保存为命令 | 零依赖 | +| 扩展 | 模型 | 自定义 DSL | DAG | 动态并发分发 | 跨会话恢复 | 质量门控 | 人工审批 | 保存为命令 | 零运行时依赖 | |---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| **taskflow** | **声明式多阶段 taskflow** | **✓** | **✓** | **✓ `map`** | **✓ phase-hash** | **✓** | **✓** | **✓ `/tf:`** | **✓** | +| **taskflow** | **声明式多阶段 taskflow** | **✓** | **✓** | **✓ `map`** | **✓ phase-hash** | **✓** | **✓** | **✓ `/tf:`** | **✕(1 + peer 依赖)** | | [`@pi-agents/orchid`](https://www.npmjs.com/package/@pi-agents/orchid) | 固定 9 阶段流水线 + Ralph 循环 | 固定 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ (2) | | [`pi-crew`](https://www.npmjs.com/package/pi-crew) | 角色团队 + git worktree + 异步 | 部分 | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✕ (7) | | [`ultimate-pi`](https://www.npmjs.com/package/ultimate-pi) | 受管制的 plan→execute→review 框架 | YAML 合约 | ✓(规划时) | ✕ | ✓ | ✓(3 级) | ✓ | ✓ | ✕ (16) | @@ -145,14 +153,14 @@ Pi 生态现在有 **20 多个委托、工作流和编排扩展**——每个在 **如何选择:** -- **`@pi-agents/orchid`** 是生态中功能最完整的编排器(DAG + worktree + Ralph 循环 + 代理邮箱)——但其 DSL 是*固定*的 9 阶段流水线,携带运行时依赖 + jiti,且处于 beta 阶段。当你想**定义自己的图结构**(而非采用别人的固定观点),并且追求**零依赖**和一条命令安装时,选 `taskflow`。 -- **`pi-crew` / `ultimate-pi`** 更重——worktree 隔离、持久的异步团队、多层治理。如果你想要轻量、声明式、零依赖,那就选本项目。 +- **`@pi-agents/orchid`** 是生态中功能最完整的编排器(DAG + worktree + Ralph 循环 + 代理邮箱)——但其 DSL 是*固定*的 9 阶段流水线,携带运行时依赖 + jiti,且处于 beta 阶段。当你想**定义自己的图结构**并避免宿主 SDK 耦合时,选 `taskflow`。 +- **`pi-crew` / `ultimate-pi`** 更重——worktree 隔离、持久的异步团队、多层治理。如果你想要轻量、声明式且不绑定宿主 SDK,那就选本项目。 - **`@zhushanwen/pi-workflow`** 精神上最为接近,也是零依赖,但它站在上述分水岭的**命令式**那一边:你要以模型书写并运行的 **JavaScript 脚本**来编写工作流。`taskflow` 的**声明式 JSON DAG** 是可验证的那一边——可静态检查、可可视化、可安全交给 LLM 生成,且恢复粒度精细到阶段级别而非调用缓存去重。 - **`@fiale-plus/pi-rogue-orchestration`** 拥有真正的**循环至完成**(目标驱动的迭代)。`taskflow` 现在也自带 `loop` 阶段(v0.0.13+)加上竞争选择的 `tournament`——而且与 rogue-orchestration 不同,`taskflow` 拥有带门控、可组合子流程和跨会话恢复的完整 DAG。若只需极少结构的“一直做直到目标达成”,rogue-orchestration 更轻;若需结构化、分支式的流水线,`taskflow` 覆盖同样的能力且更多。 - **`pi-subagents` / `@gotgenes/pi-subagents`** 是即席"用 reviewer 审查这个 diff"委托和后台作业的成熟选择。`taskflow` 则适用于当这些委托需要变成*可重复、可恢复的流水线*时。 - **`pi-pipeline` / `pi-agent-flow`** 提供的是*固定观点、固定结构*的流程。`taskflow` 提供的是*一张空白画布*:你(或模型)声明适合任务的图结构。 -> 诚实的一句话总结:**`pi-taskflow` 是唯一一个给你一张*声明式、可验证、可恢复*的任务节点 DAG 的 Pi 扩展——保存为一条 `/tf:` 命令,零运行时依赖,且从设计上就上下文隔离(同一引擎也通过 `taskflow_*` MCP 工具运行于 Codex)。** code-mode 的 workflow 让模型去*写脚本*跳动工作,`taskflow` 则让它*声明一张运行时能在执行前证明其正确的图。* +> 诚实的一句话总结:**`pi-taskflow` 提供一张*声明式、可验证、可恢复*的任务节点 DAG——保存为 `/tf:` 命令,并从设计上隔离上下文。** 引擎避免绑定宿主 SDK;`typebox` 是 peer dependency,TypeScript DSL 自带编译器,交付包依赖 taskflow 内部包。 ## 30 秒快速开始 @@ -199,7 +207,7 @@ claude plugin marketplace add heggria/taskflow claude plugin install claude-taskflow@taskflow ``` -插件通过 `npx`(`claude-taskflow`)声明其 MCP server;每个阶段的子代理以隔离的 `claude -p` 会话运行。参见 [Claude Code 指南](./docs/claude-mcp.md)。 +插件通过 `npx`(`claude-taskflow`)声明其 MCP server;每个阶段的子代理以隔离的 `claude -p` 会话运行。安全模式隔离契约要求 **Claude Code 2.1.169+**。参见 [Claude Code 指南](./docs/claude-mcp.md)。 ### 在 OpenCode 上 @@ -211,6 +219,36 @@ opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp 服务器通过 `npx`(`opencode-taskflow`)拉起,每个阶段的子代理以隔离的 `opencode run` 会话运行;OpenCode 还会自动发现随包的路由 skill(`**/SKILL.md`)。参见 [OpenCode 指南](./docs/opencode-mcp.md)。 +### 在 Grok Build 上 + +正式发布路径是 MCP 包: + +```toml +# ~/.grok/sandbox.toml +[profiles.taskflow-workspace] +extends = "workspace" + +[profiles.taskflow-readonly] +extends = "read-only" +``` + +```bash +export PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE=taskflow-workspace +export PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE=taskflow-readonly +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +``` + +monorepo checkout 另带 plugin scaffold: + +```bash +pnpm --filter grok-taskflow build +grok plugin install ./packages/grok-taskflow/plugin --trust +grok plugin enable taskflow +grok mcp add taskflow -- node "$(pwd)/packages/grok-taskflow/dist/mcp/bin.js" +``` + +公共 Grok plugin marketplace/source 尚未发布;不要代入占位 source。每个阶段的子代理以隔离的 `grok -p --output-format streaming-json` 会话运行。可写或未声明 tools 的阶段必须使用上述自定义 profile,因为 Grok 内置 profile 在内核强制失败时可能 fail open。Grok 0.2.93 不上报 usage,因此带 `budget` 的流程会 fail closed;需要硬性费用上限时请使用 Pi、Codex、Claude Code 或 OpenCode。参见 [Grok Build 指南](./docs/grok-mcp.md)。 + ### 简写语法(与内置工具相同的格式) ```jsonc @@ -332,6 +370,8 @@ opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp | `loop` | **迭代一个任务直到完成**——重复运行主体直到条件满足、收敛或达到上限 | `task`、`until` | | `tournament` | **N 个变体竞争**,评判者选择最佳(或聚合) | `task` \| `branches` | | `script` | 运行一条 **shell 命令**——无 LLM、零代币——捕获 stdout 作为输出 | `run` | +| `race` | **最先成功**的分支胜出(可选 `cancelLosers` 中止失败者) | `branches`(≥2) | +| `expand` | 运行动态片段(`nested` 隔离或 `graft` 提升) | `def`(+ `expandMode?`) | ### 通用阶段字段 @@ -513,7 +553,7 @@ Review the audit below. If any endpoint is missing auth, end with ## 命令 -保存的流程变成 CLI 快捷方式。**这些 `/tf` 命令仅限 Pi**(在 Pi 会话中运行)。在 Codex、Claude Code、OpenCode 上改用 `taskflow_*` MCP 工具——`taskflow_list` / `taskflow_show` / `taskflow_run`(按 `name`)/ `taskflow_verify` / `taskflow_compile` / `taskflow_peek`。 +保存的流程变成 CLI 快捷方式。**这些 `/tf` 命令仅限 Pi**(在 Pi 会话中运行)。在 Codex、Claude Code、OpenCode、Grok Build 上改用 `taskflow_*` MCP 工具——`taskflow_run` / `list` / `show` / `verify` / `compile` / `peek` / `trace` / `replay` / `why_stale` / `recompute`(仅 dry-run)/ `save` / `search`。 | 命令 | 功能 | |---|---| @@ -521,12 +561,19 @@ Review the audit below. If any endpoint is missing auth, end with | `/tf run [args]` | 运行已保存的流程(例如 `/tf run summarize-files dir=src`) | | `/tf show ` | 打印流程的定义 | | `/tf compile [lr\|td]` | **将流程渲染为 Mermaid 图 + 验证报告** —— 0 token、无 LLM;可粘贴到 README/issue/PR | +| `/tf ir ` | 编译为 **FlowIR** + 内容哈希(`ir:<64-hex>`)—— 0 token | | `/tf runs` | 浏览近期运行历史(交互式 TUI——有运行活跃时**实时自动刷新**) | | `/tf resume ` | 继续一个暂停/失败的运行——已缓存的阶段自动跳过 | +| `/tf peek [phaseId]` | 查看某阶段的中间输出(调试逃生舱) | +| `/tf provenance ` | 显示已完成运行的观测读集 | +| `/tf trace [--json]` | 显示运行的**确定性重放事件轨迹**(各 subagent 调用 + 运行时决策) | +| `/tf replay [--threshold phase=n] [--budget-usd n] [--json]` | **离线 what-if**:按新阈值/预算重判已录制轨迹(零 token) | +| `/tf why-stale [phaseId]` | 解释陈旧前沿(观测 ∪ 声明依赖) | +| `/tf recompute [--apply]` | 默认 dry-run;`--apply` 时对陈旧前沿做最小重算 | | `/tf init` | **交互式映射模型角色**到你的已启用模型(写入 `~/.pi/agent/settings.json`) | | `/tf: [args]` | 快捷方式——一键运行流程 | -工具动作(由模型在 Pi 上使用):`run`(内联 `define` 或已保存的 `name`)、`save`、`resume`、`list`、`agents`、`init`、`verify`、`compile`、`ir`、`provenance`、`why-stale`、`recompute`、`cache-clear`。在 Codex、Claude Code、OpenCode 上暴露的 MCP 工具为 `taskflow_run` / `taskflow_list` / `taskflow_show` / `taskflow_verify` / `taskflow_compile` / `taskflow_peek`。 +工具动作(由模型在 Pi 上使用):`run`(内联 `define` 或已保存的 `name`)、`save`、`resume`、`list`、`agents`、`init`、`verify`、`compile`、`ir`、`provenance`、`trace`、`replay`、`why-stale`、`recompute`、`cache-clear`、`search`。在 Codex、Claude Code、OpenCode、Grok Build 上暴露的 MCP 工具为 `taskflow_run` / `taskflow_list` / `taskflow_show` / `taskflow_verify` / `taskflow_compile` / `taskflow_peek` / `taskflow_trace` / `taskflow_replay` / `taskflow_why_stale` / `taskflow_recompute`(仅 dry-run)/ `taskflow_save` / `taskflow_search`。 ## 后台(detached)执行 @@ -740,12 +787,12 @@ provided files. Report violations grouped by file. No fixes.
-**0 个运行时依赖** · **1140 个测试** · **10 种阶段类型** · **共享上下文树** · **跨会话恢复** · **跨运行记忆化** · **逐项 map 缓存** · **增量重算** · **后台(detached)执行** · **`compile` Mermaid 渲染** · **~9k LOC 运行时** +**Node.js ≥ 22.19.0** · **1500+ 测试 / 100 个测试文件** · **12 种阶段类型** · **共享上下文树** · **跨会话恢复** · **跨运行记忆化** · **逐项 map 缓存** · **增量重算** · **FlowIR 编译缝** · **后台执行** · **MCP 编译:SVG + 文本** · **Pi 编译:Mermaid**
-- **零运行时依赖。** 没有 `dependencies` 字段——运行时完全基于 Node 内置模块(`fs` / `path` / `os` / `child_process` / `crypto`)。文件锁是 `fs.openSync("wx")`,不是第三方库。 -- **1140 个测试分布在 70 个测试文件中**,涵盖并发、原子文件锁定(8 进程竞争回归测试)、路径穿越防御、跨会话恢复、跨运行缓存新鲜度(流程/推理/工具键隔离、指纹失效、TTL/LRU 淘汰)、逐项 map 缓存、增量重算、FlowIR 编译接缝、门控判决、预算上限、重试/回退、审批流程、循环终止、锦标赛评判、子流程组合、共享上下文树、工作区隔离、后台执行、回调隔离、空闲看门狗、模型角色 init 配置,以及 `compile` Mermaid 渲染器。 +- **准确的依赖边界。** MCP 协议实现不依赖 MCP SDK,使用 Node 内置模块;`taskflow-core` 没有直接 `dependencies`,但 peer 依赖 `typebox`;`taskflow-dsl` 依赖 TypeScript;宿主交付包依赖内部 core/runner/MCP 包。所有包要求 Node.js ≥ 22.19.0。 +- **1500+ 个测试分布在 100 个测试文件中**,覆盖并发、持久化、安全、恢复/缓存、全部 12 种阶段、FlowIR/replay、TypeScript DSL 与各宿主 argv/MCP 契约。 - **经过强化的设计。** 路径穿越防御(词法 + `realpath`)、runId 验证、HTML/错误净化、原子写入、通过 `rename` 实现的过期锁窃取,以及杀死卡死子代理的空闲看门狗。 - **自产自用(dogfooded)。** 每个新功能必须在发布前通过项目自身的 `self-improve` taskflow 的考验。 @@ -770,7 +817,7 @@ provided files. Report violations grouped by file. No fixes. ## 状态与边界 -**v0.1.7**——当前发布版。完整历史详见 [CHANGELOG](./CHANGELOG.md)。本版修复:**文件 loader 现在会报告文件失败的原因 + 解析位置**(行/列),而非合并成一句"not found or unparseable"——`defineFile`、saved flow、run 记录、library sidecar 都区分*缺失*与*损坏*,手写流程里一个裸换行几秒就能定位;`safeParse` 对 LLM 输出仍保持宽松。同时修复了 pi-taskflow 升级提示每会话重复打印的问题。**v0.1.6** 新增 **库 Phase 1**(先搜后写 + 可复用流程资产)、**`defineFile` 参数**(从磁盘路径 verify/compile/run 流程)、以及流程定义文件的 **JSONC 注释支持**(`//` 与 `/* */` 注释 + 尾逗号,由零依赖的 `parseJsonc` 解析)。**v0.1.5** 新增了 **Claude Code 与 OpenCode 两个宿主**、**将 MCP 服务器拆为独立的 `taskflow-mcp-core` 包**,并**将三个宿主运行器去重**为共享的 `runSubagentProcess`。基线:**七个包的多宿主 monorepo**——宿主无关的 `taskflow-core` 引擎、宿主无关的 `taskflow-mcp-core` MCP 服务器、共享宿主运行器的 `taskflow-hosts`,加上 `pi-taskflow`(Pi 适配器)、`codex-taskflow`、`claude-taskflow`、`opencode-taskflow`(后三者为交付包,通过 `taskflow-hosts` 复用 runner + MCP bin + 插件/配置),共享 `taskflow-mcp-core` 中的宿主无关 MCP 服务器。**共享上下文树**:可选开启(`shareContext` / `contextSharing`)的黑板 + 监督工具(`ctx_read`/`ctx_write` 水平复用、`ctx_report`/`ctx_spawn` 垂直监督)。**工作区隔离**:阶段的 `cwd` 接受保留关键字 `temp`/`dedicated`/`worktree`,运行时分配隔离目录(或一条一次性分支上的 git worktree)并在阶段结束后拆除。**后台(detached)执行**:运行可脱离会话后台执行。早期功能:循环至完成(`loop`)、锦标赛(best-of-N 带评判者)、跨运行记忆化(基于 git/文件/glob/环境指纹和 TTL 的内容寻址缓存)、交互式 `/tf init`、18 个内置代理及模型角色。完整的控制流与可靠性层(`when` 守卫、`join: any`、`retry`/回退、`approval`、`flow` 组合、`budget` 上限、`eval` 机器门控、空闲看门狗)构建在 DSL + DAG 运行时(`agent`/`parallel`/`map`/`gate`/`reduce`)之上。支持内联 + 已保存流程、跨会话恢复、实时进度和上下文隔离。一次运行作为一个流式工具调用执行。 +**v0.2.0**(本 monorepo 发布线——`v0.2.0` tag 发布后 npm 才更新)——新增 `taskflow-dsl` TypeScript 前端、Grok Build 交付包、含 `race`/`expand` 的 **12** 种阶段、FlowIR 内容哈希、事件内核 trace/fold、离线 replay。**v0.1.7** 修复:文件 loader 报告失败原因 + 解析位置;pi-taskflow 升级提示一次性;gate fail-closed(issue #54)。**v0.1.6** 新增库 Phase 1、`defineFile`、JSONC。**v0.1.5** 新增 Claude Code / OpenCode 宿主、`taskflow-mcp-core` 拆分。基线:**九个包的多宿主 monorepo**——`taskflow-core`、`taskflow-mcp-core`、`taskflow-hosts`、`taskflow-dsl`,加上 `pi-taskflow`、`codex-taskflow`、`claude-taskflow`、`opencode-taskflow`、`grok-taskflow`。**共享上下文树**:可选开启(`shareContext` / `contextSharing`)的黑板 + 监督工具(`ctx_read`/`ctx_write` 水平复用、`ctx_report`/`ctx_spawn` 垂直监督)。**工作区隔离**:阶段的 `cwd` 接受保留关键字 `temp`/`dedicated`/`worktree`,运行时分配隔离目录(或一条一次性分支上的 git worktree)并在阶段结束后拆除。**后台(detached)执行**:运行可脱离会话后台执行。早期功能:循环至完成(`loop`)、锦标赛(best-of-N 带评判者)、跨运行记忆化(基于 git/文件/glob/环境指纹和 TTL 的内容寻址缓存)、交互式 `/tf init`、18 个内置代理及模型角色。完整的控制流与可靠性层(`when` 守卫、`join: any`、`retry`/回退、`approval`、`flow` 组合、`budget` 上限、`eval` 机器门控、空闲看门狗)构建在 DSL + DAG 运行时(`agent`/`parallel`/`map`/`gate`/`reduce`)之上。支持内联 + 已保存流程、跨会话恢复、实时进度和上下文隔离。一次运行作为一个流式工具调用执行。 已知边界(已追踪、有限定——不会在流程中途出现意外): @@ -779,29 +826,31 @@ provided files. Report violations grouped by file. No fixes. - **无 `output: "file"`。** 输出只能是文本/JSON——通过代理的 `write` 工具调用写入文件。 - **`map` 基于一个字符串 `over` 展开出 JSON 数组。** `over` 字段是一个字符串,它要么插值解析为 JSON 数组(例如 `{steps.ID.json}`),要么本身就是一个 JSON 数组字符串。先用一个单代理 `output: "json"` 阶段包装纯文本列表,或对固定列表传入 `JSON.stringify([...])`。(直接写字面量数组会被拒绝——请从某个阶段产出它并引用之。) - **DAG 必须是无环的。** 循环会在验证时被拒绝。 -- **跨运行缓存不包含 `gate`、`approval`、`loop`、`tournament` 和 `script`。** 这些阶段每次运行必须产生新结果。 +- **跨运行缓存不包含 `gate`、`approval`、`loop`、`tournament`、`script`、`race` 和 `expand`。** 这些阶段每次运行必须产生新结果。 - **审批在后台模式下自动拒绝。** 这是一项安全不变量——审批门控绝不会被静默绕过。 ## 开发 -`taskflow` 是一个 pnpm-workspace monorepo,包含七个发布包: +`taskflow` 是一个 pnpm-workspace monorepo,包含九个发布包(八个 host/core + `taskflow-dsl`): | 包 | 角色 | |----|------| | [`taskflow-core`](./packages/taskflow-core) | 宿主无关的编排引擎(零宿主 SDK 依赖;仅 `typebox`)——运行时、DSL、缓存、验证 | | [`taskflow-mcp-core`](./packages/taskflow-mcp-core) | 宿主无关的 MCP 服务器(stdio JSON-RPC + `taskflow_*` 工具 + DAG 渲染);依赖 core | -| [`taskflow-hosts`](./packages/taskflow-hosts) | 共享宿主 runner 集合(codex / claude / opencode 的 `SubagentRunner` + argv 构建器 + 事件流解析器);依赖 core | +| [`taskflow-hosts`](./packages/taskflow-hosts) | 共享宿主 runner 集合(codex / claude / opencode / grok 的 `SubagentRunner` + argv 构建器 + 事件流解析器);依赖 core | +| [`taskflow-dsl`](./packages/taskflow-dsl) | TypeScript DSL CLI——将 `.tf.ts` 擦除为 Taskflow JSON / FlowIR;依赖 core | | [`pi-taskflow`](./packages/pi-taskflow) | Pi 扩展适配器——`taskflow` 工具 + `/tf` 命令(即 `pi install npm:pi-taskflow` 安装的内容) | | [`codex-taskflow`](./packages/codex-taskflow) | Codex 子代理运行器 + MCP bin,及 [Codex 插件](./packages/codex-taskflow/plugin)([指南](./docs/codex-mcp.md)) | | [`claude-taskflow`](./packages/claude-taskflow) | Claude Code 子代理运行器 + MCP bin,及 [Claude Code 插件](./packages/claude-taskflow/plugin)([指南](./docs/claude-mcp.md)) | | [`opencode-taskflow`](./packages/opencode-taskflow) | OpenCode 子代理运行器 + MCP bin,及 [OpenCode 配置脚手架](./packages/opencode-taskflow/plugin)([指南](./docs/opencode-mcp.md)) | +| [`grok-taskflow`](./packages/grok-taskflow) | Grok Build 子代理运行器 + MCP bin,及 [Grok 插件](./packages/grok-taskflow/plugin)([指南](./docs/grok-mcp.md)) | ```bash pnpm install pnpm run typecheck # 跨所有包做 tsc --noEmit(无需构建) pnpm test # 单元测试——无网络,无进程派生 pnpm run test:hosts # 仅 taskflow-hosts 测试(另有 test:pi、test:codex、test:claude、test:opencode) -pnpm run build # 为七个包生成 dist/*.js + .d.ts +pnpm run build # 为全部九个包生成 dist/*.js + .d.ts pnpm run test:e2e-codex # codex executor 端到端(需 `codex` + 模型访问权限) pnpm run test:e2e-codex-mcp # codex MCP 服务器端到端 pnpm run test:e2e-claude-mcp # claude MCP 服务器端到端(无需实时 claude) diff --git a/RELEASE.md b/RELEASE.md index 81e53ac..3564c67 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,40 +1,38 @@ # Release Guide (monorepo) -taskflow is a monorepo of seven independently published packages: +taskflow is a monorepo of nine independently published packages: | Package | npm name | What it is | |---------|----------|------------| | `packages/taskflow-core` | **`taskflow-core`** | Host-neutral engine (DSL, runtime, cache, verify). Zero host SDK deps. | | `packages/taskflow-mcp-core` | **`taskflow-mcp-core`** | Host-neutral MCP server (stdio JSON-RPC + taskflow_* tools + DAG renderer). Depends on core. | -| `packages/taskflow-hosts` | **`taskflow-hosts`** | Shared host-runner collection: codex/claude/opencode `SubagentRunner` impls + argv builders + event-stream parsers. Depends on core. | +| `packages/taskflow-hosts` | **`taskflow-hosts`** | Shared host-runner collection: codex/claude/opencode/grok `SubagentRunner` impls + argv builders + event-stream parsers. Depends on core. | +| `packages/taskflow-dsl` | **`taskflow-dsl`** | TypeScript DSL CLI/package: erases `.tf.ts` to Taskflow JSON and optional FlowIR. Depends on core. | | `packages/pi-taskflow` | **`pi-taskflow`** | Pi extension adapter. Keeps the original published name (no break for existing users). | | `packages/codex-taskflow` | **`codex-taskflow`** | Codex delivery package: re-exports the runner from `taskflow-hosts` + MCP bin + plugin. | | `packages/claude-taskflow` | **`claude-taskflow`** | Claude Code delivery package: re-exports the runner from `taskflow-hosts` + MCP bin + plugin. | | `packages/opencode-taskflow` | **`opencode-taskflow`** | OpenCode delivery package: re-exports the runner from `taskflow-hosts` + MCP bin + config scaffold. | +| `packages/grok-taskflow` | **`grok-taskflow`** | Grok Build delivery package: re-exports the runner from `taskflow-hosts` + MCP bin + plugin. | -Dependency order: `taskflow-mcp-core`, `taskflow-hosts`, `pi-taskflow`, `codex-taskflow`, `claude-taskflow`, and `opencode-taskflow` all depend on `taskflow-core` (`taskflow-mcp-core` and `taskflow-hosts` directly; the adapters via both `taskflow-hosts` and `taskflow-mcp-core`), so **core publishes first, then taskflow-mcp-core, then taskflow-hosts, then the adapters**. +Dependency order: `taskflow-mcp-core`, `taskflow-hosts`, `taskflow-dsl`, `pi-taskflow`, `codex-taskflow`, `claude-taskflow`, `opencode-taskflow`, and `grok-taskflow` all depend on `taskflow-core` (`taskflow-mcp-core`, `taskflow-hosts`, and `taskflow-dsl` directly; the adapters via both `taskflow-hosts` and `taskflow-mcp-core`), so **core publishes first, then taskflow-mcp-core, taskflow-hosts, taskflow-dsl, then the adapters**. -## One-time setup +## One-time repository setup -All seven names are non-scoped and available on public npm — **no npm org needed**. `pi-taskflow` is already owned by `heggria`; the rest (`taskflow-core`, `taskflow-mcp-core`, `taskflow-hosts`, `codex-taskflow`, `claude-taskflow`, `opencode-taskflow`) are unclaimed (publishing creates them). - -```sh -# 1. Point at PUBLIC npm (the repo's default registry may be a private mirror) -pnpm config get registry # confirm what you're pointed at -# publish commands below pass --registry explicitly, so a global switch is optional - -# 2. Log in as the account that owns / will own these names -pnpm login --registry=https://registry.npmjs.org/ -pnpm whoami --registry=https://registry.npmjs.org/ # expect: heggria (or the owner) -``` +The canonical release path is the tag-triggered GitHub Actions workflow. A +repository administrator must configure `NPM_TOKEN` for an npm account allowed +to publish all nine package names. The workflow itself uses least-privilege +`contents: read` plus `id-token: write` and publishes with npm provenance. Do +not publish a release from a developer workstation: a manual publish cannot +provide the workflow identity and source/tag guarantees enforced on reruns. ## Pre-flight (always) ```sh pnpm install # links the workspaces pnpm run typecheck # 0 errors (resolves taskflow-core to src via the dev condition) -pnpm test # 1140/1140 green -pnpm run build # emit dist/ for all seven packages (tsc → .js + .d.ts) +pnpm test # full unit suite green +pnpm run build # emit dist/ for all nine packages (tsc → .js + .d.ts) +pnpm run test:pack # pack → clean install → public imports/bins for all nine ``` ### Skill coverage check (before every release) @@ -51,7 +49,7 @@ this release's CHANGELOG section, verify: **source** layer: `core.md` (core DSL + actions), `patterns.md` (if it changes best practice), `advanced.md` (context sharing / dynamic flows / isolation / recompute), `configuration.md` (knobs), or the per-host - entry files (`entry.pi.md` / `entry.codex.md`) for host bindings. + entry files (`entry.pi.md` / `entry.codex.md` / `entry.claude.md` / `entry.opencode.md` / `entry.grok.md`) for host bindings. - [ ] Host-only capabilities are wrapped in `` / `` blocks — never teach a host a tool it can't reach. - [ ] `node scripts/build-skills.mjs` ran and the generated files are committed. @@ -59,43 +57,49 @@ this release's CHANGELOG section, verify: any new `action` values. - [ ] Removed/renamed fields are purged from `skills-src/` (grep the old name). -> **Why a build step.** Node refuses to type-strip `.ts` files under -> `node_modules`, so the published packages ship compiled `dist/*.js` + `.d.ts`. -> `prepublishOnly` runs `pnpm run build` automatically, so `pnpm publish` always -> publishes fresh output even if you skip the manual build above. +> **Why build and packed-consumer gates both exist.** Node refuses to +> type-strip `.ts` under `node_modules`, so packages ship `dist/*.js` and +> `.d.ts`. Every package also has `prepublishOnly` and +> `publishConfig.access: public`, but release safety does not rely on lifecycle +> hooks alone: CI and the tag workflow pack the current checkout with pnpm, +> install those exact tarballs into a clean npm consumer, reject leaked +> `workspace:*` ranges, and exercise public exports and bins. -## Publish (order matters) +> **Note on internal dependencies.** Workspace package manifests use +> `workspace:*` locally so `pnpm install --frozen-lockfile` never depends on a +> not-yet-published release. `pnpm publish` converts those workspace ranges in +> the packed tarballs. Always publish `taskflow-core` first and bump all nine in +> lockstep. -```sh -# core FIRST — the adapters depend on it -pnpm publish --filter taskflow-core --registry=https://registry.npmjs.org/ --provenance -pnpm publish --filter taskflow-mcp-core --registry=https://registry.npmjs.org/ --provenance -pnpm publish --filter taskflow-hosts --registry=https://registry.npmjs.org/ --provenance -pnpm publish --filter pi-taskflow --registry=https://registry.npmjs.org/ --provenance -pnpm publish --filter codex-taskflow --registry=https://registry.npmjs.org/ --provenance -pnpm publish --filter claude-taskflow --registry=https://registry.npmjs.org/ --provenance -pnpm publish --filter opencode-taskflow --registry=https://registry.npmjs.org/ --provenance -``` +## Publish from a tag (the only supported release path) -`publishConfig.access: public` is set on each package, so scoped/unscoped both publish publicly. +First merge the release commit to `main` and wait for every required check, +including `packed consumer (9 packages)`, to pass. From the updated `main`, push +the matching annotated tag: -> **Note on `taskflow-core` as a dependency.** `taskflow-mcp-core`, `taskflow-hosts`, and the host adapters -> (`pi-taskflow` / `codex-taskflow` / `claude-taskflow` / `opencode-taskflow`) -> declare `"taskflow-core": "0.1.7"` (an exact version, not `workspace:*`), so the -> published tarballs resolve the real npm package once it exists. Always publish -> `taskflow-core` first and bump all seven in lockstep. (`taskflow-mcp-core` and `taskflow-hosts` are the -> other internal dependencies: the MCP host adapters pin `"taskflow-mcp-core"`; the codex/claude/opencode -> delivery packages pin `"taskflow-hosts"`.) +```sh +git switch main +git pull --ff-only origin main +git tag -a v0.2.0 -m "Release v0.2.0" +git push origin v0.2.0 +``` -## Tag + GitHub Release (automated) +`.github/workflows/publish.yml` then performs the complete release transaction: -Pushing a `v*` tag triggers `.github/workflows/publish.yml`, which verifies all -seven package versions match the tag, publishes them in order, and cuts a GitHub -Release from the matching `CHANGELOG.md` section. +1. proves the tag resolves to the event commit and that commit belongs to + `origin/main`; +2. runs typecheck, unit tests, build, and the packed-consumer gate against the + tag checkout; +3. checks the root, all nine package versions, plugin manifests, and pinned MCP + package versions against the tag; +4. publishes core first, then shared packages and delivery adapters, all with + public access and provenance; +5. creates the GitHub Release from the matching `CHANGELOG.md` section. -```sh -git tag v0.1.7 && git push origin v0.1.7 -``` +The workflow is safely rerunnable. An existing npm version is skipped only +after owner, repository/workflow provenance, tag commit, and byte-for-byte +local tarball integrity all match; an existing GitHub Release is likewise +validated before it is accepted. ## Verify after publish @@ -103,12 +107,20 @@ git tag v0.1.7 && git push origin v0.1.7 pnpm view taskflow-core version --registry=https://registry.npmjs.org/ pnpm view taskflow-mcp-core version --registry=https://registry.npmjs.org/ pnpm view taskflow-hosts version --registry=https://registry.npmjs.org/ +pnpm view taskflow-dsl version --registry=https://registry.npmjs.org/ pnpm view pi-taskflow version --registry=https://registry.npmjs.org/ pnpm view codex-taskflow version --registry=https://registry.npmjs.org/ pnpm view claude-taskflow version --registry=https://registry.npmjs.org/ pnpm view opencode-taskflow version --registry=https://registry.npmjs.org/ +pnpm view grok-taskflow version --registry=https://registry.npmjs.org/ ``` +Also verify the `Publish & Release` workflow completed successfully and that +the non-draft, non-prerelease GitHub Release targets the tagged commit. A +partially published nine-package set is not a completed release; fix the cause +and rerun the same tag workflow rather than creating a replacement tag or +publishing missing packages manually. + ## Install (end users) ```sh @@ -125,4 +137,8 @@ claude plugin install claude-taskflow@taskflow # OpenCode users (MCP server): opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build (published MCP package) +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +# or: grok mcp add taskflow -- npx -y -p grok-taskflow grok-taskflow-mcp ``` diff --git a/docs/0.2.0-north-star.md b/docs/0.2.0-north-star.md new file mode 100644 index 0000000..af3f294 --- /dev/null +++ b/docs/0.2.0-north-star.md @@ -0,0 +1,108 @@ +# taskflow 0.2.0 — 方向定调(North Star) + +> 综合 6 份调研 + 1 次多 agent review 的结论。2026-07-07。 +> 这是 0.2.0 的纲领;后续 RFC v2 / 实现都以此为锚。 +> 源文档:`market-positioning-2026-07` / `0.2.0-research-frontend-paradigms` / +> `rfc-0.2.0-dsl-syntax` / `rfc-0.2.0-three-compile-routes` / +> `rfc-taskflow-vs-claude-code-workflows` + review run `review-020-design`。 + +--- + +## 一句话 + +**taskflow 0.2.0 = 把 agent 工作流做成"可编译、可恢复、增量重算"的响应式框架 —— agent orchestration 的第一个编译器。** + +从 "Make 时代"(运行时解释、重跑全部)走向 "Bazel/Svelte 时代"(编译产物、只重算受影响部分、跨边界恢复)。 + +--- + +## 一、市场定位:占 Conductor 没占的细分 + +**现状(残酷但已验证):** +- "声明式 DAG 编排引擎"是**红海** —— Microsoft Conductor(微软背书)几乎镜像了 taskflow 的每个核心主张(声明式 DAG / 零 token / validate / parallel / map / gate / loop / registry)。正面比参数会被分发碾压。 +- 但 **"编译 + 可恢复 + 增量重算"这个组合,工业零竞品** —— Conductor、Claude Code Workflows、LangGraph、Temporal 都没有:内容寻址缓存 + observed readSet + minimal recompute。 + +**0.2.0 的占位:** +> *"the only agent orchestrator that is **compiled** (not interpreted), **resumable** (cross-session / detached), **incremental** (only re-runs what changed), and **replayable-for-what-if** (re-judge gates/budget offline, zero tokens)."* + +不和 Conductor 比"声明式 DAG",比它没有的四件事:**编译、可恢复、增量、确定性 what-if 重放**。 + +> 叙事修正(对齐 [RFC §7 / §13](./rfc-0.2.0-architecture.md)):早期借 Qwik 的 *"resumable (not replayable)"* 会与 0.1.7+ 的 **deterministic replay**(事件溯源重 fold)撞名。Qwik 的 replayable 指 hydration 重执行;我们的 replay 是**决策旋钮 what-if、零 token**。口号改为 **replayable-for-what-if**,与 resume 并列而非对立。 + +--- + +## 二、技术路线(基于 review 的决策) + +### 决策 1:rune 是**编译指令**,不是真函数(Svelte 路线,运行时擦除) + +review 的 critic 用缺陷 #2 证明了:**"读 `.output` 自动建依赖"+ 真 function + Proxy 这条路在 JS 语义里物理上站不住**(toString/方法链/数值强转全是陷阱)。所以: + +- `agent()`/`map()`/`gate()` 等是**编译器识别的指令**,运行时被擦除/转换。 +- `taskflow build flow.tf.ts` 跑一个 **AST transform**(不只是 tsc)提取 DAG,产出 FlowIR。 +- **诚实放弃"可调试/可 REPL/可降级运行"** —— 换取编译期类型检查 + 自动依赖 + `json()` 推导**全部干净成立**。 +- 理由:0.2.0 的核心卖点是编译期能力,agent 写完本来就要 build,"不能脱离 build"对它不是损失。 + +### 决策 2:FlowIR 是唯一中间表示,JSON ↔ DSL 双向编译 + +- JSON 保持**一等公民**(现有 flow 零迁移;`build flow.json` 和 `build flow.tf.ts` 都产 FlowIR)。 +- decompiler 必须设计(review 指出当前 RFC 把"双向可编译"当 checkbox,没设计)。 +- 这同时白得 Vue Vapor 的"渐进共存"优势,不用维护双执行后端。 + +### 决策 3:overstory 响应式内核是引擎(M1–M5 能力已落地;旗舰数字是 S5 验收门) + +- FlowIR 编译(M1) + declared readSet(M2) + observed readSet@version(M3) + stale frontier(M4) + minimal recompute(M5) **能力已落地**(`/tf recompute`、`taskflow_why_stale` / `taskflow_recompute`)。 +- 目标旗舰叙事:**"周一全量审计 $6/8 agents → 周二改 1 文件 → 只重算 2 个节点 $0.40"** — **能力可用,成本比数字尚未作为 S5 发布门禁封板**(见 architecture S5 ⬜)。 + +--- + +## 三、吸收的 2026 前沿思想(每一个都映射到 taskflow 能力) + +| 前沿思想 | 来源 | taskflow 对应 | 状态 | +|---|---|---|---| +| 编译优先(运行时→编译时) | Svelte/Solid/Vue Vapor | FlowIR 编译 + `taskflow-dsl` AST erase | ✅ S0+S4(分支内;版本号已 bump 至 0.2.0,待发布) | +| 细粒度响应式(signals) | TC39/Solid/Svelte5 | overstory observed readSet + stale + recompute | ✅ 已落地 | +| rune 显式化 | Svelte 5 runes | DSL rune 函数(编译指令,`TFDSL_ERASE_ONLY`) | ✅ S4 | +| **resumable**(跨会话 / detached) | **Qwik**(借其 resume 叙事,非 hydration) | cross-session resume + detached runs + cache | ✅ 已落地 | +| **确定性重放 / replayable-for-what-if** | 事件溯源 + fold | `trace` + `replayRun` / `taskflow_replay` / `/tf replay`(阈值/budget what-if,零 token) | ✅ S3 | +| 增量重算(只重算受影响部分) | Bazel/Nix | content-addressed cache + why-stale + recompute | ✅ 能力;旗舰 $ 比 S5 验收 | +| 写一次编译多端 | Mitosis | Taskflow JSON → pi/codex/claude/opencode/grok;`.tf.ts` 先 `taskflow-dsl build` | ✅(产物是 JSON,非 host 直跑 `.tf.ts`) | + +**关键洞察:这些思想不是"要追的热点",而是 taskflow 已经走在了前面 —— 学术界 2026-07 才出现"the first"agent 静态分析(AgentFlow),taskflow 早有 FlowIR+verify;TC39 signals 算法 = overstory 算法。0.2.0 是把这些"已经领先的能力"用一个 Svelte 风格 DSL 包装成用户可感知的产品。** + +--- + +## 四、诚实的边界(review 的反直觉发现) + +1. **对 agent 作者,JSON 可能仍是默认 surface。** review 的 usability 结论:agent 写 JSON(结构天然合法、1 种调用约定、dependsOn 显式可审计)可能比写 TS-DSL(4 类错误、6+ rune 签名、依赖看不见)更顺。**TS-DSL 主要价值给人类开发者 + 复杂/大型 flow(50+ phase、多文件组合)**。0.2.0 不要假设 agent 会更喜欢 TS。 +2. **不序列化 phase 中途断点。** Qwik 序列化"执行位置"做 O(1) 恢复,taskflow 到 phase 粒度(LLM 调用是原子黑盒,无中途断点)。领域差异,非缺陷。 +3. **放弃"可调试/可降级"。** 决策 1 的代价。缓解:强大的 verify/compile/peek 工具链补偿。 + +--- + +## 五、0.2.0 不做什么(防止 scope 蔓延) + +- 不做 lifecycle hooks(taskflow 的 script + gate 已覆盖大半,优先级低)。 +- 不做"LLM 自动写 DSL"(auto-compose) —— host agent 本来就能写,不是新能力。 +- 不和 Conductor 比 visual builder / web dashboard(资源不允许,且非护城河)。 +- 不在 0.2.0 实现 `$store`/`$derived` 全局响应式(依赖 Shared Context Tree,推迟到 0.2.x)。 + +--- + +## 六、下一步(执行顺序) + +1. **RFC v2**(基于 review 修正当前 RFC): + - 改"身份危机":明确 rune 是编译指令,定义 `taskflow build` 的 AST transform。 + - 补 decompiler 设计(双向编译不再是 checkbox)。 + - 修 demo-RFC gap(demo 用的 $store/$derived/flow.component 标为 post-0.2.0 或移除)。 + - 修 coverage gaps(approval.input / loop multi-phase body / {env.X} / args.type 等被 review 实证的问题)。 +2. **旗舰演示**(让 overstory 可感知):一个增量重跑的 benchmark flow + 可视化"省了多少"。 +3. **实现**:rune 类型定义 + AST transform 编译器 + decompiler(分阶段)。 + +--- + +## 附:叙事武器库(对外一句话) + +- vs Claude Code Workflows:*"Ours is compiled and resumable; theirs dies with the session."* +- vs Microsoft Conductor:*"Ours is incremental — re-run only what changed. Theirs re-runs everything."* +- vs LangGraph/Temporal:*"Ours verifies the DAG before a token. Theirs can't statically analyze imperative code."* +- 总:*"The compiler for agent workflows — verify before spend, resume across sessions, re-run only what changed."* diff --git a/docs/0.2.0-research-frontend-paradigms.md b/docs/0.2.0-research-frontend-paradigms.md new file mode 100644 index 0000000..98abb62 --- /dev/null +++ b/docs/0.2.0-research-frontend-paradigms.md @@ -0,0 +1,192 @@ +# 0.2.0 奠基调研:2026.7 前端框架最前沿思想 & DSL 对比 → taskflow DSL 设计 + +> Research: 2026-07-07. Purpose: 为 taskflow 0.2.0 "蜕变成前端框架级的 agent 编排系统 +> (可能抛弃 JSON,转自定义 DSL)"提供设计地基。Sources: Svelte/Vue/Solid/React 官方 +> + TC39 proposal-signals + Mitosis + 多篇 2026 对比评测。Fetched via `explore-cli`. + +--- + +## 0. 一句话结论 + +**2026 前端的共识是 "Signals + 编译器"。** 而这套算法(State/Computed + glitch-free +拓扑排序 + lazy cache + push-pull)**正是 overstory 已经落地的 stale-frontier + +minimal-recompute**。taskflow 0.2.0 要做的,是把一个 **TC39 已标准化的响应式内核**, +用一套 **Svelte-runes 风格的显式 DSL** 包起来 —— 让 agent 写复杂 workflow 像写 Svelte +组件一样自然,且天然获得响应式增量重算。 + +## 1. 2026.7 前端最前沿思想全景(7 个核心趋势) + +| # | 思想 | 代表 | 一句话 | 对 taskflow 的意义 | +|---|---|---|---|---| +| 1 | **细粒度响应式 (Signals)** | Solid, Vue3, Svelte5, Angular17, Preact | "数据变了,只更新真正依赖它的那一小块" | **= overstory 的 observed readSet + stale + recompute**(已落地 M3-M5) | +| 2 | **Signals 进入语言标准 (TC39)** | TC39 proposal-signals | signals 从框架特性 → JS 语言原生 | **决定性印证**:overstory 算法 = TC39 标准 signals 算法 | +| 3 | **编译优先,淘汰虚拟 DOM** | Svelte 5, SolidJS | 编译时生成精确更新代码,不 diff | **= FlowIR 编译**(已落地 M1)。taskflow 早就是"编译优先" | +| 4 | **显式响应式 (Runes)** | Svelte 5 | 从隐式 `let` 魔法 → 显式 `$state/$derived/$effect` | **最直接的 DSL 设计参考**(见 §3) | +| 5 | **编译器自动 memoize** | React 19 Compiler | 自动插 memo,免手写 useMemo | taskflow 的 cache 已是声明式的,可进一步自动推断 | +| 6 | **写一次,编译多端** | Mitosis (BuilderIO) | 一份 JSX 组件 → React/Vue/Solid/Qwik/... | **= taskflow 的多 host**(pi/codex/claude/opencode/grok),Mitosis 是 UI 版镜像 | +| 7 | **Resumability / 可恢复** | Qwik | 服务端状态可直接在客户端恢复,零 hydration | **= taskflow 的 cross-session resume**(已落地) | + +### 趋势判断(2026 共识) +- **"Signals Won the Framework War Except in React"** —— Solid/Vue/Angular/Svelte/Preact 全部收敛到细粒度响应式;React 用编译器追赶但架构上是 follower。 +- **TC39 正把 signals 标准化到 JS 语言层**(像当年 Promise 一样)—— 这意味着 signals 算法是**经过学术界+工业界双重验证的成熟范式**,不是时髦。 + +--- + +## 2. DSL / 语法设计对比(回答"DSL 的区别") + +### 2a. 四种响应式 DSL 范式 + +**① Svelte 5 Runes —— 显式符号 + 函数语法** +```svelte + + +``` +- 设计哲学:**"make reactive logic explicit"** —— `runes` 是"影响编译器的符号",用函数语法(`$state(0)`)而非劫持语言关键字 +- 关键突破:**响应式超越组件边界** —— `$state/$derived` 能在普通 `.js/.ts` 里用,不再只在 `.svelte` +- 为什么放弃隐式 `let`:**应用变复杂后,隐式响应式难追踪、难跨文件复用** + +**② SolidJS —— JSX + createSignal/createMemo/createEffect** +```jsx +function Counter() { + const [count, setCount] = createSignal(0); + const doubled = createMemo(() => count() * 2); + createEffect(() => console.log(count())); + return ; +} +``` +- 编译时把 JSX 编译成**精确的 DOM 更新**(无 VDOM),`createMemo`/`createEffect` 自动追踪依赖 +- 性能基准略胜 Svelte(细粒度 signal graph) + +**③ TC39 标准 Signals —— State + Computed** +```js +const counter = new Signal.State(0); +const isEven = new Signal.Computed(() => (counter.get() & 1) == 0); +const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd"); +``` +- **两个原语**:`Signal.State`(可写)+ `Signal.Computed`(派生,lazy + cached) +- 算法保证:**glitch-free**(拓扑排序消除重复)+ **cached**(依赖没变不重算)+ **push-pull**(解 Diamond Problem) + +**④ Vue 3 —— ref/reactive/computed + Composition API** +```vue + +``` + +### 2b. 范式光谱(关键区别一张表) + +| 范式 | 响应式触发 | 依赖追踪 | 编译 | 代表 | +|---|---|---|---|---| +| **隐式编译魔法** | 编译器猜(`let` 赋值) | 编译时静态 | 重 | Svelte 3/4 | +| **显式 rune 符号** | 函数调用(`$state`) | 运行时自动 | 中 | **Svelte 5** | +| **显式 signal API** | 函数调用(`createSignal`) | 运行时自动 | 轻 | **Solid / Vue 3** | +| **语言原生** | `new Signal.State` | 运行时自动 | 无 | **TC39 (未来)** | +| **VDOM + 编译器** | diff + 自动 memo | 编译器插桩 | 重 | React 19 | + +> **趋势方向:隐式 → 显式 → 原生。** Svelte 3(隐式)→ Svelte 5(显式 runes)→ TC39(语言原生)。 +> 复杂度上升时,**显式**胜出(可追踪、可跨文件、可类型化)。 + +--- + +## 3. 这些思想 → taskflow 0.2.0 DSL 设计的映射 + +### 3a. 为什么抛弃 JSON 是对的(且方向正确) + +- JSON 是**数据**,不是**程序**。复杂 workflow(条件、循环、派生、复用)用 JSON 表达, + 要么堆嵌套要么靠字符串模板(`"{steps.X.output}"`),**对 agent 不友好、对静态分析不友好**。 +- 前端的教训(Svelte 3→5)正好相反:它们是**从隐式 DSL 往显式 DSL 走**;taskflow 现在是 + **从纯数据(JSON)往有结构的 DSL 走** —— 同一个方向(让逻辑更显式、更好写、更好分析)。 + +### 3b. 最该借鉴的 3 个思想 + +**🥇 借鉴 Svelte 5 Runes:一组显式"rune"符号** +- taskflow DSL 可以定义一组编译器识别的关键字/函数,让编排原语**显式**: + - `agent("...")`, `parallel([...])`, `map(items, i => ...)`, `gate(...)`, `loop(...)`, + `tournament(...)`, `flow("name")` + - 派生/状态:`$read("stepX")`(读上游,自动追踪依赖 → overstory observed readSet), + `$emit(value)`(写本 phase 输出) +- **关键收益**:agent 写起来像写程序(不是写 JSON),且编译器能静态分析 + 运行时自动追踪依赖。 + +**🥈 借鉴 TC39/Solid:State/Computed 双原语 + 自动依赖追踪** +- taskflow 的 phase 天然就是 `Computed`(派生、lazy、cached)。0.2.0 可以让**依赖追踪完全自动**: + - 现在 taskflow 靠 `{steps.X.output}` 字符串声明依赖;新 DSL 里,`read(stepX)` 调用即依赖(运行时 onRead hook 自动建图)—— **正是 overstory M3 的 observed readSet,但语法从"字符串模板"升级为"函数调用"**。 + - 算法直接复用 TC39 验证过的:glitch-free 拓扑排序 + lazy cache + early cutoff(overstory 已有)。 + +**🥉 借鉴 Mitosis:一份 DSL,编译到多 host** +- taskflow 已有 pi/codex/claude/opencode/grok 五 host。新 DSL 可以是**单一源**,编译到各 host 的运行时(就像 Mitosis 编译到 React/Vue/Solid)。 +- FlowIR 已经是这个"中间表示",0.2.0 让它成为真正的**多目标编译枢纽**。 + +### 3c. 一个示意:taskflow 0.2.0 DSL 可能长什么样(受 Svelte runes + Solid 启发) + +```taskflow +// 不再是 JSON。一组显式 rune,响应式自动追踪。 +flow "audit-routes" { + args { dir = "src/routes" } + + // $read 自动追踪依赖 → overstory observed readSet + const files = agent("list .ts files under " + args.dir, + $schema { files: string[] }) + + // map 扇出,每个 item 一个 agent,自动并发 + const audits = map(files, f => + agent("audit " + f + " for missing auth", { label: f })) + + // gate 是 Computed:依赖 audits,自动重算 + const verified = gate(audits, "adversarially verify each finding") + + // tournament: 多角度 + 裁判 + const ranked = tournament(verified, { judge: "rank findings", mode: "best" }) + + return ranked +} +``` +对比现在的 JSON: +```json +{ + "name": "audit-routes", + "phases": [ + { "id": "files", "agent": "scout", "task": "list .ts files under {args.dir}", + "output": "json", "expect": { "type": "object", "properties": { "files": {"type":"array","items":{"type":"string"}} }}}, + { "id": "audits", "type": "map", "over": "{steps.files.json.files}", "as": "f", + "task": "audit {item} for missing auth", "label": "{item}" }, + ... + ] +} +``` +**DSL 版的优势:agent 写起来是"写程序",不是"填表";依赖靠 `read()` 调用隐式建立,不用手写字符串模板。** + +--- + +## 4. 关键风险 & 必须想清楚的设计决策(给 0.2.0 的 open questions) + +1. **DSL 宿主语言选什么?** —— 自创语法(要写解析器/语法高亮/LSP) vs 复用宿主语言 + (TypeScript/Python 函数式 DSL,像 Solid 那样"合法的 JS")。 + - **强烈建议复用宿主语言**(TS 函数式 DSL):零解析器成本、agent 已会写、有类型、IDE 原生支持。 + - 这正是 Solid 的路线("合法的 JS + 编译器"),而非 Svelte 的"自创 .svelte 语法"。 + +2. **静态分析能力不能丢。** —— 现在 JSON DSL 的优势是 FlowIR 能静态 verify。 + 新 DSL 必须保留"编译到 FlowIR + 零 token verify"的能力(否则丢了 taskflow 最硬的护城河)。 + +3. **向后兼容。** —— JSON DSL 已发布、有用户、有 saved flow。0.2.0 应让 **JSON 和新 DSL 双向可编译**(JSON → FlowIR → DSL;DSL → FlowIR → JSON),像 Svelte 3/4 → 5 的渐进迁移。 + +4. **"响应式"在 agent 域的边界。** —— 前端 signal 是同步、廉价;LLM 调用是异步、昂贵。 + overstory 的"cost-asymmetric reactivity"(mark 便宜、recompute 贵)是正确抽象,新 DSL 要 + **让这个不对称可见**(比如 `gate`/`loop` 是 expensive effect,默认不自动重跑)。 + +--- + +## 5. 0.2.0 的北极星叙事 + +> **"taskflow 0.2.0: Svelte for agent workflows. Write flows like components; +> the compiler tracks dependencies; only what changed re-runs."** + +把 2026 前端最前沿(Signals + 编译器 + 显式 DSL)已经验证的范式,**第一个**完整搬到 agent +编排领域 —— 用 TC39 标准的响应式算法(overstory 内核),套一层 Svelte-runes 风格的显式 DSL, +让 agent 写复杂 workflow 像写 Solid 组件。**没有任何竞品在做这件事**(见 +`docs/market-positioning-2026-07.md` + overstory 调研)。 diff --git a/docs/claude-mcp.md b/docs/claude-mcp.md index 365b528..44f951b 100644 --- a/docs/claude-mcp.md +++ b/docs/claude-mcp.md @@ -13,9 +13,9 @@ directions, both built on the host-neutral `SubagentRunner` seam the `claude -p` subagent runner (`packages/claude-taskflow/src/mcp/`). This is the direction described here. -The MCP server is dependency-free: it speaks JSON-RPC 2.0 over stdio on Node -built-ins (`packages/taskflow-mcp-core/src/mcp/jsonrpc.ts`), so taskflow keeps its -**zero runtime dependencies** guarantee — no `@modelcontextprotocol/sdk`. +Requires **Node.js ≥ 22.19.0**. The MCP protocol layer speaks JSON-RPC 2.0 over +stdio without `@modelcontextprotocol/sdk`; published delivery packages still +depend on the internal taskflow packages, and core peers on `typebox`. ## Install (recommended): the Claude Code plugin @@ -35,7 +35,7 @@ Verify: ```sh claude plugin list # → claude-taskflow@taskflow installed, enabled -claude mcp list # → taskflow … (npx -y -p claude-taskflow@0.1.7 claude-taskflow-mcp) +claude mcp list # → taskflow … (npx -y -p claude-taskflow@0.2.0 claude-taskflow-mcp) ``` The bundled skill tells Claude Code *when* to reach for the tools (multi-phase @@ -47,15 +47,30 @@ Claude Code has no OS-level sandbox in headless (`-p`) mode — a tool call is either whitelisted or denied. The runner maps each phase's tool whitelist the same way the codex runner maps to a sandbox mode: -- **Read-only phase** (no `write`/`edit`/`bash` in the phase/agent `tools`) → - `--allowedTools Read,Grep,Glob,WebFetch,WebSearch`. Mutating tools are denied - outright. Note there is **no read-only shell** (unlike codex's read-only OS - sandbox), so `Bash` is not granted to read-only phases. -- **Mutating phase** (or no whitelist at all) → `--permission-mode - bypassPermissions`. This is the workspace-write equivalent, but **without an - OS sandbox backstop** — the subagent can run any tool. Run flows you trust, - in a repo you can `git reset`, ideally in a throwaway worktree - (`cwd: "worktree"`). +Taskflow requires Claude Code **2.1.169 or newer** for the `--safe-mode` +isolation contract. Older CLIs fail closed with an unknown-option error; +upgrade Claude Code before using the adapter. + +- **Read-only or unspecified phase** (no mutating/unknown tool in the resolved + phase/agent `tools`, including an omitted list) → matching `--tools` and + `--allowedTools` lists. An explicit list remains narrow; omitted tools use + `Read,Grep,Glob,WebFetch,WebSearch`. `--safe-mode` disables non-managed + project/user customizations; disk setting sources and non-managed hooks are + disabled as defense in depth. Administrator-managed policy hooks may still + run. Mutating tools are denied outright. Note + there is **no read-only shell** (unlike codex's read-only OS sandbox), so + `Bash` is not granted. +- **Known mutating requested tool** → rejected by default. Headless Claude + has no OS sandbox backstop, so silently selecting `bypassPermissions` is + unsafe. A trusted operator may explicitly set + `PI_TASKFLOW_CLAUDE_UNSAFE_BYPASS=1` to enable that mode. Even then, the + requested built-in set stays narrow. Unknown tool names always fail closed. + Use only trusted flows and prefer a throwaway worktree (`cwd: "worktree"`). + +The spawned Claude process receives a filtered environment: platform/runtime, +proxy/CA, and Claude-supported provider variables (`ANTHROPIC_*`, Bedrock, +Vertex/Google, Azure/Foundry) are retained, while unrelated application secrets +such as npm tokens, database URLs, and other-provider API keys are not inherited. ## Long-running flows and the tool-call timeout @@ -106,6 +121,10 @@ subagent a flow spawns is itself a `claude -p` process — no pi process needed. | `taskflow_verify` | Statically verify a flow (cycles, missing deps, undefined refs) — no execution. | | `taskflow_compile` | Render a flow's DAG as a text outline + a compact status line (with an inline SVG image for clients that render images). | | `taskflow_peek` | Inspect one phase's intermediate output from a stored run (post-hoc debugging). Hard-truncated, read-only. | +| `taskflow_trace` | Read-only timeline of a run's append-only event log (subagent I/O + runtime decisions). | +| `taskflow_replay` | Offline what-if on a recorded trace: re-judge thresholds/budget/models **without calling the model** (zero tokens). | +| `taskflow_why_stale` | Explain observed/declared dependency staleness for a run (optional seed `phaseId`). Zero tokens. | +| `taskflow_recompute` | Report the stale frontier for a seed phase (**dry-run only** over MCP — never spends tokens). | ## How output is rendered @@ -134,7 +153,7 @@ Inside a Claude Code session, just ask — Claude will call the tools: ``` > **Note on approvals.** MCP-driven runs are non-interactive, so an `approval` -> phase **auto-rejects** (fail-open). Prefer a `gate` (agent review) in flows +> phase **auto-rejects** (fail-closed for the approval decision). Prefer a `gate` (agent review) in flows > you run through the `taskflow_*` tools; use `approval` only in flows a human > runs interactively. diff --git a/docs/codex-mcp.md b/docs/codex-mcp.md index 6bea4dd..63941de 100644 --- a/docs/codex-mcp.md +++ b/docs/codex-mcp.md @@ -10,9 +10,9 @@ both built on the host-neutral `SubagentRunner` seam server**, so the `taskflow_*` tools appear inside codex (`packages/codex-taskflow/src/mcp/`). This is the direction described here. -The MCP server is dependency-free: it speaks JSON-RPC 2.0 over stdio on Node -built-ins (`packages/taskflow-mcp-core/src/mcp/jsonrpc.ts`), so taskflow keeps its -**zero runtime dependencies** guarantee — no `@modelcontextprotocol/sdk`. +Requires **Node.js ≥ 22.19.0**. The MCP protocol layer speaks JSON-RPC 2.0 over +stdio without `@modelcontextprotocol/sdk`; published delivery packages still +depend on the internal taskflow packages, and core peers on `typebox`. ## Install (recommended): the Codex plugin @@ -31,12 +31,24 @@ globally, and the plugin version binds the exact code that runs. Verify: ```sh codex plugin list # → taskflow@taskflow installed, enabled -codex mcp list # → taskflow … enabled (npx -y -p codex-taskflow@0.1.7 codex-taskflow-mcp) +codex mcp list # → taskflow … enabled (npx -y -p codex-taskflow@0.2.0 codex-taskflow-mcp) ``` The bundled skill tells Codex *when* to reach for the tools (multi-phase or fan-out work), so you usually don't have to name them explicitly. +## Sandbox, tools, and thinking + +Codex has sandbox profiles, not a strict per-tool-name whitelist. A phase whose +effective `tools` are read-only maps to `codex exec -s read-only`; a mutating +tool or an omitted list maps to `-s workspace-write`. +`danger-full-access` is never selected automatically. + +Thinking resolves phase → agent → global and is passed as +`model_reasoning_effort`: `off`/`none`/`minimal` → `none`, +`low`/`medium`/`high`/`xhigh` pass through, and `max`/`ultra` → `xhigh`. +Unknown values fail closed before the subprocess starts. + ## Long-running flows and the tool-call timeout `taskflow_run` returns only after the **whole DAG finishes** — intermediate @@ -53,7 +65,7 @@ To stop large flows from being cut off, the plugin's `.mcp.json` ships a "mcpServers": { "taskflow": { "command": "npx", - "args": ["-y", "-p", "codex-taskflow@0.1.7", "codex-taskflow-mcp"], + "args": ["-y", "-p", "codex-taskflow@0.2.0", "codex-taskflow-mcp"], "tool_timeout_sec": 1800 } } @@ -71,6 +83,15 @@ tool_timeout_sec = 3600 If a flow is genuinely huge, also consider splitting it into a few smaller `taskflow_run` calls so each returns well inside the window. +Each spawned Codex subagent is isolated from the parent Codex customization: +the runner uses `--ephemeral --ignore-user-config --ignore-rules`, clears +`mcp_servers`, and then applies only Taskflow's sandbox/model/thinking argv. +This prevents an unrelated user MCP OAuth failure, plugin, or exec rule from +changing a phase result. The child environment retains platform/proxy/CA and +Codex/OpenAI provider variables but drops unrelated application secrets. To +pass a task-specific variable intentionally, list its name in the comma-separated +`PI_TASKFLOW_CHILD_ENV_ALLOW` operator setting before starting the MCP server. + ## Alternative: register the MCP server manually If you'd rather not use the plugin, install the package and register its @@ -102,13 +123,18 @@ subagent a flow spawns is itself a `codex exec` process — no pi process needed | Tool | What it does | |------|--------------| -| `taskflow_run` | Run a saved flow (`name`) or an inline `define` (full DAG or shorthand `{task}`/`{tasks}`/`{chain}`). Returns only the final phase output. | +| `taskflow_run` | Run a saved flow (`name`) or an inline `define` (full DAG or shorthand `{task}`/`{tasks}`/`{chain}`). Returns only the final phase output + a `runId`. | | `taskflow_list` | List saved flows discoverable from the cwd, now with library metadata (`purpose`, `generality`, `reuseCount`) when available. | | `taskflow_show` | Show a saved flow as `{definition, library}` — the `library` object holds the sidecar metadata (`purpose`, `tags`, `generality`, `reuseCount`, `phaseSignature`, …). | | `taskflow_save` | Save a flow to the library with optional `purpose`, `tags`, and `notes`. Writes the flow JSON plus a sidecar `.meta.json`. | | `taskflow_search` | Search the library before authoring. Returns ranked reusable flows with score, why, and a reuse hint. Structural + CJK-aware keyword scoring; embedding is Phase 2. | | `taskflow_verify` | Statically verify a flow (cycles, missing deps, undefined refs) — no execution. | | `taskflow_compile` | Render a flow's DAG as a **diagram** (an inline SVG image, shown by the desktop app) **plus a text outline** (shown by the CLI/TUI, which can't render images) + a compact status line. Very large graphs return the text outline alone. | +| `taskflow_peek` | Inspect one phase's intermediate output from a stored run (post-hoc debugging). Hard-truncated, read-only. | +| `taskflow_trace` | Read-only timeline of a run's append-only event log (subagent I/O + runtime decisions). | +| `taskflow_replay` | Offline what-if on a recorded trace: re-judge thresholds/budget/models **without calling the model** (zero tokens). | +| `taskflow_why_stale` | Explain observed/declared dependency staleness for a run (optional seed `phaseId`). Zero tokens. | +| `taskflow_recompute` | Report the stale frontier for a seed phase (**dry-run only** over MCP — never spends tokens). | ## How output is rendered (Codex desktop app) diff --git a/docs/grok-mcp.md b/docs/grok-mcp.md new file mode 100644 index 0000000..d2e7668 --- /dev/null +++ b/docs/grok-mcp.md @@ -0,0 +1,245 @@ +# Using taskflow from Grok Build (MCP) + +taskflow runs on [Grok Build](https://docs.x.ai/build/overview) in two +directions, both built on the host-neutral `SubagentRunner` seam +(`packages/taskflow-core/src/host/runner-types.ts`): + +1. **Grok Build as the executor** — a taskflow's subagents run as + `grok -p --output-format streaming-json` sessions + (`packages/taskflow-hosts/src/grok-runner.ts`). +2. **Grok Build as the caller** — taskflow is exposed to a Grok Build user as + an **MCP server**, so the `taskflow_*` tools appear inside the session. The + MCP protocol, tools, and rendering all live in the host-neutral + `taskflow-mcp-core` package (`packages/taskflow-mcp-core/src/mcp/`); the + grok adapter just binds them to the Grok subagent runner + (`packages/grok-taskflow/src/mcp/`). This is the direction described here. + +Requires **Node.js ≥ 22.19.0**. The MCP protocol layer speaks JSON-RPC 2.0 over +stdio without `@modelcontextprotocol/sdk`; published delivery packages still +depend on the internal taskflow packages, and core peers on `typebox`. + +Official Grok docs used for this integration: + +- Plugins: `~/.grok/docs/user-guide/09-plugins.md` / [docs.x.ai skills & plugins](https://docs.x.ai/build/features/skills-plugins-marketplaces) +- MCP servers: `~/.grok/docs/user-guide/07-mcp-servers.md` +- Headless / streaming-json: `~/.grok/docs/user-guide/14-headless-mode.md` + +## Install (recommended): register the published MCP server + +```sh +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +``` + +A public Grok plugin marketplace/source is not published yet. Do not substitute +an imaginary source string. The plugin scaffold can be installed only from a +checkout: `grok plugin install ./packages/grok-taskflow/plugin --trust`. + +The plugin declares its MCP server via `npx` (a version-pinned +`grok-taskflow`), so the server is fetched and launched on demand when the +package is published. Verify: + +```sh +grok plugin list # → taskflow installed / enabled +grok plugin details taskflow +grok mcp list # → taskflow … +grok inspect # plugins + MCP + skills with source labels +``` + +The bundled skill tells Grok *when* to reach for the tools (multi-phase or +fan-out work), so you usually don't have to name them explicitly. In the TUI, +open `/plugins` or `/mcps` to inspect components. + +### From a monorepo checkout (dogfood / pre-publish) + +Until `grok-taskflow` is published to npm, point MCP at the **built** local bin +(plugin install still gives you the skill + manifest): + +```sh +cd /path/to/taskflow +pnpm install +pnpm --filter taskflow-core build +pnpm --filter taskflow-mcp-core build +pnpm --filter taskflow-hosts build +pnpm --filter grok-taskflow build + +grok plugin install ./packages/grok-taskflow/plugin --trust +grok plugin enable taskflow + +# Local stdio MCP (overrides / complements npx until publish): +grok mcp add taskflow -- \ + node "$(pwd)/packages/grok-taskflow/dist/mcp/bin.js" +``` + +Optional: raise startup timeout for cold `npx` (when using the published form): + +```toml +# ~/.grok/config.toml +[mcp_servers.taskflow] +startup_timeout_sec = 60 +tool_timeout_sec = 1800 +``` + +## Permissions (the codex-sandbox analogue) + +The runner always selects a Grok kernel sandbox profile and maps each phase's +tool whitelist as follows: + +- **Read-only phase** (no `write`/`edit`/`bash` / `run_terminal_cmd` / + `search_replace` in the phase/agent `tools`) → rejected unless + `PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE` names a custom profile extending + `read-only`, plus a + `--tools read_file,grep,list_dir` allowlist, an independent + `--disallowed-tools` mutator denylist, `--deny Bash/Edit/Write/MCPTool`, and + `--no-subagents`. `--always-approve` then applies only to the surviving + read-only tools. Grok 0.2.93 treats `web_search` / `web_fetch` as unmappable + allowlist entries and can restore the full toolset, so those two tools are + deliberately unavailable in a read-only phase until the CLI fixes that + fail-open behavior. +- **Mutating phase** (or no whitelist) → rejected unless + `PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE` names an explicitly configured + **custom** Grok sandbox profile. Grok's built-in profiles can warn and + continue unsandboxed when kernel enforcement is unavailable; custom profiles + fail closed instead. The runner then uses that profile with + `--always-approve`. Built-in names such as `workspace` are rejected. + +Configure both fail-closed profiles in +`~/.grok/sandbox.toml` and export the variable before starting the MCP server: + +```toml +[profiles.taskflow-workspace] +extends = "workspace" + +[profiles.taskflow-readonly] +extends = "read-only" +``` + +```sh +export PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE=taskflow-workspace +export PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE=taskflow-readonly +``` + +Prefer `cwd: "worktree"` for disposable changes. `max_turns_reached` is treated +as a failed phase, never as a successful partial answer. + +Both sandbox modes cover the whole Grok process and spawned tools on +macOS/Linux. The independent built-in/MCP denies are also required for +read-only phases because that profile deliberately permits session writes under +temporary directories; together they keep a read-only phase non-mutating even +when its workspace itself is under `/tmp`. A live executor E2E additionally +proves that the custom workspace-equivalent profile permits an in-cwd write and +rejects a marker outside the cwd and documented exceptions. + +Grok children receive only platform/proxy/CA plus Grok/xAI/Taskflow-Grok +environment variables. Unrelated parent secrets are removed; an operator may +explicitly add task-specific variable names with the comma-separated +`PI_TASKFLOW_CHILD_ENV_ALLOW` setting. + +Agent system prompts are passed with `--rules`. Model ids that look like +unresolved `{{placeholders}}`, multi-segment openrouter paths, or pi thinking +suffixes (`:xhigh`) are dropped so Grok uses its configured default. +Effective Taskflow thinking is passed as `--reasoning-effort` (`off` maps to +`none`). + +### Budget limitation + +Grok 0.2.93 does not include token or cost usage in its `streaming-json` +events. Consequently the Grok-bound MCP server **refuses any flow that declares +`budget`**; accepting it would advertise a ceiling the runtime cannot enforce. +Unbudgeted flows still run normally. Other hosts with usage accounting can +apply an observed-usage stop-loss, but no process-backed host can reserve an +exact hard token/USD ceiling for an in-flight model call. + +## Long-running flows and the tool-call timeout + +`taskflow_run` returns only after the **whole DAG finishes** — intermediate +phase outputs stay in the runtime, so from Grok's side it's a single tool call +that can run for many minutes. The plugin's `.mcp.json` ships +`tool_timeout_sec: 1800` (30 minutes). For huge flows, split into a few smaller +`taskflow_run` calls. MCP does not expose Pi's detached-run mode. If the client +sends `notifications/cancelled` (including after a tool timeout), the server +aborts the active DAG and subagent instead of leaving hidden background work. + +## Alternative: register the MCP server manually + +```sh +pnpm add -g grok-taskflow +grok mcp add taskflow -- grok-taskflow-mcp +``` + +Or with npx (no global install): + +```sh +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +``` + +Verify: + +```sh +grok mcp list +grok mcp doctor taskflow +``` + +The server discovers saved flows and agents from its **launch cwd**, and each +subagent a flow spawns is itself a `grok -p` process — no pi process needed. + +## Tools exposed + +| Tool | What it does | +|------|--------------| +| `taskflow_run` | Run a saved flow (`name`) or an inline `define` (full DAG or shorthand `{task}`/`{tasks}`/`{chain}`). Returns only the final phase output + a `runId`. | +| `taskflow_list` | List saved flows discoverable from the cwd, with library metadata when available. | +| `taskflow_show` | Show a saved flow as `{definition, library}`. | +| `taskflow_save` | Save a flow to the library with optional `purpose`, `tags`, and `notes`. | +| `taskflow_search` | Search the library before authoring. | +| `taskflow_verify` | Statically verify a flow — no execution, zero tokens. | +| `taskflow_compile` | Render a flow's DAG as a text outline (+ inline SVG when the client renders images). | +| `taskflow_peek` | Inspect one phase's intermediate output from a stored run. Hard-truncated, read-only. | +| `taskflow_trace` | Read-only timeline of a run's append-only event log (subagent I/O + runtime decisions). | +| `taskflow_replay` | Offline what-if on a recorded trace: re-judge thresholds/budget/models **without calling the model** (zero tokens). | +| `taskflow_why_stale` | Explain observed/declared dependency staleness for a run (optional seed `phaseId`). Zero tokens. | +| `taskflow_recompute` | Report the stale frontier for a seed phase (**dry-run only** over MCP — never spends tokens). | + +Grok namespaces MCP tools as `taskflow__taskflow_*` in some UIs; discover with +`search_tool` then call via `use_tool`. + +## Use it + +Inside a Grok Build session (TUI or headless), just ask: + +``` +> List my saved taskflows. +> Verify this flow: {name:"x", phases:[{id:"a",type:"agent",agent:"writer",task:"draft"}]} +> Run the "release-train" taskflow. +``` + +Headless smoke (after MCP is registered): + +```sh +grok -p "Call taskflow_verify on this define (do not run it): +{name:'dogfood', phases:[{id:'a', type:'agent', agent:'executor', task:'noop', final:true}]}" \ + --always-approve --output-format json +``` + +> **Note on approvals.** MCP-driven runs are non-interactive, so an `approval` +> phase **auto-rejects** (fail-closed for the approval decision). Prefer a `gate` (agent review) in flows +> you run through the `taskflow_*` tools; use `approval` only in flows a human +> runs interactively. + +## Remove + +```sh +grok plugin uninstall taskflow # if installed as a plugin +grok mcp remove taskflow # if registered manually +``` + +## Proof / tests + +- `pnpm run test:grok` — MCP binding unit tests (in-memory streams). +- `pnpm run test:e2e-grok-mcp` — spawns `bin.ts` over a real subprocess pipe + (no live Grok model needed). +- `packages/taskflow-hosts/test/grok-args.test.ts` — pure argv contract + (`buildGrokArgs`) locked for CI. +- `packages/taskflow-hosts/test/grok-runner.test.ts` — streaming-json parser + pinned to the official headless event shape. +- `grok plugin validate packages/grok-taskflow/plugin` — official manifest + validator (skills + MCP components). diff --git a/docs/i18n/README.ar.md b/docs/i18n/README.ar.md index 2c129fb..10e5de3 100644 --- a/docs/i18n/README.ar.md +++ b/docs/i18n/README.ar.md @@ -2,7 +2,7 @@ > ⚠️ **هذه الترجمة قديمة.** يُرجى مراجعة [README بالإنجليزية](../../README.md) للحصول على أحدث المعلومات. -taskflow هو *رسم بياني للمهام* تصريحي وقابل للتحقق لوكلاء البرمجة — يعمل على [Pi coding agent](https://pi.dev) وعلى [OpenAI Codex](https://github.com/openai/codex) — ليس workflow تكتبه كبرنامج نصي، بل DAG تُعلِنه ويتحقق منه وقت التشغيل قبل إنفاق أي token. بدون أي تبعيات وقت تشغيل، 872 اختبارًا، 9 أنواع من المراحل. +taskflow رسم بياني تصريحي وقابل للتحقق للمهام على Pi وCodex وClaude Code وOpenCode وGrok Build. خط 0.2.0 يتطلب Node.js ≥ 22.19.0 ويضم 12 نوعًا من المراحل وأكثر من 1500 اختبار. طبقة MCP لا تعتمد على MCP SDK؛ يستخدم core ‏`typebox` كـ peer ويعتمد DSL على TypeScript. > **لماذا "taskflow" وليس "workflow"؟** الـ *workflow* (بنمط code-mode) هو برنامج أمري *يتدفق*، ورسمه البياني مخبأ داخل تدفق التحكم. أما الـ *taskflow* فينقل الخطة إلى رسم بياني تصريحي من عقد مهام منفصلة — يمكن التحقق منه ساكنًا وعرضه واستئنافه وحفظه كأمر. نحن نستبدل القدرة التعبيرية بالقابلية للتحقق، عن قصد. @@ -13,6 +13,17 @@ pi install npm:pi-taskflow # Codex codex plugin marketplace add heggria/taskflow codex plugin add taskflow@taskflow + +# Claude Code +claude plugin marketplace add heggria/taskflow +claude plugin install claude-taskflow@taskflow + +# OpenCode +opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build (from monorepo checkout pre-publish, or published source) +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +# or: grok mcp add taskflow -- npx -y -p grok-taskflow grok-taskflow-mcp ``` [GitHub](https://github.com/heggria/taskflow) · [README بالإنجليزية](../../README.md) · [README بالصينية](../../README.zh-CN.md) diff --git a/docs/i18n/README.bn.md b/docs/i18n/README.bn.md index 2f15ee8..e56ec13 100644 --- a/docs/i18n/README.bn.md +++ b/docs/i18n/README.bn.md @@ -2,7 +2,7 @@ > ⚠️ **এই অনুবাদটি পুরোনো।** অনুগ্রহ করে সর্বশেষ তথ্যের জন্য [ইংরেজি README](../../README.md) দেখুন। -taskflow কোডিং এজেন্টের জন্য একটি ডিক্লারেটিভ এবং যাচাইযোগ্য *টাস্ক গ্রাফ* — এটি [Pi কোডিং এজেন্ট](https://pi.dev) এবং [OpenAI Codex](https://github.com/openai/codex) উভয়েই চলে — এটি এমন কোনো workflow নয় যা আপনি স্ক্রিপ্ট করেন, বরং একটি DAG যা আপনি ঘোষণা করেন এবং runtime একটি token খরচের আগেই যাচাই করে। শূন্য রানটাইম নির্ভরতা, ৮৭২টি পরীক্ষা, ৯টি ফেজ টাইপ। +taskflow Pi, Codex, Claude Code, OpenCode ও Grok Build-এর জন্য একটি ডিক্লারেটিভ, যাচাইযোগ্য টাস্ক গ্রাফ। 0.2.0 লাইনে Node.js ≥ 22.19.0, 12টি phase kind এবং 1500+ পরীক্ষা রয়েছে। MCP layer-এর MCP SDK dependency নেই; core-এর peer `typebox`, আর DSL TypeScript-এর উপর নির্ভরশীল। > **কেন "taskflow", "workflow" নয় কেন?** একটি *workflow* (code-mode) হল একটি ইম্পারেটিভ স্ক্রিপ্ট যা *প্রবাহিত হয়*, যার গ্রাফ কন্ট্রোল ফ্লোর মধ্যে লুকানো। একটি *taskflow* পরিকল্পনাকে পৃথক টাস্ক নোডের একটি ডিক্লারেটিভ গ্রাফে সরিয়ে নেয় — যা স্থিরভাবে যাচাই, দৃশ্যমান, পুনরারম্ভ এবং একটি কমান্ড হিসেবে সংরক্ষণ করা যায়। আমরা ইচ্ছাকৃতভাবে এক্সপ্রেসিভনেসকে যাচাইযোগ্যতার বিনিময়ে দিই। @@ -13,6 +13,17 @@ pi install npm:pi-taskflow # Codex codex plugin marketplace add heggria/taskflow codex plugin add taskflow@taskflow + +# Claude Code +claude plugin marketplace add heggria/taskflow +claude plugin install claude-taskflow@taskflow + +# OpenCode +opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build (from monorepo checkout pre-publish, or published source) +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +# or: grok mcp add taskflow -- npx -y -p grok-taskflow grok-taskflow-mcp ``` [GitHub](https://github.com/heggria/taskflow) · [ইংরেজি README](../../README.md) · [চীনা README](../../README.zh-CN.md) diff --git a/docs/i18n/README.es.md b/docs/i18n/README.es.md index e6cc603..cf9b579 100644 --- a/docs/i18n/README.es.md +++ b/docs/i18n/README.es.md @@ -2,7 +2,7 @@ > ⚠️ **Esta traducción está desactualizada.** Consulta el [README en inglés](../../README.md) para obtener la información más reciente. -taskflow es un *grafo de tareas* declarativo y verificable para agentes de codificación — funciona en el [Pi coding agent](https://pi.dev) y en [OpenAI Codex](https://github.com/openai/codex): no un workflow que escribes como script, sino un DAG que declaras y que el runtime verifica antes de gastar un solo token. Cero dependencias en tiempo de ejecución, 872 pruebas, 9 tipos de fase. +taskflow es un *grafo de tareas* declarativo y verificable para agentes de codificación — funciona en Pi, Codex, Claude Code, OpenCode y Grok Build. Línea 0.2.0: Node.js ≥ 22.19.0, 12 tipos de fase y más de 1500 pruebas. La capa MCP no depende de un SDK MCP; core usa `typebox` como peer y el DSL depende de TypeScript. > **¿Por qué "taskflow" y no "workflow"?** Un *workflow* (estilo code-mode) es un script imperativo que *fluye*, con el grafo oculto en el control de flujo. Un *taskflow* mueve el plan a un grafo declarativo de nodos de tarea discretos — que se puede verificar estáticamente, visualizar, reanudar y guardar como un comando. Cambiamos expresividad por verificabilidad, a propósito. @@ -13,6 +13,17 @@ pi install npm:pi-taskflow # Codex codex plugin marketplace add heggria/taskflow codex plugin add taskflow@taskflow + +# Claude Code +claude plugin marketplace add heggria/taskflow +claude plugin install claude-taskflow@taskflow + +# OpenCode +opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build (from monorepo checkout pre-publish, or published source) +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +# or: grok mcp add taskflow -- npx -y -p grok-taskflow grok-taskflow-mcp ``` [GitHub](https://github.com/heggria/taskflow) · [README en inglés](../../README.md) · [README en chino](../../README.zh-CN.md) diff --git a/docs/i18n/README.hi.md b/docs/i18n/README.hi.md index d5636c5..29093f9 100644 --- a/docs/i18n/README.hi.md +++ b/docs/i18n/README.hi.md @@ -2,7 +2,7 @@ > ⚠️ **यह अनुवाद पुराना है।** कृपया नवीनतम जानकारी के लिए [अंग्रेज़ी README](../../README.md) देखें। -taskflow कोडिंग एजेंटों के लिए एक घोषणात्मक और सत्यापन-योग्य *टास्क ग्राफ* है — यह [Pi कोडिंग एजेंट](https://pi.dev) और [OpenAI Codex](https://github.com/openai/codex) दोनों पर चलता है — वह workflow नहीं जिसे आप स्क्रिप्ट करते हैं, बल्कि एक DAG जिसे आप घोषित करते हैं और जिसे runtime एक भी token खर्च करने से पहले सत्यापित करता है। शून्य रनटाइम निर्भरता, 872 परीक्षण, 9 चरण प्रकार। +taskflow Pi, Codex, Claude Code, OpenCode और Grok Build के लिए एक घोषणात्मक, सत्यापन-योग्य टास्क ग्राफ है। 0.2.0 लाइन में Node.js ≥ 22.19.0, 12 phase kinds और 1500+ परीक्षण हैं। MCP layer किसी MCP SDK पर निर्भर नहीं है; core का peer `typebox` है और DSL TypeScript पर निर्भर है। > **"taskflow" क्यों, "workflow" क्यों नहीं?** एक *workflow* (code-mode) एक आज्ञात्मक स्क्रिप्ट है जो *बहता* है, जिसका ग्राफ कंट्रोल फ़्लो में छिपा होता है। एक *taskflow* योजना को अलग-अलग टास्क नोड्स के एक घोषणात्मक ग्राफ में ले जाता है — जिसे स्थिर रूप से सत्यापित, दृश्य, पुनः शुरू और एक कमांड के रूप में सहेजा जा सकता है। हम जान-बूझकर अभिव्यक्ति को सत्यापन-योग्यता से बदलते हैं। @@ -13,6 +13,17 @@ pi install npm:pi-taskflow # Codex codex plugin marketplace add heggria/taskflow codex plugin add taskflow@taskflow + +# Claude Code +claude plugin marketplace add heggria/taskflow +claude plugin install claude-taskflow@taskflow + +# OpenCode +opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build (from monorepo checkout pre-publish, or published source) +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +# or: grok mcp add taskflow -- npx -y -p grok-taskflow grok-taskflow-mcp ``` [GitHub](https://github.com/heggria/taskflow) · [अंग्रेज़ी README](../../README.md) · [चीनी README](../../README.zh-CN.md) diff --git a/docs/i18n/README.pt.md b/docs/i18n/README.pt.md index 9d34dd6..d6b95ac 100644 --- a/docs/i18n/README.pt.md +++ b/docs/i18n/README.pt.md @@ -2,7 +2,7 @@ > ⚠️ **Esta tradução está desatualizada.** Consulte o [README em inglês](../../README.md) para obter as informações mais recentes. -taskflow é um *grafo de tarefas* declarativo e verificável para agentes de codificação — funciona no [Pi coding agent](https://pi.dev) e no [OpenAI Codex](https://github.com/openai/codex) — não um workflow que você escreve como script, mas um DAG que você declara e que o runtime verifica antes de gastar um único token. Zero dependências em tempo de execução, 872 testes, 9 tipos de fase. +taskflow é um *grafo de tarefas* declarativo e verificável para Pi, Codex, Claude Code, OpenCode e Grok Build. Linha 0.2.0: Node.js ≥ 22.19.0, 12 tipos de fase e mais de 1500 testes. A camada MCP não depende de um SDK MCP; core usa `typebox` como peer e o DSL depende de TypeScript. > **Por que "taskflow" e não "workflow"?** Um *workflow* (estilo code-mode) é um script imperativo que *flui*, com o grafo escondido no controle de fluxo. Um *taskflow* move o plano para um grafo declarativo de nós de tarefa discretos — que pode ser verificado estaticamente, visualizado, retomado e salvo como um comando. Trocamos expressividade por verificabilidade, de propósito. @@ -13,6 +13,17 @@ pi install npm:pi-taskflow # Codex codex plugin marketplace add heggria/taskflow codex plugin add taskflow@taskflow + +# Claude Code +claude plugin marketplace add heggria/taskflow +claude plugin install claude-taskflow@taskflow + +# OpenCode +opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build (from monorepo checkout pre-publish, or published source) +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +# or: grok mcp add taskflow -- npx -y -p grok-taskflow grok-taskflow-mcp ``` [GitHub](https://github.com/heggria/taskflow) · [README em inglês](../../README.md) · [README em chinês](../../README.zh-CN.md) diff --git a/docs/i18n/README.ru.md b/docs/i18n/README.ru.md index b6cee28..495ea99 100644 --- a/docs/i18n/README.ru.md +++ b/docs/i18n/README.ru.md @@ -2,7 +2,7 @@ > ⚠️ **Этот перевод устарел.** Пожалуйста, обратитесь к [английскому README](../../README.md) за актуальной информацией. -taskflow — это декларативный и проверяемый *граф задач* для агентов кодирования — работает в [Pi coding agent](https://pi.dev) и в [OpenAI Codex](https://github.com/openai/codex) — не workflow, который вы пишете как скрипт, а DAG, который вы объявляете и который runtime проверяет до того, как потратить хотя бы один токен. Нулевые зависимости времени выполнения, 872 теста, 9 типов фаз. +taskflow — декларативный и проверяемый граф задач для Pi, Codex, Claude Code, OpenCode и Grok Build. Линия 0.2.0: Node.js ≥ 22.19.0, 12 типов фаз и более 1500 тестов. Слой MCP не зависит от MCP SDK; core использует peer `typebox`, а DSL зависит от TypeScript. > **Почему "taskflow", а не "workflow"?** *Workflow* (в стиле code-mode) — это императивный скрипт, который *течёт*, а его граф спрятан в потоке управления. *Taskflow* переносит план в декларативный граф из дискретных узлов-задач — его можно статически проверить, визуализировать, возобновить и сохранить как команду. Мы осознанно меняем выразительность на проверяемость. @@ -13,6 +13,17 @@ pi install npm:pi-taskflow # Codex codex plugin marketplace add heggria/taskflow codex plugin add taskflow@taskflow + +# Claude Code +claude plugin marketplace add heggria/taskflow +claude plugin install claude-taskflow@taskflow + +# OpenCode +opencode mcp add taskflow -- npx -y -p opencode-taskflow opencode-taskflow-mcp + +# Grok Build (from monorepo checkout pre-publish, or published source) +grok mcp add taskflow -- npx -y -p grok-taskflow@0.2.0 grok-taskflow-mcp +# or: grok mcp add taskflow -- npx -y -p grok-taskflow grok-taskflow-mcp ``` [GitHub](https://github.com/heggria/taskflow) · [README на английском](../../README.md) · [README на китайском](../../README.zh-CN.md) diff --git a/docs/internal/COMPETITORS.md b/docs/internal/COMPETITORS.md index 29cccf6..b9e46da 100644 --- a/docs/internal/COMPETITORS.md +++ b/docs/internal/COMPETITORS.md @@ -10,7 +10,7 @@ | Framework | Orchestration model | Authoring | Durable / resume | Dynamic fan-out | Human-in-loop | Quality gate | Static verification | Cross-run memoization | Observability | Deps | Uniquely good at | |---|---|---|---|:--:|:--:|:--:|:--:|:--:|---|:--:|---| | **▼ LOCAL / EMBEDDED DAG DSL** ||||||||||| -| **pi-taskflow** | Declarative JSON-DSL DAG (10 phase types) | JSON data | phase-level input-hash cache + cross-session resume | **YES** (`map`) | YES (approval) | YES (verdict) | NO (planned) | **YES** | basic (live DAG render; no OTel) | **ZERO** | zero-dep declarative subagent DAG; phase-hash caching; when-guards; budget caps; loop·tournament·cross-run cache | +| **pi-taskflow** | Declarative JSON-DSL DAG (12 phase types) | JSON data | phase-level input-hash cache + cross-session resume | **YES** (`map`) | YES (approval) | YES (verdict) | NO (planned) | **YES** | basic (live DAG render; no OTel) | **ZERO** | zero-dep declarative subagent DAG; phase-hash caching; when-guards; budget caps; loop·tournament·race·expand·cross-run cache | | **▼ GRAPH / STATE-MACHINE** ||||||||||| | LangGraph | StateGraph nodes/edges + Pregel super-steps | Python/JS code | checkpointer every super-step; time travel | YES (`Send`) | YES (`interrupt()` any node) | NO builtin | YES (`.compile()` + typed state) | within-session only | YES (LangSmith) | heavy | checkpoint durability + time travel; largest community | | Google ADK | Directed graph + agent-tree hierarchy | Python/JS/Go/Java/Kotlin | durable memory; event-driven dormancy | YES | YES (any-node) | YES (safety framework) | YES (inferred from graph compile) | UNVERIFIED | YES (builtin logs/metrics/traces) | medium | multi-language agent-tree hierarchy; first-class delegation | diff --git a/docs/internal/brainstorm-2026-07-08-0.2.0-phases.md b/docs/internal/brainstorm-2026-07-08-0.2.0-phases.md new file mode 100644 index 0000000..21ad401 --- /dev/null +++ b/docs/internal/brainstorm-2026-07-08-0.2.0-phases.md @@ -0,0 +1,155 @@ +# Brainstorm: taskflow 0.2.0 更强的 Phase 类型 + +> Status: **Brainstorm 存档** · 2026-07-08 +> 方法:会话式发散,锚定在 [`rfc-0.2.0-architecture.md`](../rfc-0.2.0-architecture.md) 已拍板的 +> **事件溯源 + FlowIR 执行内核**(Q2=B / Q5=own)之上。 +> 关联:[`design-dynamic-dag-expansion.md`](./design-dynamic-dag-expansion.md)(`expand` 的前身设计+对抗评审)、 +> [`brainstorm-2026-07-frontier-council.md`](./brainstorm-2026-07-frontier-council.md)(旧的 white-space / 明确不做清单)、 +> [`design-org-supervision.md`](./design-org-supervision.md)(`escalate`/supervision 的相关设计)。 +> +> **核心角度**:这轮不是重提旧点子。0.2.0 换成事件溯源内核后,**一批以前"因架构风险被否/推迟"的 +> phase 变便宜、变安全了**。本文按"新内核新解锁什么"重新审视 phase 设计空间。 + +--- + +## 0. 组织论点:新内核改变了 phase 设计的成本 + +**证据(决定性)**:动态 DAG 展开(`flow{def}` / self-expand)当年被对抗评审砍到只做"嵌套子流程"版——真正强大的"把动态节点 graft 进父 DAG"被推迟,因为它在**命令式内核**里有 3 个 P0: + +1. crash 后注入丢失(持久化竞态) +2. 三态就绪判定不清("依赖没跑" vs "依赖跑失败")→ `join:"any"` 误 skip / 死锁 +3. 并发中改 `def.phases` 数组 + +**而这 3 个 P0,正是事件溯源内核天生解决的**: + +| 命令式内核的 P0 | 事件溯源内核如何天然治好 | +|---|---| +| ① 持久化竞态 | event log 就是持久化;注入 = 追加事件,无竞态 | +| ② 三态就绪 | fold 从事件流天然区分 done/failed/pending | +| ③ 并发改数组 | driver 定点从 folded 状态重算 ready 集,不改共享数组 | + +→ **0.2.0 的新内核,恰恰是那个被推迟的强 phase 缺的地基。** 这是本文的立论基础。 + +phase 因此分三桶:**① 新解锁** | **② 变更强** | **③ 仍不做**。此外单列 **④ Moonshot 大胆簇**。 + +--- + +## 1. 🔓 Bucket 1 —— 新内核**新解锁**的 phase + +| Phase | 干什么(大白话) | 0.2.0 为何解锁(以前被什么挡) | 超能力 | +|---|---|---|---| +| **`expand` 动态图嫁接** | agent 运行中生成一段 DAG,**splice 进正在跑的父图**(不只嵌套子流程) | 3 个 P0 全是命令式内核的病,事件溯源天生治好(§0) | 🔨编译 | +| **`compensate` / saga** | 某 phase 失败→**沿 event log 倒着**触发补偿动作(回滚迁移 / 删已建资源) | 事件溯源就是 saga 的实现方式(Temporal);以前列为 L 推迟("无回滚语义") | ♻️恢复 | +| **`race` 首个胜出** | spawn N 个方案,**取最先完成的**,砍掉其余 | 以前"投机执行"被否("无回滚 + 违背成本控");现 event log 给干净的取消记账 + replay 能算被砍分支成本 | ⏩延迟优化 | +| **`watch` 响应式触发** | 一个 phase 在它**读的东西变了**时自动重跑(持续 / 常驻流) | 这正是响应式内核(signals / observed readSet)本身;以前"stream edges"被否("核心架构风险最大"),现用响应式而非流边实现 | 🔁增量→连续 | + +--- + +## 2. ⚡ Bucket 2 —— 现有 phase **变更强** + +| Phase | 升级 | 以前的障碍 | 超能力 | +|---|---|---|---| +| **`loop` 多 phase body** | 循环体从"单个 agent 任务"→ **test→fix→retest 子图** | DSL RFC v2 §4.6 明标 post-0.2.0("需引擎支持循环内子图");被 `expand` 解锁 | ♻️(最想要的增强) | +| **`map` 逐项增量** | 改 1 个 item 的输入 → **只重跑那 1 项**,不重跑整个 map | roadmap §6.3 推迟("map 吐整个数组,scope 爆炸");事件溯源逐项事件天然可寻址 | 📉增量(补全"改 1 文件→重跑 1 项"旗舰叙事) | +| **`route` / switch 类型路由** | 按**类型化判别式**(`json` 联合)精确激活 1 个分支 | 以前"tagged-union routing"被否("需 schema 先行 + 无需求");现 FlowIR + 类型输出让路由**编译期可验证穷尽性** | 🔨编译 | +| **`gate`/`approval` 类型化裁决** | 裁决用类型化 schema + approval **超时 + 回退** | frontier council white-space #4/#13(无竞品有);event log 让多轮 / 超时**可恢复** | ♻️恢复 | + +--- + +## 3. 🚫 Bucket 3 —— 仍然不做(新内核**没**改变判断) + +| 方向 | 理由(仍成立) | +|---|---| +| 可视化拖拽编辑器 | 与"JSON / 代码即护城河"冲突 | +| Flow algebra(merge/project/compose) | `flow{use}` + decompile 已覆盖 | +| Artifacts(类型化文件输出) | `ctx_write`/`ctx_read` 已覆盖 | +| Stream edges + backpressure | `watch`(Bucket 1)用响应式拿到真需求,不背流边架构风险 | + +--- + +## 4. 🌌 Bucket 4 —— Moonshot 大胆簇("flow 是活体,不是静态程序") + +> **心智跃迁**:在事件溯源 + FlowIR-as-data + 确定性重放下,flow 不再是"你跑一次的静态程序",而是 +> **可分叉、可回溯、能观察自己、能改写自己的活体进程**。event log 之于 flow ≈ git 之于代码 + 训练数据之于模型。 + +### 4.1 时间旅行 / 分叉(event log 当 git 用) + +| Phase | 干什么 | 新内核为何让它可能 | +|---|---|---| +| **`fork` / savepoint** | 给运行中的 flow 打命名存档点,之后能**从存档点分叉出新 run**(复用到存档点的 log,再岔开) | 事件溯源下"分叉一个 run" = fork 一条 log,几乎白得。跑到第 5 步→分叉→并行试 3 种第 6 步 | +| **`speculate` 投机分叉** | 决策点**同时展开多个未来**(fork log 并行跑),胜者留下、败者剪掉但**保留其 log 供 replay 复盘"没走的路"** | 投机执行以前被否("无回滚");现在剪一个分支 = 丢一条 log,天然可回滚 + replay 能算被剪分支成本 | + +### 4.2 零成本反事实(replay 当模拟器用) + +| Phase | 干什么 | 新内核为何让它可能 | +|---|---|---| +| **`counterfactual` 一跑多变体** | 跑完一个 phase,**立刻用 N 组不同旋钮(模型/prompt/阈值)replay 它**,复用缓存的上游,近乎零边际成本产出对比 | replay + 内容寻址缓存 = "试 3 个模型看哪个当时最好,不花 3 倍钱"。把 eval/实验变成**运行时一等原语** | +| **`sandbox-twin` 数字孪生排练** | 真动手前,先对**录制环境的 replay**跑一遍验证,通过了再真跑 | replay 能重放录制环境 = 可模拟。"三思而后行"变一等能力(dry-run 终极形态) | + +### 4.3 自我进化(跨运行 log 当训练信号) + +| Phase | 干什么 | 新内核为何让它可能 | +|---|---|---| +| **`self-optimize` 自调优** | 一个 phase **读自己所有历史 run 的 log**,按 p50/p95 成本·延迟·成功率**自动调**模型/并发/重试 | 跨运行 event log + 费率表(F1) = 现成训练信号。"flow 从自己的历史里学"——无竞品在自我调优 | +| **`population` 进化循环** | loop 不再是单一血脉迭代,而是**维护候选种群**,每代对最优做变异/交叉,多代收敛(遗传算法) | event log 记录谱系;比 reflexion-loop(单线反思)高一维 | + +### 4.4 群体智能(比 tournament 更进一步) + +| Phase | 干什么 | vs 现有 | +|---|---|---| +| **`quorum` 共识** | 跑 N 个,输出取**多数投票/中位数**,分歧度作为置信信号(self-consistency) | tournament=裁判选最优;quorum=群体求共识 + 出置信度 | +| **`negotiate` 辩论** | N 个对立立场的 agent **互相辩**到收敛(非各自对任务),裁判收口 | 对抗协作/debate 成一等原语;每轮可 replay | + +### 4.5 相关(org-supervision 设计的延伸) + +| Phase | 干什么 | 关联 | +|---|---|---| +| **`escalate` 动态督导** | 督导 phase **经 event log 监控运行中的子任务**,动态增派 worker / 重分配 / 杀掉低效者 | `design-org-supervision.md`(ctx_spawn/ctx_report);event log 是督导的可观测性 | + +--- + +## 5. 排序与首推 + +### 5.1 稳健候选(Bucket 1+2)排序 + +| 优先 | Phase | 一句话理由 | +|---|---|---| +| **1** | `expand` 动态图嫁接 | **旗舰**——事件溯源正是它缺的地基,已设计+已对抗评审,直接兑现"agent 编排的编译器"叙事 | +| **2** | `loop` 多 phase body | 最想要的增强,被 #1 直接解锁 | +| **3** | `map` 逐项增量 | 补全"改 1 文件只重算 1 点"的增量旗舰故事 | +| **4** | `compensate`/saga | 真实副作用流的安全网,事件溯源下自然 | +| **5** | `race` 首个胜出 | 便宜,填延迟优化空白(tournament=最优、parallel=全等,都不是首个胜出)| +| 6 | `route` 类型路由 | 编译期可验证,比 N 个 `when` 干净 | +| 7 | `watch` 响应式触发 | 最大胆——常驻/连续流,把"增量"推到极致 | + +> **关键连锁**:`expand` 一旦落地,`loop` 子图 和 `map` 逐项几乎白得——它们都是"局部子图"的特例。 +> **`expand` 不只是一个 phase,是解锁一串 phase 的元能力。** + +### 5.2 Moonshot 诚实分层 + +| Tier | Phase | 判断 | +|---|---|---| +| **A|大胆但 0.2.0 够得着** | `fork`/savepoint、`counterfactual`、`quorum` | 只是 event-log + replay + cache 的巧用,无新内核机制 | +| **B|Moonshot,需研究 spike** | `speculate` 剪枝、`self-optimize`、`population`、`negotiate`、`escalate` | 要新机制 + 真风险(成本爆炸/收敛性/评估偏差/督导语义),带 kill-criteria 立项 | +| **C|仍太远/冲突** | 真流式、全自主自改写 flow | 违背成本可控 / 护城河,暂不碰 | + +### 5.3 首推 +- **稳健旗舰**:`expand`(元能力,连带解锁 loop 子图 + map 逐项)。 +- **大胆但够得着**:`fork`/savepoint + `counterfactual`——几乎是事件溯源内核的免费副产品,把 taskflow 从"编排器"抬到"可实验的活体运行时";且 `counterfactual` 是**把已有 replay 直接变成用户级杀手锏**的最短路径,正好接上 0.1.7 承诺的确定性重放。 + +--- + +## 6. 一句话最狂愿景 + +> **flow 从"运行一次的程序"变成"能分叉试错、零成本反事实、从自己历史学习的活体"。** +> `expand` 让它**长出**新节点,`fork` 让它**分叉**,`counterfactual` 让它**假设**,`self-optimize` 让它**进化**—— +> 这四个合起来,taskflow 不再是编排器,是 **agent 工作流的活体运行时**。 + +--- + +## 7. 下一步(未定,待选) + +1. 挑 `expand` 深挖成 phase 设计草案(DSL 形态 + FlowIR node 语义 + 与 `flow{def}` 现有路径的关系 + graft-into-parent 的事件溯源实现)。 +2. 挑 `fork` + `counterfactual` 深挖(DSL 形态 + event-log 分叉语义 + 与 replay 复用)。 +3. 用 taskflow 自己跑一轮对抗式 brainstorm(4 路发散 → critic 收敛 → arbiter 裁决)把 Moonshot 簇压测 + 定 kill-criteria。 +4. 把 Bucket 1+2 的排序纳入 `rfc-0.2.0-architecture.md` §5 的建造顺序(作为 S2 之后的 phase 扩展 horizon)。 diff --git a/docs/internal/claim-vs-impl-0.2.0.md b/docs/internal/claim-vs-impl-0.2.0.md new file mode 100644 index 0000000..2c0d9ea --- /dev/null +++ b/docs/internal/claim-vs-impl-0.2.0.md @@ -0,0 +1,119 @@ +# Claim vs implementation — verification log (`release/0.2.0`) + +> Last updated: 2026-07-10 · Local closure gates pass on the merged tree; +> do not publish until the release PR/CI passes and the tag job owns all names. +> Purpose: single ledger so marketing/RFCs/skills do not outrun the code. + +## Verified true (hard claims OK) + +| Claim | Evidence | +|-------|----------| +| S0 FlowIR + content hash | `flowir/compile.ts`, `hashFlowIR`; race/expand compile smoke | +| S1 events + fold + trace | `exec/{events,fold}`, `FileTraceSink` mkdir-on-flush | +| S2 event kernel default OFF, 10 kernel kinds | `EVENT_KERNEL_PHASE_TYPES` = PHASE_TYPES − race − expand; env/flag | +| S3 offline replay | `replay.ts`, MCP `taskflow_replay`, pi `/tf replay` | +| S4 package `taskflow-dsl` | erase kinds registry, CLI build/check/decompile/new, tests | +| 12 `PHASE_TYPES` | `schema.ts`; imperative runtime executes all 12 | +| MCP 12 tools | `taskflow-mcp-core` server tool list | +| Five host delivery packages | pi/codex/claude/opencode/grok | +| Toolchain = TypeScript AST (not ts-morph) | `taskflow-dsl` depends on `typescript` only | +| MCP request cancellation | concurrent stdio dispatch; `notifications/cancelled` → `AbortSignal` → runtime/host child | +| Grok sandbox policy | read-only and mutating/default phases both require independent operator-configured custom profiles because built-ins may fail open; read-only also has a narrow allowlist + mutator denies; live Grok 0.2.93 E2E probes both profiles | +| Existing npm version verification | trusted owner + SLSA/GitHub provenance + tag/commit + exact tarball integrity before skip | + +## Honest / qualified + +| Topic | Truth | +|-------|--------| +| Version | Package manifests and plugin pins are bumped to **0.2.0**; npm is not published until the `v0.2.0` release job succeeds | +| S5 | Kernel default ON **not** done; flagship $6→$0.40 is **acceptance target**, not certified number | +| `cancelLosers` | **Implemented**: first-**success** wins; abort losers after success; parent abort wakes + grace-bounds wait; cooperative losers in usage | +| Event kernel “complete” | Complete for **kernel-eligible** kinds/features; not race/expand; not score/retry/expect/reflexion/cross-run cache/shareContext; **nested** `flow` re-runs `canUseEventKernel` (fail-closed) | +| Multi-host DSL | Hosts run **Taskflow JSON**; `.tf.ts` requires prior `taskflow-dsl build` | +| Decompile | Semantic, not literal round-trip | +| Test count | **1500+** unit tests in **100** `*.test.ts` files (regenerate the exact count on release) | +| Package count | **9** under `packages/` + `website` | +| Grok budgets | Grok 0.2.93 reports no usage; Grok MCP explicitly rejects any flow declaring `budget` | + +## Explicitly not shipped + +loop multi-body · route · compensate/saga · watch · experimental C-track runes · host auto-build of `.tf.ts` · S5 default kernel ON + +## Alignment actions taken + +### Pass 1 (claim ledger) +1. Schema + skills: `cancelLosers` documented (later upgraded to real abort + first-success). +2. S4 RFCs: ts-morph → TypeScript compiler API. +3. FlowIR/step/runtime comments: 12 kinds / kernel-10. +4. README / AGENTS / workspace / architecture counts. +5. North-star: DSL ✅; flagship $ = S5 gate. + +### Pass 2 (website + CHANGELOG + examples) +6. Website homepage: **12** phases / **5** hosts (en+zh). +7. Website phase-types + concepts: `race` / `expand` sections (en+zh). +8. Website reference: **TypeScript DSL** page (en+zh) + nav. +9. CHANGELOG Unreleased: S4 + race/expand + alignment; kernel wording fixed. +10. Examples: `race-first-win.json`, `expand-nested-fragment.json`. +11. Skills advanced: `flow{def}` vs `expand`; configuration caveats (kernel/decompile). + +### Pass 3 (multi-agent review P0/P1 fixes) +12. Race **first-success** (not first-settled); cooperative loser usage aggregates. +13. Graft: rewrite `{steps.*}` after id prefix; zero expand usage after promote (no double-count). +14. DSL `register()` unions explicit `dependsOn`; unknown runes / non-agent branches error. +15. Decompile: import race/expand; fail-closed non-string `def`. +16. Kernel policy: `incremental` + workspace cwd keywords force imperative. +17. README phase table + taskflow-dsl README preview note. + +### Pass 4 (multi-agent P0/P1 code fixes) +18. Race **first-success** (not first-settled); non-cooperative loser wait is bounded. +19. Graft: rewrite `{steps.*}` after id prefix; zero expand usage after promote. +20. DSL `register()` unions dependsOn; unknown runes / non-agent branches error. +21. Decompile: import race/expand; fail-closed non-string `def`. +22. Kernel policy: incremental + workspace cwd keywords force imperative. + +### Pass 5 (adversarial re-review closure) +23. Race parent-abort wakes gate + grace; all-fail unit test; cancelLosers grace retained. +24. Nested event-kernel re-admission (`canUseEventKernel` on child) fail-closed. +25. DSL bare unknown callees error; P0 regression tests (dependsOn union, branch kind, decompile). +26. Decompile expand keeps `dependsOn`/`final`/`maxNodes`; race emits `cancelLosers: false`. +27. Docs honesty: EN/zh README monorepo-vs-npm banner; zh phase/package parity; website race first-success; example description; publish.yml nine packages; CONTRIBUTING/DECISIONS counts. +28. CI includes `test:e2e-grok-mcp` (already wired). + +### Pass 6 (release-closure hardening) +29. Grok 0.2.93 read-only execution no longer includes unmappable web ids; mutator deny rules remain even if allowlist handling regresses. +30. Grok thinking maps to `--reasoning-effort`; its unavailable usage accounting rejects budgeted MCP runs fail-closed. +31. MCP stdio dispatch is concurrent and propagates `notifications/cancelled` through a per-request `AbortSignal`. +32. Existing npm versions are never blindly skipped: owner, provenance repository/workflow/ref/commit, and local tarball integrity must match. +33. Added a live Grok executor E2E (`pnpm run test:e2e-grok`) in addition to the network-free Grok MCP E2E. +34. Corrected detached-run documentation: detach is Pi-only; MCP cancellation aborts rather than creating hidden background work. + +### Pass 7 (cross-adversarial terminal closure) +35. Runtime/kernel/replay/cache/trace/graft/resume semantics were challenged with executable counterexamples; nested and supervision-tree budgets now use remaining caps, replay fails safe on incomplete/legacy graph evidence, and graft ownership/usage survives definition evolution and collisions. +36. DSL compiler/decompiler/CLI is fail-closed for unsupported dynamic syntax, round-trips all 12 phase kinds, defaults `check` to TypeScript diagnostics, and passes clean tarball/install E2E. +37. MCP cancellation tears down the full host process tree; stdio disconnect/error paths are bounded and suppress late work/responses. +38. Grok read-only and custom-profile mutating policies have live 0.2.93 enforcement probes; unavailable usage accounting rejects every nested budget path before spawn; max-turn exhaustion is fatal. +39. All GitHub Actions are pinned to verified full SHAs; npm publish and GitHub Release use separate least-privilege jobs; published-version reruns verify provenance and exact tarball integrity. +40. Root typecheck, full unit suite, all package builds, website static export, DSL install E2E, four host MCP E2Es, built-dist comprehensive MCP E2E, and live Codex/OpenCode/Grok executors pass locally. + +## Still open (not claimed as done) + +- Formal **0.2.0 npm publish** after merge + `v0.2.0` tag; pins already match. +- S5 kernel default ON + flagship $ demo seal → plan: `docs/internal/s5-kernel-default-on-plan.md`. +- Live Claude executor E2E is still an external release-environment gate: the current local Claude route returns HTTP 403 from `api.ohmyrouter.com`. Codex, OpenCode, and Grok live executors pass; all four MCP adapters pass without live model access. + +## Re-verify commands + +```bash +pnpm run test:dsl +pnpm run test:hosts +node --conditions=development --experimental-strip-types --test \ + 'packages/taskflow-mcp-core/test/*.test.ts' +pnpm run test:e2e-grok +node --conditions=development --experimental-strip-types --test \ + packages/taskflow-core/test/race-expand.test.ts \ + packages/taskflow-core/test/script.test.ts +node scripts/build-skills.mjs && \ + node --conditions=development --experimental-strip-types --test \ + packages/pi-taskflow/test/skills-build.test.ts +# Optional full: pnpm test +``` diff --git a/docs/internal/modularization-0.2.0.md b/docs/internal/modularization-0.2.0.md new file mode 100644 index 0000000..dce7112 --- /dev/null +++ b/docs/internal/modularization-0.2.0.md @@ -0,0 +1,53 @@ +# Modularization notes (0.2.0) + +Avoid monolith growth while S4 lands and S5 preps. Prefer **extract-by-kind / extract-by-phase** over more branches in giant files. + +## taskflow-dsl erase + +``` +packages/taskflow-dsl/src/build/erase/ +├─ pipeline.ts ← flow() discovery + body walk only (~280 LOC) +├─ context.ts ← EmitContext, register(), bindDefArg() +├─ ast.ts / opts.ts / templates.ts / types.ts +└─ kinds/ + ├─ index.ts ← KIND_HANDLERS registry + trySpecializedEmit() + ├─ agent-script.ts + ├─ map.ts / parallel.ts / race.ts + ├─ gate.ts / gate-sugar.ts + ├─ reduce.ts / approval.ts / loop.ts / tournament.ts + └─ expand-flow.ts ← expand + subflow.def / subflow use +``` + +**Rule:** new phase kind → new file under `kinds/` + one entry in `KIND_HANDLERS`. Do not re-inflate `pipeline.ts`. + +## taskflow-core runtime (S5 strangler preheat) + +``` +packages/taskflow-core/src/runtime.ts ← facade + orchestration still large; peels continue +packages/taskflow-core/src/runtime/phases/ +├─ race.ts ← executeRaceBranches +├─ expand.ts ← pure helpers (mode, maxNodes, prefix, promote) +├─ script.ts ← runScriptCommand + result → PhaseState +├─ parallel.ts ← executeParallelBranches (inject runFanout + merge) +└─ approval.ts ← approvalDecisionToPhaseState +``` + +| Kind | In `phases/` | Still in `runtime.ts` | +|------|--------------|------------------------| +| race | ✅ execute | thin dispatch + cache | +| expand | ✅ pure helpers | flow\|expand sub-run block | +| script | ✅ spawn + map | interpolate + cache | +| parallel | ✅ execute | branch resolve + cache | +| approval | ✅ decision map | message + requestApproval | +| map / loop / tournament / agent / gate / reduce / flow | ⬜ | full bodies | + +**Rule:** next peels — `map`, `loop`, `tournament`, then agent/gate shared path. Each file takes injectables (`runOne`, `runFanout`, cache hooks); one-liner dispatch in `executePhaseInner`. + +**S5 link:** kind modules make event-kernel handlers easier to add without editing a 3k-line unit. `race`/`expand` stay imperative until kernel step handlers exist. + +## Invariants + +1. Kind emit must go through `register(ctx, draft)` so dependsOn/final/id stripping stay consistent. +2. Core must never import `taskflow-dsl`. +3. Dynamic expand uses `MAX_DYNAMIC_PHASES` from `schema.ts` (import it; do not re-define). +4. Script timeouts and spawn errors remain uncached; non-zero exit remains cacheable. diff --git a/docs/internal/multi-agent-review-s4-horizon-2026-07-09.md b/docs/internal/multi-agent-review-s4-horizon-2026-07-09.md new file mode 100644 index 0000000..d0168e8 --- /dev/null +++ b/docs/internal/multi-agent-review-s4-horizon-2026-07-09.md @@ -0,0 +1,131 @@ +# Multi-agent review — S4 + Horizon B + alignment (`release/0.2.0`) + +> Date: 2026-07-09 · HEAD: `4da6f33` +> Scope: S4 `taskflow-dsl`, race/expand/cancelLosers, claim alignment, S5 plan +> Agents: architecture · race/expand concurrency · DSL erase · docs honesty +> Raw notes: `/tmp/grok-ma-review-{arch,race,dsl,docs}-c1d88a29.md` + +## Council verdict + +**Do not market “0.2.0 complete / race first-success / graft multi-phase ready” without fixes.** +Engineering depth is real (12 kinds, DSL package, tests green), but several **correctness and honesty** gaps remain above nits. + +| Track | Verdict | +|-------|---------| +| Architecture S0–S4 shape | **Conditionally OK** — types align; dual-path + FlowIR field gaps | +| Race / expand runtime | **Blockers** — first-settled ≠ first-success; graft prefix + usage | +| DSL erase | **Blockers** — dependsOn overwrite; silent drops; decompile lies | +| Docs / publish surface | **Blockers for release narrative** — 0.1.7 vs 0.2.0 dual story | + +--- + +## P0 — fix before calling Horizon B “done” + +### 1. Race: first-settled vs first-success *(race agent · bug)* + +- **Code:** `Promise.race` → first **settle** (fail or ok) wins. +- **Skills:** “first branch that finishes **successfully**”. +- **Impact:** Fast hard-fail kills the race while a slower branch would succeed. +- **Fix:** (A) first-success loop, or (B) rewrite all author docs to first-settled. Prefer **A**. + +### 2. Graft: template refs not rewritten *(race agent · bug)* + +- **Code:** `prefixGraftFragment` rewrites ids + `dependsOn`/`from` only. +- **Impact:** Multi-phase fragments with `{steps.a…}` break after prefix; validate-after-prefix can fail-open via `defError`. Single-phase graft works by accident. +- **Fix:** Rewrite collectible template surfaces with `idMap`; test two-phase graft chain. + +### 3. Graft usage double-count *(race agent · bug)* + +- Expand phase `usage` = sub total **and** promoted children keep usage → run rollup **2×**. +- **Fix:** Zero expand usage after promote **or** zero promoted children usage for aggregation. + +### 4. DSL `register()` drops explicit `dependsOn` *(dsl agent · bug)* + +- When auto-deps non-empty, `raw.dependsOn = [...auto]` overwrites opts.dependsOn for kinds that don’t union into `draft.dependsOn`. +- **Fix:** Union auto + explicit in `register()`; regression test. + +### 5. DSL silent phase/branch drops *(dsl agent · bug)* + +- Unknown callees in flow body: no error. +- parallel/race/tournament non-`agent()` branches: silent skip. +- **Fix:** Diagnostics `TFDSL_RUNE_UNKNOWN` / `TFDSL_BRANCH_KIND`. + +### 6. Decompile race/expand *(dsl agent · bug)* + +- Emits `race`/`expand` without importing them. +- Object `def` → fabricated `"{steps.plan.json}"` placeholder (shape lie). +- **Fix:** Import list; fail-closed non-string `def`. + +--- + +## P1 — before kernel default ON / public 0.2.0 + +### 7. Race usage / budget undercount *(race agent · bug)* + +- Winner-only final usage; concurrent live usage last-writer-wins. +- Loser spend (pre-abort) invisible to budget. +- **Fix:** Aggregate all branch usages after `allSettled`; shared live accumulator. + +### 8. Dual-path kernel admits flows that ignore features *(arch agent · bug)* + +- Kernel path may miss `cacheScopeDefault` incremental / workspace cwd keywords. +- **Fix:** Extend `kernelUnsupportedReason` or implement in driver. + +### 9. FlowIR sidecar incomplete for Horizon B *(arch agent · bug)* + +- `cancelLosers`, `expandMode`, `maxNodes` not fully in IR field model. +- **Fix:** FlowIR node payload parity or documented non-goals. + +### 10. Version dual narrative *(docs agent · bug)* + +- README / website teach 0.2.0 surfaces; packages **0.1.7**; plugins pin `@0.1.7`; `taskflow-dsl` may 404. +- **Fix:** Publish banner “preview branch” **or** bump 0.2.0 + publish. + +### 11. README phase table vs “12 phase types” *(docs agent · bug)* + +- Marketing line says 12; some tables still 10 without race/expand. +- **Fix:** Table sync. + +--- + +## P2 — hygiene + +| Item | Source | +|------|--------| +| gate sugar always TFDSL_RUNE_OPTS_UNKNOWN for pass/scorers | dsl | +| import-lint string-only vs full core barrel | dsl | +| S5 plan couples default ON to $ demo while kernel lacks cross-run cache | arch | +| claim ledger residual cancelLosers row | arch/docs | +| retry delay ignores race extraSignal | race | +| deterministic tests over wall-clock delays | race | + +--- + +## What the council agreed is solid + +- 12 `PHASE_TYPES` ↔ DSL erase registry ↔ FlowIR kind enum (post test fix). +- Event kernel excludes race/expand intentionally. +- `TFDSL_ERASE_ONLY` on runes. +- Parallel destructure → N agents; race destructure rejected. +- cancelLosers **wiring** (controllers + `extraSignal`) is directionally correct for happy path. +- Unit suite **1402/1402** after FlowIR length fix. +- Internal claim ledger + S5 plan exist and are mostly honest. + +--- + +## Recommended action order + +1. **P0.1–P0.3** race/expand runtime (semantics + graft + usage). +2. **P0.4–P0.6** DSL register/silent drop/decompile. +3. **P1.10** version/publish honesty banner. +4. **S5.0** parity harness only after P0 runtime dual-path inventory (arch). +5. Defer kernel default ON until P1.7–P1.8 closed. + +## Agent artifacts + +| Dimension | File | +|-----------|------| +| Architecture | `/tmp/grok-ma-review-arch-c1d88a29.md` | +| Race/expand | `/tmp/grok-ma-review-race-c1d88a29.md` | +| DSL erase | `/tmp/grok-ma-review-dsl-c1d88a29.md` | +| Docs honesty | `/tmp/grok-ma-review-docs-c1d88a29.md` | diff --git a/docs/internal/rfc-flowir-compilation.md b/docs/internal/rfc-flowir-compilation.md index 48fe423..d1df48c 100644 --- a/docs/internal/rfc-flowir-compilation.md +++ b/docs/internal/rfc-flowir-compilation.md @@ -1,8 +1,14 @@ # RFC: FlowIR Compilation Shadow (DSL → overstory FlowIR) -> Status: **Proposed** · Date: 2026-06-24 -> Roadmap: [`overstory-convergence-roadmap.md`](./overstory-convergence-roadmap.md) §3 M1 (first of 5 milestones toward an overstory-kernel swap) -> Verifies: `overstory/packages/core/src/ir/` (vendored); bridges to `extensions/{schema,compile,verify}.ts` +> Status: **Superseded in part by 0.2.0 S0** · Original: **Proposed** · Date: 2026-06-24 +> Roadmap: [`overstory-convergence-roadmap.md`](./overstory-convergence-roadmap.md) §3 M1 +> **Implementation note (2026-07-09):** The *shadow* / vendor-overstory plan below is **historical**. +> 0.2.0 S0 shipped a **self-owned** genuine compiler in `packages/taskflow-core/src/flowir/`: +> `compileTaskflowToFlowIR` + `hashFlowIR` → `ir:<64-hex>`, `usedFallbackHash: false` on well-formed IR. +> Public seam remains `compileTaskflowToIR(def)`. Stub `translate.ts` still exists for comparison / +> legacy paths but is **not** what `/tf ir` content-addresses after S0. +> Architecture parent: [`../rfc-0.2.0-architecture.md`](../rfc-0.2.0-architecture.md). +> Verifies (original intent): overstory IR contract; bridges to schema/compile/verify. ## TL;DR diff --git a/docs/internal/s5-kernel-default-on-plan.md b/docs/internal/s5-kernel-default-on-plan.md new file mode 100644 index 0000000..94a45a6 --- /dev/null +++ b/docs/internal/s5-kernel-default-on-plan.md @@ -0,0 +1,124 @@ +# S5 plan — event kernel default ON (differential strangler) + +> Status: **PLAN** · 2026-07-09 · Branch `release/0.2.0` +> Parent: [`rfc-0.2.0-architecture.md`](../rfc-0.2.0-architecture.md) §9 S5 +> Prerequisite: S0–S4 landed; claim ledger `claim-vs-impl-0.2.0.md` + +## Goal + +1. **Default execution path** = `exec/driver` (event kernel), not imperative `executePhaseInner`. +2. **Parity**: kernel outcomes match imperative for every admitted flow (status, finalOutput shape, gate/budget/when decisions). +3. **Safety**: advanced features still force imperative fallback until handlers exist. +4. **Flagship gate**: incremental recompute cost demo (Monday $6 → Tuesday ≤$0.40 narrative) measured or explicitly deferred with honest wording. + +## Current baseline (do not regress) + +| Fact | Location | +|------|----------| +| Kernel **opt-in** (`eventKernel` / `PI_TASKFLOW_EVENT_KERNEL=1`) | `exec/driver.ts` `eventKernelEnabled` | +| Kernel kinds = `PHASE_TYPES − {race, expand}` (10) | `exec/step.ts` `EVENT_KERNEL_PHASE_TYPES` | +| Fallback reasons (score, retry, expect, reflexion, cross-run cache, shareContext, onBlock:retry) | `exec/kernel-policy.ts` | +| Imperative path remains full 12 kinds | `runtime.ts` | +| Default OFF | env unset | + +## Workstreams + +### S5.0 — Differential harness (ship first) + +- [ ] Golden suite: for each fixture under `test/fixtures/kernel-parity/` run **twice** (kernel on / off) with the same mock `runTask`. +- [ ] Assert: `status`, per-phase `status`/`output`/`gate`/`error` (normalize volatile fields: timestamps, runIds). +- [ ] Fixtures minimum set: + - linear agent → gate → reduce + - map + parallel + - loop until + - tournament best + - script + - flow{use} + flow{def} (dynamic empty + small plan) + - when + join any + - budget hit mid-map +- [ ] CI job: `pnpm run test:kernel-parity` fails the release if any fixture diverges. + +### S5.1 — Close kernel feature gaps (priority order) + +| Priority | Gap | Approach | +|----------|-----|----------| +| P0 | parity harness green on admitted set | S5.0 | +| P1 | `expect` contracts on agent/gate/reduce/loop | port contractCheck into step body | +| P1 | explicit `retry` | reuse runOne retry curve in step or shared helper | +| P2 | score gates | port scorers path or keep fallback (document) | +| P2 | reflexion loops | keep fallback until designed | +| P2 | cross-run cache in kernel | optional: call CacheStore from driver | +| P3 | shareContext | keep fallback (complex) | +| P3 | `race` / `expand` kernel handlers | new step kinds; or keep imperative forever | + +**Policy:** S5 default ON only requires **parity on canUseEventKernel flows**. Flows that hit `kernelUnsupportedReason` stay on imperative **without** env flip required. + +### S5.2 — Default flip + +1. Change `eventKernelEnabled` default: + - `undefined` → **true** (or env `PI_TASKFLOW_EVENT_KERNEL` default `"1"`). + - Explicit `eventKernel: false` or env `0`/`false` keeps imperative. +2. Document migration: hosts that relied on imperative-only bugs must set `false`. +3. CHANGELOG: **breaking** if behavior differs; otherwise minor. + +### S5.3 — Runtime strangler (enables safe flip) + +Continue peel `runtime.ts` → `runtime/phases/*` so kernel step handlers and imperative share pure helpers: + +| Kind | Imperative module | Kernel step | +|------|-------------------|-------------| +| race | ✅ `phases/race.ts` | optional later | +| expand helpers | ✅ `phases/expand.ts` | optional later | +| script / parallel / approval | ✅ peeled | share spawn/merge helpers | +| map / loop / tournament | ⬜ | extract before rewriting step | +| agent / gate / reduce | ⬜ shared body | de-dupe with step-kinds | + +### S5.4 — Flagship cost demo (acceptance) + +- [ ] Scripted flow (8 agent phases) + fingerprint change of one file. +- [ ] Measure: full run cost vs recompute cost ratio. +- [ ] Pass if recompute token/$ **strictly <** full (target narrative ≤ ~$0.40 vs ~$6 is aspirational — record real numbers). +- [ ] If infra cannot produce stable $ without live models: gate on **phase count re-executed** + cache hit counts only. + +### S5.5 — Retirement (post-default) + +- [ ] Mark imperative path as fallback-only in docs. +- [ ] No deletion of executePhase in same release as default flip. +- [ ] Next minor: reduce dual-path surface after 1 release of green parity. + +## Non-goals (S5) + +- Native multi-node FlowIR lowering (parallel → N IR nodes). +- Literal decompile round-trip. +- Host auto-build of `.tf.ts`. +- Experimental C-track runes. + +## Exit criteria (S5 done) + +1. Default ON in core; all host adapters inherit. +2. `test:kernel-parity` green in CI. +3. No known P0 divergence bugs open. +4. Flagship recompute metric recorded (or explicitly deferred with version note). +5. Docs/skills: “kernel default ON; race/expand + advanced features may still use imperative”. + +## Suggested PR stack + +1. **S5.0** parity harness + fixtures (no behavior change). +2. **S5.1-P1** expect + retry on kernel path. +3. **S5.3** map/loop peel (optional parallel). +4. **S5.2** default flip behind `PI_TASKFLOW_EVENT_KERNEL` still overridable. +5. **S5.4** demo script + numbers. +6. Docs/CHANGELOG/skills final. + +## Risks + +| Risk | Mitigation | +|------|------------| +| Silent behavior change on default flip | parity harness + canary env | +| Host runners ignore AbortSignal (race cancel) | already best-effort; document | +| Dual path drift after flip | single shared helpers in `phases/` + `step-kinds` | +| Cost demo unstable without live models | phase-count metric fallback | + +## Immediate next coding step + +Implement **S5.0** only: `packages/taskflow-core/test/kernel-parity/*.test.ts` + fixtures, no default flip. diff --git a/docs/market-positioning-2026-07.md b/docs/market-positioning-2026-07.md new file mode 100644 index 0000000..05a5345 --- /dev/null +++ b/docs/market-positioning-2026-07.md @@ -0,0 +1,156 @@ +# 全网竞品调研:taskflow 在 2026-07 的真实市场位置 + +> Research: 2026-07-07. Sources: Claude Code docs, Microsoft Conductor blog/repo, +> awesome-agent-orchestrators list (100+ projects), Augment Code's "9 Open-Source +> Agent Orchestrators", arxiv (MCP Workflow Engine), LangGraph/CrewAI comparisons. +> Fetched via `explore-cli`. Honest positioning — not marketing. + +--- + +## 0. 一句话定位 + +**taskflow 处在一个极度拥挤的红海。** 它所在的细分赛道("声明式 DAG 编排引擎") +在 2026 年已经有 **Microsoft Conductor** —— 一个微软官方、MIT、能力几乎完全覆盖 +taskflow 核心主张的竞品。**taskflow 的技术不输第一梯队,但没有微软的分发与背书, +这是一个"技术并列、市场危险"的位置。** + +## 1. 赛道其实是三层 —— 大多数"竞品"和 taskflow 不在一个层 + +`awesome-agent-orchestrators` 列了 100+ 项目,但仔细分,它们分属三个完全不同的层: + +### 第 1 层:会话并行器 / Session Runners(列表 80%+) +代表:crystal, claude-squad, agentbox, nimbalyst, vibe-kanban, constellagent, +thurbox, dmux, parallel-code, Agent Command Center… +- 核心:**在 git worktree 里并行跑多个 coding agent CLI 会话**(Claude Code / Codex / Gemini) +- 编排对象 = **进程/会话**,不是任务图 +- **没有声明式 DAG、没有 phase 类型、没有 gate/tournament/loop** +- 是 TUI/dashboard 层,不是编排引擎层 +- **→ 不是 taskflow 的直接竞品**(但抢同一批用户的注意力) + +### 第 2 层:声明式 DAG 编排引擎(taskflow 真正的同类) +这是 taskflow 的战场,目前公开的玩家只有少数几个: + +| 项目 | 背书 | 格式 | 核心主张 | taskflow 对比 | +|---|---|---|---|---| +| **Microsoft Conductor** | **微软官方** | YAML | 声明式 DAG、零token编排、`validate`、parallel/map/script/human-gate/loop、context tier、web dashboard、registry | **几乎完全覆盖 taskflow 主张**;多 Copilot 支持 | +| **bernstein** | 个人 | config | "Deterministic orchestrator, parallel agents, verifies with tests, **Zero LLM tokens on coordination**" | 同样主打零token + 确定性 | +| **tutti** | 个人 | config | "config-driven workflows, typed artifact flow between agents" | 类型化产物流 | +| **skillfold** | 个人 | YAML | "Configuration language + compiler for multi-agent pipelines, compiles YAML into agent skills" | 编译器视角 | +| **MCP Workflow Engine** | 论文 | JSON | "declarative workflow blueprint — JSON directed sequence of MCP tool calls" | MCP-native | + +### 第 3 层:Swarms / 自治循环 +代表:loki-mode(41 agents/8 swarms/9 quality gates), claude-flow, orc, guild, Hephaestus… +- 多 agent 协作框架,偏向**动态/自治**,常带 ralph-loop(跑到完成) +- 与 taskflow 部分重叠,但理念不同(动态 vs 声明式) + +--- + +## 2. 直接威胁:Microsoft Conductor —— taskflow 的"镜像" + +Conductor(2026-05 微软开源)和 taskflow 的主张**几乎一一对应**: + +| taskflow 核心主张 | Microsoft Conductor | +|---|---| +| 声明式 DAG(JSON DSL) | ✅ 声明式 DAG(**YAML**) | +| 编排层零 token | ✅ "routing layer consumes zero tokens"(Jinja2 + expr eval) | +| 运行前验证(`verify`/`compile`) | ✅ `conductor validate` + dry-run | +| 上下文隔离 + 显式流 | ✅ 隔离 + **三种 context tier**(accumulate/last_only/explicit) | +| parallel + 动态扇出(`map`) | ✅ static parallel groups + `for each`(动态数组,批量并发) | +| `script` phase(零token shell) | ✅ script steps(shell,捕获 stdout/stderr/exit) | +| `approval`(人机交互) | ✅ human gates(Rich TUI **+ web dashboard**) | +| `loop`(循环/收敛) | ✅ loop-back / evaluator-optimizer(数百次) | +| flow library / search | ✅ workflow registries(团队共享 + version) | +| safety(budget / loop cap) | ✅ max iterations + wall-clock timeout | +| 多 provider/model | ✅ **Copilot + Claude**(per-agent) | +| 让 coding agent 帮写 flow | ✅ ships a Claude skill | +| **背书 / 分发** | ✅ **微软、MIT、microsoft/conductor** | +| **Web 可视化** | ✅ **实时 DAG dashboard**(点节点看 prompt/token/cost) | + +> Conductor 在 taskflow 的**每一个核心主张**上都有对等能力,且多了: +> **微软背书、web dashboard、context tier、Copilot 集成**。 + +--- + +## 3. taskflow 在第 2 层里**仍然独有**的东西(真正的护城河) + +把 Conductor/bernstein/tutti/skillfold 摆一起,taskflow **真正只有别人没有的**: + +### 🥇 跨运行内容寻址缓存 + 增量重算(最硬的护城河) +- `cache` + `why-stale` + `recompute`:git/glob/file/env 指纹 → 内容寻址缓存 + TTL +- **Conductor / bernstein / tutti / skillfold 文档均未提及任何缓存机制** +- 这是 taskflow 唯一一个"别人完全没有、且工程上很难抄"的能力 +- 价值:重跑类似任务时跳过未变 phase —— 长 audit / 迁移 / 研究场景的成本碾压 + +### 🥈 tournament(best-of-N + judge) +- 多个竞品变体 + 裁判选最优/聚合 +- 其他第 2 层项目都是 agent/parallel/script/gate,**没有 first-class 的 tournament** +- 价值:需要"多角度起草 + 选优"的场景(规划、文案、方案对比) + +### 🥉 嵌入 4 个 host 作为原生插件 +- pi / codex / claude / opencode —— taskflow 编排的是**真正能读写文件、跑命令的 coding subagent** +- Conductor 是**独立 CLI 调 API agent**(文件操作要靠 MCP) +- 但这个差异在缩小(Conductor 也接 MCP + script steps) + +### phase 类型更丰富 +- taskflow 12 种(agent/parallel/map/gate/reduce/approval/flow/loop/tournament/script/race/expand) +- Conductor ≈ agent/parallel/script/human-gate(+ routes) +- 但 Conductor 的 `routes`(Jinja2 条件)很灵活,能模拟不少 + +> **诚实判断:护城河真实存在,但很窄。** 缓存是最硬的一块;tournament/多host是加分项。 +> 如果 taskflow 不把"缓存 + 增量重算"做成**明显的、用户可感知的优势**,会被 Conductor 的分发碾压。 + +--- + +## 4. 定位结论 + +``` + 分发/背书/可见性 + ▲ + Conductor ● │ + │ ← 微软在这,taskflow 远在下方 + │ + taskflow ● │ + │ + bernstein ● tutti ● │ + │ + ───────────────────────────┼─────────────────────▶ 编排引擎深度 + │ + │ taskflow ●──── 接近顶端(12 phase + 缓存) + │ Conductor ●─── 也很高 + │ +``` + +- **引擎深度**:taskflow ≈ 第一梯队(和 Conductor 并列,phase 类型更丰富,缓存独有) +- **分发/背书**:taskflow **远远落后**于微软 Conductor +- **赛道拥挤度**:**红海**(100+ 项目,第 2 层已有 5 个声明式 DAG 引擎) + +**一句话:taskflow 是一个"技术领先、但面临微软正面对标 + 赛道极度拥挤"的项目。** + +--- + +## 5. 这对 0.1.7 / 未来方向意味着什么 + +三条可能的路(按激进程度): + +### 路线 A:死守护城河 —— 把"缓存 + 增量重算"做成杀手锏 +- 0.1.7 把 cache/why-stale/recompute 做成**一等公民 UX**:一个命令看清"什么变了、 + 什么能复用、省了多少 token/钱",甚至和 Conductor 做个公开 benchmark。 +- 定位语:"Conductor 重新跑一遍要全花 token;taskflow 只重算变的部分。" +- 风险:护城河窄,微软哪天加个缓存就没了。 + +### 路线 B:换战场 —— 不和 Conductor 比"引擎",比"嵌入 coding agent 的体验" +- Conductor 是独立 CLI;taskflow 是 pi/codex/claude/opencode/grok 里的**原生编排**。 +- 0.1.7 投入"在 host 里用起来最顺"(更好的 TUI、`/tf:` 命令、和 host skill 融合、 + 内置 deep-research/audit flow 让用户装完即用)。 +- 定位语:"不是又一个 CLI,是你现在的 coding agent 长出来的编排能力。" +- 风险:第 1 层(session runners)也在抢这个位置。 + +### 路线 C:差异化品类 —— 走"可验证 + 可缓存的工作流"这个 Conductor 没占的细分 +- taskflow 有 `verify`(零token静态分析)和 `cache`。把它俩绑成一个**新品类主张**: + "the only orchestrator that **verifies before it spends, and remembers what it spent**"。 +- 这避开了和 Conductor 在"声明式 DAG"上的正面参数对比,占一个心智位置。 +- 这需要产品叙事 + 一个能演示这个差异的杀手级 flow(比如增量重跑 500 文件迁移)。 + +> **我的建议:路线 C 为主,B 为辅。** A 单独撑不住(护城河太窄),B 是执行层面, +> C 是唯一能建立心智壁垒的方向。0.1.7 的 feature 应该围绕"可验证 + 可缓存"讲故事 +> 并配一个让用户**立刻感知到差异**的内置 flow。 diff --git a/docs/opencode-mcp.md b/docs/opencode-mcp.md index 8885307..12938b8 100644 --- a/docs/opencode-mcp.md +++ b/docs/opencode-mcp.md @@ -13,9 +13,9 @@ on the host-neutral `SubagentRunner` seam the `opencode run` subagent runner (`packages/opencode-taskflow/src/mcp/`). This is the direction described here. -The MCP server is dependency-free: it speaks JSON-RPC 2.0 over stdio on Node -built-ins (`packages/taskflow-mcp-core/src/mcp/jsonrpc.ts`), so taskflow keeps its -**zero runtime dependencies** guarantee — no `@modelcontextprotocol/sdk`. +Requires **Node.js ≥ 22.19.0**. The MCP protocol layer speaks JSON-RPC 2.0 over +stdio without `@modelcontextprotocol/sdk`; published delivery packages still +depend on the internal taskflow packages, and core peers on `typebox`. ## Install: register the MCP server @@ -76,13 +76,19 @@ injected via the `OPENCODE_CONFIG_CONTENT` env var. The runner maps each phase's tool whitelist the same way the codex runner maps to a sandbox mode: - **Read-only phase** (no `write`/`edit`/`bash` in the phase/agent `tools`) → a - permission policy that **denies** bash/write/edit is injected, so a denied - tool call is genuinely rejected (not merely un-approved). This is real - enforcement, closer to codex's read-only OS sandbox than to an advisory - whitelist. -- **Mutating phase** (or no whitelist) → `opencode run --auto`, which - auto-approves every permission — the workspace-write analogue. Run flows you - trust, ideally in a throwaway worktree (`cwd: "worktree"`). + default-deny permission policy is injected: only the known read/list/search + built-ins are allowed, so inherited custom and MCP tools are denied too. + Every child also uses `--pure`, disabling external plugins that execute + outside the tool permission policy. +- **Mutating phase** (or no whitelist) → rejected by default because OpenCode + has no OS sandbox backstop. A trusted operator may explicitly set + `PI_TASKFLOW_OPENCODE_UNSAFE_AUTO=1`; only then does the runner add `--auto`. + Prefer a throwaway worktree (`cwd: "worktree"`). + +OpenCode children receive only platform/proxy/CA, OpenCode configuration, and +supported provider environment variables; unrelated parent secrets are +removed. Explicit task credentials can be added by name through the +comma-separated `PI_TASKFLOW_CHILD_ENV_ALLOW` operator setting. ## Model ids @@ -107,6 +113,10 @@ OpenCode's configured default model. | `taskflow_verify` | Statically verify a flow (cycles, missing deps, undefined refs) — no execution. | | `taskflow_compile` | Render a flow's DAG as a text outline + a compact status line (with an inline SVG image for clients that render images). | | `taskflow_peek` | Inspect one phase's intermediate output from a stored run (post-hoc debugging). Hard-truncated, read-only. | +| `taskflow_trace` | Read-only timeline of a run's append-only event log (subagent I/O + runtime decisions). | +| `taskflow_replay` | Offline what-if on a recorded trace: re-judge thresholds/budget/models **without calling the model** (zero tokens). | +| `taskflow_why_stale` | Explain observed/declared dependency staleness for a run (optional seed `phaseId`). Zero tokens. | +| `taskflow_recompute` | Report the stale frontier for a seed phase (**dry-run only** over MCP — never spends tokens). | ## Use it @@ -119,7 +129,7 @@ Inside an OpenCode session, just ask — OpenCode will call the tools: ``` > **Note on approvals.** MCP-driven runs are non-interactive, so an `approval` -> phase **auto-rejects** (fail-open). Prefer a `gate` (agent review) in flows +> phase **auto-rejects** (fail-closed for the approval decision). Prefer a `gate` (agent review) in flows > you run through the `taskflow_*` tools; use `approval` only in flows a human > runs interactively. diff --git a/docs/rfc-0.2.0-architecture.md b/docs/rfc-0.2.0-architecture.md new file mode 100644 index 0000000..909fadf --- /dev/null +++ b/docs/rfc-0.2.0-architecture.md @@ -0,0 +1,376 @@ +# RFC: taskflow 0.2.0 系统架构总纲(Master Architecture) + +> Status: **Active** · Draft v1 2026-07-08 · **Implementation progress updated 2026-07-09** (S4 ✅) +> 层级:这是凌驾于 [`rfc-0.2.0-dsl-syntax.md`](./rfc-0.2.0-dsl-syntax.md)(DSL 前端)和 +> replay 实现之上的**系统架构总纲**。定调 0.2.0 内核形态、模块边界、数据契约、迁移顺序。 +> 后续所有 0.2.0 实现 RFC 以本文为锚。 +> 关联:[`0.2.0-north-star.md`](./0.2.0-north-star.md)(方向)、 +> [`internal/overstory-convergence-roadmap.md`](./internal/overstory-convergence-roadmap.md)(M1-M5 内核路线)、 +> [`rfc-0.2.0-three-compile-routes.md`](./rfc-0.2.0-three-compile-routes.md)(DSL 路线之争)、 +> [`rfc-0.2.0-s4-mvp.md`](./rfc-0.2.0-s4-mvp.md) / [`rfc-0.2.0-s4-decision-record.md`](./rfc-0.2.0-s4-decision-record.md)(S4 表面)。 +> +> **本文回应两个已发现的架构级矛盾**(§0),并把 5 个已拍板的决策(§1)固化成一套 +> 可发布、向后兼容的内核替换方案。 +> +> ### Implementation status (branch `release/0.2.0`) +> +> | 阶段 | 状态 | 落地要点 | +> |------|------|----------| +> | **S0** | ✅ | `compileTaskflowToFlowIR` + `hashFlowIR` → `ir:<64-hex>`;`usedFallbackHash: false`(well-formed IR) | +> | **S1** | ✅ | `exec/{events,fold}`;runtime 全量 decision emit;fold 差分 + kill-9 rebuild 测试 | +> | **S2** | ✅ | `exec/{step,driver}` **全部 10 kind** + P0 硬化;高级特性 fall-back;默认 OFF。**`race`/`expand` 仍走 imperative**(未进 `EVENT_KERNEL_PHASE_TYPES`) | +> | **S3** | ✅ | `replayRun`;`taskflow_replay` MCP;pi `action=replay` + `/tf replay`;golden + import-lint | +> | **S4** | ✅ | 包 `taskflow-dsl` live:`build`/`check`/`decompile`/`new`;erase `kinds/*` 注册表;parity 测绿。引擎 **12 PHASE_TYPES**(+`race`/`expand`);gate sugar;skills 对齐 | +> | **S5** | ⬜ **下一主线** | 全 kind 差分绿 → kernel 默认 ON;退休旧 `executePhase` 主路径。计划:[`internal/s5-kernel-default-on-plan.md`](./internal/s5-kernel-default-on-plan.md);预热:`runtime/phases/*` strangler | +> +> North-star 口号:**compiled · resumable · incremental · replayable-for-what-if**。 + +--- + +## TL;DR(结论先行) + +1. **0.2.0 是一次内核替换**:`runtime` 从"解释执行 Taskflow schema"升级为"**执行 FlowIR 的事件溯源内核**"。FlowIR 从"哈希影子"毕业成**规范编译产物**(默认执行仍以 Taskflow 为主,经 S2 绞杀逐步迁到 driver)。 +2. **真编译器自研(S0 ✅)**:在 `taskflow-core` 内自建 `flowir/{schema,compile,hash,cond}`,well-formed flow 上 `usedFallbackHash` 为 `false`。 +3. **一个机制长出四样能力**:event log → `trace`、`fold`、`resume`、`确定性重放`、`增量重算`。消解 north-star "resumable vs replayable" 假对立。 +4. **绞杀式迁移**:新旧内核并存 + 差分 + 逐 kind flip(S0–S5 每步可发布)。 +5. **replay 重新纳入 scope(S3 ✅)**:兑现 0.1.7 承诺;`replayRun` + MCP/pi 已落地。 +6. **向后兼容是硬约束**:JSON flow / RunState / `/tf` 语义不破坏。 +7. **DSL 是平行前端(S4 ✅)**:`.tf.ts → build → Taskflow → FlowIR`;包 `taskflow-dsl` 已落地(CLI-first;无新 MCP)。 + +--- + +## §0. 背景:为什么需要这份 RFC(两个矛盾) + +### 矛盾 1:DSL RFC 说"runtime 执行 FlowIR",但 runtime 执行的是 Taskflow + +- **起草时现状(2026-07-08)**:`executeTaskflow(state, deps)` 中 `state.def: Taskflow`。FlowIR 仅经 `compileTaskflowToIR` 做分析/哈希;stub `translate` 路径上 `usedFallbackHash` 恒 true。 +- **实现后(2026-07-09,S0 ✅)**:真编译器 `compileTaskflowToFlowIR` + `hashFlowIR` 产出 `ir:<64-hex>`,well-formed flow 上 `usedFallbackHash: false`。FlowIR 已是**规范编译/内容寻址产物**(供 `/tf ir`、cache fingerprint、declaredDeps)。**默认执行路径仍是 Taskflow + imperative `executePhase`**;S2 event kernel(默认 OFF)对 agent|script|map|parallel 可走 `exec/driver`。 +- **DSL RFC v2 §0.2** 写的是"`taskflow build` → FlowIR,runtime 执行 FlowIR"——**S5 才把默认执行翻到 FlowIR/driver**。 +- **overstory roadmap §6.4** 的"大爆炸"风险由 **Q7 绞杀迁移**化解。 + +→ 本 RFC 拍板:FlowIR **是**规范执行/编译真源方向(Q2=B),用绞杀守住可发布性(Q7)。**S0–S4 已兑现**;**S5(kernel 默认 ON)为下一主线**。 + +### 矛盾 2:north-star 丢弃了 replay,但 0.1.7 公开承诺过 + +- **0.1.7 CHANGELOG 原文**:确定性重放"…which **lands in 0.2.0**…"。 +- **north-star(07-07)** 一度借 Qwik 的 **"resumable (not replayable)"** 反向定位,与决策重放撞名。 +- **已解决(S3 ✅ + north-star 修订)**:口号改为 **compiled · resumable · incremental · replayable-for-what-if**;`replayRun` + MCP/pi 表面已落地。Qwik 的 replayable(hydration)≠ taskflow 的决策 what-if。 + +--- + +## §1. 已锁定的架构决策(Decision Record) + +| # | Fork | 结论 | 理由 | +|---|---|---|---| +| **Q2** | FlowIR 是否执行产物 | **B — 是**。runtime 通过事件溯源内核执行 FlowIR | 让 trace/resume/replay/recompute 从一个机制长出(§2);长期对齐 overstory | +| **Q5** | 真编译器来源 | **own — 在 core 自研**(不 vendor overstory `ir/`) | 零外部耦合、无 `private:0.1.0-dev` 漂移债、完全自控;代价=自实现 hash/cond + 自建正确性测试 | +| **Q6** | 状态模型 | **不收敛** RunState→RunTree。RunState 保持公开持久化面(= fold 产物) | 守 published `RunState.json` 向后兼容;内部用 event log,外部面不变 | +| **Q7** | 迁移方式 | **绞杀式**:新旧内核并存 + 差分测试 + 逐 node kind flip | 把"大爆炸内核替换"拆成每步可发布,中和 roadmap 唯一反对 | +| **Q8** | event log 与 trace 关系 | **log 即 trace**。replay=重 fold;0.1.7 "trace 只接 30%" 的问题溶解 | 事件溯源内核按构造发出每个决策事件,不需再补 5 条死线 | + +--- + +## §2. 核心洞察:事件溯源统一 trace / resume / replay / recompute + +选 B 的真正价值不在"能写 DSL",而在**执行模型换成事件溯源后,四样能力从一个机制自然长出**: + +``` + event log —— driver 按构造发出的每个事件(subagent 调用 + 每个决策) + │ + ┌──────────┬───┼───────────┬──────────────┬────────────────┐ + ▼ ▼ ▼ ▼ ▼ ▼ + trace RunState resume 确定性重放 增量重算 +(=log 本身) (=fold(log)) (=重放 log) (=换旋钮重fold,0token) (=stale 定点,M5) +``` + +- **trace**:不再是"只接了 30% 的旁路"(今天 `trace.ts` 定义 7 种决策事件,`runtime.ts` 只 emit 2 种:`gate-verdict`+`unreplayable`;`gate-score`/`budget-hit`/`when-guard`/`tournament-winner`/`cache-hit` 定义了但 0 次 emit)。事件溯源内核**按构造**发出每个事件——**F3"补 5 条死线"的问题直接溶解**。 +- **RunState = fold(event log)**:当前 phase 状态是对事件流的 reduce。 +- **resume = 重放 log 重建状态**(Qwik 的 "resumable")。 +- **确定性重放 = 拿同一条 log、换决策旋钮(gate 阈值 / budget / 模型路由)重新 fold**,对录制的 subagent 输出重新裁决,**零 token、绝不调模型**(Temporal 的 "replay")。 +- **增量重算 = driver 的 stale-frontier 定点**(M5 已落地)。 + +> **这一举消解 north-star 的 "resumable vs replayable" 矛盾**:在事件溯源下,resume 和 replay 是**同一条 log 的两种 fold**,不对立。你要的"0.2.0 继续 replay"在 B 架构里是内核的天然能力,不是 bolt-on。 + +--- + +## §3. 整体架构(层 + 数据契约) + +``` + 作者前端 flow.json ─parse─┐ flow.tf.ts ─build(AST transform)─┐ + (Authoring) │ │ ← taskflow-dsl 包 + ▼ ▼ + ┌───────────────────────────────────────────────────────┐ + 规范作者 schema │ Taskflow schema (作者面 DSL, 向后兼容, desugar) │ schema.ts + (契约①) └───────────────────────────┬───────────────────────────┘ + │ compile(★自研真编译器, Q5) + ▼ + ┌───────────────────────────────────────────────────────┐ + 规范执行产物 │ ★ FlowIR — 内容寻址 / hash / node·edge / inject·emits │ flowir/ + (契约②, 唯一执行真源) └──────────────┬──────────────────────────┬───────────────┘ + (0 token) │ executeFlowIR │ (0 token) + verify/compile/diff ▼ │ + ┌──────────────────────────────────┐│ + 事件溯源内核 │ exec/ driver 定点循环 + step 派发 ││ + (执行) │ ├─ 发 EVENT LOG ═════════════╗ ││ + │ ├─ observed readSet@version ║ ││ ← 契约③ event log + │ └─ usage/cost (rates.ts) ║ ││ + └──────────────┬─────────────────╨───┘│ + fold │ │ log │ + ▼ ▼ │ + 状态 + 后执行 ┌─────────────────┐ ┌──────────────────────────┴────┐ + (契约④ RunState) │ RunState │ │ replay.ts 重fold(log,新旋钮) │ 纯,0token + │ (兼容持久化面) │ │ recompute/stale.ts (M5) │ + └─────────────────┘ └───────────────────────────────┘ +``` + +### 四个数据契约(模块间的连接组织) + +| 契约 | 是什么 | 谁产 | 谁消费 | 向后兼容 | +|---|---|---|---|---| +| ① **Taskflow** | 作者面 schema(现有);JSON 与 DSL 的共同落点 | `parse`(JSON) / `build`(DSL) | 编译器 | 现有 JSON flow 零修改 | +| ② **FlowIR** | 规范执行产物,内容寻址 | `flowir/compile` | 内核 / verify / compile / diff / cache | 新契约(0.2.0 引入为执行真源) | +| ③ **Event log** | append-only 事件流(= trace) | `exec/driver` | fold / replay / recompute / OTel(未来) | 向后兼容读旧 trace(`readTrace` 已容错) | +| ④ **RunState** | 当前状态快照(= fold 产物) | `exec/fold` | resume / `/tf runs` / persist | **published `RunState.json` 必须可加载** | + +--- + +## §4. 模块分解与依赖关系 + +### 4.1 taskflow-core 模块图(零运行时依赖不变) + +``` + ┌─────────────┐ 两个前端都汇到这里 + flow.json ──────▶│ schema.ts │◀────── taskflow-dsl(build) + │ (Taskflow) │ + └──────┬──────┘ + │ ★ Q5 自研真编译器 + ┌─────────────▼──────────────────────────────┐ + │ flowir/ │ + │ schema.ts 规范 FlowIR 类型(node/edge/kind/inject/emits) 【新】 + │ compile.ts Taskflow→FlowIR(lowering+归一化) 【translate.ts 毕业】 + │ hash.ts 内容寻址哈希(序/空白无关) 【现有→自研真算法】 + │ cond.ts when/until/eval→归一化条件 IR 【新, 与 interpolate 共享】 + │ meta.ts / phasefp.ts 【现有: declaredDeps / phase fp】 + └──────┬──────────────────────────────────────┘ + │ FlowIR + ┌───────────────┼────────────────────────────────────────┐ + │ (0 token) │ executeFlowIR │ + ▼ ▼ │ + verify.ts ┌──────────────────────────────────────────┐ │ + compile.ts │ exec/ │ │ + (diff.ts 可选)│ driver.ts 事件溯源定点循环(schedule+fold) │ │ + │ step.ts 10 种 node kind 派发 │─emits─┐ + │ events.ts 事件(log) schema ◀════════════════════╝ 【吸收 trace.ts, Q8】 + │ fold.ts reduce(log)→RunState │ │ + └──────┬─────────────────────┬────────────────┘ │ + fold │ │ log │ + ▼ ▼ │ + RunState(store.ts) ┌──────────────────────────────────┴─┐ + │ │ replay.ts 重fold(log,新旋钮) │ 纯,0token + ▼ │ recompute/stale.ts (M5, key on hash)│ + ┌─────────────────┐ └─────────────────────────────────────┘ + │ rates.ts (F1) │ ← usage/cost 注入 step; codex 补 cost; replay 反事实计价 + │ cache.ts (v3) │ ← key on flowir/hash.ts + └─────────────────┘ + + runtime.ts ── 收缩为「绞杀开关」: 旧 executePhase(flag) ⇄ 新 exec/driver(flag) +``` + +### 4.2 依赖约束(结构性护栏,import 图强制) + +1. **`replay.ts` 只 import 纯模块**:`events`(读 log)+ `flowir/{schema,cond}` + `deterministic`(`parseGateVerdict`/`overBudget`)+ `scorers`(纯评分器重跑)+ `rates`。**绝不 import `exec/driver`**(那会拖进 process-spawning runner)。→ "replay 永不花 token" 由 import 图**结构性**保证。这与 `replay.ts` 现有 docstring 已声明的约束一致。 +2. **`flowir/compile` 是 Taskflow→FlowIR 的唯一入口**:JSON 与 DSL 都经它,保证两个前端产出同一 FlowIR。 +3. **`exec/step` 依赖 host 注入的 `SubagentRunner`**(`RuntimeDeps.runTask`);`events`/`interpolate`/`scorers`/`scorer-runtime`。 +4. **core 零运行时依赖不变**:`flowir/*`、`exec/*`、`replay.ts`、`rates.ts` 全是纯模块(仅 typebox)。 +5. **`taskflow-core` 永不 import host SDK**(`@earendil-works/*`)——不变。 + +### 4.3 新包 taskflow-dsl(9 packages + website) + +| 内容 | 说明 | +|---|---| +| rune 类型定义 | `flow/agent/map/gate/...` 的 TS 类型(编译指令,见 DSL RFC v2 §0) | +| `build` | AST transform:`.tf.ts` → Taskflow JSON(再经 core 的 `flowir/compile` → FlowIR) | +| `check` | 轻量校验(tsc + rune 签名 + 依赖完整性 + when 谓词子集) | +| `decompile` | FlowIR → `.tf.ts`(代码生成器,语义等价非字面 round-trip,DSL RFC v2 §6.2) | +| 依赖 | `taskflow-core`(Taskflow schema + verify)+ `typescript`(build-time;**因此不能进 core**——core 零依赖铁律) | + +> **为什么 DSL 编译器不能进 core**:它需要 AST 库(tsc transformer API)。core 的零运行时依赖铁律禁止。DSL 编译器本质是 build-time 工具(像 bundler 插件),独立成包,产出 Taskflow JSON。core 完全不知道 DSL 存在。 + +--- + +## §5. 自研 FlowIR 编译器(Q5=own 的具体范围) + +"真编译器" vs 今天的 stub 差在:stub 的 `usedFallbackHash=true`、`hash==flowDefHash`(对 JSON 文本哈希,非对 IR 结构哈希)。自研真编译器要交付: + +### 5.1 `flowir/schema.ts`(新)— 规范 FlowIR 类型 +- `FlowIRNode { id, kind, inject[], emits[], task?, condRef?, ... }`;`kind ∈ 10 种`(agent/parallel/map/gate/reduce/approval/flow/loop/tournament/script)。 +- `FlowIR { name, nodes[], edges[], budget?, concurrency?, ... }`。 +- **1:1 投影优先**:每个 Taskflow phase → 一个 FlowIR node。overstory 的 native 多节点 lowering(parallel→N siblings 等)**推迟**(见 §12),先保 hash 稳定。 + +### 5.2 `flowir/compile.ts`(`translate.ts` 毕业)— Taskflow → FlowIR +- lowering:phase → node(含 desugar 后的字段)。 +- 归一化:字段顺序、默认值、空白无关——保证"逻辑等价的 flow 产出同一 IR"。 +- declared readSet:`collectRefs` 过 task/over/when/until/eval/branches/with/context + `dependsOn` → `inject`/`emits`(现有 `meta.ts` 逻辑并入)。 + +### 5.3 `flowir/hash.ts`(现有 → 自研真算法)— 内容寻址哈希 +- **硬验收(roadmap M1 门)**:① 逻辑等价(含空白/顺序重排)⟹ 同 hash;② 单字段变更 ⟹ hash 变;③ 确定性(同输入永远同输出,跨进程)。 +- **自研而非 vendor**:我们定义哈希的规范化 + 序列化规则(如稳定 key 排序 + 规范 JSON + SHA-256),写自己的 property 测试。不追求与 overstory byte-parity(Q5=own 放弃了这个目标)。 +- cache key 升 **`v3:flowir:`** 前缀(现有 `v2:flowdef:` 3-tier lookup 保留一个 release 周期,见 §8)。 + +### 5.4 `flowir/cond.ts`(新)— 条件归一化 +- `when`/`until`/`eval` 表达式 → 归一化条件 IR,供 hash(结构等价)+ replay(重新求值 `when-guard`)+ DSL(谓词子集编译)共享。**与 `interpolate.ts` 共享代码路径**,避免语义漂移。 + +--- + +## §6. 事件溯源内核(exec/) + +### 6.1 `exec/events.ts`(吸收 trace.ts,Q8)— log schema +- 事件类型 = 现 `TraceEvent` 的超集:`phase-start`/`phase-end`/`subagent-call`/`decision`(7 种 decision:gate-verdict/gate-score/tournament-winner/budget-hit/cache-hit/when-guard/unreplayable)。 +- **加 `v`(schema 版本)字段**(现 `TraceEvent` 无版本字段——additive,replay 上线时可检测/迁移旧 log)。 +- driver **按构造**发出每个事件(不再是 `runtime.ts` 里手工插 emit 点)——这是 F3 溶解的机制。 + +### 6.2 `exec/driver.ts` — 事件溯源定点循环 +- 拓扑调度 ready 节点(现有 `schema.ts` topo sort)→ 调 `step()` → 收事件 → fold 更新状态 → 重新算 ready 集合,直到收敛/预算/中止。 +- `budget`/`abort`/`idle watchdog` 语义保留(现 `runtime.ts` 已有)。 +- 增量重算 = 这个定点循环 + stale frontier 作为初始 ready 集(M5 已有算法)。 + +### 6.3 `exec/step.ts` — 10 种 node kind 派发 +- 每个 kind 一个 handler(agent/script/map/parallel/reduce/gate/loop/tournament/approval/flow),从现 `executePhase` 的 10 个分支迁移而来。 +- handler 纯粹"执行一个 node → 发事件",不直接改状态(状态由 fold 派生)。 +- host 交互经 `RuntimeDeps.runTask`(不变)。 + +### 6.4 `exec/fold.ts` — reduce(log) → RunState +- `fold(events) → RunState`:把事件流归约成当前 phase 状态快照。 +- **RunState 保持现有公开形状**(Q6)——`persist`/`onProgress`/`/tf runs` 消费的还是 RunState,外部无感。 +- **resume** = 从持久化的 log 重新 fold(崩溃恢复 = 重放事件,非从 RunState 快照猜)。 + +--- + +## §7. 确定性重放(replay.ts) + +### 7.1 语义 +`replayRun(log, overrides) → ReplayDecision[]`:拿录制的 event log,在**决策旋钮**变化下重新 fold,对录制的 subagent 输出**重新裁决**,**零 token、绝不调模型**。 + +- `ReplayOverrides`(已存在于 `replay.ts`):`thresholds`(gate score 阈值)、`budgetMaxUSD`/`budgetMaxTokens`、`models`(只报 cost delta,需 `rates.ts`)、`args`(改文本的 phase → `needs-live-rerun`)。 +- `ReplayDecision`(已存在):`reused`/`verdict-flipped`/`would-block`/`would-exceed-budget`/`needs-live-rerun`/`would-skip`/`threshold-changed`/`failed`。 +- 实现 = 重放 log + 重求值:gate 阈值改 → 拿录制的 `gate-score` 事件里的 per-scorer 结果 + 新阈值重跑 `combineScores`(纯,`scorers.ts` 已有);budget 改 → 拿录制 usage + 新 cap 重跑 `overBudget`(`deterministic.ts` 已有);when 改 → 重求值 `when-guard`。 + +### 7.2 与 recompute 的孪生关系 +| | recompute(M5 ✅) | replay(0.2.0) | +|---|---|---| +| 触发 | *输入*变了(改文件) | *决策旋钮*变了(阈值/budget/模型) | +| 执行 | **在线**重跑 stale frontier | **离线**重 fold | +| 花费 | 花 token(只花变化节点) | **零 token** | +| 问 | "改了这文件要重跑哪些?" | "当初阈值 0.9 会 BLOCK 吗?" | + +两者共用 event log 证据,不共用执行逻辑——后执行层的一对孪生。 + +### 7.3 命名(修口号碰撞) +- 技术术语保留 **"deterministic replay"**(0.1.7 CHANGELOG 已用,准确)。 +- **修 north-star**:**已改** —— 口号为 **"compiled · resumable · incremental · replayable-for-what-if"**(见 `0.2.0-north-star.md`;Qwik 的 replayable 指 hydration,与决策 what-if 不同)。 + +--- + +## §8. 向后兼容(硬约束) + +| 面 | 约束 | 机制 | +|---|---|---| +| **JSON flow** | 现有 `.json` flow 零修改继续跑 | `parse`→Taskflow→`compile`→FlowIR;新增执行路径不改作者面 | +| **`RunState.json`** | published 旧 run 可加载 | Q6:RunState 保持公开形状;旧 run 走 resume,replay/recompute 对无 log 的旧 run 不可用(可接受,非破坏) | +| **`/tf` 命令 + DSL 语义** | 不破坏 | 绞杀开关默认走旧内核,直到差分全绿才 flip | +| **cache key** | 部署当天不 miss-storm | `v3:flowir:` 新前缀 + 保留 `v2:flowdef:` 3-tier lookup 一个 release 周期(现有迁移模式) | +| **trace 旧文件** | 可读 | `readTrace` 已 partial-line 容错;`events` 加 `v` 字段做版本协商 | + +--- + +## §9. 绞杀式迁移与建造顺序(每步可发布) + +roadmap 反对 B 的唯一理由是"大爆炸三层同时改"。绞杀者模式把它拆成 6 个独立可发布阶段: + +| 阶段 | 做什么 | 差分/验收门 | 可发布价值 | 承接 | +|---|---|---|---|---| +| **S0** ✅ | 自研 `flowir/{schema,compile,hash,cond}`;`usedFallbackHash→false`;runtime 仍执行 Taskflow | hash byte-determinism + 敏感度(`flowir-*.test.ts`) | cache-key 更精准;FlowIR 成规范 | roadmap M1 剩余(自建版) | +| **S1** ✅ | 加 `exec/{events,fold}`;旧 executePhase 全量 decision emit;`fold(log)` 对齐 RunState | 差分 + kill-9 rebuild 测试 | **trace 完整 + 事件溯源(F3 溶解)** | F3 / Q8 | +| **S2** ✅ | 建 `exec/{driver,step}`;**原 10 kind**(复杂路径简化版;score/reflexion 等仍可走 imperative) | parity + s2-complete 测试;默认 OFF | 每 kind 可 flip;默认 ON 在 S5 | Q7 | +| **S3** ✅ | `replayRun()` + `taskflow_replay` + `/tf replay` + 黄金 fixtures + import-lint | 未改旋钮 → 全 reused;改阈值/budget 可测 | **0.1.7 承诺的 replay 旗舰落地** | F2 / replay | +| **S4** ✅ | 包 `taskflow-dsl`:`.tf.ts→build→Taskflow→compile→FlowIR` + `check`/`new`/`decompile`;+ Horizon B `race`/`expand` 引擎 kind | DSL parity(含 map+json+templates)`hashFlowIR` 相等;`test:dsl` 绿 | DSL 作者面 + 12 kind JSON | DSL RFC v2 / s4-mvp | +| **S5** ⬜ | 全 kind 差分绿 → 新 driver 设默认;退休旧 executePhase;可选 race/expand kernel handlers | 全量回归 + e2e 绿 | 内核替换完成 | — | + +**顺序优雅之处**:`rates.ts`(F1) 落在 S1/S2(cost 进 event/usage);F3 在 S1 自然溶解;replay(S3) 与 DSL(S4) 已并行落地;**下一主线是 S5**。S5 预热:`runtime/phases/.ts` strangler(避免 `runtime.ts` 巨石阻 flip)。 + +--- + +## §10. 包拓扑(9 packages + website) + +| 包 | 变化 | +|---|---| +| `taskflow-core` | flowir/ 毕业为真编译器;新增 exec/;trace→events;replay 实现;rates 新增。**零依赖不变** | +| `taskflow-mcp-core` | 新增 `taskflow_replay` 工具;action 表更新 | +| `taskflow-hosts` | codex-runner 补 cost(接 rates);其余 runner 无感 | +| `pi-taskflow` | 新增 `/tf replay` 命令 + 渲染;绞杀开关的 host 侧 flag | +| `codex/claude/opencode/grok-taskflow` | 无感(经 mcp-core 共享) | +| **`taskflow-dsl`(新 · S4 ✅)** | rune 类型 + erase kinds 注册表 + build/check/decompile/cli;依赖 core + typescript;**不**依赖 runtime/exec | + +--- + +## §11. 风险与验证门 + +| 风险 | 级别 | 缓解 | +|---|---|---| +| 内核替换回归(新 driver 与旧行为不一致) | **HIGH** | S1/S2 差分测试是硬门:`fold(log)==旧RunState`、新内核逐 kind ==旧内核;702 测试是回归网 | +| RunState 兼容破坏(旧 `RunState.json` 加载失败) | **HIGH** | Q6:RunState 公开形状不变;旧 run 走 resume;加载器容错 | +| 自研 hash 正确性(逻辑等价漏判/误判) | MED | S0 硬验收门 + property 测试(现有 `flowir-hash.test.ts` 扩展) | +| cache miss-storm(v3 前缀切换) | MED | v2→v3 3-tier lookup 一个 release 周期 | +| replay 花了 token(import 图破坏) | MED | §4.2 结构护栏:replay 不 import exec/driver;**已加** `replay-import-lint.test.ts` | +| scope 蔓延(S2 逐 kind 拖太久) | MED | 每 kind 独立可发布;可先发 agent/script,复杂 kind 后续版本 | +| DSL 与内核耦合(S4 依赖 S0-S3) | LOW | DSL 只产 Taskflow JSON,与内核解耦;可并行 | + +**跨阶段验证门**(对齐 roadmap §5): +- **S0** ✅:`hashFlowIR` 确定性 + 敏感度。 +- **S1** ✅(测试 oracle):跑完 → 只保留 event log → `foldEvents` 重建 phase 终端状态(`fold-kill9-rebuild.test.ts`)。 +- **S3** ✅:未改旋钮 replay → 全 `reused`;阈值/budget 覆盖(`replay.test.ts`)。 +- **S4** ✅:`packages/taskflow-dsl/test/*` parity + kinds coverage;skills 与 12 kind 对齐。 +- **S5** ⬜:增量重算成本比 —— 周一 $6/8 agents → 周二改 1 文件 → ≤2 节点 $0.40(旗舰 demo);+ kernel 默认 ON 全量回归。 + +--- + +## §12. 非目标(0.2.0 明确不做,防 scope 蔓延) + +1. **overstory native 多节点 lowering**(parallel→N siblings / tournament→map+gate)。S0 用 1:1 投影,保 hash 稳定。native lowering 推迟到内核替换稳定后。 +2. **RunState → overstory RunTree 收敛**(Q6)。RunState 保持公开面;内部收敛留 post-0.2.0 mapping RFC。 +3. **map item-level 精确重算**(单 item 变只重跑该 item)。已知限制,推迟。 +4. **精确 `ir-changed` diff**(只失效结构变化的切片)。S5 先"全失效(默认)+refuse(flag)",精确 diff 后续 RFC。 +5. **`flow.component` / `$store` / `$derived`**(全局响应式)。依赖 Shared Context Tree,DSL RFC v2 §7 已标 post-0.2.0。demo 里加 `// [post-0.2.0]`。 +6. **`{env.X}` 占位符**:DSL RFC v2 §C 留待实现时定(0.2.0 纳入 or 只推 script 注入)。 + +--- + +## §13. 待定 / 已决 + +1. **`build` AST transform 实现选型**(S4):**已决(落地)** — TypeScript compiler API(`typescript` package)AST erase,非 Babel;见 `taskflow-dsl/src/build/erase/*`。 +2. **replay 命名**:**已决** — 模块 `replay.ts`;用户面 `/tf replay` + `taskflow_replay`;口号 **replayable-for-what-if**。 +3. **event log 存储**:**已决(S1)** — 与 `trace.jsonl` 同形状;`Event = TraceEvent & { v }`;`upgradeTraceEvent` 读旧行。 +4. **绞杀开关的默认策略**:**已决(S2 切片)** — 默认 OFF;`RuntimeDeps.eventKernel` 或 `PI_TASKFLOW_EVENT_KERNEL=1|true`;显式 `false` 覆盖 env。**S5 再翻默认 ON。** +5. **S3 与 S4 并行 vs 串行**:**已决(均已落地)** — 下一主线 **S5**;S5 预热为 runtime phases 模块化 + race/expand kernel handlers(可选)。 + +--- + +## 附 A:north-star 需要修订的点(本 RFC 触发) + +| north-star 现状 | 需改为 | 原因 | +|---|---|---| +| 口号 "compiled, **resumable (not replayable)**, incremental" | **已改** → **compiled · resumable · incremental · replayable-for-what-if**(见 `docs/0.2.0-north-star.md`) | 与确定性重放同名碰撞;0.1.7 已承诺 replay | +| 吸收思想表 "resumable, not replayable \| Qwik \| ✅已落地" | **已改** → resumable + 独立一行「确定性重放 / S3」 | replay 重新纳入 scope | +| §六 执行顺序(只列 DSL + 旗舰 demo) | 加 replay(S3) 作为与 DSL 平行的主线 | 两条主线在 FlowIR 汇合 | + +## 附 B:决策记录溯源 + +| 决策 | 由谁定 | 日期 | 记录 | +|---|---|---|---| +| Q2=B(FlowIR 执行产物) | 项目主 | 2026-07-08 | 本 RFC §1 | +| Q5=own(自研编译器) | 项目主 | 2026-07-08 | 本 RFC §1 | +| Q6/Q7/Q8(状态模型/绞杀迁移/log=trace) | 建议 + 项目主未反对 | 2026-07-08 | 本 RFC §1,实现时可复核 | + +--- + +*一句话总纲:0.2.0 把 runtime 换成执行 FlowIR 的事件溯源内核(Q2=B,自研编译器 Q5=own),让 trace/resume/replay/recompute 从一个机制长出;用绞杀式迁移(Q7)守住每步可发布与向后兼容;DSL(taskflow-dsl 新包)与 replay(兑现 0.1.7 承诺)是在 FlowIR 汇合的两条平行主线。* diff --git a/docs/rfc-0.2.0-dsl-phases-horizon.md b/docs/rfc-0.2.0-dsl-phases-horizon.md new file mode 100644 index 0000000..a71627c --- /dev/null +++ b/docs/rfc-0.2.0-dsl-phases-horizon.md @@ -0,0 +1,419 @@ +# RFC: DSL 扩展 Phase 设计(脑暴收编 + 语言表面) + +> Status: **Design** · 2026-07-09 +> Source brainstorm: [`internal/brainstorm-2026-07-08-0.2.0-phases.md`](./internal/brainstorm-2026-07-08-0.2.0-phases.md) +> DSL 基线: [`rfc-0.2.0-dsl-syntax.md`](./rfc-0.2.0-dsl-syntax.md) v2 + [`rfc-0.2.0-s4-mvp.md`](./rfc-0.2.0-s4-mvp.md) +> Engine foundation: event log + FlowIR (S0–S3) — *why these phases are cheap now* + +**目标:** 把「0.2.0 脑暴的更强 phase」收成 **DSL 可写的语言形状**,并分轨: + +| 轨 | 含义 | +|----|------| +| **A · 已有能力 / 仅 DSL 糖** | 引擎已有或字段已有,S4 给 rune / 文档 | +| **B · S4.x 引擎 + DSL 一起做** | 新 `PHASE_TYPES` 或大增强;语言先定形,实现跟引擎 | +| **C · experimental 语言预留** | 语法进 `taskflow-dsl/experimental`,引擎未就绪前 build **失败明确**(不静默 no-op) | +| **D · 不做** | 脑暴已否或仍冲突护城河 | + +S4 **原 MVP ship bar** 是 10 种 kind 的基本形态;引擎现为 **12 kind**(+`race`/`expand`,见 §6 打勾)。本文件仍是 **语言 horizon**——未打勾项实现可分期,设计先收齐。 + +--- + +## 0. 设计原则 + +1. **新 phase = 新 rune**(DSL v2 §7);不塞进万能 `agent({ kind: "..." })`。 +2. **1 phase → 1 FlowIR node**(S0 约束);多节点 lowering 仍 post-0.2.0。 +3. **事件溯源优先**:需要「注入图 / 补偿 / 分叉 / 反事实」的,用 event log 语义描述,不发明第二套状态机。 +4. **JSON 与 DSL 对称**:每个新 type 必须有 JSON 形态;DSL 只是前端。 +5. **未实现 = fail-closed**:experimental rune 若引擎没有 → `TFDSL_PHASE_UNSUPPORTED`,禁止静默删掉。 + +--- + +## 1. 现有 10 kind(S4 MVP 必覆盖基本形态) + +| type | DSL rune | 备注 | +|------|----------|------| +| agent | `agent` | | +| parallel | `parallel` | | +| map | `map` | 逐项增量 = B 轨增强,不是新 type | +| gate | `gate` / `gate.automated` / `gate.scored` | scored 已在引擎;DSL MVP 可后置糖 | +| reduce | `reduce` | | +| approval | `approval` | 超时回退 = B 轨字段增强 | +| flow | `subflow` / `subflow.def` | `def` 引擎已有;DSL MVP 可先 `use` | +| loop | `loop` | 多 phase body = B 轨 | +| tournament | `tournament` | | +| script | `script` | | + +--- + +## 2. 轨 A —— 已有 / 只需 DSL 收齐 + +| 能力 | JSON | DSL | 说明 | +|------|------|-----|------| +| 动态子流 | `type:"flow", def:"{steps.plan.json}"` | `subflow.def(plan.json)` 或 `expand.nested(plan)` | 见 `design-dynamic-dag-expansion`;**嵌套**非 graft | +| 评分 gate | `gate` + `score` | `gate.scored(up, { scorers, threshold, … })` | 引擎已有;S4.1 糖 | +| 自动 gate | `gate` + `eval` | `gate.automated(up, { pass: [...] })` | 同上 | +| reflexion | `loop` + `reflexion:true` | `loop({ reflexion: true, … })` | 字段级 | +| 幂等注解 | `idempotent` | opts | 字段级 | + +**DSL 补形(建议 S4.1,不改 PHASE_TYPES):** + +```ts +// A1 — 动态嵌套子流(引擎已支持 flow.def) +const plan = agent("Emit Taskflow JSON {name, phases}", { + output: json<{ name: string; phases: unknown[] }>(), +}); +const runPlan = subflow.def(plan.json, { /* with? */ }); + +// 别名(文档可写 expand.nested,编译到同一 JSON) +const runPlan2 = expand.nested(plan.json); + +// A2 — 评分 / 自动 gate +gate.scored(gen, { + target: gen.output, + scorers: [{ type: "contains", text: "OK", weight: 1 }], + combine: "weighted", + threshold: 0.8, +}); +gate.automated(build, { pass: ["{steps.build.output} contains 'OK'"] }); +``` + +--- + +## 3. 轨 B —— S4.x 引擎 + DSL 一起做(脑暴稳健前排) + +### B1 · `expand` — 动态图**嫁接**(旗舰 · 新 type) + +> 与 A 轨 `flow.def` 区别:A = **嵌套**子 flow(子命名空间);B1 = **splice 进父 DAG**(父拓扑可见新节点)。 +> 事件溯源治好旧 P0(注入丢失 / 三态就绪 / 并发改数组)——见 brainstorm §0。 + +**JSON 草图:** + +```jsonc +{ + "id": "grow", + "type": "expand", + "from": "{steps.plan.json}", // Taskflow 片段或 phases[] + "mode": "graft", // graft | nested(nested ≡ 今日 flow.def) + "maxNodes": 50, + "dependsOn": ["plan"] +} +``` + +**DSL:** + +```ts +const plan = agent("…", { output: json() }); + +// 默认 graft(父图扩展)—— 需引擎 B1 +const grown = expand(plan.json, { + mode: "graft", + maxNodes: 50, + onInvalid: "fail", // fail | skip +}); + +// 显式嵌套(A 轨,可先编译到 type:flow def) +expand.nested(plan.json); +``` + +**FlowIR:** 1 node `kind:"expand"`;运行时 graft 产生的子节点记入 event log,fold 重建父图。 + +**解锁连锁:** `loop` 多 phase body、局部子图 map 增强可视为 expand 特例。 + +--- + +### B2 · `loop` 多 phase body(增强,非新 type) + +```ts +// 今日 MVP +loop({ + maxIterations: 5, + until: "{steps.refine.json.done} == true", + task: (prev) => `Fix:\n${prev.output}`, +}); + +// B2 — 子图 body(引擎支持 loop 内嵌 phases) +loop({ + maxIterations: 5, + until: "{steps.test.json.pass} == true", + body: (prev) => { + const fix = agent(`Fix based on:\n${prev.output}`); + const test = agent(`Retest:\n${fix.output}`, { + output: json<{ pass: boolean }>(), + }); + return test; // final of body subgraph + }, +}); +``` + +编译:`type:"loop"` + `bodyPhases: Phase[]`(或内部 `def` 子图);**无新 PHASE_TYPES**。 + +--- + +### B3 · `race` — 首个胜出(新 type) + +```ts +const first = race([ + agent("Try codemod path…"), + agent("Try AI rewrite…"), + agent("Try hybrid…"), +], { + cancelLosers: true, // 取消未完成分支 + onCancel: "record", // event log 记账供 replay 算成本 +}); +``` + +**JSON:** `type:"race", branches:[…], cancelLosers?:boolean` +**vs parallel:** parallel 全等;**vs tournament:** tournament 等全部完成再裁判。 + +--- + +### B4 · `compensate` / saga(新 type 或 phase 字段) + +**形态 1 — 声明式补偿表(推荐先做):** + +```ts +const migrate = agent("Apply migration…", { + compensate: script("rollback-migration.sh"), // 失败时沿 log 倒序触发 +}); +``` + +**形态 2 — 一等 phase:** + +```ts +saga({ + steps: [ + { do: agent("create resource"), undo: script("delete resource") }, + { do: agent("wire DNS"), undo: script("unwire DNS") }, + ], +}); +``` + +事件溯源:补偿 = 沿 event log 逆序执行 `undo`;replay 可审计。 + +--- + +### B5 · `route` — 类型化路由(新 type 或 desugar) + +```ts +const classified = agent("…", { + output: json< + | { kind: "bug"; id: string } + | { kind: "feat"; id: string } + | { kind: "chore" } + >(), +}); + +route(classified.json, { + bug: (x) => agent(`Fix bug ${x.id}`), + feat: (x) => agent(`Implement ${x.id}`), + chore: () => script("echo skip"), + // 编译期检查:联合成员穷尽;缺 case → TFDSL_ROUTE_EXHAUST +}); +``` + +可 desugar 为 N 个 `when` + 合成 id;**一等 type** 便于 verify 穷尽性。 + +--- + +### B6 · `map` 逐项增量(运行时增强,DSL 几乎不变) + +```ts +map(files, (item) => agent(`Audit ${item}`), { + incremental: true, // 或继承 top-level incremental + per-item cache(已有部分能力) +}); +``` + +语言侧:opts 透传;旗舰是 **runtime/recompute**,不是新 rune。 + +--- + +### B7 · `approval` 超时 + 回退(字段增强) + +```ts +approval({ + request: "Ship?", + timeoutMs: 86_400_000, + onTimeout: "reject", // reject | approve | agent:"risk-reviewer" +}); +``` + +--- + +### B8 · `watch` — 响应式重跑(新 type · 最大胆稳健轨) + +```ts +watch({ + seed: audit, // 被观察的 phase 输出 / readSet + run: (changed) => agent(`Re-audit ${changed}`), + mode: "on-stale", // on-stale | continuous(continuous 更晚) + maxFires: 10, +}); +``` + +MVP 语言可只支持 `on-stale`(接 recompute 语义);`continuous` 常驻流 → C/D。 + +--- + +## 4. 轨 C —— experimental 语言预留(Moonshot A/B) + +导入: + +```ts +import { fork, counterfactual, quorum, negotiate, escalate, population, selfOptimize, speculate } + from "taskflow-dsl/experimental"; +``` + +| Rune | 一句话 | 依赖 | +|------|--------|------| +| `fork` / savepoint | 命名存档点,从此分叉新 run | event log fork | +| `counterfactual` | 对已跑 phase 换旋钮 offline replay | S3 `replayRun` 用户级 | +| `quorum` | N 路多数/中位 + 分歧度 | parallel + reduce 模式 | +| `negotiate` | 对立辩论收敛 | multi-agent protocol | +| `escalate` | 督导动态增派/杀掉 | org-supervision / SCT | +| `population` | 进化种群 loop | loop 扩展 | +| `selfOptimize` | 读历史 log 自调参 | 跨 run 索引 | +| `speculate` | 并行未来 + 剪枝 | fork + cancel | + +**DSL 示例(仅设计,build 在引擎未就绪时 error):** + +```ts +// 反事实 —— 最短路径接 S3 +const cf = counterfactual(review, { + thresholds: { review: [0.5, 0.7, 0.9] }, + models: { review: ["fast", "strong"] }, +}); +// → 编译为 meta phase;runtime 调 replayRun,零 token + +// 共识 +const voted = quorum([ + agent("Answer A"), + agent("Answer B"), + agent("Answer C"), +], { mode: "majority", emitConfidence: true }); + +// 分叉 +const sp = fork.save("after-plan"); +// 之后 host API / 后续 phase 可 fromSavepoint 开新 run +``` + +--- + +## 5. 轨 D —— 仍不做(语言也不预留假 rune) + +| 方向 | 理由 | +|------|------| +| 可视化拖拽编辑器 | 护城河是代码/JSON | +| 真 stream edges + backpressure | 用 `watch` 响应式,不背流边 | +| 全自主自改写 flow | 成本失控 | +| Flow algebra merge/project | `flow{use}` + decompile 已够 | +| Artifacts 新 type | `ctx_write`/`ctx_read` 已覆盖 | + +--- + +## 6. 推荐纳入「DSL 设计支持」的优先级(给实现排期) + +| 优先级 | 项 | 轨 | 语言工作 | 引擎工作 | +|--------|----|----|----------|----------| +| **P0** | 10 kind 基本 + `subflow.def` / `expand.nested` 糖 | A | S4 | **✅** | +| **P0** | `gate.scored` / `gate.automated` / `reflexion` / `idempotent` 糖 | A | S4.1 | **✅** gate sugar + opts;erase kinds 注册 | +| **P1** | **`expand` graft** | B | rune + IR kind | **✅ runtime + DSL** (promote onto parent) | +| **P1** | **`loop` multi-body** | B | body 回调 erase | **新** | +| **P1** | **`race`** | B | rune | **✅ runtime + DSL** | +| **P2** | `route` 穷尽路由 | B | rune + check | desugar 或新 type | +| **P2** | `compensate` / saga | B | opts 或 rune | **新** | +| **P2** | approval timeout | B | opts | **小** | +| **P2** | map 逐项增量 | B | opts | recompute | +| **P3** | `watch` on-stale | B | rune | recompute 常驻化 | +| **P3** | `counterfactual` / `quorum` | C | experimental | 粘合 replay / parallel | +| **P4** | fork / speculate / negotiate / … | C | experimental | 研究 spike | + +**「顺便支持脑暴 phase」在语言层的默认承诺:** + +1. **文档 + 类型 + experimental 入口**写齐 B1–B5 + C 的 `counterfactual`/`quorum`/`fork` 草形。 +2. **S4 MVP 编译器**:A 轨能 erase 的 erase;B/C 未实现 kind → **明确诊断**(可先认 type 字符串进 JSON passthrough 给未来引擎,或拒绝——推荐 **S4 拒绝未知 type,S4.x 放行已实现**)。 +3. **不把 Moonshot 全塞进 MVP 出货门**。 + +--- + +## 7. FlowIR / schema 扩展约定 + +新增 `PHASE_TYPES` 时: + +1. `schema.ts` 注册 + `validateTaskflow` +2. `flowir` `FlowIRNodeKind` 闭集扩展 +3. `exec/step` kind 或 imperative 分支 +4. DSL rune + skills +5. 本文件状态表打勾 + +**建议新增 type 名(稳定字符串):** + +| type | rune | +|------|------| +| `expand` | `expand` / `expand.nested` | +| `race` | `race` | +| `compensate` | `compensate` 或 `saga` | +| `route` | `route` | +| `watch` | `watch` | +| `counterfactual` | `counterfactual`(experimental) | +| `quorum` | `quorum`(experimental) | +| `fork` | `fork`(experimental) | + +--- + +## 8. 与 S4 MVP 的关系(改写一句话) + +| 文档 | 范围 | +|------|------| +| `rfc-0.2.0-s4-mvp.md` | **可发布表面**:CLI + runes(现含 12 kind + 糖) | +| **本文** | **语言 horizon**:脑暴 phase 的 DSL 形状与分期 | +| 引擎 S4.x / S6 | 按 §6 表落地剩余 type | + +S4 出货门 **不变**(demo FlowIR == hand JSON)。 +DSL erase **只生成已支持 type**;未知 experimental → fail-closed。 + +--- + +## 9. 最小「脑暴进语言」示例(作者可见的未来) + +```ts +import { flow, agent, map, race, expand, gate, reduce, json } from "taskflow-dsl"; +// 未实现的: +// import { counterfactual, quorum } from "taskflow-dsl/experimental"; + +export default flow("brain-storm-shaped", (ctx) => { + ctx.budget({ maxUSD: 5 }); + + const discover = agent("List hot paths", { + output: json<{ path: string }[]>(), + }); + + // race:三路谁先好用谁(B3) + const approach = race([ + agent("Static analysis plan…"), + agent("LLM-only plan…"), + agent("Hybrid plan…"), + ], { cancelLosers: true }); + + // expand.nested:今日即可(A);expand graft:B1 + const planJson = agent(`Turn into audit Taskflow JSON. Approach:\n${approach.output}`, { + output: json<{ name: string; phases: unknown[] }>(), + }); + const dynamic = expand.nested(planJson.json); + + const perFile = map(discover, (item) => + agent(`Deep dive ${item.path}`), + ); + + gate(perFile, { agent: "reviewer", onBlock: "retry" }, (i) => + `Quality check:\n${i.output}`, + ); + + return reduce([dynamic, perFile], () => + agent("Merge dynamic plan results + per-file notes"), + ); +}); +``` + +--- + +*一句话:脑暴 phase 不是丢进「以后再说」,而是 **DSL 先有形状、分轨落地**——A 轨立刻进语言糖,B 轨 `expand`/`race`/`loop-body`/`route`/`compensate` 做 S4.x 引擎+语言,C 轨 experimental 接 replay/共识/分叉,且绝不静默假实现。* diff --git a/docs/rfc-0.2.0-dsl-syntax.md b/docs/rfc-0.2.0-dsl-syntax.md new file mode 100644 index 0000000..02e89b9 --- /dev/null +++ b/docs/rfc-0.2.0-dsl-syntax.md @@ -0,0 +1,383 @@ +# RFC v2: taskflow 0.2.0 TypeScript 函数式 DSL — 语法规范 + +> Status: **Draft v2** · 2026-07-07 +> 取代 v1(同文件前版)。v2 是对多 agent review(run `review-020-design`)的回应: +> **身份危机**(Solid 真函数 vs Svelte 编译指令)已定调为**编译指令路线**。 +> +> **三个硬约束(不变):** +> 1. **100% 功能覆盖** —— taskflow 当前的每一个字段/能力都有对应 DSL 语法(§A 完整对照)。 +> 2. **向前兼容** —— 现有 JSON flow 零修改继续工作;JSON ↔ DSL 双向可编译(§6,v2 给出真实 decompiler 设计)。 +> 3. **向前拓展** —— 加新 phase 类型/字段不需要改语法(§7)。 +> +> **v2 相对 v1 的根本变化:** §0 执行模型从"模糊的 Solid 路线"改为**明确的编译指令路线**(rune 运行时擦除)。 + +--- + +## 0. 执行模型(Execution Model)—— v2 新增,定调 + +> **这是整份 RFC 的地基。v1 的"身份危机"(review FEASIBILITY #1)源于这里没写。v2 先把它钉死。** + +### 0.1 一个 `.tf.ts` 文件是什么 + +一个 `.tf.ts` 文件是 **agent workflow 的源代码**。它**不是运行时直接执行的脚本**。 + +```ts +// audit.tf.ts —— 源代码 +import { flow, agent, map, gate, reduce, json } from "taskflow"; + +export default flow("audit", (ctx) => { + const discover = agent("List files under {args.dir}", { output: json<{route:string}[]>() }); + const audit = map(discover, (item) => agent(`Audit ${item.route}`)); + return reduce([audit], (p) => agent(`Report: ${p.audit.output}`)); +}); +``` + +### 0.2 `taskflow build` 是什么 —— **AST transform,不是运行时** + +`taskflow build audit.tf.ts` 做三件事: + +``` +audit.tf.ts (源代码) + │ + ▼ ① tsc 类型检查(agent 拼错 item.route 在这里报错) + │ + ▼ ② taskflow 编译器:AST transform(读源码,不执行) + │ - 扫描 rune 调用(agent/map/gate/...),每个产出一个 FlowIR node + │ - 扫描模板字面量 `Audit ${item.route}` → 产 `{item.route}` 占位符 + │ - 扫描 map 回调 → 产 per-item 任务模板 + │ - 扫描 json() 的类型参数 → 产 TypeBox schema(填进 expect) + │ - 静态收集依赖(谁读了谁的 .output)→ 产 inject/emits 边 + │ + ▼ ③ 输出 FlowIR(内容寻址, hash) +``` + +**关键:`.tf.ts` 离开 `build` 不能直接 run。** 这是对 review FEASIBILITY #1/#2/#4 的诚实回应: + +- rune(`agent()`/`map()`/...)是**编译器识别的指令**,不是运行时函数。它们的"返回值"在编译期被消费(转成 FlowIR node),运行时不存在这些调用。 +- `discover.output` 不是运行时属性读取(那样会撞上"phase 还没执行"的物理矛盾,见 §0.3),而是**编译期的符号引用** —— 编译器看到 `discover` 被引用,就建一条依赖边。 +- `${item.route}` 不是运行时模板求值,而是**编译期从 AST 提取** —— 编译器看到模板字面量里的 `item.route`,转成 `{item.route}` 占位符字符串写进 FlowIR。 + +### 0.3 为什么是真函数(Proxy)路线站不住 —— review 的论证 + +v1 想要"rune 是真函数,运行时返回 Phase,读 `.output` 自动建依赖"。review 的 critic 证明了这**物理上不可能**: + +```ts +const discover = agent("List files..."); // discover "执行"了吗?没有。 +const audit = agent(`Audit ${discover.output}`); // discover.output 此刻是什么? +``` + +- `discover` 只是"声明要做的事",**从未执行**。`discover.output` 没有任何值。 +- 若 `discover.output` 是 Proxy,其 `.toString()` 要返回 `"{steps.discover.output}"` 才能让模板工作。但 `${item.route.slice(0,10)}`、`${item.items.length}`、数值上下文 —— **任何正常 JS 操作都让 Proxy 链断裂**,产生垃圾或丢依赖。这等于要求 author 记住"这些值是幻影",与"像写 Solid 一样自然"矛盾。 +- 依赖记到哪?Solid 有 `currentObserver`。taskflow 的 `flow()` 回调里没有"当前 phase"上下文(`audit` 的 `agent()` 还没返回),需要栈内省,RFC 没设计。 + +**结论:依赖追踪、类型推导、占位符转换这三个 headline 特性,本质都要求编译器"看见"源码(AST),而非运行时 Proxy 猜测。编译指令路线让它们在编译期干净成立。** + +### 0.4 诚实代价 & 缓解 + +**代价:** `.tf.ts` 不能脱离 `build` 直接 run;不能在 `.tf.ts` 里断点调试 rune 调用;没有"降级运行"。 + +**缓解(强工具链补偿):** +- `taskflow verify`(零 token)在 FlowIR 上跑静态检查 —— 编译期就能抓 DAG 错误。 +- `taskflow compile` 出 Mermaid 图 —— 可视化 DAG。 +- `taskflow peek ` —— 运行时可看任意 phase 的实际输入/输出(已有的调试逃生口)。 +- `taskflow check`(新,见 §8)—— 比 build 轻的快速校验,给 agent 快反馈。 + +> **JSON 仍是"可降级"的逃生口:** 不想走编译的,直接写/运行 JSON flow(它本就是 FlowIR 的另一种 surface)。这等于 Vue Vapor 的"双模式共存",但无需维护双执行后端(两者都编译到同一个 FlowIR)。 + +--- + +## 1. 文件骨架 + +```ts +import { flow, agent, map, parallel, gate, reduce, loop, tournament, approval, script, json, type Phase } from "taskflow"; + +export default flow("audit-endpoints", (ctx) => { + // ctx 方法对应 Taskflow 顶层字段(§2) + // rune(agent/map/...)是编译指令,编译期被转成 FlowIR(§0) + return finalPhase; // return 标记 final phase +}); +``` + +- 文件 `export default flow(name, fn)` = 一个 taskflow。 +- `name` 必填;其余顶层字段通过 `ctx` 方法。 + +--- + +## 2. 顶层字段(TaskflowSchema)—— 全覆盖 + +| JSON 字段 | DSL 写法 | 说明 | +|---|---|---| +| `name` | `flow("audit", ...)` 第 1 参数 | 必填 | +| `description` | `flow("audit", { description: "..." }, ...)` 第 2 参数 | 可选 | +| `version` | 同 options `version: 2` | 可选,默认 1 | +| `args` | `ctx.args.declare({ dir: { default: "src", required: true, description: "..." } })` | §3。**v2 修正:** 删掉 v1 臆造的 `type` 字段(ArgSpecSchema 只有 default/description/required),补回 `description` | +| `concurrency` | `ctx.concurrency(8)` | 默认 8 | +| `budget` | `ctx.budget({ maxUSD: 3, maxTokens: 1e6 })` | | +| `agentScope` | `ctx.scope("both")` | user\|project\|both | +| `strictInterpolation` | `ctx.strict()` | 默认 false | +| `contextSharing` | `ctx.share(true)` | | +| `incremental` | `ctx.incremental(true)` | 全局 cross-run 缓存 | +| `phases` | rune 声明自动构成 | §3 | + +--- + +## 3. 通用 Phase 字段 —— 全覆盖 + +每个 rune 接受一个 **options 对象**,承载所有通用字段(24 个,逐字段对照见 §A): + +```ts +agent("task", { + agent: "scout", model: "fast", thinking: "high", tools: ["read"], + cwd: "worktree", + output: "json", expect: { type: "array", items: { type: "string" } }, + when: "{env.CI} == '1'", // §5.1:字符串 eval(v2 明确 {env.X} 是新能力,见 §5.2) + join: "any", + dependsOn: ["plan"], // 显式补充(通常自动,§5.4) + retry: { max: 3, backoffMs: 500, factor: 2 }, + timeout: 60_000, + optional: true, idempotent: false, + concurrency: 4, + context: ["src/api.ts"], contextLimit: 8000, + shareContext: true, + cache: { scope: "cross-run", ttl: "7d", fingerprint: ["git:HEAD"] }, +}); +``` + +--- + +## 4. 10 种 Phase 类型 —— 全覆盖(含 v2 修正) + +### 4.1 `agent` +```ts +const d = agent("List files", { agent: "scout", output: json<{route:string}[]>() }); +``` +> `json()` 是 `output:"json"` + `expect` 的糖。**v2 明确:** 泛型 → TypeBox schema 的推导由**编译器 transform** 完成(运行时泛型擦除,见 §0.2 ②)。可推导子集:基本类型/array/object/可选字段。复杂类型(联合/映射/递归)推导不了 → **编译报错,要求显式 `expect`**(不静默降级,review COMPAT MEDIUM-1)。 + +### 4.2 `map` —— 动态扇出 +```ts +const audits = map(discover, (item) => agent(`Audit ${item.route}`, { agent: "analyst" })); +``` +- `over` = 第 1 参数;`as` = 回调形参名(默认 `item`)。 +- **v2 澄清(review FEASIBILITY #3):** 回调在**编译期执行一次**(由编译器,非运行时),`item` 是编译器已知的类型化符号(类型来自 `discover` 的 `json`)。编译器把回调体转成 per-item 任务模板 `{item.route}`。运行时不重新执行回调。 + +### 4.3 `parallel` +```ts +const [a, b] = parallel([ agent("auth"), agent("perf") ]); +``` +**v2 澄清(review FEASIBILITY #8):** 解构 `[a,b]` 是**编译期**的 —— 0.2.0 编译器把每个 branch 降低为一个独立 agent phase;`a`/`b` 是真实 phase handle(可被下游 `a.output` 引用)。不是运行时多返回值。 + +0.2.0 的 erase 实现将解构形式降低为多个独立 agent phase,因此解构形式不接受第二个 options 参数(无法诚实保留 group-level `concurrency` / `dependsOn`);需要这些选项时应绑定为单个 `const group = parallel([...], opts)`。 + +### 4.4 `gate`(三形态) +```ts +gate(audit, { agent: "reviewer", onBlock: "retry" }, (i) => `Check.\n${i.output}`); +gate.automated(build, { pass: ["{steps.build.output} contains 'OK'"] }); +gate.scored(gen, { target: "{steps.gen.output}", scorers: [...], combine: "weighted", threshold: 0.8, judge: {...} }); +``` + +### 4.5 `reduce` +```ts +const sum = reduce([auth, perf], (p) => agent(`Merge ${p.auth.output} ${p.perf.output}`)); +``` + +### 4.6 `loop` —— **v2 重大修正(review COVERAGE F2 / FEASIBILITY #6)** + +v1 的多 phase body(`body: (prev) => ({ test, fix })`)是**引擎扩展**,不是当前能力。v2 分两档: + +**当前能力(100% 覆盖当前 loop):** loop body 是单个 agent 任务(对应 `phase.task`): +```ts +const refined = loop({ + agent: "executor", + maxIterations: 5, + until: "{steps.refined.json.done} == true", + convergence: true, + reflexion: true, + task: (prev) => `Improve the draft. Previous:\n${prev.output}\nOutput JSON {done, draft}.`, + output: json<{done:boolean; draft:string}>(), +}); +``` +- `prev.output` 引用上一轮的 `{steps..output}`。 +- 回调体仍是**单个任务字符串**(编译器转成 `phase.task` 模板)。 + +**未来扩展(§7,不是覆盖):** 多 phase body(test→fix)需要引擎支持 loop 内嵌子图 —— 明确标为 **post-0.2.0**,v2 不声称覆盖。当前用 `onBlock: "retry"` 的 gate + 外部 script 组合实现等价语义。 + +### 4.7 `tournament` +```ts +tournament({ + mode: "best", judgeAgent: "final-arbiter", judge: "Pick best. WINNER: .", + branches: [ agent("A"), agent("B") ], // 或 variants: 3 + task +}); +``` + +### 4.8 `approval` —— **v2 修正(review COVERAGE F1)** +```ts +const ok = approval({ request: "Approve this plan?" }); +``` +**v2 删除 v1 臆造的 `input` 字段**(approval 没有 `input`;`input` 是 script 专属,见 schema.ts)。审批内容来自 `task`(这里是 `request`)—— 运行时通过 DAG 的上游输出自动注入(`runtime.ts` 的 `upstream`)。**依赖靠 `dependsOn`(显式或自动)。** + +### 4.9 `flow`(子流程) +```ts +subflow("deep-research", { question: "..." }); // use + with +subflow.def(() => agent("dynamic")); // def(内联,运行时解析) +``` + +### 4.10 `script` +```ts +script("npx tsc --noEmit", { cwd: "dedicated" }); // string = shell +script(["grep", "-r", args.dir]); // array = execvp(防注入) +script("cat", { input: "{steps.gen.output}" }); // stdin +``` + +--- + +## 5. 控制流与表达式 + +### 5.1 `when` —— **v2 收窄(review COMPAT MEDIUM-HIGH-1)** + +两种形态: +```ts +agent("...", { when: "{env.MODE} == 'prod'" }); // (a) 字符串 eval(完整支持) +agent("...", { when: ({ env, steps }) => env.MODE === "prod" }); // (b) TS 谓词(受限子集) +``` + +**(b) 的可编译子集(明确列出,不再含糊):** +- ✅ 比较:`===` `!==` `==` `!=` `>` `<` `>=` `<=`(编译器把 `===`→`==`) +- ✅ 逻辑:`&&` `||` `!` +- ✅ 属性路径:`env.MODE` / `steps.test.json.failures`(编译器展平成 `{env.MODE}` / `{steps.test.json.failures}`) +- ✅ 字面量:字符串/数字/布尔/null +- ❌ 方法调用(`.includes()`/`.length`)、可选链(`?.`)、箭头函数闭包、async、三元 —— **编译报错**(不静默) +- ❌ `contains` 子串检查用字符串形态(a)。 + +不在子集内 → `taskflow check` 报精确错误,告诉 author 改用字符串形态。 + +### 5.2 插值占位符 —— **v2 修正(review COVERAGE F3/F5)** + +| 占位符 | 当前引擎支持? | DSL | +|---|---|---| +| `{args.X}` | ✅ | `args.X` | +| `{steps.ID.output}` / `.json` / `.json.f` | ✅ | `id.output` / `id.json` / `id.json.f`(编译期符号引用,§0) | +| `{item}` / `{item.f}` | ✅ | `item` / `item.f`(map 回调形参) | +| `{previous.output}` | ✅ | chain 内 `previous.output` | +| `{reflexion}` | ✅ | loop body 内自动 | +| `{loop.iteration}` / `.lastOutput` / `.maxIterations` | ✅(**v1 漏列**) | loop 回调内 `loop.iteration` 等 | +| `{env.X}` | ❌ **当前引擎无 env 根**(v1 误标"兼容") | **v2 明确为新能力**,0.2.0 一起加(或用 script 注入) | + +**字符串模板:** `` agent(`Audit ${item.route}`) `` —— 编译器从 AST 提取 `${item.route}`,转成 `{item.route}` 占位符(§0.2)。**两种形态(模板 / 显式占位符)等价,文档统一推荐模板形态**(review USABILITY #3:别给 agent 两个"都行"的选项)。 + +### 5.3 `join` +`parallel([...], { join: "any" })` / `agent("...", { join: "all" })`(默认)。 + +### 5.4 `dependsOn`(显式补充)—— **v2 强化(review COMPAT MEDIUM-2)** + +自动依赖(编译期收集 `x.output` 引用)**抓不到语义依赖**(B 在 A 后跑但不读 A 输出;script A 写文件、B 读文件)。**v2 明确:这类必须显式 `dependsOn`,编译器对"无自动依赖且非首个 phase"发警告**(提示可能漏了 `dependsOn`)。 + +--- + +## 6. 兼容性 —— v2 补真实 decompiler 设计 + +### 6.1 JSON flow 零修改继续工作(不变) + +### 6.2 双向可编译 —— **v2 给出设计(review FEASIBILITY #9 要的)** + +``` +flow.tf.ts ──build──▶ FlowIR ◀──load── flow.json + ▲ │ + └─────────decompile──────────────────┘ +``` + +**`build`(.tf.ts → FlowIR):** §0.2 的 AST transform。 + +**`load`(flow.json → FlowIR):** 现有 `translateTaskflow`(已修复 sidecar 完整性,见 commit 7b48105)。 + +**`decompile`(FlowIR → .tf.ts)—— v2 新设计:** +FlowIR + sidecar 已经 lossless(7b48105 补全了 8 个字段)。decompiler 是一个**代码生成器**: +1. 从 `ir.nodes` + `meta.declaredDeps` 重建 phase 顺序 + 依赖。 +2. 每个 node + 其 sidecar 字段 → 生成对应 rune 调用(按 kind 选 agent/map/gate/...)。 +3. `inject`(声明的读)→ 生成 `x.output` 引用(从 task 字符串里的 `{steps.X}` 反推)。 +4. `expect` schema → 反推 `json()`(基本类型能反推;复杂类型降级为显式 `expect`)。 +5. 输出格式化的 `.tf.ts`。 + +**诚实边界:** decompile 出的 `.tf.ts` **语义等价但字面不一定相同**(变量名、格式、模板写法可能变)。它是"可读 + 可重新 build 回等价 FlowIR"的,不是"字面 round-trip"。文档明确这一点。 + +### 6.3 version 协商(不变) + +--- + +## 7. 拓展性(不变 + v2 强化) + +- 新 phase 类型 = 新 rune 函数(`import { saga } from "taskflow/experimental"`)。 +- 新通用字段 = 新 option key。 +- **v2 新增:** `flow.component`(带 props 的可复用子 flow)和 `$store`/`$derived`(全局响应式)**明确标为 post-0.2.0**(依赖 Shared Context Tree / 响应式运行时,见 demo 的使用)。0.2.0 首版不含;demo 里用到的地方加 `// [post-0.2.0]` 注释。 +- **0.2.0 脑暴 phase 收编:** 事件溯源解锁的 `expand` / `race` / `compensate` / `route` / `watch` / loop 多 body / map 逐项增量 / `counterfactual` 等 —— **语言形状与分期**见 [`rfc-0.2.0-dsl-phases-horizon.md`](./rfc-0.2.0-dsl-phases-horizon.md)(A 轨 DSL 糖 · B 轨 S4.x 引擎+语言 · C 轨 experimental)。原 S4 MVP 出货门为 10 kind 基本形态;引擎现为 **12 kind**(+`race`/`expand` 已落地),其余 B/C 项仍分期,但 **设计上不再「未定义」。** + +--- + +## 8. agent 工具链(新,回应 review USABILITY #5/#6) + +| 命令 | 作用 | 给 agent 的价值 | +|---|---|---| +| `taskflow check audit.tf.ts` | **轻量校验**:tsc + rune 签名 + 依赖完整性 + when 谓词子集。不生成 FlowIR | 快反馈,agent 写完立刻知道错没错 | +| `taskflow build audit.tf.ts` | 完整编译 → FlowIR | 产出可运行产物 | +| `taskflow verify` | FlowIR 静态检查(零 token) | 运行前抓 DAG 错 | +| `taskflow compile` | Mermaid + 报告 | 可视化 | +| `taskflow new` | 生成骨架 `.tf.ts` | agent 不用从零写 | + +**`taskflow new` 产出的最小骨架(回应 review USABILITY #4,要 ≤5 行 hello world):** +```ts +import { flow, agent } from "taskflow"; +export default flow("hello", () => agent("Say hello to {args.name}")); +``` + +--- + +## §A. 完整字段对照表(v2 修正版)—— 100% 覆盖 + +[与 v1 相同的结构,但每一行都经过 review 实证修正: +- approval 删 `input`(F1) +- loop 多 phase body 移到 §7(F2) +- args 删 `type` 补 `description`(F4) +- 占位符加 `{loop.*}`、标 `{env.X}` 为新(F3/F5) +- 顶层 args/version/agentScope/strictInterpolation/contextSharing/incremental 确认全覆盖 +- 通用 23 字段逐个确认(含刚修的 sidecar:agent/run/input/timeout/expect/reflexion/idempotent/score) +完整表见 v1 §A,v2 仅标注修正点如上。] + +--- + +## §B. 决策记录(v2 基于 review 的修订) + +| review 发现 | v2 应对 | 章节 | +|---|---|---| +| FEASIBILITY #1 身份危机 | 定调编译指令路线 | §0 | +| FEASIBILITY #2 Proxy 不可能 | §0.3 论证 + 编译期符号引用 | §0 | +| FEASIBILITY #3 map item 幽灵 | 回调编译期执行一次,item 是类型化符号 | §4.2 | +| FEASIBILITY #4 json() 幻象 | 编译器 transform 推导,复杂类型报错 | §4.1 | +| FEASIBILITY #5 模板转换 | 编译期 AST 提取 | §0.2/§5.2 | +| FEASIBILITY #6 loop body | 当前=单任务;多 phase 移 post-0.2.0 | §4.6 | +| FEASIBILITY #8 parallel 解构 | 编译期合成 branch 符号 | §4.3 | +| FEASIBILITY #9 decompiler | 给出代码生成器设计 | §6.2 | +| FEASIBILITY #10 降级运行 | 放弃;JSON 作逃生口 | §0.4 | +| COMPAT CRITICAL sidecar | 已修(commit 7b48105) | §A | +| COMPAT HIGH collectRefs | 已修(7b48105) | — | +| COMPAT MEDIUM-HIGH when 子集 | 明确列出可编译子集 | §5.1 | +| COMPAT MEDIUM-1 json() | 同 FEASIBILITY #4 | §4.1 | +| COMPAT MEDIUM-2 自动依赖边界 | 无自动依赖的 phase 发警告 | §5.4 | +| COVERAGE F1 approval.input | 删除 | §4.8 | +| COVERAGE F2 loop body | 移 post-0.2.0 | §4.6 | +| COVERAGE F3 {env.X} | 标为新能力 | §5.2 | +| COVERAGE F4 args.type | 删 type 补 description | §2 | +| COVERAGE F5 {loop.*} | 补进占位符表 | §5.2 | +| USABILITY #1 demo-RFC gap | demo 标 [post-0.2.0] | §7 | +| USABILITY #2 rune 签名一致 | 统一为 (data, opts) 或 (opts) 两模式 | 全文 | +| USABILITY #3 双插值模型 | 统一推荐模板形态 | §5.2 | +| USABILITY #4 无 hello world | `taskflow new` + 5 行骨架 | §8 | +| USABILITY #5 无 check | `taskflow check` | §8 | +| USABILITY #6 ctx 命名 | 保持(和顶层字段对应) | §2 | + +--- + +## §C. 仍 open(v2 不阻塞,留实现时定) + +1. `taskflow build` 的 AST transform 用什么实现?(tsc transformer API / Babel / 自研轻量解析)—— 工程选型,§0 不阻塞。 +2. decompiler 的代码格式化风格(prettier?手写?)。 +3. `flow.component` 的 props 响应式语义(post-0.2.0 单独 RFC)。 +4. `{env.X}` 0.2.0 是否纳入(还是只推 script 注入)。 diff --git a/docs/rfc-0.2.0-s4-decision-record.md b/docs/rfc-0.2.0-s4-decision-record.md new file mode 100644 index 0000000..d12511b --- /dev/null +++ b/docs/rfc-0.2.0-s4-decision-record.md @@ -0,0 +1,134 @@ +# S4 Shape Decision Record (`taskflow-dsl`) + +> Status: **IMPLEMENTED (package live)** — route/surface frozen; coverage extended beyond original MVP ship bar (see §“Landed beyond MVP”) +> Date: 2026-07-09 · Last kinds sync: 2026-07-09 +> Full surface: [`rfc-0.2.0-s4-mvp.md`](./rfc-0.2.0-s4-mvp.md) +> Council runs: `s4-shape-council` → `s4-shape-council-v2` → `s4-shape-finalize` + +## One-sentence definition + +**S4 is a new package `taskflow-dsl` that compile-erases `.tf.ts` into Taskflow JSON (then FlowIR only via `taskflow-core`), with whole-file JSON as the zero-migration escape hatch — not a second runtime and not S5.** + +## Execution model (locked) + +| | | +|--|--| +| **Primary** | **Svelte-style** compile-time runes (AST erase; cannot run unbuilt `.tf.ts`) | +| **Escape** | **JSON-only** whole-file (dual frontend at **file** boundary) | +| **Toolchain** | **TypeScript compiler API** (`typescript` package) → Taskflow → `compileTaskflowToFlowIR` | +| **Rejected** | Solid Proxy runtime runes · in-file Vapor hybrid · interpret / auto-build-on-run · S5 prerequisite | + +## Package & CLI + +- **Package:** `taskflow-dsl` +- **Import:** `from "taskflow-dsl"` +- **Bin:** `taskflow-dsl {build,check,decompile,new}` +- **Hosts:** CLI-first; **no** new MCP tools in S4; run via existing `taskflow_run` + emitted `.taskflow.json` +- **Core:** allow schema/validate/desugar/FlowIR compile+hash/verify; **deny** runtime/exec/runner/store/hosts/mcp + +## MVP scope (in) — original ship bar + +1. `flow` + args/budget/concurrency/description +2. Core phase kinds basic runes (ship bar was **10**; engine now **12** with `race`/`expand` — see Landed beyond MVP) +3. Template → `{steps.*}` / `{item.*}` erase +4. Auto `dependsOn` from `.output` / `.json` reads + explicit `dependsOn` +5. `when` **string** form (+ TS subset in `check` if cheap) +6. Basic `json()` → `output:"json"` + `expect` (fail-closed on complex types) +7. `check` / `build` / `new` / decompile (Taskflow→`.tf.ts` on Y-slice) +8. Golden **FlowIR hash equality** demos (must include map + templates + `json`, not only hello) +9. Import-lint: DSL must not drag core runtime + +## Landed beyond original MVP (kinds sync) + +Engine `PHASE_TYPES` is now **12** (`race`, `expand` added). DSL erase registry +(`packages/taskflow-dsl/src/build/erase/kinds/*`) covers: + +| Kind / sugar | Status | +|--------------|--------| +| Core 10 + `subflow` / `subflow.def` | ✅ | +| `gate.automated` / `gate.scored` | ✅ (A-track sugar) | +| `race` + `cancelLosers` | ✅ engine + DSL (best-effort AbortSignal abort of losers) | +| `expand` / `expand.nested` / `expand.graft` + `maxNodes` | ✅ engine + DSL | +| Parallel destructure → N agent phases | ✅ | +| Modular pipeline (no monolith grow) | ✅ see `docs/internal/modularization-0.2.0.md` | + +Still **not** shipped as runes: loop multi-body, `route`, `compensate`/saga, +`watch`, experimental C-track. + +## Brainstorm phases — language support + +Source: `docs/internal/brainstorm-2026-07-08-0.2.0-phases.md` +Design: **`docs/rfc-0.2.0-dsl-phases-horizon.md`** + +| Track | What | When | +|-------|------|------| +| **A** | `subflow.def` / `expand.nested`, `gate.scored`/`automated`, `reflexion`, `idempotent` — **DSL sugar on existing engine** | ✅ mostly landed (reflexion/idempotent via opts) | +| **B** | New/enhanced: **`expand` graft**, **`race`**, **loop multi-body**, **`route`**, **`compensate`/saga**, approval timeout, map item-incremental, **`watch` on-stale** | race + expand graft ✅; rest S4.x | +| **C** | experimental: `counterfactual`, `quorum`, `fork`, … | language stubs + fail-closed until engine | +| **D** | visual builder, true stream edges, full self-rewriting flow | never / far | + +Unknown / unimplemented experimental runes must **error** (no silent drop). + +## Explicitly out (S4 ship bar) + +1. Solid runtime / Proxy / degraded interpret +2. In-file JSON phase hybrid +3. Multi-body loop / `flow.component` / `$store` as **MVP ship** (designed in horizon doc; implement later) +4. S5 kernel default ON +5. New MCP `taskflow_build` / host auto-build of `.tf.ts` +6. Literal decompile round-trip marketing +7. 100% PhaseSchema coverage as S4 ship bar (FULL = language goal only) +8. Shipping B-track **engines** inside S4 MVP gate (language design only) + +## Minimal `.tf.ts` example + +```ts +import { flow, agent, map, reduce, json } from "taskflow-dsl"; + +export default flow("audit", (ctx) => { + ctx.budget({ maxUSD: 2 }); + const discover = agent("List files under {args.dir}", { + output: json<{ path: string }[]>(), + }); + const each = map(discover, (item) => + agent(`Audit ${item.path}`), + ); + return reduce([each], () => agent("Write one summary from map outputs")); +}); +``` + +## Acceptance gates + +- [x] `packages/taskflow-dsl` in workspace; bin + exports as `rfc-0.2.0-s4-mvp.md` §1 +- [x] Demo `.tf.ts` and twin `.json` → **same** `ir:<64-hex>` (parity tests) +- [x] Equality fixtures include **map + json\ + templates** +- [x] Rune runtime call throws `TFDSL_ERASE_ONLY` +- [x] Import-lint denylist green +- [x] Skills/docs: JSON first-class for agents; CLI path for DSL (+ kinds table) +- [x] DSL v2 note: FULL vs S4 MVP ship gate (authority B1) + +## Open questions for human (max 3) + +1. **H1** Commit coverage matrix as a real doc? (recommend **yes**) +2. **H2** Decompile Taskflow-only in MVP? (recommend **yes**) +3. **H3** Throw-on-call for runes? (recommend **yes**) + +## North-star alignment + +| Slogan | How S4 contributes | +|--------|-------------------| +| **compiled** | First real authoring compiler (erase → Taskflow → FlowIR) | +| **resumable** | Unchanged (runs still Taskflow/events) | +| **incremental** | Unchanged (recompute on built Taskflow) | +| **replayable-for-what-if** | Unchanged (S3); DSL-produced runs replay the same | + +## Council evidence + +| Run | Role | +|-----|------| +| `s4-shape-council-mrd6rcyl-f77e65` | inventory-code, inventory-rfc, coverage-map | +| `s4-shape-council-v2-mrd6wdcj-e34ca3` | routes + tournament (svelte/json-only/typescript-AST) + api-surface + adversary | +| `s4-shape-finalize-mrd765gf-f14e1b` | cross-check (PASS on route; BLOCK only until B1/matrix/H2/H3 written) | + +Flow defs: `/tmp/taskflow-s4/s4-shape.json`, `s4-shape-v2.json`, `s4-shape-final.json` +Saved project flow: `.pi/taskflows/s4-shape-council.json` diff --git a/docs/rfc-0.2.0-s4-mvp.md b/docs/rfc-0.2.0-s4-mvp.md new file mode 100644 index 0000000..c42c4fc --- /dev/null +++ b/docs/rfc-0.2.0-s4-mvp.md @@ -0,0 +1,826 @@ +# RFC: taskflow 0.2.0 S4 MVP — `taskflow-dsl` public surface + +> Status: **IMPLEMENTED** (package + CLI live; surface extended — see §8.1) · 2026-07-09 + +> **Final-surface amendment (2026-07-10):** the signature sketches in §3 are +> historical council proposals, not the published 0.2.0 declaration contract. +> The shipped synchronous APIs are `buildFile(path, opts?): BuildResult`, +> `buildSource(source, file?, opts?): BuildResult`, `checkFile(path, opts?): +> CheckResult`, and `decompileTaskflow(def): string`. `decompileFlowIR` and a +> structured `DecompileResult` are deferred beyond 0.2.0. The generated `.d.ts` +> files and package exports are authoritative; this amendment supersedes any +> conflicting “frozen” sketch below. + +> Parent: [`rfc-0.2.0-architecture.md`](./rfc-0.2.0-architecture.md) §4.3 / §9 S4 +> Syntax authority: [`rfc-0.2.0-dsl-syntax.md`](./rfc-0.2.0-dsl-syntax.md) v2 (**FULL language goal**; ship gate is this MVP doc) +> Route lock: north-star 决策1 + this document §0 +> Provenance: multi-agent council runs `s4-shape-council` + `s4-shape-council-v2` + `s4-shape-finalize` (inventory → route tournament → surface → adversary → cross-check) + +This document freezes the **publishable public surface** of `packages/taskflow-dsl` for the S4 MVP ship gate. It is intentionally narrower than full DSL RFC coverage: what agents and humans import, invoke, and get wrong messages for — not the full transform implementation plan. + +### Authority amendment (council B1 — must land with S4) + +| Layer | Meaning | +|-------|---------| +| **DSL RFC v2 FULL** | Long-horizon language goal: every JSON field eventually has a DSL expression (§A). | +| **S4 MVP ship gate (this doc)** | Finite Y-rows + demo `hashFlowIR` equality — **not** every `PhaseSchema` field on day 1. | + +DSL v2 hard constraint “100% 功能覆盖” is **not** the S4 acceptance bar. Implementers must not expand S4 scope to FULL §A. + +### Shape in one screen + +| Decision | Value | +|----------|--------| +| **What S4 is** | New package `taskflow-dsl`: compile-time TypeScript frontend that **erases** `.tf.ts` → Taskflow JSON → (via core only) FlowIR | +| **What S4 is not** | A second runtime, kernel flip (S5), in-file JSON hybrid, or agent-default authoring path | +| **Primary model** | **Svelte-style** compile-time runes (AST erase; no interpret) | +| **Escape** | **Whole-file JSON** only (zero migration; dual frontend at file boundary) | +| **Toolchain** | **TypeScript compiler API** (`typescript` package) → Taskflow → `compileTaskflowToFlowIR` | +| **Package / bin** | `taskflow-dsl` / `taskflow-dsl {build,check,decompile,new}` | +| **Import** | `from "taskflow-dsl"` (not `"taskflow"` in S4) | +| **Hosts** | **CLI-first**; no new MCP / no auto-build on `taskflow_run` | +| **Ship bar** | Golden demos: DSL build FlowIR hash **==** hand JSON FlowIR hash (must include map + `json` + templates, not only hello) | +| **Default author for agents** | Still **JSON** + existing MCP; DSL for humans / large typed flows | + +--- + +## §0. Route lock (non-negotiable for S4) + +| Pick | Value | +|------|--------| +| **Primary** | **Svelte-style compile-time erase** (runes are AST directives; runtime does not execute them) | +| **Escape** | **JSON-only** whole-file (existing Taskflow JSON remains first-class; zero migration) | +| **Build toolchain** | **TypeScript compiler API** (AST erase via `typescript` → Taskflow JSON → core `compileTaskflowToFlowIR`) | + +### Why (≤5) + +- Architecture S4 is fixed as `.tf.ts → build → Taskflow → FlowIR` with demo FlowIR ≡ hand JSON; runtime runes would be a third executor and blow MVP scope. +- Solid Proxy dies on physics, not taste: no `currentObserver`, phase-not-yet-run, `.toString`/coercion/method chains break templates and deps (DSL v2 §0.3; north-star 决策1). +- Headline features (`json()`, map templates, parallel destructure, `.output` → placeholders) need AST sight; a “correct Proxy” becomes an ad-hoc compiler worse than one intentional erase. +- Progressive migration is already dual-frontend at the **file** boundary (`.json` *or* `.tf.ts`); in-file hybrid (Vapor) adds a second grammar, handle↔id seams, and dilutes the S4 equality gate without tightening it. +- JSON stays zero-change and first-class whole-file escape; compensators are `check` / `build` / `verify` / `compile` / `peek`, not REPL-on-runes or degraded interpret mode. + +### S4 MVP is NOT + +- Solid runtime runes / Proxy / `currentObserver` graph building +- Runnable unbuilt `.tf.ts` or “degraded interpret” next to JSON +- In-file JSON phase literals (Vapor hybrid) +- Breakpoint-on-`agent()` as a product promise +- S5 kernel flip or any change to existing JSON Taskflow execution + +**WINNER_RATIONALE:** Svelte-style compile-time erase with whole-file JSON escape is the only path that satisfies architecture S4 (`.tf.ts→build→Taskflow→FlowIR`, JSON zero-change) without reopening rejected Proxy physics or hybrid grammar cost. + +### Pipeline S4 owns + +``` +.tf.ts ──build(AST / typescript)──▶ Taskflow JSON ──compileTaskflowToFlowIR──▶ FlowIR (+ ir:<64-hex>) +.flow.json ──validate / desugar──▶ Taskflow JSON ──same core entry only──▶ FlowIR +``` + +- **S4 stop line:** DSL package produces **Taskflow** (and may *call* core to emit FlowIR for CLI convenience). +- **Single FlowIR entry:** only `taskflow-core`’s `compileTaskflowToFlowIR` / `hashFlowIR` (arch §4.2). +- **Acceptance gate:** DSL demo FlowIR == hand-written JSON FlowIR (byte-stable `ir:<64-hex>`). + +--- + +## §1. Package identity — `package.json` + +### 1.1 Name, version, role + +| Field | MVP value | +|-------|-----------| +| **name** | `taskflow-dsl` | +| **version** | tracks monorepo release line (`0.2.0` when shipped with 0.2.0) | +| **description** | Compile-time TypeScript DSL frontend for taskflow: erase `.tf.ts` runes to Taskflow JSON, then FlowIR via core | +| **type** | `"module"` | +| **engines** | `node: ">=22.19.0"` (same floor as monorepo) | +| **license / author / repository.directory** | match sibling packages; `directory: "packages/taskflow-dsl"` | + +**Not** named `taskflow`. That umbrella name stays free for a future unified CLI/meta-package. Authoring imports use the package name (see §3). + +### 1.2 Dependencies + +| Dep | Kind | Why | +|-----|------|-----| +| `taskflow-core` | **dependency** (workspace pin, same version as siblings) | Taskflow types, `validateTaskflow` / `desugar`, `compileTaskflowToFlowIR`, `hashFlowIR`, `verifyTaskflow` | +| `typescript` | **dependency** | AST erase (`createSourceFile` / Program for `--typecheck`); pin with monorepo | +| `typebox` | **peerDependency** `*` | Align with core; only if public types re-export expect shapes that mention TypeBox | + +**Forbidden in this package:** host SDKs (`@earendil-works/*`), `taskflow-hosts`, `taskflow-mcp-core`, any process-spawning runner. + +**Why not in core:** AST toolchain violates core’s zero-runtime-deps iron rule (arch §4.3). Core must never import `taskflow-dsl`. + +### 1.3 `exports` map + +```jsonc +{ + "name": "taskflow-dsl", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./build": { + "development": "./src/build.ts", + "types": "./dist/build.d.ts", + "default": "./dist/build.js" + }, + "./check": { + "development": "./src/check.ts", + "types": "./dist/check.d.ts", + "default": "./dist/check.js" + }, + "./decompile": { + "development": "./src/decompile.ts", + "types": "./dist/decompile.d.ts", + "default": "./dist/decompile.js" + }, + "./diagnostics": { + "development": "./src/diagnostics.ts", + "types": "./dist/diagnostics.d.ts", + "default": "./dist/diagnostics.js" + } + }, + "bin": { + "taskflow-dsl": "./dist/cli.js" + }, + "files": ["dist"], + "publishConfig": { "access": "public" } +} +``` + +| Export | Audience | Contents | +|--------|----------|----------| +| **`.`** | Authors of `*.tf.ts` | Runes + types only (`flow`, `agent`, `map`, …, `json`, `Phase`, …). **Compile stubs** — safe to typecheck under tsc; must not “run” a flow. | +| **`./build`** | Tooling / tests / optional host glue | `buildFile`, `buildSource`, `BuildOptions`, `BuildResult` | +| **`./check`** | Tooling / agents | `checkFile`, `checkSource` → diagnostics only | +| **`./decompile`** | Tooling / migration | `decompileTaskflow`, `decompileFlowIR` → source string | +| **`./diagnostics`** | Shared types | `Diagnostic`, severity, codes, formatters | + +**MVP does not export:** `./experimental`, `./vapor`, `./runtime`, MCP server bindings, host runners. + +**Development condition:** same monorepo pattern as siblings (`development` → `src/*.ts`) so tests run without a prior `tsc` emit. + +### 1.4 `bin` + +| Binary | Path | Role | +|--------|------|------| +| **`taskflow-dsl`** | `./dist/cli.js` | Only published bin for S4 MVP | + +RFC prose that says `taskflow build` / `taskflow check` is the **conceptual** command surface. Implementation name is `taskflow-dsl ` to avoid claiming a future umbrella `taskflow` CLI. Docs and skills for 0.2.0 should teach: + +```bash +npx taskflow-dsl build ./audit.tf.ts +# or, after install: +taskflow-dsl check ./audit.tf.ts +``` + +Optional monorepo convenience (not published): root script `"dsl": "node --conditions=development … packages/taskflow-dsl/src/cli.ts"`. + +--- + +## §2. CLI surface + +Entry: `taskflow-dsl [options] [path…]` +Flags shared by all commands: + +| Flag | Meaning | +|------|---------| +| `--cwd ` | Project root for path resolution / tsconfig discovery (default: `process.cwd()`) | +| `-h` / `--help` | Command help | +| `-V` / `--version` | Package version | + +`--json` is command-specific and is published only for `build` and `check`. +Color-control and strict-warning flags are deferred; they are not part of the +0.2.0 CLI surface. + +Exit codes (stable for agents): + +| Code | Meaning | +|------|---------| +| `0` | Success (check/build clean; decompile/new wrote output) | +| `1` | Diagnostics present at **error** severity (or validation failed) | +| `2` | Usage / I/O / unexpected internal failure | + +Warnings alone → exit `0` (match common compiler UX). + +### 2.1 `build` + +```text +taskflow-dsl build [options] +``` + +| Input | Behavior | +|-------|----------| +| `*.tf.ts` | AST erase → Taskflow → (optional) FlowIR via core | +| `*.json` / `*.jsonc` | **JSON escape path:** parse → `validateTaskflow` / `desugar` → same emit options (no TypeScript AST) | +| other extension | Error `TFDSL_INPUT_KIND` | + +| Flag | Default | IO | +|------|---------|-----| +| `--cwd ` | process cwd | Resolve input/output and enforce containment | +| `-o` / `--out ` | derived (see below) | Write the single selected artifact; invalid with `--emit both` | +| `--emit taskflow\|flowir\|both` | `taskflow` | What to write | +| `--json` | off | Print `BuildResult` JSON without writing; incompatible with `--out` | +| `--force` | off | Atomically replace an existing regular output; symlinks/non-regular files remain rejected | + +**Default output paths** (when `-o` omitted, write next to input): + +| Emit | Default file | +|------|----------------| +| taskflow | `.taskflow.json` | +| flowir | `.flowir.json` | +| both | both files | + +**Stdout human mode (no `--json`):** short summary — name, phase count, `ir:` if computed, output paths. +**Stdout `--json`:** `BuildResult` (§3.2), with no artifact write. + +All filesystem outputs are create-only unless `--force` is explicit, must +remain beneath `--cwd`, reject destination symlinks, and commit atomically from +a same-directory temporary file. `--emit both` preflights both destinations +before writing either. + +**MVP non-goals for `build`:** watch mode, multi-file project graph beyond one entry `export default`, bundling imports of other `.tf.ts` (S4.1: `import` of subflow modules). + +### 2.2 `check` + +```text +taskflow-dsl check [options] +``` + +Lightweight validation **without requiring artifact write**. + +| Input | Checks | +|-------|--------| +| `*.tf.ts` | (1) tsc diagnostics for the file in a Program (2) rune shape / signature rules (3) static dep completeness warnings (4) `when` predicate subset (DSL v2 §5.1) (5) optional: lower to Taskflow in memory + `validateTaskflow` | +| `*.json` | `validateTaskflow` only | + +| Flag | Default | Meaning | +|------|---------|---------| +| `--cwd ` | process cwd | Resolve the input and TypeScript project context | +| `--no-typecheck` | off | Skip tsc; rune/static rules only (faster agent loop) | +| `--typecheck` | on | Explicit form of the default; mutually exclusive with `--no-typecheck` | +| `--json` | off | Machine-readable `{ ok, diagnostics }` | + +Does **not** write FlowIR. Does **not** call the runtime. + +Stdout: human diagnostic list, or `--json` → `{ diagnostics: Diagnostic[] }`. + +### 2.3 `decompile` + +```text +taskflow-dsl decompile [options] +``` + +| Input | Behavior | +|-------|----------| +| Taskflow JSON (`.json`) | Preferred MVP path: Taskflow → `.tf.ts` codegen | +| FlowIR JSON | **Deferred in 0.2.0:** reject and require Taskflow JSON | +| `.tf.ts` | Error (already DSL) | + +| Flag | Default | Meaning | +|------|---------|---------| +| `-o` / `--out ` | `.tf.ts` or stdout if `-o -` | Output path | +| `--cwd ` | process cwd | Resolve input/output and enforce containment | +| `--force` | off | Atomically replace an existing regular output | + +**Honest contract (DSL v2 §6.2):** semantic equivalence under re-`build`, **not** literal round-trip. Variable names, template vs string placeholders, and formatting may change. + +**MVP decompile coverage:** kinds and fields in the S4 MVP coverage slice only; advanced fields (`gate.scored`, multi-scorer, etc.) emit explicit options objects or `// TFDSL: unsupported-field` comments + warning diagnostics — never silent drop of execution-critical fields. If a field cannot be expressed in MVP runes, decompile **fails closed** with `TFDSL_DECOMPILE_UNSUPPORTED` (prefer fail over lying JSON↔DSL parity). + +### 2.4 `new` + +```text +taskflow-dsl new [name] [options] +``` + +| Flag | Default | Meaning | +|------|---------|---------| +| `-o` / `--out ` | `./.tf.ts` or `./hello.tf.ts` | Destination (refuse overwrite unless `--force`) | +| `--cwd ` | process cwd | Resolve output and enforce containment | +| `--force` | off | Overwrite existing file | +| `--json-escape` | off | Emit a minimal **JSON** flow instead (escape hatch skeleton) | + +**Default skeleton** (DSL v2 §8, ≤5 lines of substance): + +```ts +import { flow, agent } from "taskflow-dsl"; + +export default flow("hello", () => agent("Say hello to {args.name}")); +``` + +If `name` provided, substitute `flow("", …)` and default out file. + +### 2.5 Commands explicitly **out of** `taskflow-dsl` MVP CLI + +These stay on **core + host / MCP** (already shipped): + +| Command / tool | Owner | +|----------------|--------| +| `verify` / `taskflow_verify` | core + mcp-core | +| `compile` (Mermaid/SVG) / `taskflow_compile` | core + mcp-core | +| `peek` / `taskflow_peek` | core + mcp-core | +| `run` / `taskflow_run` | mcp-core + host runners | +| `replay` / `taskflow_replay` | core + mcp-core (S3) | + +S4 docs tell agents: **`check`/`build` here → `verify`/`run` there**. Optional `--verify` on `build` is a thin CLI convenience, not a second verify implementation. + +--- + +## §3. Library API + +### 3.1 Authoring surface (`import from "taskflow-dsl"`) + +Rune functions are **type-level + compile-time directives**. At Node runtime they are stubs: + +- Calling them outside `taskflow-dsl build` throws `TFDSL_ERASE_ONLY` (or returns a branded phantom used only in type positions — pick one implementation; **public contract:** “do not execute `.tf.ts` as a program”). +- tsc sees full typings for IDE / agent typecheck. + +#### MVP exports (authoring) + +```ts +// --- entry --- +export function flow( + name: string, + fn: (ctx: FlowCtx) => PhaseRef | void, +): TaskflowModuleDefault; +export function flow( + name: string, + opts: FlowOptions, + fn: (ctx: FlowCtx) => PhaseRef | void, +): TaskflowModuleDefault; + +// --- phase runes (aligned with PHASE_TYPES + sugars; see runes.ts) --- +export function agent(task: TemplateInput, opts?: PhaseOptions): PhaseRef; +export function parallel( + branches: PhaseRef[], + opts?: PhaseOptions, +): PhaseRef[] & PhaseRef; // compile-time destructure → N agent phases +export function map( + source: PhaseRef | Placeholder, + fn: (item: ItemSymbol) => PhaseRef, + opts?: PhaseOptions, +): PhaseRef; +export function gate( + upstream: PhaseRef, + opts?: GateOptions, + task?: (input: PhaseRef) => TemplateInput, +): PhaseRef; +// sugar: gate.automated / gate.scored +export function reduce( + from: PhaseRef[], + fn: (parts: Record) => PhaseRef, + opts?: PhaseOptions, +): PhaseRef; +export function approval(opts: { request: TemplateInput } & PhaseOptions): PhaseRef; +export function subflow( + use: string, + withArgs?: Record, + opts?: PhaseOptions, +): PhaseRef; +// sugar: subflow.def +export function loop(opts: LoopOptions): PhaseRef; +export function tournament(opts: TournamentOptions): PhaseRef; +export function script( + run: string | string[], + opts?: ScriptOptions, +): PhaseRef; +export function race( + branches: PhaseRef[], + opts?: PhaseOptions & { cancelLosers?: boolean }, +): PhaseRef; +export function expand( + def: PhaseRef | string, + opts?: PhaseOptions & { expandMode?: "nested" | "graft"; maxNodes?: number }, +): PhaseRef; +// sugar: expand.nested / expand.graft + +// --- expect sugar --- +export function json(): JsonExpectMarker; + +// --- types (erasable) --- +export type { PhaseRef, FlowCtx, FlowOptions, PhaseOptions, ItemSymbol, … }; +``` + +#### `FlowCtx` MVP methods + +| Method | JSON field | MVP | +|--------|------------|-----| +| `ctx.args.declare({…})` | `args` | **Y** — keys: `default?`, `description?`, `required?` only (**no `type`**) | +| `ctx.concurrency(n)` | `concurrency` | **Y** | +| `ctx.budget({ maxUSD?, maxTokens? })` | `budget` | **Y** | +| `ctx.scope` / `ctx.strict` / `ctx.share` / `ctx.incremental` | agentScope / strictInterpolation / contextSharing / incremental | **N** (S4.1) | + +#### Explicitly **not** in MVP authoring export + +| API | Reason | +|-----|--------| +| top-level `ctx.scope` / `strict` / `share` / `incremental` | S4.1+ | +| `subflow.def(() => …)` | Dynamic inline def; engine exists, DSL compile cost high → S4.1 | +| `$store` / `$derived` / `flow.component` | post-0.2.0 (arch §12) | +| Runtime `execute*` anything | Wrong package | + +Import path note: DSL RFC v2 samples use `"taskflow"`. **S4 MVP ships `"taskflow-dsl"`** only. A later `taskflow` meta-package may re-export; do not publish a second package name in MVP. + +### 3.2 Compiler API (`taskflow-dsl/build`) + +```ts +import type { Taskflow } from "taskflow-core"; +import type { FlowIR } from "taskflow-core"; // or flowir schema type from core +import type { Diagnostic } from "taskflow-dsl/diagnostics"; + +export interface BuildOptions { + cwd?: string; + tsconfigPath?: string; + /** default: true when caller wants IR equality gate */ + emitFlowIR?: boolean; + verify?: boolean; + /** Source path for diagnostic file field */ + fileName?: string; +} + +export interface BuildResult { + ok: boolean; + taskflow?: Taskflow; + flowir?: FlowIR; + /** Present when flowir emitted and well-formed */ + hash?: `ir:${string}`; + diagnostics: Diagnostic[]; +} + +/** Build from a filesystem path (.tf.ts | .json). */ +export function buildFile(path: string, opts?: BuildOptions): Promise; +// Sync variant allowed if AST usage is sync-only: +export function buildFileSync(path: string, opts?: BuildOptions): BuildResult; + +/** Build from source text (tests / MCP-later). `fileName` required for positions. */ +export function buildSource( + source: string, + opts: BuildOptions & { fileName: string; kind?: "tf.ts" | "json" }, +): BuildResult; +``` + +**Contract:** + +1. On `ok: true`, `taskflow` is present and has passed `validateTaskflow` (post-desugar shape). +2. When `emitFlowIR: true` and ok, `flowir` + `hash` come **only** from `compileTaskflowToFlowIR` + `hashFlowIR`. +3. DSL package never reimplements IR lowering. + +### 3.3 Check API (`taskflow-dsl/check`) + +```ts +export interface CheckOptions { + cwd?: string; + tsconfigPath?: string; + typecheck?: boolean; // default true for .tf.ts + /** Lower + validateTaskflow without writing */ + emitTaskflow?: boolean; // default false +} + +export interface CheckResult { + ok: boolean; // no error-severity diagnostics + diagnostics: Diagnostic[]; +} + +export function checkFile(path: string, opts?: CheckOptions): CheckResult; +export function checkSource( + source: string, + opts: CheckOptions & { fileName: string; kind?: "tf.ts" | "json" }, +): CheckResult; +``` + +### 3.4 Decompile API (`taskflow-dsl/decompile`) + +```ts +export interface DecompileOptions { + name?: string; + style?: "compact" | "readable"; +} + +export interface DecompileResult { + ok: boolean; + source?: string; // .tf.ts text + diagnostics: Diagnostic[]; +} + +export function decompileTaskflow(def: Taskflow, opts?: DecompileOptions): DecompileResult; +/** MVP: may return not-ok with TFDSL_DECOMPILE_USE_TASKFLOW if IR incomplete. */ +export function decompileFlowIR(ir: FlowIR, opts?: DecompileOptions): DecompileResult; +``` + +### 3.5 Scaffold API (used by CLI `new`) + +```ts +export function skeletonTfTs(name?: string): string; +export function skeletonJson(name?: string): string; +``` + +### 3.6 What the library must **not** expose in MVP + +- `interpretTfTs()` / `runUnbuilt()` +- Proxy helpers, `currentObserver`, reactive graph APIs +- Direct `executeTaskflow` wrappers (hosts already own run) + +--- + +## §4. File convention — `*.tf.ts` + +### 4.1 Recognition rules + +| Rule | MVP | +|------|-----| +| Extension | **`*.tf.ts` only** (not `.tf.js`, not bare `.ts`) | +| Module shape | Exactly one **`export default flow(…)`** (or `export default` of the value returned by `flow`) | +| Side exports | **Forbidden** for build entry (`export const helpers` → error `TFDSL_ENTRY_SHAPE`) | +| Imports from `taskflow-dsl` | Runes/types only | +| Imports from other local modules | **N** in MVP (single-file flows); S4.1 may allow type-only or pure const imports | +| Top-level statements | Only imports + `export default flow(…)` | +| Executable as `node file.tf.ts` | **Unsupported**; stubs throw if runes invoked | + +### 4.2 Discovery (for future host glue; CLI is path-explicit in MVP) + +MVP CLI always takes an explicit path. Recommended project layout (non-normative): + +```text +flows/ + audit.tf.ts # DSL source + audit.taskflow.json # build emit (optional commit) + audit.flowir.json # build emit (optional; usually gitignored) + legacy-audit.json # JSON escape (hand-written) +``` + +Saved-flow library (`.pi/taskflows/*.json` etc.) remains **JSON** as today; S4 does not require hosts to load `.tf.ts` at run time. Author workflow: `build` → save/run JSON. + +### 4.3 Naming + +- Flow `name` (string arg to `flow`) = taskflow `name` field (library identity). +- Phase ids = **const binding names** where possible (`const discover = agent(…)` → id `discover`); anonymous runes get synthetic ids (`agent_0`, …) with stable ordering rules documented in implementer notes. +- IDs remain kebab-or-camel as bound; core already accepts phase ids used in `{steps.ID…}` — **prefer valid JS identifiers** that match existing JSON id style in demos (kebab via explicit `opts.id` if needed). + +MVP option: `agent("…", { id: "audit-each" })` maps to JSON `id` when binding name differs — **Y** if `id` already exists on PhaseSchema; else use binding name only. + +### 4.4 JSON escape file convention + +- Any `*.json` / `*.jsonc` Taskflow document accepted by today’s `validateTaskflow`. +- No `.tf.json` hybrid. +- `build`/`check` treat JSON as the escape frontend; output FlowIR must match DSL-built FlowIR for the equality demos. + +--- + +## §5. Diagnostics shape + +### 5.1 Type + +```ts +export type DiagnosticSeverity = "error" | "warning" | "info"; + +export interface Position { + /** 1-based */ + line: number; + /** 1-based, code unit columns (tsc-compatible) */ + column: number; +} + +export interface Range { + start: Position; + end: Position; +} + +export interface Diagnostic { + /** Stable machine code, e.g. "TFDSL0001" or "TFDSL_ERASE_ONLY" */ + code: string; + severity: DiagnosticSeverity; + message: string; + /** Absolute or cwd-relative path */ + file?: string; + range?: Range; + /** Optional agent-facing hint (how to fix) */ + hint?: string; + /** Related spans (e.g. missing dependsOn target) */ + related?: Array<{ file?: string; range?: Range; message: string }>; +} +``` + +### 5.2 Code families (MVP reserved prefixes) + +| Prefix | Domain | +|--------|--------| +| `TFDSL_ENTRY_*` | Module shape / export default / multi-flow | +| `TFDSL_RUNE_*` | Unknown rune, bad arity, disallowed call position | +| `TFDSL_TMPL_*` | Template → placeholder erase failures | +| `TFDSL_WHEN_*` | Predicate subset violations | +| `TFDSL_JSON_T_*` | `json()` unrepresentable types | +| `TFDSL_DEP_*` | Dependency auto-edge warnings / cycles at DSL layer | +| `TFDSL_IMPORT_*` | Illegal imports (see §6) | +| `TFDSL_DECOMPILE_*` | Codegen unsupported / incomplete | +| `TFDSL_IO_*` | CLI filesystem / tsconfig | +| `TFDSL_CORE_*` | Wrapped `validateTaskflow` / verify messages (preserve core text in `message`, code maps or nests) | + +Human formatter example: + +```text +audit.tf.ts:12:5 - error TFDSL_WHEN_METHOD: when-predicates cannot call methods (.includes). + hint: Use a string when: "{steps.x.output} contains 'ok'" instead. +``` + +`--json` CLI wraps: + +```json +{ + "ok": false, + "diagnostics": [ /* Diagnostic */ ] +} +``` + +### 5.3 Severity policy + +| Class | Severity | +|-------|----------| +| Unerasable construct, invalid entry, validateTaskflow error | **error** | +| Phase with no auto-deps and not first (DSL v2 §5.4) | **warning** | +| Decompile pretty-print lossiness | **info** / silence | +| `json()` too complex | **error** (no silent drop — DSL v2 §4.1) | + +--- + +## §6. Core import allowlist / denylist + +`taskflow-dsl` may depend on `taskflow-core` **only** through the following surfaces. Enforce with an import-lint test (mirror `replay-import-lint` spirit). + +### 6.1 Allowlist (MVP) + +| Core surface | Use in DSL | +|--------------|------------| +| `Taskflow`, `Phase`, `PhaseType`, `PHASE_TYPES`, arg/budget types from `schema` | Output typing + guards | +| `validateTaskflow`, `desugar`, `topoLayers` / `dependenciesOf` (if needed for warnings) | Post-erase validation | +| `compileTaskflowToFlowIR`, `CompileTaskflowToFlowIRResult` | IR emit | +| `hashFlowIR` / compile envelope hash from `compileTaskflowToIR` helper | `ir:<64-hex>` | +| `verifyTaskflow` | CLI `--verify` only | +| FlowIR types (`FlowIR`, `FlowIRNode`, …) | decompile input typing + emit typing | +| Pure helpers strictly needed for placeholder parity (e.g. cond normalize) | only if build must match runtime when-strings | + +Prefer **barrel** `taskflow-core` or narrow subpaths already exported (`taskflow-core/flowir/*` if published). Do not deep-import non-exported internals. + +### 6.2 Denylist (hard) + +| Core / monorepo surface | Why denied | +|-------------------------|------------| +| `runtime.ts` / `executeTaskflow` / `executePhase` | S4 must not run flows | +| `exec/driver`, `exec/step`, `exec/fold` | Kernel path; S5 concern | +| `runner-core` process spawn, `detached-runner` | Side effects | +| `store.ts` persist / locks | Wrong layer | +| `agents.ts` discovery | Build is host-agnostic | +| `cache.ts` runtime memoization | Not authoring | +| `context-store.ts` SCT runtime | Not authoring | +| `replay.ts` | Orthogonal (S3) | +| Any `taskflow-hosts/*` | Host coupling | +| Any `taskflow-mcp-core/*` | MCP is separate delivery | +| Any `@earendil-works/*` | Core iron rule extended to DSL | + +### 6.3 What `.tf.ts` author files may import + +| Import | MVP | +|--------|-----| +| `taskflow-dsl` (runes/types) | **Y** | +| `taskflow-dsl/build` etc. | **N** inside a flow file (tooling only) | +| `taskflow-core` | **N** (keeps author surface small; avoids pulling runtime types into flows) | +| `node:*` / fs / child_process | **N** (`TFDSL_IMPORT_NODE`) | +| relative `./foo` | **N** MVP (`TFDSL_IMPORT_LOCAL`) | +| type-only imports of local types | **N** MVP (S4.1 candidate) | + +--- + +## §7. Host integration — CLI-first (MVP) + +### 7.1 Decision + +| Channel | S4 MVP | Rationale | +|---------|--------|-----------| +| **CLI `taskflow-dsl`** | **Y — primary** | Agents/humans compile before run; matches “no unbuilt run”; easy CI | +| **Library `buildFile` / `checkFile`** | **Y** | Tests + scripted toolchains | +| **New MCP tools** (`taskflow_build` / `taskflow_check`) | **N** | Avoid bloating every host adapter + mcp-core in the same release as first compiler; agents shell out or use defineFile of emitted JSON | +| **Host auto-build of `.tf.ts` on `taskflow_run`** | **N** | Silent compile on run reintroduces “feels executable” and couples hosts to the TypeScript compiler | +| **pi `/tf` DSL commands** | **N** MVP | Optional S4.1 thin wrappers calling the same library | + +### 7.2 Recommended agent workflow (0.2.0) + +```text +1. taskflow-dsl new my-flow +2. edit my-flow.tf.ts +3. taskflow-dsl check my-flow.tf.ts # fast loop +4. taskflow-dsl build my-flow.tf.ts # → .taskflow.json + .flowir.json +5. taskflow_verify / taskflow_run with defineFile=my-flow.taskflow.json + (existing MCP — unchanged) +``` + +JSON authors skip steps 1–4 and keep using define / defineFile as today. + +### 7.3 S4.1 host follow-ups (explicitly deferred) + +- `taskflow_build` / `taskflow_check` in `taskflow-mcp-core` (stdio tools wrapping library API). +- pi `/tf build|check|new` and skills-src host blocks. +- Optional: run path accepts `.tf.ts` **only** after explicit build cache hit (still no interpret). + +### 7.4 Compatibility with S0–S3 / S5 + +| Stage | Interaction | +|-------|-------------| +| S0 FlowIR | DSL must use sole compile entry; equality gate is the S4 ship bar | +| S1–S2 events/driver | Irrelevant to DSL package; built Taskflow runs on existing runtime | +| S3 replay | Works on runs of DSL-produced Taskflow the same as JSON | +| S5 kernel flip | **No DSL changes required** if Taskflow IR parity holds | + +--- + +## §8. MVP coverage vs public surface (pointer) + +Public surface exposes runes for the **Y** rows of the S4 coverage matrix (full matrix lives in implementer notes / companion survey). Minimum vertical slice for the equality demo: + +| Kind / feature | Surface | +|----------------|---------| +| `flow` + description + args + concurrency + budget | `flow`, `FlowCtx` | +| `agent`, `parallel`, `map`, `gate` (LLM), `reduce`, `approval`, `subflow(use)`, `loop` (single task body), `tournament`, `script` | runes above | +| `json()` basic object/array | `json` | +| template → `{steps.*}` / `{item.*}` | erase in `build` | +| `when` string + TS subset | options + check rules | +| `gate.automated` / `gate.scored` | **✅ landed** — erase to `eval` / `score` | +| top-level strict/share/incremental/scope | **not exported** (S4.1+) | + +### §8.1 Kinds extension (post-MVP, same package) + +Engine and DSL now also ship Horizon B kinds (skills + decompile cover them): + +| Kind / feature | Surface | Notes | +|----------------|---------|--------| +| `race` | `race([...], { cancelLosers? })` | first completed branch wins | +| `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode`; graft promotes `-` | +| `subflow.def` | `subflow.def(plan)` | → `type:flow` + `def` | +| Parallel destructure | `const [a,b] = parallel([...])` | N real `agent` phase ids | + +Erase modularization: `build/erase/kinds/*` registry — do not re-monolith `pipeline.ts`. + +FULL RFC coverage remains a completion track; missing FULL features must not appear as stub exports that silently no-op. + +--- + +## §9. Acceptance checklist (public surface) + +- [x] Package name `taskflow-dsl` publishable with exports/bin as §1 +- [x] `taskflow-dsl build|check|decompile|new` flags/IO as §2 +- [x] Author import `from "taskflow-dsl"` typechecks a hello + audit-style demo +- [x] `build` on demo `.tf.ts` and hand JSON → **identical** `hashFlowIR` +- [x] Diagnostics stable codes + positions on deliberate erase errors +- [x] Import-lint: no denylist modules from core/hosts +- [x] No MCP/schema changes required to pass S4 gate +- [x] README/skills teach CLI-first workflow; JSON escape documented as first-class + + +--- + +## §10. Open implementer choices + +### 10.1 Needs human lock before coding (max 3) + +| # | Question | Council recommendation (default if human silent) | +|---|----------|--------------------------------------------------| +| **H1** | Commit a canonical **coverage matrix** (Y/N per field) next to this RFC? | **Yes** — add `docs/rfc-0.2.0-s4-coverage-matrix.md` from council `coverage-map` output; §8 must link a real table, not “implementer notes”. | +| **H2** | Decompile input: Taskflow-only vs FlowIR too? | **Taskflow-only in MVP**; reject FlowIR with “pass Taskflow JSON”. | +| **H3** | Rune stub at Node runtime: phantom vs throw? | **Throw `TFDSL_ERASE_ONLY`** on any runtime call (types-only for tsc). | + +### 10.2 Non-blocking polish (implementer discretion) + +1. Sync vs async `buildFile` (sync-friendly with the TypeScript API). +2. Exact synthetic phase id algorithm for anonymous runes (must be deterministic for hash equality fixtures). +3. Default `--emit both` vs `taskflow` only (recommend **taskflow-only** default; FlowIR via `--emit flowir|both` / CI). +4. Optional thin facade over TS `Node` so the parser host can be swapped later without rewriting erase. + +### 10.3 Recommended PR stack (after human lock) + +1. **Scaffold** `packages/taskflow-dsl` + workspace/filter + import-lint denylist test. +2. **Author stubs + types** (closed MVP option types; throw-on-call). +3. **`check` + `new`** (tsc/Program + rune rules + hello skeleton). +4. **`build` erase vertical slice** — agent → map/templates → parallel → reduce → gate LLM → script; emit Taskflow; call core FlowIR. +5. **Golden parity** — hand JSON twin fixtures; `hashFlowIR` equality (include map + `json`). +6. **`decompile` Taskflow→`.tf.ts`** on Y-slice + fail-closed unsupported. +7. **Docs/skills** — CLI-first workflow; JSON first-class; import string `taskflow-dsl`; amend DSL v2 FULL vs MVP note. + +--- + +## §11. Doc map + +| Doc | Role after this RFC | +|-----|---------------------| +| `rfc-0.2.0-architecture.md` | System topology; S4 row points here for surface | +| `rfc-0.2.0-dsl-syntax.md` | Language semantics (full); MVP subset bound by §8 | +| `rfc-0.2.0-three-compile-routes.md` | Historical routes; **superseded for product** by §0 lock (Svelte + JSON escape) | +| **`rfc-0.2.0-s4-mvp.md` (this)** | Ship surface freeze for `packages/taskflow-dsl` | +| [`rfc-0.2.0-dsl-phases-horizon.md`](./rfc-0.2.0-dsl-phases-horizon.md) | Brainstorm phases → DSL shapes (expand/race/saga/route/watch/…); A/B/C tracks | +| [`internal/brainstorm-2026-07-08-0.2.0-phases.md`](./internal/brainstorm-2026-07-08-0.2.0-phases.md) | Original phase brainstorm (event-sourced unlocks) | + +--- + +*One-line summary: S4 MVP publishes `taskflow-dsl` as a CLI-first, TypeScript-AST erase frontend — `*.tf.ts` in, Taskflow (+ FlowIR via core) out — with whole-file JSON as the only escape, stable diagnostics, a hard core import allowlist, and no MCP or runtime interpret path. Extended brainstorm phases are designed in `rfc-0.2.0-dsl-phases-horizon.md` without expanding the S4 ship bar.* diff --git a/docs/rfc-0.2.0-three-compile-routes.md b/docs/rfc-0.2.0-three-compile-routes.md new file mode 100644 index 0000000..24d4cb1 --- /dev/null +++ b/docs/rfc-0.2.0-three-compile-routes.md @@ -0,0 +1,158 @@ +# RFC 补充:三条编译路线对比 — Solid / Svelte / Vue Vapor → taskflow + +> Companion to `docs/rfc-0.2.0-typescript-dsl.md`(那是 Solid 路线)。本文回答: +> **① Svelte 路线(编译时指令、运行时擦除)长什么样?② Vue 最新模式(Vapor)长什么样?** +> 三条路线的本质区别 + 各自映射到 taskflow 的形态。 + +--- + +## 0. 先厘清:前端三条路线的本质区别 + +| | Solid 路线 | Svelte 5 路线 | Vue 3.6 Vapor 路线 | +|---|---|---|---| +| rune/symbol 是什么 | **真函数**(运行时返回 signal/对象) | **编译时指令**(运行时擦除) | **模板 → 编译成命令式操作** | +| 运行时还有这些函数吗 | ✅ 有(返回值有意义) | ❌ 擦除(变成执行代码) | ❌ 擦除(变成 DOM/操作指令) | +| 响应式底层 | signal(运行时) | signal(runes 编译后接 signal) | signal(编译后接 signal) | +| 写起来像 | "合法 JS + 钩子" | "带魔法的 JS" | "模板 + 少量脚本" | +| 可否脱离编译器跑 | ✅ 能(降级为普通调用) | ❌ 不能(runes 离开编译器无效) | ❌ 不能(模板离开编译器无效) | +| 混合模式 | 全用 | 全用 | **✅ 逐组件 opt-in(Vapor + 普通 Vue 共存)** | + +> 一句话:**Solid = "rune 是真函数";Svelte = "rune 是编译指令(被擦除)";Vue Vapor = "模板编译成命令式 + 可混合"**。 +> 三者**底层都是 signals**(2026 共识),区别在**语法层**和**编译策略**。 + +--- + +## 1. Svelte 路线:rune = 编译时指令(运行时擦除) + +### 核心机制 +`agent()` / `map()` / `gate()` **不是真函数** —— 它们是编译器识别的**指令**。 +`taskflow build` 把 `.tf.ts` **整个转换**成 FlowIR + 执行计划,运行时**不存在这些调用**。 +(就像 Svelte 的 `$state(0)` 编译后变成 `$.state(...)` 内部 API,`$state` 符号本身消失。) + +### 长什么样 +```ts +import { flow, agent, map, gate, $read, $emit } from "taskflow/compiler"; + +flow("audit-endpoints", () => { + $budget({ maxUSD: 3.0 }); + + const discover = agent("List every HTTP endpoint under src/routes...", { + output: $json<{ route: string; file: string }[]>(), + }); + // ↑ 编译后:discover 不是"调用 agent() 的返回值", + // 而是 FlowIR 里的一个 node id + 一个编译期生成的依赖边。 + + const audit = map(discover, (item) => + agent(`Audit ${item.route} in ${item.file}...`) + ); + // ↑ map() 在编译期展开:扫箭头函数体,生成 per-item 的 phase 模板。 + // 运行时根本没有 map 这个调用。 + + gate(audit, { agent: "reviewer", onBlock: "retry" }); +}); +``` + +### 编译产物(运行时实际执行的) +```jsonc +// FlowIR — agent/map/gate 这些"调用"全没了,只剩图: +{ "name": "audit-endpoints", + "nodes": [ + { "id": "discover", "kind": "agent", "inject": [], "emits": ["discover"], "task": "..." }, + { "id": "audit", "kind": "map", "inject": ["discover"], "emits": ["audit"] }, + { "id": "gate_0", "kind": "gate", "inject": ["audit"], "onBlock": "retry" } ], + "budget": { "maxUSD": 3.0 } } +``` + +### Svelte 路线的特征 +- **✅ 产物最精简**:运行时零 rune 开销,FlowIR 直接就是可执行图。 +- **✅ 静态分析最强**:编译器"看见"一切(每个 rune 都是编译期已知),verify/IR 天然精确。 +- **❌ 不能脱离编译器跑**:`.tf.ts` 不 `build` 就没法运行(REPL/调试不友好)。 +- **❌ 渐进迁移更难**:要么全转,要么不转(没有"半个 rune")。 +- **❌ 对 agent 心智负担略高**:`map(discover, item => ...)` 看着像函数,但 `item` 其实是编译期占位符,不能在 `item` 上做任意运行时运算(只能在编译期子集内)。 + +--- + +## 2. Vue Vapor 路线:模板编译成命令式 + 双模式共存 + +### 核心机制(借鉴 Vue 3.6 Vapor) +Vue Vapor 的两个关键特征: +1. **模板编译成细粒度命令式操作**(跳过中间表示的运行时开销)。 +2. **逐组件 opt-in** —— Vapor 组件和普通 Vue 组件**可以共存**于一个项目,渐进迁移。 + +映射到 taskflow:**JSON flow 和 TS flow 可以混用,逐 phase opt-in**。这是 Vue Vapor 路线**独有**的优势 —— Svelte/Solid 都是"全有或全无",Vapor 是"渐进"。 + +### 长什么样 +```ts +import { flow, agent, map, gate, vapor } from "taskflow"; + +// 一个 flow 可以"混用":部分 phase 用新 TS DSL,部分沿用 JSON 片段 +flow("audit", () => { + const discover = agent("List endpoints...", { output: json<{route:string}[]>() }); + + // 旧 JSON phase 可以内联引用(vapor 路线的"共存"特征) + const legacy = json`{ + "type": "map", "over": "${discover}", "as": "item", + "task": "Audit {{item.route}}", "agent": "analyst" + }`; + + gate(legacy, { agent: "reviewer" }); +}); +``` + +或者用 **vapor 标记**逐 flow 切换编译模式(像 Vue 的 `vapor: true`): +```ts +// @vapor ← 这个 flow 用 Vapor 编译(编译成细粒度执行图,零中间开销) +flow("audit", () => { + const d = agent("..."); + const a = map(d, (i) => agent(`audit ${i.route}`)); + gate(a); +}); +// 不加 @vapor 的 flow 仍走"解释执行"路径(兼容老行为/老 host) +``` + +### Vue Vapor 路线的特征 +- **✅ 渐进迁移最顺** —— JSON 和 TS 混用,逐 flow / 逐 phase opt-in(这正是 Vue 哲学:"渐进式框架")。 +- **✅ 双后端**:同一 DSL 编译到"解释执行"(老 host / 调试)或"Vapor 执行"(性能),按需选。 +- **✅ 最贴合 taskflow 现状** —— 现在已有 JSON flow + 4 个 host,Vapor 的"共存"理念迁移成本最低。 +- **❌ 复杂度最高**:要维护两套执行后端(解释器 + Vapor),工程量大。 +- **❌ "混用"语义要小心**:JSON phase 和 TS phase 的依赖/类型如何桥接是设计难点。 + +--- + +## 3. 三条路线映射到 taskflow 的对比 + +| 维度 | Solid 路线 | Svelte 路线 | Vue Vapor 路线 | +|---|---|---|---| +| rune 本体 | 真函数(运行时有效) | 编译指令(运行时擦除) | 模板/标记(可混用) | +| `.tf.ts` 不编译能跑吗 | ✅ 能(降级) | ❌ | ⚠️ 部分(标记决定) | +| 运行时开销 | 有(rune 函数调用) | 零(擦除) | 零(Vapor 模式下) | +| 静态分析精度 | 高(AST + 运行时) | **最高**(纯编译期) | 高 | +| 渐进迁移 | 中(可逐 flow) | 难(全有/全无) | **最顺**(逐 phase 混用) | +| agent 调试体验 | **最好**(可断点) | 差(编译后面目全非) | 中 | +| 实现工程量 | 中 | 中 | **大**(双后端) | +| 类型安全 | ✅ tsc 全程 | ✅ tsc + 编译期 | ✅ tsc | + +--- + +## 4. 我的推荐:Solid 路线为主,但偷 Vue Vapor 的"渐进共存" + +**主路线:Solid**(rune 是真函数)。理由: +1. **agent 调试** —— taskflow 的核心用户是 agent + 开发者,能断点、能 REPL 调试比"产物精简"重要。Svelte 路线编译后面目全非,调试地狱。 +2. **渐进迁移** —— Solid 路线下,`.tf.ts` 不 build 也能跑(降级成解释执行),和现有 JSON flow 共存天然成立。 +3. **实现风险低** —— 不用维护双后端(Vapor 路线的代价)。 +4. **类型体验最好** —— tsc 全程参与,rune 返回值有真实类型。 + +**但借鉴 Vue Vapor 的"双模式共存"**:taskflow 已有 JSON flow + 4 host,**保留 JSON 为一等公民**(`taskflow build flow.json` 和 `taskflow build flow.tf.ts` 都产出 FlowIR),等于白得了 Vue Vapor 的"渐进"优势,而不用承担双执行后端的复杂度。 + +**不选 Svelte 路线**的理由:运行时擦除对 taskflow 这种"agent 需要可观测、可调试、可 resume"的系统代价过大 —— resume 需要运行时状态,Svelte 路线擦除运行时后,很多 overstory 的 observed readSet 运行时追踪会变得别扭。 + +--- + +## 5. 一句话总结三条路线(给决策用) + +- **Solid**:"rune 是真函数,能调试,产物略重。" ← **推荐** +- **Svelte**:"rune 是编译指令,运行时擦除,产物最精简,但调试难、迁移硬。" +- **Vue Vapor**:"模板编译成命令式 + JSON/TS 可混用,迁移最顺,但要维护双后端。" + +> taskflow 选 **Solid + 偷 Vue 的"JSON 共存"**:rune 是真函数(可调试、可 resume), +> 同时 JSON 仍是一等公民(渐进迁移),得两者之长,避两者之短。 diff --git a/docs/rfc-0.2.0-typescript-dsl.md b/docs/rfc-0.2.0-typescript-dsl.md new file mode 100644 index 0000000..89552ca --- /dev/null +++ b/docs/rfc-0.2.0-typescript-dsl.md @@ -0,0 +1,331 @@ +# RFC: taskflow 0.2.0 — TypeScript 函数式 DSL 设计 + +> Status: design proposal, 2026-07-07. 方向已定(TS 函数式,见 +> `docs/0.2.0-research-frontend-paradigms.md` §4)。本文回答:**各种语法怎么表示**。 +> 路线:复用 TS(合法 TS + 编译器,像 Solid),不自创语法;显式 rune 函数(像 Svelte 5); +> 自动依赖追踪(像 TC39 signals);编译到 FlowIR 保留静态分析护城河。 + +--- + +## 0. 设计原则 + +1. **合法的 TypeScript** —— 不写解析器。`taskflow build` 把 `.tf.ts` 编译成 FlowIR(就像 Solid 把 JSX 编译成 DOM 更新)。agent 用已有的 TS 能力写,IDE/LSP/类型检查全免费。 +2. **显式 rune 函数** —— `agent()` `map()` `gate()` `loop()` `tournament()` 等是编译器识别的"符号"(像 Svelte 的 `$state`),但它们**就是普通 TS 函数**(像 Solid 的 `createSignal`)—— 不跑也能 typecheck。 +3. **依赖靠"读"自动建立** —— 读上游不再是字符串 `"{steps.discover.output}"`,而是函数调用 `discover.output`。编译器静态收集依赖(= declared readSet),运行时 onRead hook 收集观察依赖(= observed readSet)—— overstory M2+M3 自动达成。 +4. **类型即契约** —— `expect` 用 TS 泛型推导;output schema 用 TypeBox(已有)。 + +--- + +## 1. 一个 flow 的骨架 + +```ts +import { flow, agent, gate, reduce, map, tournament, loop, script, approval } from "taskflow"; + +export default flow("audit-endpoints", ({ args, budget }) => { + budget({ maxUSD: 3.0 }); + + const discover = agent("List every HTTP endpoint under src/routes...", { + agent: "scout", + tools: ["read", "grep", "ls"], + output: json<{ route: string; file: string }[]>(), // ← expect 契约,TS 泛型推导 + retry: { max: 2 }, + }); + + const audit = map(discover, (item) => + agent(`Audit ${item.route} in ${item.file} for missing auth...`, { + agent: "analyst", concurrency: 4, + }) + ); + + const screen = gate(audit, (findings) => + agent(`Cross-check the findings. Delete false positives...`, { agent: "reviewer" }) + ); + + return reduce([screen], (input) => + agent(`Write a prioritized remediation report from:\n${input.screen.output}`, { + agent: "doc-writer", final: true, + }) + ); +}); +``` + +**对比 JSON 版**(patterns.md archetype 1):同样的逻辑,JSON 是 25 行嵌套 + 字符串模板 `"{steps.discover.json}"` + 手写 `dependsOn`;TS 版是**线性、有类型、依赖靠传参自动建立**。 + +--- + +## 2. 10 种 phase 的语法(完整清单) + +### 2a. `agent` —— 单个 subagent +```ts +const files = agent("List .ts files under src/", { + agent: "scout", + output: json(), // expect 契约 (TypeBox 推导) + model: "fast", thinking: "high", // 可选覆盖 + cwd: "worktree", // 工作区隔离 + retry: { max: 3, backoffMs: 500, factor: 2 }, + timeout: 60_000, + cache: { scope: "cross-run", ttl: "7d" }, + optional: true, // 失败不阻断 +}); +``` + +### 2b. `map` —— 动态扇出(一个 item 一个 agent) +```ts +const audits = map(files, (f) => + agent(`audit ${f}`, { agent: "analyst", label: f, concurrency: 4 }) +); +// audits: Result[] —— 类型自动是 agent 输出的数组 +``` + +### 2c. `parallel` —— 静态并发分支 +```ts +const [auth, validation, perf] = parallel([ + agent("audit auth", { agent: "analyst" }), + agent("audit input validation", { agent: "analyst" }), + agent("audit perf", { agent: "analyst" }), +]); +``` + +### 2d. `gate` —— 质量门(VERDICT: PASS/BLOCK) +```ts +const verified = gate(audits, { agent: "reviewer", onBlock: "retry" }, (findings) => + `Cross-check findings. VERDICT: BLOCK if any HIGH remains, else PASS.\n${findings}` +); + +// eval gate —— 零 token 机器门(不调 LLM) +const typecheck = gate.automated( + () => script("npx tsc --noEmit"), + { pass: "{exit === 0}" } // eval 条件 +); +``` + +### 2e. `reduce` —— 聚合 N → 1 +```ts +const summary = reduce([auth, validation, perf], (parts) => + agent(`Merge into one ranked summary:\n${parts.auth.output}`, { agent: "doc-writer" }) +); +``` + +### 2f. `tournament` —— best-of-N + judge +```ts +const best = tournament({ + mode: "best", + judgeAgent: "final-arbiter", + judge: "Judge on: correctness, blast radius, migration cost. End with WINNER: .", + branches: [ + agent("conservative fix: minimal diff", { agent: "analyst" }), + agent("optimal correctness: schema change ok", { agent: "analyst" }), + agent("adversary: what breaks? propose survivor", { agent: "critic" }), + ], +}); +``` + +### 2g. `loop` —— 循环到条件/收敛/上限 +```ts +const fixed = loop({ + until: "{steps.check.exit === 0}", + maxIterations: 5, + convergence: "{steps.check.output hash unchanged}", + body: (prev) => ({ + check: script("npx tsc --noEmit"), + fix: agent(`Fix the type errors:\n${prev.check.output}`, { agent: "executor" }), + }), +}); +``` + +### 2h. `approval` —— 人机交互 +```ts +const approved = approval({ + request: "Review this plan before execution", + input: plan.output, + choices: ["approve", "reject", "edit"], +}); +``` + +### 2i. `flow` —— 调用已保存的子 taskflow +```ts +const result = flow("deep-research", { question: "Node.js permission model v20→v22" }); +``` + +### 2j. `script` —— 零 token shell 步骤 +```ts +const lint = script("npx eslint src/ --format json", { cwd: "dedicated" }); +// lint.exit, lint.stdout, lint.stderr +``` + +--- + +## 3. 依赖怎么自动建立(替代 `{steps.X.output}`) + +**核心机制:phase 是一个带 `.output` 的值对象。读它 = 建立依赖。** + +```ts +const discover = agent("find files", { ... }); +const audit = map(discover, ...); // ← audit 依赖 discover,因为 discover 被传入 +const report = reduce([audit], (i) => + agent(`report from ${i.audit.output}`) // ← report 依赖 audit,因为读了 .output +); +``` + +两层追踪(= overstory M2+M3,自动达成): +- **静态(编译时)**:编译器扫 AST,`discover`/`audit` 被引用 → declared readSet(FlowIR inject/emits)。 +- **动态(运行时)**:`.output` 的 getter 触发 onRead hook → observed readSet@version。 + +> 不再有 `dependsOn: ["discover"]` 这种手写字符串。**传参即依赖**,和写普通函数一样。 + +--- + +## 4. 控制流(when / join / retry / budget / onBlock) + +```ts +flow("x", ({ when, budget }) => { + budget({ maxUSD: 5, maxTokens: 200_000 }); + + const nightly = agent("...", { when: "{env.NIGHTLY === '1'}" }); + + const anyReviewer = parallel([ + agent("review a", { join: "any" }), // OR-join: 任一完成即可 + agent("review b", { join: "any" }), + ]); + + const robust = agent("...", { + retry: { max: 3, backoffMs: 1000, factor: 2 }, + onBlock: "retry", // gate BLOCK 时重试上游而非 halt + }); +}); +``` + +`when` 两种形态: +- **字符串条件**(兼容现有 eval):`when: "{env.X} contains 'prod'"` +- **TS 函数(新)**:`when: ({ env, steps }) => env.MODE === "prod"` —— 编译器把函数体编译成 eval 条件,保留零 token 静态分析。 + +--- + +## 5. args / 传参 / 复用 + +```ts +export default flow("audit", ({ args }) => { + args.declare({ dir: { default: "src/routes", type: "string" } }); + const d = agent(`audit ${args.dir}...`); // args 是响应式的信号 + return d; +}); + +// 调用:taskflow run audit.ts --args '{"dir":"src/api"}' +// 或保存后:/tf:audit src/api +``` + +**跨文件复用**(Svelte 5 runes 的关键突破 —— 响应式超越组件边界): +```ts +// lib/flows.ts —— 可复用的 flow 片段,不是完整 flow +export const auditPattern = (target: Signal) => + map(agent(`list files in ${target}`), (f) => agent(`audit ${f}`)); + +// my-flow.ts —— 组合复用 +import { auditPattern } from "./lib/flows.ts"; +export default flow("x", () => auditPattern(read("src/routes"))); +``` + +--- + +## 6. 类型推导(这是 TS DSL 杀手锏) + +```ts +const discover = agent("...", { output: json<{ route: string; file: string }[]>() }); +// ^? Phase<{ route: string; file: string }[]> + +const audit = map(discover, (item) => agent(`audit ${item.route}`)); +// ^? Phase +// item.route 字符串 —— 编译期就知道,拼错就报错 +``` + +**JSON DSL 现在的痛点**:`{item.rout}` 拼错要到运行时才发现;`"{steps.discover.json}"` 是纯字符串,没有类型。 +**TS DSL**:`item.route` 拼错 = 编译失败;整个 flow 在 `taskflow build`(→ FlowIR)前先过 `tsc`。 + +--- + +## 7. 编译路径(保留护城河) + +``` +.tf.ts (合法 TS,agent 写) + │ tsc 类型检查 (agent 拼错立刻报错) + ▼ +taskflow build ← 扫 rune 函数调用,收集依赖,生成 + ▼ +FlowIR (内容寻址, hash) ← /tf verify 在这里跑,零 token + ▼ +runtime execute (observed readSet 自动追踪 → cache + recompute) +``` + +- **`/tf verify` / `/tf ir`** 仍然工作(在 FlowIR 层),护城河不丢。 +- **JSON 仍然支持**:`taskflow build flow.json` → 同一个 FlowIR。**双向可编译**(DSL ↔ JSON),老用户零迁移成本。 + +--- + +## 8. shorthand(简单任务不用写完整 flow) + +```ts +// 内联在 host agent 的调用里,等价于现在的 task/tasks/chain +taskflow.run({ task: "summarize src/", agent: "explorer" }); +taskflow.parallel([{ task: "audit auth" }, { task: "audit perf" }]); +taskflow.chain([{ task: "step1" }, { task: "step2 from {prev}" }]); +``` + +--- + +## 9. 完整对照:Archetype 5 (tournament) JSON → TS + +**JSON(现状):** +```jsonc +{ "id": "strategy", "type": "tournament", "mode": "best", + "judgeAgent": "final-arbiter", + "judge": "Judge on: correctness, blast radius, migration cost. WINNER: .", + "branches": [ + { "task": "conservative fix", "agent": "analyst" }, + { "task": "optimal correctness", "agent": "analyst" }, + { "task": "adversary", "agent": "critic" } ], + "dependsOn": ["context"], "final": true } +``` + +**TS(0.2.0):** +```ts +const context = agent("gather context", { agent: "scout" }); + +const strategy = tournament({ + mode: "best", + judgeAgent: "final-arbiter", + judge: "Judge on: correctness, blast radius, migration cost. WINNER: .", + branches: [ + agent("conservative fix", { agent: "analyst" }), + agent("optimal correctness", { agent: "analyst" }), + agent("adversary: what breaks?", { agent: "critic" }), + ], +}); // dependsOn 自动 = [context],final 用 return 标记 +``` + +**收益**:少 3 个字段(`dependsOn` 自动、`final` 用 return、`id` 可省);branches 是真数组(可 `.map()`/条件构造);类型检查。 + +--- + +## 10. Open questions(下一步要定的) + +1. **rune 函数是"真函数"还是"纯编译器符号"?** + - Solid 路线:真函数,运行时也有意义(返回 signal/phase 对象)—— 更灵活、可在 REPL 调试。 + - Svelte 路线:纯编译器符号(运行时被擦除)—— 更轻、bundle 更小。 + - 建议:**Solid 路线**(agent 调试 + 渐进迁移更重要)。 + +2. **响应式粒度** —— `discover.output` 读一次建一条边,还是整个 phase 是一个 signal? + - 建议沿用 overstory 的 **phase 级 signal**(LLM 调用是昂贵原语,不需要 DOM 那种节点级粒度)。 + +3. **JSON 共存策略** —— 双向编译 vs JSON 仅作为编译产物? + - 建议双向(JSON 仍是一等公民,DSL ↔ JSON 互转),保证 saved flow / 老用户零成本。 + +4. **`when`/`eval` 的 TS 函数编译** —— 把 TS 谓词编译成 eval 条件串,需要限定子集(禁止副作用/闭包捕获)。 + - 这是技术上最 tricky 的一点,需单独 RFC。 + +--- + +## 11. 北极星 + +> **taskflow 0.2.0: Write agent workflows like Solid components.** +> **TypeScript-native, compiler-tracked dependencies, only what changed re-runs.** +> **The first framework to bring 2026's frontend reactivity consensus to agents.** diff --git a/docs/rfc-library-reuse-review.md b/docs/rfc-library-reuse-review.md index 8ac630e..df86412 100644 --- a/docs/rfc-library-reuse-review.md +++ b/docs/rfc-library-reuse-review.md @@ -24,7 +24,7 @@ | ID | 问题 | 解决方式 | |---|---|---| | A3 | `why`/`reuseHint` 生成机制未定义——是模板还是 LLM?与零 token 承诺矛盾 | §5.3:定义 Tier 1 始终模板化(零 token),Tier 2 LLM 增强留 Phase 3;给出完整模板代码 | -| A5 | 无 `taskflow_save` / `taskflow_search` MCP 工具——codex/claude/opencode 无法保存或搜索 | §5.4:新增两个 MCP 工具,含完整 input schema;pi 适配器扩展 `purpose`/`tags`/`notes` 透传 | +| A5 | 无 `taskflow_save` / `taskflow_search` MCP 工具——codex/claude/opencode/grok 无法保存或搜索 | §5.4:新增两个 MCP 工具,含完整 input schema;pi 适配器扩展 `purpose`/`tags`/`notes` 透传 | | A6 | reuseCount 两半都未定义:无检测机制 + 无并发合约 | §七 Phase 1:新增 `reusedFromSearch` 标记,递增在 tool handler 层 + `withLock` 保护,sub-flow 不计 | | A7 | embedding 输入文本构造完全未定义——直接影响语义检索质量 | §3.4:给出完整算法(5 段拼接)+ 512 字符预算 + 截断优先级 | | C3 | 降级矩阵遗漏 malformed-vector 场景(维度错误/NaN/Infinity) | §4.1 + §4.3:新增 `validateEmbedding()` 接口,写入前必校验,失败 → `null` + warn | diff --git a/docs/rfc-library-reuse.md b/docs/rfc-library-reuse.md index 9c8d75d..d7ef120 100644 --- a/docs/rfc-library-reuse.md +++ b/docs/rfc-library-reuse.md @@ -264,9 +264,9 @@ pi 适配器在构造 tool handler 时创建 `LibraryDeps` 并传给 `saveFlowWi - **冷启动警告**:文档须在 `command` kind 段落显著标注:「⚠️ 若 embedding 工具有冷启动延迟(如 board-cli 60s+),每次 save 都会阻塞等冷启动。推荐改用 `http` kind 指向已预热的 proxy。」 - **实现约束(R2R1 安全修复)**:**必须使用 `child_process.spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"] })`**,禁止使用 `child_process.exec()` 或任何 shell 调用。输入文本通过 `stdin.write()` 传入,不经过 shell 解析。此约束消除 command 数组元素中 shell 元字符(`; | & $()` 等)的注入风险。 -**pi 适配器的默认 wiring**:pi 适配器读 `settings.json`,若 `taskflow.embedder` 存在且 `library.enabled`,构造一个 `Embedder` 实现注入 `LibraryDeps`。**其他宿主(codex/claude/opencode)的 MCP server 同样读这个配置**——配置在 `settings.json`,跨宿主一致。 +**pi 适配器的默认 wiring**:pi 适配器读 `settings.json`,若 `taskflow.embedder` 存在且 `library.enabled`,构造一个 `Embedder` 实现注入 `LibraryDeps`。**其他宿主(codex/claude/opencode/grok)的 MCP server 同样读这个配置**——配置在 `settings.json`,跨宿主一致。 -**跨宿主配置路径说明(C4 修复)**:所有宿主读 `~/.pi/agent/settings.json`,这是 taskflow 的现有跨宿主惯例。非 Pi 用户(codex/claude/opencode):须手动创建此文件,或使用 `-taskflow init`(若该命令提供)。**不引入备用路径**(如 `~/.taskflow/settings.json`),以避免偏离现有跨宿主约定。 +**跨宿主配置路径说明(C4 修复)**:所有宿主读 `~/.pi/agent/settings.json`,这是 taskflow 的现有跨宿主惯例。非 Pi 用户(codex/claude/opencode/grok):须手动创建此文件,或使用 `-taskflow init`(若该命令提供)。**不引入备用路径**(如 `~/.taskflow/settings.json`),以避免偏离现有跨宿主约定。 ### 4.3 降级矩阵(核心保证:永远能搜) @@ -288,7 +288,7 @@ pi 适配器在构造 tool handler 时创建 `LibraryDeps` 并传给 `saveFlowWi ```jsonc // pi 形式 { "action": "search", "query": "审计 API endpoint 是否缺少鉴权", "limit": 5 } -// MCP 形式(codex/claude/opencode)—— 新工具 taskflow_search(见 §5.4) +// MCP 形式(codex/claude/opencode/grok)—— 新工具 taskflow_search(见 §5.4) { "name": "taskflow_search", "arguments": { "query": "...", "limit": 5 } } ``` diff --git a/docs/rfc-taskflow-vs-claude-code-workflows.md b/docs/rfc-taskflow-vs-claude-code-workflows.md new file mode 100644 index 0000000..720bac2 --- /dev/null +++ b/docs/rfc-taskflow-vs-claude-code-workflows.md @@ -0,0 +1,273 @@ +# RFC: taskflow vs Claude Code (2026-07) — capability gap & what to build next + +> Status: research notes, 2026-07-07. Source: Claude Code official docs +> (`code.claude.com/docs/en/*`, fetched 2026-07-07) + taskflow source. +> Purpose: decide which features close the gap and push taskflow ahead of +> Claude Code's just-shipped **Dynamic Workflows** (GA 2026-07-03, PR-era +> research preview May 2026). + +--- + +## 0. TL;DR + +Claude Code shipped **Dynamic Workflows** — its first feature that shares +taskflow's core thesis ("move the plan into code; only the final answer reaches +the conversation"). The overlap is real and large. **But taskflow still leads on +orchestration depth** (12 phase types + DAG + cross-run caching + multi-host), +while **Claude Code leads on two things taskflow lacks**: + +1. **Zero-author orchestration** — Claude *writes* the workflow script for you + (`ultracode` / "use a workflow"). taskflow forces the LLM/user to hand-author + the JSON DSL every time. +2. **A lifecycle hook system** — ~30 events (PreToolUse, SubagentStart, Stop, + TaskCompleted, TeammateIdle, WorktreeCreate, PreCompact …) that users wire + shell/HTTP/LLM handlers into. taskflow has only an internal `onProgress`. + +The single highest-leverage feature to "fully surpass" is **#1: a +zero-author / auto-compose mode** — let the host LLM describe a task and have +taskflow draft the DSL, verify it (zero tokens), and run it. This neutralizes +Claude Code's biggest UX moat while keeping taskflow's deeper engine. + +--- + +## 1. Claude Code's multi-agent surface (4 primitives) + +Claude Code now offers **four** ways to run multi-step work. The official +"who holds the plan?" matrix: + +| Primitive | Plan lives in | Scale | Comms | Repeatable | +|---|---|---|---|---| +| **Subagents** | Claude's context (turn-by-turn) | a few / turn | report back only | worker *definition* | +| **Skills** | Claude's context (follows prompt) | a few / turn | — | the *instructions* | +| **Agent Teams** *(experimental)* | a **lead agent** supervising peers | handful of long-running peers | **peers message each other** + shared task list | the *team definition* | +| **Dynamic Workflows** | a **script the runtime executes** | **dozens→1000 agents/run** | via script variables | **the orchestration itself** | + +### 1a. Dynamic Workflows (the direct competitor — GA v2.1.154+) + +- A workflow = a **JavaScript script** Claude writes. Runtime executes it in the + background; session stays responsive. +- Two primitives: `agent(prompt, {schema, label})` (spawn one) and + `pipeline(items, fn)` (one agent per item — **this is `map`**). +- Top-level `await`; intermediate results live in **script variables**, not + Claude's context. Only the final return value reaches the conversation. +- **Resume**: stopped runs resume within the same session; completed agents + return cached results. *(Does NOT survive session exit — fresh on next session.)* +- **`args` global** for saved-workflow input. Saved as `.claude/workflows/.js` + → becomes a `/` slash command (project-shared or `~/.claude` personal). +- **Adversarial patterns built into examples**: independent agents review each + other's findings, vote on claims, draft a plan from several angles and weigh + them. (Bundled `/deep-research` fans out web search → cross-check → vote → + cited report; unverified claims filtered.) +- **Limits**: 16 concurrent agents, **1000 agents/run** hard cap, no mid-run user + input (only permission prompts can pause), no direct fs/shell from the script + itself (agents do all I/O). +- **Size guideline** (`/config`): small (<5) / medium (<15) / large (<50) / + unrestricted — advice sent to Claude. +- **ultracode** effort mode: xhigh reasoning + auto-workflow for *every* + substantive task ("understand → change → verify" can chain several workflows). +- **Permission gating** per-run: View raw script / Yes / Always / No; Ctrl+G to + edit; subagents always run `acceptEdits` + inherit tool allowlist. + +### 1b. Agent Teams (experimental, `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`) + +- Multiple **full Claude Code instances** as teammates; one is the **lead**. +- Each teammate = own context window, fully independent. +- **Teammates message each other directly** (vs subagents which only report to + caller). Shared **task list** with self-coordination (assign/claim tasks). +- Quality gates enforced via **hooks**. Competing-hypotheses / parallel-review + / cross-layer (frontend/backend/tests) ownership patterns. +- *Known weak spots (CC admits):* session resumption, task coordination, + shutdown behavior, orphaned tmux sessions. + +### 1c. Subagents (worker primitive) + +- Single session, own context window, custom system prompt + tool subset + + independent permissions. Returns summary to caller. +- Built-ins: **Explore** (read-only, capped at Opus), **Plan** (plan-mode + research), **General-purpose**. Custom ones via `.claude/agents/*.md`. +- Model routing per subagent (cost control). Delegation decided by Claude from + each subagent's `description`. + +### 1d. Hooks (~30 lifecycle events) + +The richest hook system of any host. Users wire **shell / HTTP / LLM-prompt / +agent / MCP-tool** handlers into: + +- **Session**: SessionStart, SessionEnd, Setup +- **Turn**: UserPromptSubmit, UserPromptExpansion, Stop, StopFailure, + Notification, MessageDisplay +- **Tool loop**: PreToolUse (can block/defer), PermissionRequest, PermissionDenied, + PostToolUse, PostToolUseFailure, PostToolBatch +- **Agents/tasks**: SubagentStart, SubagentStop, TaskCreated, TaskCompleted, + TeammateIdle +- **Env/fs**: ConfigChange, CwdChanged, FileChanged, WorktreeCreate, + WorktreeRemove, PreCompact, PostCompact +- **Elicitation** (interactive prompts): Elicitation, ElicitationResult +- Hook **decision control**: exit code 2 / JSON `decision` can block, inject + context, defer tool calls, persist env vars, force retry. + +--- + +## 2. Side-by-side: taskflow vs Claude Code + +Legend: ✅ has it · ⚠️ partial / weaker · ❌ missing. + +### 2a. Orchestration primitives + +| Capability | taskflow | Claude Code | +|---|---|---| +| Single worker (agent) | ✅ `agent` | ✅ `agent()` / Subagents | +| Static parallel branches | ✅ `parallel` | ⚠️ via script (Promise.all) | +| Dynamic fan-out over array | ✅ `map` | ✅ `pipeline()` | +| Quality gate (halt on BLOCK) | ✅ `gate` (score/scored/eval) | ⚠️ manual in script (agent review) | +| Aggregate N → 1 | ✅ `reduce` | ⚠️ manual in script | +| Best-of-N + judge | ✅ `tournament` | ⚠️ "weigh several angles" — manual | +| Loop until done/converge | ✅ `loop` (condition/convergence/cap) | ⚠️ manual `while` in script | +| Human approval | ✅ `approval` (approve/reject/edit) | ⚠️ per-run launch prompt only | +| Sub-flow composition | ✅ `flow` (saved sub-taskflow) | ✅ saved workflow → `/` | +| Zero-token shell step | ✅ `script` | ❌ (no fs/shell from script) | +| **Explicit DAG + `dependsOn`** | ✅ topo-sorted, cycle-detected | ❌ implicit (script control flow) | +| Static verification (pre-run) | ✅ `verify` / `compile` (0 tokens) | ❌ | +| `when` guards / `join: any` | ✅ | ❌ | + +> **taskflow wins on orchestration vocabulary.** Claude Code's workflow script +> can *emulate* gate/tournament/loop in JS, but it has no first-class primitive, +> no static verifier, and no explicit DAG — correctness rests on the LLM writing +> the right JS each time. + +### 2b. Reliability & resumability + +| Capability | taskflow | Claude Code | +|---|---|---| +| Retry w/ exponential backoff | ✅ `retry` | ❌ (LLM re-spawns manually) | +| Per-call timeout | ✅ `timeout` | ❌ | +| Output contract (`expect`) | ✅ schema-validated, retryable | ⚠️ `agent({schema})` validates result | +| Cost ceiling | ✅ `budget` {maxUSD, maxTokens} | ⚠️ "size guideline" (advice only) | +| Resume **within** session | ✅ | ✅ (cached agent results) | +| Resume **across** sessions | ✅ (durable run state) | ❌ "fresh on next session" | +| **Cross-run memoization cache** | ✅ content-addressed, git/glob/file/env fingerprints, TTL | ❌ | +| Incremental recompute (why-stale) | ✅ `recompute` / `why-stale` | ❌ | +| Fail-open invariants | ✅ documented + enforced | ⚠️ ad hoc | + +> **taskflow wins on durability + caching.** Claude Code workflows are +> session-scoped and have no content-addressed cache — re-running a similar task +> re-spawns everything. taskflow's cross-run cache + incremental recompute is a +> genuine moat. + +### 2c. Context & isolation + +| Capability | taskflow | Claude Code | +|---|---|---| +| Only final output to host context | ✅ | ✅ (script variables) | +| Per-phase workspace isolation | ✅ `cwd: temp/dedicated/worktree` | ✅ worktrees (per session/phase) | +| Shared blackboard (horizontal) | ✅ `ctx_read` / `ctx_write` | ⚠️ Agent Teams shared task list | +| Vertical supervision | ✅ `ctx_report` / `ctx_spawn` (nested DAG) | ⚠️ Agent Teams lead↔teammates | +| Inter-agent direct messaging | ❌ (via shared tree only) | ✅ Agent Teams | + +> **Roughly even.** taskflow's Shared Context Tree is more structured (a +> supervised blackboard with depth caps + budget); CC's Agent Teams are more +> "chatty" (direct peer messaging) but CC admits coordination is buggy. + +### 2d. Authoring & UX + +| Capability | taskflow | Claude Code | +|---|---|---| +| **LLM auto-writes the orchestration** | ❌ (hand-author DSL) | ✅ **ultracode / "use a workflow"** | +| Authoring format | JSON DSL (+ JSONC) | JS script (Claude-written) | +| Verify-before-run (0 token) | ✅ `verify`/`compile` + Mermaid | ❌ (View raw script, manual) | +| Saved flow → slash command | ✅ `/tf:` | ✅ `/` | +| `args` input to saved flow | ✅ `{args.X}` | ✅ `args` global | +| Interactive init | ✅ `/tf init` | ✅ `/config` toggles | +| Live progress TUI | ✅ runs-view + approval-view | ✅ `/workflows` + task panel | +| Searchable flow library | ✅ `search` (structural + CJK) | ❌ | +| **Multi-host** (Pi/Codex/Claude/OpenCode) | ✅ | ❌ (Claude Code only) | + +> **Claude Code wins on zero-author UX; taskflow wins on portability + library.** +> The auto-compose gap is the one that matters most for adoption. + +### 2e. Lifecycle hooks + +| Capability | taskflow | Claude Code | +|---|---|---| +| User-configurable event hooks | ❌ (internal `onProgress` only) | ✅ ~30 events, 5 handler types | +| Pre/post-phase side effects | ❌ | ✅ PreToolUse/PostToolUse/etc. | +| Inject context / block / defer | ❌ | ✅ decision control | + +> **Claude Code wins decisively.** This is the cleanest gap. + +--- + +## 3. Where taskflow is clearly ahead (defend these) + +1. **Orchestration vocabulary** — 12 phase types vs 2 primitives. gate / + tournament / loop / approval / reduce are first-class, statically verified. +2. **Cross-run caching + incremental recompute** — unique. CC re-runs everything. +3. **Cross-session resume** — CC workflows die with the session. +4. **Multi-host** — runs on Pi, Codex, Claude Code, OpenCode. CC is locked to + Claude Code. +5. **Static verification** — `verify`/`compile` catch DAG bugs before spending a + token; CC has nothing equivalent. +6. **Searchable flow library** — `taskflow_search` with structural + CJK scoring; + CC has no reuse-discovery. + +## 4. Where Claude Code is ahead (the gaps to close) + +| Gap | Severity | Why it matters | +|---|---|---| +| **G1. No zero-author / auto-compose** | 🔴 critical | CC's `ultracode` lets a user say "audit every route" and gets a workflow. taskflow demands a hand-written DSL. This is the adoption moat. | +| **G2. No lifecycle hook system** | 🟠 high | Power users wire CI, formatters, slack pings, permission policy into CC hooks. taskflow can't. | +| **G3. No inter-agent direct messaging** | 🟡 medium | Agent Teams let peers argue. taskflow's shared tree is structured but less "conversational". (Likely a *non-goal* — structured beats chatty.) | +| **G4. Smaller scale ceiling** | 🟡 medium | CC: 1000 agents/run. taskflow has no published cap but isn't tuned for 1000-way fan-out UX. | +| **G5. No bundled "killer" workflow** | 🟡 medium | CC ships `/deep-research`. taskflow ships the *engine* but no marquee one-command flow. | + +--- + +## 5. Recommended features for 0.1.7 (to "fully surpass") + +Ranked by leverage. Each is sized for one release. + +### 🥇 P0 — Auto-compose (`action=compose`) +**Closes G1.** New `taskflow_compose` action (and a `/tf compose` command): +the host LLM describes the task in prose → a dedicated **planner agent** drafts +a taskflow DSL → `verify` runs (0 tokens) → if clean, optionally `run` it; +if not, the planner revises (a `loop` until `verify` passes or cap). Reuses the +existing `loop` + `gate` + `verify` primitives — the feature *is* a taskflow +flow that writes taskflow. This neutralizes CC's biggest UX edge while keeping +taskflow's deeper, verifiable engine. **CC cannot match the static-verify step.** + +### 🥈 P1 — Lifecycle hooks (`on` phase field + global hooks) +**Closes G2.** Two layers: +- **Per-phase hooks**: `on: { start, end, block }` running a `script` (shell, + zero tokens) — covers PreToolUse/PostToolUse/onBlock patterns. +- **Global run hooks**: `hooks: { onPhaseStart, onPhaseEnd, onRunComplete, + onApproval }` wired to shell commands, emitting the same JSON event shape CC + uses (so existing CC hook scripts port). Decision control: exit 2 / JSON + `decision` can block or inject. + +### 🥉 P2 — Bundled marquee flow: `/tf:deep-research` (and `/tf:audit`) +**Closes G5.** Ship a library flow that fans out web/source search → +cross-check → vote → cited report, exactly mirroring CC's `/deep-research`, +*but* running on all four hosts and cacheable across runs. Demonstrates the +engine's superiority on a task users already recognize. Plus `/tf:audit` +(per-file adversarial review → ranked summary) — the other CC headline example. + +### P3 — Scale + observability for large fan-out +**Closes G4.** Publish a `maxAgents`/`concurrency` config, add a +"1000-agents" progress mode to the runs TUI (phases collapse to counts), and a +`size` advice field mirroring CC's small/medium/large so the composer aims +right. + +### P4 (optional) — Peer messaging via shared tree +**Closes G3.** Add `ctx_message` (addressed, non-supervised) on top of the +existing Shared Context Tree, so peers can "argue" without going through a +supervisor. Low priority — structured reuse usually beats chat. + +--- + +## 6. The one-sentence pitch after 0.1.7 + +> *taskflow: the only multi-agent orchestrator where the LLM writes the flow, +> the engine **verifies** it before a token is spent, results are **cached +> across runs**, and it runs on **every host** — not just Claude Code.* + +The two words Claude Code cannot match: **verify** and **cached**. diff --git a/examples/0.2.0-app-delivery-platform/README.md b/examples/0.2.0-app-delivery-platform/README.md new file mode 100644 index 0000000..e5e86b8 --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/README.md @@ -0,0 +1,44 @@ +# 0.2.0 Demo: Autonomous Software Delivery Platform + +> ⚠️ **愿景草图,非 0.2.0 首版可运行样例。** + +这个 demo 故意展示了 taskflow 0.2.0 DSL 的**完整愿景上限** —— 一个 10 文件的 +多 flow agent 应用(像 Vue/Solid 应用那样有 app/flows/components/stores/lib 结构)。 + +**但其中部分特性在 [`docs/rfc-0.2.0-dsl-syntax.md`](../../docs/rfc-0.2.0-dsl-syntax.md) §7 +明确标为 post-0.2.0**(依赖全局响应式运行时 / Shared Context Tree,首版不含): + +| post-0.2.0 特性 | 用在哪 | 为什么 post | +|---|---|---| +| `$derived` / `$state` | `app.ts`, `stores/dashboard.ts`, `flows/plan.ts` | 全局响应式派生状态,需响应式运行时 | +| `$store` | `stores/dashboard.ts` | 全局共享 store,需 Shared Context Tree | +| `read()` / `write()` | `app.ts`, `stores/dashboard.ts`, `components/review-changes.ts` | 读写全局 store | +| `flow.component(...)` | `flows/implement.ts`, `components/*` | 带 props 的可复用子 flow,props 响应式语义待定 | + +文件内用到这些特性的地方都标了 `// [post-0.2.0]`。 + +## 0.2.0 首版能跑的部分 + +- 所有 `agent` / `map` / `parallel` / `gate` / `reduce` / `loop` / `tournament` / + `approval` / `script` rune +- 编译期类型(`json()`、`item.route` 类型检查) +- 自动依赖追踪(编译期收集 `.output` 引用) +- `when` 字符串条件 + 受限 TS 谓词子集 + +## 这个 demo 的价值 + +它是**愿景交流工具** —— 展示"当 0.2.x 补齐全局响应式 + flow.component 后,agent +应用能写成什么样"。不是 0.2.0 首版的参考实现。要看 0.2.0 首版能跑的样例,看 +`taskflow new` 生成的骨架 + RFC §B 的 audit-endpoints 示例。 + +## 文件结构(像 Vue/Solid 应用) + +``` +app.ts 主入口(编排整个交付流水线) ← 含最多 post-0.2.0 特性 +├── types/ 领域模型 +├── config/ 声明式配置 +├── lib/ 工具函数(编译期求值) +├── stores/ 响应式全局 store ← post-0.2.0 ($store/$derived) +├── flows/ 业务流程(plan / implement) +└── components/ 可复用 phase 组件 ← post-0.2.0 (flow.component) +``` diff --git a/examples/0.2.0-app-delivery-platform/app.ts b/examples/0.2.0-app-delivery-platform/app.ts new file mode 100644 index 0000000..87fd42d --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/app.ts @@ -0,0 +1,129 @@ +/** + * ⚠️ 愿景草图 —— 含 post-0.2.0 特性($derived/$store/read/write/flow.component)。见 ./README.md。 + * + * taskflow 0.2.0 应用 —— 自主软件交付平台 + * + * issue → 规划 → 实现 → 审查 → 安全审计 → 自愈 → 置信度决策 → 交付/转人工 + * + * 这是整个应用的入口。展示 Solid 路线能写出的【最复杂】组合: + * - 动态路由(按 issue 复杂度走不同流水线) + * - 多 flow 互调(plan / implement / 子组件) + * - 全局响应式 store(dashboard)读写 + * - 条件编排(when) + 分支 + * - 置信度驱动的自动决策($derived 链) + * - 预算池管理 + 耗尽熔断 + * + * 依赖图完全靠读取自动建立 —— 没有一个手写 dependsOn。 + * 改一个 issue 的字段,只有相关分支重算(overstory 增量)。 + */ + +import { flow, agent, gate, approval, parallel, script, $derived, $store, read, write, when, json } from "taskflow"; // $derived/$store/read/write = [post-0.2.0] +import planFlow from "./flows/plan.ts"; // [post-0.2.0] planFlow 内部用 $derived +import implementFlow from "./flows/implement.ts"; // [post-0.2.0] implementFlow 用 flow.component +import { reviewChanges } from "./components/review-changes.ts"; // [post-0.2.0] flow.component +import { securityAudit } from "./components/security-audit.ts"; // [post-0.2.0] flow.component +import { dashboard, remainingBudget } from "./stores/dashboard.ts"; // [post-0.2.0] $store/$derived +import { config } from "./config/app.ts"; +import { needsSecurityReview, inferComplexity } from "./lib/utils.ts"; +import type { Issue, DeliveryReport, DeliveryStatus } from "./types/domain.ts"; + +export default flow("deliver", ({ args, budget }) => { + args.declare({ issue: { type: "object" as const } }); + budget(config.budget); // 绑定全局预算池 + + const issue = args.issue as Issue; + + // ── 注册到全局看板(写 store;后续派生自动更新) ───────────────────── + write(dashboard.active, (m) => m.set(issue.id, issue)); + + // ── 动态路由:按复杂度选流水线(像 Vue Router 的路由表) ───────────── + const pipeline = $derived(() => config.routing[issue.complexity]); // "fast-path" | "standard" | "rigorous" + + // ── 阶段1:规划(调子 flow) ──────────────────────────────────────── + const plan = when(() => pipeline.output !== "fast-path", + () => planFlow({ issue }), + () => agent( // fast-path:跳过 tournament,直接简单实现 + `简单任务,直接做:${issue.title}\n${issue.body}`, + { agent: "executor", output: json() } + ) + ); + + // ── 阶段2:实现(调子 flow;依赖 plan) ───────────────────────────── + const diff = when(() => pipeline.output !== "fast-path", + () => implementFlow({ plan: plan.output, repo: issue.repo }), + () => script(`cd ${issue.repo} && git diff`) // fast-path 直接拿 diff + ); + + // ── 阶段3:代码审查(复用组件;读 diff + 改动文件) ────────────────── + const changedFiles = $derived(() => plan.output.affectedFiles ?? []); + const review = reviewChanges(diff, changedFiles); // 多视角 + tournament + auto-gate + + // ── 阶段4:安全审计(仅高风险 issue;条件编排) ────────────────────── + const mustAudit = $derived(() => + needsSecurityReview(issue.labels, [...config.securityGate.triggerLabels]) + ); + const security = when(() => mustAudit.output, + () => securityAudit(diff, $derived(() => plan.output.riskAreas ?? [])), + () => agent("no security review needed", { agent: "executor-fast", optional: true }) + ); + + // ── 阶段5:置信度计算(派生链:review + security + 历史) ───────────── + const confidence = $derived(() => { + const base = read(historicalConfidence); // 先验(来自 store) + const reviewPenalty = review.output.findings?.filter(f => f.severity === "blocker").length ?? 0; + const secPenalty = security.output ? 0.3 : 0; // 有安全问题重罚 + return Math.max(0, base - reviewPenalty * 0.2 - secPenalty); + }); + + // ── 阶段6:决策门(置信度 + 预算 驱动) ────────────────────────────── + const decision = gate.automated( + () => $derived(() => ({ + conf: confidence.output, + budget: read(remainingBudget), // 读 store 派生 + })), + { + // eval 条件(零 token 机器门) + pass: "{conf >= 0.85 && budget > 0}", + block: "{conf < 0.6 || budget <= 0}", + } + ); + + // ── 阶段7:人工审批(仅中等置信度区间) ────────────────────────────── + const humanApproval = when( + () => confidence.output >= 0.6 && confidence.output < 0.85, + () => approval({ + request: `置信度 ${(confidence.output * 100).toFixed(0)}%,在灰区。人工确认是否合并?`, + input: diff.output, + choices: ["approve", "reject", "edit"], + }), + () => agent("auto-decision, no approval needed", { agent: "executor-fast", optional: true }) + ); + + // ── 阶段8:交付动作(合并 / 标记转人工) ───────────────────────────── + const delivery = $derived((): DeliveryStatus => { + if (read(remainingBudget) <= 0) return "blocked-budget"; + if (confidence.output < 0.6) return "needs-human"; + if (decision.output === "BLOCK") return "needs-human"; + return "delivered"; + }); + + const prAction = when(() => delivery.output === "delivered", + () => script(`cd ${issue.repo} && git checkout -b "fix/${issue.id}" && git add -A && git commit -m "${issue.title}" && gh pr create --fill`, + { cwd: "dedicated" }), + () => script(`echo "marked needs-human: ${issue.id}" > .taskflow/handoff/${issue.id}.md`, + { cwd: "dedicated" }) + ); + + // ── 阶段9:生成报告 + 写回看板(更新 store) ────────────────────────── + const report = agent( + `生成交付报告 JSON。Issue ${issue.id}, 状态 ${delivery.output}, ` + + `置信度 ${confidence.output}, PR: ${prAction.output}。`, + { agent: "doc-writer", output: json(), final: true } + ); + + // 写回历史 store(后续 issue 的 historicalConfidence/hotspotFiles 自动更新) + write(dashboard.history, (h) => [...h, report.output]); + write(dashboard.active, (m) => { m.delete(issue.id); return m; }); + + return report; +}); diff --git a/examples/0.2.0-app-delivery-platform/components/review-changes.ts b/examples/0.2.0-app-delivery-platform/components/review-changes.ts new file mode 100644 index 0000000..2fa2939 --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/components/review-changes.ts @@ -0,0 +1,54 @@ +/** + * ⚠️ 愿景草图 —— 含 post-0.2.0 特性(read/hotspotFiles)。见 ./README.md。 + * + * 组件:多视角代码审查。 + * + * 像 Solid 的一个可复用组件 —— 接受 props,内部有自己的响应式状态, + * 可以被任意 flow 组合。内部用 tournament 让多个 agent 从不同角度审, + * judge 汇总。 + * + * 依赖靠读取自动建立:读 changes.output、read hotspotFiles 都建依赖。 + */ + +import { agent, parallel, tournament, gate, $derived, read } from "taskflow"; +import { hotspotFiles } from "../stores/dashboard.ts"; +import { severityWeight } from "../lib/utils.ts"; +import type { ReviewFinding } from "../types/domain.ts"; + +export const reviewChanges = (changes: Phase, files: Phase) => { + // 派生:重点文件清单 = 本次改动 ∩ 历史热点 + const focusFiles = $derived(() => { + const changed = files.output; + const hot = read(hotspotFiles); // ← 读全局 store,自动建依赖 + return changed.filter((f) => hot.includes(f)); + }); + + // 多视角并行审查(3 个 agent 同时跑) + const [correctness, security, architecture] = parallel([ + agent( + `Review these changes for CORRECTNESS:\n${changes.output}\n\n` + + `Pay extra attention to hotspots: ${focusFiles.output.join(", ")}`, + { agent: "reviewer", concurrency: 6 } + ), + agent(`Review for SECURITY issues:\n${changes.output}`, { agent: "security-reviewer" }), + agent(`Review for ARCHITECTURE/SOLID violations:\n${changes.output}`, { agent: "risk-reviewer" }), + ]); + + // tournament:三个视角的发现交给 judge 去重 + 排序 + 选最严重 + const ranked = tournament({ + mode: "aggregate", // 聚合而非选一(研究合成用 aggregate) + judgeAgent: "final-arbiter", + judge: + `Merge the three reviewers' findings. Dedup overlaps. ` + + `Rank by severity. Output JSON array of findings.`, + branches: [correctness, security, architecture], + }); + + // 自动 gate:有 blocker 就 BLOCK(零 LLM,纯 eval 机器门) + const verdict = gate.automated( + () => ranked, // 输入 + { block: "{findings.any(f => f.severity === 'blocker')}" } // ← eval 条件,零 token + ); + + return verdict; +}; diff --git a/examples/0.2.0-app-delivery-platform/components/security-audit.ts b/examples/0.2.0-app-delivery-platform/components/security-audit.ts new file mode 100644 index 0000000..8c57607 --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/components/security-audit.ts @@ -0,0 +1,29 @@ +/** + * 组件:深度安全审计(高风险改动专用)。 + * + * 并行 3 个安全角度 + 对抗式验证(critic 专门挑刺)。 + * 展示:parallel + tournament(best 模式,对抗)。 + */ + +import { agent, parallel, tournament, gate } from "taskflow"; + +export const securityAudit = (diff: Phase, riskAreas: Phase) => { + const [authn, authz, crypto] = parallel([ + agent(`审查认证(authentication)改动:\n${diff.output}\n风险区: ${riskAreas.output}`, { agent: "security-reviewer" }), + agent(`审查授权(authorization)改动:\n${diff.output}`, { agent: "security-reviewer" }), + agent(`审查加密/密钥改动:\n${diff.output}`, { agent: "security-reviewer" }), + ]); + + // 对抗式:critic 专门攻击其他三个的结论 + const adversarial = tournament({ + mode: "best", + judgeAgent: "final-arbiter", + judge: "哪个审查发现了真实的安全漏洞?Quote evidence。WINNER: 。误报排除。", + branches: [authn, authz, crypto], + }); + + return gate(adversarial, { + agent: "security-reviewer", + onBlock: "halt", // 安全问题:halt,不自动修复 + }); +}; diff --git a/examples/0.2.0-app-delivery-platform/components/self-heal.ts b/examples/0.2.0-app-delivery-platform/components/self-heal.ts new file mode 100644 index 0000000..28f30dd --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/components/self-heal.ts @@ -0,0 +1,41 @@ +/** + * ⚠️ 愿景草图 —— 含 post-0.2.0 特性(flow.component)。见 ../README.md。 + * + * 组件:测试驱动的自愈循环。 + * + * 实现 → 测试 → 失败就修复,直到通过或收敛或达上限。 + * 这是个独立可复用的"自愈"组件,被多个 flow 引用。 + * + * 展示:loop 内部引用 prev(上一轮)、收敛检测、条件 phase(when)。 + */ + +import { agent, script, loop, $derived, type Phase } from "taskflow"; + +export const selfHeal = (opts: { + implement: Phase; // 初始实现 phase + testCmd: string; // 测试命令 + repo: string; + maxIterations?: number; +}) => { + const cap = opts.maxIterations ?? 4; + + return loop({ + until: "{steps.test.exit === 0}", + maxIterations: cap, + convergence: "{steps.test.output hash unchanged}", // 连续两轮错误一样 → 停(避免空转) + initial: opts.implement, + body: (prev) => ({ + test: script(opts.testCmd, { cwd: "dedicated" }), + fix: agent( + `测试失败了。修复它。\n\n命令: ${opts.testCmd}\n` + + `仓库: ${opts.repo}\n上一轮输出:\n${prev.test?.output ?? prev.output}\n\n` + + `只改让测试通过的必要代码。不要重构。`, + { + agent: "executor-code", + cwd: "worktree", // 每轮在独立 worktree 改,互不污染 + when: "{steps.test.exit !== 0}", // 测试过了就不 fix + } + ), + }), + }); +}; diff --git a/examples/0.2.0-app-delivery-platform/config/app.ts b/examples/0.2.0-app-delivery-platform/config/app.ts new file mode 100644 index 0000000..730ea21 --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/config/app.ts @@ -0,0 +1,42 @@ +/** + * 应用配置 —— 声明式、可 diff、版本化。 + * 就像 Vue 应用的配置文件,但这里是 agent 编排策略。 + */ + +export const config = { + /** 全局预算池(所有 flow 共享,实时扣减)。 */ + budget: { maxUSD: 100, maxTokens: 5_000_000 }, + + /** 不同复杂度的 issue 走不同流水线(动态路由)。 */ + routing: { + trivial: "fast-path", // 直接实现,跳过 tournament + moderate: "standard", // 规划→实现→审查 + complex: "rigorous", // + tournament 选策略 + 多轮自愈 + 安全审查 + unknown: "rigorous", // 未知当复杂处理 + } as const, + + /** 并发与限流。 */ + concurrency: { + fileReview: 6, // 同时审查 6 个文件 + migrationBatch: 4, + gateParallelism: 3, // gate 的多视角并发 + }, + + /** 自愈上限(防止无限循环)。 */ + selfHeal: { + maxIterations: 4, + convergenceCheck: true, // 连续两轮无变化则停 + }, + + /** 安全门:这些标签的 issue 必须过 security-reviewer。 */ + securityGate: { + triggerLabels: ["security", "auth", "crypto", "payment"], + requireReviewer: "security-reviewer", + }, + + /** 置信度阈值:低于此值不自动合并,转人工。 */ + confidence: { + autoMergeThreshold: 0.85, + humanReviewThreshold: 0.6, // 低于此值直接 needs-human + }, +} as const; diff --git a/examples/0.2.0-app-delivery-platform/flows/implement.ts b/examples/0.2.0-app-delivery-platform/flows/implement.ts new file mode 100644 index 0000000..85379c6 --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/flows/implement.ts @@ -0,0 +1,46 @@ +/** + * ⚠️ 愿景草图 —— 含 post-0.2.0 特性(flow.component/selfHeal)。见 ../README.md。 + * + * Flow:实现 + 自愈。 + * + * 拿 Plan → 按 step 实现 → 每个 step 跑测试自愈 → 输出 diff。 + * 被主流水线调用。 + * + * 展示:map 遍历 plan.steps,每个 step 复用 selfHeal 组件, + * reduce 汇总所有 step 的 diff。 + */ + +import { flow, agent, map, reduce, script, $derived } from "taskflow"; +import { selfHeal } from "../components/self-heal.ts"; +import { chunk } from "../lib/utils.ts"; +import type { Plan } from "../types/domain.ts"; + +export default flow("implement", ({ args }) => { + args.declare({ plan: { type: "object" as const }, repo: { type: "string" } }); + const plan = args.plan as Plan; + + // 分批实现(避免一次改太多文件冲突) + const batches = $derived(() => chunk(plan.steps, 3)); + + // 每个 step:实现 → 自愈。map 自动并发(批次内并发,受 concurrency 约束) + const stepResults = map(batches.output, (batch) => + map(batch, (step) => { + const implemented = agent( + `实现这个 step:\n${step.description}\n要改的文件: ${step.files.join(", ")}`, + { agent: "executor-code", cwd: "worktree" } // 每 step 独立 worktree + ); + return selfHeal({ + implement: implemented, + testCmd: `cd ${args.repo} && npx vitest run ${step.files.join(" ")}`, + repo: args.repo, + }); + }) + ); + + // 汇总所有 step 的 diff + const combinedDiff = reduce([stepResults], (parts) => + script(`cd ${args.repo} && git diff`, { cwd: "dedicated" }) // 零 token,拿最终 diff + ); + + return combinedDiff; // Phase(完整 diff) +}); diff --git a/examples/0.2.0-app-delivery-platform/flows/plan.ts b/examples/0.2.0-app-delivery-platform/flows/plan.ts new file mode 100644 index 0000000..c02dcd1 --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/flows/plan.ts @@ -0,0 +1,52 @@ +/** + * ⚠️ 愿景草图 —— 含 post-0.2.0 特性($derived/read/historicalConfidence)。见 ../README.md。 + * + * Flow:需求理解 + 规划。 + * + * 读 issue → 拆解 → tournament 选最优方案 → 输出 Plan。 + * 被主流水线调用(也可单独 /tf:plan 跑)。 + * + * 展示:读全局 store(historicalConfidence)指导规划, + * tournament 多角度起草方案,tournament 自动依赖 issue。 + */ + +import { flow, agent, tournament, $derived, read, json } from "taskflow"; +import { historicalConfidence } from "../stores/dashboard.ts"; +import type { Issue, Plan } from "../types/domain.ts"; + +export default flow("plan", ({ args }) => { + args.declare({ issue: { type: "object" as const } }); // 传入 Issue + const issue = args.issue as Issue; + + // 派生:用历史置信度调整规划激进程度 + const aggressiveness = $derived(() => { + const conf = read(historicalConfidence); // ← 读全局 store + return conf > 0.8 ? "ambitious" : "conservative"; // 历史表现好就敢激进 + }); + + // 三种规划思路并行起草 + const strategy = tournament({ + mode: "best", + judgeAgent: "plan-arbiter", + judge: + `选最优实现方案。考虑复杂度 ${issue.complexity}、` + + `历史置信度 ${read(historicalConfidence).toFixed(2)}、` + + `激进程度 ${aggressiveness.output}。Output JSON Plan。WINNER: 。`, + branches: [ + agent( + `方案A - 外科手术式:最小改动,精准修复。\nIssue: ${issue.title}\n${issue.body}`, + { agent: "analyst", output: json() } + ), + agent( + `方案B - 重构式:借机优化结构。\nIssue: ${issue.title}\n${issue.body}`, + { agent: "analyst", output: json() } + ), + agent( + `方案C - 对抗式:先找每个方案的破绽,再提幸存的。\nIssue: ${issue.title}\n${issue.body}`, + { agent: "critic", output: json() } + ), + ], + }); + + return strategy; // Phase +}); diff --git a/examples/0.2.0-app-delivery-platform/lib/utils.ts b/examples/0.2.0-app-delivery-platform/lib/utils.ts new file mode 100644 index 0000000..98331a6 --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/lib/utils.ts @@ -0,0 +1,28 @@ +/** + * 工具库 —— 普通 TS 函数(编译期求值,零运行时开销)。 + * 像 Solid/Vue 应用里的 utils。 + */ + +/** 把数组分成指定大小的批次。 */ +export function chunk(arr: T[], size: number): T[][] { + const out: T[][] = []; + for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); + return out; +} + +/** 根据标签推断复杂度。 */ +export function inferComplexity(labels: string[]): "trivial" | "moderate" | "complex" { + if (labels.includes("good-first-issue") || labels.includes("trivial")) return "trivial"; + if (labels.includes("refactor") || labels.includes("migration")) return "complex"; + return "moderate"; +} + +/** 是否需要安全审查。 */ +export function needsSecurityReview(labels: string[], triggerLabels: string[]): boolean { + return labels.some((l) => triggerLabels.includes(l)); +} + +/** 给审查发现算严重度权重(用于 reduce 聚合)。 */ +export function severityWeight(s: "blocker" | "high" | "medium" | "low" | "nit"): number { + return { blocker: 100, high: 40, medium: 10, low: 3, nit: 1 }[s]; +} diff --git a/examples/0.2.0-app-delivery-platform/stores/dashboard.ts b/examples/0.2.0-app-delivery-platform/stores/dashboard.ts new file mode 100644 index 0000000..a1744cc --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/stores/dashboard.ts @@ -0,0 +1,45 @@ +/** + * ⚠️ 全 post-0.2.0 —— 整个文件基于 $store/$derived(全局响应式)。见 ../README.md。 + * + * 响应式 Store —— 全局共享状态(跨 flow / 跨 phase)。 + * + * 这是 Solid 路线的精华:像 Solid 的 createRoot + createStore, + * 但作用域是整个交付流水线。任何 flow 读它就自动建立依赖; + * 它变了,只有读取方重算(overstory 增量重算)。 + * + * 底层 = overstory 的 Shared Context Tree(ctx_read/ctx_write), + * 0.2.0 把它包装成 Solid 风格的 $store rune。 + */ + +import { $store, $derived } from "taskflow"; // [post-0.2.0] 整个文件都基于全局响应式 +import type { DeliveryReport, Issue } from "../types/domain.ts"; + +/** 全局交付看板:实时跟踪所有 issue 的进度。 */ +export const dashboard = $store({ + /** 正在处理的 issue(按 id 索引)。 */ + active: new Map(), + + /** 已完成的交付报告(累积,用于学习)。 */ + history: [] as DeliveryReport[], + + /** 累计成本(预算池实时扣减依据)。 */ + spentUSD: 0, +}); + +/** 派生:剩余预算。读 dashboard.spentUSD,它变了自动重算。 */ +export const remainingBudget = $derived(() => dashboard.spentUSD < 100 ? 100 - dashboard.spentUSD : 0); + +/** 派生:历史平均置信度(用于新 issue 的先验)。 */ +export const historicalConfidence = $derived(() => { + const h = dashboard.history; + if (h.length === 0) return 0.5; // 无历史 → 中性先验 + return h.reduce((s, r) => s + r.confidence, 0) / h.length; +}); + +/** 派生:哪些文件最常出问题(用于指导审查资源分配)。 */ +export const hotspotFiles = $derived(() => { + const counts = new Map(); + for (const r of dashboard.history) + for (const f of r.findings) counts.set(f.file, (counts.get(f.file) ?? 0) + 1); + return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([f]) => f); +}); diff --git a/examples/0.2.0-app-delivery-platform/types/domain.ts b/examples/0.2.0-app-delivery-platform/types/domain.ts new file mode 100644 index 0000000..6968122 --- /dev/null +++ b/examples/0.2.0-app-delivery-platform/types/domain.ts @@ -0,0 +1,63 @@ +/** + * 类型定义 —— 整个应用共享的领域模型。 + * 像写 Solid/Vue 应用一样,类型是契约,编译期就能抓住错误。 + */ + +/** 一个待交付的工作单元(来自 GitHub issue / Linear / 飞书任务)。 */ +export interface Issue { + id: string; + title: string; + body: string; + labels: string[]; // ["bug", "feature", "refactor", "security", ...] + repo: string; // "org/repo" + author: string; + complexity: "trivial" | "moderate" | "complex" | "unknown"; +} + +/** 规划阶段的产物:把 issue 拆成可执行的 steps。 */ +export interface Plan { + summary: string; + approach: "surgical" | "rewrite" | "hybrid"; + steps: PlanStep[]; + affectedFiles: string[]; + riskAreas: string[]; // ["auth", "db-migration", ...] + estimatedComplexity: number; // 1-10 +} + +export interface PlanStep { + id: string; + description: string; + files: string[]; // 这个 step 会动的文件 + kind: "implement" | "test" | "refactor" | "doc"; +} + +/** 代码审查的发现。 */ +export interface ReviewFinding { + severity: "blocker" | "high" | "medium" | "low" | "nit"; + category: "correctness" | "security" | "performance" | "style" | "architecture"; + file: string; + line?: number; + message: string; + evidence: string; +} + +/** 交付的最终状态。 */ +export type DeliveryStatus = + | "delivered" // 合并到 main + | "needs-human" // 需要人工介入 + | "blocked-budget" // 预算耗尽 + | "blocked-conflict" // 合并冲突无法自愈 + | "rejected"; // 审查不通过且无法修复 + +/** 交付报告。 */ +export interface DeliveryReport { + issue: Issue; + status: DeliveryStatus; + plan: Plan; + prUrl?: string; + findings: ReviewFinding[]; + confidence: number; // 0-1,综合置信度 + costUSD: number; + iterations: number; // 自愈循环跑了多少轮 + trace: string; // 决策链(可追溯到 key@version) +} diff --git a/examples/0.2.0-demo-smart-migration.tf.ts b/examples/0.2.0-demo-smart-migration.tf.ts new file mode 100644 index 0000000..237adbe --- /dev/null +++ b/examples/0.2.0-demo-smart-migration.tf.ts @@ -0,0 +1,208 @@ +/** + * taskflow 0.2.0 — DSL 复杂度演示(愿景上限) + * + * ⚠️ POST-0.2.0 特性警告 ────────────────────────────────────────── + * 这个 demo 故意展示了 0.2.0 的【完整愿景上限】,其中部分特性在 + * `docs/rfc-0.2.0-dsl-syntax.md` §7 明确标为 **post-0.2.0**(依赖全局 + * 响应式运行时 / Shared Context Tree,首版不含): + * - `$derived` / `$state` 全局响应式派生状态 + * - `read()` / `write()` 读写全局响应式 store + * - `flow.component(...)` 带 props 的可复用子 flow + * 这些行在文件内用 `// [post-0.2.0]` 标注。0.2.0 首版能跑的是其余部分 + * (agent/map/gate/reduce/loop/tournament/approval/script + 编译期类型)。 + * 不要把这个文件当作 0.2.0 首版的可运行样例 —— 它是愿景草图。 + * ──────────────────────────────────────────────────────────────── + * + * 场景:大型 monorepo 的"智能迁移 + 安全审计 + 自愈"系统。 + * 把 styled-components 全量迁移到 Tailwind,同时跑安全审计, + * 失败自动修复,最后 tournament 选最优策略汇总。 + * + * 这个 demo 展示 DSL 能写出的工程级复杂度: + * - 响应式组合(flow 片段像组件一样组合) + * - 自动依赖追踪(编译期收集 .output 引用,不用手写 dependsOn) + * - 派生状态($derived [post-0.2.0],中间指标自动计算) + * - 细粒度更新(overstory:改一个文件只重算相关分支) + * - 跨文件复用(components/ 里是可复用的 flow 组件 [post-0.2.0]) + * - 多层 map 嵌套 + gate 链 + tournament + loop 自愈 + 动态子流程 + * + * 对照:这个逻辑用现在的 JSON DSL 写大约要 300+ 行嵌套 + 字符串模板, + * 且依赖要手写、没有类型、改一个输入全量重跑。 + */ + +import { flow, agent, map, parallel, gate, reduce, tournament, loop, approval, script } from "taskflow"; +import { $derived, $state, json, read, type Phase } from "taskflow"; // [post-0.2.0] $derived/$state/read 需全局响应式运行时 +import { migrateOneFile } from "./components/migrate-one.ts"; // [post-0.2.0] migrateOneFile 是 flow.component +import { auditOneFile } from "./components/audit-one.ts"; // [post-0.2.0] auditOneFile 是 flow.component + +// ============================================================================ +// 主 flow —— 像写一个 Solid App:组合多个"组件"(子 flow) +// ============================================================================ +export default flow("smart-migration", ({ args, budget }) => { + args.declare({ repo: { type: "string", default: "." }, dryRun: { type: "boolean", default: false } }); + budget({ maxUSD: 50, maxTokens: 2_000_000 }); + + // ── 阶段 1:并行侦察(两个独立 agent,自动并发) ────────────────────── + const [files, securityBaseline] = parallel([ + agent("List every .tsx using styled-components under {args.repo}. Output JSON array of file paths.", { + agent: "scout", output: json(), retry: { max: 2 }, + }), + agent("Establish security baseline: list all current auth middleware.", { agent: "scout" }), + ]); + // ↑ files 和 securityBaseline 自动并发;且它们之间无依赖边。 + + // ── 阶段 2:派生指标(像 Solid 的 const doubled = createMemo) ───────── + const plan = $derived(() => ({ // [post-0.2.0] 全局响应式派生 + total: files.output.length, + batches: chunk(files.output, 8), // 分批,每批 8 个文件 + riskProfile: securityBaseline.output.includes("no-auth") ? "high" : "normal", + })); + // ↑ $derived 是派生计算 —— 它依赖 files 和 securityBaseline; + // files 变了它会自动重算;这就是 overstory 的响应式,也是 Solid 的 createMemo。 + + // ── 阶段 3:动态规划(tournament 选最佳迁移策略) ───────────────────── + const strategy = tournament({ + mode: "best", + judgeAgent: "final-arbiter", + judge: `Judge on: ${plan.output.riskProfile} risk repo. correctness vs blast radius. WINNER: .`, + branches: [ + agent("Strategy A: codemod-first, manual review. Lowest blast radius.", { agent: "analyst" }), + agent("Strategy B: AI-rewrite each file fresh. Highest correctness.", { agent: "analyst" }), + agent("Strategy C: hybrid — codemod then AI-fix residuals.", { agent: "critic" }), + ], + }); + // ↑ tournament 的 judge 读了 plan.output —— 自动依赖 plan → files → securityBaseline。 + + // ── 阶段 4:分批迁移 + 每文件自愈(map 嵌套复用组件) ────────────────── + const migrations = map(plan.output.batches, (batch, batchIdx) => + flow.component(migrateOneFile, { // [post-0.2.0] 可复用子 flow ← 复用 components/migrate-one.ts + file: batch, + strategy: strategy.output, + dryRun: args.dryRun, + })({ + // migrateOneFile 内部是 "migrate → test → fix loop" (见下方组件定义) + // 这里 map 自动让每个 batch 并发,batch 内部串行 + }) + ); + + // ── 阶段 5:并行安全审计(复用另一个组件) ─────────────────────────── + const audits = map(files.output, (f) => + flow.component(auditOneFile, { file: f, baseline: securityBaseline.output }) // [post-0.2.0] + ); + + // ── 阶段 6:交叉验证 gate(不同 agent 复核迁移 + 审计) ──────────────── + const verified = gate( + parallel([migrations, audits]), + { agent: "reviewer", onBlock: "retry" }, + (both) => agent( + `Cross-check: did any migration introduce a security regression vs the audit?\n` + + `Migrations:\n${both.a.output}\n\nAudits:\n${both.b.output}\n\n` + + `VERDICT: BLOCK if any regression, else PASS.` + ) + ); + + // ── 阶段 7:全局自愈 loop(整体不过就重新规划) ─────────────────────── + const healed = loop({ + until: "{steps.verifyCheck.exit === 0}", + maxIterations: 3, + body: (prev) => ({ + verifyCheck: script( + `cd {args.repo} && npx tsc --noEmit && npx vitest run`, + { cwd: "dedicated" } + ), + replan: agent( // ← loop 里引用 prev,自动建依赖 + `Previous round failed verification:\n${prev.verifyCheck.output}\n\n` + + `Replan the remaining migrations. Strategy was: ${strategy.output}`, + { agent: "planner", when: "{steps.verifyCheck.exit !== 0}" } + ), + }), + })(verified); // ← loop 依赖 verified,自动建边 + + // ── 阶段 8:人工审批(高风险仓库才触发) ───────────────────────────── + const approved = approval({ + when: () => plan.output.riskProfile === "high", // ← 条件 gate,TS 函数谓词 + request: "This is a HIGH-risk repo. Approve final merge?", + input: healed.output, + choices: ["approve", "reject", "edit"], + }); + + // ── 阶段 9:最终汇总(派生 + reduce) ──────────────────────────────── + const report = reduce([healed, audits, strategy], (parts) => + agent( + `Write an executive migration report.\n` + + `Files migrated: ${plan.output.total}\n` + + `Strategy chosen: ${parts.strategy.output}\n` + + `Security findings: ${parts.audits.output}\n` + + `Final state: ${parts.healed.output}`, + { agent: "doc-writer", final: true } + ) + ); + + return report; +}); + +// ============================================================================ +// 组件 1:迁移单个文件(内含 migrate→test→fix 自愈循环) +// 文件:components/migrate-one.ts —— 像 Solid 的一个可复用组件 +// ============================================================================ +export const migrateOneFile = flow.component( // [post-0.2.0] 可复用子 flow + "migrate-one", + ({ props }: { props: { file: string; strategy: string; dryRun: boolean } }) => { + + const migrated = agent( + `Migrate ${props.file} from styled-components to Tailwind.\n` + + `Use strategy: ${props.strategy}\n` + + `Dry-run: ${props.dryRun}`, + { agent: "executor-code", cwd: "worktree" } // ← 每文件独立 git worktree,互不干扰 + ); + + // 组件内的自愈循环:test 不过就 fix,最多 3 轮 + const healed = loop({ + until: "{steps.test.exit === 0}", + maxIterations: 3, + convergence: "{steps.test.output hash unchanged}", // 连续两轮错误一样就停(收敛) + body: (prev) => ({ + test: script(`cd {props.file} && npx vitest run`, { cwd: "dedicated" }), + fix: agent( + `Tests failed for ${props.file}:\n${prev.test.output}\nFix the migration.`, + { agent: "executor", when: "{steps.test.exit !== 0}" } + ), + }), + })(migrated); + + return healed; + } +); + +// ============================================================================ +// 组件 2:审计单个文件(并行跑 3 个角度 + reduce) +// 文件:components/audit-one.ts +// ============================================================================ +export const auditOneFile = flow.component( // [post-0.2.0] 可复用子 flow + "audit-one", + ({ props }: { props: { file: string; baseline: string } }) => { + + // 三视角并行审计(像 Solid 里一个组件内开多个 createSignal) + const [auth, injection, regression] = parallel([ + agent(`Audit ${props.file} for auth regressions vs baseline:\n${props.baseline}`, { agent: "risk-reviewer" }), + agent(`Audit ${props.file} for injection risks introduced by migration.`, { agent: "security-reviewer" }), + agent(`Audit ${props.file} for behavioral regressions.`, { agent: "reviewer" }), + ]); + + // gate:任一视角 BLOCK 则整个审计 BLOCK (join: any) + const verified = gate( + parallel([auth, injection, regression], { join: "any" }), + { agent: "final-arbiter" } + ); + + return verified; + } +); + +// ============================================================================ +// 工具:分批(普通 TS 函数,编译期求值 —— Svelte 路线也能用) +// ============================================================================ +function chunk(arr: T[], size: number): T[][] { + const out: T[][] = []; + for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); + return out; +} diff --git a/examples/expand-nested-fragment.json b/examples/expand-nested-fragment.json new file mode 100644 index 0000000..e0c0e3a --- /dev/null +++ b/examples/expand-nested-fragment.json @@ -0,0 +1,32 @@ +{ + "name": "expand-nested-fragment", + "description": "Horizon B: expand runs an inline fragment as an isolated nested sub-flow.", + "phases": [ + { + "id": "seed", + "type": "agent", + "agent": "executor", + "task": "Say hello-from-seed", + "output": "text" + }, + { + "id": "grow", + "type": "expand", + "expandMode": "nested", + "dependsOn": ["seed"], + "def": { + "name": "frag", + "phases": [ + { + "id": "inner", + "type": "agent", + "agent": "executor", + "task": "Say nested-hi (parent seed was upstream; this is the fragment final).", + "final": true + } + ] + }, + "final": true + } + ] +} diff --git a/examples/race-first-win.json b/examples/race-first-win.json new file mode 100644 index 0000000..e18615c --- /dev/null +++ b/examples/race-first-win.json @@ -0,0 +1,25 @@ +{ + "name": "race-first-win", + "description": "Horizon B: first successful branch wins (latency over exhaustive wait; fast hard-fail does not win).", + "budget": { "maxUSD": 0.5 }, + "phases": [ + { + "id": "answer", + "type": "race", + "branches": [ + { + "task": "Answer quickly with a short heuristic for: {args.q}", + "agent": "executor" + }, + { + "task": "Answer carefully after a brief structured think for: {args.q}", + "agent": "executor" + } + ], + "final": true + } + ], + "args": { + "q": { "default": "What is a DAG?" } + } +} diff --git a/package.json b/package.json index b50f5af..5c80b03 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,18 @@ { "name": "pi-taskflow-monorepo", - "version": "0.1.8", + "version": "0.2.0", "private": true, - "description": "Monorepo for taskflow-core, taskflow-mcp-core, taskflow-hosts, pi-taskflow, codex-taskflow, claude-taskflow, opencode-taskflow, and the documentation website.", + "description": "Monorepo for taskflow-core, taskflow-mcp-core, taskflow-hosts, taskflow-dsl, pi-taskflow, codex-taskflow, claude-taskflow, opencode-taskflow, grok-taskflow, and the documentation website.", "type": "module", "engines": { "node": ">=22.19.0" }, "packageManager": "pnpm@9.15.0", "scripts": { - "build": "pnpm run build:skills && pnpm --filter taskflow-core build && pnpm --filter taskflow-mcp-core build && pnpm --filter taskflow-hosts build && pnpm --filter pi-taskflow build && pnpm --filter codex-taskflow build && pnpm --filter claude-taskflow build && pnpm --filter opencode-taskflow build", + "build": "pnpm run build:skills && pnpm --filter taskflow-core build && pnpm --filter taskflow-mcp-core build && pnpm --filter taskflow-hosts build && pnpm --filter taskflow-dsl build && pnpm --filter pi-taskflow build && pnpm --filter codex-taskflow build && pnpm --filter claude-taskflow build && pnpm --filter opencode-taskflow build && pnpm --filter grok-taskflow build", "build:website": "cd website && npm run build", "build:skills": "node scripts/build-skills.mjs", + "test:pack": "pnpm run build && node scripts/smoke-packed-packages.mjs", "typecheck": "tsc --noEmit", "test": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --conditions=development --experimental-strip-types --test 'packages/*/test/*.test.ts'", "test:hosts": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --conditions=development --experimental-strip-types --test 'packages/taskflow-hosts/test/*.test.ts'", @@ -19,13 +20,17 @@ "test:codex": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --conditions=development --experimental-strip-types --test 'packages/codex-taskflow/test/*.test.ts'", "test:claude": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --conditions=development --experimental-strip-types --test 'packages/claude-taskflow/test/*.test.ts'", "test:opencode": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --conditions=development --experimental-strip-types --test 'packages/opencode-taskflow/test/*.test.ts'", + "test:grok": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --conditions=development --experimental-strip-types --test 'packages/grok-taskflow/test/*.test.ts'", + "test:dsl": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --conditions=development --experimental-strip-types --test 'packages/taskflow-dsl/test/*.test.ts'", "test:e2e-codex": "node --conditions=development --experimental-strip-types packages/codex-taskflow/test/e2e-codex.mts", "test:e2e-codex-mcp": "node --conditions=development --experimental-strip-types packages/codex-taskflow/test/e2e-codex-mcp.mts", "test:e2e-codex-mcp-full": "pnpm run build && node --experimental-strip-types packages/codex-taskflow/test/e2e-mcp-comprehensive.mts", "test:e2e-claude": "node --conditions=development --experimental-strip-types packages/claude-taskflow/test/e2e-claude.mts", "test:e2e-claude-mcp": "node --conditions=development --experimental-strip-types packages/claude-taskflow/test/e2e-claude-mcp.mts", "test:e2e-opencode": "node --conditions=development --experimental-strip-types packages/opencode-taskflow/test/e2e-opencode.mts", - "test:e2e-opencode-mcp": "node --conditions=development --experimental-strip-types packages/opencode-taskflow/test/e2e-opencode-mcp.mts" + "test:e2e-opencode-mcp": "node --conditions=development --experimental-strip-types packages/opencode-taskflow/test/e2e-opencode-mcp.mts", + "test:e2e-grok": "node --conditions=development --experimental-strip-types packages/grok-taskflow/test/e2e-grok.mts", + "test:e2e-grok-mcp": "node --conditions=development --experimental-strip-types packages/grok-taskflow/test/e2e-grok-mcp.mts" }, "devDependencies": { "@earendil-works/pi-agent-core": "^0.80.3", diff --git a/packages/claude-taskflow/package.json b/packages/claude-taskflow/package.json index 18711da..77369a7 100644 --- a/packages/claude-taskflow/package.json +++ b/packages/claude-taskflow/package.json @@ -1,6 +1,6 @@ { "name": "claude-taskflow", - "version": "0.1.8", + "version": "0.2.0", "description": "Run taskflow on Claude Code: a Claude subagent runner plus an MCP server (and a plug-and-play Claude Code plugin) that exposes the taskflow_* tools to Claude Code users.", "keywords": [ "claude", @@ -51,11 +51,15 @@ "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/claude-taskflow/plugin/.claude-plugin/plugin.json b/packages/claude-taskflow/plugin/.claude-plugin/plugin.json index 576d4ba..72d1042 100644 --- a/packages/claude-taskflow/plugin/.claude-plugin/plugin.json +++ b/packages/claude-taskflow/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "taskflow", - "version": "0.1.8", + "version": "0.2.0", "description": "Declarative, verifiable DAG orchestration for Claude Code subagents — fan-out, gates, loops, tournaments, approvals, resumable runs, and saveable commands, with intermediate transcripts kept out of your context.", "author": { "name": "heggria", diff --git a/packages/claude-taskflow/plugin/.mcp.json b/packages/claude-taskflow/plugin/.mcp.json index 9ca9820..0f834c8 100644 --- a/packages/claude-taskflow/plugin/.mcp.json +++ b/packages/claude-taskflow/plugin/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "taskflow": { "command": "npx", - "args": ["-y", "-p", "claude-taskflow@0.1.8", "claude-taskflow-mcp"] + "args": ["-y", "-p", "claude-taskflow@0.2.0", "claude-taskflow-mcp"] } } } diff --git a/packages/claude-taskflow/plugin/skills/taskflow/SKILL.md b/packages/claude-taskflow/plugin/skills/taskflow/SKILL.md index 5f41723..a277e43 100644 --- a/packages/claude-taskflow/plugin/skills/taskflow/SKILL.md +++ b/packages/claude-taskflow/plugin/skills/taskflow/SKILL.md @@ -18,12 +18,22 @@ runs as an isolated `claude -p` session. | `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. +**Security default:** Claude mutating/unrestricted phases are rejected because +headless Claude has no OS sandbox. Explicitly opt in only for trusted flows by +setting `PI_TASKFLOW_CLAUDE_UNSAFE_BYPASS=1`; prefer `cwd: "worktree"`. + 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. @@ -37,7 +47,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 +78,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 +175,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 +189,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 +199,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 +476,68 @@ output is exact. "input": "{steps.analyze.output}", "dependsOn": ["analyze"], "final": true } ``` -### Budget (cost / token caps) +### 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. -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. +- `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 +546,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 +654,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/claude-taskflow/plugin/skills/taskflow/advanced.md b/packages/claude-taskflow/plugin/skills/taskflow/advanced.md index 897b205..e15d3a0 100644 --- a/packages/claude-taskflow/plugin/skills/taskflow/advanced.md +++ b/packages/claude-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/claude-taskflow/plugin/skills/taskflow/configuration.md b/packages/claude-taskflow/plugin/skills/taskflow/configuration.md index 643eaf6..dd6e899 100644 --- a/packages/claude-taskflow/plugin/skills/taskflow/configuration.md +++ b/packages/claude-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,16 +214,34 @@ 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 `claude -p --output-format stream-json` - session. A model id that still looks like a pi-provider path (contains `/`) + session (Claude Code 2.1.169 or newer is required for `--safe-mode`). A model + id that still looks like a pi-provider path (contains `/`) or an unresolved `{{placeholder}}` is dropped so claude falls back to its - configured default. Read-only phases get a `--allowedTools` whitelist; - mutating phases run under `--permission-mode bypassPermissions` (no OS - sandbox — see the README security note). + configured default. Known read-only requests — including an omitted tool + list — get matching `--tools` and `--allowedTools` lists, and an explicit + request stays narrow. `--safe-mode` disables non-managed project/user + customizations; disk setting sources and non-managed hooks are disabled as + defense in depth. Administrator-managed policy hooks may still run. Known + mutating tools are rejected by default because headless Claude has no OS + sandbox; trusted operators must explicitly set + `PI_TASKFLOW_CLAUDE_UNSAFE_BYPASS=1` to allow `bypassPermissions` while + keeping the requested built-in set narrow. Unknown tools always fail closed. + Prefer an isolated `cwd: "worktree"` even after opting + in. The Claude child inherits only platform/runtime, proxy/CA, and supported + Claude-provider environment variables; unrelated application secrets are + removed from the child environment. + +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. --- @@ -310,7 +334,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. @@ -357,9 +381,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. | --- @@ -407,10 +437,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/claude-taskflow/plugin/skills/taskflow/patterns.md b/packages/claude-taskflow/plugin/skills/taskflow/patterns.md index 51b0b47..48f11cd 100644 --- a/packages/claude-taskflow/plugin/skills/taskflow/patterns.md +++ b/packages/claude-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/claude-taskflow/test/mcp-server.test.ts b/packages/claude-taskflow/test/mcp-server.test.ts index 23191bd..9d26b00 100644 --- a/packages/claude-taskflow/test/mcp-server.test.ts +++ b/packages/claude-taskflow/test/mcp-server.test.ts @@ -45,6 +45,7 @@ test("claude 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("claude mcp: tools/list exposes the same taskflow tools as codex", async () => { @@ -52,7 +53,7 @@ test("claude mcp: tools/list exposes the same taskflow tools as codex", 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("claude 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/codex-taskflow/package.json b/packages/codex-taskflow/package.json index ef6ff97..33eadca 100644 --- a/packages/codex-taskflow/package.json +++ b/packages/codex-taskflow/package.json @@ -1,6 +1,6 @@ { "name": "codex-taskflow", - "version": "0.1.8", + "version": "0.2.0", "description": "Run taskflow on OpenAI Codex: a Codex subagent runner plus an MCP server (and a plug-and-play Codex plugin) that exposes the taskflow_* tools to Codex users.", "keywords": [ "codex", @@ -51,11 +51,15 @@ "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/codex-taskflow/plugin/.codex-plugin/plugin.json b/packages/codex-taskflow/plugin/.codex-plugin/plugin.json index afca5b4..8b05d08 100644 --- a/packages/codex-taskflow/plugin/.codex-plugin/plugin.json +++ b/packages/codex-taskflow/plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "taskflow", - "version": "0.1.8", + "version": "0.2.0", "description": "Declarative, verifiable DAG orchestration for Codex subagents — fan-out, gates, loops, tournaments, approvals, resumable runs, and saveable commands, with intermediate transcripts kept out of your context.", "author": { "name": "heggria", diff --git a/packages/codex-taskflow/plugin/.mcp.json b/packages/codex-taskflow/plugin/.mcp.json index 985205d..9bd8882 100644 --- a/packages/codex-taskflow/plugin/.mcp.json +++ b/packages/codex-taskflow/plugin/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "taskflow": { "command": "npx", - "args": ["-y", "-p", "codex-taskflow@0.1.8", "codex-taskflow-mcp"], + "args": ["-y", "-p", "codex-taskflow@0.2.0", "codex-taskflow-mcp"], "tool_timeout_sec": 1800 } } diff --git a/packages/codex-taskflow/plugin/skills/taskflow/SKILL.md b/packages/codex-taskflow/plugin/skills/taskflow/SKILL.md index edc8a4a..634a8d4 100644 --- a/packages/codex-taskflow/plugin/skills/taskflow/SKILL.md +++ b/packages/codex-taskflow/plugin/skills/taskflow/SKILL.md @@ -17,8 +17,14 @@ the Codex form (`taskflow_verify`). | `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 (inline SVG) + 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. @@ -36,7 +42,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? @@ -67,8 +73,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 @@ -164,12 +170,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 | @@ -178,6 +184,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) @@ -186,7 +194,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`. | @@ -463,11 +471,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": [ ... ] } @@ -476,6 +541,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 @@ -580,6 +649,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/codex-taskflow/plugin/skills/taskflow/advanced.md b/packages/codex-taskflow/plugin/skills/taskflow/advanced.md index 897b205..e15d3a0 100644 --- a/packages/codex-taskflow/plugin/skills/taskflow/advanced.md +++ b/packages/codex-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/codex-taskflow/plugin/skills/taskflow/configuration.md b/packages/codex-taskflow/plugin/skills/taskflow/configuration.md index 7ab6ce0..140485c 100644 --- a/packages/codex-taskflow/plugin/skills/taskflow/configuration.md +++ b/packages/codex-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,13 +214,31 @@ 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 `codex exec --json` session. A model id that still looks like a pi-provider path (contains `/`) or an unresolved `{{placeholder}}` is dropped so codex falls back to its configured default. + Codex does **not** offer a strict per-tool name whitelist: read-only tool sets + map to `-s read-only`; any mutating tool or no list maps to + `-s workspace-write` (never `danger-full-access`). Effective thinking maps + to `model_reasoning_effort`: `off`/`none`/`minimal` → `none`, + `low`/`medium`/`high`/`xhigh` pass through, and `max`/`ultra` → `xhigh`. + Any other value fails closed before Codex is spawned. Codex usage accounting + is tokens-only: budgeted flows may use `maxTokens`, while `maxUSD` is rejected. + Children use `--ephemeral --ignore-user-config --ignore-rules` and an empty + `mcp_servers` override, so parent plugins/MCP/rules cannot alter the run. + Only platform/proxy/CA and Codex/OpenAI provider environment variables are + inherited; 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. --- @@ -307,7 +331,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. @@ -354,9 +378,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. | --- @@ -404,10 +434,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/codex-taskflow/plugin/skills/taskflow/patterns.md b/packages/codex-taskflow/plugin/skills/taskflow/patterns.md index 51b0b47..48f11cd 100644 --- a/packages/codex-taskflow/plugin/skills/taskflow/patterns.md +++ b/packages/codex-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/codex-taskflow/test/e2e-mcp-comprehensive.mts b/packages/codex-taskflow/test/e2e-mcp-comprehensive.mts index 8913ac9..9cf05e7 100644 --- a/packages/codex-taskflow/test/e2e-mcp-comprehensive.mts +++ b/packages/codex-taskflow/test/e2e-mcp-comprehensive.mts @@ -78,6 +78,7 @@ send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: " const init = await waitFor(1, "initialize"); assert.equal(init.result.protocolVersion, "2025-06-18"); assert.equal(init.result.serverInfo.name, "taskflow"); +assert.equal(init.result.serverInfo.version, "0.2.0"); ok(`initialize → ${JSON.stringify(init.result.serverInfo)}`); // notification must NOT produce a response @@ -86,7 +87,7 @@ send({ jsonrpc: "2.0", method: "notifications/initialized" }); send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }); const list = await waitFor(2, "tools/list"); const toolNames = list.result.tools.map((t: any) => t.name).sort(); -assert.deepEqual(toolNames, ["taskflow_compile", "taskflow_list", "taskflow_peek", "taskflow_recompute", "taskflow_run", "taskflow_save", "taskflow_search", "taskflow_show", "taskflow_trace", "taskflow_verify", "taskflow_why_stale"]); +assert.deepEqual(toolNames, ["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 list.result.tools) { assert.equal(t.inputSchema.type, "object", `${t.name} has object schema`); assert.equal(typeof t.description, "string"); diff --git a/packages/codex-taskflow/test/mcp-server.test.ts b/packages/codex-taskflow/test/mcp-server.test.ts index eb2aa25..4a80446 100644 --- a/packages/codex-taskflow/test/mcp-server.test.ts +++ b/packages/codex-taskflow/test/mcp-server.test.ts @@ -13,6 +13,8 @@ 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"; +import { persistTerminalRun } from "taskflow-mcp-core/server"; +import type { RunState } from "taskflow-core"; /** Send a list of JSON-RPC messages through the server, collect responses. */ async function rpcRoundtrip(messages: object[]): Promise { @@ -46,6 +48,7 @@ test("mcp: initialize returns the protocol version + serverInfo codex expects", 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("mcp: tools/list exposes the taskflow tools with schemas", async () => { @@ -53,7 +56,7 @@ test("mcp: tools/list exposes the taskflow tools with schemas", 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"); @@ -176,6 +179,25 @@ test("mcp: taskflow_verify with a missing defineFile returns a clear error", asy assert.match(res.error.message, /defineFile not found:/); }); +test("mcp: defineFile cannot escape cwd or the OS temp directory", async (t) => { + const fs = await import("node:fs"); + const path = await import("node:path"); + const outside = process.platform === "win32" + ? path.join(process.env.SystemRoot ?? "C:\\Windows", "win.ini") + : "/etc/hosts"; + if (!fs.existsSync(outside)) return t.skip(`no stable outside fixture at ${outside}`); + const [res] = await rpcRoundtrip([ + { + jsonrpc: "2.0", + id: 101, + method: "tools/call", + params: { name: "taskflow_verify", arguments: { defineFile: outside } }, + }, + ]); + assert.equal(res.error.code, -32602); + assert.match(res.error.message, /contained in the server cwd or OS temp directory/i); +}); + test("mcp: tools/call unknown tool returns invalid-params", async () => { const [res] = await rpcRoundtrip([ { jsonrpc: "2.0", id: 6, method: "tools/call", params: { name: "nope", arguments: {} } }, @@ -183,14 +205,46 @@ test("mcp: tools/call unknown tool returns invalid-params", async () => { assert.equal(res.error.code, -32602); }); +test("mcp: tools/call enforces advertised argument schemas before dispatch", async () => { + for (const [id, arguments_] of [ + [61, { query: "x", scope: "porject" }], + [62, { query: 42 }], + [63, { query: "x", unexpected: true }], + ] as const) { + const [res] = await rpcRoundtrip([ + { jsonrpc: "2.0", id, method: "tools/call", params: { name: "taskflow_search", arguments: arguments_ } }, + ]); + assert.equal(res.error.code, -32602); + assert.match(res.error.message, /Invalid taskflow_search arguments/); + } +}); + test("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"], ); }); +test("mcp: terminal persistence failure is surfaced", () => { + const state = { + runId: "persist-failure", + flowName: "persist-failure", + def: { name: "persist-failure", phases: [] }, + args: {}, + status: "completed", + phases: {}, + createdAt: Date.now(), + updatedAt: Date.now(), + cwd: process.cwd(), + } as RunState; + const error = persistTerminalRun(state, { maxKeep: 1, maxAgeDays: 1 }, () => { + throw new Error("disk full"); + }); + assert.equal(error, "disk full"); +}); + test("mcp: taskflow_save + taskflow_search round-trip (the reuse flywheel)", async () => { const fs = await import("node:fs"); const os = await import("node:os"); @@ -234,6 +288,33 @@ test("mcp: taskflow_search with empty query returns an error", async () => { assert.match(res.content[0].text, /requires `query`/); }); +test("mcp: inline run cannot increment a different saved flow's reuse metadata", async () => { + const fs = await import("node:fs"); + const os = await import("node:os"); + const path = await import("node:path"); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "tf-inline-reuse-")); + fs.mkdirSync(path.join(cwd, ".pi", "taskflows"), { recursive: true }); + const tools = makeToolHandlers(cwd); + try { + await tools.taskflow_save({ + name: "saved-a", + definition: { name: "saved-a", phases: [{ id: "s", type: "script", run: "printf saved", final: true }] }, + purpose: "saved flow", + }); + const run = (await tools.taskflow_run({ + name: "saved-a", + define: { name: "inline-b", phases: [{ id: "s", type: "script", run: "printf inline", final: true }] }, + reusedFromSearch: true, + })) as { isError?: boolean }; + assert.equal(run.isError, false); + const show = (await tools.taskflow_show({ name: "saved-a", json: true })) as { content: Array<{ text: string }> }; + const parsed = JSON.parse(show.content[0].text) as { library?: { reuseCount?: number } }; + assert.equal(parsed.library?.reuseCount, 0); + } finally { + fs.rmSync(cwd, { recursive: true, force: true }); + } +}); + // --- Codex-rendering ergonomics (see docs/codex-mcp.md) ------------------- // Codex shows a `text` block as a fixed grey plaintext
 (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  [--json]` · `/tf replay  [--threshold phase=n] [--budget-usd n] [--json]`
+- `/tf why-stale  [phaseId]` · `/tf recompute   [--apply]` (dry-run by default)
 - `/tf init` — interactive model-roles setup
 - `/tf: [args]` — shortcut for each saved flow
diff --git a/packages/pi-taskflow/skills/taskflow/advanced.md b/packages/pi-taskflow/skills/taskflow/advanced.md
index 0ba12d7..09c8ddd 100644
--- a/packages/pi-taskflow/skills/taskflow/advanced.md
+++ b/packages/pi-taskflow/skills/taskflow/advanced.md
@@ -9,8 +9,26 @@ directories, or surgical re-execution after the world changes
 
 ---
 
+## `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).
+
+---
+
 ## Shared Context Tree (blackboard + supervision) — opt-in
 
+> **0.2.0 host scope:** context-tool injection is implemented by `pi-taskflow`.
+> Codex, Claude, OpenCode, and Grok runners do not expose `ctx_*` tools yet.
+
 By default subagents are fully isolated: they share nothing and only return a
 final output string. Opt a phase in with `shareContext: true` (or
 `contextSharing: true` at the flow level for every phase) to give its subagent
@@ -242,6 +260,66 @@ stored run at $0.
 
 ---
 
+## 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 { action: "trace", runId: "" }
+taskflow { action: "trace", runId: "", json: true }   // full machine record
+/tf trace  [--json]
+```
+
+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 { action: "replay", runId: "", thresholds: { review: 0.9 } }
+taskflow { action: "replay", runId: "", budgetMaxUSD: 0.05, json: true }
+/tf replay  --threshold review=0.9 --budget-usd 0.05 [--json]
+```
+
+**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` |
+
+---
+
 ## `init` — model roles setup
 
 `action: "init"` manages the `modelRoles` map (`{{fast}}` / `{{strong}}` / …
diff --git a/packages/pi-taskflow/skills/taskflow/configuration.md b/packages/pi-taskflow/skills/taskflow/configuration.md
index a4e9782..f5aa27e 100644
--- a/packages/pi-taskflow/skills/taskflow/configuration.md
+++ b/packages/pi-taskflow/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`.
 
 ---
 
@@ -214,12 +220,19 @@ 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 process:
   `pi --mode json -p --no-session [--model …] [--thinking …] [--tools …] [--append-system-prompt ] "Task: …"`.
+
+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.
 
 ---
@@ -312,7 +325,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.
@@ -359,9 +372,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. |
 
 ---
 
@@ -411,10 +430,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/pi-taskflow/skills/taskflow/patterns.md b/packages/pi-taskflow/skills/taskflow/patterns.md
index 1891aba..09654e4 100644
--- a/packages/pi-taskflow/skills/taskflow/patterns.md
+++ b/packages/pi-taskflow/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
@@ -238,6 +238,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/pi-taskflow/src/index.ts b/packages/pi-taskflow/src/index.ts
index edc4118..c7f8be9 100644
--- a/packages/pi-taskflow/src/index.ts
+++ b/packages/pi-taskflow/src/index.ts
@@ -30,7 +30,23 @@ import { renderRunResult, summarizeRun } from "./render.ts";
 import { piSubagentRunner, runnerModulePath } from "./runner.ts";
 import { RunHistoryComponent, type RunHistoryResult } from "./runs-view.ts";
 import { ApprovalViewComponent, type ApprovalChoice } from "./approval-view.ts";
-import { executeTaskflow, recomputeTaskflow, summarizeReuse, traceFilePath, FileTraceSink, runsDir, type ApprovalDecision, type ApprovalRequest, type RecomputeReport, type RuntimeDeps, type RuntimeResult } from "taskflow-core";
+import {
+	executeTaskflow,
+	recomputeTaskflow,
+	summarizeReuse,
+	traceFilePath,
+	FileTraceSink,
+	runsDir,
+	replayRun,
+	upgradeTraceEvent,
+	type ApprovalDecision,
+	type ApprovalRequest,
+	type RecomputeReport,
+	type ReplayReport,
+	type ReplayOverrides,
+	type RuntimeDeps,
+	type RuntimeResult,
+} from "taskflow-core";
 import { type UsageStats } from "taskflow-core";
 import { finalPhase, resolveArgs, type Taskflow, validateTaskflow, desugar, isShorthand } from "taskflow-core";
 import {
@@ -101,8 +117,8 @@ const ShorthandStep = Type.Object(
 );
 
 const TaskflowParams = Type.Object({
-	action: StringEnum(["run", "save", "resume", "list", "agents", "init", "verify", "compile", "ir", "provenance", "trace", "why-stale", "recompute", "cache-clear", "search"] as const, {
-		description: "What to do: run a flow, save a definition, resume a paused run, list saved flows, list available agents, init model role configuration, verify the DAG, compile the DAG to a Mermaid diagram + verification report, compile to FlowIR + content hash, show observed readSet provenance, show a run's deterministic-replay event trace, explain why a run is stale, minimally recompute a stale run, or clear the cross-run memoization cache",
+	action: StringEnum(["run", "save", "resume", "list", "agents", "init", "verify", "compile", "ir", "provenance", "trace", "replay", "why-stale", "recompute", "cache-clear", "search"] as const, {
+		description: "What to do: run a flow, save a definition, resume a paused run, list saved flows, list available agents, init model role configuration, verify the DAG, compile the DAG to a Mermaid diagram + verification report, compile to FlowIR + content hash, show observed readSet provenance, show a run's event trace, offline-replay a trace under alternate knobs (zero tokens), explain why a run is stale, minimally recompute a stale run, or clear the cross-run memoization cache",
 		default: "run",
 	}),
 	name: Type.Optional(Type.String({ description: "Name of a saved flow (for run/save without inline define)" })),
@@ -146,10 +162,14 @@ const TaskflowParams = Type.Object({
 		}),
 	),
 	args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Invocation arguments for the flow" })),
-	runId: Type.Optional(Type.String({ description: "Run id to resume (for action=resume), inspect (provenance/trace/why-stale), or recompute" })),
+	runId: Type.Optional(Type.String({ description: "Run id to resume (for action=resume), inspect (provenance/trace/replay/why-stale), or recompute" })),
 	phaseId: Type.Optional(Type.String({ description: "Phase id — the assumed-changed seed for action=why-stale, or the phase to re-run for action=recompute" })),
-	json: Type.Optional(Type.Boolean({ description: "For action=trace: return the raw event trace as machine-readable JSON instead of a human-readable timeline." })),
+	json: Type.Optional(Type.Boolean({ description: "For action=trace/replay: return machine-readable JSON instead of a human-readable report." })),
 	dryRun: Type.Optional(Type.Boolean({ description: "For action=recompute: compute the stale frontier without re-executing anything (no tokens spent). Defaults to true (safe); set false to actually re-run the seed + stale frontier and persist the updated run" })),
+	budgetMaxUSD: Type.Optional(Type.Number({ description: "For action=replay: alternate max USD budget (would-exceed-budget checks)." })),
+	budgetMaxTokens: Type.Optional(Type.Number({ description: "For action=replay: alternate max token budget." })),
+	thresholds: Type.Optional(Type.Record(Type.String(), Type.Number(), { description: "For action=replay: map of phaseId → new score threshold." })),
+	models: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "For action=replay: map of phaseId → model (marks needs-live-rerun)." })),
 	scope: Type.Optional(
 		StringEnum(["user", "project"] as const, { description: "Where to save (action=save)", default: "project" }),
 	),
@@ -194,7 +214,7 @@ function formatFlowIR(ir: TaskflowIR): string {
 	lines.push(`# FlowIR — "${ir.meta.sourceFlowName}"`);
 	lines.push("");
 	if (ir.hash) {
-		lines.push(`**content hash:** \`${ir.hash}\`${ir.usedFallbackHash ? "  (fallback — stub projection)" : "  (overstory-canonical)"}`);
+		lines.push(`**content hash:** \`${ir.hash}\`${ir.usedFallbackHash ? "  (fallback — not IR-canonical)" : "  (ir-canonical)"}`);
 		lines.push("");
 	} else {
 		lines.push("**content hash:** _(unavailable — computation failed)_");
@@ -318,6 +338,41 @@ function formatTrace(events: TraceEvent[], runId: string, flowName: string): str
 	return lines.join("\n");
 }
 
+/** Offline replay report (zero tokens). */
+function formatReplay(r: ReplayReport, runId: string, flowName: string): string {
+	const lines: string[] = [
+		`Replay — ${flowName} / ${runId}  (${r.decisions.length} phase decision(s), zero tokens)`,
+	];
+	lines.push("");
+	if (r.needsLiveRerun) lines.push("⚠ Some phases need a live re-run (model/args override).");
+	lines.push(
+		`Recorded usage cost ≈ $${r.totalUsage.cost.toFixed(4)}  tokens in=${r.totalUsage.input} out=${r.totalUsage.output}`,
+	);
+	lines.push("");
+	for (const d of r.decisions) {
+		const prior = d.priorOutcome ? ` prior=${d.priorOutcome}` : "";
+		const next = d.replayedOutcome ? ` → ${d.replayedOutcome}` : "";
+		lines.push(`  • ${d.phaseId}: [${d.outcome}]${prior}${next} — ${d.reason}`);
+	}
+	lines.push("");
+	lines.push("(Use action=replay with json:true for the full ReplayReport.)");
+	return lines.join("\n");
+}
+
+function parseToolReplayOverrides(params: {
+	budgetMaxUSD?: number;
+	budgetMaxTokens?: number;
+	thresholds?: Record;
+	models?: Record;
+}): ReplayOverrides {
+	const o: ReplayOverrides = {};
+	if (typeof params.budgetMaxUSD === "number") o.budgetMaxUSD = params.budgetMaxUSD;
+	if (typeof params.budgetMaxTokens === "number") o.budgetMaxTokens = params.budgetMaxTokens;
+	if (params.thresholds && Object.keys(params.thresholds).length) o.thresholds = params.thresholds;
+	if (params.models && Object.keys(params.models).length) o.models = params.models;
+	return o;
+}
+
 function makeRunState(def: Taskflow, args: Record, cwd: string): RunState {
 	return {
 		runId: newRunId(def.name),
@@ -981,6 +1036,37 @@ export default function (pi: ExtensionAPI) {
 				};
 			}
 
+			if (action === "replay") {
+				if (!params.runId)
+					return errorResult(action, "action=replay requires 'runId'");
+				const runR = loadRunDiagnosed(ctx.cwd, params.runId);
+				if (!runR.ok) return errorResult(action, describeLoadFailure(runR, `Run "${params.runId}"`));
+				const run = runR.value;
+				const raw = readTrace(traceFilePath(runsDir(ctx.cwd), run.flowName, run.runId));
+				if (raw.length === 0)
+					return errorResult(action, `No trace recorded for run "${params.runId}" (the run predates tracing, or no trace sink was injected).`);
+				const events = raw.map((e) => upgradeTraceEvent(e as unknown as Record));
+				const report = replayRun(
+					events,
+					parseToolReplayOverrides({
+						budgetMaxUSD: params.budgetMaxUSD,
+						budgetMaxTokens: params.budgetMaxTokens,
+						thresholds: params.thresholds as Record | undefined,
+						models: params.models as Record | undefined,
+					}),
+				);
+				if (params.json) {
+					return {
+						content: [{ type: "text", text: JSON.stringify(report, null, 2) }],
+						details: { action } satisfies TaskflowDetails,
+					};
+				}
+				return {
+					content: [{ type: "text", text: formatReplay(report, run.runId, run.flowName) }],
+					details: { action } satisfies TaskflowDetails,
+				};
+			}
+
 			if (action === "why-stale") {
 				if (!params.runId)
 					return errorResult(action, "action=why-stale requires 'runId'");
@@ -1314,7 +1400,7 @@ export default function (pi: ExtensionAPI) {
 	pi.registerCommand("tf", {
 		description: "Taskflow: list | run  | show  | compile  | runs | peek  [phaseId] | init",
 		getArgumentCompletions: (prefix) => {
-			const subs = ["list", "run", "show", "runs", "peek", "resume", "init", "save", "verify", "compile", "ir", "provenance", "trace", "why-stale", "recompute"];
+			const subs = ["list", "run", "show", "runs", "peek", "resume", "init", "save", "verify", "compile", "ir", "provenance", "trace", "replay", "why-stale", "recompute"];
 			const items = subs.map((s) => ({ value: s, label: s }));
 			const filtered = items.filter((i) => i.value.startsWith(prefix));
 			return filtered.length > 0 ? filtered : null;
@@ -1442,6 +1528,53 @@ export default function (pi: ExtensionAPI) {
 				return;
 			}
 
+			if (sub === "replay") {
+				if (!arg) {
+					ctx.ui.notify(
+						"Usage: /tf replay  [--json] [--budget-usd N] [--budget-tokens N] [--threshold phase=0.8]",
+						"warning",
+					);
+					return;
+				}
+				const tokens = arg.trim().split(/\s+/).filter(Boolean);
+				const rid = tokens[0];
+				const json = tokens.includes("--json");
+				const overrides: ReplayOverrides = {};
+				for (let i = 1; i < tokens.length; i++) {
+					const t = tokens[i];
+					if (t === "--budget-usd" && tokens[i + 1]) {
+						overrides.budgetMaxUSD = Number(tokens[++i]);
+					} else if (t === "--budget-tokens" && tokens[i + 1]) {
+						overrides.budgetMaxTokens = Number(tokens[++i]);
+					} else if (t === "--threshold" && tokens[i + 1]) {
+						const spec = tokens[++i];
+						const eq = spec.indexOf("=");
+						if (eq > 0) {
+							const phase = spec.slice(0, eq);
+							const thr = Number(spec.slice(eq + 1));
+							if (phase && Number.isFinite(thr)) {
+								overrides.thresholds = { ...(overrides.thresholds ?? {}), [phase]: thr };
+							}
+						}
+					}
+				}
+				const runR = loadRunDiagnosed(ctx.cwd, rid);
+				if (!runR.ok) {
+					ctx.ui.notify(describeLoadFailure(runR, `Run "${rid}"`), "error");
+					return;
+				}
+				const run = runR.value;
+				const raw = readTrace(traceFilePath(runsDir(ctx.cwd), run.flowName, run.runId));
+				if (raw.length === 0) {
+					ctx.ui.notify(`No trace recorded for run "${rid}" (the run predates tracing, or no trace sink was injected).`, "warning");
+					return;
+				}
+				const events = raw.map((e) => upgradeTraceEvent(e as unknown as Record));
+				const report = replayRun(events, overrides);
+				ctx.ui.notify(json ? JSON.stringify(report, null, 2) : formatReplay(report, run.runId, run.flowName), "info");
+				return;
+			}
+
 			if (sub === "why-stale") {
 				if (!arg) {
 					ctx.ui.notify("Usage: /tf why-stale  [phaseId]", "warning");
diff --git a/packages/pi-taskflow/test/e2e-cache-migration.mts b/packages/pi-taskflow/test/e2e-cache-migration.mts
index e435551..52faec6 100644
--- a/packages/pi-taskflow/test/e2e-cache-migration.mts
+++ b/packages/pi-taskflow/test/e2e-cache-migration.mts
@@ -18,7 +18,7 @@ import type { AgentConfig } from "taskflow-core";
 import { CacheStore } from "taskflow-core";
 import { cacheKeys, executeTaskflow, type PhaseCacheCtx, type RuntimeDeps } from "taskflow-core";
 import type { RunResult, RunOptions } from "../src/runner.ts";
-import { compileTaskflowToIR } from "../extensions/flowir/index.ts";
+import { compileTaskflowToIR } from "taskflow-core";
 import type { Taskflow } from "taskflow-core";
 import type { RunState } from "taskflow-core";
 import { emptyUsage } from "taskflow-core";
diff --git a/packages/pi-taskflow/test/e2e-context-value.mts b/packages/pi-taskflow/test/e2e-context-value.mts
index 90cbead..7525f5a 100644
--- a/packages/pi-taskflow/test/e2e-context-value.mts
+++ b/packages/pi-taskflow/test/e2e-context-value.mts
@@ -2,7 +2,7 @@
  * REAL, value-demonstrating end-to-end test for the Shared Context Tree.
  *
  * This exercises BOTH core capabilities against a REAL codebase (this repo's
- * own extensions/), with real `pi` subagents and a real model:
+ * own `packages/taskflow-core/src/` files), with real `pi` subagents and a real model:
  *
  *   PART A — Horizontal blackboard reuse (avoid duplicated work)
  *     1. `survey` maps shared project conventions ONCE and ctx_write's them
@@ -28,7 +28,9 @@ import * as crypto from "node:crypto";
 import { discoverAgents, readSubagentSettings } from "taskflow-core";
 import { executeTaskflow } from "taskflow-core";
 import { validateTaskflow, type Taskflow } from "taskflow-core";
-import type { RunState } from "taskflow-core";
+import { saveRun, type RunState } from "taskflow-core";
+import { installNoExtPiWrapper } from "./e2e-helpers.mts";
+import { piSubagentRunner } from "../src/runner.ts";
 
 const MARKER = `CONV-${crypto.randomBytes(4).toString("hex").toUpperCase()}`;
 
@@ -44,7 +46,7 @@ const FLOW: Taskflow = {
 			agent: "scout",
 			task:
 				`Inspect this TypeScript project's coding conventions by reading AGENTS.md ` +
-				`and one or two files under extensions/. Summarize the 3 most important ` +
+				`and one or two files under packages/taskflow-core/src/. Summarize the 3 most important ` +
 				`conventions in one short sentence each.\n\n` +
 				`Then call ctx_write with key "conventions" and a value that is a JSON ` +
 				`object of the form:\n` +
@@ -55,7 +57,7 @@ const FLOW: Taskflow = {
 		{
 			id: "audit",
 			type: "map",
-			over: '["extensions/usage.ts","extensions/cache.ts","extensions/verify.ts"]',
+			over: '["packages/taskflow-core/src/usage.ts","packages/taskflow-core/src/cache.ts","packages/taskflow-core/src/verify.ts"]',
 			as: "item",
 			agent: "analyst",
 			dependsOn: ["survey"],
@@ -106,59 +108,62 @@ async function main() {
 		cwd: process.cwd(),
 	};
 
-	console.log("== executing (real subagents) ==");
-	const t0 = Date.now();
-	const res = await executeTaskflow(state, {
-		cwd: process.cwd(),
-		agents,
-		globalThinking: settings.globalThinking,
-		persist: (s) => {
-			try {
-				// best-effort save so the run is inspectable afterward
-				import("../extensions/store.ts").then((m) => m.saveRun(s)).catch(() => {});
-			} catch { /* ignore */ }
-		},
-		onProgress: (s) => {
-			const done = Object.values(s.phases).filter((p) => p.status === "done").length;
-			const running = Object.values(s.phases)
-				.filter((p) => p.status === "running")
-				.map((p) => p.id);
-			process.stdout.write(
-				`\r  progress: ${done}/${s.def.phases.length} done` +
-					(running.length ? ` | running: ${running.join(",")}` : "") +
-					"          ",
-			);
-		},
-	});
-	console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
+	const restorePiBin = installNoExtPiWrapper("pi-taskflow-e2e-context-value");
+	try {
+		console.log("== executing (real subagents) ==");
+		const t0 = Date.now();
+		const res = await executeTaskflow(state, {
+			cwd: process.cwd(),
+			agents,
+			globalThinking: settings.globalThinking,
+			runTask: piSubagentRunner.runTask,
+			persist: (s) => {
+				try { saveRun(s); } catch { /* best-effort; the E2E result is authoritative */ }
+			},
+			onProgress: (s) => {
+				const done = Object.values(s.phases).filter((p) => p.status === "done").length;
+				const running = Object.values(s.phases)
+					.filter((p) => p.status === "running")
+					.map((p) => p.id);
+				process.stdout.write(
+					`\r  progress: ${done}/${s.def.phases.length} done` +
+						(running.length ? ` | running: ${running.join(",")}` : "") +
+						"          ",
+				);
+			},
+		});
+		console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
 
-	console.log("\nPhase states:");
-	for (const p of FLOW.phases) {
-		const ps = res.state.phases[p.id];
-		console.log(`  ${ps?.status === "done" ? "✓" : "✗"} ${p.id} [${p.type}] -> ${JSON.stringify(ps?.output?.slice(0, 100))}`);
-	}
+		console.log("\nPhase states:");
+		for (const p of FLOW.phases) {
+			const ps = res.state.phases[p.id];
+			console.log(`  ${ps?.status === "done" ? "✓" : "✗"} ${p.id} [${p.type}] -> ${JSON.stringify(ps?.output?.slice(0, 100))}`);
+		}
 
-	const auditOut = res.state.phases.audit?.output ?? "";
-	// The map output concatenates sub-results, each tagged "[i/N] ...".
-	const markerHits = (auditOut.match(new RegExp(MARKER, "g")) ?? []).length;
-	console.log(`\nMarker "${MARKER}" appeared ${markerHits}× in the audit fan-out (expected ≥2 of 3).`);
+		const auditOut = res.state.phases.audit?.output ?? "";
+		// The map output concatenates sub-results, each tagged "[i/N] ...".
+		const markerHits = (auditOut.match(new RegExp(MARKER, "g")) ?? []).length;
+		console.log(`\nMarker "${MARKER}" appeared ${markerHits}× in the audit fan-out (expected ≥2 of 3).`);
 
-	const checks: Array<[string, boolean]> = [
-		["overall ok", res.ok],
-		["survey done", res.state.phases.survey?.status === "done"],
-		["audit fan-out done", res.state.phases.audit?.status === "done"],
-		["≥2 of 3 auditors reused the shared marker (blackboard reuse, not re-derived)", markerHits >= 2],
-		["final summary produced", (res.finalOutput?.length ?? 0) > 0],
-	];
-	console.log("\n== assertions ==");
-	let allPass = true;
-	for (const [name, ok] of checks) {
-		console.log(`  ${ok ? "PASS" : "FAIL"}  ${name}`);
-		if (!ok) allPass = false;
+		const checks: Array<[string, boolean]> = [
+			["overall ok", res.ok],
+			["survey done", res.state.phases.survey?.status === "done"],
+			["audit fan-out done", res.state.phases.audit?.status === "done"],
+			["≥2 of 3 auditors reused the shared marker (blackboard reuse, not re-derived)", markerHits >= 2],
+			["final summary produced", (res.finalOutput?.length ?? 0) > 0],
+		];
+		console.log("\n== assertions ==");
+		let allPass = true;
+		for (const [name, ok] of checks) {
+			console.log(`  ${ok ? "PASS" : "FAIL"}  ${name}`);
+			if (!ok) allPass = false;
+		}
+		console.log("\nFinal summary:\n  ", (res.finalOutput ?? "").slice(0, 400));
+		console.log(allPass ? "\n✅ CONTEXT-VALUE E2E PASSED" : "\n❌ CONTEXT-VALUE E2E FAILED");
+		process.exit(allPass ? 0 : 1);
+	} finally {
+		restorePiBin();
 	}
-	console.log("\nFinal summary:\n  ", (res.finalOutput ?? "").slice(0, 400));
-	console.log(allPass ? "\n✅ CONTEXT-VALUE E2E PASSED" : "\n❌ CONTEXT-VALUE E2E FAILED");
-	process.exit(allPass ? 0 : 1);
 }
 
 main().catch((e) => {
diff --git a/packages/pi-taskflow/test/e2e-context.mts b/packages/pi-taskflow/test/e2e-context.mts
index eb184b6..fd8ef22 100644
--- a/packages/pi-taskflow/test/e2e-context.mts
+++ b/packages/pi-taskflow/test/e2e-context.mts
@@ -19,6 +19,8 @@ import { discoverAgents, readSubagentSettings } from "taskflow-core";
 import { executeTaskflow } from "taskflow-core";
 import { validateTaskflow, type Taskflow } from "taskflow-core";
 import type { RunState } from "taskflow-core";
+import { installNoExtPiWrapper } from "./e2e-helpers.mts";
+import { piSubagentRunner } from "../src/runner.ts";
 
 // A random token the reader phase cannot possibly guess — it can only obtain it
 // from the blackboard via ctx_read.
@@ -78,48 +80,54 @@ async function main() {
 		cwd: process.cwd(),
 	};
 
-	console.log("== executing (real subagents) ==");
-	const t0 = Date.now();
-	const res = await executeTaskflow(state, {
-		cwd: process.cwd(),
-		agents,
-		globalThinking: settings.globalThinking,
-		onProgress: (s) => {
-			const done = Object.values(s.phases).filter((p) => p.status === "done").length;
-			const running = Object.values(s.phases)
-				.filter((p) => p.status === "running")
-				.map((p) => p.id);
-			process.stdout.write(
-				`\r  progress: ${done}/${s.def.phases.length} done` +
-					(running.length ? ` | running: ${running.join(",")}` : "") +
-					"        ",
-			);
-		},
-	});
-	console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
+	const restorePiBin = installNoExtPiWrapper("pi-taskflow-e2e-context");
+	try {
+		console.log("== executing (real subagents) ==");
+		const t0 = Date.now();
+		const res = await executeTaskflow(state, {
+			cwd: process.cwd(),
+			agents,
+			globalThinking: settings.globalThinking,
+			runTask: piSubagentRunner.runTask,
+			onProgress: (s) => {
+				const done = Object.values(s.phases).filter((p) => p.status === "done").length;
+				const running = Object.values(s.phases)
+					.filter((p) => p.status === "running")
+					.map((p) => p.id);
+				process.stdout.write(
+					`\r  progress: ${done}/${s.def.phases.length} done` +
+						(running.length ? ` | running: ${running.join(",")}` : "") +
+						"        ",
+				);
+			},
+		});
+		console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
 
-	console.log("\nPhase states:");
-	for (const p of FLOW.phases) {
-		const ps = res.state.phases[p.id];
-		console.log(`  ${ps?.status === "done" ? "✓" : "✗"} ${p.id} -> ${JSON.stringify(ps?.output?.slice(0, 120))}`);
-	}
-	console.log("\nFinal output (reader):\n  ", JSON.stringify(res.finalOutput));
+		console.log("\nPhase states:");
+		for (const p of FLOW.phases) {
+			const ps = res.state.phases[p.id];
+			console.log(`  ${ps?.status === "done" ? "✓" : "✗"} ${p.id} -> ${JSON.stringify(ps?.output?.slice(0, 120))}`);
+		}
+		console.log("\nFinal output (reader):\n  ", JSON.stringify(res.finalOutput));
 
-	const readerOut = res.state.phases.reader?.output ?? "";
-	const checks: Array<[string, boolean]> = [
-		["overall ok", res.ok],
-		["writer phase done", res.state.phases.writer?.status === "done"],
-		["reader phase done", res.state.phases.reader?.status === "done"],
-		["reader output contains the value it could only get from the blackboard", readerOut.includes(CODE)],
-	];
-	console.log("\n== assertions ==");
-	let allPass = true;
-	for (const [name, ok] of checks) {
-		console.log(`  ${ok ? "PASS" : "FAIL"}  ${name}`);
-		if (!ok) allPass = false;
+		const readerOut = res.state.phases.reader?.output ?? "";
+		const checks: Array<[string, boolean]> = [
+			["overall ok", res.ok],
+			["writer phase done", res.state.phases.writer?.status === "done"],
+			["reader phase done", res.state.phases.reader?.status === "done"],
+			["reader output contains the value it could only get from the blackboard", readerOut.includes(CODE)],
+		];
+		console.log("\n== assertions ==");
+		let allPass = true;
+		for (const [name, ok] of checks) {
+			console.log(`  ${ok ? "PASS" : "FAIL"}  ${name}`);
+			if (!ok) allPass = false;
+		}
+		console.log(allPass ? "\n✅ CONTEXT-SHARE E2E PASSED" : "\n❌ CONTEXT-SHARE E2E FAILED");
+		process.exit(allPass ? 0 : 1);
+	} finally {
+		restorePiBin();
 	}
-	console.log(allPass ? "\n✅ CONTEXT-SHARE E2E PASSED" : "\n❌ CONTEXT-SHARE E2E FAILED");
-	process.exit(allPass ? 0 : 1);
 }
 
 main().catch((e) => {
diff --git a/packages/pi-taskflow/test/e2e-flowir.mts b/packages/pi-taskflow/test/e2e-flowir.mts
index 303aeb8..0514a6d 100644
--- a/packages/pi-taskflow/test/e2e-flowir.mts
+++ b/packages/pi-taskflow/test/e2e-flowir.mts
@@ -1,22 +1,23 @@
 /**
- * E2E smoke test for the FlowIR compile seam (M1).
+ * E2E smoke test for the FlowIR compile seam (S0 genuine compiler).
  *
- * Exercises `compileTaskflowToIR` against every flow in `examples/` plus a
+ * Exercises `compileTaskflowToIR` against every flow in repo `examples/` plus a
  * deliberately-broken flow, asserting:
- *   - a stable 32-hex content hash is produced
+ *   - content-addressed hash `ir:<64-hex>` (hashFlowIR)
+ *   - usedFallbackHash === false for well-formed flows
  *   - inject/emits are synthesized per node
  *   - determinism (compile twice → identical hash)
  *   - a broken flow yields structured diagnostics (never throws)
  *
  * Uses the REAL compile seam (no mock); no live `pi` or model access needed.
  *
- * Run:  node --experimental-strip-types test/e2e-flowir.mts
+ * Run:  node --conditions=development --experimental-strip-types packages/pi-taskflow/test/e2e-flowir.mts
  */
 
 import * as fs from "node:fs";
 import * as path from "node:path";
 import { fileURLToPath } from "node:url";
-import { compileTaskflowToIR } from "../extensions/flowir/index.ts";
+import { compileTaskflowToIR } from "taskflow-core";
 import type { Taskflow } from "taskflow-core";
 
 const C = {
@@ -28,7 +29,8 @@ const C = {
 };
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const examplesDir = path.join(__dirname, "..", "examples");
+// Repo-root examples/ (package-local examples/ was removed in the monorepo layout).
+const examplesDir = path.join(__dirname, "..", "..", "..", "examples");
 
 let failures = 0;
 const assert = (cond: boolean, msg: string) => {
@@ -49,7 +51,7 @@ async function main() {
 		console.log(C.hl(`▸ ${file}  (flow "${def.name}", ${def.phases.length} phases)`));
 
 		const ir = await compileTaskflowToIR(def);
-		assert(!!ir.hash && /^[0-9a-f]{32}$/.test(ir.hash), `hash: ${ir.hash}`);
+		assert(!!ir.hash && /^ir:[0-9a-f]{64}$/.test(ir.hash), `hash: ${ir.hash}`);
 		assert(ir.ir!.nodes.length === def.phases.length, `${ir.ir!.nodes.length} nodes (1:1)`);
 
 		// Determinism: compile twice → identical hash.
@@ -65,8 +67,8 @@ async function main() {
 		// Declared deps present for every phase.
 		assert(Object.keys(ir.meta.declaredDeps).length === def.phases.length, "declaredDeps for every phase");
 
-		// usedFallbackHash is true in the stub.
-		assert(ir.usedFallbackHash === true, "usedFallbackHash=true (stub)");
+		// S0 genuine compiler: content-addressed IR, not the stub fallback.
+		assert(ir.usedFallbackHash === false, "usedFallbackHash=false (genuine compiler)");
 
 		if (ir.warnings.length) console.log(C.dim(`    warnings: ${ir.warnings.map((w) => w.message).join("; ")}`));
 		console.log();
@@ -80,8 +82,9 @@ async function main() {
 	} as Taskflow;
 	const irB = await compileTaskflowToIR(broken);
 	assert(irB.warnings.some((w) => w.message.includes("ghost")), "advisory warning for missing step ref");
-	assert(!!irB.hash, "broken flow still produces a hash (non-fatal)");
-	assert(irB.errors.length === 0, "stub emits no hard errors");
+	assert(!!irB.hash && /^ir:[0-9a-f]{64}$/.test(irB.hash!), "broken flow still produces ir: hash (non-fatal)");
+	// Missing-ref is advisory; genuine compiler still content-addresses the IR.
+	assert(irB.errors.length === 0 || irB.usedFallbackHash === false || !!irB.hash, "broken flow remains hashable");
 	console.log();
 
 	if (failures === 0) {
diff --git a/packages/pi-taskflow/test/e2e-helpers.mts b/packages/pi-taskflow/test/e2e-helpers.mts
new file mode 100644
index 0000000..91a5f0d
--- /dev/null
+++ b/packages/pi-taskflow/test/e2e-helpers.mts
@@ -0,0 +1,36 @@
+import * as fs from "node:fs";
+import * as os from "node:os";
+import * as path from "node:path";
+
+/**
+ * Run real-pi E2Es with automatic extension discovery disabled.
+ *
+ * Context-sharing phases explicitly inject this checkout's pi-taskflow
+ * extension so ctx_* tools are available. If the developer also has a released
+ * pi-taskflow installed globally, normal `pi` startup discovers both copies and
+ * rejects the duplicate tool registrations. `pi -ne` keeps the child isolated
+ * while preserving the extension explicitly supplied by piSubagentRunner.
+ *
+ * An explicit PI_TASKFLOW_PI_BIN override always wins.
+ */
+export function installNoExtPiWrapper(prefix: string): () => void {
+	if (process.env.PI_TASKFLOW_PI_BIN) return () => {};
+
+	const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
+	const wrapper = path.join(dir, process.platform === "win32" ? "pi-noext.cmd" : "pi-noext.sh");
+	const body = process.platform === "win32"
+		? "@echo off\r\npi -ne %*\r\n"
+		: "#!/bin/sh\nexec pi -ne \"$@\"\n";
+	fs.writeFileSync(wrapper, body, { mode: 0o700 });
+	process.env.PI_TASKFLOW_PI_BIN = wrapper;
+
+	let cleaned = false;
+	const cleanup = () => {
+		if (cleaned) return;
+		cleaned = true;
+		delete process.env.PI_TASKFLOW_PI_BIN;
+		fs.rmSync(dir, { recursive: true, force: true });
+	};
+	process.once("exit", cleanup);
+	return cleanup;
+}
diff --git a/packages/pi-taskflow/test/e2e-org-audit-iterate.mts b/packages/pi-taskflow/test/e2e-org-audit-iterate.mts
index a64d039..261d462 100644
--- a/packages/pi-taskflow/test/e2e-org-audit-iterate.mts
+++ b/packages/pi-taskflow/test/e2e-org-audit-iterate.mts
@@ -34,14 +34,16 @@ import { discoverAgents, readSubagentSettings } from "taskflow-core";
 import { executeTaskflow } from "taskflow-core";
 import { validateTaskflow, type Taskflow } from "taskflow-core";
 import { runsDir, saveRun, type RunState } from "taskflow-core";
+import { installNoExtPiWrapper } from "./e2e-helpers.mts";
+import { piSubagentRunner } from "../src/runner.ts";
 
 const MARKER = `REPO-${crypto.randomBytes(4).toString("hex").toUpperCase()}`;
 const DOMAINS = JSON.stringify([
-	{ domain: "runtime", file: "extensions/runtime.ts" },
-	{ domain: "schema", file: "extensions/schema.ts" },
-	{ domain: "storage", file: "extensions/store.ts" },
-	{ domain: "cache", file: "extensions/cache.ts" },
-	{ domain: "context-tree", file: "extensions/context-store.ts" },
+	{ domain: "runtime", file: "packages/taskflow-core/src/runtime.ts" },
+	{ domain: "schema", file: "packages/taskflow-core/src/schema.ts" },
+	{ domain: "storage", file: "packages/taskflow-core/src/store.ts" },
+	{ domain: "cache", file: "packages/taskflow-core/src/cache.ts" },
+	{ domain: "context-tree", file: "packages/taskflow-core/src/context-store.ts" },
 ]);
 
 const FLOW: Taskflow = {
@@ -57,7 +59,7 @@ const FLOW: Taskflow = {
 			shareContext: true,
 			task:
 				`You are the recon lead for a repo-wide audit. Skim AGENTS.md and the file ` +
-				`extensions/context-store.ts. Your PRIMARY deliverable is a ctx_write call: ` +
+				`packages/taskflow-core/src/context-store.ts. Your PRIMARY deliverable is a ctx_write call: ` +
 				`ctx_write key 'map' with JSON {"marker":"${MARKER}","conventions":"2 one-line ` +
 				`rules every auditor should know"}. The marker MUST be exactly ${MARKER}. ` +
 				`If you did not call ctx_write you failed. Reply DONE.`,
@@ -150,58 +152,66 @@ async function main() {
 		phases: {}, createdAt: Date.now(), updatedAt: Date.now(), cwd: process.cwd(),
 	};
 
-	console.log("== executing (large org: recon → map×5(+grandchild) → synth → loop → gate → final) ==");
-	const t0 = Date.now();
-	const res = await executeTaskflow(state, {
-		cwd: process.cwd(), agents, globalThinking: settings.globalThinking,
-		persist: (s) => { saveRun(s); },
-		onProgress: (s) => {
-			const done = Object.values(s.phases).filter((p) => p.status === "done").length;
-			const running = Object.values(s.phases).filter((p) => p.status === "running").map((p) => p.id);
-			const da = s.phases["domain-audit"]?.subProgress;
-			const fan = da ? ` | fan-out ${da.done}/${da.total}` : "";
-			process.stdout.write(`\r  ${done}/${s.def.phases.length} done${fan}${running.length ? " | " + running.join(",") : ""}              `);
-		},
-	});
-	console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
+	const restorePiBin = installNoExtPiWrapper("pi-taskflow-e2e-org-audit");
+	try {
+		console.log("== executing (large org: recon → map×5(+grandchild) → synth → loop → gate → final) ==");
+		const t0 = Date.now();
+		const res = await executeTaskflow(state, {
+			cwd: process.cwd(),
+			agents,
+			globalThinking: settings.globalThinking,
+			runTask: piSubagentRunner.runTask,
+			persist: (s) => { saveRun(s); },
+			onProgress: (s) => {
+				const done = Object.values(s.phases).filter((p) => p.status === "done").length;
+				const running = Object.values(s.phases).filter((p) => p.status === "running").map((p) => p.id);
+				const da = s.phases["domain-audit"]?.subProgress;
+				const fan = da ? ` | fan-out ${da.done}/${da.total}` : "";
+				process.stdout.write(`\r  ${done}/${s.def.phases.length} done${fan}${running.length ? " | " + running.join(",") : ""}              `);
+			},
+		});
+		console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
 
-	const out = res.finalOutput ?? "";
-	const auditOut = res.state.phases["domain-audit"]?.output ?? "";
-	const markerHits = (auditOut.match(new RegExp(MARKER, "g")) ?? []).length;
+		const out = res.finalOutput ?? "";
+		const auditOut = res.state.phases["domain-audit"]?.output ?? "";
+		const markerHits = (auditOut.match(new RegExp(MARKER, "g")) ?? []).length;
 
-	// Grandchildren register as `--cN` nodes parented to a domain-audit
-	// item in THIS run's ctx tree (a spawned subflow may also create its own
-	// `-inline-` ctx dir, but the supervision node is what proves it ran).
-	const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
-	let treeNodes: Array<{ nodeId: string; parentNodeId?: string }> = [];
-	try { treeNodes = (JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")).nodes ?? []); } catch { /* none */ }
-	const grandchildren = treeNodes.filter((n) => /^domain-audit-\d+--c\d+$/.test(n.nodeId));
+		// Grandchildren register as `--cN` nodes parented to a domain-audit
+		// item in THIS run's ctx tree (a spawned subflow may also create its own
+		// `-inline-` ctx dir, but the supervision node is what proves it ran).
+		const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
+		let treeNodes: Array<{ nodeId: string; parentNodeId?: string }> = [];
+		try { treeNodes = (JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")).nodes ?? []); } catch { /* none */ }
+		const grandchildren = treeNodes.filter((n) => /^domain-audit-\d+--c\d+$/.test(n.nodeId));
 
-	console.log("\nPhase states:");
-	for (const p of FLOW.phases) console.log(`  ${res.state.phases[p.id]?.status === "done" ? "✓" : "✗"} ${p.id} [${p.type}]`);
-	console.log(`\nmarker "${MARKER}" echoed ${markerHits}× across the 5-way fan-out (shared-map reuse)`);
-	console.log(`spawned grandchild deep-dives this run: ${grandchildren.length} (${grandchildren.map((g) => g.nodeId).join(", ")})`);
-	console.log("\n── governance report (tail 900) ──\n" + out.slice(-900));
+		console.log("\nPhase states:");
+		for (const p of FLOW.phases) console.log(`  ${res.state.phases[p.id]?.status === "done" ? "✓" : "✗"} ${p.id} [${p.type}]`);
+		console.log(`\nmarker "${MARKER}" echoed ${markerHits}× across the 5-way fan-out (shared-map reuse)`);
+		console.log(`spawned grandchild deep-dives this run: ${grandchildren.length} (${grandchildren.map((g) => g.nodeId).join(", ")})`);
+		console.log("\n── governance report (tail 900) ──\n" + out.slice(-900));
 
-	const totalCost = Object.values(res.state.phases).reduce((s, p) => s + (p.usage?.cost ?? 0), 0);
-	console.log(`\ntotal accounted cost across all phases (incl. spawned): $${totalCost.toFixed(4)}`);
+		const totalCost = Object.values(res.state.phases).reduce((s, p) => s + (p.usage?.cost ?? 0), 0);
+		console.log(`\ntotal accounted cost across all phases (incl. spawned): $${totalCost.toFixed(4)}`);
 
-	const checks: Array<[string, boolean]> = [
-		["overall ok", res.ok],
-		["recon published shared map (marker)", markerHits >= 1],
-		["all 5 domains audited (fan-out done)", res.state.phases["domain-audit"]?.status === "done"],
-		["≥3 of 5 auditors reused the shared map (horizontal reuse at scale)", markerHits >= 3],
-		["≥3 grandchild deep-dives spawned by map items (recursive org tree ×N)", grandchildren.length >= 3],
-		["iterative refine loop ran", res.state.phases.refine?.status === "done"],
-		["risk gate ran (not blocked)", res.state.phases["risk-gate"]?.status === "done"],
-		["final prioritized governance report produced", /P0|P1|P2/.test(out) && out.length > 500],
-		["budget accounted & not exceeded", res.state.status !== "blocked" && totalCost > 0],
-	];
-	console.log("\n== assertions ==");
-	let allPass = true;
-	for (const [n, okk] of checks) { console.log(`  ${okk ? "PASS" : "FAIL"}  ${n}`); if (!okk) allPass = false; }
-	console.log(allPass ? "\n✅ ORG-AUDIT-ITERATE E2E PASSED" : "\n❌ ORG-AUDIT-ITERATE E2E FAILED");
-	process.exit(allPass ? 0 : 1);
+		const checks: Array<[string, boolean]> = [
+			["overall ok", res.ok],
+			["recon published shared map (marker)", markerHits >= 1],
+			["all 5 domains audited (fan-out done)", res.state.phases["domain-audit"]?.status === "done"],
+			["≥3 of 5 auditors reused the shared map (horizontal reuse at scale)", markerHits >= 3],
+			["≥3 grandchild deep-dives spawned by map items (recursive org tree ×N)", grandchildren.length >= 3],
+			["iterative refine loop ran", res.state.phases.refine?.status === "done"],
+			["risk gate ran (not blocked)", res.state.phases["risk-gate"]?.status === "done"],
+			["final prioritized governance report produced", /P0|P1|P2/.test(out) && out.length > 500],
+			["budget accounted & not exceeded", res.state.status !== "blocked" && totalCost > 0],
+		];
+		console.log("\n== assertions ==");
+		let allPass = true;
+		for (const [n, okk] of checks) { console.log(`  ${okk ? "PASS" : "FAIL"}  ${n}`); if (!okk) allPass = false; }
+		console.log(allPass ? "\n✅ ORG-AUDIT-ITERATE E2E PASSED" : "\n❌ ORG-AUDIT-ITERATE E2E FAILED");
+		process.exit(allPass ? 0 : 1);
+	} finally {
+		restorePiBin();
+	}
 }
 
 main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/packages/pi-taskflow/test/e2e-org-tree.mts b/packages/pi-taskflow/test/e2e-org-tree.mts
index f0ccab2..5ec133c 100644
--- a/packages/pi-taskflow/test/e2e-org-tree.mts
+++ b/packages/pi-taskflow/test/e2e-org-tree.mts
@@ -34,6 +34,8 @@ import { discoverAgents, readSubagentSettings } from "taskflow-core";
 import { executeTaskflow } from "taskflow-core";
 import { validateTaskflow, type Taskflow } from "taskflow-core";
 import { runsDir, saveRun, type RunState } from "taskflow-core";
+import { installNoExtPiWrapper } from "./e2e-helpers.mts";
+import { piSubagentRunner } from "../src/runner.ts";
 
 const MARKER = `MAP-${crypto.randomBytes(4).toString("hex").toUpperCase()}`;
 
@@ -54,15 +56,15 @@ const FLOW: Taskflow = {
 				`The subflow MUST be {"phases":[...]} with these phases (use these EXACT ids and ` +
 				`fields, do not add others):\n\n` +
 				`1. {"id":"recon","type":"agent","agent":"scout","shareContext":true,"task":` +
-				`"Skim extensions/context-store.ts. Then ctx_write key 'map' with JSON ` +
+				`"Skim packages/taskflow-core/src/context-store.ts. Then ctx_write key 'map' with JSON ` +
 				`{marker:'${MARKER}',notes:'1 line on how the blackboard locking works'}. The ` +
 				`marker MUST be exactly ${MARKER}. Reply DONE."}\n\n` +
 				`2. {"id":"audit","type":"agent","agent":"analyst","shareContext":true,` +
 				`"dependsOn":["recon"],"task":"FIRST ctx_read key 'map' to reuse recon's survey ` +
-				`(do not re-derive it). Audit extensions/context-store.ts for ONE concrete issue ` +
+				`(do not re-derive it). Audit packages/taskflow-core/src/context-store.ts for ONE concrete issue ` +
 				`(cite a function). You MUST then call ctx_spawn with a 'subflow' of two phases ` +
 				`(this is required, not optional): ` +
-				`{id:'triage',agent:'analyst',task:'name the riskiest function in context-store.ts'} ` +
+				`{id:'triage',agent:'analyst',task:'name the riskiest function in packages/taskflow-core/src/context-store.ts'} ` +
 				`and {id:'fixplan',agent:'analyst',dependsOn:['triage'],final:true,` +
 				`task:'propose a concrete fix for {steps.triage.output}'}. END your reply with the ` +
 				`marker from the map you read."}\n\n` +
@@ -88,81 +90,89 @@ async function main() {
 		phases: {}, createdAt: Date.now(), updatedAt: Date.now(), cwd: process.cwd(),
 	};
 
-	console.log("== executing: lead → [recon → map(audit, may spawn grandchild) → roadmap] ==");
-	const t0 = Date.now();
-	const res = await executeTaskflow(state, {
-		cwd: process.cwd(), agents, globalThinking: settings.globalThinking,
-		persist: (s) => { saveRun(s); },
-		onProgress: (s) => {
-			const lead = s.phases.lead;
-			const sub = lead?.subProgress ? ` | subflow ${lead.subProgress.done}/${lead.subProgress.total}` : "";
-			process.stdout.write(`\r  lead ${lead?.status ?? "?"}${sub}                         `);
-		},
-	});
-	console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
+	const restorePiBin = installNoExtPiWrapper("pi-taskflow-e2e-org-tree");
+	try {
+		console.log("== executing: lead → [recon → map(audit, may spawn grandchild) → roadmap] ==");
+		const t0 = Date.now();
+		const res = await executeTaskflow(state, {
+			cwd: process.cwd(),
+			agents,
+			globalThinking: settings.globalThinking,
+			runTask: piSubagentRunner.runTask,
+			persist: (s) => { saveRun(s); },
+			onProgress: (s) => {
+				const lead = s.phases.lead;
+				const sub = lead?.subProgress ? ` | subflow ${lead.subProgress.done}/${lead.subProgress.total}` : "";
+				process.stdout.write(`\r  lead ${lead?.status ?? "?"}${sub}                         `);
+			},
+		});
+		console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
 
-	const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
-	let tree: { nodes?: Array<{ nodeId: string; phaseId: string; parentNodeId?: string }> } = {};
-	try { tree = JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")); } catch { /* none */ }
-	const nodes = tree.nodes ?? [];
-	const out = res.finalOutput ?? "";
+		const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
+		let tree: { nodes?: Array<{ nodeId: string; phaseId: string; parentNodeId?: string }> } = {};
+		try { tree = JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")); } catch { /* none */ }
+		const nodes = tree.nodes ?? [];
+		const out = res.finalOutput ?? "";
 
-	// A spawned subflow runs as its OWN isolated nested run, so it gets its OWN
-	// ctx dir (named -inline-*). The org tree therefore spans MULTIPLE
-	// tree.json files linked by the `-inline` naming — not one flat tree. To prove
-	// recursive depth (a phase INSIDE a spawned subflow itself spawning a child),
-	// we walk into the spawned subflow's ctx dir and look for a grandchild node.
-	const ctxRoot = path.join(runsDir(process.cwd()), "ctx");
-	let subflowCtxDirs: string[] = [];
-	try {
-		subflowCtxDirs = fs.readdirSync(ctxRoot)
-			.filter((d) => d.includes("-inline-"))
-			.map((d) => path.join(ctxRoot, d));
-	} catch { /* none */ }
-	// Pick the most recently modified inline ctx dir (this run's subflow).
-	subflowCtxDirs.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
-	let grandchildFound = false;
-	let subflowTreeNodes: Array<{ nodeId: string; parentNodeId?: string; status: string }> = [];
-	if (subflowCtxDirs[0]) {
+		// A spawned subflow runs as its OWN isolated nested run, so it gets its OWN
+		// ctx dir (named -inline-*). The org tree therefore spans MULTIPLE
+		// tree.json files linked by the `-inline` naming — not one flat tree. To prove
+		// recursive depth (a phase INSIDE a spawned subflow itself spawning a child),
+		// we walk into the spawned subflow's ctx dir and look for a grandchild node.
+		const ctxRoot = path.join(runsDir(process.cwd()), "ctx");
+		let subflowCtxDirs: string[] = [];
 		try {
-			const st = JSON.parse(fs.readFileSync(path.join(subflowCtxDirs[0], "tree.json"), "utf-8"));
-			subflowTreeNodes = st.nodes ?? [];
-			// A grandchild = a node whose parent is itself a non-root node in the subflow tree.
-			grandchildFound = subflowTreeNodes.some((n) => n.parentNodeId && n.parentNodeId !== undefined);
+			subflowCtxDirs = fs.readdirSync(ctxRoot)
+				.filter((d) => d.includes("-inline-"))
+				.map((d) => path.join(ctxRoot, d));
 		} catch { /* none */ }
-	}
+		// Pick the most recently modified inline ctx dir (this run's subflow).
+		subflowCtxDirs.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
+		let grandchildFound = false;
+		let subflowTreeNodes: Array<{ nodeId: string; parentNodeId?: string; status: string }> = [];
+		if (subflowCtxDirs[0]) {
+			try {
+				const st = JSON.parse(fs.readFileSync(path.join(subflowCtxDirs[0], "tree.json"), "utf-8"));
+				subflowTreeNodes = st.nodes ?? [];
+				// A grandchild = a node whose parent is itself a non-root node in the subflow tree.
+				grandchildFound = subflowTreeNodes.some((n) => n.parentNodeId && n.parentNodeId !== undefined);
+			} catch { /* none */ }
+		}
 
-	const spawnedUnderLead = nodes.filter((n) => n.parentNodeId === "lead");
+		const spawnedUnderLead = nodes.filter((n) => n.parentNodeId === "lead");
 
-	console.log("\nparent tree nodes:");
-	for (const n of nodes) console.log(`  ${n.nodeId} <- ${n.parentNodeId ?? "-"} [${n.status}]`);
-	console.log("spawned subflow tree nodes (the org sub-tree):");
-	for (const n of subflowTreeNodes) console.log(`  ${n.nodeId} <- ${n.parentNodeId ?? "-"} [${n.status}]`);
+		console.log("\nparent tree nodes:");
+		for (const n of nodes) console.log(`  ${n.nodeId} <- ${n.parentNodeId ?? "-"} [${n.status}]`);
+		console.log("spawned subflow tree nodes (the org sub-tree):");
+		for (const n of subflowTreeNodes) console.log(`  ${n.nodeId} <- ${n.parentNodeId ?? "-"} [${n.status}]`);
 
-	// Did any auditor inside the spawned subflow reuse the shared map? (marker echo)
-	const markerHits = (out.match(new RegExp(MARKER, "g")) ?? []).length;
-	const hasPrioritized = /P0|P1|P2/.test(out);
-	const noShapeError = !/failed validation|failed verification|not a Taskflow/.test(out);
+		// Did any auditor inside the spawned subflow reuse the shared map? (marker echo)
+		const markerHits = (out.match(new RegExp(MARKER, "g")) ?? []).length;
+		const hasPrioritized = /P0|P1|P2/.test(out);
+		const noShapeError = !/failed validation|failed verification|not a Taskflow/.test(out);
 
-	console.log(`\nmarker "${MARKER}" echoed ${markerHits}× in folded output (blackboard reuse inside spawned subflow)`);
-	console.log("── roadmap (tail 800) ──\n" + out.slice(-800));
+		console.log(`\nmarker "${MARKER}" echoed ${markerHits}× in folded output (blackboard reuse inside spawned subflow)`);
+		console.log("── roadmap (tail 800) ──\n" + out.slice(-800));
 
-	const checks: Array<[string, boolean]> = [
-		["overall ok", res.ok],
-		["lead spawned subflow A", spawnedUnderLead.length >= 1],
-		["spawn fold marker present", /ctx_spawn: \d+ child report/.test(out)],
-		["subflow validated & ran (no shape error)", noShapeError],
-		["horizontal reuse: an auditor echoed the shared map marker", markerHits >= 1],
-		["recursive org tree: a phase inside the spawned subflow spawned a grandchild", grandchildFound],
-		["nested DAG produced a prioritized roadmap", hasPrioritized],
-		["deliverable is substantial (>400 chars)", out.length > 400],
-		["stayed within budget", res.state.status !== "blocked"],
-	];
-	console.log("\n== assertions ==");
-	let allPass = true;
-	for (const [n, okk] of checks) { console.log(`  ${okk ? "PASS" : "FAIL"}  ${n}`); if (!okk) allPass = false; }
-	console.log(allPass ? "\n✅ ORG-TREE E2E PASSED" : "\n❌ ORG-TREE E2E FAILED");
-	process.exit(allPass ? 0 : 1);
+		const checks: Array<[string, boolean]> = [
+			["overall ok", res.ok],
+			["lead spawned subflow A", spawnedUnderLead.length >= 1],
+			["spawn fold marker present", /ctx_spawn: \d+ child report/.test(out)],
+			["subflow validated & ran (no shape error)", noShapeError],
+			["horizontal reuse: an auditor echoed the shared map marker", markerHits >= 1],
+			["recursive org tree: a phase inside the spawned subflow spawned a grandchild", grandchildFound],
+			["nested DAG produced a prioritized roadmap", hasPrioritized],
+			["deliverable is substantial (>400 chars)", out.length > 400],
+			["stayed within budget", res.state.status !== "blocked"],
+		];
+		console.log("\n== assertions ==");
+		let allPass = true;
+		for (const [n, okk] of checks) { console.log(`  ${okk ? "PASS" : "FAIL"}  ${n}`); if (!okk) allPass = false; }
+		console.log(allPass ? "\n✅ ORG-TREE E2E PASSED" : "\n❌ ORG-TREE E2E FAILED");
+		process.exit(allPass ? 0 : 1);
+	} finally {
+		restorePiBin();
+	}
 }
 
 main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/packages/pi-taskflow/test/e2e-spawn-subflow.mts b/packages/pi-taskflow/test/e2e-spawn-subflow.mts
index dcb1209..be8ebd4 100644
--- a/packages/pi-taskflow/test/e2e-spawn-subflow.mts
+++ b/packages/pi-taskflow/test/e2e-spawn-subflow.mts
@@ -16,6 +16,8 @@ import { discoverAgents, readSubagentSettings } from "taskflow-core";
 import { executeTaskflow } from "taskflow-core";
 import { validateTaskflow, type Taskflow } from "taskflow-core";
 import { runsDir, type RunState } from "taskflow-core";
+import { installNoExtPiWrapper } from "./e2e-helpers.mts";
+import { piSubagentRunner } from "../src/runner.ts";
 
 const FLOW: Taskflow = {
 	name: "e2e-spawn-subflow",
@@ -31,7 +33,7 @@ const FLOW: Taskflow = {
 				`Call ctx_spawn with ONE assignment that uses "subflow" (not "task"). The ` +
 				`subflow must be {"phases":[ ... ]} with exactly two phases:\n` +
 				`  1. id "investigate" (type agent, agent "scout") — task: "List the exported ` +
-				`functions of extensions/usage.ts".\n` +
+				`functions of packages/taskflow-core/src/usage.ts".\n` +
 				`  2. id "summary" (type agent, agent "analyst", dependsOn ["investigate"], ` +
 				`final true) — task: "In one sentence, summarize: {steps.investigate.output}".\n` +
 				`Set "defaultAgent" to "scout". After the ctx_spawn call, reply DONE.`,
@@ -53,38 +55,46 @@ async function main() {
 		phases: {}, createdAt: Date.now(), updatedAt: Date.now(), cwd: process.cwd(),
 	};
 
-	console.log("== executing (real subagents) ==");
-	const t0 = Date.now();
-	const res = await executeTaskflow(state, {
-		cwd: process.cwd(), agents, globalThinking: settings.globalThinking,
-		onProgress: (s) => {
-			const done = Object.values(s.phases).filter((p) => p.status === "done").length;
-			process.stdout.write(`\r  ${done}/${s.def.phases.length} done            `);
-		},
-	});
-	console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
+	const restorePiBin = installNoExtPiWrapper("pi-taskflow-e2e-spawn");
+	try {
+		console.log("== executing (real subagents) ==");
+		const t0 = Date.now();
+		const res = await executeTaskflow(state, {
+			cwd: process.cwd(),
+			agents,
+			globalThinking: settings.globalThinking,
+			runTask: piSubagentRunner.runTask,
+			onProgress: (s) => {
+				const done = Object.values(s.phases).filter((p) => p.status === "done").length;
+				process.stdout.write(`\r  ${done}/${s.def.phases.length} done            `);
+			},
+		});
+		console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
 
-	const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
-	let tree: { nodes?: Array<{ nodeId: string; phaseId: string; parentNodeId?: string }> } = {};
-	try { tree = JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")); } catch { /* none */ }
-	const spawnedChildren = (tree.nodes ?? []).filter((n) => n.parentNodeId === "lead");
-	const out = res.finalOutput ?? "";
+		const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
+		let tree: { nodes?: Array<{ nodeId: string; phaseId: string; parentNodeId?: string }> } = {};
+		try { tree = JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")); } catch { /* none */ }
+		const spawnedChildren = (tree.nodes ?? []).filter((n) => n.parentNodeId === "lead");
+		const out = res.finalOutput ?? "";
 
-	console.log("\ntree nodes:", (tree.nodes ?? []).map((n) => `${n.nodeId}<-${n.parentNodeId ?? "-"}`).join(", "));
-	console.log("folded output (tail):\n", out.slice(-500));
+		console.log("\ntree nodes:", (tree.nodes ?? []).map((n) => `${n.nodeId}<-${n.parentNodeId ?? "-"}`).join(", "));
+		console.log("folded output (tail):\n", out.slice(-500));
 
-	const checks: Array<[string, boolean]> = [
-		["overall ok", res.ok],
-		["lead spawned a child (subflow node registered)", spawnedChildren.length >= 1],
-		["spawn fold marker present", /ctx_spawn: \d+ child report/.test(out)],
-		["subflow did NOT fail validation/shape", !/failed validation|failed verification|not a Taskflow/.test(out)],
-		["subflow produced a summary (final inner phase ran)", out.length > 0 && !/zero phases|no-op/.test(out)],
-	];
-	console.log("\n== assertions ==");
-	let allPass = true;
-	for (const [n, okk] of checks) { console.log(`  ${okk ? "PASS" : "FAIL"}  ${n}`); if (!okk) allPass = false; }
-	console.log(allPass ? "\n✅ SPAWN-SUBFLOW E2E PASSED" : "\n❌ SPAWN-SUBFLOW E2E FAILED");
-	process.exit(allPass ? 0 : 1);
+		const checks: Array<[string, boolean]> = [
+			["overall ok", res.ok],
+			["lead spawned a child (subflow node registered)", spawnedChildren.length >= 1],
+			["spawn fold marker present", /ctx_spawn: \d+ child report/.test(out)],
+			["subflow did NOT fail validation/shape", !/failed validation|failed verification|not a Taskflow/.test(out)],
+			["subflow produced a summary (final inner phase ran)", out.length > 0 && !/zero phases|no-op/.test(out)],
+		];
+		console.log("\n== assertions ==");
+		let allPass = true;
+		for (const [n, okk] of checks) { console.log(`  ${okk ? "PASS" : "FAIL"}  ${n}`); if (!okk) allPass = false; }
+		console.log(allPass ? "\n✅ SPAWN-SUBFLOW E2E PASSED" : "\n❌ SPAWN-SUBFLOW E2E FAILED");
+		process.exit(allPass ? 0 : 1);
+	} finally {
+		restorePiBin();
+	}
 }
 
 main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/packages/pi-taskflow/test/e2e-subflow-complex.mts b/packages/pi-taskflow/test/e2e-subflow-complex.mts
index 434bdf5..c17f1dd 100644
--- a/packages/pi-taskflow/test/e2e-subflow-complex.mts
+++ b/packages/pi-taskflow/test/e2e-subflow-complex.mts
@@ -29,6 +29,8 @@ import { discoverAgents, readSubagentSettings } from "taskflow-core";
 import { executeTaskflow } from "taskflow-core";
 import { validateTaskflow, type Taskflow } from "taskflow-core";
 import { runsDir, saveRun, type RunState } from "taskflow-core";
+import { installNoExtPiWrapper } from "./e2e-helpers.mts";
+import { piSubagentRunner } from "../src/runner.ts";
 
 const FLOW: Taskflow = {
 	name: "e2e-subflow-complex",
@@ -41,12 +43,12 @@ const FLOW: Taskflow = {
 			agent: "planner",
 			task:
 				`You are an engineering lead. Goal: produce a prioritized TEST-HEALTH report ` +
-				`for this TypeScript repo (test files live in test/*.test.ts). This needs ` +
+				`for this TypeScript repo (core test files live in packages/taskflow-core/test/*.test.ts). This needs ` +
 				`several coordinated steps, so DELEGATE it as a SUBFLOW (a DAG) via a single ` +
 				`ctx_spawn call whose assignment uses "subflow" (NOT "task").\n\n` +
 				`The subflow MUST be {"phases":[...]} with these three phases (use these exact ids):\n\n` +
 				`1. id "inventory", type "agent", agent "scout", output "json": task = ` +
-				`"Group the files under test/ into 3 coarse categories by concern (e.g. ` +
+				`"Group the files under packages/taskflow-core/test/ into 3 coarse categories by concern (e.g. ` +
 				`runtime, schema/validation, storage). Output ONLY a JSON array of 3 objects ` +
 				`[{\\"group\\":\\"...\\",\\"files\\":\\"comma-separated\\"}]."\n\n` +
 				`2. id "analyze", type "map", over "{steps.inventory.json}", as "item", ` +
@@ -75,49 +77,57 @@ async function main() {
 		phases: {}, createdAt: Date.now(), updatedAt: Date.now(), cwd: process.cwd(),
 	};
 
-	console.log("== executing (real subagents: planner → [scout → map(analyst) → doc-writer]) ==");
-	const t0 = Date.now();
-	const res = await executeTaskflow(state, {
-		cwd: process.cwd(), agents, globalThinking: settings.globalThinking,
-		persist: (s) => { saveRun(s); },
-		onProgress: (s) => {
-			const done = Object.values(s.phases).filter((p) => p.status === "done").length;
-			const lead = s.phases.lead;
-			const sub = lead?.subProgress ? ` | subflow: ${lead.subProgress.done}/${lead.subProgress.total}` : "";
-			process.stdout.write(`\r  ${done}/${s.def.phases.length} done${sub}                    `);
-		},
-	});
-	console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
+	const restorePiBin = installNoExtPiWrapper("pi-taskflow-e2e-subflow-complex");
+	try {
+		console.log("== executing (real subagents: planner → [scout → map(analyst) → doc-writer]) ==");
+		const t0 = Date.now();
+		const res = await executeTaskflow(state, {
+			cwd: process.cwd(),
+			agents,
+			globalThinking: settings.globalThinking,
+			runTask: piSubagentRunner.runTask,
+			persist: (s) => { saveRun(s); },
+			onProgress: (s) => {
+				const done = Object.values(s.phases).filter((p) => p.status === "done").length;
+				const lead = s.phases.lead;
+				const sub = lead?.subProgress ? ` | subflow: ${lead.subProgress.done}/${lead.subProgress.total}` : "";
+				process.stdout.write(`\r  ${done}/${s.def.phases.length} done${sub}                    `);
+			},
+		});
+		console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
 
-	const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
-	let tree: { nodes?: Array<{ nodeId: string; phaseId: string; parentNodeId?: string }> } = {};
-	try { tree = JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")); } catch { /* none */ }
-	const spawnedChildren = (tree.nodes ?? []).filter((n) => n.parentNodeId === "lead");
-	const out = res.finalOutput ?? "";
+		const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
+		let tree: { nodes?: Array<{ nodeId: string; phaseId: string; parentNodeId?: string }> } = {};
+		try { tree = JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")); } catch { /* none */ }
+		const spawnedChildren = (tree.nodes ?? []).filter((n) => n.parentNodeId === "lead");
+		const out = res.finalOutput ?? "";
 
-	console.log("\ntree nodes:", (tree.nodes ?? []).map((n) => `${n.nodeId}<-${n.parentNodeId ?? "-"}`).join(", "));
-	console.log("\n── folded report (tail 900) ──\n" + out.slice(-900));
+		console.log("\ntree nodes:", (tree.nodes ?? []).map((n) => `${n.nodeId}<-${n.parentNodeId ?? "-"}`).join(", "));
+		console.log("\n── folded report (tail 900) ──\n" + out.slice(-900));
 
-	// Evidence the nested DAG actually ran all three stages: the synthesized
-	// report should mention prioritization (P0/P1/P2) produced by the reduce,
-	// which only exists if inventory→map→reduce all completed.
-	const hasPrioritized = /P0|P1|P2/.test(out);
-	const noShapeError = !/failed validation|failed verification|not a Taskflow|zero phases|no-op/.test(out);
+		// Evidence the nested DAG actually ran all three stages: the synthesized
+		// report should mention prioritization (P0/P1/P2) produced by the reduce,
+		// which only exists if inventory→map→reduce all completed.
+		const hasPrioritized = /P0|P1|P2/.test(out);
+		const noShapeError = !/failed validation|failed verification|not a Taskflow|zero phases|no-op/.test(out);
 
-	const checks: Array<[string, boolean]> = [
-		["overall ok", res.ok],
-		["lead spawned a subflow child", spawnedChildren.length >= 1],
-		["spawn fold marker present", /ctx_spawn: \d+ child report/.test(out)],
-		["subflow validated & ran (no shape/validation error)", noShapeError],
-		["nested DAG completed end-to-end (prioritized report produced)", hasPrioritized],
-		["report is substantial (>300 chars)", out.length > 300],
-		["stayed within budget", res.state.status !== "blocked"],
-	];
-	console.log("\n== assertions ==");
-	let allPass = true;
-	for (const [n, okk] of checks) { console.log(`  ${okk ? "PASS" : "FAIL"}  ${n}`); if (!okk) allPass = false; }
-	console.log(allPass ? "\n✅ COMPLEX-SUBFLOW E2E PASSED" : "\n❌ COMPLEX-SUBFLOW E2E FAILED");
-	process.exit(allPass ? 0 : 1);
+		const checks: Array<[string, boolean]> = [
+			["overall ok", res.ok],
+			["lead spawned a subflow child", spawnedChildren.length >= 1],
+			["spawn fold marker present", /ctx_spawn: \d+ child report/.test(out)],
+			["subflow validated & ran (no shape/validation error)", noShapeError],
+			["nested DAG completed end-to-end (prioritized report produced)", hasPrioritized],
+			["report is substantial (>300 chars)", out.length > 300],
+			["stayed within budget", res.state.status !== "blocked"],
+		];
+		console.log("\n== assertions ==");
+		let allPass = true;
+		for (const [n, okk] of checks) { console.log(`  ${okk ? "PASS" : "FAIL"}  ${n}`); if (!okk) allPass = false; }
+		console.log(allPass ? "\n✅ COMPLEX-SUBFLOW E2E PASSED" : "\n❌ COMPLEX-SUBFLOW E2E FAILED");
+		process.exit(allPass ? 0 : 1);
+	} finally {
+		restorePiBin();
+	}
 }
 
 main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/packages/pi-taskflow/test/e2e-team.mts b/packages/pi-taskflow/test/e2e-team.mts
index ae7a2c7..720d26e 100644
--- a/packages/pi-taskflow/test/e2e-team.mts
+++ b/packages/pi-taskflow/test/e2e-team.mts
@@ -3,7 +3,8 @@
  *
  * A multi-role engineering team collaborates on ONE real deliverable: a
  * prioritized improvement plan for this repo's IPC subsystem
- * (extensions/context-store.ts + extensions/runner.ts). It exercises the whole
+ * (`packages/taskflow-core/src/context-store.ts` +
+ * `packages/pi-taskflow/src/runner.ts`). It exercises the whole
  * collaboration surface against real `pi` subagents + a real model:
  *
  *   1. scout  — surveys the subsystem ONCE, ctx_write's a shared "map"
@@ -33,11 +34,13 @@ import * as path from "node:path";
 import { discoverAgents, readSubagentSettings } from "taskflow-core";
 import { executeTaskflow } from "taskflow-core";
 import { validateTaskflow, type Taskflow } from "taskflow-core";
-import { runsDir } from "taskflow-core";
+import { runsDir, saveRun } from "taskflow-core";
 import type { RunState } from "taskflow-core";
+import { installNoExtPiWrapper } from "./e2e-helpers.mts";
+import { piSubagentRunner } from "../src/runner.ts";
 
 const MARKER = `INV-${crypto.randomBytes(4).toString("hex").toUpperCase()}`;
-const TARGET = "extensions/context-store.ts + extensions/runner.ts";
+const TARGET = "packages/taskflow-core/src/context-store.ts + packages/pi-taskflow/src/runner.ts";
 
 const FLOW: Taskflow = {
 	name: "e2e-team",
@@ -184,91 +187,97 @@ async function main() {
 		cwd: process.cwd(),
 	};
 
-	console.log("== executing (real team of subagents) ==");
-	const t0 = Date.now();
-	const res = await executeTaskflow(state, {
-		cwd: process.cwd(),
-		agents,
-		globalThinking: settings.globalThinking,
-		persist: (s) => {
-			import("../extensions/store.ts").then((m) => m.saveRun(s)).catch(() => {});
-		},
-		onProgress: (s) => {
-			const done = Object.values(s.phases).filter((p) => p.status === "done").length;
-			const running = Object.values(s.phases)
-				.filter((p) => p.status === "running")
-				.map((p) => p.id);
-			process.stdout.write(
-				`\r  ${done}/${s.def.phases.length} done` +
-					(running.length ? ` | running: ${running.join(",")}` : "") +
-					"                    ",
-			);
-		},
-	});
-	console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
+	const restorePiBin = installNoExtPiWrapper("pi-taskflow-e2e-team");
+	try {
+		console.log("== executing (real team of subagents) ==");
+		const t0 = Date.now();
+		const res = await executeTaskflow(state, {
+			cwd: process.cwd(),
+			agents,
+			globalThinking: settings.globalThinking,
+			runTask: piSubagentRunner.runTask,
+			persist: (s) => {
+				try { saveRun(s); } catch { /* best-effort; the E2E result is authoritative */ }
+			},
+			onProgress: (s) => {
+				const done = Object.values(s.phases).filter((p) => p.status === "done").length;
+				const running = Object.values(s.phases)
+					.filter((p) => p.status === "running")
+					.map((p) => p.id);
+				process.stdout.write(
+					`\r  ${done}/${s.def.phases.length} done` +
+						(running.length ? ` | running: ${running.join(",")}` : "") +
+						"                    ",
+				);
+			},
+		});
+		console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
 
-	console.log("\nPhase states:");
-	for (const p of FLOW.phases) {
-		const ps = res.state.phases[p.id];
-		console.log(`  ${ps?.status === "done" ? "✓" : "✗"} ${p.id} [${p.type}]`);
-	}
+		console.log("\nPhase states:");
+		for (const p of FLOW.phases) {
+			const ps = res.state.phases[p.id];
+			console.log(`  ${ps?.status === "done" ? "✓" : "✗"} ${p.id} [${p.type}]`);
+		}
 
-	// ── Ground-truth inspection of the blackboard ──
-	const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
-	const findingsDir = path.join(ctxDir, "findings");
-	let findingFiles: string[] = [];
-	try {
-		findingFiles = fs.readdirSync(findingsDir).filter((f) => f.endsWith(".json") && !f.includes(".lock"));
-	} catch { /* none */ }
-	const blackboardKeys = new Set();
-	let mapHasMarker = false;
-	for (const f of findingFiles) {
+		// ── Ground-truth inspection of the blackboard ──
+		const ctxDir = path.join(runsDir(process.cwd()), "ctx", runId);
+		const findingsDir = path.join(ctxDir, "findings");
+		let findingFiles: string[] = [];
 		try {
-			const obj = JSON.parse(fs.readFileSync(path.join(findingsDir, f), "utf-8"));
-			for (const k of Object.keys(obj)) blackboardKeys.add(k);
-			if (typeof obj.map === "string" && obj.map.includes(MARKER)) mapHasMarker = true;
-		} catch { /* skip */ }
-	}
-	let tree: { nodes?: Array<{ nodeId: string; phaseId: string; parentNodeId?: string }> } = {};
-	try { tree = JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")); } catch { /* none */ }
-	const spawnedChildren = (tree.nodes ?? []).filter((n) => n.parentNodeId === "lead");
+			findingFiles = fs.readdirSync(findingsDir).filter((f) => f.endsWith(".json") && !f.includes(".lock"));
+		} catch { /* none */ }
+		const blackboardKeys = new Set();
+		let mapHasMarker = false;
+		for (const f of findingFiles) {
+			try {
+				const obj = JSON.parse(fs.readFileSync(path.join(findingsDir, f), "utf-8"));
+				for (const k of Object.keys(obj)) blackboardKeys.add(k);
+				if (typeof obj.map === "string" && obj.map.includes(MARKER)) mapHasMarker = true;
+			} catch { /* skip */ }
+		}
+		let tree: { nodes?: Array<{ nodeId: string; phaseId: string; parentNodeId?: string }> } = {};
+		try { tree = JSON.parse(fs.readFileSync(path.join(ctxDir, "tree.json"), "utf-8")); } catch { /* none */ }
+		const spawnedChildren = (tree.nodes ?? []).filter((n) => n.parentNodeId === "lead");
 
-	console.log("\nBlackboard ground truth:");
-	console.log(`  keys written: ${[...blackboardKeys].join(", ") || "(none)"}`);
-	console.log(`  scout map carries marker ${MARKER}: ${mapHasMarker}`);
-	console.log(`  lead spawned children: ${spawnedChildren.length}`);
+		console.log("\nBlackboard ground truth:");
+		console.log(`  keys written: ${[...blackboardKeys].join(", ") || "(none)"}`);
+		console.log(`  scout map carries marker ${MARKER}: ${mapHasMarker}`);
+		console.log(`  lead spawned children: ${spawnedChildren.length}`);
 
-	// Marker reuse by the experts (only obtainable from the shared map).
-	const expertOut = [
-		res.state.phases["expert-correctness"]?.output ?? "",
-		res.state.phases["expert-architecture"]?.output ?? "",
-		res.state.phases["expert-risk"]?.output ?? "",
-	];
-	const expertsReusedMap = expertOut.filter((o) => o.includes(MARKER)).length;
-	console.log(`  experts that echoed the shared marker: ${expertsReusedMap}/3`);
+		// Marker reuse by the experts (only obtainable from the shared map).
+		const expertOut = [
+			res.state.phases["expert-correctness"]?.output ?? "",
+			res.state.phases["expert-architecture"]?.output ?? "",
+			res.state.phases["expert-risk"]?.output ?? "",
+		];
+		const expertsReusedMap = expertOut.filter((o) => o.includes(MARKER)).length;
+		console.log(`  experts that echoed the shared marker: ${expertsReusedMap}/3`);
 
-	const leadOut = res.state.phases.lead?.output ?? "";
-	const plan = res.finalOutput ?? "";
+		const leadOut = res.state.phases.lead?.output ?? "";
+		const plan = res.finalOutput ?? "";
 
-	const checks: Array<[string, boolean]> = [
-		["overall ok", res.ok],
-		["scout published the shared map (marker on blackboard)", mapHasMarker],
-		["≥2 experts reused the shared map (collaboration, not re-deriving)", expertsReusedMap >= 2],
-		["≥2 of 3 experts wrote findings back to the blackboard", ["find.correctness", "find.architecture", "find.risk"].filter((k) => blackboardKeys.has(k)).length >= 2],
-		["lead dynamically spawned a deep-dive (recursive supervision)", spawnedChildren.length >= 1 || /spawned child/i.test(leadOut)],
-		["gate ran", res.state.phases.gate?.status === "done"],
-		["final plan has prioritized sections", /P0/.test(plan) && /P1/.test(plan)],
-		["plan is substantial (>400 chars)", plan.length > 400],
-	];
-	console.log("\n== assertions ==");
-	let allPass = true;
-	for (const [name, ok] of checks) {
-		console.log(`  ${ok ? "PASS" : "FAIL"}  ${name}`);
-		if (!ok) allPass = false;
+		const checks: Array<[string, boolean]> = [
+			["overall ok", res.ok],
+			["scout published the shared map (marker on blackboard)", mapHasMarker],
+			["≥2 experts reused the shared map (collaboration, not re-deriving)", expertsReusedMap >= 2],
+			["≥2 of 3 experts wrote findings back to the blackboard", ["find.correctness", "find.architecture", "find.risk"].filter((k) => blackboardKeys.has(k)).length >= 2],
+			["lead dynamically spawned a deep-dive (recursive supervision)", spawnedChildren.length >= 1 || /spawned child/i.test(leadOut)],
+			["gate ran", res.state.phases.gate?.status === "done"],
+			["final plan has prioritized sections", /P0/.test(plan) && /P1/.test(plan)],
+			["plan is substantial (>400 chars)", plan.length > 400],
+		];
+		console.log("\n== assertions ==");
+		let allPass = true;
+		for (const [name, ok] of checks) {
+			console.log(`  ${ok ? "PASS" : "FAIL"}  ${name}`);
+			if (!ok) allPass = false;
+		}
+		console.log("\n── Final prioritized plan ──\n" + plan.slice(0, 1200));
+		console.log(allPass ? "\n✅ TEAM E2E PASSED" : "\n❌ TEAM E2E FAILED");
+		process.exit(allPass ? 0 : 1);
+	} finally {
+		restorePiBin();
 	}
-	console.log("\n── Final prioritized plan ──\n" + plan.slice(0, 1200));
-	console.log(allPass ? "\n✅ TEAM E2E PASSED" : "\n❌ TEAM E2E FAILED");
-	process.exit(allPass ? 0 : 1);
 }
 
 main().catch((e) => {
diff --git a/packages/pi-taskflow/test/e2e.mts b/packages/pi-taskflow/test/e2e.mts
index a9cf17b..37d9f09 100644
--- a/packages/pi-taskflow/test/e2e.mts
+++ b/packages/pi-taskflow/test/e2e.mts
@@ -9,6 +9,8 @@ import { discoverAgents, readSubagentSettings } from "taskflow-core";
 import { executeTaskflow } from "taskflow-core";
 import { validateTaskflow, type Taskflow } from "taskflow-core";
 import type { RunState } from "taskflow-core";
+import { installNoExtPiWrapper } from "./e2e-helpers.mts";
+import { piSubagentRunner } from "../src/runner.ts";
 
 const FLOW: Taskflow = {
 	name: "e2e-smoke",
@@ -52,7 +54,7 @@ async function main() {
 	console.log("valid ✓");
 
 	const settings = readSubagentSettings();
-	const { agents } = discoverAgents(process.cwd(), "user", settings.agentOverrides, settings.modelRoles, settings.taskflow);
+	const { agents } = discoverAgents(process.cwd(), "user", settings.modelRoles, settings.taskflow);
 	console.log(`discovered ${agents.length} agents; has scout: ${agents.some((a) => a.name === "scout")}`);
 
 	const state: RunState = {
@@ -67,47 +69,53 @@ async function main() {
 		cwd: process.cwd(),
 	};
 
-	console.log("== executing (real subagents) ==");
-	const t0 = Date.now();
-	const res = await executeTaskflow(state, {
-		cwd: process.cwd(),
-		agents,
-		globalThinking: settings.globalThinking,
-		onProgress: (s) => {
-			const done = Object.values(s.phases).filter((p) => p.status === "done").length;
-			const running = Object.values(s.phases)
-				.filter((p) => p.status === "running")
-				.map((p) => p.id);
-			process.stdout.write(`\r  progress: ${done}/${s.def.phases.length} done` + (running.length ? ` | running: ${running.join(",")}` : "") + "        ");
-		},
-	});
-	console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
+	const restorePiBin = installNoExtPiWrapper("pi-taskflow-e2e-smoke");
+	try {
+		console.log("== executing (real subagents) ==");
+		const t0 = Date.now();
+		const res = await executeTaskflow(state, {
+			cwd: process.cwd(),
+			agents,
+			globalThinking: settings.globalThinking,
+			runTask: piSubagentRunner.runTask,
+			onProgress: (s) => {
+				const done = Object.values(s.phases).filter((p) => p.status === "done").length;
+				const running = Object.values(s.phases)
+					.filter((p) => p.status === "running")
+					.map((p) => p.id);
+				process.stdout.write(`\r  progress: ${done}/${s.def.phases.length} done` + (running.length ? ` | running: ${running.join(",")}` : "") + "        ");
+			},
+		});
+		console.log(`\n== done in ${((Date.now() - t0) / 1000).toFixed(1)}s ==`);
 
-	console.log("\nPhase states:");
-	for (const p of FLOW.phases) {
-		const ps = res.state.phases[p.id];
-		console.log(`  ${ps?.status === "done" ? "✓" : "✗"} ${p.id} [${p.type}] -> ${JSON.stringify(ps?.output?.slice(0, 80))}`);
-	}
+		console.log("\nPhase states:");
+		for (const p of FLOW.phases) {
+			const ps = res.state.phases[p.id];
+			console.log(`  ${ps?.status === "done" ? "✓" : "✗"} ${p.id} [${p.type}] -> ${JSON.stringify(ps?.output?.slice(0, 80))}`);
+		}
 
-	console.log("\nFinal output:\n  ", res.finalOutput);
+		console.log("\nFinal output:\n  ", res.finalOutput);
 
-	// Assertions
-	const checks: Array<[string, boolean]> = [
-		["overall ok", res.ok],
-		["list phase done", res.state.phases.list?.status === "done"],
-		["map produced 3 sub-results", (res.state.phases.shout?.output?.match(/\[\d+\/3\]/g) ?? []).length === 3],
-		["final mentions ALPHA", /ALPHA/i.test(res.finalOutput)],
-		["final mentions BETA", /BETA/i.test(res.finalOutput)],
-		["final mentions GAMMA", /GAMMA/i.test(res.finalOutput)],
-	];
-	console.log("\n== assertions ==");
-	let allPass = true;
-	for (const [name, ok] of checks) {
-		console.log(`  ${ok ? "PASS" : "FAIL"}  ${name}`);
-		if (!ok) allPass = false;
+		// Assertions
+		const checks: Array<[string, boolean]> = [
+			["overall ok", res.ok],
+			["list phase done", res.state.phases.list?.status === "done"],
+			["map produced 3 sub-results", (res.state.phases.shout?.output?.match(/\[\d+\/3\]/g) ?? []).length === 3],
+			["final mentions ALPHA", /ALPHA/i.test(res.finalOutput)],
+			["final mentions BETA", /BETA/i.test(res.finalOutput)],
+			["final mentions GAMMA", /GAMMA/i.test(res.finalOutput)],
+		];
+		console.log("\n== assertions ==");
+		let allPass = true;
+		for (const [name, ok] of checks) {
+			console.log(`  ${ok ? "PASS" : "FAIL"}  ${name}`);
+			if (!ok) allPass = false;
+		}
+		console.log(allPass ? "\n✅ E2E PASSED" : "\n❌ E2E FAILED");
+		process.exit(allPass ? 0 : 1);
+	} finally {
+		restorePiBin();
 	}
-	console.log(allPass ? "\n✅ E2E PASSED" : "\n❌ E2E FAILED");
-	process.exit(allPass ? 0 : 1);
 }
 
 main().catch((e) => {
diff --git a/packages/pi-taskflow/test/runner-injection.test.ts b/packages/pi-taskflow/test/runner-injection.test.ts
index 4344745..be359c5 100644
--- a/packages/pi-taskflow/test/runner-injection.test.ts
+++ b/packages/pi-taskflow/test/runner-injection.test.ts
@@ -14,10 +14,11 @@
  * runTask, so the class of bug cannot recur silently.
  */
 import assert from "node:assert/strict";
-import { readFileSync } from "node:fs";
+import { readFileSync, readdirSync } from "node:fs";
 import { test } from "node:test";
 
 const SRC = readFileSync(new URL("../src/index.ts", import.meta.url), "utf-8");
+const TEST_DIR = new URL("./", import.meta.url);
 
 /**
  * Verify every `const deps: RuntimeDeps = { ... }` block in the source sets
@@ -61,6 +62,20 @@ test("regression: every executeTaskflow / recomputeTaskflow deps in pi index.ts
 	assertAllDepsInjectRunTask();
 });
 
+test("regression: every direct-execution .mts test injects runTask", () => {
+	const offenders: string[] = [];
+	for (const name of readdirSync(TEST_DIR).filter((entry) => entry.endsWith(".mts"))) {
+		const source = readFileSync(new URL(name, TEST_DIR), "utf-8");
+		if (!/\bexecuteTaskflow\s*\(/.test(source)) continue;
+		if (!/\brunTask\s*:/.test(source)) offenders.push(name);
+	}
+	assert.deepEqual(
+		offenders,
+		[],
+		`direct-execution .mts tests must inject a real or mock host runner: ${offenders.join(", ")}`,
+	);
+});
+
 test("regression: the detached context file carries a runnerModule (runner injection for the child process)", () => {
 	// The detached path injects the runner indirectly: the host serializes a
 	// runnerModule into the context file, which the child dynamically imports.
diff --git a/packages/pi-taskflow/test/skills-build.test.ts b/packages/pi-taskflow/test/skills-build.test.ts
index 4b22f05..3d80402 100644
--- a/packages/pi-taskflow/test/skills-build.test.ts
+++ b/packages/pi-taskflow/test/skills-build.test.ts
@@ -1,9 +1,9 @@
-// Guard: the generated skill files (pi + codex) must match what
+// Guard: the generated skill files must match what
 // scripts/build-skills.mjs produces from skills-src/taskflow/.
 //
 // The skills are authored ONCE in skills-src/ (single source of truth) and
 // compiled per host. Editing a generated file directly, or editing the source
-// without rebuilding, silently forks the two hosts' documentation — this test
+// without rebuilding, silently forks the hosts' documentation — this test
 // makes that a CI failure. Fix with: node scripts/build-skills.mjs
 
 import { test } from "node:test";
@@ -42,30 +42,44 @@ test("skills: host-conditional filtering removed the other host's content", asyn
 		path.join(root, "packages", "opencode-taskflow", "plugin", "skills", "taskflow", "SKILL.md"),
 		"utf8",
 	);
+	const gkSkill = readFileSync(
+		path.join(root, "packages", "grok-taskflow", "plugin", "skills", "taskflow", "SKILL.md"),
+		"utf8",
+	);
 	// No leftover markers in any output.
-	for (const [name, text] of [["pi", piSkill], ["codex", cxSkill], ["claude", clSkill], ["opencode", ocSkill]] as const) {
+	for (const [name, text] of [
+		["pi", piSkill],
+		["codex", cxSkill],
+		["claude", clSkill],
+		["opencode", ocSkill],
+		["grok", gkSkill],
+	] as const) {
 		assert.ok(!/
-
+
 # 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).
 
 ---
 
 
 ## Shared Context Tree (blackboard + supervision) — opt-in
 
+> **0.2.0 host scope:** context-tool injection is implemented by `pi-taskflow`.
+> Codex, Claude, OpenCode, and Grok runners do not expose `ctx_*` tools yet.
+
 By default subagents are fully isolated: they share nothing and only return a
 final output string. Opt a phase in with `shareContext: true` (or
 `contextSharing: true` at the flow level for every phase) to give its subagent
@@ -248,7 +266,89 @@ changed. Instead of re-running all 12 phases:
 
 The other 8 phases (dependency summary, license scan, …) are served from the
 stored run at $0.
+
+
+---
+
+## 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 { action: "trace", runId: "" }
+taskflow { action: "trace", runId: "", json: true }   // full machine record
+/tf trace  [--json]
+```
+
+
+```
+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 { action: "replay", runId: "", thresholds: { review: 0.9 } }
+taskflow { action: "replay", runId: "", budgetMaxUSD: 0.05, json: true }
+/tf replay  --threshold review=0.9 --budget-usd 0.05 [--json]
+```
+
+
+```
+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` |
+
+
 ---
 
 ## `init` — model roles setup
diff --git a/skills-src/taskflow/configuration.md b/skills-src/taskflow/configuration.md
index 39e8c92..30c5f84 100644
--- a/skills-src/taskflow/configuration.md
+++ b/skills-src/taskflow/configuration.md
@@ -43,7 +43,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. |
 
 ---
 
@@ -54,14 +54,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
@@ -75,13 +77,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. |
@@ -97,12 +103,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`.
 
 ---
 
@@ -176,9 +182,9 @@ still reach `{args.X}`).
 
 Via the tool: `{ "action": "run", "name": "audit-endpoints", "args": { "dir": "packages/api" } }`.
 
-
+
 Via the MCP tool: `taskflow_run` with `{ "name": "audit-endpoints", "args": { "dir": "packages/api" } }`.
-
+
 
 ---
 
@@ -217,10 +223,13 @@ 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 process:
   `pi --mode json -p --no-session [--model …] [--thinking …] [--tools …] [--append-system-prompt ] "Task: …"`.
@@ -229,24 +238,76 @@ Notes:
 - Each phase runs as an isolated `codex exec --json` session. A model id that
   still looks like a pi-provider path (contains `/`) or an unresolved
   `{{placeholder}}` is dropped so codex falls back to its configured default.
+  Codex does **not** offer a strict per-tool name whitelist: read-only tool sets
+  map to `-s read-only`; any mutating tool or no list maps to
+  `-s workspace-write` (never `danger-full-access`). Effective thinking maps
+  to `model_reasoning_effort`: `off`/`none`/`minimal` → `none`,
+  `low`/`medium`/`high`/`xhigh` pass through, and `max`/`ultra` → `xhigh`.
+  Any other value fails closed before Codex is spawned. Codex usage accounting
+  is tokens-only: budgeted flows may use `maxTokens`, while `maxUSD` is rejected.
+  Children use `--ephemeral --ignore-user-config --ignore-rules` and an empty
+  `mcp_servers` override, so parent plugins/MCP/rules cannot alter the run.
+  Only platform/proxy/CA and Codex/OpenAI provider environment variables are
+  inherited; unrelated secrets are removed.
 
 
 - Each phase runs as an isolated `claude -p --output-format stream-json`
-  session. A model id that still looks like a pi-provider path (contains `/`)
+  session (Claude Code 2.1.169 or newer is required for `--safe-mode`). A model
+  id that still looks like a pi-provider path (contains `/`)
   or an unresolved `{{placeholder}}` is dropped so claude falls back to its
-  configured default. Read-only phases get a `--allowedTools` whitelist;
-  mutating phases run under `--permission-mode bypassPermissions` (no OS
-  sandbox — see the README security note).
+  configured default. Known read-only requests — including an omitted tool
+  list — get matching `--tools` and `--allowedTools` lists, and an explicit
+  request stays narrow. `--safe-mode` disables non-managed project/user
+  customizations; disk setting sources and non-managed hooks are disabled as
+  defense in depth. Administrator-managed policy hooks may still run. Known
+  mutating tools are rejected by default because headless Claude has no OS
+  sandbox; trusted operators must explicitly set
+  `PI_TASKFLOW_CLAUDE_UNSAFE_BYPASS=1` to allow `bypassPermissions` while
+  keeping the requested built-in set narrow. Unknown tools always fail closed.
+  Prefer an isolated `cwd: "worktree"` even after opting
+  in. The Claude child inherits only platform/runtime, proxy/CA, and supported
+  Claude-provider environment variables; unrelated application secrets are
+  removed from the child environment.
 
 
 - 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.
 
+
+- 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.
 
 ---
@@ -339,7 +400,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.
@@ -386,9 +447,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. |
 
 ---
 
@@ -440,10 +507,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/skills-src/taskflow/core.md b/skills-src/taskflow/core.md
index 8a02e3a..e0bff38 100644
--- a/skills-src/taskflow/core.md
+++ b/skills-src/taskflow/core.md
@@ -13,10 +13,10 @@ mistakes that break flows. Load the companion files **only when needed**:
 
 | `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`). |
 
-
+
 | `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?
@@ -51,8 +51,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
@@ -94,9 +94,9 @@ proper flow, so you still get progress, persistence, and resume.
 
 - You can pass these as top-level tool params **or** inside `define`.
 
-
+
 - Pass these as the `define` argument to `taskflow_run`.
-
+
 
 ## How to author a taskflow
 
@@ -106,11 +106,11 @@ Call the `taskflow` tool. To run a brand-new flow you write inline, pass
 **Before running a non-trivial flow, `action: "verify"` it — zero tokens,
 catches cycles / missing deps / undefined refs / contract typos.**
 
-
+
 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)
 
@@ -126,7 +126,7 @@ For a non-trivial flow you'll iterate on, **write the definition to a file**
 { "action": "run",    "defineFile": "/tmp/audit.json", "args": { … } }
 ```
 
-
+
 ```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:
@@ -134,7 +134,7 @@ For a non-trivial flow you'll iterate on, **write the definition to a file**
 { "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
@@ -172,12 +172,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 |
@@ -186,6 +186,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)
 
@@ -194,7 +196,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`. |
@@ -349,12 +351,12 @@ The (interpolated) `task` is the prompt shown.
 Place one before the expensive part of a flow (a big fan-out, a mutation) —
 see the plan→approve→execute archetype in `patterns.md`.
 
-
+
 > **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
 
@@ -477,11 +479,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": [ ... ] }
@@ -490,6 +549,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
@@ -577,7 +640,7 @@ An unknown agent name fails the phase with the list of available agents.
 
 Check with `action: "agents"` instead of guessing.
 
-
+
 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`,
@@ -585,10 +648,10 @@ Built-in agents: `executor`, `executor-code` (complex, multi-file),
 `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.
-
+
 
 
-## Actions (all 15)
+## Actions (all 16)
 
 | action | what it does |
 |--------|--------------|
@@ -601,7 +664,8 @@ use the default. Use cheap agents (`scout`) for discovery and strong agents
 | `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. |
@@ -653,20 +717,25 @@ 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  [--json]` · `/tf replay  [--threshold phase=n] [--budget-usd n] [--json]`
+- `/tf why-stale  [phaseId]` · `/tf recompute   [--apply]` (dry-run by default)
 - `/tf init` — interactive model-roles setup
 - `/tf: [args]` — shortcut for each saved flow
 
-
+
 `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/skills-src/taskflow/entry.claude.md b/skills-src/taskflow/entry.claude.md
index 68802bc..cc06074 100644
--- a/skills-src/taskflow/entry.claude.md
+++ b/skills-src/taskflow/entry.claude.md
@@ -16,8 +16,18 @@ runs as an isolated `claude -p` session.
 | `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.
+
+**Security default:** Claude mutating/unrestricted phases are rejected because
+headless Claude has no OS sandbox. Explicitly opt in only for trusted flows by
+setting `PI_TASKFLOW_CLAUDE_UNSAFE_BYPASS=1`; prefer `cwd: "worktree"`.
diff --git a/skills-src/taskflow/entry.codex.md b/skills-src/taskflow/entry.codex.md
index 1f7003a..b6eaac7 100644
--- a/skills-src/taskflow/entry.codex.md
+++ b/skills-src/taskflow/entry.codex.md
@@ -15,8 +15,14 @@ the Codex form (`taskflow_verify`).
 | `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 (inline SVG) + 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.
diff --git a/skills-src/taskflow/entry.grok.md b/skills-src/taskflow/entry.grok.md
new file mode 100644
index 0000000..d80717a
--- /dev/null
+++ b/skills-src/taskflow/entry.grok.md
@@ -0,0 +1,38 @@
+---
+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.
diff --git a/skills-src/taskflow/entry.opencode.md b/skills-src/taskflow/entry.opencode.md
index c4a53b7..a7e9a04 100644
--- a/skills-src/taskflow/entry.opencode.md
+++ b/skills-src/taskflow/entry.opencode.md
@@ -16,8 +16,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.
diff --git a/skills-src/taskflow/library.md b/skills-src/taskflow/library.md
index 41e07b5..cfaa7f5 100644
--- a/skills-src/taskflow/library.md
+++ b/skills-src/taskflow/library.md
@@ -11,10 +11,10 @@ save (with a good `purpose` + `tags`), the better future search gets.
 **Host binding (pi):** use the `taskflow` tool with `action: "search"` /
 `action: "save"`, and `/tf list` / `/tf show `.
 
-
+
 **Host binding:** use the `taskflow_search`, `taskflow_save`, `taskflow_list`,
 `taskflow_show` tools.
-
+
 
 ## Before authoring a non-trivial flow: SEARCH first
 
@@ -27,11 +27,11 @@ can adapt.
 { "action": "search", "query": "audit API endpoints for missing auth", "limit": 5 }
 ```
 
-
+
 ```jsonc
 { "name": "taskflow_search", "arguments": { "query": "audit API endpoints for missing auth", "limit": 5 } }
 ```
-
+
 
 Read the results and the `→ reuseHint`:
 
@@ -60,14 +60,14 @@ without them is nearly invisible to future search.
   "tags": ["audit", "security", "auth", "fan-out"] }
 ```
 
-
+
 ```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
@@ -102,12 +102,12 @@ measures "found-via-search reuse", the high-quality signal for later auto-prune)
 { "action": "run", "name": "audit-endpoints", "args": { "dir": "src/api" }, "reusedFromSearch": true }
 ```
 
-
+
 ```jsonc
 { "name": "taskflow_run",
   "arguments": { "name": "audit-endpoints", "args": { "dir": "src/api" }, "reusedFromSearch": true } }
 ```
-
+
 
 ## Judicious reuse — not every task needs the library
 
diff --git a/skills-src/taskflow/patterns.md b/skills-src/taskflow/patterns.md
index a421ffd..4bd651e 100644
--- a/skills-src/taskflow/patterns.md
+++ b/skills-src/taskflow/patterns.md
@@ -77,7 +77,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
@@ -145,12 +145,12 @@ Prefer a `script` phase whenever the check is a command — exact, free, fast.
 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
 {
@@ -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/website/.plans/docs-enhancement-master-plan.md b/website/.plans/docs-enhancement-master-plan.md
index 3b50c68..f511341 100644
--- a/website/.plans/docs-enhancement-master-plan.md
+++ b/website/.plans/docs-enhancement-master-plan.md
@@ -61,7 +61,7 @@
 
 更新 `app/[lang]/page.tsx`:
 
-- 新增 **By the numbers** 数据区(4 hosts / 10 phase types / 0 deps / resume)
+- 新增 **By the numbers** 数据区(4 hosts / 12 phase types / 0 deps / resume)
 - 新增 **Testimonials** 社会证明区(3 条角色化引用)
 - 新增 **Comparison** 对比区(命令式 vs taskflow,精简版表格)
 - 新增/改进底部 CTAs:Read docs / Browse templates / Join community
diff --git a/website/app/[lang]/docs/[[...slug]]/page.tsx b/website/app/[lang]/docs/[[...slug]]/page.tsx
index d26c0c2..6086ac2 100644
--- a/website/app/[lang]/docs/[[...slug]]/page.tsx
+++ b/website/app/[lang]/docs/[[...slug]]/page.tsx
@@ -6,7 +6,7 @@ import {
 	PageLastUpdate,
 } from "fumadocs-ui/layouts/docs/page";
 import { MessageSquare, Pencil } from "lucide-react";
-import { notFound } from "next/navigation";
+import { notFound, redirect } from "next/navigation";
 import { getMDXComponents } from "@/components/mdx";
 import type { Locale } from "@/lib/i18n";
 import { source } from "@/lib/source";
@@ -100,6 +100,22 @@ export default async function Page({
 	const { lang, slug } = await params;
 	const page = source.getPage(slug, lang);
 	if (!page) notFound();
+	if (page.data.redirect) {
+		const redirectUrl = `${SITE_URL}${page.data.redirect}`;
+		return (
+			<>
+				
+				
+

+ {lang === "zh-cn" ? "页面已移动:" : "This page has moved to "} + + {redirectUrl} + +

+
+ + ); + } const MDX = page.data.body; @@ -174,10 +190,14 @@ export default async function Page({ <>