Embedding ai-rulez (Go API)¶
github.com/Goldziher/ai-rulez/v5/pkg/airulez loads a project, validates it, plans or applies a generate run and writes or checks its lock from Go, inside a process that may serve many projects at once. It is Experimental for its first minor release: after that, pkg/... changes only additively inside a major version (CI runs gorelease against the last release). Everything under internal/ is private.
ws := airulez.NewMemWorkspace()
ws.Set(".ai-rulez/config.toml", "version = \"5.0\"\nname = \"svc\"\npresets = [\"claude\"]\n", 0o644)
ws.Set(".ai-rulez/rules/style.md", "# Style\n\nBe concise.\n", 0o644)
project, err := airulez.Load(ctx, airulez.Options{Workspace: ws})
plan, err := project.Plan(ctx, airulez.PlanOptions{})
fmt.Println(plan.Digest, len(plan.Files))
Workspaces¶
A Workspace is the read view of a project tree (an io/fs file system plus Lstat and ReadLink). Three implementations ship:
| Constructor | Reads |
|---|---|
DirWorkspace(dir) |
a directory of the real file system |
NewMemWorkspace() |
files you Set in memory |
GitSnapshot(ctx, repoDir, rev, runner) |
the tree of a commit, nothing checked out |
A symlink resolves only inside the workspace. A symlink the load refuses goes to Options.Logger, and validation fails on one in the project's own content; without a logger nothing is written to stderr. A snapshot has no machine-local files, like --no-local. A directory workspace ignores them too unless you set Options.WithLocal: machine-local files (config.local.toml, .ai-rulez/local/) carry the trust of the machine that wrote them, which a service loading someone else's tree should not grant.
Plans¶
Project.Plan lists every file a run would write, merge into or remove, with a digest of the rendered content, and writes nothing. Plan.Digest is the SHA-256 of the canonical plan document (schema/plan.schema.json, also what ai-rulez generate --emit-plan prints). It covers the sources and the existing outputs and manifests the workspace holds, so it is not a fingerprint of the sources alone: the same sources in a fresh directory and in one holding earlier outputs give different digests. The plan reads what already exists from the workspace itself: the previous manifest, merged documents such as .claude/settings.json and hand-edited outputs. A workspace in memory or in a commit therefore plans its own removals (stale files, entries taken back out of merged documents) the way a directory does. A machine-local record found in a commit is not believed, because a commit holds whatever its author chose.
Project.Generate applies a run: Write (directory workspaces only; anything else fails with CodeDiskRequired before touching anything), DryRun (the action list) or Check (files that differ from the plan). The same appliers back generate, generate --dry-run, generate --check and generate --emit-plan.
Project.Validate checks the configuration; ValidateOptions.Strict also runs the content and security checks (stable AR codes) and needs a directory workspace. Git, used to index tracked files, runs through Options.Runner; with the default DenyAll() the directory is walked instead. A canceled context stops Validate between its steps.
Locks¶
Project.Lock writes .ai-rulez/ai-rulez.lock the way ai-rulez lock does, through the same code and with the same gates: the minimum release age of version ranges, the security scan (AR001-AR009) of every remote tree about to be pinned and of every served skill, the deny list (AR717) and the carry-over of approvals. LockOptions mirrors the command's flags (Names, Kind, ContentOnly, Profile, Roles, AcceptFindings, Strict). Both Lock and LockCheck need a directory workspace (anything else is CodeDiskRequired).
- A remote tree whose scan has error findings, or with
Stricta served skill the scan refuses, fails withCodeFindingsand writes nothing.errors.As(err, &fe)with a*airulez.FindingsErrorlists each one as aLockFindingwith itsARcode. This is exit 2 of the command. - Without
Stricta refused served skill is left out of the lock and reported inLockResult.Unpinned, while the rest is written. This is exit 3 of the command. - Remote sources are re-resolved through
Options.Runneronly when the project was loaded withOptions.Remote. Release times are then also asked of the forge over HTTPS; withoutRemotethe forge is never contacted. - The
Projectkeeps the configuration it was loaded with.Loadagain to plan against the new lock.
Project.LockCheck is ai-rulez lock --check without the network: it compares the lock with the sources, the rendered outputs and the cached remote content, and returns LockStatus{InSync, Changes, Notes}. Cached remote content that disagrees with the lock is drift (InSync false), not an error. The remote tag (--verify-tags) and signature checks of the command are not part of it. A project with no lock, and no [lock] enforce, fails with CodeLock.
No ambient authority¶
Loading and planning use no working directory, no process environment, no clock and no subprocess unless you give them: pass Options.Env, Options.Clock, Options.Runner (default DenyAll(); implement the Runner interface with the public Spec and Result types to deny, record or sandbox commands) and Options.Logger (every warning of a load, validation or plan goes there, and a service that gives none gets none). A load is bound by no organization policy unless one is given to it; the process-wide policy of the command line does not reach a library user. Remote includes and installed skills are fetched only with Options.Remote, through your Runner, with Options.GitToken. An ai-rulez.lock is enforced exactly as by ai-rulez generate: when the lock exists (and [lock] enforce is not false), a remote source it does not cover fails Load. Two Projects share no state, so a service can plan different projects concurrently; the operations of one Project are serialized.
Not in the API¶
The agent subcommand, file watching, init prompts, scanners, usage recording, the eval runners and the MCP server stay in the command line.
Versioning¶
- Additive changes in minor releases; breaking changes only with a new major version and
/vNpath. - New surface is
Experimentalfor one minor release, marked in its godoc. - The plan document has its own
schemanumber: a new field is additive, a removed or re-typed one raises it and is announced in the changelog. - Errors are
*airulez.Errorwith a stableCode(load,plan,validate,apply,disk-required,refused,lock,findings); the cause is reachable witherrors.Isanderrors.As.refused(errors.Is(err, airulez.ErrRefused)) is a run that would overwrite a file ai-rulez cannot prove it wrote; nothing was written.