@signalridge/pi-rules1.3.1
Project and user rule files for Pi: always-on guidance, an on-demand rule index, and rules enforced on tool calls.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-rules
@signalridge/pi-rules loads rule files from the project and from the user's
Pi agent directory and hands each one to the model in the way its frontmatter
asks for:
- always — the rule text is part of the system prompt;
- index — the system prompt lists the rule's name and description, and the
model reads the text on demand with the
read_ruletool; - glob — when a tool call touches a file matching the rule's
globs, the model is told the rule exists and is asked to read it; - block / remind — a regular-expression
conditionis checked againstedit,write,bashandpowershellarguments before the call runs. Ablockrule stops the call and returns the rule as the tool error; aremindrule lets the call run and steers the rule into the conversation as a hidden message before the next model request.
The package makes no model requests of its own. It is experimental and opt-in.
Install
pi install npm:@signalridge/pi-rules
From a checkout of this repository: pi -e ./packages/pi-rules.
Where rules come from
| Precedence | Location | Kind |
|---|---|---|
| 1 | .pi/rules/*.md |
project |
| 2 | .agents/rules/*.md |
project |
| 3 | .cursor/rules/*.mdc |
project, Cursor format |
| 4 | <agent dir>/rules/*.md |
user |
<agent dir> is Pi's agent directory (~/.pi/agent unless
PI_CODING_AGENT_DIR is set). Project directories are relative to the session's
working directory; ancestors and subdirectories are not searched.
A rule's name is its file name without the extension: 1–64 letters, digits,
., _ or -. When two files share a name, the one with higher precedence
wins and the other is shadowed (/rules lists it). A file that fails to parse
still claims its name, so a broken project rule never quietly hands its name to
a user rule. A file with enabled: false is skipped entirely and does not claim
its name. Files are read at session start and on /reload; a file over 64 KiB
is rejected, and at most 200 files per directory are read, in name order.
Project trust
Project rules are untrusted input: an always-apply rule becomes system-prompt
text and an enforced rule can stop tool calls. The three project directories are
read only when ctx.isProjectTrusted() is true; user rules always load.
Pi decides trust only for projects with protected resources (.pi/settings.json,
.pi/extensions, .pi/skills, .agents/skills and the others listed in Pi's
security documentation). A project whose only Pi-specific content is rule files
is treated as trusted without a prompt, the same exposure as AGENTS.md, which
Pi loads regardless of trust. Start Pi with --no-approve to keep a project's
rules out of a session. /rules says when project rules were skipped.
Rule file format
A rule file is Markdown with optional frontmatter. The body after the frontmatter is the rule text and must not be empty.
| Field | Values | Meaning |
|---|---|---|
description |
text | Shown in the index and in glob offers. |
alwaysApply |
true / false |
Put the rule text in the system prompt. |
globs |
glob or list; a string may be comma-separated | Offer the rule for matching files. On a condition rule, the call must also touch a matching file. |
condition |
regex or list of regexes | Check tool-call arguments. A leading (?i), (?m) or (?s) becomes the matching JavaScript flag. |
scope |
tokens, comma-separated or a list | Which calls a condition checks (see below). Default: all four tools. |
enforce |
block / remind |
What a condition match does. Default: remind. |
interruptMode |
never, prose-only, tool-only, always |
oh-my-pi's name for enforce; use one of the two. |
repeatMode |
once / after-gap |
When the rule's text is delivered again. Default: once. |
repeatGap |
1–9999 | Assistant responses between deliveries for after-gap. Default: 10. |
enabled |
true / false |
false removes the file from discovery. |
A rule's mode is decided in this order: a condition makes it block or
remind; otherwise alwaysApply: true makes it always; otherwise globs
make it glob; otherwise a description puts it in the index. A rule with
none of these is manual: it is loaded and read_rule returns it, but nothing
announces it.
The frontmatter reader accepts the YAML subset rule files use — one
key: value per line, quoted strings, true/false, one-line [a, b] lists
and - item lists — and rejects anything else with a line number. Unlike YAML,
an unquoted value may start with *, so Cursor's globs: *.ts works. Unknown
fields, wrong types, invalid regexes and invalid globs reject the whole file;
nothing is guessed. A startup notice counts rejected files and warnings, and
/rules lists each one.
Scope
scope tokens follow oh-my-pi's syntax:
toolortoolcall— every checkable tool;tool:edit,tool:write,tool:bash,tool:powershell(or the bare name);tool:edit(<glob>),tool:write(<glob>)— only calls whosepathmatches;tool(<glob>)means both.
What is checked: each edits[].newText of an edit (the replacement text, not
the text being replaced), the content of a write, the command of bash
and powershell. bash and powershell calls have no path, so a path glob on
them is an error, and a rule with globs never matches them.
Path matching mirrors Pi's exact lowercase file:// recognition and decodes those URLs before
matching. Other spellings, such as file:src/a.ts or uppercase FILE://, remain literal paths,
not decoded URLs. Filename whitespace and escaped space/Unicode bytes remain significant.
Some oh-my-pi scope tokens name streams Pi cannot check before a call runs. They
produce a warning and are dropped, which only ever narrows where a rule fires:
text and thinking (assistant prose, see Not supported) and
tools other than the four above. A rule left with no checkable scope is
rejected.
Sharing rule files with oh-my-pi and Cursor
oh-my-pi's description, alwaysApply, globs, condition, scope and
interruptMode mean the same here. interruptMode maps onto what Pi can do
before a tool runs:
interruptMode |
Here | Why |
|---|---|---|
never |
remind |
The call runs; the rule follows as a reminder. |
prose-only |
remind |
Only prose matches would interrupt, and prose is not checked, so tool matches remind. |
tool-only, always |
block |
Pi cannot abort a model stream, but it can refuse the call before it has any effect. |
Two differences matter when sharing a file:
- oh-my-pi interrupts by default; here a rule without
interruptModeorenforcereminds. StateinterruptMode: alwaysif a shared rule must block. - oh-my-pi stops interrupting once a rule has fired. Here a
blockrule blocks every matching call, because a retried identical call would otherwise go through; the repeat policy only decides whether the block message quotes the rule again or points atread_rule.
oh-my-pi's astCondition, question, agents and legacy ttsr_trigger fields
are rejected with a reason rather than ignored, because ignoring them would make
a rule fire in places its author excluded. Cursor's .mdc fields
(description, globs, alwaysApply) are read as written.
System prompt and caching
Always-apply rules and the index are one system-prompt section, <pi_rules>,
set through before_agent_start systemPromptOptions.sections. Its text depends
only on the loaded files, in precedence and file-name order, with no counters or
timestamps, so it is byte-identical on every run and Pi never appends a section
patch; the provider's prompt cache keeps its prefix. Editing a rule file changes
the section after /reload, which costs one cache miss. Enforced and glob rules are not in the prompt
until they match. When read_rule is not an active tool, the index lists each
rule's file path instead. Glob offers—including description-less rules absent from the index—and
repeated block references likewise provide the rule-file path when read_rule is inactive or hidden.
The extension never widens the selected tool whitelist.
Repeat policy and persistence
A queued reminder or glob offer counts as delivered only when its own authenticated custom message
is admitted at Pi's native message_start boundary, not when it is queued. Pending matches are
deduplicated in memory; cancellation, settlement, branch changes and runtime retirement release
undelivered claims so a later match can offer them again. Copied message details cannot confer a
receipt. Block messages and successful read_rule calls retain their immediate delivery behavior. Each
delivery appends a pi-rules:delivered custom entry to the current session
branch with pi.appendEntry; these entries never reach the model. Repeat state
is recomputed from the current branch whenever a rule matches:
once: delivered at most once per branch;after-gap: delivered again afterrepeatGapassistant responses.
Because the state lives on the branch, /reload and /resume do not deliver a
rule twice, and /tree navigation to a point before a delivery re-arms the
rule on that branch. Reminders are hidden custom messages of type
pi-rules:reminder; like any steered message they stay in the transcript and
in model context.
Tool and command
read_rule({ name }) returns the text of any loaded rule. An unknown name returns an error listing the loaded names./ruleslists every loaded rule with its mode, source, matches since load and deliveries on this branch, plus shadowed and disabled files and every rule-file error or warning. In the TUI it opens a list; choose a rule to see its details. RPC clients get the same report as a notification./rules <name>shows one rule.
Examples
.pi/rules/style.md:
---
alwaysApply: true
---
Use the repository's formatter settings; never reformat files you did not change.
.pi/rules/http-handlers.md:
---
description: How HTTP handlers validate input and report errors
---
Validate the request body with the shared schema before touching storage, and
return errors through `problemResponse()`.
.cursor/rules/react.mdc:
---
description: React component conventions
globs: src/**/*.tsx
alwaysApply: false
---
Function components only; colocate a component's styles in the same folder.
.pi/rules/no-force-push.md:
---
description: Never rewrite shared history
condition: 'git\s+push\b.*(--force\b|-f\b)'
scope: tool:bash
enforce: block
---
Do not force-push. Ask the user how to reconcile the branch instead.
<agent dir>/rules/ts-no-any.md:
---
condition: '(?:: |\bas )any\b'
scope: tool:edit(*.ts), tool:write(*.ts), tool:edit(*.tsx), tool:write(*.tsx)
interruptMode: never
repeatMode: after-gap
repeatGap: 20
---
Avoid `any`: use `unknown` and narrow it, or name the real type.
The package ships no built-in rules. oh-my-pi's bundled rules are language-idiom opinions, and even generic ones would need an opt-in setting this package otherwise has no reason to own; a copied example is a rule the user owns and can change.
Load order with other policy extensions
Pi runs tool_call handlers in extension load order and stops at the first
block. Load pi-rules before an extension that asks a human to approve calls, so
a rule blocks a call before anyone is asked about it. A reminder is sent when
pi-rules lets a call through, even if a later handler then blocks it; its text
says only that pi-rules did not stop the call.
Not supported
- Assistant prose (
scope: text/thinking). A prose match can only be acted on after the text has reached the user, by requesting one more model turn atagent_before_settle. That would make this package a continuation controller next to whatever controller the session already uses, and cost a request per match. Prose scope is dropped with a warning. - Mid-stream abort and retry. Pi has no "abort this stream and retry"
primitive distinct from the user pressing Escape. Blocking at
tool_callstops a call before it has any effect, which covers tool rules. - AST conditions, model-judged rules and rule authoring from a complaint
(oh-my-pi's
astCondition,questionand/omfg). - Per-agent rules (
agents). Rules apply in every session that loads this extension, including child sessions another extension starts with it. - Windsurf, Cline and Copilot instruction files, and searching ancestor directories.
Provenance
The rule model is adapted from oh-my-pi
(can1357/oh-my-pi, MIT, © Mario
Zechner, Can Bölük, Stencil Labs), read at commit 1f23be9b: its frontmatter
field names, the rule bucketing order (docs/rulebook-matching-pipeline.md,
src/capability/rule-buckets.ts), the scope token syntax and inline-flag
translation (src/capability/rule.ts, src/export/ttsr.ts), the
once/after-gap repeat policy, and the shape of its tool reminder
(src/prompts/system/ttsr-tool-reminder.md).
This package is an independent implementation for Pi's public extension API.
It checks rules at tool_call instead of watching the model stream, blocks
instead of aborting, persists delivery on the session branch instead of in a
session-manager field, reads its own strict frontmatter subset instead of YAML
with a line-based fallback, reads Pi's and Cursor's directories instead of
oh-my-pi's, and rewrites every prompt for Pi's tool names.