Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cad0p/pi-steering-hooks/llms.txt

Use this file to discover all available pages before exploring further.

definePredicate<T> is a pure pass-through at runtime — it returns the handler unchanged. Use it so plugin authors can declare typed argument shapes without casting at the plugin registration site. The generic parameter T narrows the handler’s first argument to the author’s intended shape; the PredicateHandler<T> return type preserves that narrowing when the result is stored in a local variable. The Plugin.predicates registry slot then accepts the result cast-free via AnyPredicateHandler (which uses TypeScript’s bivariance fallback to admit typed handlers directly).

Signature

import { definePredicate } from "pi-steering";

function definePredicate<T>(
  handler: (args: T, ctx: PredicateContext) => PredicateVerdict | Promise<PredicateVerdict>
): PredicateHandler<T>

Type parameter

T
type parameter
The typed argument shape the predicate accepts. This is whatever the rule author writes under the when.<key> slot in their config. The handler is responsible for validating the actual value at runtime — TypeScript can catch obvious mismatches, but a user writing when: { myPredicate: "not-the-expected-shape" } bypasses compile-time narrowing. Guard the args shape at the top of the handler body (e.g. if (typeof args !== "boolean") return false).

Complete example

import { definePredicate } from "pi-steering";
import type { PredicateHandler } from "pi-steering";

interface RequiresFlagArgs {
  flag?: string;
  flags?: readonly string[];
  env?: string;
}

export const requiresFlag = definePredicate<RequiresFlagArgs>(
  async (args, ctx) => {
    if (ctx.input.tool !== "bash") return false;
    const hasMatchingFlag =
      args.flags?.some(f =>
        ctx.input.args?.some(w => w.value === f || w.text === f)
      ) ?? false;
    return !hasMatchingFlag;
  }
);
Registering the predicate in a plugin:
import type { Plugin } from "pi-steering";
import { requiresFlag } from "./predicates/requires-flag.ts";

export const myPlugin = {
  name: "my-plugin",
  predicates: { requiresFlag },
} as const satisfies Plugin;
Rule authors can then reference it via when.requiresFlag:
{
  name: "must-pass-flag",
  tool: "bash",
  field: "command",
  pattern: /^my-tool\b/,
  reason: "Pass --safe-mode when running my-tool.",
  when: { requiresFlag: { flags: ["--safe-mode"] } },
}

PredicateVerdict

type PredicateVerdict = boolean | "unknown";
Handlers return a trinary verdict:
Return valueMeaning
truePredicate fires — rule condition is satisfied
falsePredicate does not fire — rule condition is not satisfied
"unknown"Handler could not resolve its value
Pre-trinary handlers that return plain boolean remain source-compatible — boolean is a subtype of PredicateVerdict, so existing handlers assign without changes.

"unknown" verdict and the onUnknown policy

When a handler returns "unknown" — typically because some piece of walker-tracked state (cwd, branch, etc.) was not statically resolvable — the engine applies the onUnknown policy configured on that predicate leaf to project the trinary verdict back to a definite boolean:
  • "block" (default): treat as fail-closed — the predicate fires.
  • "allow": treat as fail-open — the predicate skips.
Rule authors configure the policy per-leaf using the spread form:
when: {
  cwd: { pattern: /\/workspace\//, onUnknown: "allow" },
}
Inside a not: block, the onUnknown policy lives at the block level rather than the leaf level, and applies to all leaves in the block.

Throw behavior

Throws inside a handler — synchronous or from a rejected promise — are caught by the engine, treated as "unknown", and the onUnknown policy applies. This prevents a buggy handler from silently failing open by skipping its rule. Prefer explicit "unknown" returns; the catch exists as a safety net, not a control-flow mechanism.
// Prefer this — explicit signal to the engine
export const myHandler = definePredicate<MyArgs>(async (args, ctx) => {
  if (ctx.walkerState?.cwd === "unknown") return "unknown";
  // ...
});

// The engine also catches sync/async throws and treats them as "unknown"
// — but explicit returns are clearer.

Registering module augmentations

For TypeScript to offer autocomplete and shape-checking on when.requiresFlag in rule definitions, declare the predicate in the global PiSteeringPredicates interface. This is separate from the handler registration in Plugin.predicates and is required for the registry-driven mapped types to surface the field:
import type { PredicateShape } from "pi-steering";
import type { RequiresFlagArgs } from "./predicates/requires-flag.ts";

declare global {
  interface PiSteeringPredicates {
    requiresFlag: PredicateShape<RequiresFlagArgs>;
  }
}
With this declaration in place, when.requiresFlag is shape-checked at every rule definition site that uses defineConfig or satisfies Rule.

Build docs developers (and LLMs) love