This package turns the mechanical parts of OpenAI's "Harness engineering" write-up into ast-grep lint rules.
Domain layer order: Types → Config → Repo → Service → Runtime → UI
Documentation alone doesn't keep a fully agent-generated codebase coherent. By enforcing invariants, not micromanaging implementations, we let agents ship fast without undermining the foundation. For example, we require Codex to parse data shapes at the boundary, but are not prescriptive on how that happens. The model seems to like Zod, but we didn't specify that specific library.
Agents are most effective in environments with strict boundaries and predictable structure, so we built the application around a rigid architectural model. Each business domain is divided into a fixed set of layers, with strictly validated dependency directions and a limited set of permissible edges. These constraints are enforced mechanically via custom linters (Codex-generated, of course!) and structural tests.
The diagram below shows the rule: within each business domain (e.g. App Settings), code can only depend “forward” through a fixed set of layers (Types → Config → Repo → Service → Runtime → UI). Cross-cutting concerns (auth, connectors, telemetry, feature flags) enter through a single explicit interface: Providers. Anything else is disallowed and enforced mechanically.
The rules focus on invariants that are realistic for ast-grep to enforce:
| Principle | Rules |
|---|---|
| Strict domain layers | harness-no-forward-layer-import-from-* |
| Cross-cutting services enter through Providers | harness-cross-cutting-through-providers-* |
| Parse data shapes at boundaries | harness-no-raw-boundary-json-*, harness-no-direct-request-body-field-* |
| Structured logging | harness-no-console-logging-* |
| Schema/type naming conventions | harness-zod-schema-name-suffix-* |
| Platform reliability through explicit config | harness-no-direct-process-env-* |
| Garbage collection of ad-hoc helpers | harness-no-local-concurrency-primitive-*, harness-no-p-limit-import-* |
The rules are written for TypeScript and JavaScript packages. JavaScript
path-sensitive rules cover .js, .jsx, .mjs, and .cjs.
npm install
npm run scanTo scan only the included examples:
npm run scan:fixturesThe fixture scan is expected to fail because
tests/fixtures/packages/domains/billing/** contains intentionally bad
examples. Test fixtures live under tests/fixtures so the package root mirrors
the repository layout being enforced.
The architecture rules assume a Turborepo-style package workspace with domain
code under packages/domains:
apps/<app>/**
packages/domains/<domain>/types/**
packages/domains/<domain>/config/**
packages/domains/<domain>/repo/**
packages/domains/<domain>/service/**
packages/domains/<domain>/runtime/**
packages/domains/<domain>/ui/**
packages/domains/<domain>/providers/**
packages/shared*/**
packages/tooling*/**
Layer imports are allowed to point toward more foundational layers only: Types → Config → Repo → Service → Runtime → UI.
The rules intentionally target packages/**; app-specific apps/** checks can
be added separately if an app needs its own architecture policy. If your
repository uses different names, update the files globs and the source-path
regexes in rules/harness/architecture-layers.yml.
Direct process.env reads are allowed in package config modules. TypeScript
also keeps common *.config.ts(x) and env.ts(x) carve-outs. JavaScript does
not ignore *.config.js or env.js; use a package config module when JS code
needs direct environment access.
The data-boundary rules are heuristics. They intentionally flag raw JSON and request body access, then tell the agent how to remediate: parse the external shape at the boundary with a schema or typed SDK and pass typed values inward.
The article also mentions file size limits, doc freshness, quality grades, and recurring cleanup agents. ast-grep is the right tool for structural code patterns, but those checks need companion scripts because they depend on line counts, timestamps, links, ownership metadata, or generated reports.
