A plugin is a named bundle of predicates, rules, observers, and trackers that users opt into viaDocumentation 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.
plugins: [...]. Publishing your logic as a plugin rather than inlining it in a config gives you independent versioning, npm discoverability, and the ability to share predicate helpers and observer event constants with downstream consumers. The engine composes plugins at config load time — there is no plugin registry to maintain and no approval process.
Plugin shape
name is optional. A plugin that only ships predicates (no rules) is valid — the predicates become available as when.<key> slots for users to write their own rules against.
Canonical file layout (ADR §13)
Separate each concern into its own file. The engine doesn’t enforce this structure, but it is the expected layout for production plugins and is the layout followed by the git plugin and thework-item-plugin example.
index.ts assembles the plugin object from its parts, declares the PiSteeringPredicates augmentation for TypeScript, and re-exports composable building blocks for downstream consumers. Each sub-file has its own unit test suite; an index.test.ts (or integration.test.ts) pins the end-to-end wiring.
Typed predicate handlers with definePredicate<T>
definePredicate<T> is a zero-cost type helper — pure pass-through at runtime — that lets you declare a typed argument shape for your predicate without having to cast at the plugin registration site.
PredicateHandler<T> type narrows args to T inside the callback. Consumers of your predicate who import the handler directly get the narrow type; consumers who use it via when.<key> in a rule config get it via the registry.
Registering predicates on PiSteeringPredicates
Every predicate key a plugin introduces must be declared on the global PiSteeringPredicates interface so the engine’s mapped type (TopLevelWhenClause) accepts it in the when: slot with the correct argument shape. Without the declaration, a reference to when.myKey is rejected at the type level.
PredicateShape<Bare, SpreadBase> takes two type parameters. The first is the “bare” value type (a bare when.branch: /^main$/ works when Bare is Patterns). The second is the spread form’s object shape, auto-detected from Bare when not supplied (a Patterns bare auto-detects to { pattern: Patterns }).
Place the declare global block in index.ts so that import "your-plugin" pulls the registry augmentation transitively for any consumer.
Observer encapsulation convention (ADR §14)
Every observer file exports three things — an event constant, a mark helper, and the observer itself. Rules that consume the event import the constant, never the raw string. This encapsulates the session-entry shape and ensures that a future rename of the event literal is a single-site change that the compiler propagates. When no observer corresponds to an event (a self-marking rule only), the constant and helper live in the rule file instead of a dedicated observer file.TEST_PASSED_EVENT in when.happened.event rather than the raw string "test-passed". If a typo were introduced into the constant, the compiler would catch every usage site simultaneously.
Assembling the plugin object
as const satisfies Plugin (rather than : Plugin) preserves the literal name: "work-item" in the inferred type, which is load-bearing for defineConfig’s disabledPlugins inference. It also preserves the writes tuples from rules and observers so defineConfig can cross-reference when.happened.event usages against this plugin’s declared writes.
Ecosystem conventions
Follow these conventions to make your plugin discoverable and interoperable with the broader pi ecosystem. Package name:pi-steering-<domain> (unscoped). Mirrors pi-steering core and the two built-in external plugins. Scoped names (@org/pi-steering-<x>) are fine for internal packages.
Keywords — add both tags to package.json:
pi-package surfaces your package alongside every pi extension. pi-steering-package surfaces it specifically in pi-steering plugin listings.
Peer range — during v0.x, pin tightly:
"^1" once pi-steering reaches v1.
Ship .ts source as the package entry for hot-reload during plugin development. Set "main": "./src/index.ts" in package.json, use allowImportingTsExtensions: true and noEmit: true in tsconfig.json, and optionally add erasableSyntaxOnly: true to reject non-strippable TypeScript features at compile time. Consumers must run Node ≥ 22.6 for native type-stripping.
Name collisions on
Plugin.trackers are a hard error — two plugins claiming the same tracker state dimension cannot both be loaded. Modifier collisions (two plugins extending the same tracker with the same modifier key) log a WARN and keep the first-registered modifier. Both cases surface at config load time before any rules are evaluated.