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 ¶
- func AppendRule(path string, r Rule) error
- func DefaultRoots() []string
- func IsValidEvent(e Event) bool
- func IsValidSeverity(s Severity) bool
- func LocalRulesPath() string
- func RemoveRule(path, name string) (bool, error)
- func UserRulesPath() string
- type Context
- type Event
- type File
- type Result
- type Rule
- type Severity
- type Verdict
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AppendRule ¶ added in v0.21.4
AppendRule writes one new rule to the file at path, creating the file (and parent dirs) when missing. Validates the rule's shape and condition syntax BEFORE persisting so a malformed add never corrupts the existing rules. Returns ErrDuplicate when a rule with the same Name already exists in the file.
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 IsValidSeverity ¶
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
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 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
}
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" )
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 ¶
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 ¶
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 ParseBytes ¶
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 ¶
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.