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.

defineConfig is the preferred way to author pi-steering configurations. It threads rule, plugin, and observer names through TypeScript generics so typos are caught at tsc --noEmit time rather than silently failing at runtime. At runtime the function does minimal work — it returns a normalized shallow copy of the input config. All the value is in the types.

Signature

import { defineConfig } from "pi-steering";

function defineConfig<
  const P extends readonly Plugin[] = [],
  const Inline extends readonly Observer[] = [],
  const R extends readonly Rule<
    AllObserverNames<P, Inline>,
    AllWrites<P, R, Inline>
  >[] = [],
>(config: DefineConfigInput<P, Inline, R>): SteeringConfig

DefineConfigInput fields

plugins
Plugin[]
Plugins to register. Names must be unique across the active plugin set — collisions log a plugin-name-collision diagnostic. Pass plugins as as const satisfies Plugin[] to preserve literal name values for typo-checking in disabledPlugins.
rules
Rule[]
User-authored rules. Added on top of DEFAULT_RULES unless disableDefaults: true. Use as const satisfies Rule[] on external constants so the literal name values survive for disabledRules typo-checking and when.happened.event narrowing.
observers
Observer[]
Standalone observers not tied to a plugin. Observer name literals thread through AllObserverNames so string references in Rule.observer are typo-checked at compile time. Use as const satisfies Observer[] to preserve the literal types.
disabledRules
string[]
Rule names to disable. Typo-checked against the union of all registered rule names: engine defaults (DEFAULT_RULES), plugin-shipped rules, and your own inline rules. A name not in that union is a compile-time error.
disabledPlugins
string[]
Plugin names to disable. Typo-checked against the union of DEFAULT_PLUGINS names and any plugins passed in the plugins array. A disabled plugin contributes nothing — no rules, predicates, trackers, or observers.
disableDefaults
boolean
default:"false"
When true, drops both DEFAULT_RULES and DEFAULT_PLUGINS entirely. Useful for isolated test harnesses or minimal configs that opt into only their own rules. Distinct from disabledRules / disabledPlugins — this flag is imperative (“disable the defaults”), while the lists are declarative (“these items are disabled”).
defaultNoOverride
boolean
default:"true"
Fail-closed override policy applied to rules that don’t specify their own noOverride field. When true (the default), override escape hatches are blocked unless the rule explicitly sets noOverride: false. Set to false to make all rules overridable unless they individually set noOverride: true.
failOnWarnings
boolean
default:"true"
Strict mode. When true (the default), any warning-class SteeringDiagnostic produced while loading the config escalates to a thrown error that disables the extension for the session. Set to false to fall through to console.warn and keep the bridge running with whatever subset loaded cleanly. Error-class diagnostics always throw regardless of this flag.

Compile-time safety

defineConfig threads four name unions through TypeScript generics:
GenericWhat it constrains
AllRuleNames<P, R>disabledRules — must be a known rule name
AllPluginNames<P>disabledPlugins — must be a known plugin name
AllObserverNames<P, Inline>Rule.observer string references
AllWrites<P, R, Inline>when.happened.event and when.happened.since
// ✅ Valid — typo-checked against AllRuleNames
defineConfig({ disabledRules: ["no-force-push"] })

// ❌ TypeScript error — "wrong-name" is not a registered rule
// @ts-expect-error
defineConfig({ disabledRules: ["wrong-name"] })

as const satisfies requirement

For cross-reference checking to work — observer name inference, rule name typo-checking, and when.happened.event narrowing — TypeScript must preserve the literal types of name and writes fields. The as const satisfies pattern does this. A plain type annotation widens the literals to string and silently disables all checking.
// ✅ Works — preserves literal "name" and "writes" types
const myRule = {
  name: "no-npm-publish",
  tool: "bash",
  field: "command",
  pattern: /^npm\s+publish\b/,
  reason: "Use the org policy script instead.",
  writes: ["npm-publish-blocked"],
} as const satisfies Rule;

// ❌ Widens name and writes to string — breaks inference
const myRule: Rule = {
  name: "no-npm-publish",
  writes: ["npm-publish-blocked"],
  // ...
};
The same requirement applies to Plugin and Observer constants passed into defineConfig. Rules and observers declared inline inside the defineConfig({ rules: [...] }) call are inferred directly through the const R generic and don’t need the annotation.

Complete example

import { defineConfig } from "pi-steering";
import gitPlugin from "pi-steering/plugins/git";

export default defineConfig({
  plugins: [gitPlugin],
  rules: [
    {
      name: "no-npm-publish",
      tool: "bash",
      field: "command",
      pattern: /^npm\s+publish\b/,
      reason: "Use `pnpm publish` with the org policy script instead.",
    },
  ],
  disabledRules: ["no-main-commit"],
  defaultNoOverride: false,
  failOnWarnings: true,
});

Behavior with no observers declared

When no plugins contribute observers and no inline observers array is passed, AllObserverNames resolves to never. Any string Rule.observer reference is then a compile-time error — fail-closed on unknown observer names. For configs that intentionally reference observers by name without registering them inline, use satisfies SteeringConfig as a fallback; you lose typo detection but regain flexibility.

Walk-up merge

defineConfig operates on a single config layer. When the loader discovers multiple .pi/steering.ts files walking up from the session cwd to $HOME, it merges layers with inner (closer to cwd) layers taking precedence on name collisions. Each layer is authored with defineConfig independently; the merging is the loader’s concern, not defineConfig’s.

Build docs developers (and LLMs) love