Skip to content

Authoring a pack

A pack is a directory of rule markdown files plus a pack.toml manifest. To ship one, push that directory to a public GitHub repo; your users install it with sextant rules add github:<you>/<repo>@<tag>.

my-pack/ # repo root, or a subdirectory of one
├── pack.toml # required
├── README.md # optional, but recommended — shown in your repo
└── rules/
├── rule-one.md
├── rule-two.md
└── …

The rules/ subdirectory is non-negotiable: the loader looks for .md files there. Anything outside rules/ (a README, a LICENSE) is part of the pack’s hash record but isn’t loaded as a rule.

name = "typescript"
version = "0.1.0"
description = "Strict TypeScript rules for AI agents."
homepage = "https://github.com/yourname/your-pack"
license = "MIT"
sextant = ">=0.1.0" # min sextant version (parsed, not yet enforced)
FieldRequiredNotes
nameyesBecomes the directory name under .sextant/rules/vendor/. Use kebab-case or snake_case; the loader will reject mismatched names.
versionrecommendedSemVer-style. Surfaced in sextant rules add output.
descriptionrecommendedOne-liner.
homepageoptionalURL.
licenserecommendedSPDX identifier.
sextantoptionalVersion constraint string. Parsed today; enforced in a future release.

Each rule is a markdown file with YAML frontmatter, identical to the repo-local rule schema — same fields, same evaluator types — with one convention:

Use vendor.<pack-name>.<short-id> as the rule id.

---
id: vendor.typescript.no-any
name: "No `any` type"
description: "Bans `any` in any type position."
severity: error
category: reliability
scope: file
languages: [typescript, tsx]
evaluator:
type: ast
query: '((predefined_type) @t (#eq? @t "any"))'
capture: t
tags: [strict, types]
---
# No `any` type
…body shown by `sextant rules explain`…

The vendor.<pack>. prefix isn’t enforced syntactically, but it’s how users tell vendor rules apart in sextant rules list and how the source: vendor:<pack> provenance pairs cleanly with the id.

Most pack rules will want the ast evaluator, which runs a tree-sitter query over each file’s parse tree. Compared to regex, AST queries:

  • Don’t fire on matches inside string literals or comments (those are different node kinds).
  • Can target type-position vs. value-position keywords precisely.
  • Support not_under: [<node-kind>] for context-sensitive exemptions (“allow unknown only inside a catch_clause”).
evaluator:
type: ast
query: '((predefined_type) @t (#eq? @t "unknown"))'
capture: t
not_under: [catch_clause]
message: "no `unknown` here — use a generic"
FieldRequiredNotes
queryyesTree-sitter query S-expression. Compiled once per language listed in languages.
capturenoCapture name to anchor the finding line. Defaults to the first capture in the query.
messagenoOverride message. Falls back to <rule.name>: matched <snippet>.
not_undernoDrop a match if any ancestor’s node kind is in this list.

Authoring tip: keep one pack.toml checked into the repo while you iterate, install via file:./path/to/pack, and sextant rules update re-syncs each time you change a rule. When the pack is ready, tag a release and your users switch their install spec to github:.

Pack rules typically ship at severity: error so they hard-block under [verdict] max_errors = 0. That’s the strictest signal you can send: violations of your pack’s rules are not warnings.

If a rule is genuinely advisory, mark it severity: info (or warn) and include that in the pack’s README so users tune their thresholds appropriately.

Tags should be SemVer:

  • PATCH — query refinement that catches new cases without breaking existing ones.
  • MINOR — new rule, or expanded rule scope (more files match).
  • MAJOR — rule removal, id rename, or behavior that flips previously-passing code to failing.

Document breaking changes in your release notes. Users update by re-running sextant rules add <spec>@<new-tag>.

The recommended layout for a dedicated pack repo:

your-pack-repo/
├── pack.toml
├── README.md
├── LICENSE
├── rules/*.md
└── tests/ # optional but recommended
└── … # see Sextant's own packs/typescript tests
# for a fixture pattern

If your repo ships multiple packs, put each one under its own subdir and have users install with the #<subdir> form:

Terminal window
sextant rules add github:you/repo@v1.0.0#packs/typescript
sextant rules add github:you/repo@v1.0.0#packs/python