Skip to content

AI Agents

ts-archunit was built to enforce the architecture rules AI coding agents tend to violate — inline logic, generic errors, empty stubs, copy-paste. This page shows how to wire it into an agent's workflow so the agent knows the constraints upfront and self-corrects before it commits, instead of after CI fails.

Everything here rides the CLI you already have — there is no server or plugin to install.

The workflow

  1. Setup (once): author arch.rules.ts with the agent guardrails preset (and any topology rules).
  2. Context (once per repo): generate an imperative rule block into the agent's instructions (CLAUDE.md, .cursorrules, a system prompt, …).
  3. Loop (every edit): the agent runs check --format json in its own terminal after writing code, reads the violations, and fixes them.

1. Setup — agentGuardrails

agentGuardrails bundles the mistakes agents make most often. It returns severity-carrying builders, so spread it into the default export:

typescript
// arch.rules.ts
import { project } from '@nielspeter/ts-archunit'
import { agentGuardrails } from '@nielspeter/ts-archunit/presets'

const p = project('tsconfig.json')

export default [
  ...agentGuardrails(p, {
    src: '**/src/**',
    noInlineLogic: ['parseInt', 'JSON.parse', 'eval'],
    noGenericErrors: true,
    noStubs: true,
    noEmptyBodies: true,
    noCopyPaste: true,
  }),
  // ...your topology rules as builders
]

Each rule carries because / suggestion / imperative metadata, so both the agent-facing rule block and the check output give the agent an actionable fix. no-copy-paste is a warning (reported but non-failing); the rest are errors.

A warning is invisible to an agent: the CLI's exit code counts error-severity findings only, so the loop terminates on exit 0 and the text is never read. If you want copy-paste to block, promote it — no separate API, just an override:

typescript
agentGuardrails(p, {
  src: '**/src/**',
  overrides: { 'preset/agent/no-copy-paste': 'error' },
})

The same works in reverse ('warn', or 'off' to drop the rule entirely), and on any preset rule id. See Architecture Presets.

2. Context — explain --format agent

Generate an imperative rule block and append it to the agent's instructions:

bash
npx ts-archunit explain arch.rules.ts --format agent >> CLAUDE.md

The output is a self-contained block wrapped in sentinel markers. The command writes to stdout, so >> appends — running it twice stacks two blocks. The markers delimit a managed span: to regenerate cleanly, replace the text between <!-- ts-archunit:start --> and <!-- ts-archunit:end --> (your editor or a small sed/awk splice), or generate into a dedicated file the agent reads.

markdown
<!-- ts-archunit:start -->

## Architecture Rules (auto-generated by ts-archunit)

The following rules are enforced by CI. Violations will block your PR.

**Verify before you continue:** after writing or changing code, run
`npx ts-archunit check --format json`, read the `violations` array, and fix each
one using its `suggestion`. Do this in your edit loop — do not wait for CI.

### Preset

- Do NOT call parseInt inline — extract it behind a named helper
- Do NOT throw new Error() — throw a domain-specific error class
- Do NOT leave a function body empty
- Do NOT duplicate a function body — extract the shared logic

<!-- ts-archunit:end -->

The block includes a fixed verify instruction — this is what closes the loop: it tells the agent to run check after each edit rather than waiting for CI.

3. Loop — check --format json

In its edit loop the agent runs:

bash
npx ts-archunit check --format json

which emits a single JSON document the agent parses to self-correct:

jsonc
{
  "summary": { "total": 1, "errors": 1, "warnings": 0, "reason": null },
  "violations": [
    {
      "ruleId": "preset/agent/no-generic-errors",
      "severity": "error",
      "file": "src/services/order.ts",
      "line": 42,
      "message": "…",
      "because": "a generic Error loses the type/context callers need to handle it",
      "suggestion": "throw a domain-specific error (NotFoundError, ValidationError, …)",
      "codeFrame": "  41 |   if (!order) {\n> 42 |     throw new Error('not found')\n  43 |   }",
      "kind": "violation",
    },
  ],
}

severity lets the agent distinguish blocking from advisory; suggestion and codeFrame tell it what to change and where. check exits non-zero only on error-severity violations, so warnings surface without failing the loop.

kind — the field that changes what you do

kind: "configuration" means the rule enforces nothing: its selector matched no subjects, its glob cannot match, it asserts nothing. The code is not the problem, so editing the named file will not clear it — the fix is in the rule definition.

Detect them by kind, never by an empty file. On the CLI most of these carry a real path — the rule file that declared the rule, not the code under test — because check attributes them to their origin so you can find the declaration. When it is non-null, line: 1 is a file-level marker, not a position:

jsonc
{
  "ruleId": "arch/domain-isolation",
  "severity": "error",
  "element": "arch/domain-isolation",
  // The rule FILE, not the code under test. `line: 1` marks the file, not a spot in it.
  "file": "arch.rules.ts",
  "line": 1,
  "message": "Selector matched 0 subjects…",
  "suggestion": "Widen the selector until it matches at least one subject…",
  "kind": "configuration",
}

So: open file, find the rule by ruleId, and edit the rule declaration. Do not anchor an edit to line, and do not look for the problem in your source.

A few of these arrive with "file": null, "line": null — the baseline meta-findings, which are produced after attribution and have no origin to name. Handle both: file is either null or the rule file, and never a location in the code being judged.

Two more things about this kind of finding, both deliberate:

  • It cannot be suppressed — not by .warn(), .asSeverity('warn'), .excluding(), a // ts-archunit-exclude comment, a baseline, or diff-aware mode. A rule that enforces nothing is not something to grade down.
  • It is always error. There is no advisory version.

Composing with topology rules

agentGuardrails enforces how code is written; a topology preset enforces where code goes. They compose in one file:

typescript
export default [
  ...agentGuardrails(p, { src: '**/src/**', noGenericErrors: true }),
  // + layered/boundary rules as builders
]

WARNING

Entries must be un-terminated builders — do not end your own rules with .check() / .warn() (those execute immediately and are silently skipped by the runner). See CLI § rule files.

If you also run the recommended preset, note it overlaps agentGuardrails on empty bodies and eval — use agentGuardrails alone for agent setups, or override the duplicated ids to 'off' in one of them.

Why no MCP server?

An earlier design exposed an MCP server. It was dropped: agents all have a terminal tool and can run check --format json / explain directly, so the server added no capability the CLI lacks. Discoverability is solved by the verify preamble in the agent rule block, which tells the agent to run the command. (If cold-start latency on a large monorepo ever hurts the loop, a CLI daemon — not MCP — is the answer.)

Released under the MIT License.