A Reference

Every rule,
with a specimen
from the field.

A reader's guide to the rules that ship by default in phr-mcp init --packs llm,rust. Each entry carries its predicate, its severity, a one-line distillation, and a worked example drawn from real-world Rust code.

Default packs
6
Rules shipped
41±
Blocks (pre)
13
Warnings (pre/post)
11
Audit-only
10
Field examples
24

How to read this

Each rule is presented as a labelled specimen. The circular glyph encodes the severity at a glance: a red is a block (pre-phase, exit 2, the host refuses the action); an amber ! is a warning (post-phase, exit 1, the action applies but the operator is told); a slate is audit-only (silent at hook time, surfaced only by phr-mcp audit).

Tags beneath each rule name show its phase and whether it participates in audits. The italic line is the rule's one-sentence point. From the field blocks show synthetic but plausible Rust code that would trip the rule, paired with a refactor that satisfies it.

Examples are illustrative, not extracted verbatim. They are written to clarify the rule, not to document any one project's actual state.

§I
1

Pack · LLM — behavior, not code shape.

6Rules

The LLM pack catches phrasing patterns that recur in long LLM-assisted sessions: deflective disclaimers that shift blame to pre-existing code, unverified completion claims that report success without evidence, and workflow moments where a nudge before commit or push is worth more than a thousand words in a CLAUDE.md that has already faded from context.

BLOCK phase: pre ×3 variants

enforce-no-pre-existing-issue · -not-from-our-changes · -not-caused-by-our

Stops three closely-related disclaimer phrases that pre-emptively blame the existing tree rather than naming the issue and deciding to fix or defer.

All three fire on substring match against new content — no file gates. They apply equally to code edits, commit messages, and PR bodies. The shared shape: the model has noticed a problem and is reaching for a phrase that retires it without a decision.

trigger in a commit message
// Subject line, ~turn 80 of a long session
fix(scoring): switch priority ordering to descending

The test failure in play::turn_resolver_tests is
a pre​‑existing issue not from our changes; ignored.
accepted form
// Subject line
fix(scoring): switch priority ordering to descending

Known broken: play::turn_resolver_tests::priority_order
fails on the new sort comparator. Filed as #421;
this PR does not address it. (See follow-up.)

Why The disclaimer pattern is a known failure mode in long sessions: an issue gets surfaced, attribution-shifted, and then forgotten. The accepted form forces a binary — fix it as part of this change, or name it and defer it explicitly.

BLOCK phase: pre ×2 variants

enforce-no-should-work-claim · enforce-no-should-be-fixed-claim

Stops "should​ work now" and "should​ be fixed" phrasing when they appear without accompanying verification.

These phrases are the linguistic marker of the unfinished handoff: a change made, a hypothesis stated, no test run. The rule cannot tell whether you actually ran the test — but it can force the language to match the evidence.

trigger
The duplicate-entry bug should​ work now. Closes #87.
verified report
Verified against the seed that triggered #87:
    $ cargo test hand::duplicate_entry -- --exact
    test ... ok (1 passed)
Closes #87.
WARN phase: pre on Bash content

nudge-verify-before-commit

Fires when a Bash command contains "git​ commit -m". A reminder, not a refusal — to trace the call chain end-to-end before claiming done.

Hook fires before the commit runs. The host prints the rule's message to stderr at exit 1; the commit still proceeds. The point is the moment of friction — a half-second to ask did I actually verify this before the message is permanent.

trigger
$ git​ commit -m "wire up new play ledger"
phronesis: WARNING — About to commit. Trace
the call chain end-to-end before reporting done.
Half-fixes where one layer is wired but another is
not are a recurring failure mode.
verified, then commit
$ cargo test --workspace -p core
test ... ok (124 passed)
$ cargo run -- --scenario play-smoke
... play smoke passed
$ git​ commit -m "wire up new play ledger"
§II
2

Pack · Rust — code shape, error handling, debt.

29Rules

The Rust pack mixes hard blocks (panic-shaped exits, sync/async misuse, raw Result<_, String>), softer warns (signature smells, dbg!() leftovers), and audit-only debt indicators (newtype opportunities, god-files, manual error propagation). The examples below all draw from a representative Rust application — a game engine — chosen because the domain tends to surface every category of Rust pain at once.

BLOCK phase: pre audit: yes ×4 variants

enforce-no-{unwrap, panic, todo, unimplemented}-in-src

Four sibling rules. Each catches one panic-shaped exit (.unwrap(), panic!(), todo!(), unimplemented!()) added in any file under src/. Test code is excluded via inline #[cfg(test)] stripping.

All four share the same shape: substring match against new content, gated by a file-path match on src. The hook strips test blocks before evaluation, so usage inside inline test modules — where panics are idiomatic — does not trip the rule.

From the field src/example/hand.rs
pub fn current_card(&self) -> &Card {
    self.members
        .get(&self.turn_order[self.turn_index])
        .unwrap()
}
refactored
pub fn current_card(&self) -> Result<&Card, HandError> {
    let id = self.turn_order
        .get(self.turn_index)
        .ok_or(HandError::TurnIndexOutOfRange)?;
    self.members
        .get(id)
        .ok_or_else(|| HandError::CardNotFound(id.clone()))
}

Why Panic-shaped exits in production paths crash the host runtime. The cost of carrying a Result through one more call is much lower than the cost of a mid-session crash in a tool that holds long-lived state.

BLOCK phase: pre predicate: AST

enforce-no-result-string-error

Fires when a function's signature returns Result<_, String>. Strings are not error types.

Detected via tree-sitter: the rule walks function signatures and matches the precise return shape. Test functions are excluded automatically. The remedy is a proper error enum, usually with thiserror.

From the field src/core/event_pipeline.rs
pub fn enqueue(&mut self, event: Event) -> Result<(), String> {
    if self.queue.len() >= self.cap {
        return Err(format!("queue full at cap {}", self.cap));
    }
    self.queue.push(event);
    Ok(())
}
refactored
// New: a domain error type
#[derive(Debug, thiserror::Error)]
pub enum PipelineError {
    #[error("event queue full at cap {0}")]
    QueueFull(usize),
}

pub fn enqueue(&mut self, event: Event) -> Result<(), PipelineError> {
    if self.queue.len() >= self.cap {
        return Err(PipelineError::QueueFull(self.cap));
    }
    self.queue.push(event);
    Ok(())
}

Why A typed error makes the pipeline's failure modes explicit to callers, lets ? compose with From impls, and survives refactors. A stringified error is a one-way trapdoor — every caller has to parse the message to recover any structure.

WARN phase: pre audit: yes

warn-dbg-in-src

Catches dbg!() in any src/ file. Diagnostic leftover; never an intentional shipping pattern.

From the field src/core/play/turn_resolver.rs
fn resolve_play(&mut self, play: Play) -> Resolution {
    let roll = dbg!(self.compute_priority() + play.bonus);
    match roll {
        _ if roll >= play.target_threshold => Resolution::Valid,
        _ => Resolution::Invalid,
    }
}
refactored
use tracing::trace;

fn resolve_play(&mut self, play: Play) -> Resolution {
    let roll = self.compute_priority() + play.bonus;
    trace!(target: "play", ?play, roll, "resolving");
    match roll {
        _ if roll >= play.target_threshold => Resolution::Valid,
        _ => Resolution::Invalid,
    }
}
WARN phase: post predicate: AST

warn-rust-public-fn-takes-string-ref

Public function takes a &String parameter where &str would accept a strictly larger set of callers.

From the field src/example/hand.rs
pub fn find_card(&self, name: &String) -> Option<&Card> {
    self.members.iter().find(|c| &c.name == name)
}
refactored
pub fn find_card(&self, name: &str) -> Option<&Card> {
    self.members.iter().find(|c| c.name == name)
}

Why A &str accepts string literals, slices into existing buffers, and the result of .as_str() on a String. A &String forces every caller to first own a String — a needless allocation requirement at an API boundary.

WARN phase: post predicate: AST

warn-rust-public-fn-takes-vec-ref

Public function takes a &Vec<T> where &[T] would accept slices, arrays, and Vecs.

From the field src/world/state.rs
pub fn apply_events(&mut self, events: &Vec<Event>) {
    for event in events {
        self.apply(event);
    }
}
refactored
pub fn apply_events(&mut self, events: &[Event]) {
    for event in events {
        self.apply(event);
    }
}
WARN phase: post audit: yes threshold: 5

warn-rust-function-param-count-high

Function takes five or more parameters (excluding self). A god-signature smell.

From the field src/core/play/turn_resolver.rs
pub fn new(
    id: String,
    name: String,
    score: u32,
    max_score: u32,
    rank: u32,
    priority: i32,
    actions: Vec<Action>,
    members: Hand,
) -> Self { ... }
refactored
pub struct HandStats { pub score: u32, pub max_score: u32, pub rank: u32, pub priority: i32 }

pub fn new(
    id: ActorId,
    name: String,
    values: HandStats,
    strategy: Strategy,   // holds actions + members
) -> Self { ... }

Why Long parameter lists correlate with god-functions and make call sites stateal and brittle. Grouping related params into named structs improves readability and gives a place for invariants to live.

WARN phase: pre audit: yes

warn-deref-for-non-pointer-type

Catches impl Deref for outside of true smart-pointer wrappers. Deref polymorphism is an anti-pattern.

From the field src/example/hand.rs
pub struct HandView { inner: Hand }
impl Deref for HandView {
    type Target = Hand;
    fn deref(&self) -> &Hand { &self.inner }
}
refactored
pub struct HandView { inner: Hand }
impl HandView {
    pub fn hand(&self) -> &Hand { &self.inner }
}
    // expose specific methods explicitly
    pub fn name(&self) -> &str { self.inner.name() }
}
WARN phase: pre on Bash content

warn-cargo-build-without-workspace

Catches cargo build / test / check / clippy invoked without --workspace in a multi-crate workspace.

trigger
$ cargo test
phronesis: WARNING — Running `cargo test` without
`--workspace` only checks part of the workspace. Use
`cargo <subcommand> --workspace --tests --examples`
to catch sibling-crate breakage, or pass `-p <crate>`
if scope was intentional.
scoped, full
# Whole workspace, including sibling crates
$ cargo test --workspace --tests --examples

# Or scoped intentionally to one crate
$ cargo test -p core hand::

Why A bare cargo build in a multi-crate workspace silently skips sibling crates whose paths Cargo can't infer from the cwd. The result is the classic "green locally, red on CI" failure when a small refactor breaks a downstream crate.

WARN phase: pre predicate: AST threshold: 3

warn-clone-heavy

A single function body contains three or more .clone() calls — usually a sign that the borrow checker has been argued with rather than appeased.

From the field src/example/hand.rs
fn snapshot_hand(hand: &Hand) -> HandSnapshot {
    HandSnapshot {
        name:     hand.name.clone(),
        members:    hand.members.clone(),
        state:    hand.state.clone(),
        log:      hand.log.clone(),
    }
}
refactored
#[derive(Clone)]
pub struct HandSnapshot { ... }

fn snapshot_hand(hand: &Hand) -> HandSnapshot {
    hand.snapshot.clone()  // one borrow into the canonical clone
}

// or, if read-only is enough:
fn snapshot_hand(hand: &Hand) -> &HandSnapshot { &hand.snapshot }
WARN phase: post predicate: AST

warn-empty-test

A #[test] function body with no assertion macro and no ? operator. A placeholder that always passes is a regression hidden in plain sight.

From the field tests/example/hand.rs
#[test]
fn hand_can_be_created() {
    let _p = Hand::new("the winning hand");
}
refactored
#[test]
fn hand_starts_empty_and_named() {
    let p = Hand::new("the winning hand");
    assert_eq!(p.name(), "the winning hand");
    assert!(p.members().is_empty());
}
BLOCK phase: pre audit: yes ×2 variants

block-await-on-sync-execute-all-agenda-items · -fire-all-consequences

Catches stale .await calls on methods that were made synchronous in a prior refactor — a recurring source of stale call sites after API changes.

Project-specific in shape, broadly useful in concept: these two methods on phronesis's own RETE network were converted from async to sync in an earlier refactor, and old .await call sites kept turning up in test code and integration helpers afterwards. The rule pins the API surface so that resurrected snippets get caught before they reach a future is not awaitable compile error. The same shape — naming two specific now-sync methods you wish to police — generalizes cleanly to any project undergoing a sync/async migration.

From the field tests/integration/round_smoke.rs
#[tokio::test]
async fn round_smoke() -> Result<(), TestError> {
    let engine = make_engine();
    let actions = engine.execute_all_agenda_items().await?;
    assert_eq!(actions.len(), 3);
    Ok(())
}
refactored
#[test]
fn round_smoke() -> Result<(), TestError> {
    let engine = make_engine();
    let actions = engine.execute_all_agenda_items()?;
    assert_eq!(actions.len(), 3);
    Ok(())
}

Why Without the rule, the compiler error is confusing — "Result<…> is not a future" — and easy to misdiagnose as a Tokio version mismatch. The named rule turns a five-minute hunt into a one-line fix.

AUDIT phase: audit silent at hook

audit-manual-err-return

Catches the shape => return Err(…) in a match arm — almost always a place where ? would replace the whole construct.

Audit-only by design. Firing this at hook time would interrupt every refactor mid-flight; firing it at sweep time gives a debt count of "places where the ? operator wasn't used."

From the field src/core/event_pipeline.rs
let event = match parse_event(&payload) {
    Ok(e) => e,
    Err(e) => return Err(e),
};
refactored
let event = parse_event(&payload)?;
AUDIT phase: audit ×2 variants

audit-newtype-id-string · audit-newtype-id-u64

Surfaces struct fields named *_id: String or *_id: u64 — typical candidates for the newtype pattern.

From the field src/topology/graph.rs
pub struct State {
    pub id: String,
    pub name: String,
    pub exits: Vec<String>,    // also ids, also strings
    pub slot_ids: Vec<String>,
}
refactored
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StateId(String);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SlotId(String);

impl StateId {
    pub fn new(s: impl Into<String>) -> Self { Self(s.into()) }
    pub fn as_str(&self) -> &str { &self.0 }
}

pub struct State {
    pub id: StateId,
    pub name: String,
    pub exits: Vec<StateId>,
    pub slot_ids: Vec<SlotId>,
}

Why A bare String id lets you pass a SlotId where a StateId is expected. The compiler will not save you. A newtype turns the mistake into a type error at the call site.

AUDIT phase: audit ×2 variants

audit-if-let-opportunity-none-empty · -err-empty

Catches None => {} and Err(_) => {} match arms — either redundant if let opportunities or silent error-swallows.

From the field src/core/play/turn_resolver.rs
match self.active_target.as_ref() {
    Some(target) => self.narrate_play(target),
    None => {}
}

match self.write_to_log(&entry) {
    Ok(()) => {},
    Err(_) => {}   // silent swallow!
}
refactored
if let Some(target) = self.active_target.as_ref() {
    self.narrate_play(target);
}

if let Err(e) = self.write_to_log(&entry) {
    tracing::warn!(?e, "play log write failed");
}
AUDIT phase: audit threshold: 800

audit-file-loc-high

Flags .rs files exceeding 800 lines. A god-file signal; not a violation, just a candidate for splitting.

audit output
$ phr-mcp audit --rule audit-file-loc-high

Rule                 Level  Hits  Files
audit-file-loc-high  warn      3      3
  src/core/event_pipeline.rs   lines: 1
  src/core/play/turn_resolver.rs  lines: 1
  src/example/round/mod.rs             lines: 1
a plan, not a fix
// Split event_pipeline.rs (1418 LOC) into:
src/core/event_pipeline/
├── mod.rs                  // 80 LOC: the trait, re-exports
├── queue.rs                // 220 LOC: the ring buffer
├── dispatch.rs             // 410 LOC: the dispatch table
├── consequences.rs         // 340 LOC: side-effect drainage
└── shape.rs                // 180 LOC: payload types
                            // 1230 LOC total, 188 LOC saved as overlap

Why A file that crosses 800 lines is usually doing several jobs. The rule is a sweep indicator, not a mechanical fix — the value is in being told the pile exists during a quarterly review, not on every edit.

BLOCK phase: pre audit: yes patterns book

block-deny-warnings-attribute

Blocks #![deny(warnings)] in any .rs file. A toolchain-fragility anti-pattern: every new rustc release introduces new warnings and breaks the build.

avoid src/lib.rs
#![deny(warnings)]

// every cargo update is a coin flip
pub fn hello() { println!("hello"); }
put the policy in CI
// src/lib.rs — no global deny
pub fn hello() { println!("hello"); }

# .github/workflows/ci.yml
- run: cargo clippy --workspace -- -D warnings
  env:
    RUSTFLAGS: "-D warnings"

Why Library code with #![deny(warnings)] stops compiling for downstream consumers as soon as their toolchain introduces a new warning. The policy belongs in CI, where it gates merges without propagating fragility to anyone using the crate. Source: rust-unofficial/patterns §Anti-patterns (deny-warnings).

WARN phase: pre audit: yes patterns book

warn-public-fn-takes-box-ref

Detects parameter types of the form &Box<T>. The borrow of an owned pointer adds a useless layer of indirection; &T conveys the same intent without the wrapper.

avoid
pub fn describe(card: &Box<Card>) -> String {
    format!("{}: VALUE {}", card.name, card.score)
}
prefer
pub fn describe(card: &Card) -> String {
    format!("{}: VALUE {}", card.name, card.score)
}

Why A &Box<T> can always be obtained from a &T via auto-deref, but the reverse forces every caller to pre-wrap their value in a Box regardless of where it actually lives. Source: rust-unofficial/patterns §Idioms (borrowed-types-for-arguments).

WARN phase: pre audit: yes

warn-expect-with-empty-message

Flags .expect("") in src/. Strictly worse than .unwrap() — the same panic, plus the noise of a method named expect with nothing to expect.

avoid
let config = load_config().expect("");
let port = config.port.expect("");
give the invariant a name
let config = load_config()
    .expect("config must exist at $CARGO_MANIFEST_DIR/config.toml");
let port = config.port
    .expect("validate_config() guarantees port is Some");

Why When the panic eventually fires in production, the message is what tells the operator which invariant was broken. An empty message turns a debuggable assertion into an opaque crash; the existing warn-unwrap-in-src rule already flags .unwrap(), but .expect("") escapes it by being technically a different call.

AUDIT phase: audit predicate: substring borrow-checker workaround

audit-rc-refcell-in-src

Flags occurrences of Rc<RefCell<T>> inside src/. The textbook "fighting-the-borrow-checker" shape — often a signal that the ownership model could be redesigned.

audit output
$ phr-mcp audit --rule audit-rc-refcell-in-src

Rule                      Level  Hits  Files
audit-rc-refcell-in-src   warn      4      2
  src/world/region.rs        lines: 18, 42
  src/world/graph.rs         lines: 7, 91
redesign options
// arena + indices replaces shared mutability
pub struct RegionArena {
    regions: Vec<Region>,
}
pub struct RegionId(u32);

// or: precompute the graph, treat it as immutable
// or: move mutability into a single owner, pass &mut

Why Rc<RefCell<T>> is sometimes the right answer — for graph-shaped data with true aliasing, for callback registries, for single-threaded observer patterns. But it is also the canonical shape that appears when someone has lost a fight with the borrow checker and reached for the escape hatch. Surface it during one-time audits so design intent gets revisited.

AUDIT phase: audit predicate: substring patterns book

audit-string-concat-with-plus

Flags the " + & shape — string-literal concatenated with a borrowed string. A signal that format! would be clearer.

avoid
let msg = "hand " + &name + " drew " + &card.to_string();
prefer
let msg = format!("hand {} drew {}", name, card);

Why Plus-chained string concatenation produces a sequence of intermediate String allocations and forces awkward .to_string() calls on non-string arguments. format! reads as a template, allocates once, and accepts anything that implements Display. Source: rust-unofficial/patterns §Idioms (concat-format).

AUDIT phase: audit predicate: substring edition 2024

audit-env-set-var-in-src

Flags env::set_var( in src/. Edition 2024 marks this call unsafe because mutating the process environment is unsound when other threads read it concurrently. The audit gives you a chance to verify intent.

avoid in library code
pub fn configure_database(url: &str) {
    unsafe { std::env::set_var("DATABASE_URL", url); }
}
pass configuration explicitly
pub struct DbConfig { pub url: String }

pub fn connect(cfg: &DbConfig) -> Result<Conn, DbError> {
    Conn::open(&cfg.url)
}

Why The standard library was tightened in edition 2024 for the same reason this rule exists: env-var mutation is process-global and unsynchronized, and any read from another thread during the write is undefined behavior. Tests where you control thread count are usually fine (and already opt in with unsafe); library code almost never is. Prefer passing configuration explicitly through arguments or a context struct.

AUDIT phase: audit predicate: substring

audit-allow-dead-code-in-src

Flags #[allow(dead_code)] attributes in src/. The compiler is telling you something; the attribute silences it without addressing the underlying question of why the code exists.

avoid
#[allow(dead_code)]
pub fn compute_secondary_score(&self) { /* never called */ }
either delete it, or document why it stays
// option A: delete

// option B: document the reason
/// Held in reserve for the upcoming scoring-revision RFC (see #142).
/// Not yet wired into the dispatch table; cargo would otherwise warn.
#[allow(dead_code)]
pub fn compute_secondary_score(&self) { /* ... */ }

Why Many #[allow(dead_code)] attributes get added during refactors and then forgotten. Audit sweeps surface them so the team can decide, code by code, whether each one is still earning its keep.

audit-rust-let-binding-count-high · audit-rust-let-mut-count-high

Two heuristic audits that surface candidates for John Nunley's block pattern: functions whose top-level let ladder runs eight bindings or longer, or whose top-level let mut declarations reach three or more. Both go silent on functions that have already adopted the pattern.

audited
fn build() {
  let a = step1();
  let b = step2(a);
  let c = step3(b);
  let d = step4(c);
  // … eight outer-scope
  //   lets total
  use_result(h);
}
block pattern (silent)
fn build() {
  let result = {
    let a = step1();
    let b = step2(a);
    let c = step3(b);
    step4(c)
  };
  use_result(result);
}

Why A block expression leads with intent (let result = …), keeps intermediate names from polluting the outer scope, and scopes mutability — a let mut inside a block returns an immutable binding to its caller.

The walker halts at child { … }, closures, and nested fn bodies, so the rule does not punish the very shape it surfaces. Conditional and loop bodies still recurse (they continue the outer flow). Source: John Nunley, “Rust’s Block Pattern” (Dec 2025).

§III
3

Pack · Rhai — script hygiene for embedded engines.

2Rules

Discipline for projects that embed the Rhai scripting language. The pack assumes a host that loads .rhai files, compiles them to ASTs, and registers host-side functions the scripts can call. The two rules push back on the two most common entropy sources: inline engine.eval in the Rust source, and ad-hoc print() in the scripts themselves. Messages are intentionally generic — wire any project-specific guidance (which loader helper, which response-proxy) into your own .phronesis/rules.json.

BLOCK phase: pre predicate: AST

block-rhai-inline-eval-string

Blocks calls of the shape engine.eval(<string literal>) in Rust source. Inline string-eval bypasses any script registry and resists isolated testing; a .rhai file plus compile_file / eval_ast is the cached, reviewable alternative.

avoid src/scripting/util.rs
let result: i64 = engine.eval("let base = 10; base * 2 + 5")?;
precompiled AST
// scripts/calc_priority.rhai is loaded once,
// compiled to an AST, then eval'd many times.
let ast = engine.compile_file("scripts/calc_priority.rhai".into())?;
let result: i64 = engine.eval_ast(&ast)?;

Why The compiled-AST path lets the script be values-checked at build time, exercised in isolation, cached across calls, and discovered via the file system rather than buried as a literal in unrelated code. Inline string-eval gives up all of those affordances for a character-count win.

BLOCK phase: pre audit: yes

block-rhai-print-in-script

Blocks print( in any .rhai file. Rhai's print is the equivalent of dbg!() — debug output that bypasses whatever response/logging channel the host has registered.

avoid scripts/save.rhai
fn execute(actor, slot) {
    print(`Saving state for ${actor} to slot ${slot}`);
    save_state(actor, slot);
}
use the host-registered channel
// Whatever your Engine exposes via register_fn:
// log / emit / notify / response_append / etc.
fn execute(actor, slot) {
    log(`Saving state for ${actor} to slot ${slot}`);
    save_state(actor, slot);
}

Why Output from a shipped script should flow through the same path as the rest of the application's output — to be captured, replayed, formatted, or addressed to a specific surface (user chat, server log, audit trail). print escapes to stdout, which is usually nowhere useful.

§IV
4

Other packs — Python · TypeScript · Swift.

11Rules

Smaller language packs ship with minimal seeds. Compose them with --packs llm,python or similar. The intent is a starting point; downstream projects are expected to grow their own rules over time.

Python · --packs python

warn · warn-print-in-src block · enforce-no-bare-except

Catches print() in src/ (use logging) and bare except: clauses (which swallow KeymapInterrupt and SystemExit together with everything else).

TypeScript · --packs typescript

warn · warn-any-in-src warn · warn-console-log-in-src

Catches : any annotations (use unknown and narrow with guards) and stray console.log calls in committed source.

Swift · --packs swift

warn · warn-swift-force-unwrap warn · warn-swift-try-bang warn · warn-swift-force-cast · audit-swift-fatal-error · audit-swift-mutable-singleton · audit-swift-legacy-constructor · audit-swift-legacy-random

Three force-bang warnings cover the postfix !, try!, and as! escape hatches — prefer guard let / if let, try? with do/catch, and as? binding. Four audit-only rules surface debt: fatalError( sites (prefer throws, precondition, or assertionFailure); static var shared mutable singletons (the canonical pattern uses static let shared); the pre-Swift-3 C-style constructors CGRectMake, UIEdgeInsetsMake, and their siblings (use the labeled Swift initializers); and the pre-Swift-4.2 random APIs arc4random, arc4random_uniform, drand48 (use Int.random(in:) / randomElement()). Sourced from eleev/swift-design-patterns and SwiftLint's default-enabled rule set.

All three packs are intentionally lean. Phronesis's value scales with how well the rules fit the project; canned defaults beyond these few would be presumptuous about your codebase. Start with the seeds, then run the drift detectors to find gaps. phr-mcp claude-md-drift flags CLAUDE.md imperatives without a corresponding rule. phr-mcp memory-drift flags auto-memory entries. phr-mcp wiki-drift flags ADR-style decision pages under .phronesis/wiki/decisions/ — decisions with explicit enforces: frontmatter resolve deterministically; the rest fall through to Jaccard matching. Grow the pack from there.