@nicknisi/pi-workflows

extensionmaintained

Model-facing front door to the first-party workflow engine — run JS workflow scripts over the subagent runtime, replacing the third-party @quintinshaw/pi-dynamic-workflows extension

by — · v0.3.2 · published 5d ago

$ pi install npm:@nicknisi/pi-workflows
downloads/mo
638
stars
41
last push
1d ago
open issues
4

Signals

license: MITtestspi manifest: missinginstall size: —deps: 0peer deps: 0

Download trend

1.4K downloads · last 12 weeks (weekly)

README

@nicknisi/pi-workflows

The model-facing front door to the first-party workflow engine. One workflow tool runs JavaScript workflow scripts that orchestrate subagents over the in-process runtime, replacing the third-party @quintinshaw/pi-dynamic-workflows extension. A /wf command is the thin human-facing wrapper.

The platform story

Four pieces compose the workflow platform: @nicknisi/pi-shared's subagent runtime (hermetic in-process child sessions), @nicknisi/pi-codemode's VM approach (compile a model-written script in node:vm with injected bindings), @nicknisi/pi-shared's workflow.ts engine (declarative multi-stage DAGs with needs/foreach/gates/retries), and this tool as the model-facing front door that ties them together with the script contract the old third-party engine used. The third-party @quintinshaw/pi-dynamic-workflows engine is being evicted — its script contract lives on unchanged here, its built-in pattern library / model tiers / agent-type registry / trigger-word arming do not.

What it adds

  • workflow tool (model-facing) — actions: run (inline JS script OR name of a saved workflow file), list, status <runId>, stop <runId>, pause, resume.
  • /wf command (human-facing) — /wf list | /wf run <name> [argsJson] | /wf status <runId> | /wf stop <runId> | /wf pause | /wf resume.
  • Human gates inside scripts — checkpoint(label?) pauses the run on a confirm dialog (reject/dismiss stops the run); ask(question, options?) asks mid-run (select with options, yes/no without, undefined when dismissed).
  • Footer status — while a run is active the footer shows wf <name> [running|paused] <last phase>.
  • autoplan skill — teaches the model the "autoplan this" trigger: run the saved autoplan workflow with args derived from the conversation, then respect the run's decision outcome. Disable with "skills": ["-skills/autoplan"] on the package entry.

The script contract

A workflow script is a JavaScript statement body (no imports) with a leading export const meta = { name, description } declaration and a trailing return value. The body is wrapped in an async function so a top-level return compiles; export const meta = is rewritten so node:vm compiles it (a stranded export fails loudly) and meta.name/meta.description surface in the result.

Injected globals — the exact names the old third-party tool's scripts use, so existing scripts run unchanged:

export const meta = { name: 'research', description: 'parallel research fan-out' };

const questions = ['How does the auth refresh flow work?', 'Where are sessions persisted?'];
const results = await parallel(questions.map((q) => () => agent(q, { label: 'researcher' })));
return { answered: results.length, results };
GlobalBehavior
agent(prompt, opts)Spawns a hermetic in-process child via the subagent runtime (namespace workflows). Throws ${kind}: ${error} on failure — wrap with a safeAgent that returns { ok, value, error } so a failure inside parallel() reports which stage died instead of collapsing the wave to null. Returns res.data ?? res.text ?? null.
parallel(thunks)Promise.all over zero-arg thunks — pass () => agent(...), not agent(...).
pipeline(items, ...stages)Folds items through stages: each stage maps over the previous stage's outputs in parallel, producing the next array.
phase(name)Logging marker that also drives the footer status (wf <name> [state] <phase>). NOT a budget boundary.
log(...args)Captured into the result logs.
checkpoint(label?)Human gate: suspends the run on a confirm dialog; rejecting throws and stops the run. Without a UI host it is a logged no-op. Put it before destructive or expensive steps.
ask(question, options?)Human answer mid-run: a select when options are given, a yes/no confirm otherwise; undefined when dismissed. Without a UI host it throws — never invent an answer.
argsThe args JSON value passed to run.
budget{ total, spent, remaining } over the run's token usage. total defaults to Infinity; spent accumulates across agent() calls. Read-only.
cwdThe session working directory.

agent() opts: model ('provider/id'), tools (allowlist — default read-only ['read','grep','find','ls']; pass ['read','bash','edit','write'] for builders), label (child agent label), systemPrompt, schema (validated; parsed JSON lands in result.data), effort (thinking level), timeoutMs, maxTurns, worktree (run the child in an isolated git worktree; on settle the change set is captured to a .patch and agent() returns { value, patchPath, runId } instead of the bare value — opt-in, so non-worktree calls are unchanged), agentType (accepted but ignored — no agent-type registry; resolve systemPrompt in the script itself).

agent() awaits the run's pause gate before every spawn: pause lets the in-flight step finish, then holds the run before the next one; resume releases it. Pause/resume are session-scoped (/wf pause, /wf resume, or the tool actions) — they apply to every active run, in practice one. Stopping or timing out a run aborts the gate, so a parked run rejects instead of hanging.

The script executes in the host process with full Node access — process, require, and fs are all reachable, the same trust boundary as the bash tool. Keep the returned value small: summaries, counts, key findings — never raw file dumps.

Saved workflows

Plain files. The registry is ls — no database, no manifest, no config keys.

  • ~/.pi/agent/workflows/*.js — global.
  • .pi/workflows/*.js — project-local, trusted projects only (the same trust gate as codemode's /cx). Untrusted projects see only global workflows.

Names are bare file stems (research, not research.js, never a path — .. and / are rejected to prevent escaping the workflows dirs). Global shadows a same-named project workflow. Files are read on demand, so /reload needs no workflow-specific wiring.

Runs are visible

Every agent() call spawns through @nicknisi/pi-shared's subagent runtime with artifactsDir set to ~/.pi/agent/subagent-runs/, namespace workflows, and the owning Pi session recorded. Active children therefore appear in that session's default fleet view; settled and machine-wide records remain available through the fleet's explicit all scope. status <runId> and stop <runId> here read from / cancel via the same runtime's run records — no parallel store. stop cancels in-flight spawns through a live runId → AbortController registry (mirroring subagents' cascading-cancellation); a run belonging to a different host process is reported as not cancellable from here.

Migration from @quintinshaw/pi-dynamic-workflows

Old conceptNew home
Built-in named patterns (e.g. research, review)Example .js files you drop in ~/.pi/agent/workflows/. No built-in library — the registry is ls.
Model tiers (fast / balanced / deep)Explicit model: strings passed to agent(prompt, { model: 'anthropic/claude-haiku-4-5' }). No tier registry.
agentType → tool/systemPrompt resolutionResolve systemPrompt in the script itself: agent(prompt, { systemPrompt: 'You are a reviewer…', tools: ['read','grep','bash'] }). agentType is accepted but ignored (logged).
Trigger-word arming (the tool activates on keywords)The model calls the workflow tool when intent warrants — no arming, no keyword matching.
Phases with per-phase budgetsphase(name) is a logging marker only. Budget is a single run-level { total, spent, remaining }; per-stage budgets are the workflow.ts engine's tokenBudget (use runWorkflow from codemode for that).
The third-party engine's script globalsUnchanged: args, agent, parallel, pipeline, phase, log, budget, cwd. Existing scripts run as-is.

Recipes

The examples/ directory ships standalone, copy-and-adapt workflow scripts — the registry is ls, so these are code you read and copy, never APIs you import (no index re-exports them). Drop any of them into ~/.pi/agent/workflows/ and run via /wf run <name>.

  • lanes.js — N parallel agents editing FILE-DISJOINT lanes of one repo under a hard-rules preamble (each lane owns a fixed file set; no git, no installs; the parent integrates centrally). Use it when a task splits into independent edits that don't overlap on files. Adapt by setting VERIFY to your typecheck command and filling the LANES array with { name, files, brief } per lane.
  • gates.js — three judge/verify prompt builders returning prompt strings: adversarial refutation (defeats confirmation bias), deep-research coverage (defeats silent source omission), and a 3-way code-review verdict (defeats verdict collapse). Use it when you need a reliable gate inside your own workflow. Adapt by copying the builder whose failure mode you need and calling it from an agent() with a JSON schema. Prompt patterns distilled from @quintinshaw/pi-dynamic-workflows.
  • bake-off.js — race N models on the SAME task in isolated worktrees (worktree: true), then an advisory judge reads each contender's .patch and picks a winner. Use it on hard build tasks where a single GLM-5.2-class builder produces decent-but-flawed code; the 2x token cost buys a measurably better hit rate. Adapt by setting CONTENDERS to the models to race and passing task in args; the workflow returns the winner's patchPath to apply via /patches.
  • autoplan.js — 3 solution candidates in parallel + a Holy Grail pass, an advisor that ranks with a recommendation (curbing Grail ideas that need upstream changes), then the HUMAN decides via ask() — recommendation on top, reject-all always offered — and the chosen option gets the full plan write. Ported from osolmaz/pi-workflows' decision-gate demo. Pass { problem, scope, constraints } in args, or say "autoplan this" and the bundled autoplan skill derives the args from the conversation.
  • sanity-check.js — read-only contribution review: evidence collection, four parallel area reviewers (necessity, duplication, contracts, scope/tests), then a verifier that tries to REFUTE every finding before the keep/simplify/refactor/drop/needs-evidence verdict. Ported from osolmaz/pi-workflows. Pass { baseRef } in args.
  • autoimplement.js — implement a supplied plan (never devises one) behind an ask plan gate, then a bounded build → verify → review/fix loop where P0/P1 block and the round cap prevents an unbounded fix spiral. Ported from osolmaz/pi-workflows. Pass { task, plan, verify } in args.

Dependencies

  • @nicknisi/pi-shared (workspace:*) — the subagent runtime and runWorkflow engine.
  • typebox — the tool's parameter schema.
  • @earendil-works/pi-coding-agent (peer) — the extension API, getAgentDir, CONFIG_DIR_NAME.

Caveats

  • The script runs in the host process with full Node access — the same trust boundary as the bash and codemode tools. Your model, your session.
  • checkpoint/ask need an interactive UI host. In a headless/RPC host checkpoint degrades to a logged no-op and ask throws — scripts that require an answer fail loudly instead of inventing one.
  • Pause/resume are session-scoped and best-effort for tool-initiated runs: slash commands may queue behind an in-flight turn, so a pause issued mid-turn engages at the next agent() boundary after it's processed. For a guaranteed human gate, put checkpoint/ask in the script itself.
  • Project-local workflows (.pi/workflows/) load only in trusted projects; untrusted projects are limited to global workflows so a cloned repo cannot silently inject orchestration scripts.
  • agent() cannot spawn children of its own (the ecosystem recursion guard refuses nested orchestration). For dependent multi-stage work where stages spawn, use @nicknisi/pi-codemode's runWorkflow instead.
  • stop cancels only runs spawned by this host process; persisted runs from other hosts show in status but are not cancellable here.