# Invariants

> Guided authoring of an architectural invariant catalog entry through a 6-step interview (elicit, locate, weight, enforce, number, commit). Drives the invariants_scaffold, invariants_add and invariants_amend orchestrate verbs — the agent supplies judgment and natural-language elicitation; the verbs own schema validation, file writing, and event emission. Correcting an existing entry is invariants_amend (id-targeted, field-scoped), never a re-run of invariants_add. Defaults to mode: audit; mode: check is an advanced opt-in. Triggers: 'add an invariant', 'author an invariant', 'enforce an architectural rule', 'fix/correct/amend an invariant', or invariants. Do NOT use for: editing workflow state, running a review, or hand-writing YAML (the verbs write it — never emit catalog YAML yourself).

- Skill: `lvlup-sw/invariants` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add lvlup-sw/invariants`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lvlup-sw/invariants/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: lvlup-sw (https://skillmd.com/u/lvlup-sw)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lvlup-sw/invariants

---


# Authoring Invariants Skill

## Overview

This skill turns an architectural rule in the author's head into a **registered,
enforced catalog entry**. It is the on-ramp described in
`docs/designs/archive/2026-05-25-invariants-authoring-wizard.md` — an LLM-driven
authoring conversation, **not** a stdin question loop.

The division of labor is strict and load-bearing:

- **The agent (you) supplies judgment + natural-language elicitation.** You
  interview the author, draft prose, propose globs, and shape the entry.
- **The verbs own validation + writing.** Every *mutation* goes through a
  deterministic, schema-validated orchestrate action — `invariants_scaffold`,
  `invariants_add`, and `invariants_amend`. **You never hand-write, append, or
  edit catalog YAML yourself.** The verb renders it; you confirm it.

This shape is the only one consistent with the agent-first philosophy: inputs
are constrained at the schema level (INV-5a), mutating verbs default to
dry-run (INV-5c), and authoring is event-sourced (INV-1).

## When to Use

- The author wants a new architectural rule enforced on their own SDLC path
  (surfaced at ideate, turned into acceptance criteria at planning, audited at
  review by the `check_invariant_conformance` gate).
- An author says "add an invariant", "enforce that X", "make this a rule".
- An author wants to **correct** an already-committed entry — a wrong summary, a
  stale glob, a weak `audit-prompt`. That is `invariants_amend`, and it is the
  only sanctioned way to change a shipped entry (see step 7).

## When NOT to Use

- The author wants to *hand-write* YAML — point them at the verbs instead; the
  skill exists so they don't have to.
- The rule is a one-off lint, not an architectural invariant → a project linter
  is the right home.
- You are mid-workflow editing state, running a review, or planning — those are
  other skills. This skill only authors catalog entries.

## The verbs (what you drive — never bypass)

| Action (MCP) | CLI facade | What it does |
|---|---|---|
| `invariants_scaffold` | `exarchos invariants scaffold` | Create a starter catalog file for a tier; idempotently register it in `.exarchos.yml`. Never overwrites an existing file. |
| `invariants_add` | `exarchos invariants add` | Validate ONE **new** entry against the v3 schema (including the `.strict()` enforcement DSL — INV-4), then append it. **`dryRun` defaults to true**: returns the rendered entry + file diff, writes nothing. Append-only: a colliding `id` is rejected. |
| `invariants_amend` | `exarchos invariants amend` | Correct ONE **existing** entry in place. `id` names the target (identity is not patchable); `patch` names the top-level fields to replace, and anything it omits survives verbatim. The merged entry is re-validated in full. **`dryRun` defaults to true**: returns the amended entry + a before/after diff, writes nothing. |
| `doctor` | `exarchos doctor` | Reuse the existing `invariants-catalog` check — the validator. No new validate verb. |
| `invariants_effective` | `exarchos view invariants_effective` | The merged, projected catalog the gate will enforce. Post-write confirmation. |

The agent **never** declares an entry valid on its own authority — the verb is
the validator. `invariants_add` returns the INV-5b carrier shape: success
carries `next_actions` (`["doctor", "view invariants_effective"]`); validation
errors carry `validTargets` / `expectedShape` / `suggestedFix` sourced from the
Zod error, so you can self-correct and re-run rather than re-guess.

## The interview (6 steps)

Walk the author through these in order. Elicit in prose, never make them think
in YAML field names.

### 1. Elicit — the rule → `summary`

Ask the author to state the rule in one sentence. Distill it into a precise
`summary`. Probe for the *failure* it prevents ("what goes wrong if this is
violated?") — that sharpens both the summary and the later enforcement.

### 2. Locate — `dimension`, `applies-to`, affinities

- `dimension`: a free-text grouping (e.g. `audit-completeness`, `error-handling`).
- `applies-to`: the glob(s) the rule governs — ask the author to name the paths
  (`src/handlers/**`, `**/*.ts`). These are **author-named globs**, never
  framework-inferred (INV-6: the surface is workload-neutral).
- `phase-affinity`: phases where it bites (`ideate | plan | delegate | review |
  synthesize`). Absent ⇒ all phases.
- `workflow-affinity`: workflow types (`feature | debug | refactor | discovery |
  oneshot`). Absent ⇒ all.

### 3. Weight — `severity`, `integrity-class`, and the `tier` you author into

- `severity.default`: `blocking` or `advisory`.
- `severity.by-workflow` (optional): downgrade for cheap workflows
  (e.g. `oneshot: advisory`).
- `integrity-class` (entry field; enum `substrate | sdlc | authoring | user`):
  the entry's **override authority**, not its namespace. For a consumer-authored
  rule this is `user`. (`substrate`/`sdlc` are exarchos's own classes — you do
  not author those.)
- **`tier` (the verb arg) picks the catalog _namespace_, and the choice is
  *exarchos-substrate vs project-authored*, NOT "which developers".** This is the
  one that bites — get it wrong and you silently collide with exarchos's own ids:
  - **`tier: user` → `U-N` ids — your project's own invariants. The default for
    _everyone_ consuming exarchos.** If you are authoring a rule for your own
    repo, this is always the answer (even if your project happens to name its
    rules `INV-N` internally — they map to `U-N` here).
  - **`tier: dev` → `INV-N` ids — exarchos's _own_ reserved substrate catalog.**
    Exarchos ships its own `INV-1..6` inside the tool, and they merge into every
    `invariants_effective` projection. Authoring into `dev` from a consumer repo
    **collides your `INV-N` with exarchos's own** — a silent namespace clash the
    `doctor` check can't catch (it only flags `INV-*` in a *user* catalog). Use
    `dev` **only when working inside the exarchos repo itself**. The verbs
    enforce this: `invariants_scaffold` / `invariants_add` reject `tier: dev`
    outside the exarchos repo (heuristic: `package.json` name ≠
    `@lvlup-sw/exarchos`) with a `RESERVED_TIER` error that redirects to `user`;
    a genuine exarchos fork opts in with `allowReservedTier: true`.

### 4. Enforce — DEFAULT `mode: audit`, `mode: check` is opt-in

**Default to `mode: audit`.** You draft the `audit-prompt` from the elicited
rule — a question the review subagent answers against the diff. Audit mode is
pure judgment and always portable:

```yaml
enforcement:
  mode: audit
  audit-prompt: >-
    Does this diff let a request handler return before emitting an audit event?
    Cite the offending file + line.
```

Offer `mode: check` only as an **advanced opt-in** when the rule is mechanically
checkable. If the author opts in, propose a declarative combinator tree over
grep/structural/heuristic leaves and **validate it live via `invariants_add`
with `dryRun: true`** before showing it. See `@references/check-mode.md` for the
combinator vocabulary and the opt-in flow. The enforcement DSL is `.strict()`
and declarative-only: there is no `script` / `exec` / `code` escape hatch
(INV-4), so you cannot emit an executable check even if asked.

### 5. Number — auto-id in the target namespace

Do **not** pick an id. `invariants_add` auto-assigns the next free id in the
target catalog's namespace (`U-N` for user, `INV-N` for dev). Mention the id the
verb assigned when you show the dry-run.

### 6. Commit — dry-run → confirm → write → verify

This step is a **gate, not a formality.** Always:

1. Call `invariants_add` with `dryRun: true` (the default). This renders the
   entry + diff and writes nothing.
2. Show the author the rendered entry and the file diff verbatim. Make the
   confirmation explicit: ask "commit this entry?" — do not silently re-invoke.
3. Only on explicit confirmation, re-invoke `invariants_add` with
   `dryRun: false`. This appends the entry, wires `.exarchos.yml` if the catalog
   is unregistered, and emits `invariant.authored` (+ `catalog.registered` on
   first registration — INV-1).
4. Run `doctor` (the `invariants-catalog` check) to validate the resolved catalog.
5. Show the `invariants_effective` delta so the author sees exactly what the
   gate will now enforce.

If `invariants_add` returns a `CATALOG_NOT_FOUND` error, the target catalog does
not exist yet — run `invariants_scaffold` first (the error's `suggestedFix`
names the call), then resume at step 6.

### 7. Amend — correcting an entry that is already committed

Steps 1–6 author a NEW entry. Correcting one that already exists is a different
verb, and `invariants_add` is not it: it only ever **appends**, and re-running it
with an existing `id` is rejected (`DUPLICATE_INVARIANT_ID`) precisely because
committing that duplicate would author a catalog the loader then refuses to read.

Use `invariants_amend`. It is **id-targeted and field-scoped**:

- `id` names the entry to correct. It is **not** patchable — an entry's identity
  survives an amendment, because every `references:` pointer and audit record
  naming the old id would otherwise go stale. Renaming is not an amendment.
- `patch` names only the top-level fields that change. **Every field the patch
  omits is carried through verbatim** — you do not restate the whole entry, and
  an amendment cannot silently drop a `references` list or an affinity the
  author never mentioned. A named field is replaced *wholesale* (patching
  `enforcement` swaps the entire enforcement block).
- The merged entry is re-validated against the full v3 schema, so an amendment
  can never produce an entry that would have been rejected at authoring time —
  including the `.strict()` enforcement DSL (INV-4).

The gate is the same as step 6: `dryRun: true` first (the default) → show the
author the before/after diff verbatim → explicit confirmation → re-invoke with
`dryRun: false`, which emits `invariant.amended` naming the changed fields →
`doctor` → show the `invariants_effective` delta.

Two refusals worth recognizing:

- `ENTRY_NOT_FOUND` — no entry carries that id. The error's `validTargets` lists
  the ids actually resolved, so pick from those or author a new entry instead.
- `CATALOG_EMPTY` / `CATALOG_UNREADABLE` — the catalog resolved zero entries, or
  its `invariants:` list could not be read at all. Neither is reported as a
  clean "not found": a moved or renamed catalog must not look like a catalog
  that simply lacks your entry. Check you named the right catalog.

## Tool invocations

Scaffold a user catalog (idempotent; never overwrites):

```ts
exarchos:exarchos_orchestrate({
  action: "invariants_scaffold",
  tier: "user",
  path: ".exarchos/invariants.md"
})
```

Dry-run preview (DEFAULT — writes nothing):

```ts
exarchos:exarchos_orchestrate({
  action: "invariants_add",
  tier: "user",
  catalog: ".exarchos/invariants.md",
  entry: { /* the fields from steps 1-4; NO id — auto-assigned */ }
})
```

Commit after explicit confirmation:

```ts
exarchos:exarchos_orchestrate({
  action: "invariants_add",
  tier: "user",
  catalog: ".exarchos/invariants.md",
  entry: { /* same entry */ },
  dryRun: false
})
```

Amend an EXISTING entry — dry-run first (note: only the fields that change;
everything else is carried through):

```ts
exarchos:exarchos_orchestrate({
  action: "invariants_amend",
  tier: "user",
  catalog: ".exarchos/invariants.md",
  id: "U-3",
  patch: { summary: "The corrected one-sentence rule." }
})
```

Commit the amendment after explicit confirmation:

```ts
exarchos:exarchos_orchestrate({
  action: "invariants_amend",
  tier: "user",
  catalog: ".exarchos/invariants.md",
  id: "U-3",
  patch: { summary: "The corrected one-sentence rule." },
  dryRun: false
})
```

Use `exarchos:exarchos_orchestrate({ action: "describe" })` (or the CLI
`--help`) to discover the exact schema at runtime — flags auto-emit from each
action's Zod schema (the CLI is schema-driven; do not assume hand-added flags).

## Worked example

For one `U-*` entry authored end-to-end through all 6 steps, see
`@references/worked-example.md`.

## Anti-Patterns

| Don't | Do Instead |
|-------|------------|
| Hand-write or `Edit` catalog YAML | Route mutations through a verb: `invariants_add` to append a NEW entry, `invariants_amend` to correct an EXISTING one |
| Re-run `invariants_add` with an existing `id` to "update" an entry | `invariants_add` only appends — a colliding `id` is rejected. Use `invariants_amend` |
| Re-author a whole entry to change one field | `invariants_amend` is field-scoped: `patch` names only what changes, everything else survives |
| Rename an entry's `id` | `id` is the catalog's primary key and is not patchable — every reference to the old id would go stale |
| Declare an entry valid yourself | The verb validates; you confirm |
| Default to `mode: check` | Default to `mode: audit`; `check` is opt-in (`@references/check-mode.md`) |
| Skip the dry-run | `dryRun: true` first, ALWAYS, then explicit confirm |
| Silently re-invoke with `dryRun: false` | Make the confirmation step explicit |
| Pick an id by hand | The verb auto-assigns the next free id in the namespace |
| Author into `dev`/`INV-N` from a consumer repo | `dev` is exarchos's reserved substrate namespace — use `user`/`U-N` (the verb rejects consumer `tier: dev`) |
| Infer globs from the framework | Ask the author to name the globs (INV-6) |
| Skip `doctor` + `invariants_effective` after commit | Verify the resolved catalog and show the delta |

