rules

package
v0.22.138 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 2, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package rules — predicate-based rule engine for clawtool. Rules gate operator-defined invariants ("README must be updated when shipping a feature", "no Co-Authored-By in commits", "skill routing-map row required when adding a core tool"). Each rule fires on an event (pre_commit / post_edit / session_end / pre_send) and produces a Result the caller surfaces to the agent or operator.

Why a new package, not BIAM hooks: internal/hooks fires SHELL COMMANDS for every event. This engine is in-process Go evaluation against a structured Context — no shell roundtrip, no JSON encoding to stdin, full type safety on conditions and predicates. The two compose: a hook entry can call `clawtool rules check` to invoke this engine, but most callers (the future Commit tool, the unattended-mode supervisor) should call rules.Evaluate directly.

Design notes:

  • Rules are PURE: given a Context, the same rule produces the same Result. No I/O inside Eval; all state is on the Context.
  • Conditions are a tiny DSL (changed(glob), commit_message_contains(s), tool_call_count(name) > N) parsed once at load time.
  • Severity is a 3-tier ladder (off / warn / block); a "block" result is the caller's signal to refuse the action.

This file declares the public types; eval.go implements the evaluator; loader.go reads .clawtool/rules.toml.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AppendRule added in v0.21.4

func AppendRule(path string, r Rule) error

AppendRule writes one new rule to the file at path, creating the file (and parent dirs) when missing. Pre-flight checks (shape, condition syntax, duplicate name) run via CheckRuleAdd so a malformed add never corrupts the existing rules file. Returns the same error CheckRuleAdd would when the rule wouldn't be appendable.

func CheckRuleAdd added in v0.22.40

func CheckRuleAdd(path string, r Rule) error

CheckRuleAdd runs every check `AppendRule` would run BEFORE persisting — shape validation, condition-syntax parse, and duplicate-name detection against the existing rules file at path. Returns nil when `AppendRule(path, r)` would succeed.

Used by `clawtool rules new --dry-run` to preview an add without writing the file. AppendRule itself calls this first so the validation logic stays single-source.

func DefaultRoots

func DefaultRoots() []string

DefaultRoots returns the search roots for rules.toml. Project- local (walked up from cwd) takes precedence over user-global, same convention skill / sandbox discovery uses.

func IsValidEvent

func IsValidEvent(e Event) bool

IsValidEvent guards against typos in TOML.

func IsValidSeverity

func IsValidSeverity(s Severity) bool

IsValidSeverity is the loader's allowlist guard. Empty severity in TOML defaults to "warn" — most operators want notification, not a hard block, when first wiring a rule.

func LocalRulesPath added in v0.21.4

func LocalRulesPath() string

LocalRulesPath returns the project-scoped rules path. Prefers an existing `.clawtool/rules.toml` walked up from cwd (so RulesAdd from anywhere inside the project lands in the right file); falls back to creating one in the literal cwd when no ancestor is found (first rule in a fresh project).

func RemoveRule added in v0.21.4

func RemoveRule(path, name string) (bool, error)

RemoveRule deletes the named rule from the file at path. Returns ok=false when no rule with that name exists; the file stays untouched.

func RewriteBashCommand added in v0.22.57

func RewriteBashCommand(cmd string) string

RewriteBashCommand returns cmd with `rtk ` prepended when:

  • cmd's first whitespace-delimited token is in the allowlist;
  • rtk is on PATH (memoized).

Otherwise cmd is returned unchanged. The function is pure modulo the one-time PATH probe — no I/O on the hot path.

Already-rewritten commands (`rtk git status`) are detected by looking at the first token and returned unchanged.

func UserRulesPath added in v0.21.4

func UserRulesPath() string

UserRulesPath returns the user-scoped rules path: $XDG_CONFIG_HOME/clawtool/rules.toml (or ~/.config/...).

Types

type Context

type Context struct {
	// Event is the lifecycle stage producing the evaluation. A
	// rule whose `when` doesn't match Event is skipped without
	// being parsed.
	Event Event

	// ChangedPaths lists the files modified in the current
	// session / commit / edit. Forward-slash paths relative to
	// the repo root. Backs `changed(glob)` and `any_change(glob)`.
	ChangedPaths []string

	// CommitMessage is the proposed commit message body (incl.
	// trailers). Empty when Event != EventPreCommit. Backs
	// `commit_message_contains(s)`.
	CommitMessage string

	// ToolCalls counts tool invocations in the current session
	// keyed by tool name. Backs `tool_call_count(name) > N`.
	ToolCalls map[string]int

	// Now is injected so tests can pin time. Loader-built
	// contexts default to time.Now().
	Now time.Time

	// Args carries free-form key→string values — escape hatch
	// for predicates that don't deserve a typed field yet
	// (e.g. SendMessage's target instance for EventPreSend).
	// Backs `arg(key) == value`.
	Args map[string]string

	// DocsyncViolations lists the Go source paths that changed
	// in this Context but whose sibling `.md` doc was not also
	// updated (per checkpoint.CheckDocsync). The caller
	// pre-computes this with internal/checkpoint.CheckDocsync
	// BEFORE calling Evaluate — eval.go must stay pure (no
	// stat, no I/O), so the FS work happens in the caller.
	// Backs `docsync_violation()` (no-arg predicate; true when
	// this slice is non-empty). Per ADR-022 §Resolved
	// (2026-05-02), the rule type ships as a 3-mode severity
	// gradient that reuses Severity verbatim — no parallel
	// enum.
	DocsyncViolations []string
}

Context is what conditions evaluate against. The caller populates the fields relevant to the firing event; unset fields behave as their zero value (empty slices, zero counts).

Fields are intentionally named to match the predicate vocabulary (e.g. ChangedPaths backs `changed(glob)`, CommitMessage backs `commit_message_contains(s)`).

type Event

type Event string

Event names the lifecycle hook a rule binds to. The set is fixed at v1; new events are additive, never renamed (same stability promise as internal/hooks).

const (
	// EventPreCommit fires before the Commit core tool finalises
	// a commit. Rules here gate message format, file scope, etc.
	EventPreCommit Event = "pre_commit"
	// EventPostEdit fires after Edit / Write succeed. Rules here
	// track "you edited X, now you must edit Y" pairings.
	EventPostEdit Event = "post_edit"
	// EventSessionEnd fires when the BIAM task / agent loop
	// terminates. Last-chance gate: "did you update the README?"
	EventSessionEnd Event = "session_end"
	// EventPreSend fires before SendMessage dispatches. Rules
	// here gate routing (e.g. "code-writing tasks never go to
	// opencode" — operator's memory feedback, codified).
	EventPreSend Event = "pre_send"
	// EventPreUnattended fires when --unattended is about to
	// activate. Rules here are the safety brake before the
	// agent loop runs without operator presence.
	EventPreUnattended Event = "pre_unattended"
	// EventPreToolUse fires before a tool dispatch (Bash, Read,
	// etc.) is handed to the underlying tool. Rules here gate
	// or rewrite tool invocations — e.g. the rtk token-filter
	// rule prepends `rtk ` to allowlisted Bash commands so the
	// proxy compresses output before it reaches the agent.
	EventPreToolUse Event = "pre_tool_use"
	// EventInterceptorPreToolUse is the upstream MCP RFC spelling
	// of EventPreToolUse — see SEP-1763 / modelcontextprotocol/
	// experimental-ext-interceptors. Accepted as a synonym at
	// load time; the loader normalizes occurrences to
	// "pre_tool_use" so Evaluate sees a single canonical event
	// name and predicates / dispatch keep one code path.
	EventInterceptorPreToolUse Event = "interceptor:pre_tool_use"
	// EventPrePush fires before `git push` (or the Commit tool's
	// Push=true path) sends commits upstream. Rules here gate
	// what's allowed to leave the local branch — the canonical
	// example is the `wip-on-protected-branch` rule that blocks
	// `wip!:` checkpoint commits from reaching main/master/
	// develop/release/* (operator's autosquash flow keeps `wip!:`
	// commits local until they're squashed into a real subject
	// via `git rebase -i --autosquash`).
	EventPrePush Event = "pre_push"
)

type File

type File struct {
	Rule []Rule `toml:"rule"`
}

File is the on-disk shape — the [[rule]] array hosts the actual rules; future top-level metadata (version, comment) goes here.

type Result

type Result struct {
	Rule     string   `json:"rule"`
	Severity Severity `json:"severity"`
	Passed   bool     `json:"passed"`
	// Reason is the human-readable justification. Empty when
	// Passed is true — passing rules are silent.
	Reason string `json:"reason,omitempty"`
	Hint   string `json:"hint,omitempty"`
}

Result is one rule's verdict against one Context.

type Rule

type Rule struct {
	Name        string   `toml:"name"`
	Description string   `toml:"description,omitempty"`
	When        Event    `toml:"when"`
	Condition   string   `toml:"condition"`
	Severity    Severity `toml:"severity"`
	Hint        string   `toml:"hint,omitempty"`
	// contains filtered or unexported fields
}

Rule is one operator-declared invariant. Loaded from .clawtool/rules.toml and evaluated against a Context at the matching Event.

func Load

func Load(path string) ([]Rule, error)

Load reads the TOML file at path, validates each rule, and pre-parses each condition so Evaluate doesn't re-parse on every fire.

func LoadDefault

func LoadDefault() ([]Rule, string, bool, error)

LoadDefault tries each root in DefaultRoots order; returns the first that exists. ok=false when no rules file is configured; callers should treat that as "no rules to enforce" (clawtool's default mode is permissive — rules are opt-in).

func LookupRule added in v0.22.40

func LookupRule(path, name string) (Rule, bool, error)

LookupRule reads path and returns the rule named `name` (if present) without modifying the file. Used by `clawtool rules remove --dry-run` to preview the delete: the same lookup RemoveRule does, just without the saveRules write afterwards. ok=false when no rule with that name exists in the file.

Returns (zero Rule, false, err) on parse / read errors so callers can distinguish "rule absent" from "couldn't read".

func ParseBytes

func ParseBytes(body []byte) ([]Rule, error)

ParseBytes is the test seam — same as Load but takes the body directly. Useful for ad-hoc rule strings in tests.

type Severity

type Severity string

Severity ladders the operator's response to a violation.

const (
	// SeverityOff — rule defined but disabled. Useful for
	// staging a new rule without flipping it on yet.
	SeverityOff Severity = "off"
	// SeverityWarn — surface the violation in the result
	// payload so the agent / operator sees it, but don't block.
	SeverityWarn Severity = "warn"
	// SeverityBlock — refuse the action. Callers MUST treat
	// a block result as a hard stop.
	SeverityBlock Severity = "block"
)

type Verdict

type Verdict struct {
	Event    Event    `json:"event"`
	Results  []Result `json:"results"`
	Warnings []Result `json:"warnings,omitempty"`
	Blocked  []Result `json:"blocked,omitempty"`
}

Verdict aggregates the result of evaluating every applicable rule against one Context. Callers act on Blocked first (hard stop); Warnings are non-fatal but should be surfaced.

func Evaluate

func Evaluate(rules []Rule, ctx Context) Verdict

Evaluate runs every rule whose When matches ctx.Event against the context. Rules are evaluated in declaration order. A condition parse failure surfaces as a Result with Passed=false, Reason naming the parse error, Severity propagated from the rule — otherwise a typo in TOML would silently skip the rule.

func (Verdict) IsBlocked

func (v Verdict) IsBlocked() bool

Blocked reports whether at least one block-severity rule failed. Callers MUST consult this before proceeding with the action the rules guarded.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL