What problem does this solve?
verifyTaskflow is a great zero-token structural audit (dead-ends, unreachable, gate-exhaustion, budget-overflow, concurrency, guard-contradictions, contract-ref mismatches) — but it stops at the graph. The contents of a script phase's run command are unchecked beyond shape validation in validateTaskflow (presence of run, no interpolation in the string form). So a flow can pass /tf compile clean and then fail at runtime for a reason that was statically knowable before any model was spawned — the opposite of the "verify before spend" thesis.
Concrete class of misses I hit: script gates that run a project's own test/build command. A few things that are 100% statically detectable, zero-token, and runner-agnostic:
- a
grep whose pattern starts with - with no -- separator (grep parses it as flags → exit 2, a false RED);
- a
grep/sed with an invalid regex under its dialect flags (exit 2);
runner | grep … without pipefail / PIPESTATUS (the filter's exit masks the runner's — a failing runner can read as GREEN);
- a suite/smoke
script that names a per-task test file the flow itself creates in an earlier phase → RED until that phase runs, RED-forever if it's skipped.
These are all "the plan is data, so it can be verified" — just at the command-string layer instead of the DAG layer.
Current workaround
Run my own verifier as a separate pre-flight step in my own extension before calling /tf run — i.e. re-implement verifyTaskflow's aggregation and call site wiring myself, outside the engine. It works, but it's a parallel verification path that doesn't flow into /tf compile's Mermaid overlay, /tf verify's report, or the runtime's pre-flight (runtime.ts calls verifyTaskflow before execution). The diagnostics live in a different place than the built-in ones, which is worse for users than having them in one report.
Proposed solution
A pluggable verifier seam on the existing public API, matching the pure-function, zero-dependency shape verify.ts already has:
-
Add an optional second argument to verifyTaskflow:
export type Verifier = (flow: VerifiableFlow) => VerificationIssue[];
export function verifyTaskflow(
flow: VerifiableFlow,
verifiers?: Verifier[],
): VerificationResult;
Built-in detectors run first (unchanged), then each caller-supplied verifier runs against the same sanitized safeFlow; issues are merged into the singleVerificationResult. Verifiers are pure (no I/O), matching verify.ts's existing "no I/O" contract.
-
Thread it through CompileOptions so /tf compile overlays them on the Mermaid diagram like any other issue:
export interface CompileOptions {
direction?: "TD" | "LR";
title?: string;
verifiers?: Verifier[]; // forwarded to verifyTaskflow
}
-
Thread an optional verifiers through the runtime pre-flight call sites (runtime.ts, the two verifyTaskflow(...) calls before execution) so a host embedding the engine can register domain verifiers that block spend.
The one open typing question is IssueCategory, which is a closed union today. Two options, happy with either:
- add a generic
"custom" (or "plugin") category, or
- widen
category to string (built-ins keep their union as the documented set).
Discovery / loading is the part I'd most like your call on. Minimum viable is programmatic-only (a host/extension passes verifiers when it calls the engine — this is all I personally need). If you want /tf verify and /tf compile to pick up project-local verifiers too, options are a convention dir (e.g. taskflow.verifiers/ auto-loaded), a named registry in a config file, or neither (programmatic only).
I'd suggest shipping programmatic-only first and deciding discovery later.
Alternatives considered
gate.eval / gate.score: these judge a phase's output (or an upstream output target) with a fixed operator set (eval) or a fixed 6-type scorer set (score). Neither lints a script phase's run definition before execution, and neither allows arbitrary logic — which is what command-string lint needs. Wrong layer.
- A new phase type (
lint): heavier than necessary and doesn't run at compile time. The checks belong next to the existing structural detectors, not in the runtime. External pre-flight only (the current workaround): works but fragments the verification surface and misses the runtime pre-flight + the Mermaid overlay.
Is this a good fit?
If this lines up with the roadmap I'm happy to open a PR — small, focused (verify.ts and compile.ts and threading the two runtime call sites + tests under packages/taskflow-core/test/), one concern. Naming (verifiers vs validators vs checks) and the discovery question above I'll follow your preference on.
What problem does this solve?
verifyTaskflowis a great zero-token structural audit (dead-ends, unreachable, gate-exhaustion, budget-overflow, concurrency, guard-contradictions, contract-ref mismatches) — but it stops at the graph. The contents of ascriptphase'sruncommand are unchecked beyond shape validation invalidateTaskflow(presence ofrun, no interpolation in the string form). So a flow can pass/tf compileclean and then fail at runtime for a reason that was statically knowable before any model was spawned — the opposite of the "verify before spend" thesis.Concrete class of misses I hit:
scriptgates that run a project's own test/build command. A few things that are 100% statically detectable, zero-token, and runner-agnostic:grepwhose pattern starts with-with no--separator (grep parses it as flags → exit 2, a false RED);grep/sedwith an invalid regex under its dialect flags (exit 2);runner | grep …withoutpipefail/PIPESTATUS(the filter's exit masks the runner's — a failing runner can read as GREEN);scriptthat names a per-task test file the flow itself creates in an earlier phase → RED until that phase runs, RED-forever if it's skipped.These are all "the plan is data, so it can be verified" — just at the command-string layer instead of the DAG layer.
Current workaround
Run my own verifier as a separate pre-flight step in my own extension before calling
/tf run— i.e. re-implementverifyTaskflow's aggregation and call site wiring myself, outside the engine. It works, but it's a parallel verification path that doesn't flow into/tf compile's Mermaid overlay,/tf verify's report, or the runtime's pre-flight (runtime.tscallsverifyTaskflowbefore execution). The diagnostics live in a different place than the built-in ones, which is worse for users than having them in one report.Proposed solution
A pluggable verifier seam on the existing public API, matching the pure-function, zero-dependency shape
verify.tsalready has:Add an optional second argument to
verifyTaskflow:Built-in detectors run first (unchanged), then each caller-supplied verifier runs against the same sanitized
safeFlow; issues are merged into the singleVerificationResult. Verifiers are pure (no I/O), matchingverify.ts's existing "no I/O" contract.Thread it through
CompileOptionsso/tf compileoverlays them on the Mermaid diagram like any other issue:Thread an optional
verifiersthrough the runtime pre-flight call sites (runtime.ts, the twoverifyTaskflow(...)calls before execution) so a host embedding the engine can register domain verifiers that block spend.The one open typing question is
IssueCategory, which is a closed union today. Two options, happy with either:"custom"(or"plugin") category, orcategorytostring(built-ins keep their union as the documented set).Discovery / loading is the part I'd most like your call on. Minimum viable is programmatic-only (a host/extension passes
verifierswhen it calls the engine — this is all I personally need). If you want/tf verifyand/tf compileto pick up project-local verifiers too, options are a convention dir (e.g.taskflow.verifiers/auto-loaded), a named registry in a config file, or neither (programmatic only).I'd suggest shipping programmatic-only first and deciding discovery later.
Alternatives considered
gate.eval/gate.score: these judge a phase's output (or an upstream output target) with a fixed operator set (eval) or a fixed 6-type scorer set (score). Neither lints ascriptphase'srundefinition before execution, and neither allows arbitrary logic — which is what command-string lint needs. Wrong layer.lint): heavier than necessary and doesn't run at compile time. The checks belong next to the existing structural detectors, not in the runtime. External pre-flight only (the current workaround): works but fragments the verification surface and misses the runtime pre-flight + the Mermaid overlay.Is this a good fit?
If this lines up with the roadmap I'm happy to open a PR — small, focused (verify.ts and compile.ts and threading the two runtime call sites + tests under
packages/taskflow-core/test/), one concern. Naming (verifiersvsvalidatorsvschecks) and the discovery question above I'll follow your preference on.