# Archgate CLI > Archgate is a CLI for enterprise-grade linting and guardrails for AI work, built on Architecture Decision Records (ADRs). It combines human-readable documentation with machine-checkable TypeScript rules to enforce architectural decisions across codebases, for both humans and AI agents. Archgate lets teams write an ADR once and enforce it everywhere. ADRs are Markdown files with YAML frontmatter that describe architectural decisions. Companion `.rules.ts` files contain automated TypeScript checks that run against the codebase and report violations, with file paths and line numbers when rules provide them. ## Key capabilities - **Executable rules**: Write lint rules in TypeScript. Archgate runs them against your codebase and reports violations, with file paths and line numbers when rules provide them. - **CI integration**: Wire `archgate check` into any CI/CD pipeline. Exit code 1 blocks merges when rules are violated. - **AI-aware guardrails**: Editor plugins give AI agents (Claude, Cursor, Copilot) live access to ADRs. Agents read decisions before writing code and validate after. - **Editor plugins**: Claude Code, VS Code, Cursor, and GitHub Copilot plugins give AI agents role-based enforcement skills. - **Checks itself**: Archgate enforces its own rules on its own codebase using the same tool. ## Installation Install standalone (no Node.js required): `curl -fsSL https://raw.githubusercontent.com/archgate/cli/main/install.sh | sh` (macOS/Linux) or `irm https://raw.githubusercontent.com/archgate/cli/main/install.ps1 | iex` (Windows PowerShell). Also available via npm (`npm install -g archgate`) or direct download from GitHub Releases. ## Documentation - [Getting Started: Installation](https://cli.archgate.dev/getting-started/installation/): Install on macOS, Linux, or Windows via npm, Homebrew, or standalone binary. - [Getting Started: Quick Start](https://cli.archgate.dev/getting-started/quick-start/): Set up Archgate in under 5 minutes with your first ADR and rule. - [Core Concepts: ADRs](https://cli.archgate.dev/concepts/adrs/): How Architecture Decision Records work as both documentation and executable rules. - [Core Concepts: Rules](https://cli.archgate.dev/concepts/rules/): The TypeScript rule system, the lint step that turns ADR decisions into executable checks. - [Core Concepts: Domains](https://cli.archgate.dev/concepts/domains/): Organize ADRs by domain to scope and enforce rules. - [Guide: Writing ADRs](https://cli.archgate.dev/guides/writing-adrs/): Complete guide to writing effective ADRs with YAML frontmatter and markdown structure. - [Guide: Writing Rules](https://cli.archgate.dev/guides/writing-rules/): Write TypeScript rules using the satisfies RuleSet pattern with file matching and violation reporting. - [Guide: CI Integration](https://cli.archgate.dev/guides/ci-integration/): Add Archgate checks to GitHub Actions, GitLab CI, or any pipeline. - [Guide: Claude Code Plugin](https://cli.archgate.dev/guides/claude-code-plugin/): Give AI agents a guardrails workflow that reads ADRs, validates code, and captures patterns. - [Guide: VS Code Plugin](https://cli.archgate.dev/guides/vscode-plugin/): Real-time ADR compliance in VS Code. - [Guide: GitHub Copilot Plugin](https://cli.archgate.dev/guides/copilot-cli-plugin/): Add architecture guardrails to the GitHub Copilot CLI and desktop app. - [Guide: Cursor Integration](https://cli.archgate.dev/guides/cursor-integration/): Configure Cursor IDE with Archgate agent rules and skills. - [Guide: Pre-commit Hooks](https://cli.archgate.dev/guides/pre-commit-hooks/): Automatically check ADR compliance before every commit. - [Reference: CLI Commands](https://cli.archgate.dev/reference/cli-commands/): Complete reference for init, check, adr create/list/show, login, and more. - [Reference: Rule API](https://cli.archgate.dev/reference/rule-api/): TypeScript API reference for RuleSet with satisfies, RuleContext, and violation reporting. - [Reference: ADR Schema](https://cli.archgate.dev/reference/adr-schema/): YAML frontmatter schema and markdown structure reference for ADRs. - [Examples: Common Rule Patterns](https://cli.archgate.dev/examples/common-rule-patterns/): Ready-to-use rule patterns for naming conventions, import restrictions, and more. ## Full documentation For the complete documentation in a single file, see [llms-full.txt](https://cli.archgate.dev/llms-full.txt). ## Optional - [GitHub Repository](https://github.com/archgate/cli) - [Editor Plugin Beta](https://plugins.archgate.dev) - [npm Package](https://www.npmjs.com/package/archgate) --- # Full documentation Below is the complete English documentation for Archgate CLI. ## Archgate Source: https://cli.archgate.dev/ AI agents write code fast, but they don't know your rules. Archgate turns your team's decisions into executable checks: a lint step for architecture, conventions, and AI-generated code. Your agents read the rules before writing code, and `archgate check` blocks what slips through. In CI, in pre-commit hooks, and inside every major AI coding tool. ## How it works Archgate has two layers that work together: 1. **ADRs as documents**: Markdown files with YAML frontmatter that describe architectural decisions in plain language. Humans read them. AI agents read them. Everyone stays aligned. 2. **ADRs as rules**: Companion `.rules.ts` files with automated checks written in TypeScript. They run against your codebase and report violations with file paths and line numbers. When you run `archgate check`, the CLI loads every ADR that has `rules: true` in its frontmatter, executes the companion rules file, and reports any violations. Exit code 0 means your code complies. Exit code 1 means it does not. The loop does not stop at detection. Every mistake can become a new rule or ADR, and once it does, your AI never repeats it. Guardrails get tighter over time instead of more expensive. ## Key Features Write rules in TypeScript. Archgate runs them against your codebase and reports violations with file paths and line numbers. Rules live next to the decisions they enforce. Wire `archgate check` into your pipeline. Exit code 1 blocks merges when rules are violated. Works with GitHub Actions, GitLab CI, or any CI system that respects exit codes. Editor plugins give AI agents direct access to your ADRs via CLI commands. They read decisions before writing code and validate after. No copy-pasting rules into prompts. Archgate enforces its own rules on its own codebase. The same tool that checks your code checks ours. The rules paired with our own ADRs enforce command structure, error handling, output formatting, testing, and more. [See them on GitHub](https://github.com/archgate/cli/tree/main/.archgate/adrs). ## Editor plugins The Archgate CLI works standalone, but **editor plugins** unlock a full AI guardrails workflow. Plugins give AI agents role-based skills so they read your ADRs before coding, validate after, and capture new patterns for your team -- automatically. The Claude Code plugin adds a developer agent plus five skills: reviewer, lessons-learned, adr-author, onboard, and cli-reference. The developer agent follows a structured read-validate-capture loop on every task. The Cursor plugin provides pre-built agent rules and skills that give Cursor's AI agent the same guardrails workflow as Claude Code. Editor plugins are currently in beta. Run `archgate login` to sign up and authenticate, then `archgate init --install-plugin` to set up the plugin. ## Learn more --- ## Getting Started: Installation Source: https://cli.archgate.dev/getting-started/installation/ ## Install standalone (recommended) The fastest way to install Archgate. No Node.js or package manager required: ```bash # macOS / Linux curl -fsSL https://cli.archgate.dev/install-unix | sh # Windows (PowerShell) irm https://cli.archgate.dev/install-windows | iex # Windows (Git Bash / MSYS2) curl -fsSL https://cli.archgate.dev/install-unix | sh ``` This downloads a pre-built binary for your platform and installs it to `~/.archgate/bin/`. The installer detects your shell profiles and offers to add the directory to your PATH. On Windows, the PowerShell installer also detects Git Bash shell profiles (`.bashrc`, `.bash_profile`, `.profile`) and offers to configure PATH there as well. You can customize the install with environment variables: | Variable | Description | Default | | ---------------------- | ------------------------------------------- | ----------------- | | `ARCHGATE_VERSION` | Install a specific version (e.g. `v0.11.2`) | Latest release | | `ARCHGATE_INSTALL_DIR` | Custom install directory | `~/.archgate/bin` | You can also download binaries directly from [GitHub Releases](https://github.com/archgate/cli/releases). ## Install via winget (Windows) `Archgate.Archgate` is queued for acceptance into the [winget community repository](https://github.com/microsoft/winget-pkgs). Until it is published there, this command returns "No package found" — use the standalone installer above in the meantime. On Windows, install Archgate with the [Windows Package Manager](https://learn.microsoft.com/windows/package-manager/): ```powershell winget install Archgate.Archgate ``` This installs a small portable wrapper and puts `archgate` on your PATH. On first run it downloads the platform binary, verifies its checksum, and caches it to `~/.archgate/bin/` — the same cache every other install method uses, so switching between them re-downloads nothing. Upgrade with `winget upgrade Archgate.Archgate`. ## Install via npm Install Archgate globally using your preferred Node.js package manager: ```bash # npm npm install -g archgate # Bun bun install -g archgate # Yarn yarn global add archgate # pnpm pnpm add -g archgate ``` This installs a lightweight wrapper that delegates to a platform-specific binary. The CLI itself is a standalone binary compiled with Bun. Node.js is only needed for the npm/yarn/pnpm wrapper. ## Install as a dev dependency You can also add Archgate as a dev dependency in your project and run it through your package manager's script runner. This is useful for pinning a specific version per project or running checks in CI without a global install. ```bash # npm npm install -D archgate # Bun bun add -d archgate # Yarn yarn add -D archgate # pnpm pnpm add -D archgate ``` Then run Archgate via your package manager: ```bash # npm / Yarn / pnpm npx archgate check # Bun bun run archgate check ``` Or add a script to your `package.json`: ```json { "scripts": { "check:adrs": "archgate check" } } ``` ```bash # Works with any package manager npm run check:adrs bun run check:adrs yarn check:adrs pnpm check:adrs ``` ## Install via pip (Python) Install Archgate globally using pip or pipx: ```bash # pip pip install archgate # pipx (recommended for CLI tools) pipx install archgate ``` This installs a lightweight Python wrapper that delegates to a platform-specific binary. Python 3.8+ is required. ## Install via dotnet Install Archgate as a .NET global tool: ```bash dotnet tool install -g archgate ``` Requires .NET 8.0+ SDK. The tool downloads the platform binary on first run. ## Install via Go Install Archgate using `go install`: ```bash go install github.com/archgate/cli/shims/go/cmd/archgate@latest ``` Requires Go 1.21+. The compiled Go wrapper downloads the platform binary on first run. ## Install via RubyGems Install Archgate as a Ruby gem: ```bash gem install archgate ``` Requires Ruby 2.7+. The gem downloads the platform binary on first run. ## Install via Maven / jbang (Java) Install Archgate using jbang: ```bash jbang app install archgate@dev.archgate ``` Or download the latest executable JAR from [Maven Central](https://central.sonatype.com/artifact/dev.archgate/archgate-cli) (`dev.archgate:archgate-cli`) and run it directly: ```bash java -jar archgate-cli-.jar check ``` Requires Java 11+. The shim downloads the platform binary on first run. ## Platform support Archgate ships pre-built binaries for the following platforms: | Platform | Architecture | Artifact | | -------- | ------------ | ----------------------- | | macOS | arm64 | `archgate-darwin-arm64` | | Linux | x86_64 | `archgate-linux-x64` | | Windows | x86_64 | `archgate-win32-x64` | The correct binary is downloaded automatically from GitHub Releases on first run and cached to `~/.archgate/bin/`. ## Verify installation ```bash archgate --version ``` You should see the installed version printed to stdout. ## Install via proto If you use [proto](https://moonrepo.dev/proto) (moonrepo's toolchain manager), you can install Archgate directly as a proto plugin. No Node.js or npm required. Add the plugin to your `.prototools`: ```toml [plugins.tools] archgate = "github://archgate/proto-plugin" ``` Then install and use it like any other proto tool: ```bash proto install archgate archgate check ``` Proto manages the binary for you, including version pinning and auto-installation. To pin a specific version, add it at the root of `.prototools`: ```toml archgate = "0.15.0" [plugins.tools] archgate = "github://archgate/proto-plugin" ``` You can also list available versions and manage installations with proto commands: ```bash proto list-remote archgate # list available versions proto install archgate 0.15.0 # install a specific version proto pin archgate 0.15.0 # pin version in .prototools ``` If you prefer `npm install -g archgate` instead of the proto plugin, you need to configure proto to expose global npm binaries. Add `shared-globals-dir = true` under `[tools.npm]` in `~/.proto/config.toml`, then add `$HOME/.proto/tools/node/globals/bin` to your shell PATH. ## Next steps Once installed, run `archgate init` in your project to set up linting and guardrails. See the [Quick Start](/getting-started/quick-start/) guide for a walkthrough. Want your AI agent to read ADRs before coding and validate after? The editor plugins for [Claude Code](/guides/claude-code-plugin/) and [Cursor](/guides/cursor-integration/) add a full guardrails workflow on top of the CLI. Run `archgate login` to sign up and get started. --- ## Getting Started: Quick Start Source: https://cli.archgate.dev/getting-started/quick-start/ ## 1. Install Archgate If you have not installed the CLI yet: ```bash # Standalone (no Node.js required) curl -fsSL https://cli.archgate.dev/install-unix | sh # Or via npm npm install -g archgate ``` See the [Installation](/getting-started/installation/) page for all options, including Windows and custom install directories. ## 2. Initialize your project Navigate to your project root and run the `init` command: ```bash cd my-project archgate init ``` This creates the `.archgate/` directory with the following structure: ``` .archgate/ adrs/ GEN-001-example.md # Example ADR (rules: false) lint/ README.md # Conventions for linter-specific rules rules.d.ts # Type definitions for .rules.ts files ``` The generated files give you a working example to build on. ## 3. Edit the example ADR Open `.archgate/adrs/GEN-001-example.md`. Every ADR starts with YAML frontmatter that defines its identity: ```yaml --- id: GEN-001 title: Example Architecture Decision domain: general rules: false --- ``` - **id**: Unique identifier. Convention is `-NNN` (e.g. `ARCH-001`, `GEN-001`) but any string works. - **title**: Human-readable name for the decision. - **domain**: Groups related ADRs together (`architecture`, `backend`, `frontend`, `data`, or `general`). - **rules**: Set to `true` if this ADR has a companion `.rules.ts` file with automated checks. The generated example ships with `rules: false`. - **files**: Optional glob patterns that scope which files the rules apply to. Omit it to scope the whole project. Below the frontmatter, write the decision in Markdown. Archgate does not enforce a specific section structure, but the recommended sections are: Context, Decision, Do's and Don'ts, Consequences, Compliance, and References. ## 4. Add a companion rules file Create a `.rules.ts` file next to your ADR with the same name prefix (e.g. `GEN-001-example.rules.ts` for the generated example). Then set `rules: true` in the ADR's frontmatter — `archgate check` only loads a companion rules file when the ADR opts in. Rules are written in TypeScript using the `RuleSet` type: ```typescript /// export default { rules: { "no-console-error": { description: "Use logError() instead of console.error()", async check(ctx) { for (const file of ctx.scopedFiles) { const matches = await ctx.grep(file, /console\.error\(/); for (const match of matches) { ctx.report.violation({ message: "Use logError() instead of console.error()", file: match.file, line: match.line, fix: "Import logError from your helpers and use it instead", }); } } }, }, }, } satisfies RuleSet; ``` Each rule has a unique key, a description, and an async `check` function. Inside `check`, you have access to: - **`ctx.scopedFiles`**: Files matching the ADR's `files` glob patterns. - **`ctx.grep(file, pattern)`**: Search a file for regex matches, returning file paths and line numbers. - **`ctx.report.violation()`**: Report a violation with a message, file path, line number, and optional fix suggestion. ## 5. Run checks Run the compliance checker against your codebase: ```bash archgate check ``` Archgate loads every ADR with `rules: true`, executes its companion rules file, and prints results. The exit code tells you the outcome: | Exit code | Meaning | | --------- | --------------------------------------------- | | 0 | All rules pass. No violations found. | | 1 | One or more violations detected. | | 2 | Internal error (e.g., malformed ADR or rule). | To check only staged files (useful in pre-commit hooks or CI): ```bash archgate check --staged ``` ## What's next? Now that you have a working setup, dive deeper: **Understand the concepts:** - [ADRs](/concepts/adrs/): What Architecture Decision Records are and how Archgate uses them. - [Rules](/concepts/rules/): How companion `.rules.ts` files turn decisions into automated checks. - [Domains](/concepts/domains/): How domains group related ADRs and scope file matching. **Write your own:** - [Writing ADRs](/guides/writing-adrs/): Learn the full ADR format and best practices for writing effective decisions. - [Writing Rules](/guides/writing-rules/): Explore the rule API, advanced patterns, and how to test your rules. - [Common Rule Patterns](/examples/common-rule-patterns/): Copy-pasteable patterns for dependency checks, naming conventions, and more. **Integrate into your workflow:** - [CI Integration](/guides/ci-integration/): Wire `archgate check` into GitHub Actions, GitLab CI, or any pipeline. - [Pre-commit Hooks](/guides/pre-commit-hooks/): Run checks locally before every commit. - [Claude Code Plugin](/guides/claude-code-plugin/): Give AI agents architecture-aware guardrails with role-based skills. - [Cursor Integration](/guides/cursor-integration/): Use Archgate with Cursor IDE for AI-assisted development. Want AI agents that automatically read your ADRs before coding? Run `archgate login` to sign up and authenticate, then run `archgate init --install-plugin` to set up the plugin. --- ## Core Concepts: Architecture Decision Records Source: https://cli.archgate.dev/concepts/adrs/ An Architecture Decision Record (ADR) is a short document that captures a single architectural decision along with its context and consequences. ADRs answer the question: _why_ was this decision made, and _what_ are its trade-offs? Archgate builds on the ADR concept by giving each decision two expressions: a **document** that humans and AI agents read, and an optional **rules file** that machines execute. The document is the guardrail your agents follow before they write. The rules file is the check that blocks what slips through. ## Two Expressions of an ADR ### ADR as Document The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/`. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. Both humans and AI agents consume this document. When an AI coding agent is about to write code, it reads the relevant ADRs to understand the constraints before generating anything. With the [Claude Code](/guides/claude-code-plugin/) or [Cursor](/guides/cursor-integration/) plugin, your AI agent reads the applicable ADRs automatically before every coding task -- no manual copy-pasting into prompts. [Sign up for beta access](https://plugins.archgate.dev). ### ADR as Rules The rules file is a companion `.rules.ts` file that exports a plain object typed with `satisfies RuleSet`. When you run `archgate check`, the CLI loads every ADR that has `rules: true` in its frontmatter, executes the companion rules file against your codebase, and reports any violations with file paths and line numbers. Not every ADR needs rules. Some decisions are best enforced through code review alone. Set `rules: false` when no automated check is practical. ## File Naming Convention ADR files follow a strict naming convention that encodes the domain prefix, sequence number, and a human-readable slug: ``` {PREFIX}-{NNN}-{slug}.md # The document {PREFIX}-{NNN}-{slug}.rules.ts # The companion rules file (optional) ``` For example, an architecture-domain ADR about command structure would produce: ``` ARCH-001-command-structure.md ARCH-001-command-structure.rules.ts ``` The prefix comes from the ADR's domain (see [Domains](/concepts/domains/)). The sequence number is zero-padded to three digits and auto-incremented by `archgate adr create`. ## YAML Frontmatter Every ADR document starts with a YAML frontmatter block between `---` delimiters. The frontmatter is the machine-readable metadata that Archgate uses to load, filter, and scope rules. | Field | Type | Required | Description | | ------------------ | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Yes | Unique identifier like `ARCH-001` or `BE-003` | | `title` | string | Yes | Human-readable title of the decision | | `domain` | string | Yes | Registered domain name. Built-ins: `backend`, `frontend`, `data`, `architecture`, `general`. [Custom domains](/concepts/domains/#custom-domains) can be added via `archgate adr domain add`. | | `rules` | boolean | Yes | Whether this ADR has a companion `.rules.ts` file | | `files` | string array | No | Glob patterns that scope which files the rules check | | `respectGitignore` | boolean | No | Whether to filter out `.gitignore`d files. Defaults to `true`. | The `files` field is optional. When present, it restricts rule execution to only the files matching the given globs. When absent, rules run against all project files. For example, `files: ["src/commands/**/*.ts"]` limits checks to command files only. The `respectGitignore` field is also optional. By default, files listed in `.gitignore` are excluded from all file-scanning operations (`ctx.scopedFiles`, `ctx.glob()`, `ctx.grepFiles()`). Set `respectGitignore: false` to include gitignored files -- useful for rules that need to inspect build output or generated files. ## ADR Body Sections After the frontmatter, the ADR body follows a standard section structure: ### Context Describes the problem or situation that prompted the decision. Include alternatives that were considered and why they were rejected. ### Decision States the decision itself and its key constraints. This is the section AI agents pay the most attention to when deciding how to write code. ### Do's and Don'ts Concrete, actionable guidance split into two sub-sections. These act as a quick-reference checklist for developers and AI agents. ### Consequences Split into three sub-sections: - **Positive** -- benefits the decision provides - **Negative** -- trade-offs accepted - **Risks** -- things that could go wrong and how to mitigate them ### Compliance and Enforcement Describes how the decision is enforced, both through automated rules (with rule IDs and severities) and manual review checklists. ### References Links to related ADRs, external documentation, or design documents. ## Complete Example Below is a full ADR with frontmatter and all sections filled in. ```markdown --- id: BE-001 title: API Response Envelope domain: backend rules: true files: ["src/api/**/*.ts"] --- ## Context The API returns data in inconsistent shapes across endpoints. Some endpoints wrap responses in `{ data, error }`, others return raw arrays, and error responses vary between plain strings and structured objects. **Alternatives considered:** - **No envelope** -- Return raw data and rely on HTTP status codes alone. Simple, but clients cannot distinguish between "the endpoint returned an empty array" and "the endpoint errored." - **GraphQL-style errors array** -- Use `{ data, errors: [] }`. Flexible but adds complexity for simple REST endpoints. The chosen envelope balances consistency with simplicity. ## Decision All API endpoints MUST return responses in a standard envelope: - Success: `{ data: T }` - Error: `{ error: { code: string, message: string } }` HTTP status codes remain the primary success/failure signal. The envelope provides a predictable structure for clients to parse. ## Do's and Don'ts ### Do - Wrap all API responses in the `{ data }` or `{ error }` envelope - Use specific error codes (e.g., `VALIDATION_FAILED`, `NOT_FOUND`) - Include the HTTP status code that matches the error semantics ### Don't - Don't return raw arrays or primitives from API endpoints - Don't nest envelopes (no `{ data: { data: ... } }`) - Don't put stack traces in the error message field ## Consequences ### Positive - Clients can parse every response with the same logic - Error responses always have a machine-readable code for programmatic handling ### Negative - Adds a small amount of boilerplate to every endpoint handler - Slightly larger payloads due to the wrapper object ### Risks - Developers may forget the envelope on new endpoints. Mitigated by the automated rule that scans for non-conforming return statements. ## Compliance and Enforcement ### Automated Enforcement - **Archgate rule** BE-001/response-envelope: Scans API handler files for return statements and verifies they use the envelope helper. Severity: error. ### Manual Enforcement Code reviewers MUST verify: 1. New API endpoints use the response envelope 2. Error responses include a specific error code, not a generic message ## References - [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines) - [ARCH-002 -- Error Handling](./ARCH-002-error-handling.md) ``` --- ## Core Concepts: Domains Source: https://cli.archgate.dev/concepts/domains/ Domains are categories that group related ADRs together. Every ADR belongs to exactly one domain, and the domain determines the prefix used in the ADR's identifier. ## Built-in Domains Archgate ships with five built-in domains. Each has a short prefix that appears at the start of every ADR ID in that domain. | Domain | Prefix | Use for | | -------------- | ------ | --------------------------------------------------- | | `backend` | `BE` | Server-side logic, APIs, databases, services | | `frontend` | `FE` | UI components, client-side logic, styling patterns | | `data` | `DATA` | Data models, schemas, pipelines, storage strategies | | `architecture` | `ARCH` | Cross-cutting architectural decisions | | `general` | `GEN` | General project conventions and workflows | For example, the third backend ADR would have the ID `BE-003`, and a first frontend ADR would be `FE-001`. ## How Domains Are Used ### ADR Identification The domain prefix is baked into every ADR's `id` field. When you run `archgate adr create` and select a domain, the CLI automatically determines the next available sequence number for that domain's prefix. An architecture domain with two existing ADRs (`ARCH-001`, `ARCH-002`) would assign `ARCH-003` to the next one. The file name mirrors the ID: ``` ARCH-003-dependency-policy.md ARCH-003-dependency-policy.rules.ts ``` ### Filtering The `archgate adr list` command supports a `--domain` flag to show only ADRs from a specific domain: ```bash archgate adr list --domain backend archgate adr list --domain architecture ``` This is useful in large projects where dozens of ADRs span multiple concerns. Filtering by domain lets you focus on the decisions relevant to your current work. ### AI Agent Context The `archgate review-context` command groups changed files by domain when providing context to AI agents. When an agent is about to write code, it receives only the ADR briefings relevant to the domains its changes touch, rather than the full set of all ADRs. This scoping reduces noise and helps agents focus on the constraints that actually apply. ### Scoped Validation While domains themselves do not restrict which files a rule can check (that is the job of the `files` glob in the ADR frontmatter), domains provide a logical grouping that helps teams organize their governance. A backend team can review all `BE-*` ADRs to understand their constraints, while the frontend team focuses on `FE-*`. ## When to Use Which Domain ### backend Use for decisions about server-side code: API design patterns, database access conventions, authentication flows, service-to-service communication, queue handling, and background job patterns. **Example ADRs:** API response envelope format, database migration strategy, error code taxonomy. ### frontend Use for decisions about client-side code: component structure, state management patterns, styling approaches, accessibility requirements, and build tooling choices. **Example ADRs:** Component file structure, CSS methodology, form validation pattern. ### data Use for decisions about data: schema design, data pipeline conventions, storage engine choices, serialization formats, and data validation strategies. **Example ADRs:** Event schema versioning, database naming conventions, data retention policy. ### architecture Use for cross-cutting decisions that span multiple domains or affect the project's overall structure. These are decisions that backend, frontend, and data teams all need to follow. **Example ADRs:** Command structure, error handling conventions, dependency management policy, testing standards. ### general Use for project-wide conventions that do not fit neatly into a technical domain: code review processes, commit message formats, documentation standards, and onboarding practices. **Example ADRs:** Commit message format, PR description template, documentation requirements. ## Choosing the Right Domain When deciding which domain an ADR belongs to, consider who needs to follow it: - If only backend developers need to follow it, use `backend`. - If only frontend developers need to follow it, use `frontend`. - If it concerns data modeling or pipelines specifically, use `data`. - If it applies across multiple technical domains, use `architecture`. - If it is a process or convention rather than a technical decision, use `general`. When in doubt between `architecture` and a specific domain, prefer the more specific domain. Reserve `architecture` for decisions that genuinely cut across boundaries. ## Custom Domains When the built-in five are a genuine mismatch for a category of decisions (for example, `security`, `ml-ops`, or `compliance`), you can register a custom domain via the CLI: ```bash # See what's currently recognised in this project archgate adr domain list # Register a new domain with its ID prefix archgate adr domain add security SEC # Remove a custom domain (built-ins cannot be removed) archgate adr domain remove security ``` Custom domain-to-prefix mappings persist in [`.archgate/config.json`](/reference/configuration/) and are merged with the built-ins at read time. A registered custom domain behaves exactly like a built-in: `archgate adr create --domain security` auto-generates IDs like `SEC-001`, and `archgate adr list --domain security` filters to those ADRs. ### Naming rules - **Name**: lowercase kebab-case, 2–32 characters (e.g., `security`, `ml-ops`, `compliance`). - **Prefix**: uppercase letters, digits, or underscores, 2–10 characters (e.g., `SEC`, `MLOPS`, `COMP`). - Custom names and prefixes cannot collide with built-ins or any other custom entry. ### When to prefer a built-in The built-in five are deliberately opinionated. Before registering a custom domain, check whether the decision can be folded under an existing one: - A decision about auth middleware usually fits under `backend`, even if the motivation is security. - A decision about schema versioning usually fits under `data`, even if the motivation is compliance. - A decision that spans multiple technical areas usually fits under `architecture`. Reach for a custom domain only when none of the built-ins is a genuine fit: for example, when you have a dedicated team or compliance regime that needs its own governance surface. ### AI agent guidance When using the Archgate editor plugin to author ADRs, agents are instructed to default to the built-in domains and to ask before introducing a custom one. They'll surface the merged list via `archgate adr domain list` and only register a new domain after confirming with you that no built-in fits. --- ## Core Concepts: Rules Source: https://cli.archgate.dev/concepts/rules/ Rules are the executable side of an ADR. They live in companion `.rules.ts` files alongside the ADR document and export a plain object typed with `satisfies RuleSet`. When you run `archgate check`, the CLI loads each ADR that has `rules: true`, imports its companion rules file, and executes every check against your codebase. This pairing is what makes Archgate the lint step for AI work: every check traces back to a documented decision, and violations point to the file and line where they occurred, when rules provide them. ## Defining Rules A rules file is a TypeScript module that default-exports a plain object conforming to the `RuleSet` type. The type is provided by the local shim auto-generated by `archgate init` (no npm install needed): ```typescript /// export default { rules: { "rule-key": { description: "What this rule checks", severity: "error", async check(ctx) { // Inspect files and report violations }, }, }, } satisfies RuleSet; ``` Each key in the `rules` object becomes the rule ID. The full rule identifier shown in check output combines the ADR ID and the rule key, for example `ARCH-004/no-barrel-files`. ## Rule Structure Every rule has three parts: | Property | Type | Required | Description | | ------------- | -------- | -------- | --------------------------------------------- | | `description` | string | Yes | A short summary of what the rule checks | | `severity` | string | No | `"error"` (default), `"warning"`, or `"info"` | | `check` | function | Yes | Async function receiving a `RuleContext` | ### Severity Levels Severity determines what happens when a rule finds a problem: | Severity | Exit Code | Effect | | --------- | --------- | ----------------------------------------- | | `error` | 1 | Violation is reported and the check fails | | `warning` | 0 | Warning is logged but the check passes | | `info` | 0 | Informational message, check passes | When `archgate check` runs without `--strict`, exit code 1 means at least one `error`-severity violation was found. Exit code 0 means no errors (warnings and info messages are logged but do not block). Under `--strict`, warnings and certain advisory findings can also produce exit code 1 -- see [Configuration -- `strict`](/reference/configuration/#strict). ## The RuleContext The `check` function receives a `RuleContext` object that provides everything a rule needs to inspect the codebase and report findings. ### Project Information | Property | Type | Description | | ------------------ | ---------- | ----------------------------------------------------------------------------------- | | `ctx.projectRoot` | `string` | Absolute path to the project root directory | | `ctx.scopedFiles` | `string[]` | Files matching the ADR's `files` globs, or all project files if no globs are set | | `ctx.changedFiles` | `string[]` | Files changed in git (branch diff plus uncommitted changes, or `--staged`/`--base`) | ### File Operations | Method | Returns | Description | | -------------------- | ------------------- | ---------------------------------- | | `ctx.glob(pattern)` | `Promise` | Find files matching a glob pattern | | `ctx.readFile(path)` | `Promise` | Read a file's content as a string | | `ctx.readJSON(path)` | `Promise` | Read and parse a JSON file | ### Search Operations | Method | Returns | Description | | ---------------------------------- | ---------------------- | -------------------------------------------- | | `ctx.grep(file, pattern)` | `Promise` | Search a single file with a regex pattern | | `ctx.grepFiles(pattern, fileGlob)` | `Promise` | Search across multiple files matching a glob | Both `grep` and `grepFiles` return an array of `GrepMatch` objects: ```typescript interface GrepMatch { file: string; // Relative path from project root line: number; // 1-based line number column: number; // 1-based column number content: string; // The full line content } ``` ### Reporting The `ctx.report` object provides three methods for reporting findings: ```typescript ctx.report.violation({ message, file?, line?, fix? }); ctx.report.warning({ message, file?, line?, fix? }); ctx.report.info({ message, file?, line?, fix? }); ``` Each method accepts an object with: | Property | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------ | | `message` | string | Yes | What the problem is | | `file` | string | No | Relative path to the offending file | | `line` | number | No | Line number where the problem occurs | | `fix` | string | No | Suggested fix for the violation | Use `ctx.report.violation()` for problems that must block merges. Use `ctx.report.warning()` for issues worth flagging but not blocking. Use `ctx.report.info()` for purely informational output. ## Rule Timeout Each rule has a 30-second execution timeout. If a rule's `check` function does not complete within 30 seconds, it is terminated and reported as an error. This prevents runaway rules from blocking the pipeline indefinitely. ## Complete Example Here is a complete rules file that checks for a banned import pattern. It enforces that no source file imports directly from `node:fs` (the project requires using a wrapper instead). ```typescript /// export default { rules: { "no-direct-fs-import": { description: "Source files must not import directly from node:fs; use the fs wrapper", severity: "error", async check(ctx) { const sourceFiles = ctx.scopedFiles.filter( (f) => f.endsWith(".ts") && !f.endsWith(".test.ts") ); for (const file of sourceFiles) { const matches = await ctx.grep(file, /from ["']node:fs["']/); for (const match of matches) { ctx.report.violation({ message: `Direct import from "node:fs" is not allowed. Use the fs wrapper from "src/helpers/fs" instead.`, file: match.file, line: match.line, fix: 'Replace the import with: import { readFile, writeFile } from "../helpers/fs"', }); } } }, }, }, } satisfies RuleSet; ``` When this rule runs against a file containing `import { readFileSync } from "node:fs"`, the output looks like: ``` ARCH-007/no-direct-fs-import ERROR src/services/config.ts:3 Direct import from "node:fs" is not allowed. Use the fs wrapper from "src/helpers/fs" instead. Fix: Replace the import with: import { readFile, writeFile } from "../helpers/fs" ``` ## Execution Model Rules execute with the following guarantees: - **Parallel across ADRs** -- Rules from different ADRs run concurrently for faster execution. - **Sequential within an ADR** -- Rules belonging to the same ADR run one after another, so earlier rules can establish context for later ones. - **Scoped files are pre-resolved** -- The `ctx.scopedFiles` array is populated before your `check` function is called, based on the ADR's `files` globs. - **Changed files auto-detected** -- `ctx.changedFiles` is automatically populated with the branch diff against the base branch (e.g., `main`) plus uncommitted working-tree changes (staged, unstaged, and untracked non-ignored files). Use `--staged` for pre-commit hooks (staged files only) or `--base ` for an explicit base. This enables cross-file dependency rules to work locally, not just in CI. The editor plugins for [Claude Code](/guides/claude-code-plugin/) and [Cursor](/guides/cursor-integration/) run `archgate check` automatically after every code change. The agent reads the applicable ADRs, writes compliant code, and validates -- no manual check commands needed. [Sign up for beta access](https://plugins.archgate.dev). --- ## Guides: CI Integration Source: https://cli.archgate.dev/guides/ci-integration/ Archgate checks fit into any CI system that respects exit codes. Add a single step to your pipeline: error-severity violations fail the job with exit code 1 (pass `--strict` to escalate warnings too). Mark the job as a required check in your branch protection rules and violations block merges. ## GitHub Actions The fastest way to add Archgate to GitHub Actions is with the official [`archgate/check-action`](https://github.com/archgate/check-action). It installs the CLI, runs `archgate check --output github`, and outputs violations as inline annotations on the pull request's "Files changed" tab: ```yaml name: Archgate on: pull_request: push: branches: [main] jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: archgate/check-action@v1 ``` That's it. No Node.js setup, no install step. If any rule reports a violation with `error` severity, the job fails with exit code 1. ### Pin a version ```yaml - uses: archgate/check-action@v1 with: version: v0.15.0 ``` ### Setup-only action If you need to run Archgate commands beyond `check` (e.g. `archgate adr list --json`), use [`archgate/setup-action`](https://github.com/archgate/setup-action) to install the CLI and add it to PATH, then run whatever commands you need: ```yaml steps: - uses: actions/checkout@v4 - uses: archgate/setup-action@v1 - run: archgate check --output github - run: archgate adr list --json ``` ### Cross-platform workflow Both actions support Ubuntu, macOS, and Windows runners: ```yaml jobs: check: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: archgate/check-action@v1 ``` ### Manual setup If you prefer not to use the official actions, you can install Archgate manually: ```yaml steps: - uses: actions/checkout@v4 - run: npm install -g archgate - run: archgate check ``` ## GitHub Actions annotations Use `--output github` to output violations as GitHub Actions workflow annotations. These appear inline on the pull request's "Files changed" tab, pointing directly to the offending file and line. ```yaml - run: archgate check --output github ``` The `github` format produces `::error` and `::warning` annotations in the format GitHub Actions expects. Each annotation includes the ADR ID, rule ID, file path, and line number. :::note The `archgate/check-action` passes `--output github` automatically. You only need this flag when running `archgate check` manually. ## SARIF output for Code Scanning Use `--output sarif` to emit [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html), the format GitHub's Code Scanning and Code Quality features ingest. Unlike `github` annotations (which only appear on the PR diff), SARIF results also populate the repository's Security tab and persist across runs. ```yaml permissions: contents: read security-events: write steps: - name: Run archgate check run: archgate check --output sarif > results.sarif - name: Upload SARIF to GitHub Security tab if: success() || failure() uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: sarif_file: results.sarif ``` Two details matter here: the upload step needs `if: success() || failure()`, because `archgate check` exits 1 on violations and the findings must still reach the Security tab when there are findings (prefer this over `always()`, which would also run for cancelled jobs); and the job needs the `security-events: write` permission, or the upload is rejected. Every rule violation becomes a SARIF result; advisory findings (briefing-budget, suppression, and unparsed-ADR warnings) are included too, as synthetic results under dedicated rule IDs, always at `warning` level. `--output sarif` is opt-in only -- it is never auto-detected. See [`archgate check` -- SARIF output](/reference/cli/check/#sarif-output) for the full field mapping. ## Machine-readable output Use `--output json` for structured output that other tools can parse: ```yaml - run: archgate check --output json > results.json ``` The JSON output includes: ```json { "pass": false, "total": 6, "passed": 5, "failed": 1, "warnings": 0, "errors": 1, "infos": 0, "ruleErrors": 0, "truncated": false, "results": [ { "adrId": "ARCH-006", "ruleId": "no-unapproved-deps", "description": "Production dependencies must be on the approved list", "status": "fail", "totalViolations": 1, "shownViolations": 1, "violations": [ { "message": "Unapproved production dependency: \"chalk\"", "file": "package.json", "severity": "error" } ], "durationMs": 18 } ], "durationMs": 142 } ``` ## Exit codes | Code | Meaning | CI behavior | | ---- | ---------------- | ------------ | | 0 | All checks pass | Job succeeds | | 1 | Violations found | Job fails | | 2 | Internal error | Job fails | Warnings (severity `warning`) are logged but do not affect the exit code. Only `error`-severity violations cause exit code 1. ## Narrowing scope ### Check files changed in the PR Use `--base` to compare against the PR's base branch. This gives cross-file dependency rules the full picture of what changed: ```yaml - run: archgate check --base origin/${{ github.base_ref }} ``` Without `--base`, the base branch is auto-detected from `origin/HEAD`. The explicit form is recommended in CI for deterministic behavior. ### Check only staged files Use `--staged` to limit checking to git-staged files. This is useful in pre-commit hooks or when you only want to validate what is about to be committed: ```yaml - run: archgate check --staged ``` ### Check a specific ADR Use `--adr ` to run rules from a single ADR: ```yaml - run: archgate check --adr ARCH-006 ``` This is useful when a PR only touches files governed by one ADR and you want faster feedback. ## Adding to an existing pipeline If you already have a CI configuration, add Archgate as a single step after your checkout: ```yaml # Existing pipeline steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" - run: npm ci - run: npm test # Add Archgate check - run: npm install -g archgate - run: archgate check --output github ``` No additional dependencies or configuration files are needed beyond the `.archgate/` directory already in your repository. ## Caching the installation Cache the `~/.archgate` directory to speed up repeated installs: ```yaml jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Cache Archgate uses: actions/cache@v4 with: path: ~/.archgate key: archgate-${{ runner.os }} - run: npm install -g archgate - run: archgate check --output github ``` ## GitLab CI ```yaml adr-compliance: image: node:22 script: - npm install -g archgate - archgate check ``` ## Standalone installer (no Node.js) If your CI environment does not have Node.js, use the standalone installer to download a pre-built binary directly from GitHub Releases: ```yaml jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: curl -fsSL https://cli.archgate.dev/install-unix | sh - run: ~/.archgate/bin/archgate check --output github ``` This works in any environment with `curl` and `tar`, no runtime dependencies needed. You can pin a version with the `ARCHGATE_VERSION` environment variable: ```yaml - run: curl -fsSL https://raw.githubusercontent.com/archgate/cli/main/install.sh | ARCHGATE_VERSION=v0.11.2 sh ``` ## Bun-based CI If your CI already uses Bun, install Archgate with `bun` instead of `npm`: ```yaml jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: bun install -g archgate - run: archgate check --output github ``` ## Other CI systems Archgate works with any CI system that can run shell commands. The pattern is always the same: 1. Install: `npm install -g archgate`, `bun install -g archgate`, or use the [standalone installer](/getting-started/installation/#install-standalone-recommended) 2. Run: `archgate check` 3. Check the exit code (0 = pass, 1 = violations, 2 = error) For systems that support annotations (Azure DevOps, Buildkite, etc.), use `--output json` to parse the output and emit annotations in the format your CI expects. ## Pre-commit hooks You can also run Archgate as a local pre-commit hook. Add this to `.git/hooks/pre-commit` (or use a hook manager like Husky or Lefthook): ```bash #!/bin/sh archgate check --staged ``` The `--staged` flag ensures only files about to be committed are checked, keeping the hook fast. ## Verbose output Use `--verbose` to see passing rules and timing information alongside failures. This is helpful for debugging slow checks or confirming that rules are running as expected: ```yaml - run: archgate check --verbose ``` CI catches violations at merge time. Editor plugins catch them at coding time. With the [Claude Code](/guides/claude-code-plugin/) or [Cursor](/guides/cursor-integration/) plugin, your AI agent reads ADRs before writing code and validates compliance before you even commit. [Sign up for beta access](https://plugins.archgate.dev). --- ## Guides: Claude Code Plugin Source: https://cli.archgate.dev/guides/claude-code-plugin/ The Archgate Claude Code plugin gives AI agents working in [Claude Code](https://claude.ai/code) built-in guardrails. Instead of relying on prompt instructions that drift over time, agents read your ADRs directly via Archgate CLI commands and validate their own code against your rules. ## What the plugin provides The plugin adds agents and role-based skills to Claude Code. The developer agent orchestrates the guardrails workflow, invoking skills as needed -- read decisions before coding, validate after, and capture new patterns for the team. ### Agents | Agent | Purpose | | -------------------- | --------------------------------------------------------------------------- | | `archgate:developer` | General development agent that reads ADRs before coding and validates after | | `archgate:planner` | Planning agent for scoping work and breaking tasks into ADR-compliant steps | The `archgate:developer` agent is set as the default agent via `.claude/settings.local.json`. It orchestrates the skills below automatically as part of its workflow. ### Skills | Skill | Purpose | | -------------------------- | ------------------------------------------------------------------------------------- | | `archgate:reviewer` | Validates code changes against all project ADRs for structural compliance | | `archgate:lessons-learned` | Captures learnings and proposes new ADRs when patterns emerge | | `archgate:adr-author` | Creates and edits ADRs following project conventions | | `archgate:onboard` | One-time setup: explores the codebase, interviews the developer, creates initial ADRs | | `archgate:cli-reference` | Internal reference for AI agents with the complete Archgate CLI command guide | ## Installation The Claude Code plugin is currently in beta. Run `archgate login` to sign up and authenticate. ### 1. Log in with GitHub Authenticate with your GitHub account to obtain a plugin token: ```bash archgate login ``` This starts a GitHub Device Flow. The CLI displays a one-time code and URL -- open the URL in your browser, enter the code, and authorize. Once complete, credentials are stored securely in your OS credential manager via `git credential approve`. ### 2. Initialize your project with the plugin Run `archgate init` with the `--editor claude` flag. If you are already logged in, the plugin is installed automatically: ```bash archgate init --editor claude ``` Without `--editor`, an interactive run detects the editors installed on your machine and lets you pick one or more; a non-interactive run (an agent or CI) defaults to Claude Code. To explicitly request plugin installation: ```bash archgate init --editor claude --install-plugin ``` This creates `.claude/settings.local.json` with the `archgate:developer` agent and skill permissions pre-configured. If the `claude` CLI is on your PATH, the plugin is installed automatically via: 1. `claude plugin marketplace add https://plugins.archgate.dev/archgate.git` (registers the Archgate marketplace) 2. `claude plugin install archgate@archgate` (installs the plugin) The marketplace URL carries no credentials -- git authenticates against `plugins.archgate.dev` using the token `archgate login` stored in your OS credential manager. If the `claude` CLI is not found, the command prints the manual commands for you to run. ### Installing the plugin on an existing project If your project is already initialized, you can install or reinstall the plugin without re-running `archgate init`: ```bash archgate plugin install --editor claude ``` Without `--editor`, an interactive run prompts you to select editors and a non-interactive run defaults to Claude Code. To print the plugin repository URL for manual configuration: ```bash archgate plugin url --editor claude ``` ## Initial setup with onboard After installation, run the `archgate:onboard` skill in your project once. This skill: 1. Explores your codebase structure (directories, key files, package configuration) 2. Interviews you about your team's conventions, constraints, and architectural decisions 3. Creates an initial set of ADRs based on your responses 4. Sets up the `.archgate/` directory with your first rules The onboard skill is designed to run once per project. After onboarding, the other skills handle day-to-day development. ## How it works in practice The plugin follows a structured workflow for every coding task: ### 1. Read applicable ADRs When the developer gives a coding task, the agent runs `archgate review-context` to read all ADRs that apply to the files being changed. This provides a condensed briefing with the **Decision** and **Do's and Don'ts** sections from each relevant ADR. The agent does not write code until it has read the applicable ADRs. This is part of the `archgate:developer` agent's workflow. ### 2. Write code following ADR constraints The agent writes code that complies with the constraints from the ADRs. The Do's and Don'ts sections serve as concrete guardrails -- the agent references them while coding. ### 3. Validate changes After writing code, the agent runs `archgate check` to execute automated rules against the changes. Any violations are fixed before proceeding. ### 4. Review changes The agent invokes `archgate:reviewer` to validate structural ADR compliance beyond what automated rules catch. The reviewer skill reviews the full context of the changes against all applicable ADRs. ### 5. Capture learnings The agent invokes `archgate:lessons-learned` to review the work and identify patterns worth capturing. That skill may propose new ADRs or updates to existing ones when recurring conventions emerge. ## ADR-driven refusal When the agent encounters a task that would require violating an ADR, it refuses and explains which ADR would be violated. It then suggests how to achieve the same goal while staying compliant. For example, if a developer asks the agent to add `chalk` as a dependency in a project governed by ARCH-006 (dependency policy), the agent will: 1. Refuse, citing ARCH-006 and the approved dependency list 2. Suggest using `styleText()` from `node:util` instead 3. Offer to implement the task using the compliant alternative This behavior is consistent regardless of how the developer phrases the request. ADRs are treated as mandatory constraints, not suggestions. ## How the plugin accesses ADRs The plugin uses Archgate CLI commands directly to read ADRs and run compliance checks. The key commands are: - **`archgate review-context`** -- condensed briefings of all ADRs applicable to changed files, grouped by domain - **`archgate check --staged`** -- automated rule checking with violation reporting - **`archgate adr show `** -- full text of a specific ADR - **`archgate adr list`** -- inventory of all ADRs in the project with metadata - **`archgate session-context`** -- read session transcripts for context recovery All commands run locally and read directly from your `.archgate/adrs/` directory. No ADR or source content leaves your machine. ## When to use each agent or skill | Scenario | Agent or skill | | -------------------------------------------- | -------------------------- | | Starting a new project with Archgate | `archgate:onboard` | | Day-to-day coding tasks | `archgate:developer` | | Planning and scoping work | `archgate:planner` | | Reviewing a PR for ADR compliance | `archgate:reviewer` | | Noticing a recurring pattern worth codifying | `archgate:lessons-learned` | | Creating or editing an ADR | `archgate:adr-author` | The `archgate:developer` agent orchestrates the skills automatically -- it invokes `archgate:reviewer` and `archgate:lessons-learned` as part of its workflow. Most of the time, you only need to interact with the developer agent directly. --- ## Guides: GitHub Copilot Plugin Source: https://cli.archgate.dev/guides/copilot-cli-plugin/ The Archgate GitHub Copilot plugin gives AI agents working in [GitHub Copilot](https://github.com/features/copilot) built-in guardrails. Agents read your ADRs before writing code, validate after, and capture new patterns for the team -- the same workflow available in the [Claude Code plugin](/guides/claude-code-plugin/). Both Copilot distributions are supported: the **Copilot CLI** (`copilot` on your PATH) and the **Copilot desktop app**. They share the same configuration directory (`~/.copilot/` by default), so one install covers both. ## How it works Copilot supports plugin marketplaces backed by git repositories. The Archgate plugin is served from `plugins.archgate.dev/archgate/vscode.git` -- the same marketplace used by the VS Code extension, in the `.github/plugin/` manifest format Copilot recognizes natively. Installation is declarative: the CLI writes the marketplace and plugin declaration into `~/.copilot/settings.json` (`extraKnownMarketplaces` + `enabledPlugins`). Copilot reads this file on startup and installs any declared plugin automatically. When the `copilot` CLI is on your PATH, the CLI also runs `copilot plugin install` so the plugin is available immediately without a restart. ## Installation The GitHub Copilot plugin is currently in beta. Run `archgate login` to sign up and authenticate before following the steps below. ### 1. Log in with GitHub Authenticate with your GitHub account to obtain a plugin token: ```bash archgate login ``` This starts a GitHub Device Flow. The CLI displays a one-time code and URL -- open the URL in your browser, enter the code, and authorize. Once complete, credentials are stored securely in your OS credential manager via `git credential approve`. ### 2. Initialize your project with the plugin Run `archgate init` with the `--editor copilot` flag: ```bash archgate init --editor copilot ``` If you are logged in and Copilot is installed (CLI or desktop app), the plugin is installed automatically: - **Copilot CLI on PATH:** the plugin is installed immediately via `copilot plugin install`. - **Desktop app only:** the plugin is declared in `~/.copilot/settings.json`; restart the Copilot app and it installs automatically on launch. To explicitly request plugin installation: ```bash archgate init --editor copilot --install-plugin ``` To install or reinstall the plugin on an already-initialized project: ```bash archgate plugin install --editor copilot ``` ### Generated files The command creates the `.github/copilot/` directory for project-level configuration and updates the user-level `~/.copilot/settings.json` with the Archgate marketplace and plugin declaration. An entry pointing at an outdated marketplace URL is corrected in place. ### Manual installation To install the plugin manually with the `copilot` CLI: ```bash copilot plugin marketplace add "$(archgate plugin url --editor copilot)" copilot plugin install archgate@archgate ``` Credentials are provided automatically by your git credential manager (stored during `archgate login`). ## What the plugin provides The plugin adds agents and role-based skills to Copilot. The agent orchestrates the guardrails workflow, invoking skills as needed. ### Agents | Agent | Purpose | | -------------------- | --------------------------------------------------------------------------- | | `archgate:developer` | General development agent that reads ADRs before coding and validates after | | `archgate:planner` | Read-only planning agent that designs ADR-compliant implementation plans | The `archgate:developer` agent is set as the default agent via the plugin settings. It orchestrates the skills below automatically as part of its workflow. ### Skills | Skill | Purpose | | -------------------------- | ------------------------------------------------------------------------------------- | | `archgate:reviewer` | Validates code changes against all project ADRs for structural compliance | | `archgate:lessons-learned` | Reviews rule coverage and proposes new ADRs when patterns emerge | | `archgate:adr-author` | Creates and edits ADRs following project conventions | | `archgate:cli-reference` | Provides the CLI command reference and rules authoring guide to agents | | `archgate:onboard` | One-time setup: explores the codebase, interviews the developer, creates initial ADRs | ## Initial setup with onboard After installation, run the `archgate:onboard` skill in your project once. This skill: 1. Explores your codebase structure (directories, key files, package configuration) 2. Interviews you about your team's conventions, constraints, and architectural decisions 3. Creates an initial set of ADRs based on your responses 4. Sets up the `.archgate/` directory with your first rules The onboard skill is designed to run once per project. After onboarding, the other skills handle day-to-day development. ## How it works in practice The plugin follows a structured workflow for every coding task: ### 1. Read applicable ADRs When the developer gives a coding task, the agent runs `archgate review-context` to read all ADRs that apply to the files being changed. This provides a condensed briefing with the **Decision** and **Do's and Don'ts** sections from each relevant ADR. ### 2. Write code following ADR constraints The agent writes code that complies with the constraints from the ADRs. The Do's and Don'ts sections serve as concrete guardrails. ### 3. Validate changes After writing code, the agent runs `archgate check` to execute automated rules against the changes. Any violations are fixed before proceeding. ### 4. Reviewer validation The agent invokes `archgate:reviewer` to validate structural ADR compliance beyond what automated rules catch. ### 5. Capture learnings The agent invokes `archgate:lessons-learned` to review the work and identify patterns worth capturing as new ADRs. ## Tips - **Run onboard once per project** to generate your initial ADRs from your actual codebase. - **Keep ADR rule files up to date** -- the agent enforces what the rules check for. - **Desktop app users:** after the first install, restart the Copilot app so it picks up the declared plugin. --- ## Guides: Cursor Integration Source: https://cli.archgate.dev/guides/cursor-integration/ Archgate integrates with [Cursor](https://cursor.com) to give AI agents built-in guardrails. The agent reads your ADRs before writing code, validates after, and captures new patterns for the team -- the same workflow available in the [Claude Code plugin](/guides/claude-code-plugin/). ## Setup Run `archgate init` with the `--editor cursor` flag to configure Cursor integration in your project: ```bash archgate init --editor cursor ``` The Cursor plugin is currently in beta. Run `archgate login` to sign up and authenticate. If you have logged in via `archgate login`, the init command also installs the Archgate plugin for Cursor. The plugin provides pre-built agents, skills, and hooks that give Cursor's AI agent a full guardrails workflow. The plugin is distributed as an authenticated tarball. The CLI downloads it and extracts only the `agents/` and `skills/` directories into `~/.cursor/`; anything else in the tarball is discarded. Stale `archgate-*` agents and skill directories from a previous install are removed first, so local edits to those files do not survive a reinstall. No CLI detection is needed -- files are written directly to the Cursor user directory. The `afterFileEdit` hook is not taken from the tarball. The CLI writes it into `~/.cursor/hooks.json` itself, preserving any hooks you already have there and leaving a malformed file untouched. To explicitly install the plugin: ```bash archgate login # one-time setup archgate init --editor cursor --install-plugin ``` To install or reinstall the plugin on an already-initialized project: ```bash archgate plugin install --editor cursor ``` ### Generated files **User scope** (`~/.cursor/`): | File | Purpose | | ---------------------------------------------------- | ------------------------------------------------------------------- | | `~/.cursor/skills/archgate-reviewer/SKILL.md` | Validates code changes against all project ADRs | | `~/.cursor/skills/archgate-lessons-learned/SKILL.md` | Captures learnings and proposes new ADRs when patterns emerge | | `~/.cursor/skills/archgate-adr-author/SKILL.md` | Creates and edits ADRs following project conventions | | `~/.cursor/skills/archgate-onboard/SKILL.md` | One-time setup: explores the codebase, interviews you, creates ADRs | | `~/.cursor/skills/archgate-cli-reference/SKILL.md` | Internal reference for AI agents with the Archgate CLI guide | | `~/.cursor/agents/archgate-developer.md` | Primary development agent with the full ADR guardrails workflow | | `~/.cursor/agents/archgate-planner.md` | Planning agent for scoping and breaking down work | | `~/.cursor/hooks.json` | `afterFileEdit` hook, merged into any hooks you already have | **Project scope**: | File | Purpose | | -------------------- | ------------------------------------------------------------------- | | `.cursor/hooks.json` | `afterFileEdit` hook that runs `archgate check` on each edited file | The `.cursor/hooks.json` file is the only file written to your project tree, and `archgate init` writes it only when it does not already exist -- an existing file is left alone. It ensures `archgate check` runs automatically after every file edit, catching ADR violations in real time. ## What the plugin provides The plugin adds agents and skills to Cursor, installed at the user scope so they are available across all your projects. ### Agents | Name | Purpose | | -------------------- | --------------------------------------------------------------------------- | | `archgate-developer` | General development agent that reads ADRs before coding and validates after | | `archgate-planner` | Planning agent for scoping work and breaking tasks into ADR-compliant steps | Users invoke agents explicitly via `/archgate-developer` or `/archgate-planner` in the Cursor chat. ### Skills | Name | Purpose | | -------------------------- | ----------------------------------------------------------------------------- | | `archgate-reviewer` | Validates code changes against all project ADRs for structural compliance | | `archgate-lessons-learned` | Reviews rule coverage and proposes new ADRs when patterns emerge | | `archgate-adr-author` | Creates and edits ADRs following project conventions | | `archgate-onboard` | One-time setup: explores the codebase, interviews you, creates initial ADRs | | `archgate-cli-reference` | Internal reference for AI agents with the complete Archgate CLI command guide | These are the same roles available in the Claude Code plugin (`archgate:reviewer`, `archgate:lessons-learned`, etc.), adapted for Cursor's skill and agent system. ### Hooks | Hook | Trigger | Action | | --------------- | --------------- | ---------------------------------------------------------------- | | `afterFileEdit` | Every file edit | Runs `archgate check` on the edited file to catch ADR violations | The hook is defined in `.cursor/hooks.json` at the project level, so it works both locally and in cloud agent environments. The plugin install additionally registers the same hook in your user-level `~/.cursor/hooks.json`. ## Initial setup with onboard After installing the plugin, invoke `/archgate-developer` and ask it to run the onboard skill in your project. This skill: 1. Explores your codebase structure (directories, key files, package configuration) 2. Interviews you about your team's conventions, constraints, and architectural decisions 3. Creates an initial set of ADRs based on your responses 4. Sets up the `.archgate/` directory with your first rules The onboard skill is designed to run once per project. After onboarding, the other skills handle day-to-day development. ## How it works in practice Invoke `/archgate-developer` in Cursor's chat when starting a coding task. The agent follows a structured workflow for every change: 1. **Read applicable ADRs** -- The agent runs `archgate review-context` to see which ADRs apply to the files being changed. It does not write code until it has read the applicable ADRs. 2. **Write code following ADR constraints** -- The agent implements changes following the Do's and Don'ts from the applicable ADRs. 3. **Run compliance checks** -- The agent runs `archgate check` to execute automated rules. The `afterFileEdit` hook also catches violations in real time. Any violations are fixed before proceeding. 4. **Review changes** -- The agent invokes the `archgate-reviewer` skill to validate structural ADR compliance beyond what automated rules catch. 5. **Capture learnings** -- The agent invokes the `archgate-lessons-learned` skill to review the work and identify patterns worth capturing as new ADRs or updates to existing ones. ## ADR-driven refusal When the agent encounters a task that would require violating an ADR, it refuses and explains which ADR would be violated. It then suggests how to achieve the same goal while staying compliant. For example, if a developer asks the agent to add `chalk` as a dependency in a project governed by a dependency policy ADR, the agent will: 1. Refuse, citing the ADR and the approved dependency list 2. Suggest using the approved alternative instead 3. Offer to implement the task using the compliant approach This behavior is consistent regardless of how the developer phrases the request. ADRs are treated as mandatory constraints, not suggestions. ## When to use each agent or skill | Scenario | Agent / Skill | | -------------------------------------------- | ---------------------------------------------- | | Starting a new project with Archgate | `/archgate-developer` (then ask it to onboard) | | Day-to-day coding tasks | `/archgate-developer` | | Planning and scoping work | `/archgate-planner` | | Reviewing a change for ADR compliance | `archgate-reviewer` | | Noticing a recurring pattern worth codifying | `archgate-lessons-learned` | | Creating or editing an ADR | `archgate-adr-author` | The `archgate-developer` agent orchestrates the skills automatically -- it invokes `archgate-reviewer` and `archgate-lessons-learned` as part of its workflow. Most of the time, you only need to invoke `/archgate-developer` and let it run. ## Cloud agent support Cursor supports cloud agents that run on remote VMs. These environments do not have access to `~/.cursor/`, so user-scoped skills and agents are not available. However, the `.cursor/hooks.json` file is part of your project tree and works in cloud VMs. This means `archgate check` still runs automatically after every file edit, even in cloud agent sessions. For full governance in cloud environments, ensure `archgate` is available on the VM's PATH (e.g., via the install script in your project's setup). ## Session transcript access The `archgate session-context` command reads Cursor agent session transcripts from disk. This allows skills to access the history of the current conversation, which is useful for recovering context that may have been compacted or truncated. When Cursor is the detected editor, no editor needs to be named. Pass `--editor cursor` to read Cursor's transcripts regardless of what was detected — useful when another agent is running inside Cursor and wins detection. The two options that matter for Cursor are: - `--max-entries ` -- Maximum number of entries to return (default: 200, most recent entries). Must be a positive integer. - `--editor ` -- Read another editor's transcripts instead of the detected one. (A third option, `--root`, applies to opencode only.) Use `archgate session-context list` to discover earlier sessions, and `archgate session-context show ` to read a specific one. ## Tips for effective usage - **Invoke `/archgate-developer` for coding tasks.** It orchestrates the full read-validate-capture workflow automatically. - **Run onboard once per project.** It sets up your initial ADRs based on your actual codebase and conventions. - **Use `archgate-reviewer` for reviews.** It validates structural compliance beyond what automated rules catch. - **Use `archgate-lessons-learned` after resolving tricky issues.** It captures learnings so the same mistakes are not repeated. - **Commit the `.cursor/` directory.** The `hooks.json` file ensures every team member gets `archgate check` on file edits when they clone the repository. - **Keep ADR rules files up to date.** The agent enforces what the rules check for -- if a rule is missing, the violation will not be caught. - **Re-run `archgate plugin install --editor cursor` to upgrade.** The service returns the latest plugin bundle on every authenticated download. --- ## Guides: Importing ADRs Source: https://cli.archgate.dev/guides/importing-adrs/ ## What are ADR packs? An **ADR pack** is a curated collection of Architecture Decision Records bundled together under a shared theme. Each pack includes: - An `archgate-pack.yaml` manifest with metadata (name, version, maintainers, tags) - One or more ADR markdown files in an `adrs/` directory - Optional companion `.rules.ts` files that enforce each decision automatically Packs let you bootstrap a project with proven architectural conventions instead of writing everything from scratch. ## Importing from the registry The [Archgate awesome-adrs registry](https://github.com/archgate/awesome-adrs) hosts community-maintained packs. Import one with: ```bash archgate adr import packs/typescript-strict ``` This clones the registry, copies the ADRs into your `.archgate/adrs/` directory, and remaps IDs to fit your project's numbering scheme. ### Pinning a version Append `@` to lock to a specific git tag or branch: ```bash archgate adr import packs/typescript-strict@0.3.0 ``` ## Cherry-picking individual ADRs You don't have to import an entire pack. Point to a specific ADR file within a pack: ```bash archgate adr import packs/security/adrs/SEC-001-no-secrets-in-code ``` Only that single ADR (and its companion rules file, if present) will be imported. ## Importing from third-party repos Any GitHub repository with ADR files works as a source. Use the three-segment `org/repo/path` syntax: ```bash archgate adr import acme/company-adrs/packs/api-standards ``` This clones `https://github.com/acme/company-adrs.git` and imports from the specified subpath. ## Importing from any git URL For non-GitHub repositories or when you need full control, pass a complete URL: ```bash archgate adr import https://github.com/org/repo/tree/main/packs/my-pack ``` The CLI parses the GitHub `/tree//` format automatically. For other hosts, you can pass any git-cloneable URL: ```bash archgate adr import https://gitlab.com/team/repo.git ``` ## Preview with `--dry-run` See what would be imported without writing anything: ```bash archgate adr import packs/typescript-strict --dry-run ``` Output shows the original IDs, remapped IDs, and titles in a table. ## Listing imported ADRs with `--list` Check what has been imported previously: ```bash archgate adr import --list ``` This reads `.archgate/imports.json` and displays each source, version, and the ADR IDs it produced. ## How ID remapping works When you import ADRs, the original IDs are **remapped** to match your project's domain prefixes. Each ADR's `domain` field determines which prefix it gets. For example, an ADR with `domain: frontend` becomes `FE-XXX`, while one with `domain: backend` becomes `BE-XXX`. Each domain has its own counter, so importing a pack with mixed domains produces correctly prefixed IDs without collisions. For example, importing a pack with three frontend ADRs and two backend ADRs into a project that already has `FE-001` and `BE-001` produces: - `FE-002`, `FE-003`, `FE-004` (frontend) - `BE-002`, `BE-003` (backend) The remapping ensures: 1. No ID collisions with existing ADRs 2. Each domain maintains its own numbering sequence 3. Imported rules files work immediately without manual edits ## The imports.json manifest Every import is recorded in `.archgate/imports.json`: ```json { "imports": [ { "source": "packs/typescript-strict", "version": "0.3.0", "importedAt": "2026-05-10T14:32:00.000Z", "adrIds": ["ARCH-006", "ARCH-007", "ARCH-008"] } ] } ``` This manifest lets you track provenance: where each imported ADR came from and when. Commit it to version control alongside your ADRs. ## Command options reference | Option | Description | | ----------- | ------------------------------------- | | `--yes` | Skip the confirmation prompt | | `--json` | Output results as JSON | | `--dry-run` | Preview changes without writing files | | `--list` | List previously imported ADRs | --- ## Guides: opencode Integration Source: https://cli.archgate.dev/guides/opencode-integration/ Archgate integrates with [opencode](https://opencode.ai) to give AI agents built-in guardrails. The agent reads your ADRs before writing code, validates after, and captures new patterns for the team -- the same workflow available in the [Claude Code plugin](/guides/claude-code-plugin/). ## Setup Run `archgate init` with the `--editor opencode` flag to configure opencode integration in your project: ```bash archgate init --editor opencode ``` The opencode agent bundle is currently in beta. Run `archgate login` to sign up and authenticate. The opencode agents and skills are **not written to your project tree** — unlike Cursor, which also installs its agents and skills at the user scope but still writes its `afterFileEdit` hook into the project's `.cursor/hooks.json`. For opencode nothing editor-specific lands in the repository: everything lives on your machine and is available across every project you open with opencode. opencode uses the XDG Base Directory convention on every platform (via the `xdg-basedir` package), so the install location resolves to `$XDG_CONFIG_HOME/opencode/` when that variable is set, and falls back to `$HOME/.config/opencode/` otherwise. That means Windows installs land under `C:\Users\\.config\opencode\`, not under `%APPDATA%`: | Platform | Install location | | ------------- | ------------------------------------------------------------------- | | Linux / macOS | `$XDG_CONFIG_HOME/opencode/`, falling back to `~/.config/opencode/` | | Windows | `C:\Users\\.config\opencode\` (same fallback rule) | Inside that directory, agents are written to `agents/` and skills to `skills/`. ### Authenticated install The install step only runs when Archgate can see opencode on your machine — either the `opencode` CLI on your PATH, or the opencode config directory (`~/.config/opencode/`), which the Desktop app also uses even though it ships no CLI binary. Archgate needs that confirmation before writing files into the user-scope config directory. Otherwise the bundle would sit in a location nothing reads. Install opencode from [opencode.ai](https://opencode.ai/docs/) first, then re-run `archgate init --editor opencode` or `archgate plugin install --editor opencode`. If you have logged in via `archgate login` **and** opencode is detected, the init command downloads and installs the Archgate bundle for opencode. The bundle provides two pre-built primary agents and five skills that give opencode's AI a full guardrails workflow. To explicitly install the bundle: ```bash archgate login # one-time setup archgate init --editor opencode --install-plugin ``` To install or reinstall on an already-initialized project: ```bash archgate plugin install --editor opencode ``` The install step downloads an authenticated tarball from the Archgate plugins service and extracts its `agents/` and `skills/` directories into the opencode user-scope directory. Any `archgate-*` agent files and `archgate-*/` skill directories from a previous install are removed first, so re-running the command replaces the bundle rather than layering on top of it. Files that are not under `agents/` or `skills/` are ignored, so nothing else in your opencode config directory is touched. ### Generated files (user scope) | File | Purpose | | ------------------------------------------------------------ | --------------------------------------------------------------------------- | | `/agents/archgate-developer.md` | Primary agent that runs the full ADR workflow | | `/agents/archgate-planner.md` | Read-only primary agent that designs ADR-compliant implementation plans | | `/skills/archgate-reviewer/SKILL.md` | Validates code changes against all project ADRs | | `/skills/archgate-lessons-learned/SKILL.md` | Captures learnings and proposes new ADRs | | `/skills/archgate-adr-author/SKILL.md` | Creates and edits ADRs following project conventions | | `/skills/archgate-cli-reference/SKILL.md` | Internal reference with the Archgate CLI command guide | | `/skills/archgate-onboard/SKILL.md` | One-time setup: explores the codebase, interviews you, creates initial ADRs | | `/opencode.json` | User settings — `default_agent` is set to `archgate-developer` if unset | The `opencode.json` merge is additive and never overwrites an existing `default_agent`, so your own choice of default agent is preserved. `.archgate/adrs/` and `.archgate/lint/` are still created in your project as usual. Only the opencode-specific files live outside the project tree. ## What the bundle provides The `archgate-` prefix avoids collision with any user-authored opencode agents or skills in the same directories. Skills are invoked via opencode's `@-mention` syntax. ### Primary agents | Name | Purpose | | -------------------- | -------------------------------------------------------------------------------- | | `archgate-developer` | General development agent that reads ADRs before coding and validates after | | `archgate-planner` | Read-only planning agent that designs ADR-compliant plans without modifying code | The `archgate-developer` agent orchestrates the skills below automatically as part of its workflow. Both agents are opencode _primary_ agents, so you switch between them with the Tab key. ### Skills | Name | Purpose | | -------------------------- | ------------------------------------------------------------------------------------- | | `archgate-reviewer` | Validates code changes against all project ADRs for structural compliance | | `archgate-lessons-learned` | Reviews rule coverage and proposes new ADRs when patterns emerge | | `archgate-adr-author` | Creates and edits ADRs following project conventions | | `archgate-cli-reference` | Internal reference for AI agents with the complete Archgate CLI command guide | | `archgate-onboard` | One-time setup: explores the codebase, interviews the developer, creates initial ADRs | These are the same roles available in the Claude Code plugin, adapted for opencode's native agent and skill model. ## How it works in practice The install sets `archgate-developer` as opencode's `default_agent` when you have not already chosen one, so it is usually selected for you; otherwise switch to it with the Tab key when starting a coding task. The agent follows a structured workflow for every change: 1. **Read applicable ADRs** -- The agent runs `archgate review-context` to see which ADRs apply to the files being changed. It does not write code until it has read the applicable ADRs. 2. **Write code following ADR constraints** -- The agent implements changes following the Do's and Don'ts from the applicable ADRs. 3. **Run compliance checks** -- The agent runs `archgate check` to execute automated rules. Any violations are fixed before proceeding. 4. **Reviewer validation** -- The agent mentions `@archgate-reviewer` to validate structural ADR compliance beyond what automated rules catch. 5. **Capture learnings** -- The agent mentions `@archgate-lessons-learned` to review the work and identify patterns worth capturing as new ADRs or updates to existing ones. ## ADR-driven refusal When `archgate-developer` encounters a task that would require violating an ADR, it refuses and explains which ADR would be violated. It then suggests how to achieve the same goal while staying compliant. For example, if a developer asks the agent to add `chalk` as a dependency in a project governed by a dependency policy ADR, the agent will: 1. Refuse, citing the ADR and the approved dependency list 2. Suggest using the approved alternative instead 3. Offer to implement the task using the compliant approach This behavior is consistent regardless of how the developer phrases the request. ADRs are treated as mandatory constraints, not suggestions. ## When to use each agent or skill | Scenario | Agent / Skill | | --------------------------------------------------- | ------------------------------ | | Day-to-day coding tasks | `archgate-developer` (primary) | | Planning a change without writing code | `archgate-planner` (primary) | | Setting up Archgate in a project for the first time | `@archgate-onboard` | | Reviewing a change for ADR compliance | `@archgate-reviewer` | | Noticing a recurring pattern worth codifying | `@archgate-lessons-learned` | | Creating or editing an ADR | `@archgate-adr-author` | The `archgate-developer` agent orchestrates the skills automatically -- it mentions `@archgate-reviewer` and `@archgate-lessons-learned` as part of its workflow. Most of the time, you only need to select `archgate-developer` and let it run. ## User-scope vs project-scope The opencode bundle lives in your user-scope opencode directory rather than in `.opencode/` inside your project. Consequences: - **One install per machine.** `archgate plugin install --editor opencode` installs the bundle globally. Every project you open with opencode sees the same `archgate-*` agents and skills. - **Your repo stays clean.** No `.opencode/` folder is ever created by `archgate init`. Team members who want the agents run their own `archgate plugin install --editor opencode`. - **Upgrades are global.** Re-running `archgate plugin install --editor opencode` removes the previous `archgate-*` agents and skills and replaces them with the latest bundle. ## Tips for effective usage - **Run `@archgate-onboard` once per project** to generate your initial ADRs from your actual codebase. - **Select `archgate-developer` at the start of coding sessions.** It orchestrates the full read-validate-capture workflow automatically. - **Use `@archgate-reviewer` for reviews.** It validates structural compliance beyond what automated rules catch. - **Use `@archgate-lessons-learned` after resolving tricky issues.** It captures learnings so the same mistakes are not repeated. - **Keep ADR rules files up to date.** The agent enforces what the rules check for -- if a rule is missing, the violation will not be caught. - **Re-run `archgate plugin install --editor opencode` to upgrade.** The service returns the latest agent bundle on every authenticated download. --- ## Guides: Pre-commit Hooks Source: https://cli.archgate.dev/guides/pre-commit-hooks/ ## Overview The `archgate check --staged` command checks only git-staged files against your ADR rules. Because it skips unstaged and untracked files, it runs fast enough to use as a pre-commit hook without slowing down your workflow. When a check fails, the commit is blocked. Violations are printed to stdout with file paths and line numbers so you can locate and fix them immediately. ## Lefthook [Lefthook](https://github.com/evilmartians/lefthook) is a fast, cross-platform git hooks manager. Add the following to your `lefthook.yml`: ```yaml # lefthook.yml pre-commit: commands: adr-check: run: archgate check --staged ``` Install the hook with: ```bash lefthook install ``` ## Husky [Husky](https://typicode.github.io/husky/) is a popular git hooks tool for Node.js projects. Add the check to your pre-commit hook: ```bash # .husky/pre-commit archgate check --staged ``` Make sure the hook file is executable: ```bash chmod +x .husky/pre-commit ``` ## What happens when checks fail When `archgate check --staged` finds violations, it exits with code 1. This blocks the commit. The output includes: - The ADR ID and rule name that was violated - The file path where the violation was found - The line number (when available) - A description of what the rule expects Fix the violations, re-stage the files with `git add`, and commit again. ## Performance The `--staged` flag restricts checks to only the files in the git staging area. This means: - A project with hundreds of source files but only three staged files will only check those three files. - Rules that do not match any staged files are skipped entirely. - Typical pre-commit checks complete in under a second. Without `--staged`, `archgate check` scans all files matched by each ADR's `files` glob pattern, which is useful for CI but slower for interactive use. ## Useful flags | Flag | Purpose | | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | | `--staged` | Only check git-staged files (required for pre-commit) | | `--verbose` | Show passing rules and timing information -- helpful when debugging why a check is slow or which rules are being evaluated | | `--output json` | Output results as JSON -- useful for piping to other tools or custom reporting scripts | | `--adr ` | Only check rules from a specific ADR -- useful for isolating a single rule during debugging | | `--output github` | Output GitHub Actions annotations -- use this in CI workflows instead of pre-commit hooks | ## Combining with other hooks Pre-commit hooks can run multiple commands. For example, with Lefthook: ```yaml # lefthook.yml pre-commit: commands: lint: run: npm run lint typecheck: run: npm run typecheck adr-check: run: archgate check --staged ``` Each command runs independently. If any command exits with a non-zero code, the commit is blocked. --- ## Guides: Security Source: https://cli.archgate.dev/guides/security/ Archgate executes TypeScript rules from `.rules.ts` files in your repository. This page explains the trust model, what rules can and cannot do, and how to run checks safely. ## Trust model **`.rules.ts` files are executable code.** When you run `archgate check`, the CLI dynamically imports every `.rules.ts` companion file and runs its `check` functions. This is equivalent to running `bun .archgate/adrs/*.rules.ts` -- the code has the same capabilities as any other script on your machine. This means: - Only run `archgate check` on repositories you trust. - Review `.rules.ts` files with the same scrutiny as any other source code in the project. - In open-source projects, treat `.rules.ts` changes in pull requests as security-sensitive. ### What rules can access Rules receive a `RuleContext` object with sandboxed file operations. All `RuleContext` methods (`readFile`, `readJSON`, `grep`, `grepFiles`, `glob`) are restricted to the project root directory -- path traversal via `../`, absolute paths, and symbolic links are blocked and throw an error. In addition to the `RuleContext` path restrictions above, Archgate runs a **static analysis security scanner** on every `.rules.ts` file before executing it. Neither is a runtime sandbox -- a rule still executes in-process with your privileges; the scanner is a static gate, and the restrictions apply only to the `RuleContext` API a well-behaved rule uses. Because a rule file runs in-process with the full privileges of whoever runs `archgate check`, any module it can reach is arbitrary code -- so the scanner uses an **allowlist, not a denylist**: a rule may import only a small set of safe modules, and everything else is rejected. **The only modules a rule file may import** are `node:path`, `node:url`, `node:util`, and `node:crypto` -- utility modules with no filesystem, network, or process capabilities. They must use the `node:` prefix; the bare forms (`path`) can be shadowed by a package in the target project and are blocked. Every other import is rejected, in any form -- static (`import ... from`), dynamic (`await import(...)`, whether the specifier is a literal or a variable), re-exported (`export ... from`), or reached through `require()` / `import.meta.require()`. The scanner also blocks the other ways to reach code or capabilities outside that set: | Pattern | Blocked | | ---------------------------------------------------------------------- | ------- | | Importing any module other than the four allowed above | Yes | | `require()`, `import.meta.require()` | Yes | | `Bun.spawn()`, `Bun.spawnSync()`, `Bun.write()`, `Bun.file()`, `Bun.$` | Yes | | `fetch()` | Yes | | `eval()`, `new Function()` | Yes | | `process.binding()`, `process.dlopen()` (on any alias of `process`) | Yes | | Computed property access (`Bun[variable]`, `globalThis[variable]`) | Yes | | Assignment to `globalThis` or `process.env` | Yes | | Bidirectional or invisible Unicode characters ("Trojan Source") | Yes | If any of these is found, the rule file is **not imported or executed** and `archgate check` exits with an error. Because the scanner works from the parsed syntax tree, it sees through escapes: `await import("\x6e...")` resolves to the same module name the allowlist checks, so string tricks do not slip past it. The one thing a syntax tree cannot see is a character that makes the rendered source differ from the code that runs -- which is why the scanner also rejects bidirectional and invisible Unicode characters outright. **Imported rules are scanned too.** When you bring in third-party rules with `archgate adr import`, each `.rules.ts` runs through the same scanner as first-party rules -- **before it is written to disk**, so an imported pack cannot smuggle in code that your next `archgate check` would execute. Well-behaved rules only use the `RuleContext` methods (`ctx.readFile`, `ctx.grep`, `ctx.glob`, `ctx.ast`, etc.) and `ctx.report` for output. When a rule needs language tooling -- parsing Python, or comparing a file against its base git revision -- `ctx.ast()` is the sanctioned door; a rule never needs, and cannot open, a subprocess of its own. ### What rules cannot do - **Write files** -- the `RuleContext` API is read-only. Rules report violations but cannot modify the codebase. - **Escape the 30-second timeout** -- each rule is killed after 30 seconds of wall-clock time. - **Affect other rules** -- rules from different ADRs run in parallel but share no mutable state through the context API. - **Reach outside the allowlist through a recognized path** -- a rule may import only `node:path`, `node:url`, `node:util`, and `node:crypto`. Imports of any other module, Bun APIs (`Bun.spawn`, `Bun.file`), network access (`fetch`), subprocess or native-code access (`require`, `process.binding`), and code generation (`eval`, `new Function`) are rejected before execution in their direct, recognized forms. This is a static scan, not a jail: a capability assembled at runtime is a residual it cannot catch, which is why untrusted rule files still warrant review (see below). ## CI/CD best practices Running `archgate check` in CI is safe when you control the repository content. Extra care is needed for pull requests from external contributors. ### Trusted branches For pushes to `main` or other protected branches, `archgate check` runs code that has already been reviewed and merged. This is safe: ```yaml on: push: branches: [main] jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: archgate/check-action@v1 ``` ### Pull requests from forks When a pull request comes from a fork, the `.rules.ts` files in the PR may contain arbitrary code. This is the same risk as running any untrusted CI script. **Option 1: Require approval before running.** Use GitHub's environment protection rules or `pull_request_target` with manual approval to gate CI on review: ```yaml on: pull_request_target: jobs: check: runs-on: ubuntu-latest environment: pr-check # Requires manual approval steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - uses: archgate/check-action@v1 ``` **Option 2: Only run checks on trusted files.** Use a separate workflow that checks out the base branch's `.rules.ts` files and runs them against the PR's source files. This ensures only reviewed rules execute. **Option 3: Skip checks on fork PRs.** If your rules are primarily for internal governance, skip automated checks on fork PRs and run them manually after review: ```yaml on: pull_request: jobs: check: if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: archgate/check-action@v1 ``` ### Least-privilege runners Run `archgate check` on runners with minimal permissions. The job only needs read access to the repository -- no secrets, deployment keys, or write permissions are required: ```yaml jobs: check: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4 - uses: archgate/check-action@v1 ``` ## Local development ### Reviewing rules in new repositories When cloning or forking a repository that uses Archgate, the security scanner automatically enforces the import allowlist in `.rules.ts` files. It is a strong first line of defense -- not a complete sandbox -- so you should still review rule files before running `archgate check` for the first time: - The scanner blocks the direct routes to dangerous capabilities, but it is static analysis, not a jail: anything built at runtime — a property name (`obj[name]` with a computed `name`) or a code string — is opaque to a source scan, so review rule files you do not trust before running them - Top-level code that runs on import (before the `check` function is called) is still executed if it passes the scanner - Well-behaved rules only use the `RuleContext` methods (`ctx.readFile`, `ctx.grep`, `ctx.glob`, `ctx.ast`, etc.) and `ctx.report` for output ### Credentials The `archgate login` command stores your authentication token in the operating system's credential manager (macOS Keychain, Windows Credential Manager, or Linux libsecret) via `git credential approve`. No credentials are written to disk as plain-text files. The token is used for plugin installation and is never sent to third parties beyond the Archgate plugins service. - Plugin installation commands never embed the token in a URL or pass it as a command-line argument: git-based installs authenticate through the OS credential manager, and API downloads send the token in an `Authorization` header. Neither is visible in process listings. - To revoke access, run `archgate login logout`. ### Self-update integrity When you run `archgate upgrade`, the CLI downloads the release binary from GitHub Releases and verifies its SHA256 checksum before extraction. If the checksum does not match, the upgrade is aborted. This protects against tampered downloads due to network interception or compromised mirrors. ## Reporting vulnerabilities Please do not report security vulnerabilities through public GitHub issues. If you discover a security issue in Archgate, report it via [GitHub Security Advisories](https://github.com/archgate/cli/security/advisories/new), including a description of the vulnerability, steps to reproduce, affected versions, and any potential impact. --- ## Guides: VS Code Plugin Source: https://cli.archgate.dev/guides/vscode-plugin/ The Archgate VS Code plugin gives AI agents working in [VS Code](https://code.visualstudio.com/) built-in guardrails. Agents read your ADRs before writing code, validate after, and capture new patterns for the team -- the same workflow available in the [Claude Code plugin](/guides/claude-code-plugin/). ## How it works VS Code supports **agent plugins** installed from git-based marketplaces. The Archgate plugin is served from a git repository at `plugins.archgate.dev/archgate/vscode.git`. When you add this marketplace to your VS Code user settings, VS Code discovers and installs the plugin automatically. The plugin is served in VS Code Copilot's native `.github/plugin/` manifest format, separate from the Claude Code `.claude-plugin/` format. ## Installation Agent plugins require **VS Code 1.110 (February 2026 release) or later**. Earlier versions do not support git-based agent plugin marketplaces. Check your version with `code --version`. The VS Code plugin is currently in beta. Run `archgate login` to sign up and authenticate before following the steps below. ### 1. Log in with GitHub Authenticate with your GitHub account to obtain a plugin token: ```bash archgate login ``` This starts a GitHub Device Flow. The CLI displays a one-time code and URL -- open the URL in your browser, enter the code, and authorize. Once complete, credentials are stored securely in your OS credential manager via `git credential approve`. ### 2. Initialize your project with the plugin Run `archgate init` with the `--editor vscode` flag: ```bash archgate init --editor vscode ``` If you are already logged in, this command: 1. Creates the `.archgate/` directory with ADRs and lint rules 2. Adds the marketplace URL to your VS Code **user settings** `archgate init` does not download the VS Code extension. To install the extension itself, run `archgate plugin install --editor vscode` (see below). The `chat.plugins.marketplaces` setting is application-scoped in VS Code, so it cannot be set per-workspace. The CLI automatically writes it to your user-level `settings.json`: | Platform | User settings path | | -------- | ------------------------------------------------------- | | Windows | `%APPDATA%\Code\User\settings.json` | | macOS | `~/Library/Application Support/Code/User/settings.json` | | Linux | `~/.config/Code/User/settings.json` | | WSL | The Windows-side path above (VS Code runs on Windows) | ### 3. Install the extension To install or reinstall the extension on an already-initialized project: ```bash archgate plugin install --editor vscode ``` Without `--editor`, an interactive run detects the editors installed on your machine and lets you pick; a non-interactive run (an agent or CI) defaults to Claude Code. This re-writes the marketplace URL into your user settings and, when the `code` CLI is on your PATH, downloads the Archgate VS Code extension from the plugins service and installs it via `code --install-extension`. If the `code` CLI is not available, manual installation instructions are printed. If the install itself fails, the downloaded `.vsix` is kept at `~/.archgate/archgate.vsix` so you can install it by hand. ### Generated files The commands create or update the following: | File | Scope | Purpose | | -------------------- | ----------- | ---------------------------------------------------------------------- | | User `settings.json` | User | `chat.plugins.marketplaces` with the Archgate marketplace URL (`init`) | | Archgate extension | Application | VS Code extension installed via `.vsix` (`archgate plugin install`) | The user settings file is merged additively -- existing settings are never overwritten. VS Code's built-in default marketplaces (`github/copilot-plugins`, `github/awesome-copilot`) are preserved when the key is set for the first time. The user-level marketplace setting (added to your `settings.json`): ```json { "chat.plugins.marketplaces": [ "https://plugins.archgate.dev/archgate/vscode.git" ] } ``` The URL carries no credentials. Git authenticates against `plugins.archgate.dev` using the token `archgate login` stored in your OS credential manager. ### Manual setup If you prefer not to let the CLI modify your user settings, you can set things up manually: **Marketplace URL:** Open VS Code's user settings JSON (`Ctrl+Shift+P` → "Preferences: Open User Settings (JSON)") and add the `chat.plugins.marketplaces` entry shown above. You can print the URL by running `archgate plugin url --editor vscode`. **Extension:** Download and install the `.vsix` file directly: ```bash curl -H "Authorization: Bearer " https://plugins.archgate.dev/api/vscode -o archgate.vsix code --install-extension archgate.vsix rm archgate.vsix ``` Replace `` with the plugin token `archgate login` stored for `plugins.archgate.dev` in your OS credential manager. You can read it back with `git credential fill` (`printf 'protocol=https\nhost=plugins.archgate.dev\n\n' | git credential fill`). ## What the plugin provides The plugin adds agents and role-based skills to VS Code's AI. The developer agent orchestrates the guardrails workflow, invoking skills as needed. ### Agents | Agent | Purpose | | -------------------- | --------------------------------------------------------------------------- | | `archgate:developer` | General development agent that reads ADRs before coding and validates after | | `archgate:planner` | Planning agent for scoping work and breaking tasks into ADR-compliant steps | The `archgate:developer` agent is set as the default agent via the plugin settings. It orchestrates the skills below automatically as part of its workflow. ### Skills | Skill | Purpose | | -------------------------- | ------------------------------------------------------------------------------------- | | `archgate:reviewer` | Validates code changes against all project ADRs for structural compliance | | `archgate:lessons-learned` | Captures learnings and proposes new ADRs when patterns emerge | | `archgate:adr-author` | Creates and edits ADRs following project conventions | | `archgate:onboard` | One-time setup: explores the codebase, interviews the developer, creates initial ADRs | | `archgate:cli-reference` | Internal reference for AI agents with the complete Archgate CLI command guide | ## Initial setup with onboard After installation, run the `archgate:onboard` skill in your project once. This skill: 1. Explores your codebase structure (directories, key files, package configuration) 2. Interviews you about your team's conventions, constraints, and architectural decisions 3. Creates an initial set of ADRs based on your responses 4. Sets up the `.archgate/` directory with your first rules The onboard skill is designed to run once per project. After onboarding, the other skills handle day-to-day development. ## How it works in practice The plugin follows a structured workflow for every coding task: ### 1. Read applicable ADRs When the developer gives a coding task, the agent runs `archgate review-context` to read all ADRs that apply to the files being changed. This provides a condensed briefing with the **Decision** and **Do's and Don'ts** sections from each relevant ADR. ### 2. Write code following ADR constraints The agent writes code that complies with the constraints from the ADRs. The Do's and Don'ts sections serve as concrete guardrails. ### 3. Validate changes After writing code, the agent runs `archgate check` to execute automated rules against the changes. Any violations are fixed before proceeding. ### 4. Review changes The agent invokes `archgate:reviewer` to validate structural ADR compliance beyond what automated rules catch. ### 5. Capture learnings The agent invokes `archgate:lessons-learned` to review the work and identify patterns worth capturing as new ADRs. ## Tips - **Each developer runs `archgate init --editor vscode`** after `archgate login` to configure their user-level marketplace URL, then `archgate plugin install --editor vscode` to install the extension. - **Run onboard once per project** to generate your initial ADRs from your actual codebase. - **Keep ADR rule files up to date** -- the agent enforces what the rules check for. --- ## Guides: Writing ADRs Source: https://cli.archgate.dev/guides/writing-adrs/ ## Creating an ADR Use `archgate adr create` to generate a new ADR with the standard template. The command supports both interactive and non-interactive modes. ### Interactive mode Run the command with no arguments to get guided prompts: ```bash archgate adr create ``` You will be prompted for: 1. **Domain** -- one of the built-in `backend`, `frontend`, `data`, `architecture`, or `general`, plus any [custom domains](/concepts/domains/#custom-domains) the project has registered via `archgate adr domain add` 2. **Title** -- a short, descriptive name for the decision 3. **File patterns** -- optional comma-separated globs that scope rule checking (e.g., `src/commands/**/*.ts`) The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/`. ### Non-interactive mode Pass `--title` and `--domain` to skip prompts: ```bash archgate adr create --title "API Response Format" --domain backend --files "src/api/**/*.ts" ``` Available flags: | Flag | Description | | ----------------- | --------------------------------------------------------------------- | | `--title ` | ADR title (required for non-interactive mode) | | `--domain <name>` | Domain name (built-in or [custom](/concepts/domains/#custom-domains)) | | `--files <globs>` | Comma-separated file patterns for rule scoping | | `--rules` | Scaffold a companion `.rules.ts` file next to the ADR | | `--body <md>` | Full ADR body markdown (skip template) | | `--json` | Output the result as JSON | :::note `--rules` always writes the companion `.rules.ts` stub. It sets `rules: true` in the frontmatter only when you also pass `--body`; the generated template (no `--body`) is always written with `rules: false`, so flip that field by hand once the companion file contains real checks -- `archgate check` only loads companions for ADRs whose frontmatter says `rules: true`. ## The generated template When you create an ADR without `--body`, the CLI generates a template with all standard sections: ```markdown --- id: BE-001 title: API Response Format domain: backend rules: false files: ["src/api/**/*.ts"] --- # API Response Format ## Context Describe the context and problem statement. ## Decision Describe the decision that was made. ## Do's and Don'ts ### Do - ### Don't - ## Consequences ### Positive - ### Negative - ### Risks - ## Compliance and Enforcement Describe how this decision will be enforced. ## References - ``` ## Section-by-section writing guidance ### Context Explain why this decision was needed. What problem prompted it? What alternatives were considered and why were they rejected? Good context sections include: - The problem or pain point that triggered the decision - Alternatives that were evaluated, with brief trade-off analysis - Any constraints that narrowed the options (team size, runtime, compatibility) ```markdown ## Context The CLI needs a consistent pattern for defining and registering commands. As the command surface grows (init, check, adr, login, upgrade, clean), the registration mechanism must scale without introducing hidden coupling or making the dependency graph opaque. **Alternatives considered:** - **Auto-discovery via `executableDir()`** -- Commander.js supports automatic command discovery by scanning a directory. This hides the dependency graph and makes dead command detection impossible. - **Single-file command map** -- Simple but creates a monolithic file that grows with every command. ``` ### Decision State what was decided. Be specific and concrete -- this section should leave no ambiguity about what developers must do. Include numbered constraints when the decision has multiple facets: ```markdown ## Decision Commands live in src/commands/ and export a register\*Command(program) function. The main entry point (src/cli.ts) explicitly imports and calls each register function. **Key constraints:** 1. **One command per file** -- Each .ts file defines exactly one command 2. **Explicit registration** -- Every command must be manually imported in src/cli.ts 3. **Thin commands** -- Command files handle I/O only; no business logic ``` ### Do's and Don'ts This is the section developers and AI agents reference most frequently. Write concrete examples of correct and incorrect patterns. Use real code when possible. ```markdown ## Do's and Don'ts ### Do - Export a register\*Command function from each command module - Keep commands thin: parse args, call helpers/engine, format output - Use src/commands/<name>.ts for top-level commands ### Don't - Don't put business logic in command files -- move it to src/engine/ or src/helpers/ - Don't use executableDir() for command discovery - Don't call .parse() in command files -- the entry point handles parsing ``` ### Consequences Break consequences into three categories: - **Positive** -- benefits the team gains from this decision - **Negative** -- trade-offs the team accepts (every decision has them) - **Risks** -- what could go wrong, and how you plan to mitigate it ```markdown ## Consequences ### Positive - In-process execution enables testing without spawning subprocesses - Explicit imports make all commands visible at a glance in src/cli.ts ### Negative - Manual import bookkeeping -- each new command requires adding an import ### Risks - Stale imports when commands are removed. Mitigation: TypeScript catches missing modules at compile time. ``` ### Compliance and Enforcement Describe how this decision is enforced. There are two enforcement mechanisms: 1. **Automated rules** -- companion `.rules.ts` files that run during `archgate check` 2. **Manual enforcement** -- what code reviewers should verify ```markdown ## Compliance and Enforcement ### Automated Enforcement - **Archgate rule** ARCH-001/register-function-export: Scans all command files and verifies each exports a register\*Command function. Severity: error. ### Manual Enforcement Code reviewers MUST verify: 1. New commands are imported and registered in src/cli.ts 2. Command files delegate to engine/helpers for business logic ``` ### References Link to related ADRs, external documentation, or relevant discussions: ```markdown ## References - [Commander.js documentation](https://github.com/tj/commander.js) - [ARCH-004 -- No Barrel Files](./ARCH-004-no-barrel-files.md) - [ARCH-002 -- Error Handling](./ARCH-002-error-handling.md) ``` ## Scoping rules with `files` The `files` field in the frontmatter is an array of glob patterns. When set, `archgate check` only passes matching files to the rule's `ctx.scopedFiles`. This keeps rules focused on the code they govern. ```yaml --- id: ARCH-001 title: Command Structure domain: architecture rules: true files: ["src/commands/**/*.ts"] --- ``` If `files` is omitted, `ctx.scopedFiles` includes all project files. This is appropriate for project-wide rules like dependency policies. By default, files listed in `.gitignore` (e.g., `node_modules/`, `dist/`) are automatically excluded from `ctx.scopedFiles`, `ctx.glob()`, and `ctx.grepFiles()`. To include gitignored files, set `respectGitignore: false`: ```yaml --- id: BUILD-001 title: Build Output Structure domain: architecture rules: true respectGitignore: false files: ["dist/**/*.js"] --- ``` The CLI warns when an ADR's `files` patterns resolve to more than 1,000 files or the glob scan takes over 2 seconds. Broad patterns like `**/*.ts` may cause slow checks in large projects. Prefer targeting specific directories (e.g., `src/commands/**/*.ts`) over project-wide globs. Common patterns: | Pattern | Matches | | ---------------------- | -------------------------------- | | `src/commands/**/*.ts` | All TypeScript files in commands | | `src/**/*.ts` | All TypeScript source files | | `package.json` | Only the root package.json | | `src/api/**/*.ts` | API layer files | | `tests/**/*.test.ts` | Test files | ## When to set `rules: true` vs `rules: false` Set `rules: true` when you have a companion `.rules.ts` file with automated checks. The file must be named identically to the ADR markdown file but with a `.rules.ts` extension: ``` .archgate/adrs/ ARCH-001-command-structure.md # rules: true ARCH-001-command-structure.rules.ts # companion rules file ARCH-002-error-handling.md # rules: true ARCH-002-error-handling.rules.ts # companion rules file GEN-001-code-review-process.md # rules: false (no automated checks) ``` Set `rules: false` for decisions that are enforced through code review alone -- process decisions, team agreements, or guidelines that are difficult to check programmatically. ## Updating ADRs Use `archgate adr update` to modify an existing ADR: ```bash archgate adr update --id ARCH-001 --body "## Context\n\nUpdated context..." --title "New Title" ``` The `--id` and `--body` flags are required. All other frontmatter fields (`--title`, `--domain`, `--files`, `--rules`) are optional and preserve their existing values when omitted. | Flag | Description | | ----------------- | ------------------------------------------------- | | `--id <id>` | ADR ID to update (required) | | `--body <md>` | Full replacement body markdown (required) | | `--title <title>` | New title (preserves existing if omitted) | | `--domain <name>` | New domain (preserves existing if omitted) | | `--files <globs>` | New file patterns (preserves existing if omitted) | | `--rules` | Set `rules: true` | | `--json` | Output the result as JSON | ## Tips for effective ADRs 1. **Keep each ADR focused on a single decision.** If you find yourself writing about two unrelated topics, split them into separate ADRs. 2. **Be specific in Do's and Don'ts.** Vague guidelines like "write clean code" are not actionable. Show concrete code patterns. 3. **Include real code examples.** The Do's and Don'ts section is where developers and AI agents look first. Annotated code examples make the decision unambiguous. 4. **Document alternatives you rejected.** Future contributors will ask "why didn't we use X?" The Context section should answer that question. 5. **State trade-offs honestly.** Every decision has negative consequences. Documenting them builds trust and helps the team understand what was traded away. 6. **Write rules for decisions that can be checked automatically.** If a rule can catch a violation before code review, set `rules: true` and write a companion `.rules.ts` file. See the [Writing Rules](/guides/writing-rules/) guide. 7. **Use domain prefixes to organize.** The domain field determines the ID prefix and helps filter ADRs by area. Prefer the five built-ins (`backend`, `frontend`, `data`, `architecture`, `general`); register a [custom domain](/concepts/domains/#custom-domains) only when none of them is a genuine fit. The editor plugins for [Claude Code](/guides/claude-code-plugin/) and [Cursor](/guides/cursor-integration/) include an ADR Author skill that creates and updates ADRs following your project's conventions. The Quality Manager skill also proposes new ADRs when it detects recurring patterns. [Sign up for beta access](https://plugins.archgate.dev). --- ## Guides: Writing Rules Source: https://cli.archgate.dev/guides/writing-rules/ Rules are TypeScript checks that lint your codebase against your team's decisions. They live in companion `.rules.ts` files next to ADR markdown files and run when you execute `archgate check`. ``` .archgate/adrs/ ARCH-001-command-structure.md # The decision ARCH-001-command-structure.rules.ts # The automated checks ``` The companion file must share the ADR file's base name, and the ADR's frontmatter must set `rules: true` -- `archgate check` loads companions only for ADRs that declare it. An ADR with `rules: true` and no companion file is reported as a rule error (exit code 2). ## Basic setup Every rules file exports a default plain object typed with `satisfies RuleSet`. Each key in the `rules` object becomes a rule ID, and each rule has a `description` and an async `check` function that receives a context object. ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "my-rule-id": { description: "What this rule checks", async check(ctx) { // Your check logic here }, }, }, } satisfies RuleSet; ``` :::note The `/// <reference path>` directive may conflict with the `@typescript-eslint/triple-slash-reference` rule in ESLint or oxlint. If your project uses this rule, disable it for `.archgate/adrs/` files. For **ESLint** (flat config): ```js { files: [".archgate/adrs/*.rules.ts"], rules: { "@typescript-eslint/triple-slash-reference": "off" }, } ``` For **oxlint** (`.oxlintrc.json`): ```json { "overrides": [ { "files": [".archgate/adrs/*.rules.ts"], "rules": { "typescript/triple-slash-reference": "off" } } ] } ``` A single rules file can define multiple rules: ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "first-rule": { description: "Checks one thing", async check(ctx) { // ... }, }, "second-rule": { description: "Checks another thing", async check(ctx) { // ... }, }, }, } satisfies RuleSet; ``` ## The Context API The `ctx` object passed to every `check` function provides file reading, searching, and reporting capabilities. Here is a detailed reference with examples. ### ctx.scopedFiles An array of file paths matching the ADR's `files` glob from its frontmatter. If the ADR has no `files` field, this includes all project files. ```typescript for (const file of ctx.scopedFiles) { const content = await ctx.readFile(file); // Check content... } ``` Use `ctx.scopedFiles` when your rule should only apply to files the ADR governs. For example, a command structure rule scoped to `src/commands/**/*.ts` will only receive command files. ### ctx.changedFiles An array of file paths that differ from the base branch, including uncommitted working-tree changes (staged, unstaged, and untracked non-ignored files). Auto-detected by default, or populated from `--staged` / `--base <ref>`. Useful for incremental checking and cross-file dependency rules. ```typescript // Incremental checking -- only validate changed files const filesToCheck = ctx.scopedFiles.filter((f) => ctx.changedFiles.includes(f) ); // Cross-file dependency -- if file A changed, file B must also change if (ctx.changedFiles.includes("config/database.yml")) { if (!ctx.changedFiles.includes("deploy/manifest.yml")) { ctx.report.violation({ message: "config changed but manifest was not bumped", file: "config/database.yml", }); } } ``` ### ctx.readFile(path) Read a file's content as a string. The path is relative to the project root. ```typescript const content = await ctx.readFile("src/cli.ts"); ``` ### ctx.readJSON(path) Read and parse a JSON file. Returns `unknown` for an arbitrary path -- cast it to the expected shape. The literal path `"package.json"` has a dedicated overload that returns a typed `PackageJson`, so no cast is needed there. ```typescript // Typed by the overload -- pkg.dependencies is Record<string, string> | undefined const pkg = await ctx.readJSON("package.json"); // Any other path returns unknown const tsconfig = (await ctx.readJSON("tsconfig.json")) as { compilerOptions?: Record<string, unknown>; }; ``` ### ctx.grep(file, pattern) Search a single file with a regular expression. Returns an array of `GrepMatch` objects, each with `file`, `line`, `column`, and `content` properties. ```typescript const matches = await ctx.grep(file, /console\.error\(/); for (const match of matches) { ctx.report.violation({ message: "Use logError() instead of console.error()", file: match.file, line: match.line, }); } ``` ### ctx.grepFiles(pattern, fileGlob) Search across multiple files matching a glob pattern. Returns a flat array of `GrepMatch` objects from all matching files. Files ignored by `.gitignore` are excluded by default. Set `respectGitignore: false` in the ADR frontmatter to include them. ```typescript const matches = await ctx.grepFiles(/TODO:/, "src/**/*.ts"); for (const match of matches) { ctx.report.warning({ message: "TODO comment found", file: match.file, line: match.line, }); } ``` ### ctx.glob(pattern) Find files by glob pattern. Returns an array of file paths relative to the project root. Files ignored by `.gitignore` are excluded by default. Set `respectGitignore: false` in the ADR frontmatter to include them. ```typescript const testFiles = await ctx.glob("tests/**/*.test.ts"); ``` ### ctx.ast(path, language, opts?) Parse a source file into its language-native AST. Supported languages: `"typescript"`, `"javascript"`, `"python"`, and `"ruby"`. TypeScript and JavaScript parse in-process into an ESTree `Program`; Python and Ruby shell out to the system interpreter's standard-library parser. Throws on parse failure or missing interpreter -- it never returns `null`. ```typescript const program = await ctx.ast("src/cli.ts", "typescript"); ``` An optional third argument covers two more needs: - `rev: "base"` parses the file at the **base git revision** instead of the working tree -- pair it with a working-tree parse to detect whether the executable structure changed (see below). - `comments: true` attaches a structured `comments` array to the tree (all four languages), so comment-governance rules can work against comment tokens instead of regex. See [Structural checks with ctx.ast()](#structural-checks-with-ctxast) below for complete examples, and the [Rule API Reference](/reference/rule-api/#ast) for the returned shape per language and the full options. ### ctx.fileAtBase(path) Read a file's source at the **base git revision** -- the merge base of the base ref and `HEAD`. `archgate check` resolves that ref itself (`--base <ref>`, then the configured base branch, then auto-detection), so a base is normally available without passing any flag. Returns `null` when no base could be resolved (a `--staged` run, a project that is not a git repository, no detectable base branch, or unrelated histories) or when the file did not exist at the base (an added file), so a single null check covers both. For a structural comparison, prefer `ctx.ast()` with `rev: "base"`. ```typescript const before = await ctx.fileAtBase("src/config.ts"); if (before !== null && before !== (await ctx.readFile("src/config.ts"))) { // the file changed since the base } ``` ### ctx.report The reporting interface with three severity methods: - `ctx.report.violation(detail)` -- error severity (exit code 1, blocks CI) - `ctx.report.warning(detail)` -- warning severity (logged but does not block) - `ctx.report.info(detail)` -- informational (logged for visibility) Each method accepts an object with: | Field | Type | Required | Description | | ----------- | -------- | -------- | --------------------------------------------------------------- | | `message` | `string` | Yes | What the violation is | | `file` | `string` | No | Path to the offending file | | `line` | `number` | No | Line number of the violation | | `endLine` | `number` | No | End line of the offending range (`--output json` and SARIF) | | `endColumn` | `number` | No | End column of the offending range (`--output json` only) | | `fix` | `string` | No | Suggested fix (`--output json`; console only under `--verbose`) | ```typescript ctx.report.violation({ message: "Command file must export a register*Command function", file: "src/commands/check.ts", line: 5, fix: "Add: export function registerCheckCommand(program: Command) { ... }", }); ``` ### ctx.projectRoot The absolute path to the project root directory. Useful when you need to construct absolute paths. ## Structural checks with ctx.ast() Regex works for surface patterns, but breaks down for structural questions like "does this file contain only re-exports?" or "is this a bare `except:` clause?" -- multi-line statements, comments, and string contents all defeat line-based matching. `ctx.ast()` parses a file into a real syntax tree so your rule can check structure directly. The returned tree is language-native, not unified across languages: - **TypeScript / JavaScript** -- an [ESTree](https://github.com/estree/estree) `Program` parsed in-process by [meriyah](https://github.com/meriyah/meriyah). TypeScript is transpiled first, so type-only syntax (`interface`, type aliases, `export type { ... } from`) is erased from the tree; a file containing only type-level statements parses to an empty `Program` body. - **Python** -- the standard-library [`ast` module](https://docs.python.org/3/library/ast.html)'s tree serialized to JSON. Each node is an object with a `_type` field plus the node's own fields and `lineno` / `col_offset` positions. - **Ruby** -- the standard-library [`Ripper`](https://docs.ruby-lang.org/en/master/Ripper.html)'s `Ripper.sexp` output: nested arrays like `["program", [["command", ...]]]` with `[line, column]` position pairs. `ctx.ast()` throws on parse failure or a missing interpreter -- it never returns `null`. The throw is isolated to the failing rule and surfaces as a rule execution error (exit code 2), so a broken environment shows up as a visible failure rather than a false pass. :::caution Python and Ruby parsing invokes the system interpreter. The corresponding interpreter (`python3`/`python`, `ruby`) must be on `PATH` wherever `archgate check` runs -- on every developer machine **and** in CI. TypeScript and JavaScript parsing is built into Archgate and needs no interpreter. ### TypeScript: no barrel files A barrel file is an `index.ts` whose every top-level statement is a re-export. With the ESTree `Program`, that question becomes a direct check on statement types instead of a line-matching heuristic: ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "no-barrel-files": { description: "index.ts files must not be pure re-export barrels", async check(ctx) { const indexFiles = ctx.scopedFiles.filter((f) => f.endsWith("/index.ts") ); const checks = indexFiles.map(async (file) => { const program = await ctx.ast(file, "typescript"); const isBarrel = program.body.length > 0 && program.body.every( (node) => node.type === "ExportAllDeclaration" || (node.type === "ExportNamedDeclaration" && node.source !== null) ); if (isBarrel) { ctx.report.violation({ message: `Barrel file detected: ${file} contains only re-exports`, file, fix: "Delete the barrel and import directly from the source modules", }); } }); await Promise.all(checks); }, }, }, } satisfies RuleSet; ``` Because TypeScript is transpiled before parsing, `export type { ... } from` re-exports are erased -- a barrel containing only type re-exports parses to an empty `body`, which the `body.length > 0` guard skips. If you also need to flag type-only barrels, combine the AST check with a text check. The same transpilation also shifts positions: `loc` line numbers refer to the transpiled text, not your original `.ts` file, so a `"typescript"` rule must re-locate the construct in the original source (for example with `ctx.readFile()` and `indexOf`) before reporting a `line` -- or omit `line`. ### JavaScript: no require() in ES modules Walk the ESTree tree for `CallExpression` nodes whose callee is the identifier `require`. A recursive walk over object values and arrays covers every node type without enumerating them: ```typescript /// <reference path="../rules.d.ts" /> function findRequireCalls(node: unknown, lines: number[]): void { if (Array.isArray(node)) { for (const item of node) findRequireCalls(item, lines); return; } if (node === null || typeof node !== "object") return; const n = node as EsTreeNode; const callee = n.callee as EsTreeNode | undefined; if ( n.type === "CallExpression" && callee?.type === "Identifier" && callee.name === "require" ) { if (n.loc) lines.push(n.loc.start.line); } for (const value of Object.values(n)) findRequireCalls(value, lines); } export default { rules: { "no-require-in-esm": { description: ".mjs files must not call CommonJS require()", async check(ctx) { const files = await ctx.glob("src/**/*.mjs"); const checks = files.map(async (file) => { const program = await ctx.ast(file, "javascript"); const lines: number[] = []; findRequireCalls(program, lines); for (const line of lines) { ctx.report.violation({ message: "CommonJS require() call in an ES module", file, line, fix: "Use a static import or await import() instead", }); } }); await Promise.all(checks); }, }, }, } satisfies RuleSet; ``` The `loc.start.line` reporting here is valid only because `"javascript"` parses the original source untranspiled -- a `"typescript"` rule must locate the line in the original source instead, since its `loc` refers to the transpiled output. ### Python: no bare except clauses In the Python `ast` module, an `except:` clause is an `ExceptHandler` node whose `type` field holds the caught exception expression -- a bare `except:` has `"type": null`. Walk the JSON tree for that shape: ```typescript /// <reference path="../rules.d.ts" /> function findBareExcepts(node: unknown, lines: number[]): void { if (Array.isArray(node)) { for (const item of node) findBareExcepts(item, lines); return; } if (node === null || typeof node !== "object") return; const n = node as PythonAstNode; if (n._type === "ExceptHandler" && n.type === null) { if (n.lineno !== undefined) lines.push(n.lineno); } for (const value of Object.values(n)) findBareExcepts(value, lines); } export default { rules: { "no-bare-except": { description: "Python code must not use bare except: clauses", async check(ctx) { const files = await ctx.glob("**/*.py"); const checks = files.map(async (file) => { const tree = await ctx.ast(file, "python"); const lines: number[] = []; findBareExcepts(tree, lines); for (const line of lines) { ctx.report.violation({ message: "Bare except: catches every exception, including SystemExit", file, line, fix: "Catch a specific exception class, e.g. except ValueError:", }); } }); await Promise.all(checks); }, }, }, } satisfies RuleSet; ``` ### Ruby: no puts calls `Ripper.sexp` represents `puts "hello"` as `["command", ["@ident", "puts", [1, 0]], [...args]]` -- the `[line, column]` pair sits inside the `@ident` token. Walk the nested arrays for that shape: ```typescript /// <reference path="../rules.d.ts" /> function findPutsCalls(node: unknown, lines: number[]): void { if (!Array.isArray(node)) return; const [kind, first] = node; if ( kind === "command" && Array.isArray(first) && first[0] === "@ident" && first[1] === "puts" ) { const [line] = first[2] as [number, number]; lines.push(line); } for (const child of node) findPutsCalls(child, lines); } export default { rules: { "no-puts": { description: "Ruby code must use the application logger, not puts", async check(ctx) { const files = await ctx.glob("app/**/*.rb"); const checks = files.map(async (file) => { const sexp = await ctx.ast(file, "ruby"); const lines: number[] = []; findPutsCalls(sexp, lines); for (const line of lines) { ctx.report.violation({ message: "puts writes to stdout directly", file, line, fix: "Replace puts with logger.info", }); } }); await Promise.all(checks); }, }, }, } satisfies RuleSet; ``` Ripper uses a different node shape for the parenthesized form -- `puts("hello")` appears under a `method_add_arg` / `fcall` pair rather than `command` -- so a production rule would match the `fcall` token the same way. ### Comparing against the base revision Some rules care not about a file's current state but about **what changed**. A common one: waive a requirement (a version bump, a changelog entry) when a change is documentation-only -- the executable structure is untouched and only comments or formatting moved. Calling `ctx.ast()` with `rev: "base"` parses the file at the base git revision, so you can compare it against the working-tree parse. Comments are absent from the ESTree and Python `ast` shapes -- but node **positions are not**: `loc` / `lineno` fields shift when a comment or blank line moves the lines below it, so a raw serialization differs even for a doc-only edit. Compare a **location-free projection** of the two trees instead. (Python docstrings are ordinary string nodes in the `ast`, so an edited docstring is a genuine tree change -- strip them too if your rule should treat doc edits as neutral.) ```typescript export default { rules: { "bump-required-on-behavior-change": { description: "A .py change that alters behavior must bump the version", async check(ctx) { // Drop position metadata so only executable structure is compared: a // comment or blank line shifts lineno/col_offset without changing // behavior. (Strip docstrings here too if doc edits should be neutral.) const POS = ["lineno", "col_offset", "end_lineno", "end_col_offset"]; const structure = (node: unknown): unknown => { if (Array.isArray(node)) return node.map(structure); if (node && typeof node === "object") { return Object.fromEntries( Object.entries(node) .filter(([k]) => !POS.includes(k)) .map(([k, v]) => [k, structure(v)]) ); } return node; }; const changed = ctx.changedFiles.filter((f) => f.endsWith(".py")); for (const file of changed) { // No base counterpart (e.g. an added file) -- skip. if ((await ctx.fileAtBase(file)) === null) continue; const before = await ctx.ast(file, "python", { rev: "base" }); const after = await ctx.ast(file, "python"); if ( JSON.stringify(structure(before)) !== JSON.stringify(structure(after)) ) { ctx.report.violation({ message: `${file} changed behavior -- bump the version`, file, }); } } }, }, }, } satisfies RuleSet; ``` `archgate check` resolves a base automatically (or takes an explicit `--base <ref>`), and `ctx.changedFiles` is populated from it -- including uncommitted work. The per-file guard is what makes the rule safe: when no base is resolved, or for a file absent at the base, `ctx.fileAtBase()` returns `null` and the loop skips that file. Because base parsing goes through `ctx.ast()` and its git access is built into Archgate, the rule never runs `git` or an interpreter itself -- which the [rule sandbox](/guides/security/) would block anyway. ### Governing comments Calling `ctx.ast()` with `comments: true` attaches a `comments` array to the tree so comment-policy rules work against structured tokens (`type`, `value`, `loc`) instead of line-by-line regex. Comment positions are accurate against the original source even for TypeScript. Supported for all four languages -- for `ruby`, where the tree is a `Ripper.sexp` array, the `comments` array rides on that array as a non-index property. ```typescript const tree = await ctx.ast(file, "typescript", { comments: true }); for (const comment of tree.comments ?? []) { if (comment.value.split("\n").length > 10) { ctx.report.warning({ message: "Comment block too long -- move rationale to an ADR", file, line: comment.loc.start.line, }); } } ``` :::note Rule files may import only `node:path`, `node:url`, `node:util`, and `node:crypto`. Anything a rule needs beyond that -- parsing a language, reading a base revision -- comes through the `RuleContext` API. See [Rule execution security](/guides/security/) for why. ## Severity levels A finding's severity is decided by the `ctx.report.*` method that produced it -- there is no separate severity setting to keep in sync: | Report method | Severity | Exit code | Behavior | | ------------------------ | --------- | --------- | ------------------------------------ | | `ctx.report.violation()` | `error` | 1 | Blocks CI, must be fixed | | `ctx.report.warning()` | `warning` | 0 | Logged but does not block | | `ctx.report.info()` | `info` | 0 | Informational, logged for visibility | So a rule that should never block CI reports through `warning()`: ```typescript export default { rules: { "my-rule": { description: "...", async check(ctx) { // Reported as a warning -- the check still exits 0 ctx.report.warning({ message: "..." }); }, }, }, } satisfies RuleSet; ``` A single rule can mix all three, reporting some findings as errors and others as warnings or notes. :::caution `RuleConfig` also accepts an optional `severity` field. It is validated when the rule file loads, but it does **not** change the severity of anything the rule reports -- `ctx.report.violation()` is always an error even in a rule declared `severity: "warning"`. Choose the report method, not the field. Under `archgate check --strict`, any `warning`-severity finding fails the run (exit 1) instead of merely being logged. `info` findings never affect the exit code. ## Rule timeout Each rule has a 30-second execution timeout. If a rule exceeds this limit, it is treated as an error. This prevents runaway checks from blocking the pipeline. Keep rules fast by: - Using `ctx.grepFiles()` instead of reading every file manually - Using `Promise.all()` to check files in parallel - Scoping rules with the `files` frontmatter field to limit the number of files processed ## The `fix` field The `fix` field is an optional string that describes what action to take to resolve the issue. Fixes are not auto-applied -- they are guidance. ```typescript ctx.report.violation({ message: `Unapproved dependency: "chalk"`, file: "package.json", fix: "Use styleText() from node:util instead of chalk", }); ``` The console reporter prints the fix under the violation **only with `--verbose`**; otherwise it shows the message and `file:line` alone: ``` x ARCH-006/no-unapproved-deps Only approved dependencies may be added [error] Unapproved dependency: "chalk" package.json fix: Use styleText() from node:util instead of chalk ``` `--output json` always carries the `fix` string, so agents and editor integrations see it regardless of `--verbose`. The GitHub Actions and SARIF reporters emit the message only -- put anything a reviewer must read into `message`, not `fix`. ## Tips for writing rules 1. **Use `Promise.all()` for parallel file checks.** When checking multiple files independently, process them in parallel instead of sequentially. ```typescript // Good: parallel const checks = files.map(async (file) => { const content = await ctx.readFile(file); // ... }); await Promise.all(checks); // Avoid: sequential for (const file of files) { const content = await ctx.readFile(file); // ... } ``` 2. **Use `ctx.changedFiles` for incremental checking.** `ctx.changedFiles` is auto-populated with the branch diff plus uncommitted working-tree changes (or staged files with `--staged`). Filter `ctx.scopedFiles` against it to check only what changed, or use it directly for cross-file dependency rules. You do not need to guard against an empty intersection: when the change set is non-empty and none of it matches the ADR's `files` globs, the ADR is [skipped entirely](/reference/cli/check/#skipping-untouched-adrs) and the rule is never called. 3. **Keep rules focused on one concern.** A rule that checks both naming conventions and import patterns should be split into two rules with separate IDs. 4. **Use `ctx.grepFiles()` over manual iteration.** When searching for a pattern across many files, `ctx.grepFiles()` is more efficient than reading each file and running a regex. 5. **Provide actionable `fix` messages.** A fix like "Don't do this" is not helpful. Tell the developer exactly what to do instead. 6. **Filter out non-applicable files early.** If your rule only applies to certain files within the scope, filter `ctx.scopedFiles` before processing: ```typescript const commandFiles = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts")); ``` 7. **Handle missing files gracefully.** If your rule reads a specific file like `package.json`, wrap the read in a try/catch and return early if the file does not exist. ## Opt-out directives There are two ways to handle exceptions in Archgate: **engine-level suppression** (works with any rule automatically) and **custom rule-level directives** (implemented by the rule author for domain-specific opt-outs). ### Engine-level suppression Archgate supports inline `archgate-ignore` comments that suppress violations without modifying the rule itself. The engine parses these comments and filters matching violations before reporting. **Next-line suppression** suppresses the violation on the immediately following line: ```typescript // archgate-ignore ARCH-006/no-unapproved-deps legacy dep, migration planned for Q3 import chalk from "chalk"; ``` **File-level suppression** suppresses all matching violations anywhere in the file: ```typescript // archgate-ignore-file ARCH-005/test-mirrors-src generated file, no manual test ``` **Multiple rules**: stack comments to suppress more than one rule on the same line: ```typescript // archgate-ignore ARCH-006/no-unapproved-deps legacy dep // archgate-ignore ARCH-003/use-style-text third-party lib handles colors import chalk from "chalk"; ``` Consecutive suppression comments all target the first non-suppression line that follows. The format is `ADR-ID/rule-id` followed by a reason. The reason is **required**. A suppression without a reason is ignored and produces a warning: ``` [suppression] Suppression for ARCH-006/no-unapproved-deps is missing a reason src/foo.ts:1 ``` Both `//` and `#` comment styles are supported, so suppressions work in TypeScript, JavaScript, YAML, Python, shell scripts, and other file types your rules may scan. **Markdown** files (`.md`, `.mdx`) also accept the HTML-comment form, which stays invisible in the rendered page — `#` would become a heading and `//` would render as body text: ```md <!-- archgate-ignore ARCH-021/no-escaped-backtick the backtick is quoted from a shell transcript --> <!-- archgate-ignore-file ARCH-021/ai-writing-signs verbatim excerpt from an external RFC --> ``` The comment must occupy the whole line, and directives inside fenced code blocks are ignored so documented examples stay inert. :::tip Unused suppression comments (comments that don't match any violation) also produce warnings, helping you clean up stale exceptions as code evolves. ### Custom rule-level directives For domain-specific opt-outs, rule authors can implement their own comment-based directives inside the `check` function. This pattern gives the rule full control over the directive syntax, placement, and validation. ```typescript // In your .rules.ts file: async check(ctx) { const files = await ctx.glob("src/components/**/*Connected.tsx"); for (const file of files) { const content = await ctx.readFile(file); // Support opt-out directive at the top of the file if (/^\/\/\s*@no-presentational:/u.test(content.trimStart())) continue; // ... rule logic that may report a violation ... ctx.report.violation({ message: "Missing presentational component", file, fix: 'Add "// @no-presentational: <reason>" at the top of the file to opt out', }); } } ``` The developer opts out by adding the directive to their file: ```typescript // @no-presentational: this component only redirects, no UI to render import { useNavigate } from "react-router"; ``` ### When to use which | Approach | Best for | Who controls it | | ----------------- | ------------------------------------------------ | ------------------------ | | `archgate-ignore` | Ad-hoc exceptions for any rule | Developer using the rule | | Custom directive | Domain-specific opt-outs with structured reasons | Rule author | Use `archgate-ignore` when a developer needs to suppress a one-off violation. Use custom directives when the opt-out is a first-class concept in your rule's domain, for example, marking a component as intentionally unpaired, or a file as auto-generated. ## Next steps - [Common Rule Patterns](/examples/common-rule-patterns/): Copy-pasteable patterns organized by category: dependency management, import restrictions, file structure, code quality, database schema, and architecture boundaries. - [Rule API Reference](/reference/rule-api/): Full reference for all rule API types and functions. - [CI Integration](/guides/ci-integration/): Wire `archgate check` into your pipeline to enforce rules on every PR. --- ## Reference: ADR Schema Source: https://cli.archgate.dev/reference/adr-schema/ Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. ## Frontmatter Schema The YAML frontmatter block sits between `---` delimiters at the top of the file. ```yaml --- id: ARCH-001 title: Command Structure domain: architecture rules: true files: ["src/commands/**/*.ts"] --- ``` ### Fields | Field | Type | Required | Description | | ------------------ | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | `string` | Yes | Unique identifier. Must be non-empty. Convention: `PREFIX-NNN` (e.g., `ARCH-001`). | | `title` | `string` | Yes | Human-readable title of the decision. Must be non-empty. | | `domain` | `string` | Yes | A registered domain name in lowercase kebab-case. Built-ins: `backend`, `frontend`, `data`, `architecture`, `general`. Custom domains are those registered via [`archgate adr domain add`](/reference/cli/adr/#archgate-adr-domain). | | `rules` | `boolean` | Yes | Whether this ADR has a companion `.rules.ts` file with automated checks. | | `files` | `string[]` | No | Glob patterns that scope which files the rules apply to. | | `respectGitignore` | `boolean` | No | Whether to filter out `.gitignore`d files. Defaults to `true`. | ### id The ADR identifier. By convention, it uses the domain prefix followed by a zero-padded sequence number (e.g., `ARCH-001`, `BE-003`). The `archgate adr create` command generates IDs automatically. Any non-empty string is valid, but following the prefix convention keeps ADRs organized and sortable. ### title A short, descriptive name for the architectural decision. Displayed in `archgate adr list` output and used as the heading when AI agents reference the ADR. ### domain Groups related ADRs together. The domain also determines the ID prefix used by `archgate adr create`. Projects can extend the built-in set with custom domains via [`archgate adr domain add`](/reference/cli/adr/#archgate-adr-domain). Custom domain → prefix mappings live in `.archgate/config.json` and are merged with the built-ins at read time. ### rules Set to `true` when this ADR has a companion `.rules.ts` file. When `archgate check` runs, it skips ADRs where `rules` is `false`. ### files An optional array of glob patterns that scope the rule's file coverage. When present, `ctx.scopedFiles` in the rules file only contains files matching these patterns. When absent, all project files are in scope. ```yaml files: ["src/commands/**/*.ts"] ``` Multiple patterns can be specified: ```yaml files: ["src/api/**/*.ts", "src/middleware/**/*.ts"] ``` ### respectGitignore Controls whether `.gitignore`d files are excluded from `ctx.scopedFiles`, `ctx.glob()`, and `ctx.grepFiles()`. Defaults to `true` when omitted -- gitignored files (e.g., `node_modules/`, `dist/`) are automatically filtered out. Set to `false` when a rule intentionally needs to inspect ignored files, such as checking build output structure: ```yaml respectGitignore: false files: ["dist/**/*.js"] ``` When `respectGitignore` is `false`, all files matched by the `files` globs are included regardless of `.gitignore` rules. When not inside a git repository, this field has no effect -- all matched files are included. --- ## Domain Prefixes Each domain maps to a prefix used in the ADR ID convention. ### Built-in domains | Domain | Prefix | Example ID | | -------------- | ------ | ---------- | | `backend` | `BE` | `BE-001` | | `frontend` | `FE` | `FE-001` | | `data` | `DATA` | `DATA-001` | | `architecture` | `ARCH` | `ARCH-001` | | `general` | `GEN` | `GEN-001` | The `archgate adr create` command uses this mapping to auto-generate IDs. ### Custom domains Projects can register additional domain → prefix mappings via [`archgate adr domain add`](/reference/cli/adr/#archgate-adr-domain). Once registered, custom domains behave like built-ins: `archgate adr create --domain <name>` auto-generates IDs using the associated prefix, and ADRs with custom domains parse cleanly. See the [Domains concept page](/concepts/domains/) for guidance on when to introduce a custom domain. --- ## File Naming Convention ADR files follow a naming convention that encodes the ID and a human-readable slug: ``` {ID}-{slug}.md # The document {ID}-{slug}.rules.ts # The companion rules file (optional) ``` For example: ``` ARCH-001-command-structure.md ARCH-001-command-structure.rules.ts ``` The slug is a kebab-case version of the title, auto-generated by `archgate adr create`. --- ## Markdown Sections After the frontmatter, the ADR body follows a standard section structure. While Archgate does not enforce specific sections, the following structure is recommended for consistency. ### Context Describes the problem or situation that prompted the decision. Include alternatives that were considered and why they were rejected. ```markdown ## Context The CLI returns errors in inconsistent formats. Some commands print raw stack traces, others print nothing, and a few use `console.error()` with custom formatting. **Alternatives considered:** - **No standard** -- Let each command handle errors its own way. Simple but leads to an inconsistent user experience. - **Try/catch wrapper** -- A global try/catch at the CLI entry point. Loses context about which command failed. ``` ### Decision States the decision itself and its key constraints. This is the primary section AI agents read before writing code. ```markdown ## Decision All commands MUST use `logError()` from `src/helpers/log.ts` for error output. Commands MUST NOT call `console.error()` directly. ``` ### Do's and Don'ts Concrete, actionable guidance split into two sub-sections. These act as a quick-reference checklist for developers and AI agents. ```markdown ## Do's and Don'ts ### Do - Use `logError(message, detail?)` for all error output - Include a suggested fix in the detail parameter when possible - Exit with code 1 for user errors, code 2 for internal errors ### Don't - Don't call `console.error()` directly in command files - Don't print stack traces to users - Don't exit without printing an error message first ``` ### Consequences Split into three sub-sections that document trade-offs. ```markdown ## Consequences ### Positive - Consistent error formatting across all commands - Machine-parseable error output when combined with `--json` ### Negative - Requires importing `logError` in every command file - Cannot use built-in error formatting from libraries ### Risks - New contributors may use `console.error()` by habit. Mitigated by the automated rule that scans for direct `console.error()` calls. ``` ### Compliance and Enforcement Describes how the decision is enforced through automated rules and manual review. ```markdown ## Compliance and Enforcement ### Automated Enforcement - **Archgate rule** ARCH-002/no-console-error: Scans command files for `console.error()` calls. Severity: error. ### Manual Enforcement Code reviewers MUST verify: 1. Error messages are actionable and include context 2. Exit codes match the error type (1 for user, 2 for internal) ``` ### References Links to related ADRs, external documentation, or design documents. ```markdown ## References - [ARCH-001 -- Command Structure](./ARCH-001-command-structure.md) - [Node.js process.exit documentation](https://nodejs.org/api/process.html#processexitcode) ``` --- ## Companion Rules File When `rules: true`, Archgate looks for a companion file with the same name but `.rules.ts` extension. ``` ARCH-002-error-handling.md # rules: true in frontmatter ARCH-002-error-handling.rules.ts # companion rules file ``` The rules file must export a default `RuleSet` using a plain object with `satisfies RuleSet`: ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "no-console-error": { description: "Use logError() instead of console.error()", async check(ctx) { for (const file of ctx.scopedFiles) { const matches = await ctx.grep(file, /console\.error\(/); for (const match of matches) { ctx.report.violation({ message: "Use logError() instead of console.error()", file: match.file, line: match.line, fix: "Import logError from src/helpers/log and use it instead", }); } } }, }, }, } satisfies RuleSet; ``` See the [Rule API](/reference/rule-api/) for the complete TypeScript API reference. --- ## Validation The YAML frontmatter is validated at parse time using a Zod schema. Invalid frontmatter causes a parse error with a descriptive message. ### Required fields If a required field is missing, the ADR fails to parse: ``` Invalid ADR frontmatter in ARCH-001-example.md: - domain: Invalid input: expected string, received undefined ``` ### Invalid domain format If `domain` is not a valid kebab-case identifier (e.g., has uppercase letters or spaces): ``` Invalid ADR frontmatter in ARCH-001-example.md: - domain: domain must be lowercase kebab-case (e.g. 'backend', 'ml-ops') ``` Note: the parser accepts any name that matches the kebab-case pattern. Whether a specific name is "known" to the project (and therefore has a prefix that `archgate adr create` can use) depends on the built-in set plus any custom domains registered via [`archgate adr domain add`](/reference/cli/adr/#archgate-adr-domain). Creating an ADR with an unregistered domain name fails with a "Unknown ADR domain" error that suggests running `archgate adr domain add`. ### Type mismatches If `rules` is a string instead of a boolean: ``` Invalid ADR frontmatter in ARCH-001-example.md: - rules: Invalid input: expected boolean, received string ``` ADRs that fail validation are skipped by `archgate check` and reported as errors. --- ## Reference: archgate adr Source: https://cli.archgate.dev/reference/cli/adr/ ## archgate adr create Create a new ADR interactively or via flags. ```bash archgate adr create [options] ``` When run without `--title` and `--domain`, the command prompts interactively for the domain, title, and optional file patterns. When both `--title` and `--domain` are provided, it runs non-interactively. The ADR ID is auto-generated with the domain prefix and the next available sequence number (e.g., `ARCH-002`, `BE-001`). ### Options | Option | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--title <title>` | ADR title (skip interactive prompt) | | `--domain <domain>` | ADR domain. Built-ins: `backend`, `frontend`, `data`, `architecture`, `general`. Custom domains must first be registered via [`archgate adr domain add`](#archgate-adr-domain). | | `--files <patterns>` | File patterns, comma-separated | | `--body <markdown>` | Full ADR body markdown (skip template) | | `--rules` | Set `rules: true` in frontmatter | | `--json` | Output as JSON | ### Examples Interactive mode: ```bash archgate adr create ``` Non-interactive mode: ```bash archgate adr create \ --title "API Response Envelope" \ --domain backend \ --files "src/api/**/*.ts" \ --rules ``` --- ## archgate adr list List all ADRs in the project. ```bash archgate adr list [options] ``` ### Options | Option | Description | | ------------------- | ---------------- | | `--json` | Output as JSON | | `--domain <domain>` | Filter by domain | ### Examples List all ADRs in table format: ```bash archgate adr list ``` ``` ID Domain Rules Title ──────────────────────────────────────────────────────── ARCH-001 architecture true Command Structure ARCH-002 architecture true Error Handling BE-001 backend true API Response Envelope ``` List ADRs as JSON: ```bash archgate adr list --json ``` Filter by domain: ```bash archgate adr list --domain backend ``` --- ## archgate adr show Print a specific ADR by ID. ```bash archgate adr show <id> ``` Prints the full ADR content (frontmatter and body) to stdout. ### Arguments | Argument | Description | | -------- | ----------------------------------- | | `<id>` | ADR ID (e.g., `ARCH-001`, `BE-003`) | ### Example ```bash archgate adr show ARCH-001 ``` --- ## archgate adr update Update an existing ADR by ID. ```bash archgate adr update --id <id> --body <markdown> [options] ``` Replaces the ADR body with the provided markdown. Frontmatter fields (`--title`, `--domain`, `--files`, `--rules`) are updated only when explicitly passed; otherwise the existing values are preserved. ### Options | Option | Required | Description | | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--id <id>` | Yes | ADR ID to update (e.g., `ARCH-001`) | | `--body <markdown>` | Yes | Full replacement ADR body markdown | | `--title <title>` | No | New ADR title (preserves existing if omitted) | | `--domain <domain>` | No | New domain. Built-ins: `backend`, `frontend`, `data`, `architecture`, `general`. Custom domains must first be registered via [`archgate adr domain add`](#archgate-adr-domain). | | `--files <patterns>` | No | New file patterns, comma-separated (preserves existing if omitted) | | `--rules` | No | Set `rules: true` in frontmatter | | `--json` | No | Output as JSON | ### Example ```bash archgate adr update \ --id ARCH-001 \ --title "Updated Command Structure" \ --body "## Context\n\nUpdated context..." ``` --- ## archgate adr domain Manage custom ADR domains. Custom domains are name → ID-prefix mappings persisted in `.archgate/config.json` and merged with the five built-ins (`backend`, `frontend`, `data`, `architecture`, `general`) at read time. Use this command when a decision doesn't cleanly fit any built-in domain. Before registering a new one, check whether the decision can be folded under an existing domain. Built-ins are the default and a custom domain should only be introduced when no built-in is a genuine fit. ```bash archgate adr domain <subcommand> [options] ``` ### Options | Option | Applies to | Description | | -------- | --------------- | -------------- | | `--json` | all subcommands | Output as JSON | ### archgate adr domain list Show all merged (built-in + custom) domains and their prefixes: ```bash archgate adr domain list ``` ``` Domain Prefix Source ──────────────────────────────── architecture ARCH default backend BE default data DATA default frontend FE default general GEN default security SEC custom ``` ### archgate adr domain add Register a custom domain: ```bash archgate adr domain add <name> <prefix> ``` Naming rules: - `<name>`: lowercase kebab-case, 2–32 chars, must start with a lowercase letter (e.g., `security`, `ml-ops`) - `<prefix>`: uppercase letters, digits, or underscores, 2–10 chars, must start with an uppercase letter (e.g., `SEC`, `MLOPS`) - Custom names and prefixes cannot collide with built-ins or with any other custom entry. Example: ```bash archgate adr domain add security SEC ``` ### archgate adr domain remove Unregister a custom domain (built-ins cannot be removed): ```bash archgate adr domain remove <name> ``` --- ## archgate adr import Import ADRs from the registry or a git repository. ```bash archgate adr import <source...> [options] ``` The command clones the source, reads ADR files, remaps IDs to fit the local project's sequence, and writes them to `.archgate/adrs/`. It tracks imports in `.archgate/imports.json` so they can later be checked for upstream updates via [`archgate adr sync`](#archgate-adr-sync). ### Arguments | Argument | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------------- | | `<source...>` | Registry path(s), `org/repo/path`, or git URL(s). Required by the command even with `--list`, which ignores the value. | ### Options | Option | Description | | ----------- | ------------------------------- | | `--yes` | Skip confirmation prompt | | `--json` | Output as JSON | | `--dry-run` | Preview changes without writing | | `--list` | List previously imported ADRs | ### Examples Import from the registry: ```bash archgate adr import archgate/packs/typescript ``` Import from a git repository: ```bash archgate adr import https://github.com/acme/adr-packs.git ``` Preview what would be imported without writing any files: ```bash archgate adr import archgate/packs/typescript --dry-run ``` Import non-interactively (skip confirmation): ```bash archgate adr import archgate/packs/typescript --yes ``` List previously imported ADRs (the `<source...>` argument is still required by the parser, but its value is ignored with `--list`): ```bash archgate adr import archgate/packs/typescript --list ``` --- ## archgate adr sync Check for upstream updates to imported ADRs. ```bash archgate adr sync [source...] [options] ``` The command compares local imported ADRs against their upstream source and shows which sections changed. In interactive mode, it prompts for each changed ADR with three choices: keep local, take upstream, or skip. ### Arguments | Argument | Description | | ------------- | ------------------------------------------------------- | | `[source...]` | Optional source filter(s) to sync only matching imports | ### Options | Option | Description | | ---------- | -------------------------------------------------------------------------------------------------------------------- | | `--check` | Exit 1 if upstream has updates (CI mode) | | `--yes` | Skip confirmation prompts | | `--json` | Output as JSON | | `--strict` | Fail (exit 1) when sync encountered errors (unresolved import source, failed clone, missing local/upstream ADR file) | `--strict` resolves the same way as [`archgate check --strict`](/reference/cli/check/#strict-mode): CLI flag, then a `strict: boolean` key in `.archgate/config.json`, then off. There is no `--no-strict` flag -- the CLI flag can only turn strict mode on, not override a configured `strict: true`. ### Examples Check all imported ADRs for upstream updates: ```bash archgate adr sync ``` Check only imports from a specific source: ```bash archgate adr sync archgate/packs/typescript ``` CI mode, failing the build if any imported ADR is outdated: ```bash archgate adr sync --check ``` Accept all upstream updates non-interactively: ```bash archgate adr sync --yes ``` Fail the build if any import couldn't be resolved, cloned, or matched to a local ADR: ```bash archgate adr sync --check --strict ``` --- ## Reference: archgate check Source: https://cli.archgate.dev/reference/cli/check/ Run all automated ADR compliance checks against the codebase. ```bash archgate check [options] [files...] ``` Loads every ADR with `rules: true` in its frontmatter, executes the companion `.rules.ts` file, and reports violations with file paths and line numbers. When file paths are provided as positional arguments, only ADRs whose `files` patterns match those files are executed. ## Options | Option | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `--staged` | Only check git-staged files (useful for pre-commit hooks) | | `--base [ref]` | Compare changed files against a base ref (auto-detects when omitted) | | `--output <format>` | Output format: `console` (default), `json`, `github`, or `sarif`. See [SARIF output](#sarif-output). | | `--adr <id>` | Only check rules from a specific ADR | | `--verbose` | Show passing rules and timing info | | `--strict` | Treat any rule-severity warning, and advisory findings (briefing budget, suppression, unparsed ADRs), as failures. See [Strict mode](#strict-mode). | ## Arguments | Argument | Description | | ------------ | --------------------------------------------------------------------------------------------------------------- | | `[files...]` | Optional file paths to scope checks to. Only ADRs whose `files` patterns match will run. Supports stdin piping. | ## Exit codes | Code | Meaning | | ---- | ----------------------------------------------------------------------------------------------------- | | 0 | All rules pass. No violations found. | | 1 | One or more violations detected, or `--strict` escalated warnings or advisory findings into failures. | | 2 | Rule execution error (e.g., malformed rule, security scanner block). | ## Examples Check the entire project: ```bash archgate check ``` Check only staged files before committing: ```bash archgate check --staged ``` Check all files changed on the current branch vs `main`: ```bash archgate check --base main ``` Check a single ADR: ```bash archgate check --adr ARCH-001 ``` Treat any rule-severity warning and any advisory finding (briefing budget, suppression, unparsed ADRs) as a failure (useful in CI): ```bash archgate check --strict ``` Check specific files (only matching ADRs run): ```bash archgate check src/foo.ts src/bar.ts ``` Pipe from git (check only changed files): ```bash git diff --name-only | archgate check --output json ``` Get JSON output for CI integration: ```bash archgate check --output json ``` Get GitHub Actions annotations: ```bash archgate check --output github ``` Get SARIF output for GitHub Code Scanning: ```bash archgate check --output sarif > results.sarif ``` ## SARIF output `archgate check --output sarif` emits [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html), the standard format GitHub's Code Scanning and Code Quality features ingest in CI. Every rule violation becomes a SARIF result; `error`/`warning`/`info` severities map to SARIF `error`/`warning`/`note`. Advisory findings (briefing-budget, suppression, and unparsed-ADR warnings) are included too, as synthetic results under dedicated rule IDs (`archgate/briefing-budget`, `archgate/suppression-warning`, `archgate/unparsed-adr`), always at `warning` level -- matching how they're never treated as blocking outside `--strict`. `--output sarif` is opt-in only: unlike agent-context `json`, it is never auto-detected. Upload results to GitHub's Security tab in CI: ```yaml - name: Run archgate check run: archgate check --output sarif > results.sarif - name: Upload SARIF to GitHub Security tab if: success() || failure() uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: sarif_file: results.sarif ``` The `if: success() || failure()` condition is required: `archgate check` exits 1 when it finds violations, which would otherwise skip the upload step exactly when there are findings to report. Prefer it over `always()`, which would also run the upload for cancelled jobs. The job also needs the `security-events: write` permission for the upload to succeed. See the [CI integration guide](/guides/ci-integration/) for the full workflow. ## Changed files detection By default, `archgate check` auto-detects the base branch and populates `ctx.changedFiles` with the branch diff (`git diff <base>...HEAD`) plus any uncommitted working-tree changes (staged, unstaged, and untracked non-ignored files). This enables cross-file dependency rules to work locally -- not just in CI -- and ensures edits that haven't been committed yet are still checked. The base ref is resolved in priority order: | Priority | Source | `changedFiles` populated with | | -------- | ------------------------------------ | ------------------------------------------------------- | | 1 | `--staged` | Git staging area only | | 2 | `--base <ref>` | `git diff <ref>...HEAD` + working-tree changes | | 3 | `.archgate/config.json` `baseBranch` | `git diff <resolved-ref>...HEAD` + working-tree changes | | 4 | Git auto-detect | `git diff <detected-ref>...HEAD` + working-tree changes | | 5 | Detection fails | Empty (full-scan mode) | Auto-detection tries `origin/HEAD`, then `origin/main`, `origin/master`, local `main`, and local `master`. To set a project default, add `baseBranch` to `.archgate/config.json`: ```json { "baseBranch": "main" } ``` ### Skipping untouched ADRs Whenever the change set is non-empty (any of priorities 1--4 above), an ADR whose `files` globs match none of the changed files is skipped entirely: its rules never run, and it does not appear in the results or the pass/fail counts -- the same behavior as a `[files...]` filter that matches nothing the ADR governs. Deleted files count as changes, so removing a file inside an ADR's scope still runs that ADR. ADRs without a `files` scope always run. This means a rule does not need to filter `ctx.scopedFiles` against `ctx.changedFiles` just to avoid work when its ADR is irrelevant to the change; the framework never invokes it in that case. Filtering inside the rule is still useful when some in-scope files changed and the rule only wants to inspect those. When the change set is empty -- detection fails, `--staged` finds nothing staged, or the working tree matches the base (for example on the base branch itself in CI) -- every ADR runs (full-scan mode). ## Diagnostics During execution, `archgate check` emits warnings for common misconfigurations that may cause slow or unexpected results: | Warning | Condition | Recommendation | | ----------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | **Broad file scope** | An ADR's `files` patterns resolve to more than 1,000 files or the glob scan takes over 2 seconds | Narrow the `files` patterns in the ADR frontmatter to target only the relevant source directories | | **Unscoped gitignore opt-out** | `respectGitignore: false` is set without a `files` scope | Add `files` patterns to avoid scanning all files including `node_modules/`, `.git/`, etc. | | **All files excluded by gitignore** | Explicit `files` patterns match files, but every match is excluded by `.gitignore` | Set `respectGitignore: false` in the ADR frontmatter to include gitignored files | These warnings appear in the standard output and do not affect the exit code. They also appear in JSON output when `--output json` is used (as violations with `"severity": "warning"`). ### Briefing budget `archgate review-context --verbose` embeds each applicable ADR's **Decision** and **Do's and Don'ts** sections and truncates each one at a fixed character cap. Prose past that point never reaches the agent the ADR governs, and no companion rule can detect it — rules measure code, not the ADR's own prose. `archgate check` therefore reports every ADR section that exceeds the cap, across all ADRs including those with `rules: false`: ``` [briefing] ARCH-024 "Decision" is 3574 chars; review-context truncates at 2000, hiding 1574 from agent briefings ``` The same entries appear in JSON output under `briefingWarnings`, with `adrId`, `file`, `section`, `length`, and `cap`. An ADR that cannot be read or parsed is measured by nothing, so it is reported separately rather than counted as compliant: ``` [adr] could not be parsed, so it was excluded from every check above .archgate/adrs/BROKEN.md ``` JSON output lists those files under `unparsedAdrs`. **An empty `briefingWarnings` means "nothing over budget" only when `unparsedAdrs` is empty too** — otherwise part of the corpus was never inspected. These are **advisory and never affect `pass`, unless `--strict` is set** — see [Strict mode](#strict-mode). To clear an overflow, apply the remedies that cannot cost a rule: move rationale into Context or Consequences, which are never briefed and therefore never capped; drop historical narration; and merge bullets stating the same rule twice. If the next cut would remove a normative clause — an enumerated identifier list, an ordered guardrail, an exemption — **stop**. That section is expected to exceed the cap and MUST NOT be shortened further. Record why in the ADR's own Compliance section, and order the section so its most normative content precedes the cut, since truncation always removes the tail. By default, warnings (both diagnostics and rule-reported `"severity": "warning"` violations) never change the exit code. Pass `--strict` to escalate them into failures — see [Strict mode](#strict-mode). ## Strict mode `--strict` combines two escalations behind one flag: any rule-severity `"warning"` violation fails the build (the JSON output's `warningsExceeded` field is `true` and `pass` is `false`), and it separately fails the build when `briefingWarnings`, `suppressionWarnings`, or `unparsedAdrs` is non-empty — the advisory diagnostics above that never affect `pass` by default. The JSON output's `strictAdvisoryExceeded` field is `true` when the latter condition triggered the failure. The briefing-budget and unparsed-ADR diagnostics are corpus-wide, not rule-scoped: they are collected and enforced even when no `rules: true` ADR exists, so a prose-only ADR corpus still fails `--strict` on a briefing overrun or an unparseable ADR file. Suppression warnings, by contrast, derive from rule violations and only arise when rules run. `--strict` also applies to [`archgate review-context`](/reference/cli/review-context/) and `archgate adr sync`. See the [Configuration reference](/reference/configuration/#strict) for the full schema. To avoid passing `--strict` on every invocation, set a project default in `.archgate/config.json`: ```json { "strict": true } ``` There is no `--no-strict` flag: `--strict` on the command line can only turn strict mode _on_ over an absent or `false` config default — it cannot turn off a configured `strict: true`. ## JSON output format When `--output json` is used, the output is a single JSON object. `results` contains only rules that have something to report: failures, rule errors, and any rule with violations (including warning- and info-only rules). Rules that pass cleanly are omitted — their entry would only restate static ADR text, and on a large project those entries dominate the payload. The `total` and `passed` counts still report exactly how many rules ran and passed. Pass `--verbose` to include every rule in `results`. ```json { "pass": false, "total": 4, "passed": 3, "failed": 1, "warnings": 0, "errors": 1, "infos": 0, "ruleErrors": 0, "warningsExceeded": false, "strictAdvisoryExceeded": false, "truncated": false, "results": [ { "adrId": "ARCH-001", "ruleId": "register-function-export", "description": "Command file must export a register*Command function", "status": "fail", "totalViolations": 1, "shownViolations": 1, "violations": [ { "message": "Command file must export a register*Command function", "file": "src/commands/broken.ts", "line": 1, "endLine": 1, "endColumn": 42, "severity": "error" } ], "durationMs": 12 } ], "durationMs": 42 } ``` ### Violation fields | Field | Type | Description | | ----------- | ------- | ------------------------------------------------------- | | `message` | string | What the violation is | | `file` | string? | Relative file path | | `line` | number? | Start line (1-based) | | `endLine` | number? | End line (1-based) -- for precise editor highlighting | | `endColumn` | number? | End column (0-based) -- for precise editor highlighting | | `fix` | string? | Suggested fix (guidance only) | | `severity` | string | `"error"`, `"warning"`, or `"info"` | ### Blocked rule files When a rule file is blocked by the security scanner (e.g., uses `Bun.spawn()`) or a companion `.rules.ts` file is missing, the result appears in the JSON output with `status: "error"` and `ruleId: "security-scan"`. Violations include the exact file and line of the blocked code (or the `rules: true` line in the ADR for missing companions). --- ## Reference: archgate clean Source: https://cli.archgate.dev/reference/cli/clean/ Remove the CLI cache directory. ```bash archgate clean ``` Removes `~/.archgate/`, which stores the CLI's config, cache, and (for binary installs) the downloaded `archgate` executable. Safe to run at any time -- the directory is recreated automatically when needed. If the running binary was installed to `~/.archgate/bin/`, `clean` preserves that `bin/` directory so it does not delete the executable it is currently running as, and removes everything else. If `~/.archgate/` does not exist, it prints `Nothing to clean.` and exits without error. ## Example ```bash archgate clean ``` ``` /home/user/.archgate cleaned up ``` When the running binary lives under `~/.archgate/bin/`, the message notes that `bin/` was kept: ``` /home/user/.archgate cleaned up (bin/ preserved) ``` --- ## Reference: archgate doctor Source: https://cli.archgate.dev/reference/cli/doctor/ Check the system environment, installation method, and editor integrations. Useful for diagnosing configuration issues and sharing debug context in bug reports. ```bash archgate doctor [options] ``` ## Options | Option | Description | | -------- | ----------------------- | | `--json` | Machine-readable output | ## Output sections - **System** -- OS, architecture, WSL detection, Bun and Node versions - **Archgate** -- CLI version, install method (binary, proto, local, global-pm), config directory, telemetry and login status - **Project** -- Whether an `.archgate/` project exists, ADR count, domains - **Editor CLIs** -- Whether `claude`, `cursor`, `code` (VS Code), `copilot`, and `git` are available on PATH - **Project Integrations** -- Whether editor-specific plugin files exist in the current project (`.claude/settings.local.json`, `.vscode/settings.json`, `.github/copilot/instructions.md`); for Cursor, whether the `cursor` CLI is available, since the Cursor plugin is embedded in a VSIX extension rather than a project file ## Example ```bash archgate doctor ``` ``` System OS: win32/x64 Bun: 1.3.11 Node: v24.3.0 Archgate Version: 0.25.1 Install: binary Exec path: /home/user/.archgate/bin/archgate Config dir: /home/user/.archgate OK Telemetry: enabled Logged in: yes Project ADRs: 5 (3 with rules) Domains: ARCH, GEN Editor CLIs claude: OK cursor: MISSING code (vscode):OK copilot: MISSING git: OK Project Integrations Claude: OK (.claude/settings.local.json) Cursor: MISSING (VSIX extension with embedded plugin) VS Code: OK (.vscode/settings.json) Copilot: MISSING (.github/copilot/instructions.md) ``` --- ## Reference: CLI Commands Source: https://cli.archgate.dev/reference/cli/ ## Global options These options are available on all commands: | Option | Description | | --------------------- | ---------------------------------------------------------------- | | `--version`, `-V` | Print the Archgate version | | `--help`, `-h` | Show help for any command | | `--log-level <level>` | Set log verbosity: `error`, `warn`, `info` (default), or `debug` | ```bash archgate --version archgate check --help archgate --log-level debug check ``` ## Commands | Command | Description | | ------------------------------------------------ | ----------------------------------------------------- | | [`archgate login`](./login/) | Authenticate with GitHub | | [`archgate init`](./init/) | Set up linting and rules enforcement | | [`archgate plugin`](./plugin/) | Manage editor plugins | | [`archgate check`](./check/) | Run ADR compliance checks | | [`archgate adr`](./adr/) | Create, list, show, and update ADRs | | [`archgate review-context`](./review-context/) | Pre-compute review context for CI/editor integrations | | [`archgate session-context`](./session-context/) | Read AI editor session transcripts | | [`archgate upgrade`](./upgrade/) | Upgrade Archgate to the latest version | | [`archgate doctor`](./doctor/) | Check system environment and editor integrations | | [`archgate clean`](./clean/) | Remove the CLI cache directory | | [`archgate telemetry`](./telemetry/) | Manage anonymous usage data collection | --- ## Reference: archgate init Source: https://cli.archgate.dev/reference/cli/init/ Set up Archgate linting and rules enforcement in the current project. ```bash archgate init [options] ``` Creates the `.archgate/` directory with an example ADR and a linter rules directory. Optionally configures editor integration for AI agent workflows and installs the Archgate editor plugin. ## Options | Option | Default | Description | | ------------------- | -------- | ------------------------------------------------------------------------------------- | | `--editor <editor>` | `claude` | Editor integration to configure (`claude`, `cursor`, `vscode`, `copilot`, `opencode`) | | `--install-plugin` | auto | Install the Archgate editor plugin (requires prior `archgate login`) | When `--install-plugin` is passed, the CLI installs the Archgate plugin for the selected editor. If the flag is omitted, the CLI auto-detects: it installs the plugin when valid credentials exist (from a previous `archgate login`) and skips otherwise. ## Plugin installation behavior **Claude Code:** If the `claude` CLI is on your PATH, the plugin is installed automatically via `claude plugin marketplace add` and `claude plugin install`. If the `claude` CLI is not found, the command prints the manual installation commands instead. **Cursor:** Downloads an authenticated tarball of skills, agents, and hooks into `~/.cursor/`. Also writes `.cursor/hooks.json` to the project for cloud agent compatibility. No CLI detection needed. **opencode:** Requires opencode to be detected -- either the `opencode` CLI on your PATH, or opencode's user-scope config directory existing (the Desktop app ships no CLI binary). If neither is found, the install is skipped and a message prompts you to install opencode first. When detected, the CLI downloads an authenticated tarball from the Archgate plugins service and extracts its `agents/` and `skills/` directories into the user-scope opencode config root (`$XDG_CONFIG_HOME/opencode/`, falling back to `$HOME/.config/opencode/` on every platform including Windows; opencode uses XDG paths via `xdg-basedir` and does not read `%APPDATA%`), and sets the default agent in `opencode.json` when none is configured. No files are written to the project tree. See the [opencode integration guide](/guides/opencode-integration/) for details. ## Output ``` Initialized Archgate governance in /path/to/project adrs/ - architecture decision records lint/ - linter-specific rules .claude/ - Claude Code settings configured Archgate plugin installed for Claude Code. ``` When `--editor cursor` is used, the output shows `.cursor/` for project-level files and notes that user-scope components were installed to `~/.cursor/`. ## Base branch detection When run inside a git repository, `archgate init` auto-detects the base branch and saves it to `.archgate/config.json` as the `baseBranch` field. This allows `archgate check` to skip branch detection on every run, saving 1-4 git subprocess calls. The detection tries `origin/HEAD`, `origin/main`, `origin/master`, local `main`, and local `master` (first match wins). If none are found (e.g., not a git repo), no `baseBranch` is written. Re-running `archgate init` does **not** overwrite a manually configured `baseBranch`. See [Configuration -- `baseBranch`](/reference/configuration/#basebranch) for details. ## Generated structure ``` .archgate/ adrs/ GEN-001-example.md # Example ADR (rules: false — no companion rules file) lint/ README.md # Linter rules guide ``` --- ## Reference: archgate login Source: https://cli.archgate.dev/reference/cli/login/ Authenticate with GitHub to access Archgate editor plugins. If you are not registered yet, the CLI handles signup automatically -- it prompts for your email, editor preference (Claude Code, Cursor, VS Code, GitHub Copilot, or opencode), and use case, then registers you before completing the login. ```bash archgate login ``` Starts a GitHub Device Flow (OAuth). The CLI displays a one-time code and a URL. Open the URL in your browser, enter the code, and authorize the Archgate GitHub OAuth App. Once authorized, the CLI exchanges your GitHub identity for an Archgate plugin token and stores it securely in your OS credential manager (macOS Keychain, Windows Credential Manager, or Linux libsecret) via `git credential approve`. No credentials are written to disk as plain-text files. If your GitHub account is not yet registered, the CLI prompts for your email, preferred editor, and use case, then signs you up automatically. Credentials are required to install editor plugins via `archgate init --install-plugin`. The CLI itself (check, init, etc.) works without login. Editor plugins are currently in beta. Run `archgate login` to sign up and authenticate. ## Subcommands | Subcommand | Description | | ------------------------ | ----------------------------------------- | | `archgate login` | Authenticate (skips if already logged in) | | `archgate login status` | Show current authentication status | | `archgate login logout` | Remove stored credentials | | `archgate login refresh` | Re-authenticate and claim a new token | ## Examples Log in for the first time: ```bash archgate login ``` ``` By signing up, you agree to the Archgate Terms of Service: https://archgate.dev/terms-of-service info: Authenticating with GitHub... Open https://github.com/login/device in your browser and enter the code: ABCD-1234 Waiting for authorization... info: GitHub user: yourname info: Claiming archgate plugin token... Your GitHub account yourname is not yet registered. Let's sign you up now. ? Email address: you@example.com ? Which editor will you use with archgate? Claude Code ? How do you plan to use archgate? Enforcing ADRs in our monorepo ? I agree to be contacted by the Archgate team to provide feedback during the beta period. Yes info: Submitting signup request... info: Claiming archgate plugin token... info: Authenticated as yourname. Plugin access is now available. Run `archgate init` to set up a project with the archgate plugin. ``` If the project already has `.archgate/adrs/`, the final line reads: ``` Run `archgate check` to validate your project against its ADRs. ``` ## Troubleshooting ### TLS/corporate proxy errors If `archgate login` fails with a TLS certificate error (common behind corporate proxies), point your runtime at your organization's CA bundle using the `NODE_EXTRA_CA_CERTS` environment variable. On macOS/Linux: ```bash export NODE_EXTRA_CA_CERTS=/path/to/your-corporate-ca.pem archgate login ``` On Windows (PowerShell): ```powershell $env:NODE_EXTRA_CA_CERTS = "C:\path\to\your-corporate-ca.pem" archgate login ``` On Windows (cmd): ```cmd set NODE_EXTRA_CA_CERTS=C:\path\to\your-corporate-ca.pem archgate login ``` On Windows (Git Bash): ```bash export NODE_EXTRA_CA_CERTS=/c/path/to/your-corporate-ca.pem archgate login ``` Ask your IT team for the correct certificate path if you are unsure. Check login status: ```bash archgate login status ``` ``` Logged in as yourname. ``` Log out: ```bash archgate login logout ``` Re-authenticate: ```bash archgate login refresh ``` --- ## Reference: archgate plugin Source: https://cli.archgate.dev/reference/cli/plugin/ Manage Archgate editor plugins independently of `archgate init`. ```bash archgate plugin <subcommand> [options] ``` Use `archgate plugin` to install plugins or retrieve the authenticated repository URL on projects that have already been initialized. ## Subcommands ### archgate plugin url Print the plugin repository URL for manual tool configuration. ```bash archgate plugin url [options] ``` | Option | Default | Description | | ------------------- | -------- | ------------------------------------------------------------------- | | `--editor <editor>` | `claude` | Target editor (`claude`, `cursor`, `vscode`, `copilot`, `opencode`) | The URL can be used to manually configure editor tools. Credentials are provided automatically by your git credential manager (stored during `archgate login`). For example, to add the Archgate marketplace in Claude Code: ```bash claude plugin marketplace add "$(archgate plugin url)" claude plugin install archgate@archgate ``` For VS Code, the URL points to a separate plugin repository: ```bash archgate plugin url --editor vscode ``` ### archgate plugin install Install the Archgate plugin for the specified editor on an already-initialized project. ```bash archgate plugin install [options] ``` | Option | Default | Description | | ------------------- | -------- | ------------------------------------------------------------------- | | `--editor <editor>` | `claude` | Target editor (`claude`, `cursor`, `vscode`, `copilot`, `opencode`) | Installation behavior varies by editor: - **Claude Code:** Auto-installs via `claude` CLI if available; prints manual commands otherwise. - **GitHub Copilot:** Declares the Archgate marketplace and plugin in Copilot's `settings.json` (`~/.copilot/` by default, honoring `COPILOT_HOME`), which both the Copilot CLI and the Copilot desktop app read. When the `copilot` CLI is on PATH the plugin is also installed immediately; otherwise Copilot installs it automatically on next launch. Prints manual commands when no `copilot` CLI or Copilot settings directory is detected. - **Cursor:** Downloads an authenticated tarball and extracts skills and agents into `~/.cursor/`; also merges an archgate entry into `~/.cursor/hooks.json` (written locally by the CLI, not part of the downloaded bundle, and any pre-existing hooks are preserved). No CLI detection needed. Files are written directly to the Cursor user directory. - **VS Code:** Installs the VS Code extension (`.vsix`) via `code` CLI if available; prints manual instructions otherwise. - **opencode:** Detects opencode via the `opencode` CLI on PATH, or (if the CLI is absent) the presence of opencode's shared user-scope config directory, which the Desktop app also uses. Skips the install with a clear message when neither is found. When detected, downloads an authenticated tarball of agent files and extracts it into the user-scope opencode agents directory. `archgate plugin url --editor opencode` prints "N/A" because opencode has no marketplace URL. See the [opencode integration guide](/guides/opencode-integration/) for details. ## Examples Get the plugin URL for manual configuration: ```bash archgate plugin url ``` Install the plugin for Claude Code: ```bash archgate plugin install ``` Install the plugin for Cursor: ```bash archgate plugin install --editor cursor ``` Install the agent bundle for opencode: ```bash archgate plugin install --editor opencode ``` --- ## Reference: archgate review-context Source: https://cli.archgate.dev/reference/cli/review-context/ Pre-compute review context with ADR briefings for changed files. Designed for CI and editor plugin integrations that need a summary of which ADRs apply to the files being changed. ```bash archgate review-context [options] ``` ## Options | Option | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------- | | `--staged` | Only include git-staged files | | `--base [ref]` | Compare changed files against a base ref (auto-detects when omitted) | | `--run-checks` | Include ADR compliance check results | | `--domain <domain>` | Filter to a single domain | | `--verbose` | Include each ADR's Decision and Do's/Don'ts prose | | `--strict` | Exit 1 when briefings were truncated, or (with `--run-checks`) when `check` found strict-relevant findings | ## Example ```bash archgate review-context --staged ``` ## Which files count as changed Even with no flags, this command works against a base ref rather than the working tree alone: the changed-file set is the branch diff (`git diff <base>...HEAD`) unioned with staged, unstaged, and untracked non-ignored files. The base ref resolves the same way as for [`archgate check`](/reference/cli/check/): | Priority | Source | Changed files | | -------- | ------------------------------------ | ------------------------------------------------------- | | 1 | `--staged` | Staged files only — no base ref is resolved | | 2 | `--base <ref>` | `git diff <ref>...HEAD` + working-tree changes | | 3 | `.archgate/config.json` `baseBranch` | `git diff <resolved-ref>...HEAD` + working-tree changes | | 4 | Auto-detection | `git diff <detected-ref>...HEAD` + working-tree changes | Auto-detection tries `origin/HEAD`, then `origin/main`, `origin/master`, local `main`, and local `master`, and the result is persisted to `.archgate/config.json` so later runs skip the probe. `--base` with no value is the same as omitting it: config first, then auto-detection. When no base ref can be resolved at all, only working-tree changes are used. ## Strict mode Pass `--strict` to make this command fail (exit 1) rather than just report: it fails when `truncatedBriefings` is non-empty (requires `--verbose` to have anything to truncate), or, with `--run-checks`, when the reused `checkSummary.warningsExceeded` or `checkSummary.strictAdvisoryExceeded` is true. `--strict` does **not** fail on ordinary rule violations (`checkSummary.failed`/`ruleErrors`) — this command stays a context generator for agents, not a second compliance gate; use [`archgate check`](/reference/cli/check/) to gate on rule violations. A `--strict` failure prints the full JSON payload first, then logs the reason to stderr before exiting. `--strict` resolves the same way as `archgate check --strict`: CLI flag, then a `strict: boolean` key in `.archgate/config.json`, then off. ## Output size By default each ADR is identified by `id`, `title`, `domain`, `files`, and `rules` only — enough to know which ADRs apply to the changed files. Read the ones you need with `archgate adr show <id>`. Pass `--verbose` to embed every applicable ADR's Decision and Do's/Don'ts prose in the response instead. That prose grows with the number of matched ADRs and dominates the payload — large enough on a repository with many ADRs that agent harnesses stop displaying the result inline. Prefer the default and drill down; reach for `--verbose` only when a single self-contained payload is genuinely required. ## Truncated briefings Briefing prose is capped per section. When a Decision or Do's and Don'ts section exceeds the cap it is cut, and the omission is reported four ways: - The cut point is marked inline with `[... truncated — read full ADR via adr://<id>]`. - The ADR's own briefing lists the affected section names in `truncatedSections`. - Every affected ADR id is collected in the top-level `truncatedBriefings` array. It is populated after `--domain` filtering, so it names only ADRs present in the response. - A warning naming those ADRs is written to stderr, leaving stdout valid JSON. Treat any of these as meaning the ADR's governing text is incomplete: the rules it states may sit in the part that was cut. Read the full document with `archgate adr show <id>` before relying on the briefing. Two other limits truncate this payload, each with its own stderr warning. `truncatedFiles` is set when the changed-file list exceeds its cap, so files beyond it are absent from every domain. With `--run-checks`, `checkSummary.truncated` is set when a rule reported more violations than the per-rule cap — run [`archgate check`](/reference/cli/check/) for the complete list. When `--run-checks` is passed, `checkSummary` follows the same rule as [`archgate check --output json`](/reference/cli/check/): its `results` array carries only rules with something to report, while the counts beside it still cover every rule that ran. --- ## Reference: archgate session-context Source: https://cli.archgate.dev/reference/cli/session-context/ Read AI editor session transcripts for the project. Useful for auditing what an AI agent did during a coding session. ```bash archgate session-context [subcommand] [options] ``` With no subcommand it reads the **current conversation** for the editor running the command, which Archgate works out from the environment. Two subcommands cover the rest: `list` to discover earlier sessions, and `show <session-id>` to read a specific one. Every form prints JSON to stdout, and only sessions belonging to the current project are considered. ## Options | Option | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `--editor <name>` | Editor to read: `antigravity`, `claude-code`, `codex`, `copilot`, `cursor`, `opencode`, or `pi`. Defaults to the detected editor. | | `--max-entries <n>` | Maximum transcript entries to return, most recent first (default: 200). Must be a positive integer. | | `--root` | opencode only: resolve a sub-agent child session up to its top-level ancestor. Rejected for any other editor. | ## Subcommands ### archgate session-context list List available sessions for the project as JSON (`id`, `updatedAt`, and `title` for editors that store one), most recent first. Accepts `--editor`. ```bash archgate session-context list ``` ### archgate session-context show Read a specific session by ID (from `list`). An explicit ID always wins over the session the environment points at. Accepts `--editor`, `--max-entries`, and `--root`. ```bash archgate session-context show <session-id> ``` ## Editor detection Every supported editor marks the processes it spawns, and Archgate reads those markers to work out which editor is asking. Pass `--editor` to override the result, or to read a different editor's transcripts. Some editors also publish the ID of the conversation they are currently running. When one does, Archgate reads that **exact** conversation rather than the most recent one. This matters when a project has several sessions open at once, where the most recent may not be the conversation you are part of. A published ID that matches no session for the project is ignored and recency applies, so a stale ID never turns a working command into an error. A published ID only ever applies to the editor that published it. Passing `--editor cursor` from inside Claude Code reads Cursor's transcripts by recency. When more than one editor's markers are present — an agent running inside another agent — the winner is decided by a fixed order: `antigravity`, `claude-code`, `codex`, `copilot`, `cursor`, `pi`, then `opencode`. Every match is still reported in the output. That order applies whatever session IDs happen to be published: an empty or unusable ID changes which session is selected, never which editor. Detection fails when Archgate runs from a plain shell rather than inside an AI editor. The command then exits 1 and asks for `--editor`. ## Output Every invocation reports what it resolved in a `detection` object alongside the session payload: ```json { "detection": { "editor": "claude-code", "via": "CLAUDECODE", "session": "pinned", "candidates": ["claude-code"] }, "sessionFile": "6ee6f0a5-1b2c-4d5e-8f90-a1b2c3d4e5f6.jsonl", "totalEntries": 182, "relevantEntries": 125, "transcript": [] } ``` | Field | Meaning | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `detection.editor` | Editor whose sessions were read | | `detection.via` | Environment variable that identified the editor, or `--editor` when you named one | | `detection.session` | `pinned` (the editor's own session ID was used), `recent` (the most recent session was taken), or `explicit` (an ID was passed to `show`). Not reported by `list`, which selects no single session. | | `detection.candidates` | Every editor whose marker was present, in precedence order | | `sessionId` / `sessionFile` | Identifies the session that was read; which of the two appears depends on the editor | | `totalEntries` | Entries in the stored session | | `relevantEntries` | Conversational entries left after skipping bookkeeping events and turns with no prose | | `transcript` | The last `--max-entries` of those entries, each with `role` and `contentPreview` | `list` replaces the session payload with a `sessions` array. When no session can be read, the reason is written to stderr and the command exits 1. ## Editor-specific behavior - **Antigravity** and **Codex** each ship a CLI and a desktop app, and conversations from both distributions are read. - **Codex** honors `CODEX_HOME`. - **Pi** honors `PI_CODING_AGENT_DIR` and `PI_CODING_AGENT_SESSION_DIR`. Pi branches a session in place rather than starting a new one, so only the active branch is read — a forked or rewound turn is left out. Pi publishes its session ID only to commands its agent runs, so a command you type yourself is still detected as Pi but selected by recency. - **opencode** records sub-agent runs as child sessions of the conversation that started them. Child sessions are excluded from `list` and from recency selection, so the most recent top-level session is always the main development session. They can still be read by ID with `show`, and `--root` resolves a child session up to its top-level ancestor — useful when a sub-agent knows its own session ID and needs the conversation it belongs to. ## Examples Read the current session, whichever editor is running: ```bash archgate session-context ``` List sessions for the detected editor: ```bash archgate session-context list ``` Read another editor's current session: ```bash archgate session-context --editor opencode ``` Read a specific earlier session: ```bash archgate session-context show 6ee6f0a5-1b2c-4d5e-8f90-a1b2c3d4e5f6 ``` Resolve an opencode sub-agent child session to its top-level ancestor: ```bash archgate session-context show ses_child123 --editor opencode --root ``` --- ## Reference: archgate telemetry Source: https://cli.archgate.dev/reference/cli/telemetry/ Manage anonymous usage data collection for the Archgate CLI. Telemetry is opt-out: enabled by default, never captures personally identifiable information, and anonymizes the client IP server-side. ```bash archgate telemetry <subcommand> ``` See [CLI telemetry](/reference/telemetry/) for the full privacy policy, the list of events emitted, and how telemetry interacts with `ARCHGATE_TELEMETRY=0` and CI environments. ## Subcommands | Subcommand | Description | | ---------------------------- | --------------------------------------------------- | | `archgate telemetry status` | Show whether telemetry is enabled for this CLI user | | `archgate telemetry enable` | Enable anonymous usage data collection | | `archgate telemetry disable` | Disable anonymous usage data collection | All three subcommands read and write `~/.archgate/config.json`, which persists the opt-in state and the anonymous install ID used as the telemetry `distinct_id`. ## Examples Check the current state: ```bash archgate telemetry status ``` ``` Telemetry is enabled. Anonymous usage data helps improve Archgate. No personal information is collected. To disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0 Legal basis: https://archgate.dev/legitimate-interest-assessment Learn more: https://cli.archgate.dev/reference/telemetry ``` Disable telemetry: ```bash archgate telemetry disable ``` ``` Telemetry disabled. No usage data will be collected. ``` Re-enable telemetry: ```bash archgate telemetry enable ``` ``` Telemetry enabled. Thank you for helping improve Archgate. ``` ## Environment variable override Setting `ARCHGATE_TELEMETRY=0` (or `false`, `no`, `off`, case-insensitive) disables telemetry for a single invocation regardless of the persisted state. Use this in CI or shared environments where you want opt-out without mutating the user's config: ```bash ARCHGATE_TELEMETRY=0 archgate check ``` `archgate telemetry status` detects the override and reports: ``` Telemetry is disabled (ARCHGATE_TELEMETRY environment variable). ``` When the override is in effect, running `archgate telemetry enable` persists the opt-in but prints a note that the env var continues to disable telemetry until it is unset. --- ## Reference: archgate upgrade Source: https://cli.archgate.dev/reference/cli/upgrade/ Upgrade Archgate to the latest version. ```bash archgate upgrade ``` Checks GitHub Releases for the latest published version. If a newer version is available, the command auto-detects how Archgate was installed and runs the appropriate upgrade strategy. If already up-to-date, prints a message and exits. ## Options | Option | Description | | ----------- | ------------------------------------------ | | `--plugins` | Also update editor plugins after upgrading | ## Install method detection The upgrade command inspects the running binary path to determine the install method, then delegates to the matching strategy: | Install method | Detection | Upgrade action | | --------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Binary install** (`~/.archgate/bin/`) | Binary lives inside `~/.archgate/bin/` | Downloads the latest binary from GitHub Releases and replaces the existing one | | **Proto** | Binary lives inside `~/.proto/tools/archgate/` | Runs `proto install archgate latest --pin` | | **Local dev dependency** | Binary lives inside `node_modules/` | Detects the package manager from the nearest lockfile (bun, pnpm, yarn, or npm) and runs the appropriate add command (e.g. `bun add -d archgate@latest`) | | **Global package manager** | Binary lives in a global bin directory | Detects which package manager owns the global bin directory and runs its upgrade command (e.g. `npm install -g archgate@latest`) | If no specific method is detected, the command falls back to `npm install -g archgate@latest`. ## Plugin updates After a successful upgrade, the CLI offers to update your editor plugins. In an interactive terminal, you get a confirmation prompt: ``` Archgate upgraded to 0.35.0 successfully. ? Would you like to update your editor plugins too? (Y/n) ``` The CLI auto-detects installed editors (Claude Code, Cursor, VS Code, GitHub Copilot, opencode) and runs the plugin install for each selected editor. Plugin update failures are reported but do not affect the exit code -- the CLI upgrade itself is already complete. Use `--plugins` to skip the prompt and update all detected editors automatically: ```bash archgate upgrade --plugins ``` :::note Plugin updates require authentication. If you are not logged in, the CLI prints a reminder to run `archgate login` first. ## Examples Upgrade the CLI: ```bash archgate upgrade ``` ``` Checking for latest Archgate release... Upgrading 0.34.0 -> 0.35.0... Archgate upgraded to 0.35.0 successfully. ``` Upgrade and update all editor plugins in one step: ```bash archgate upgrade --plugins ``` --- ## Reference: Configuration Source: https://cli.archgate.dev/reference/configuration/ The `.archgate/config.json` file stores project-level configuration that is committed to version control and shared across the team. This file is created automatically by `archgate init` (to store the auto-detected base branch and any custom domains) or when you manually add configuration. It lives inside the `.archgate/` directory at your project root. ## Schema ```json { "domains": { "security": "SEC", "compliance": "COMP" }, "paths": { "adrs": "docs/adrs", "rules": "docs/adrs" }, "baseBranch": "main", "strict": true } ``` ### `domains` Custom domain-to-prefix mappings. See [Custom Domains](/concepts/domains/#custom-domains) for details. | Key | Type | Description | | ------ | -------- | ------------------------------------------------------------------------------------------- | | _name_ | `string` | Domain name (lowercase kebab-case, 2-32 chars) maps to an ID prefix (uppercase, 2-10 chars) | These are merged with the built-in domains (`backend`, `frontend`, `data`, `architecture`, `general`) at read time. Custom entries cannot override built-in names or prefixes. ### `baseBranch` Base branch for change detection in `archgate check`. When set, `archgate check` skips the auto-detection probes and uses this value directly for `ctx.changedFiles` population via `git diff <baseBranch>...HEAD`. | Type | Default | Description | | -------- | --------------- | ------------------------------------------------------- | | `string` | _(auto-detect)_ | Branch name or remote ref (e.g., `main`, `origin/main`) | This field is **auto-populated** by `archgate init` when a git repository is detected. The auto-detection tries `origin/HEAD`, `origin/main`, `origin/master`, local `main`, and local `master` (first match wins). Re-running `archgate init` does not overwrite a manually configured value. You can also set it manually: ```json { "baseBranch": "main" } ``` See [`archgate check` -- Changed files detection](/reference/cli/check/#changed-files-detection) for the full resolution priority. ### `strict` Project-wide default for `--strict`, supported by `archgate check`, `archgate review-context`, and `archgate adr sync`. `--strict` escalates otherwise-advisory diagnostics into failures -- see [`archgate check` -- Strict mode](/reference/cli/check/#strict-mode) for exactly what each command fails on. | Type | Default | Description | | --------- | ------- | ----------------------------------------------- | | `boolean` | `false` | Enables strict mode for this project by default | Resolution order is CLI flag, then this config value, then `false`. There is no `--no-strict` flag, so the CLI flag can only turn strict mode _on_ over an absent or `false` config value -- it cannot turn off a configured `strict: true`. ```json { "strict": true } ``` ### `paths` Override default directories for ADRs and rules. | Field | Type | Default | Description | | ------- | -------- | ---------------- | ----------------------------------------- | | `adrs` | `string` | `.archgate/adrs` | Relative path to the ADR directory | | `rules` | `string` | `.archgate/lint` | Relative path to the rules/lint directory | Both fields are optional. When omitted, the default `.archgate/adrs/` and `.archgate/lint/` directories are used. #### Path validation - Paths **must be relative** to the project root -- absolute paths (e.g., `/docs/adrs`, `C:\docs\adrs`) are rejected. - Paths **must not contain `..` segments** -- traversal above the project root is not allowed (e.g., `../other-repo/adrs` is rejected). - Paths use forward slashes (`/`) as separators, matching standard glob conventions. ## Custom ADR directory By default, ADRs live in `.archgate/adrs/`. To store them in a different directory (e.g., `docs/adrs/`), add a `paths` section to `.archgate/config.json`: ```json { "paths": { "adrs": "docs/adrs" } } ``` After adding the configuration: 1. Create the target directory (e.g., `mkdir -p docs/adrs`) 2. Move existing ADR files and their companion `.rules.ts` files from `.archgate/adrs/` to the new directory 3. Run `archgate check` to verify the rules still load correctly All CLI commands (`archgate adr list`, `archgate adr create`, `archgate check`, `archgate review-context`) automatically read the configured directory. :::caution The `.archgate/` directory must still exist -- it is the project marker used by the CLI to locate your project root. Do not delete it after configuring custom paths. ### Example: monorepo documentation folder A common pattern is placing ADRs alongside other documentation: ``` my-project/ .archgate/ config.json # { "paths": { "adrs": "docs/adrs" } } lint/ rules.d.ts docs/ adrs/ ARCH-001-api-design.md ARCH-001-api-design.rules.ts BE-001-database-access.md BE-001-database-access.rules.ts rules.d.ts # auto-generated by archgate check guides/ ... src/ ... ``` ## Notes - The `paths` configuration is a **team-wide setting** -- it is committed to version control and applies to all team members. There is no user-level override for ADR paths. - Changing the configuration requires manually editing `.archgate/config.json` after running `archgate init`. - The `rules.d.ts` type definitions file is automatically written to both `.archgate/` and the parent of the configured ADR directory, so companion `.rules.ts` files resolve their `/// <reference path="../rules.d.ts" />` directive correctly. --- ## Reference: Privacy Policy Source: https://cli.archgate.dev/reference/privacy-policy/ For the full Archgate Privacy Policy, please visit: **[archgate.dev/privacy-policy](https://archgate.dev/privacy-policy)** The sections below summarize the most relevant points for CLI users. The canonical policy on the website is the legally binding version. **Data controller:** Dasolve AS (Org.nr 936 035 019), Lillogata 5P, 0484 Oslo, Norway. Contact: [privacy@archgate.dev](mailto:privacy@archgate.dev). ## Summary for CLI users ### What the CLI collects Archgate collects **anonymous usage analytics** (via PostHog) and **crash reports** (via Sentry) to improve the tool. No personal information, source code, file content, or AI prompts are ever collected by the CLI itself. See the [Telemetry](/reference/telemetry/) page for a detailed breakdown of every data point. **Legal basis:** Legitimate interest under GDPR Article 6(1)(f). See our [Legitimate Interest Assessment](https://archgate.dev/legitimate-interest-assessment). ### Data storage and retention | Service | Data | Region | Retention | | ------------- | ---------------------------- | -------------- | ------------------------ | | PostHog Cloud | Anonymous usage analytics | EU (Frankfurt) | 1 year | | Sentry Cloud | Crash reports | EU (Frankfurt) | 90 days | | Turso | Plugins Service account data | EU | Until deletion requested | Data is transmitted via reverse proxies at `n.archgate.dev` (analytics) and `s.archgate.dev` (errors). These are transparent forwarders operated by Dasolve AS on Cloudflare, with no logging or storage. ### Archgate Plugins Service When you sign up via `archgate login`, the **Plugins Service** (`plugins.archgate.dev`) collects personal information: your **email address**, **GitHub username**, **editor choice**, and a **use case description**. This data is used to provision your account and send a welcome email. Authentication tokens are stored as SHA-256 hashes on our servers; on your machine, credentials are kept in your OS credential manager (never as plain-text files). By creating an account, you agree to the [Terms of Service](https://archgate.dev/terms-of-service). See the [full privacy policy](https://archgate.dev/privacy-policy) for complete details. ### How to opt out ```bash # Environment variable (immediate, per-session or in shell profile) export ARCHGATE_TELEMETRY=0 # Or persistently via the CLI archgate telemetry disable ``` Note: telemetry opt-out disables CLI analytics and crash reporting. It does not affect the Plugins Service account data, which is required for plugin access. To delete your account data, contact [privacy@archgate.dev](mailto:privacy@archgate.dev). ### Your rights - **Access:** Request a copy of your data. Email [privacy@archgate.dev](mailto:privacy@archgate.dev) with your install ID (from `~/.archgate/config.json` or `archgate telemetry status`). - **Erasure:** Request deletion of historical data. Email with your install ID. Completed within 30 days. - **Object:** Disable telemetry at any time via `archgate telemetry disable`. - **Portability:** Request data export in JSON or CSV format. - **Complaint:** Contact the Norwegian Data Protection Authority ([Datatilsynet](https://www.datatilsynet.no)). ### What the docs site collects This documentation site (`cli.archgate.dev`) uses [Cloudflare Web Analytics](https://www.cloudflare.com/web-analytics/), a privacy-first analytics service. Cloudflare Web Analytics does not use cookies, does not track individual visitors, and does not collect personal information. It provides aggregate page-view and performance metrics only. PostHog is also used on this site with **cookieless, memory-only tracking** (`persistence: "memory"`). No cookies are set, no localStorage is written, and no data persists between page loads. --- ## Reference: Rule API Source: https://cli.archgate.dev/reference/rule-api/ Archgate rules are TypeScript files that export a plain object typed with `satisfies RuleSet`. Each rule receives a `RuleContext` with utilities for searching files, reading content, and reporting violations. ## RuleSet ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "my-rule-id": { description: "Human-readable description of what this rule checks", async check(ctx) { // Rule logic here -- the report method decides the severity ctx.report.violation({ message: "..." }); }, }, }, } satisfies RuleSet; ``` A rules file default-exports a plain object with a `rules` record keyed by rule ID. Keys become the rule IDs that appear in check output and violation reports. The `satisfies RuleSet` annotation provides type checking without wrapping in a function call. ```typescript type RuleSet = { rules: Record<string, RuleConfig> }; ``` --- ## RuleConfig Each rule in the record must conform to the `RuleConfig` interface. ```typescript interface RuleConfig { description: string; severity?: Severity; check: (ctx: RuleContext) => Promise<void>; } ``` | Field | Type | Required | Description | | ------------- | ------------------------------------- | -------- | ------------------------------------------------ | | `description` | `string` | Yes | Human-readable description shown in check output | | `severity` | `Severity` | No | Accepted and validated, but inert -- see below | | `check` | `(ctx: RuleContext) => Promise<void>` | Yes | Async function containing the rule logic | :::caution `severity` does **not** set the severity of the rule's findings. Each finding takes its severity from the `ctx.report.*` method that produced it: `violation()` is always `error`, `warning()` is `warning`, `info()` is `info` -- even inside a rule declared `severity: "warning"`. To report non-blocking findings, call `ctx.report.warning()`. --- ## RuleContext The `check` function receives a `RuleContext` object with the project state and utility methods. ```typescript interface RuleContext { projectRoot: string; scopedFiles: string[]; changedFiles: string[]; glob(pattern: string): Promise<string[]>; grep(file: string, pattern: RegExp): Promise<GrepMatch[]>; grepFiles(pattern: RegExp, fileGlob: string): Promise<GrepMatch[]>; readFile(path: string): Promise<string>; fileAtBase(path: string): Promise<string | null>; readJSON(path: "package.json"): Promise<PackageJson>; readJSON(path: string): Promise<unknown>; readYAML(path: string): Promise<ReadYamlResult>; ast( path: string, language: "typescript" | "javascript", opts?: AstOptions ): Promise<EsTreeProgram>; ast( path: string, language: "python", opts?: AstOptions ): Promise<PythonAstModule>; ast( path: string, language: "ruby", opts?: AstOptions ): Promise<RubyAstProgram>; ast(path: string, language: AstLanguage, opts?: AstOptions): Promise<AstNode>; findAstNodes(tree: EsTreeNode, ...types: string[]): EsTreeNode[]; findAstNodes(tree: PythonAstNode, ...types: string[]): PythonAstNode[]; findAstNodes( tree: unknown, ...types: string[] ): (EsTreeNode | PythonAstNode)[]; checkCase(value: string, scheme: CaseScheme): boolean; report: RuleReport; } ``` ### Properties #### projectRoot ```typescript projectRoot: string; ``` Absolute path to the project root directory (where `.archgate/` lives). #### scopedFiles ```typescript scopedFiles: string[]; ``` Files matching the ADR's `files` glob patterns from its frontmatter. If the ADR has no `files` field, this contains all project files. Use this as the primary file list for your rule checks. #### changedFiles ```typescript changedFiles: string[]; ``` Files that have been modified according to git. By default, this is auto-populated with the branch diff against the detected base branch (e.g., `origin/main`) plus any uncommitted working-tree changes (staged, unstaged, and untracked non-ignored files). When `--staged` is used, this contains only staged files. When `--base <ref>` is used, this contains all files changed since that ref plus uncommitted working-tree changes. Empty when base detection fails or no changes are found. Use this to build cross-file dependency rules (e.g., "if file A changed, file B must also change"). #### report ```typescript report: RuleReport; ``` The reporting interface for recording violations, warnings, and informational messages. See [RuleReport](#rulereport) below. ### Methods #### glob ```typescript glob(pattern: string): Promise<string[]>; ``` Find files matching a glob pattern relative to the project root. Returns an array of file paths. Files ignored by `.gitignore` are excluded by default. Set `respectGitignore: false` in the ADR frontmatter to include them. ```typescript const testFiles = await ctx.glob("tests/**/*.test.ts"); ``` #### grep ```typescript grep(file: string, pattern: RegExp): Promise<GrepMatch[]>; ``` Search a single file for lines matching a regular expression. Returns an array of `GrepMatch` objects with file path, line number, column, and matched content. ```typescript const matches = await ctx.grep(file, /console\.error\(/); ``` #### grepFiles ```typescript grepFiles(pattern: RegExp, fileGlob: string): Promise<GrepMatch[]>; ``` Search multiple files matching a glob pattern for lines matching a regular expression. Combines `glob` and `grep` into a single call. Files ignored by `.gitignore` are excluded by default. Set `respectGitignore: false` in the ADR frontmatter to include them. ```typescript const matches = await ctx.grepFiles(/TODO:/i, "src/**/*.ts"); ``` #### readFile ```typescript readFile(path: string): Promise<string>; ``` Read the contents of a file as a string. The path is relative to the project root. ```typescript const content = await ctx.readFile("src/config.ts"); ``` #### fileAtBase ```typescript fileAtBase(path: string): Promise<string | null>; ``` Read a file's source at the **comparison base revision** -- the merge base of the base ref and `HEAD`, the same commit `changedFiles` is computed against. Use it to compare the working tree against the point the change set diverged from. `archgate check` resolves the base ref itself, in this order: `--base <ref>`, then the base branch stored in `.archgate/config.json`, then auto-detection (`origin/HEAD`, `origin/main`/`origin/master`, then local `main`/`master`). A base is therefore normally available without passing any flag. Returns `null` in the two "nothing to compare against" cases, so a single null check covers both: - **No base is resolved** -- a `--staged` run, a project that is not a git repository, no base branch could be detected, or the histories are unrelated. - **The file did not exist at the base** -- an added file. ```typescript const before = await ctx.fileAtBase("data/schema.py"); if (before === null) { // No base version to compare against -- skip. return; } const after = await ctx.readFile("data/schema.py"); ``` For a **structural** comparison (ignoring comments and formatting) prefer `ast(path, language, { rev: "base" })` below. #### readJSON ```typescript readJSON(path: "package.json"): Promise<PackageJson>; readJSON(path: string): Promise<unknown>; ``` Read and parse a JSON file. The path is relative to the project root. Returns the parsed value as `unknown` -- cast to the expected type in your rule. The literal path `"package.json"` matches a dedicated overload that returns the ambient `PackageJson` interface (`name`, `version`, `scripts`, `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, `workspaces`, `engines`, ... plus an index signature), so the common case needs no cast. Unlike `glob`, `readFile`, and `ast`, the parsed value is **not** cached across rules: each call returns a fresh object, so mutating it cannot leak into another rule. ```typescript const pkg = await ctx.readJSON("package.json"); const deps = pkg.dependencies ?? {}; const tsconfig = (await ctx.readJSON("tsconfig.json")) as { compilerOptions?: Record<string, unknown>; }; ``` #### readYAML ```typescript readYAML(path: string): Promise<ReadYamlResult>; interface ReadYamlResult { frontmatter: Record<string, YamlValue> | null; content: YamlValue; } type YamlValue = | string | number | boolean | null | YamlValue[] | { [key: string]: YamlValue }; ``` Read a YAML file or a Markdown file with YAML frontmatter. The path is relative to the project root and passes through the same sandbox as `readFile`. One result object covers both shapes -- a nullable `frontmatter` mapping and a `content` typed as `YamlValue`, the JSON-like data YAML's core schema produces (after a `typeof` check you can index into mappings and sequences without casting) -- and dispatch is **extension-based**: - **`.yml` / `.yaml` files**: the whole document is parsed as YAML. `frontmatter` is always `null`; `content` is the parsed value, narrowed by a `typeof` check rather than a cast -- unlike `readJSON`, which returns `unknown`. Because dispatch is by extension, a multi-document stream's `---` separators (Kubernetes manifests, CI configs) are never misread as frontmatter. - **Every other file** (typically Markdown): the leading `---`-delimited block is parsed as `frontmatter` -- `null` when absent ("does this file have frontmatter?" is a single null test, mirroring `fileAtBase`), `{}` when present but empty. `content` is the remaining body text, trimmed -- it is **not** parsed as YAML. `readYAML()` **throws** (fail-closed, like `ast()`) when a `.yml`/`.yaml` file is invalid YAML, or when a frontmatter block is invalid YAML or parses to something other than a mapping (a scalar or a sequence) -- surfacing as a rule execution error with exit code 2 rather than a false pass. ```typescript const { content } = await ctx.readYAML(".github/workflows/ci.yml"); if ( typeof content === "object" && content !== null && !Array.isArray(content) ) { const jobs = content.jobs; // YamlValue -- no cast needed } ``` ```typescript for (const file of await ctx.glob("docs/**/*.md")) { const { frontmatter } = await ctx.readYAML(file); if (frontmatter === null) { ctx.report.violation({ message: `${file} is missing frontmatter`, file }); continue; } if (typeof frontmatter.title !== "string") { ctx.report.violation({ message: `${file} frontmatter must declare a title`, file, }); } } ``` #### ast ```typescript ast( path: string, language: "typescript" | "javascript", opts?: AstOptions ): Promise<EsTreeProgram>; ast( path: string, language: "python", opts?: AstOptions ): Promise<PythonAstModule>; ast( path: string, language: "ruby", opts?: AstOptions ): Promise<RubyAstProgram>; ast(path: string, language: AstLanguage, opts?: AstOptions): Promise<AstNode>; interface AstOptions { rev?: "base"; comments?: boolean; } ``` Parse a source file into its language-native AST. The path is relative to the project root and passes through the same sandbox as `readFile`. Passing the language as a string literal selects a per-language overload, so the return type is already narrowed -- `program.body` is typed without a cast, and only a non-literal `AstLanguage` falls back to the `AstNode` union. TypeScript and JavaScript are parsed in-process; Python and Ruby are parsed by invoking the system interpreter's own standard-library AST facility as a subprocess. The returned tree shape differs per language -- see [AstNode](#astnode). Parse results are cached for the lifetime of a single `archgate check` run, keyed on `(path, language, rev, comments)`: repeated -- even concurrent -- identical calls across rules cost one parse (one interpreter spawn for Python/Ruby), and a failed parse rethrows the same error to every caller. Treat the returned tree as read-only -- it may be shared with other rules. ```typescript const program = await ctx.ast("src/cli.ts", "typescript"); for (const node of program.body) { console.log(node.type); } ``` ##### Parsing the base revision (`{ rev: "base" }`) With `{ rev: "base" }`, `ast()` parses the file at the [comparison base revision](#fileatbase) instead of the working tree -- everything else (return shape, throw contract) is identical. Parse both revisions to ask "did the executable structure change?" Comments are absent from the ESTree and Python `ast` shapes, but node **positions are not**: `loc` / `lineno` fields shift when a comment or blank line moves the lines below it. Compare a **location-free projection** -- strip `loc` / `range` (ESTree) and `lineno` / `col_offset` (Python) before comparing -- so a comment-only edit reads as unchanged. (Python docstrings are string nodes in the tree, so an edited docstring is a real change; strip those too if doc edits should be neutral.) ```typescript // Flag a change only when the executable structure actually changed. // `structurallyEqual` compares the trees with position metadata (loc/range, // lineno/col_offset) stripped -- see the writing-rules guide for a concrete // implementation. for (const file of ctx.changedFiles.filter((f) => f.endsWith(".py"))) { // Skip files with no base counterpart -- ast({ rev: "base" }) would throw. if ((await ctx.fileAtBase(file)) === null) continue; const before = await ctx.ast(file, "python", { rev: "base" }); const after = await ctx.ast(file, "python"); if (!structurallyEqual(before, after)) { ctx.report.violation({ message: `${file} changed behavior`, file }); } } ``` Reach for `fileAtBase()` first when you need to detect the no-base or added-file cases as ordinary control flow -- `ast({ rev: "base" })` **throws** for them (see below). ##### Collecting comments (`{ comments: true }`) With `{ comments: true }`, the returned tree carries a `comments` array -- structured comment data for comment-governance rules, in place of line-by-line regex. Supported for all four languages. For `ruby`, the returned tree is an array (`Ripper.sexp` output), so the `comments` array rides on it as a non-index property. ```typescript interface CommentToken { type: "line" | "block"; value: string; // delimiters (`//`, `/* */`, `#`) removed loc: { start: { line: number; column: number }; end: { line: number; column: number }; }; } ``` ```typescript const tree = await ctx.ast("src/api.ts", "typescript", { comments: true }); for (const comment of tree.comments ?? []) { const lines = comment.value.split("\n").length; if (lines > 10) { ctx.report.warning({ message: "Comment block is too long -- link to an ADR instead", file: "src/api.ts", line: comment.loc.start.line, }); } } ``` Comment positions are accurate against the **original source**, even for TypeScript. This is a deliberate advantage over the tree's own `loc`, which is transpiled-relative for TypeScript (see [AstNode](#astnode)): comments are scanned from the pre-transpile source, so their `loc` never drifts. Python comments are always `type: "line"` (`#`) -- Python has no block comments, and `"""` docstrings are string expressions in the tree, not comments. Ruby `#` comments are `type: "line"`; each `=begin`/`=end` documentation block is a single `type: "block"` token whose `value` is the inner content (the `=begin`/`=end` marker lines stripped, analogous to `/* */` delimiter stripping) and whose `loc` spans the `=begin` line through the `=end` line. Ruby comment `loc` columns are character offsets, consistent with the other languages (Ripper's own sexp node positions are byte offsets), and block `value` line endings are normalized to LF regardless of the source file's line endings. For Python and Ruby, comment extraction is a second pass over the same source (`tokenize` / `Ripper.lex`); a tokenizer failure on otherwise-parseable source degrades to an empty comment list rather than failing the parse. The TypeScript/JavaScript scanner is string- and template-literal-aware, but does not track regular-expression literals, so a comment delimiter inside a regex literal is a known blind spot. ##### Failure behavior `ast()` **throws** on failure -- it never returns `null`: - **Parse failure**: the file does not parse as the requested language. The error message includes the parser's diagnostic. - **Missing interpreter** (`python`/`ruby` only): no suitable interpreter was found on `PATH`. - **Implausible input**: the file's name does not match the requested language (e.g. `ctx.ast("config.json", "python")` throws before any interpreter is invoked). Accepted names are `.ts`/`.tsx`/`.mts`/`.cts` for `typescript`, `.js`/`.jsx`/`.mjs`/`.cjs` for `javascript`, `.py`/`.pyi` for `python`, and `.rb`/`.rake`/`.gemspec` plus the bare basenames `Rakefile` and `Gemfile` for `ruby`. - **No base revision** (`{ rev: "base" }` only): no base ref resolved for this run (see [fileAtBase](#fileatbase)), so there is nothing to parse. Use `fileAtBase()` to detect this as `null` instead. - **File absent at base** (`{ rev: "base" }` only): the path was added since the base and did not exist there. A thrown error is isolated to the failing rule: other rules and ADRs in the same check run continue normally, and the failure surfaces as a rule execution error with exit code 2 (distinct from exit code 1 for violations). The throw cases are distinguishable by message text, so check output tells "this environment cannot run this rule" apart from "this file has a syntax error" or "there is no base revision". :::caution Python and Ruby rules require the corresponding interpreter (`python3`/`python`, `ruby`) on `PATH` wherever `archgate check` runs -- on every developer machine **and** in CI. TypeScript and JavaScript parsing is built into Archgate and needs no interpreter. #### findAstNodes ```typescript findAstNodes(tree: EsTreeNode, ...types: string[]): EsTreeNode[]; findAstNodes(tree: PythonAstNode, ...types: string[]): PythonAstNode[]; findAstNodes(tree: unknown, ...types: string[]): (EsTreeNode | PythonAstNode)[]; ``` Recursively collect every node in a parsed AST whose type-discriminant field matches one of `types`, in preorder (empty when nothing matches). This is the built-in replacement for the recursive walker AST rules used to hand-roll: `ctx.ast()` deliberately returns language-native shapes, but finding nodes by type name only depends on the discriminant field, so one helper covers all languages. Synchronous -- no `await` needed. - **Language-agnostic**: each object node is checked against whichever discriminant field it carries -- `_type` (Python) or `type` (ESTree TypeScript/JavaScript). - **Full traversal**: own-enumerable object values and arrays are recursed, and the `tree` argument itself is a match candidate. - **Multi-type matching**: pass several names when one construct spans multiple node types -- the common case (`"FunctionDef"`/`"AsyncFunctionDef"`, sync/async variants). - **Ruby**: `Ripper.sexp` nodes are plain arrays with no object discriminant field, so a Ruby tree is recursed but its array-shaped nodes never match -- walk Ripper output against its own grammar instead. Before -- the collector each rule file had to repeat (rule files cannot import shared helper modules): ```typescript function collectFunctionDefs( node: unknown, out: PythonAstNode[] = [] ): PythonAstNode[] { if (Array.isArray(node)) { for (const item of node) collectFunctionDefs(item, out); return out; } if (!node || typeof node !== "object") return out; const n = node as PythonAstNode; if (n._type === "FunctionDef" || n._type === "AsyncFunctionDef") out.push(n); for (const value of Object.values(n)) { if (value && typeof value === "object") collectFunctionDefs(value, out); } return out; } const tree = await ctx.ast("app/models.py", "python"); const funcDefs = collectFunctionDefs(tree); ``` After: ```typescript const tree = await ctx.ast("app/models.py", "python"); const funcDefs = ctx.findAstNodes(tree, "FunctionDef", "AsyncFunctionDef"); ``` #### checkCase ```typescript checkCase(value: string, scheme: CaseScheme): boolean; type CaseScheme = | "kebab-case" | "camelCase" | "PascalCase" | "snake_case" | "SCREAMING_SNAKE_CASE"; ``` Check whether a string conforms to a casing scheme -- the built-in replacement for the per-scheme regexes naming rules used to hand-roll. Synchronous and pure -- no `await` needed. Matching is all-or-nothing (the entire string must conform; the empty string matches no scheme) and the vocabulary is ASCII letters and digits only. | Scheme | Matches | Rejects | | ---------------------- | ---------------------------- | -------------------------------------------------- | | `kebab-case` | `writing-rules`, `2fa-setup` | `Writing-Rules`, `writing_rules`, `writing--rules` | | `camelCase` | `checkCase`, `parseURL` | `CheckCase`, `check_case`, `2fast` | | `PascalCase` | `CheckCase`, `HTTPServer` | `checkCase`, `Check_Case`, `1Value` | | `snake_case` | `check_case`, `2fa_setup` | `Check_Case`, `check-case`, `check__case` | | `SCREAMING_SNAKE_CASE` | `CHECK_CASE`, `V2` | `check_case`, `CHECK-CASE`, `CHECK__CASE` | `camelCase` and `PascalCase` follow the ecosystem convention (typescript-eslint's `naming-convention`): a leading lower/uppercase letter followed by any ASCII alphanumerics, so acronym runs (`parseURL`, `HTTPServer`) match. Degenerate values can satisfy several schemes at once (`value` is valid kebab-case, snake_case, and camelCase). Passing an unrecognized scheme name **throws** rather than silently returning `false`, so a typo surfaces as a rule error instead of a false pass/fail. ```typescript for (const file of await ctx.glob("docs/**/*.md")) { const stem = file.split("/").pop()?.replace(/\.md$/, "") ?? ""; if (!ctx.checkCase(stem, "kebab-case")) { ctx.report.violation({ message: `${file} must have a kebab-case filename`, file, }); } } ``` --- ## RuleReport The reporting interface for recording check results. Each method accepts a detail object describing the issue. ```typescript interface RuleReport { violation(detail: ReportDetail): void; warning(detail: ReportDetail): void; info(detail: ReportDetail): void; } ``` #### violation ```typescript report.violation(detail: ReportDetail): void; ``` Report a rule violation. Violations cause the check to fail with exit code 1. Use for hard constraints that must not be merged. #### warning ```typescript report.warning(detail: ReportDetail): void; ``` Report a warning. Warnings appear in check output but do not cause the check to fail -- unless `archgate check --strict` is used, which turns any warning into a failure (exit 1). Use for non-blocking guidance. #### info ```typescript report.info(detail: ReportDetail): void; ``` Report an informational message. Does not affect the check exit code. Use for suggestions or notes. ### ReportDetail The detail object passed to `violation`, `warning`, and `info`. ```typescript interface ReportDetail { message: string; file?: string; line?: number; endLine?: number; endColumn?: number; fix?: string; } ``` | Field | Type | Required | Description | | ----------- | -------- | -------- | --------------------------------------------------- | | `message` | `string` | Yes | Human-readable description of the issue | | `file` | `string` | No | File path where the issue was found | | `line` | `number` | No | Start line number (1-based) | | `endLine` | `number` | No | End line number (1-based), for range highlighting | | `endColumn` | `number` | No | End column number (0-based), for range highlighting | | `fix` | `string` | No | Suggested fix or remediation action | Which reporter surfaces which field: - **Console** prints `message` and `file:line`. It prints `fix` only under `--verbose`, and never columns. - **`--output json`** carries every field above, so agents and editor integrations can highlight the exact range instead of the whole line. - **`--output sarif`** carries `message`, `file`, `line`, and `endLine` (as a SARIF `region`). Columns and `fix` are omitted -- Archgate's report detail carries an `endColumn` but no `startColumn` to anchor it to, so the serializer emits only the line fields. - **`--output github`** emits `file`/`line` annotations with `message` only. Put anything a reviewer must read in `message`; treat `fix` as an enrichment for local and agent-facing output. --- ## GrepMatch Returned by `ctx.grep()` and `ctx.grepFiles()`. ```typescript interface GrepMatch { file: string; line: number; column: number; content: string; } ``` | Field | Type | Description | | --------- | -------- | ----------------------------------------- | | `file` | `string` | Project-relative path to the matched file | | `line` | `number` | Line number of the match (1-based) | | `column` | `number` | Column number of the match (1-based) | | `content` | `string` | Full content of the matched line | --- ## AstNode Returned by `ctx.ast()`. The shape is language-native and deliberately **not** unified across languages -- each language returns its own standard AST vocabulary, so a rule inspecting Python code works against a different grammar than one inspecting TypeScript. ```typescript type AstLanguage = "typescript" | "javascript" | "python" | "ruby"; /** Only the fields every node shares are typed; walk the rest through the index signature. */ interface EsTreeNode { type: string; loc?: { start: { line: number; column: number }; end: { line: number; column: number }; } | null; range?: [number, number]; [key: string]: unknown; } interface EsTreeProgram extends EsTreeNode { type: "Program"; sourceType: "module" | "script"; body: EsTreeNode[]; comments?: CommentToken[]; } interface PythonAstNode { _type: string; lineno?: number; col_offset?: number; end_lineno?: number; end_col_offset?: number; [key: string]: unknown; } interface PythonAstModule extends PythonAstNode { _type: "Module"; body: PythonAstNode[]; comments?: CommentToken[]; } type RubyAstNode = unknown[]; interface RubyAstProgram extends Array<unknown> { comments?: CommentToken[]; } /** Only the return type of the non-literal-language overload. */ type AstNode = EsTreeProgram | PythonAstModule | RubyAstProgram; ``` All of these names are ambient in `.rules.ts` files through the `rules.d.ts` reference -- no import is needed to annotate a walker with `EsTreeNode` or `PythonAstNode`. When parsed with [`{ comments: true }`](#ast), the root node also carries a `comments: CommentToken[]` array (all four languages; for `ruby` it is a non-index property on the root sexp array). It is absent otherwise. | Language | Backing parser | Returned shape | | ------------ | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `typescript` | [meriyah](https://github.com/meriyah/meriyah), in-process, after transpiling TypeScript away with `Bun.Transpiler` | [ESTree](https://github.com/estree/estree) `Program` with `loc` position info. Type-only syntax (`interface`, type aliases, `export type { ... } from`) is erased before parsing -- a file containing only type-level statements parses to an empty `Program` body. `loc` positions refer to the **transpiled output**, not the original `.ts` file -- dropped type-only statements, comments, and blank lines make line numbers drift, so re-locate the construct in the original source (e.g. `ctx.readFile()` plus `indexOf`) before reporting a `line`, or omit `line` entirely; `loc` is source-accurate only for `javascript` | | `javascript` | [meriyah](https://github.com/meriyah/meriyah), in-process | [ESTree](https://github.com/estree/estree) `Program` with `loc` position info | | `python` | Python's standard-library [`ast` module](https://docs.python.org/3/library/ast.html), via the system interpreter | JSON-serialized `ast` nodes: `{ "_type": "Module", "body": [...] }` -- each node carries `_type`, the node's own fields, and `lineno` / `col_offset` positions | | `ruby` | Ruby's standard-library [`Ripper`](https://docs.ruby-lang.org/en/master/Ripper.html), via the system interpreter | `Ripper.sexp` nested arrays: `["program", [["command", ...]]]` with `[line, column]` position pairs embedded in token entries | See [Structural checks with ctx.ast()](/guides/writing-rules/#structural-checks-with-ctxast) for a complete example rule per language. --- ## Severity ```typescript type Severity = "error" | "warning" | "info"; ``` | Value | Reported by | Exit code impact | Description | | ----------- | ------------------------ | --------------------------------- | ------------------------------- | | `"error"` | `ctx.report.violation()` | Causes exit 1 | Hard constraint, blocks merges | | `"warning"` | `ctx.report.warning()` | None, but exit 1 under `--strict` | Non-blocking guidance | | `"info"` | `ctx.report.info()` | None | Informational, suggestions only | A rule that throws (or exceeds the 30-second rule timeout) is a rule execution error, which exits 2 rather than 1. --- ## ViolationDetail The internal representation of a reported issue, used in check output and JSON results. ```typescript interface ViolationDetail { ruleId: string; adrId: string; message: string; file?: string; line?: number; endLine?: number; endColumn?: number; fix?: string; severity: Severity; } ``` | Field | Type | Description | | ----------- | ---------- | ----------------------------------------------------- | | `ruleId` | `string` | Rule ID from the `rules` object key | | `adrId` | `string` | ADR ID from the frontmatter | | `message` | `string` | Human-readable description | | `file` | `string?` | File path where the issue was found | | `line` | `number?` | Start line number (1-based) | | `endLine` | `number?` | End line (1-based), for precise editor highlighting | | `endColumn` | `number?` | End column (0-based), for precise editor highlighting | | `fix` | `string?` | Suggested fix | | `severity` | `Severity` | Effective severity of this violation | --- ## Inline suppression Violations can be suppressed in source code using `archgate-ignore` comments. The engine handles this automatically. Rules do not need any special logic. ```typescript // archgate-ignore ARCH-006/no-unapproved-deps legacy dep, migration planned import chalk from "chalk"; ``` A reason is required. File-level suppression uses `archgate-ignore-file`. Stack multiple comments to suppress more than one rule on the same line. See [Opt-out directives](/guides/writing-rules/#opt-out-directives) for full details and custom directive patterns. --- ## Reference: Telemetry Source: https://cli.archgate.dev/reference/telemetry/ Archgate collects **anonymous usage data** to help us understand how the CLI is used, prioritize features, and fix crashes. This page explains exactly what is collected, what is not, and how to opt out. ## What we collect ### Usage analytics (PostHog) When you run an Archgate command, we record: - **Command name** and **which flags were used** (e.g., `check --output json`; flag presence plus the selected `--output` format — a fixed choice of `console`/`json`/`github`/`sarif` — never free-form flag values) - **Exit code** (0, 1, 2, or 130) and **execution duration** (milliseconds), plus a short **outcome** tag (`success`, `user_error`, `internal_error`, `cancelled`) - **Environment**: OS, architecture, Bun version, Archgate version, CI detection (including provider: GitHub Actions / GitLab CI / CircleCI / etc.), TTY detection, WSL detection, shell (bash, zsh, pwsh...), and locale - **Install context**: how the CLI was installed (binary, proto, local dev dependency, or global package manager) - **Project context**: whether an Archgate project exists in the current directory, how many ADRs it has, how many have automated rules, and how many distinct ADR domains are used - **Repo context** (non-identifying): whether the current directory is a git repository, the host bucket (`github` / `gitlab` / `bitbucket` / `azure-devops` / `other`), a **hashed `repo_id`** (SHA-256 of the normalized remote URL, truncated to 16 hex characters, not reversible), and the default branch name - **Coarse location**: country and region (resolved server-side from your IP, then the IP is discarded; see [IP anonymization](#ip-anonymization)) - **Anonymous install ID**: a random UUID generated on first run, not derived from any personal data In addition to the general command lifecycle events (`command_executed` / `command_completed`), specific commands send enriched outcome events: - **`check`**: aggregate rule counts (total, passed, failed, warnings, errors), output format used, whether filters were applied, files scanned, load duration, check duration, and detected project languages/runtimes/frameworks (matched against a fixed list of known dependency and config-file signals, e.g. `typescript`, `bun`, `nextjs`). No file paths or violation content - **`init`**: editor choice, whether the plugin was installed, whether the project already existed. A separate one-time `project_initialized` event is emitted with the repo host bucket, `repo_is_git`, and a `repo_public` flag. For repos confirmed public on GitHub / GitLab / Bitbucket / Azure DevOps, this event also carries the remote URL, owner, and repo name. See [Repo identity](#repo-identity). Private and self-hosted repos never have identity shared. - **`upgrade`**: version transition (from → to), install method, success/failure, and an optional failure reason - **`login`**: subcommand used (login, logout, refresh, status), success/failure, and a failure bucket (`network`, `tls`, `denied`, `other`) when it fails - **`telemetry_preference_changed`**: fires once when you enable or disable telemetry, so we can understand opt-out rates ## Repo identity Archgate sends a **hashed** `repo_id` with every event so we can count distinct repositories using the CLI without learning their names. The raw remote URL, owner, and repository name are **not** included in the common event stream. On `archgate init`, a one-time `project_initialized` event is emitted. If, and only if, the repository is confirmed **public** on GitHub, GitLab, Bitbucket, or Azure DevOps (via an unauthenticated API probe against the host), that event additionally includes `remote_url`, `repo_owner`, and `repo_name`. This lets us see which public repositories are adopting Archgate without ever exposing private ones. **What's never shared:** - Private repositories (API probe returns 404, 401, or `private: true`) - Self-hosted Git hosts (the probe skips these entirely) - Repositories where the probe times out, is rate-limited, or otherwise fails to return a definitive public answer **Don't want the event at all?** Disable telemetry entirely. The whole `project_initialized` event is then suppressed along with everything else: ```bash # Per-shell / per-invocation export ARCHGATE_TELEMETRY=0 # Or persistently archgate telemetry disable ``` See [How to opt out](#how-to-opt-out) below for the full details. ### Error tracking (Sentry) When the CLI crashes (exit code 2), we send: - **Error type, message, and stack trace**, including the file paths from the crash location. For the compiled binary (the default install), these are Bun's internal bundle paths (e.g. `/$bunfs/root/cli.js`), not paths from your filesystem. For a `local` (dev dependency) or `global-pm` install, stack frames can include the real absolute path to the installed CLI file on your machine - **Install path**: the absolute path to the installed `archgate` executable or entry script, sent as a Sentry tag on every crash report - **Runtime context**: OS, architecture, Bun version, Archgate version, install method - **Anonymous install ID** (same random UUID as analytics) ## What we do NOT collect - **No personal information**: no emails, passwords, secrets, or IP addresses. Two qualified exceptions: the one-time `project_initialized` event can include the remote URL, `repo_owner`, and `repo_name` for repositories confirmed public by their host (see [Repo identity](#repo-identity); private and self-hosted repos never have identity shared), and crash reports include the absolute install path of the `archgate` executable, which can contain your OS account name (see [Error tracking](#error-tracking-sentry)). - **No file content**: no ADR content or source code. Crash reports (Sentry) include the file paths from the crash location in the stack trace and the absolute install path of the `archgate` executable (see [Error tracking](#error-tracking-sentry) for exactly what that looks like across install methods), but never file contents. - **No prompt or AI context**: nothing from agent interactions, prompts, or AI-generated content - **No free-form flag values**: the one exception is `--output`'s selected format (a fixed enum: `console`/`json`/`github`/`sarif`); we never record user-provided values or what the output contained - **No network activity**: no API keys or tokens, and no URLs — with one exception: the remote URL of a repository confirmed public by its host, in the one-time `project_initialized` event (see [Repo identity](#repo-identity)) ## IP anonymization Archgate uses PostHog's built-in IP anonymization: 1. Your CLI sends an event to PostHog with `$ip: null` 2. PostHog resolves your IP to a **country and region** (e.g., "US", "California") server-side 3. The IP address is then **discarded**. It is never stored in PostHog For Sentry error tracking, the project has **"Prevent Storing of IP Addresses"** enabled, so IPs are stripped before storage. ## How to opt out You can disable all telemetry (both analytics and error tracking) in two ways: ### Environment variable ```bash export ARCHGATE_TELEMETRY=0 ``` Accepted values: `0`, `false`, `no`, `off` (case-insensitive). Add this to your shell profile (`.bashrc`, `.zshrc`, etc.) to disable permanently. ### CLI command ```bash archgate telemetry disable ``` To re-enable: ```bash archgate telemetry enable ``` To check current status: ```bash archgate telemetry status ``` The environment variable takes precedence over the CLI setting. If `ARCHGATE_TELEMETRY=0` is set, telemetry is disabled regardless of the CLI config. ## Legal basis Archgate CLI telemetry operates on an **opt-out basis** under GDPR Article 6(1)(f) and LGPD Article 7, IX c/c Article 10: legitimate interests of the controller. We have published a formal [Legitimate Interest Assessment](https://archgate.dev/legitimate-interest-assessment) documenting why this is proportionate and lawful. In summary: the data is anonymous (random UUID, no PII), the impact on users is minimal, robust safeguards are in place (IP anonymization, EU storage, limited retention, transparency), and users retain full control via an easy, permanent opt-out. ## Where data is stored | Service | Data | Region | Retention | | ------------- | --------------------------------- | -------------- | ------------------- | | PostHog Cloud | Anonymous usage analytics | EU (Frankfurt) | 1 year | | Sentry Cloud | Crash reports | EU (Frankfurt) | 90 days | | Local config | Telemetry preference + install ID | Your machine | Until you delete it | Analytics events are routed through `n.archgate.dev` and error reports through `s.archgate.dev`. These are transparent reverse proxies operated by Dasolve AS on Cloudflare infrastructure. They forward requests without logging, storing, or inspecting payloads. ## Your rights - **Right of access**: Request a copy of all data associated with your install ID. Email [privacy@archgate.dev](mailto:privacy@archgate.dev) with your install ID (found via `archgate telemetry status` or in `~/.archgate/config.json`). Response within 30 days. - **Right to erasure**: Request deletion of historical analytics and crash data. Disabling telemetry stops future collection but does not delete past events. Email [privacy@archgate.dev](mailto:privacy@archgate.dev) with your install ID for deletion. - **Right to object**: Disable telemetry at any time via `archgate telemetry disable` or `ARCHGATE_TELEMETRY=0`. - **Right to lodge a complaint**: Contact the Norwegian Data Protection Authority ([Datatilsynet](https://www.datatilsynet.no)) or, for Brazilian users, the ANPD ([www.gov.br/anpd](https://www.gov.br/anpd)). **Data controller:** Dasolve AS (Org.nr 936 035 019), Lillogata 5P, 0484 Oslo, Norway. Contact: [privacy@archgate.dev](mailto:privacy@archgate.dev). **Brazilian users (LGPD):** For LGPD-specific rights (Art. 18), international transfer details (Art. 33), and ANPD contact information, see the [Portuguese privacy policy](https://archgate.dev/pt-br/privacy-policy). ## Open source The telemetry implementation is fully open source. You can inspect exactly what data is collected by reading: - [`src/helpers/telemetry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry.ts): PostHog event tracking - [`src/helpers/sentry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/sentry.ts): Sentry error capture - [`src/helpers/telemetry-config.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry-config.ts): Config and opt-out logic --- ## Examples: clean-architecture-layers Source: https://cli.archgate.dev/examples/clean-architecture-layers/ Enforce dependency direction in clean architecture: inner layers must not reference outer layers. ## Rule details Clean architecture mandates that dependencies point inward: API → Application → Domain. The Domain layer must have zero external dependencies, the Application layer must not reference Infrastructure directly, and no lower layer should reference the API project. This rule checks `using` directives (C#) or `import` statements to enforce these boundaries. The same pattern applies to any layered architecture in any language. Adjust the import patterns and layer paths accordingly. ## Examples of **incorrect** code ```csharp title="Domain/Entities/User.cs" using Microsoft.EntityFrameworkCore; // ✗ Domain → Infrastructure using StavangerChallenge.Infrastructure; // ✗ Domain → Infrastructure ``` ```csharp title="Application/Services/UserService.cs" using StavangerChallenge.Infrastructure; // ✗ Application → Infrastructure using StavangerChallenge.API; // ✗ Application → API ``` ## Examples of **correct** code ```csharp title="Domain/Entities/User.cs" namespace StavangerChallenge.Domain.Entities; // No external dependencies, pure domain logic ``` ```csharp title="Application/Services/UserService.cs" using StavangerChallenge.Domain.Entities; // ✓ Application → Domain // Uses IRepository<T> interface, not Infrastructure directly ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "no-infrastructure-in-domain": { description: "Domain layer must not reference Infrastructure or ORM packages", async check(ctx) { const domainFiles = await ctx.glob("backend/src/**/Domain/**/*.cs"); for (const file of domainFiles) { const matches = await ctx.grep( file, /using\s+(Microsoft\.EntityFrameworkCore|MyApp\.Infrastructure)/ ); for (const match of matches) { ctx.report.violation({ message: `Domain must not reference Infrastructure: "${match.content.trim()}"`, file: match.file, line: match.line, fix: "Remove this using directive. Domain entities should have zero external dependencies.", }); } } }, }, "no-infrastructure-in-application": { description: "Application layer must not reference Infrastructure directly", async check(ctx) { const appFiles = await ctx.glob("backend/src/**/Application/**/*.cs"); for (const file of appFiles) { const matches = await ctx.grep(file, /using\s+MyApp\.Infrastructure/); for (const match of matches) { ctx.report.violation({ message: `Application must not reference Infrastructure: "${match.content.trim()}"`, file: match.file, line: match.line, fix: "Define an interface in Application and implement it in Infrastructure.", }); } } }, }, "no-upward-api-references": { description: "No layer should reference the API project", async check(ctx) { const lowerLayerPatterns = [ "backend/src/**/Domain/**/*.cs", "backend/src/**/Application/**/*.cs", "backend/src/**/Infrastructure/**/*.cs", ]; for (const pattern of lowerLayerPatterns) { const files = await ctx.glob(pattern); for (const file of files) { const matches = await ctx.grep(file, /using\s+MyApp\.API/); for (const match of matches) { ctx.report.violation({ message: `Lower layers must not reference the API project: "${match.content.trim()}"`, file: match.file, line: match.line, fix: "Dependencies flow inward: API → Application → Domain.", }); } } } }, }, }, } satisfies RuleSet; ``` For TypeScript projects, adapt the patterns: ```typescript // Check that domain/ does not import from infrastructure/ /import\s+.*from\s+["'].*\/infrastructure\// // Check that no layer imports from api/ /import\s+.*from\s+["'].*\/api\// ``` ## When to use it When your project follows clean architecture, hexagonal architecture, or any layered architecture where dependency direction must be enforced. ## When not to use it When your project does not use a layered architecture, or when layers are not organized into distinct directories. --- ## Examples: Common Rule Patterns Source: https://cli.archgate.dev/examples/common-rule-patterns/ Browse complete, copy-pasteable rule examples organized by category. Each rule page follows a consistent format: what the rule checks, examples of incorrect and correct code, the full `.rules.ts` implementation, and guidance on when to use it. ## Dependency & Package Management | Rule | Description | | ------------------------------------------------------- | ---------------------------------------------------------------------------- | | [no-unapproved-deps](/examples/no-unapproved-deps/) | Restrict production dependencies to an approved allowlist | | [version-catalog](/examples/version-catalog/) | Enforce centralized version management in monorepos with `catalog:` notation | | [monorepo-task-runner](/examples/monorepo-task-runner/) | Ban `package.json` scripts and require task runner config in every package | ## Import & API Restrictions | Rule | Description | | ----------------------------------------------------- | ------------------------------------------------------------------------- | | [no-banned-imports](/examples/no-banned-imports/) | Prevent usage of banned libraries with a data-driven pattern list | | [no-banned-api](/examples/no-banned-api/) | Ban specific runtime APIs that cause cross-platform or reliability issues | | [wrapper-enforcement](/examples/wrapper-enforcement/) | Enforce use of a project wrapper instead of a raw platform API | ## File Structure & Organization | Rule | Description | | ------------------------------------------------------- | --------------------------------------------------------------------- | | [kebab-case-filenames](/examples/kebab-case-filenames/) | Enforce consistent file naming conventions using regex validation | | [no-barrel-files](/examples/no-barrel-files/) | Detect and ban barrel files (re-export-only `index.ts`) | | [test-file-coverage](/examples/test-file-coverage/) | Verify that every source file has a corresponding test file | | [component-pairing](/examples/component-pairing/) | Enforce Connected/presentational component pairs with opt-out support | ## Code Quality & Output | Rule | Description | | ------------------------------------------------------------------- | ------------------------------------------------------------------ | | [no-todo-comments](/examples/no-todo-comments/) | Flag TODO, FIXME, HACK, and XXX comments before merging | | [no-emoji-in-output](/examples/no-emoji-in-output/) | Ban emoji and raw ANSI codes in CLI output strings | | [max-file-length](/examples/max-file-length/) | Warn when source files exceed a configurable line count | | [page-component-constraints](/examples/page-component-constraints/) | Enforce size limits and ban data-fetching hooks in page components | ## Database Schema | Rule | Description | | --------------------------------------------------------- | --------------------------------------------------------------- | | [database-audit-fields](/examples/database-audit-fields/) | Ensure all tables include `created_at` and `updated_at` columns | ## Architecture Boundaries | Rule | Description | | ----------------------------------------------------------------- | ----------------------------------------------------- | | [required-export-pattern](/examples/required-export-pattern/) | Verify files export a required function signature | | [openapi-routes](/examples/openapi-routes/) | Ensure backend routes use OpenAPI-typed definitions | | [clean-architecture-layers](/examples/clean-architecture-layers/) | Enforce dependency direction in layered architectures | The editor plugins for [Claude Code](/guides/claude-code-plugin/) and [Cursor](/guides/cursor-integration/) include a Quality Manager skill that identifies recurring patterns in your codebase and proposes new rules to enforce them. [Sign up for beta access](https://plugins.archgate.dev). --- ## Examples: component-pairing Source: https://cli.archgate.dev/examples/component-pairing/ Enforce that stateful "Connected" components have a corresponding presentational component file. ## Rule details The container/presentational pattern separates data-fetching (Connected) components from pure UI (presentational) components. This rule checks that every `*Connected.tsx` file has a matching `*.tsx` presentational counterpart. It supports an opt-out directive (`// @no-presentational: <reason>`) for cases where a Connected component does not need a presentational pair. ## Examples of **incorrect** code ``` src/components/ UserListConnected.tsx ← no UserList.tsx ✗ ``` ## Examples of **correct** code ``` src/components/ UserListConnected.tsx ← fetches data, passes to UserList UserList.tsx ← pure presentational component ``` Or, with an opt-out: ```typescript title="src/components/RedirectConnected.tsx" // @no-presentational: this component only redirects, no UI to render import { useNavigate } from "react-router"; // ... ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "connected-wrapper-existence": { description: "Connected wrappers must have a corresponding presentational component file", async check(ctx) { const connectedFiles = await ctx.glob( "packages/frontend/src/components/**/*Connected.tsx" ); for (const file of connectedFiles) { if (file.includes(".stories.") || file.includes(".test.")) continue; const content = await ctx.readFile(file); // Support opt-out directive if (/^\/\/\s*@no-presentational:/.test(content.trimStart())) continue; const presentationalFile = file.replace(/Connected\.tsx$/, ".tsx"); try { await ctx.readFile(presentationalFile); } catch { ctx.report.violation({ message: `Connected wrapper has no corresponding presentational component (expected ${presentationalFile}). Add "// @no-presentational: <reason>" to opt out.`, file, fix: `Create ${presentationalFile} as the presentational counterpart`, }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your frontend architecture follows the container/presentational pattern and you want to enforce that data-fetching logic is always separated from UI rendering. ## When not to use it When your project uses a different component architecture (e.g., hooks-only, or server components), or when the pattern is applied selectively rather than universally. --- ## Examples: database-audit-fields Source: https://cli.archgate.dev/examples/database-audit-fields/ Ensure all database tables include required audit columns. ## Rule details Audit fields (`created_at`, `updated_at`) are essential for debugging, data governance, and sync protocols. This rule parses schema files to find table definitions, extracts each table's column block using brace-depth tracking, and checks for the presence of required columns. It works with any ORM that defines tables as function calls with object arguments (Drizzle, Prisma schema-in-code, etc.). ## Examples of **incorrect** code ```typescript title="packages/db/src/schema.ts" export const users = sqliteTable("users", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull(), // Missing created_at and updated_at }); ``` ## Examples of **correct** code ```typescript title="packages/db/src/schema.ts" export const users = sqliteTable("users", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull(), created_at: text("created_at") .notNull() .$defaultFn(() => new Date().toISOString()), updated_at: text("updated_at") .notNull() .$defaultFn(() => new Date().toISOString()), }); ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "audit-fields": { description: "All database tables must have created_at and updated_at columns", async check(ctx) { const schemaFiles = await ctx.glob("packages/**/src/schema.ts"); const TABLE_PATTERN = /sqliteTable\s*\(\s*["']([^"']+)["']/g; for (const file of schemaFiles) { const content = await ctx.readFile(file); let match; while ((match = TABLE_PATTERN.exec(content)) !== null) { const tableName = match[1]; const tableStart = match.index; // Extract the table's column block using brace-depth tracking const afterMatch = content.slice(tableStart); const firstBrace = afterMatch.indexOf("{"); if (firstBrace === -1) continue; let depth = 0; let tableEnd = tableStart + firstBrace; for (let i = firstBrace; i < afterMatch.length; i++) { if (afterMatch[i] === "{") depth++; else if (afterMatch[i] === "}") { depth--; if (depth === 0) { tableEnd = tableStart + i; break; } } } const tableBlock = content.slice(tableStart, tableEnd + 1); if (!tableBlock.includes('"created_at"')) { ctx.report.violation({ message: `Table "${tableName}" is missing "created_at" column`, file, fix: 'Add created_at: text("created_at").notNull().$defaultFn(() => new Date().toISOString())', }); } if (!tableBlock.includes('"updated_at"')) { ctx.report.violation({ message: `Table "${tableName}" is missing "updated_at" column`, file, fix: 'Add updated_at: text("updated_at").notNull().$defaultFn(() => new Date().toISOString())', }); } } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your data model requires consistent audit fields for traceability, debugging, or compliance. Adapt the `TABLE_PATTERN` regex and column names for your ORM: ```typescript // For Drizzle with PostgreSQL const TABLE_PATTERN = /pgTable\s*\(\s*["']([^"']+)["']/g; // For Prisma-style definitions const TABLE_PATTERN = /model\s+(\w+)\s*\{/g; ``` ## When not to use it When some tables intentionally omit audit fields (e.g., join tables, materialized views), or when audit fields are added automatically at the database level via triggers. --- ## Examples: kebab-case-filenames Source: https://cli.archgate.dev/examples/kebab-case-filenames/ Enforce consistent file naming conventions across source directories. ## Rule details Inconsistent file naming (mixing `camelCase`, `PascalCase`, `snake_case`, and `kebab-case`) makes files harder to find and causes case-sensitivity issues across operating systems. This rule validates every scoped file name against a regex pattern and suggests corrections. ## Examples of **incorrect** code ``` src/ helpers/ pathUtils.ts ← camelCase Git_Helper.ts ← PascalCase + snake_case ADRWriter.ts ← PascalCase ``` ## Examples of **correct** code ``` src/ helpers/ path-utils.ts git-helper.ts adr-writer.ts ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> import { basename } from "node:path"; const KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*\.(ts|tsx|js|jsx)$/; export default { rules: { "kebab-case-filenames": { description: "Source files must use kebab-case naming", async check(ctx) { for (const file of ctx.scopedFiles) { const name = basename(file); // Skip test files and type declaration files if (name.endsWith(".test.ts") || name.endsWith(".d.ts")) continue; if (!KEBAB_CASE.test(name)) { ctx.report.violation({ message: `File "${name}" does not follow kebab-case naming convention`, file, fix: `Rename to ${name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`, }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your team has standardized on a naming convention and wants to enforce it automatically. Adapt the regex for your convention: ```typescript // PascalCase (React components) const PASCAL_CASE = /^[A-Z][a-zA-Z0-9]*\.(ts|tsx)$/; // camelCase const CAMEL_CASE = /^[a-z][a-zA-Z0-9]*\.(ts|tsx|js|jsx)$/; // snake_case (Python-style) const SNAKE_CASE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*\.(ts|tsx|js|jsx)$/; ``` ## When not to use it When your project intentionally mixes naming conventions (e.g., PascalCase for React components and kebab-case for utilities), or when migrating a legacy codebase where bulk renames are not feasible. --- ## Examples: license-compatibility Source: https://cli.archgate.dev/examples/license-compatibility/ Prevent copyleft or incompatible licenses from entering your dependency tree. ## Rule details When you compile or bundle dependencies into your application, their license terms apply to the combined work. A single GPL dependency in a permissive (MIT/Apache-2.0) project can force the entire project to adopt copyleft terms. This rule checks all direct dependencies against a permissive-license allowlist. ## Examples of **incorrect** code ```json title="package.json" { "dependencies": { "zod": "^3.23.0" }, "devDependencies": { "readline-sync": "^1.4.10" } } ``` If `readline-sync` uses GPL-3.0, it triggers a violation even as a devDependency. ## Examples of **correct** code ```json title="package.json" { "dependencies": { "zod": "^3.23.0" }, "devDependencies": { "fast-check": "^4.7.0" } } ``` Both `zod` (MIT) and `fast-check` (MIT) are on the permissive allowlist. ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> const ALLOWED_LICENSES = new Set([ "MIT", "Apache-2.0", "ISC", "BSD-2-Clause", "BSD-3-Clause", "0BSD", "CC0-1.0", "Unlicense", "BlueOak-1.0.0", ]); function isAllowed(license: string | undefined): boolean { if (!license) return false; if (ALLOWED_LICENSES.has(license)) return true; // Handle SPDX OR expressions: at least one option must be allowed const normalized = license.trim().replace(/^\(/u, "").replace(/\)$/u, ""); if (ALLOWED_LICENSES.has(normalized)) return true; if (normalized.includes(" OR ")) { return normalized.split(" OR ").some((l) => ALLOWED_LICENSES.has(l.trim())); } return false; } /** * Extract package name from a node_modules path. * Handles both regular and scoped (@scope/name) packages. */ function extractPackageName(path: string): string { const parts = path.replaceAll("\\", "/").split("/"); const nmIdx = parts.lastIndexOf("node_modules"); if (nmIdx === -1) return path; const afterNm = parts.slice(nmIdx + 1); if (afterNm[0]?.startsWith("@") && afterNm.length >= 2) { return `${afterNm[0]}/${afterNm[1]}`; } return afterNm[0] ?? path; } export default { rules: { "no-copyleft-deps": { description: "All dependencies (including transitive) must use permissive licenses", async check(ctx) { // Scan ALL packages in node_modules, direct AND transitive. // Brace expansion covers both regular and scoped packages. const pkgFiles = await ctx.glob("node_modules/{*,@*/*}/package.json"); const depResults = await Promise.all( pkgFiles.map(async (pkgPath) => { try { const depPkg = (await ctx.readJSON(pkgPath)) as { license?: string; }; return { dep: extractPackageName(pkgPath), license: depPkg.license, }; } catch { return null; } }) ); for (const result of depResults) { if (result === null) continue; if (!isAllowed(result.license)) { ctx.report.violation({ message: `Dependency "${result.dep}" has disallowed license: "${result.license ?? "(none)"}".`, file: "package.json", fix: `Remove "${result.dep}" or find an alternative with a permissive license.`, }); } } }, }, }, } satisfies RuleSet; ``` ## Customization - **Change the allowlist**: Add or remove license identifiers based on your project's license. GPL projects can allow GPL dependencies; Apache-2.0 projects should block them. - **Scan transitives (the default here)**: the glob above walks every package installed at the top level of `node_modules/`, scoped packages included — with npm's or Bun's flat layout that covers hoisted transitive dependencies too. Two requirements: the containing ADR must set `respectGitignore: false`, because `node_modules/` is normally gitignored and `ctx.glob` honors `.gitignore` by default; and nested `node_modules` trees (pnpm layouts, version conflicts) are not crossed — switch the pattern to `node_modules/**/package.json` to include them, at the cost of also matching `package.json` files that ship inside packages. - **Check only production deps**: Read the root `package.json` with `ctx.readJSON("package.json")` and filter `pkgFiles` down to names present in its `dependencies` field if you only want to gate bundled/shipped code, not `devDependencies`. Note this keeps **direct** production dependencies only — the transitive packages they pull in are dropped from the scan; covering the full production graph needs a lockfile-aware resolution. ## When to use it When your project uses a permissive license (MIT, Apache-2.0, ISC, BSD) and you want to prevent copyleft contamination, especially in compiled/bundled distributions. ## When not to use it In GPL-licensed projects (where copyleft dependencies are compatible), or when you have a dedicated license-scanning SaaS tool (FOSSA, Snyk) that already gates your CI pipeline. --- ## Examples: max-file-length Source: https://cli.archgate.dev/examples/max-file-length/ Warn when source files exceed a line count threshold. ## Rule details Large files are harder to navigate, review, and test. This rule counts lines in each scoped file and reports a warning when the count exceeds a configurable maximum. Using `warning` severity keeps CI green while surfacing files that should be refactored. ## Examples of **incorrect** code A file with 450 lines when the threshold is 300: ``` src/engine/runner.ts (450 lines) ``` ## Examples of **correct** code The same logic split into focused modules: ``` src/engine/runner.ts (120 lines) src/engine/loader.ts (95 lines) src/engine/reporter.ts (85 lines) ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> const MAX_LINES = 300; export default { rules: { "max-file-length": { description: `Source files should not exceed ${MAX_LINES} lines`, severity: "warning", async check(ctx) { const checks = ctx.scopedFiles.map(async (file) => { const content = await ctx.readFile(file); const lineCount = content.split("\n").length; if (lineCount > MAX_LINES) { ctx.report.warning({ message: `File has ${lineCount} lines (max: ${MAX_LINES}). Consider splitting it.`, file, fix: "Extract related functions into separate modules", }); } }); await Promise.all(checks); }, }, }, } satisfies RuleSet; ``` ## When to use it When you want a soft guardrail against files growing too large. Adjust `MAX_LINES` to match your team's preference (200-500 is common). ## When not to use it When some files are legitimately large (generated code, test suites with many cases), or when you prefer other complexity metrics like cyclomatic complexity. --- ## Examples: monorepo-task-runner Source: https://cli.archgate.dev/examples/monorepo-task-runner/ Enforce that all packages use a centralized task runner instead of `package.json` scripts. ## Rule details In a monorepo, `package.json` scripts are local to each package and cannot express cross-package dependencies, caching, or orchestration. This rule enforces that all packages use a centralized task runner (e.g., Moon, Turborepo, Nx) by banning `scripts` in `package.json` and requiring a task runner config file (`moon.yml`, `turbo.json`, etc.) in every package. ## Examples of **incorrect** code ```json title="packages/api/package.json" { "name": "@myorg/api", "scripts": { "build": "tsc", "test": "vitest", "lint": "eslint ." } } ``` ## Examples of **correct** code ```json title="packages/api/package.json" { "name": "@myorg/api" } ``` ```yaml title="packages/api/moon.yml" tasks: build: command: tsc inputs: - src/**/* test: command: vitest deps: - ~:build lint: command: eslint . ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "no-package-scripts": { description: "package.json must not have scripts. Use the task runner instead", async check(ctx) { const packageJsonFiles = [ ...(await ctx.glob("packages/*/package.json")), ...(await ctx.glob("packages/*/*/package.json")), ]; for (const file of packageJsonFiles) { const pkg = (await ctx.readJSON(file)) as { scripts?: Record<string, string>; }; if (pkg.scripts && Object.keys(pkg.scripts).length > 0) { ctx.report.violation({ message: `${file}: has "scripts" field. Use task runner config instead`, file, fix: 'Move scripts to the task runner config and remove "scripts" from package.json', }); } } }, }, "task-runner-config": { description: "All packages must have a task runner configuration file", async check(ctx) { const packageJsonFiles = [ ...(await ctx.glob("packages/*/package.json")), ...(await ctx.glob("packages/*/*/package.json")), ]; for (const file of packageJsonFiles) { const configPath = file.replace("/package.json", "/moon.yml"); try { await ctx.readFile(configPath); } catch { ctx.report.violation({ message: `Missing task runner config: ${configPath}`, file: configPath, fix: "Create a moon.yml file with appropriate task definitions for this package", }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it In monorepos that use a centralized task runner for build orchestration. Adapt the config file check for your runner: ```typescript // Turborepo const configPath = file.replace("/package.json", "/turbo.json"); // Nx const configPath = file.replace("/package.json", "/project.json"); ``` ## When not to use it In single-package repositories, or in monorepos where the task runner is configured centrally (e.g., a single `turbo.json` at the root) rather than per-package. --- ## Examples: no-banned-api Source: https://cli.archgate.dev/examples/no-banned-api/ Ban specific runtime APIs that cause cross-platform or reliability issues. ## Rule details Some APIs work correctly on one platform but fail on another. For example, Bun's shell API (`Bun.$`) hangs on Windows due to pipe deadlocks. This rule detects multiple variants of a banned API (the direct call, the import, and destructured usage), ensuring no form slips through. This pattern generalizes to any API you want to ban while allowing a safe alternative. ## Examples of **incorrect** code ```typescript title="src/helpers/git.ts" // Direct usage const result = await Bun.$`git status`; // Import import { $ } from "bun"; // Destructured usage const output = await $`ls -la`; ``` ## Examples of **correct** code ```typescript title="src/helpers/git.ts" const proc = Bun.spawn(["git", "status"], { stdout: "pipe", stderr: "pipe" }); const output = await new Response(proc.stdout).text(); ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "no-bun-shell": { description: "Subprocess execution must use Bun.spawn, not Bun.$ (shell hangs on Windows)", async check(ctx) { const files = ctx.scopedFiles.filter( (f) => !f.includes("tests/") && !f.includes(".archgate/") ); // Variant 1: Bun.$` template literal const bunShellMatches = await Promise.all( files.map((file) => ctx.grep(file, /Bun\.\$`/)) ); for (const fileMatches of bunShellMatches) { for (const m of fileMatches) { ctx.report.violation({ message: "Do not use Bun.$ template literals. They hang on Windows. Use Bun.spawn instead.", file: m.file, line: m.line, fix: "Replace Bun.$`cmd args` with Bun.spawn(['cmd', 'args'], { stdout: 'pipe', stderr: 'pipe' })", }); } } // Variant 2: import { $ } from "bun" const dollarImportMatches = await Promise.all( files.map((file) => ctx.grep(file, /import\s*\{[^}]*\$[^}]*\}\s*from\s*["']bun["']/) ) ); for (const fileMatches of dollarImportMatches) { for (const m of fileMatches) { ctx.report.violation({ message: 'Do not import $ from "bun". The shell API hangs on Windows. Use Bun.spawn instead.', file: m.file, line: m.line, fix: "Remove the $ import and replace shell calls with Bun.spawn", }); } } // Variant 3: await $` (destructured) const destructuredMatches = await Promise.all( files.map((file) => ctx.grep(file, /await\s+\$`/)) ); for (const fileMatches of destructuredMatches) { for (const m of fileMatches) { ctx.report.violation({ message: "Do not use $` template literals. They hang on Windows. Use Bun.spawn instead.", file: m.file, line: m.line, fix: "Replace $`cmd args` with Bun.spawn(['cmd', 'args'], { stdout: 'pipe', stderr: 'pipe' })", }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your project must run on multiple platforms and a specific API is known to fail on one of them. Also useful for banning deprecated APIs with known reliability issues. ## When not to use it When your project targets a single platform and the banned API works reliably there. --- ## Examples: no-banned-imports Source: https://cli.archgate.dev/examples/no-banned-imports/ Prevent usage of banned libraries and enforce recommended alternatives. ## Rule details Teams often ban heavy or deprecated libraries in favor of lighter or native alternatives. This rule uses a data-driven configuration: an array of objects specifying the regex pattern, the library name, and the recommended alternative. Adding a new ban is a one-line change. ## Examples of **incorrect** code ```typescript title="src/utils/date.ts" import { format } from "moment"; ``` ```typescript title="src/api/client.ts" import axios from "axios"; ``` ## Examples of **correct** code ```typescript title="src/utils/date.ts" import { format } from "date-fns"; ``` ```typescript title="src/api/client.ts" const response = await fetch("/api/data"); ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> const BANNED_IMPORTS = [ { pattern: /from\s+['"]lodash['"]/, name: "lodash", alternative: "native array methods", }, { pattern: /from\s+['"]moment['"]/, name: "moment", alternative: "Temporal API or date-fns", }, { pattern: /from\s+['"]axios['"]/, name: "axios", alternative: "native fetch()", }, ]; export default { rules: { "no-banned-imports": { description: "Prevent usage of banned libraries", async check(ctx) { for (const banned of BANNED_IMPORTS) { const matches = await ctx.grepFiles(banned.pattern, "src/**/*.ts"); for (const match of matches) { ctx.report.violation({ message: `Banned import: "${banned.name}" is not allowed. Use ${banned.alternative} instead.`, file: match.file, line: match.line, fix: `Replace ${banned.name} with ${banned.alternative}`, }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your team has standardized on specific libraries and wants to prevent drift toward alternatives. Common bans include lodash (native methods), moment (date-fns or Temporal), and axios (native fetch). ## When not to use it When your project has no preference between libraries, or when the banned library is still in active migration and some usage is expected temporarily. --- ## Examples: no-barrel-files Source: https://cli.archgate.dev/examples/no-barrel-files/ Detect and ban barrel files: `index.ts` files that contain only re-exports and no logic. ## Rule details Barrel files (re-export-only `index.ts`) obscure where code actually lives, hurt tree-shaking, create circular dependency risks, and slow down IDE navigation. This rule uses a custom analysis function to determine whether an `index.ts` file is a pure re-export barrel by inspecting every non-comment, non-blank line. ## Examples of **incorrect** code ```typescript title="src/helpers/index.ts" export { logInfo, logError } from "./log"; export { resolvePaths } from "./paths"; export type { PathConfig } from "./paths"; ``` A file that only re-exports symbols from other modules is a barrel file. ## Examples of **correct** code ```typescript title="src/helpers/index.ts" export { logInfo, logError } from "./log"; export { resolvePaths } from "./paths"; // This file has its own logic, so it is not a barrel export function getHelperVersion(): string { return "1.0.0"; } ``` Or better, delete `index.ts` entirely and import directly: ```typescript import { logInfo } from "./helpers/log"; import { resolvePaths } from "./helpers/paths"; ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> function isBarrelFile(content: string): boolean { const lines = content .split("\n") .map((l) => l.trim()) .filter( (l) => l !== "" && !l.startsWith("//") && !l.startsWith("/*") && !l.startsWith("*") ); if (lines.length === 0) return false; return lines.every( (line) => line.startsWith("export ") || line.startsWith("export{") || line.startsWith("import ") || line.startsWith("} from") || line.startsWith("type ") || /^[A-Za-z_$,\s]+$/.test(line) || line === "}" || line === "};" ); } export default { rules: { "no-barrel-files": { description: "index.ts files must not be pure re-export barrels", async check(ctx) { const indexFiles = ctx.scopedFiles.filter((f) => f.endsWith("/index.ts") ); const checks = indexFiles.map(async (file) => { const content = await ctx.readFile(file); if (isBarrelFile(content)) { ctx.report.violation({ message: `Barrel file detected: ${file} contains only re-exports and no logic.`, file, fix: "Delete this barrel file and update imports to point directly to the source module", }); } }); await Promise.all(checks); }, }, }, } satisfies RuleSet; ``` ## When to use it When you want to enforce direct imports and avoid the indirection that barrel files introduce. Especially valuable in large codebases where barrel files cause slow IDE performance and make dependency graphs harder to reason about. ## When not to use it When your project intentionally uses barrel files as a public API boundary (e.g., a library that exports a curated API from `index.ts`). --- ## Examples: no-emoji-in-output Source: https://cli.archgate.dev/examples/no-emoji-in-output/ Ban emoji characters in CLI output strings. ## Rule details Emoji render inconsistently across terminals, break alignment in monospace output, and cause issues with screen readers and CI log parsers. This rule uses Unicode range regex to detect emoji characters and a secondary check to ensure they appear inside string literals (not comments or variable names). The rule also checks for raw ANSI escape codes, enforcing the use of `styleText()` from `node:util` for terminal formatting. ## Examples of **incorrect** code ```typescript title="src/commands/check.ts" console.log("✅ All checks passed!"); console.log("❌ Validation failed"); console.log("\x1b[32mSuccess\x1b[0m"); // raw ANSI ``` ## Examples of **correct** code ```typescript title="src/commands/check.ts" import { styleText } from "node:util"; console.log("All checks passed"); console.log("Validation failed"); console.log(styleText("green", "Success")); ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> const EMOJI_PATTERN = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; const EMOJI_IN_STRING = /["'`].*[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}].*["'`]/u; export default { rules: { "no-emoji-in-output": { description: "CLI output must not contain emoji characters", async check(ctx) { const files = ctx.scopedFiles.filter( (f) => !f.includes("tests/") && !f.includes(".archgate/") ); const matches = await Promise.all( files.map((file) => ctx.grep(file, EMOJI_PATTERN)) ); for (const fileMatches of matches) { for (const m of fileMatches) { if (EMOJI_IN_STRING.test(m.content)) { ctx.report.violation({ message: "Do not use emoji in CLI output strings", file: m.file, line: m.line, fix: "Remove emoji from output strings", }); } } } }, }, "use-style-text": { description: "Use styleText from node:util instead of raw ANSI codes", async check(ctx) { const files = ctx.scopedFiles.filter( (f) => !f.includes("tests/") && !f.includes(".archgate/") ); const matches = await Promise.all( files.map((file) => ctx.grep(file, /\\u001b\[|\\x1b\[|\\033\[/)) ); for (const fileMatches of matches) { for (const m of fileMatches) { ctx.report.violation({ message: "Use styleText() from node:util instead of raw ANSI escape codes", file: m.file, line: m.line, fix: "Import { styleText } from 'node:util' and use styleText(style, text)", }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it In CLI tools where output consistency across terminals matters, or when accessibility is a priority. ## When not to use it In web applications or tools where emoji are part of the expected UI, or when your output is always consumed by humans in modern terminals. --- ## Examples: no-todo-comments Source: https://cli.archgate.dev/examples/no-todo-comments/ Flag `TODO`, `FIXME`, `HACK`, and `XXX` comments so they are resolved before merging. ## Rule details TODO comments are useful during development but should not accumulate in the main branch. This rule uses `ctx.grepFiles` to scan all source files for common task-marker comments. It uses `warning` severity so it does not block CI, but makes the comments visible in every check run. ## Examples of **incorrect** code ```typescript title="src/helpers/git.ts" // TODO: handle merge conflicts // FIXME: this breaks on Windows // HACK: workaround for upstream bug // XXX: revisit this logic ``` ## Examples of **correct** code ```typescript title="src/helpers/git.ts" // Proper implementation with no deferred work ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "no-todo-comments": { description: "TODO and FIXME comments should be resolved before merging", severity: "warning", async check(ctx) { const matches = await ctx.grepFiles( /\/\/\s*(TODO|FIXME|HACK|XXX):/i, "src/**/*.ts" ); for (const match of matches) { ctx.report.warning({ message: `${match.content.trim()} (resolve before merging)`, file: match.file, line: match.line, }); } }, }, }, } satisfies RuleSet; ``` ## When to use it When you want visibility into deferred work and want to prevent TODO comments from accumulating over time. Change severity to `"error"` and use `ctx.report.violation()` to make it a hard blocker. ## When not to use it When TODO comments are intentional documentation (e.g., tracked by a separate tool that creates issues from TODO comments). --- ## Examples: no-unapproved-deps Source: https://cli.archgate.dev/examples/no-unapproved-deps/ Restrict production dependencies to an approved allowlist. ## Rule details Large dependency trees increase supply-chain risk and bloat bundle sizes. This rule reads `package.json` and reports any production dependency that is not on an explicit allowlist. Dev dependencies are not checked. ## Examples of **incorrect** code ```json title="package.json" { "dependencies": { "zod": "^3.23.0", "chalk": "^5.3.0" } } ``` If only `zod` is on the approved list, `chalk` triggers a violation. ## Examples of **correct** code ```json title="package.json" { "dependencies": { "zod": "^3.23.0" }, "devDependencies": { "chalk": "^5.3.0" } } ``` All production dependencies are on the approved list. Libraries needed only at build time are in `devDependencies`. ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> const APPROVED_DEPS = [ "@commander-js/extra-typings", "inquirer", "@modelcontextprotocol/sdk", "zod", ]; export default { rules: { "no-unapproved-deps": { description: "Production dependencies must be on the approved list", async check(ctx) { let pkg: { dependencies?: Record<string, string> }; try { pkg = (await ctx.readJSON("package.json")) as typeof pkg; } catch { return; // No package.json, nothing to check } const deps = Object.keys(pkg.dependencies ?? {}); for (const dep of deps) { if (!APPROVED_DEPS.includes(dep)) { ctx.report.violation({ message: `Unapproved production dependency: "${dep}". Approved: ${APPROVED_DEPS.join(", ")}`, file: "package.json", fix: `Either add "${dep}" to the approved list in the ADR or move it to devDependencies`, }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your team has an explicit dependency governance policy and wants to prevent unapproved packages from entering the production bundle. ## When not to use it In early-stage projects where the dependency list is still evolving rapidly, or when dependency governance is handled by a separate tool like Socket or Snyk. --- ## Examples: openapi-routes Source: https://cli.archgate.dev/examples/openapi-routes/ Ensure all backend route files use OpenAPI-typed route definitions. ## Rule details When a backend framework supports OpenAPI integration (e.g., `@hono/zod-openapi`), raw HTTP method handlers (`.get()`, `.post()`) bypass schema validation and documentation generation. This rule checks that route files import the OpenAPI integration and use `.openapi()` instead of raw methods. ## Examples of **incorrect** code ```typescript title="packages/backend/src/routes/users.ts" import { Hono } from "hono"; const app = new Hono(); app.get("/users", async (c) => { // No OpenAPI schema, no auto-generated docs return c.json(await getUsers()); }); ``` ## Examples of **correct** code ```typescript title="packages/backend/src/routes/users.ts" import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; const route = createRoute({ method: "get", path: "/users", responses: { 200: { content: { "application/json": { schema: UserListSchema } }, description: "List of users", }, }, }); app.openapi(route, async (c) => { return c.json(await getUsers()); }); ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "openapi-route-completeness": { description: "All backend routes must use @hono/zod-openapi, not raw HTTP methods", async check(ctx) { const routeFiles = await ctx.glob("packages/backend/src/routes/*.ts"); for (const file of routeFiles) { if (file.includes(".test.") || file.includes(".spec.")) continue; const content = await ctx.readFile(file); const importsOpenApi = /from\s+["']@hono\/zod-openapi["']/.test( content ); const hasRawMethods = /\.(?:get|post|put|delete|patch)\s*\(/.test( content ); const hasOpenApiCalls = /\.openapi\s*\(/.test(content); if (!importsOpenApi && hasRawMethods) { ctx.report.violation({ message: `Route file uses raw HTTP methods without importing @hono/zod-openapi`, file, fix: "Import from @hono/zod-openapi and use .openapi() instead of raw .get()/.post()", }); } if (importsOpenApi && hasRawMethods && !hasOpenApiCalls) { ctx.report.violation({ message: `Route file imports @hono/zod-openapi but uses raw HTTP methods instead of .openapi()`, file, fix: "Replace raw .get()/.post() calls with .openapi() route definitions", }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your backend framework supports OpenAPI integration and you want to ensure all routes are documented and schema-validated. Adapt the import pattern and method detection for your framework: ```typescript // For Express with express-openapi-validator /from\s+["']express-openapi-validator["']/ // For Fastify with @fastify/swagger /from\s+["']@fastify\/swagger["']/ ``` ## When not to use it When not all routes require OpenAPI documentation (e.g., internal health checks, metrics endpoints), or when OpenAPI specs are maintained separately from route code. --- ## Examples: page-component-constraints Source: https://cli.archgate.dev/examples/page-component-constraints/ Enforce that page components are thin layout wrappers: small in size and free of data-fetching logic. ## Rule details In frontend architectures that separate routing from logic, page components should only compose layout and delegate data-fetching to Connected components. This rule enforces two constraints in a single rules file: 1. **Size limit**: Page components must stay under a configurable line count (default: 75 lines). 2. **No data hooks**: Page components must not use `useState`, `useQuery`, `useMutation`, or other data-fetching hooks directly. ## Examples of **incorrect** code ```tsx title="src/pages/DashboardPage.tsx" import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; export default function DashboardPage() { const [filter, setFilter] = useState(""); // ✗ state hook const { data } = useQuery({ queryKey: ["dashboard"] }); // ✗ data hook // ... 120 lines of layout + logic } ``` ## Examples of **correct** code ```tsx title="src/pages/DashboardPage.tsx" import { DashboardConnected } from "../components/DashboardConnected"; import { Sidebar } from "../components/Sidebar"; export default function DashboardPage() { return ( <div className="flex"> <Sidebar /> <DashboardConnected /> </div> ); } ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> const PAGE_MAX_LINES = 75; export default { rules: { "page-max-lines": { description: `Page components must be under ${PAGE_MAX_LINES} lines`, async check(ctx) { const pageFiles = await ctx.glob("src/pages/*Page.tsx"); for (const file of pageFiles) { if (file.includes(".test.")) continue; const content = await ctx.readFile(file); const lineCount = content.split("\n").length; if (lineCount > PAGE_MAX_LINES) { ctx.report.violation({ message: `Page component has ${lineCount} lines (max ${PAGE_MAX_LINES}). Extract logic to Connected components.`, file, fix: "Move data-fetching and business logic to Connected components", }); } } }, }, "page-no-data-hooks": { description: "Page components must not use data-fetching or state hooks", async check(ctx) { const pageFiles = await ctx.glob("src/pages/*Page.tsx"); const FORBIDDEN_HOOKS = /\b(useState|useForm|useQuery|useMutation|useSuspenseQuery|useInfiniteQuery)\s*[<(]/g; const ALLOWED_HOOKS = new Set([ "useParams", "useNavigate", "useRouter", "useMatch", "useLocation", "useSearch", ]); for (const file of pageFiles) { if (file.includes(".test.")) continue; const content = await ctx.readFile(file); let match; while ((match = FORBIDDEN_HOOKS.exec(content)) !== null) { ctx.report.violation({ message: `Page component uses "${match[1]}" hook. Extract to a Connected component.`, file, fix: `Move the "${match[1]}" hook to a Connected component`, }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your frontend architecture follows the page/container/presentational pattern and you want to enforce that pages remain thin routing endpoints. Adjust `PAGE_MAX_LINES` and the hook lists for your framework. ## When not to use it When pages are expected to contain logic (e.g., in Next.js server components where data fetching in the page is idiomatic), or when your project does not follow this architectural pattern. --- ## Examples: required-export-pattern Source: https://cli.archgate.dev/examples/required-export-pattern/ Verify that files in a specific directory export a required function signature. ## Rule details Convention-based architectures (CLI command files, route handlers, plugin modules) often require each file to export a function with a specific naming pattern. This rule scans scoped files for a required export and reports any file that does not match. It uses `Promise.all()` to check files in parallel for performance. ## Examples of **incorrect** code ```typescript title="src/commands/deploy.ts" // Missing the required register*Command export export function deploy() { // ... } ``` ## Examples of **correct** code ```typescript title="src/commands/deploy.ts" import type { Command } from "commander"; export function registerDeployCommand(program: Command) { program .command("deploy") .description("Deploy the application") .action(() => { // ... }); } ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "register-function-export": { description: "Command files must export a register*Command function", async check(ctx) { const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts")); const checks = files.map(async (file) => { const content = await ctx.readFile(file); if (!/export\s+function\s+register\w+Command/.test(content)) { ctx.report.violation({ message: "Command file must export a register*Command function", file, }); } }); await Promise.all(checks); }, }, }, } satisfies RuleSet; ``` Adapt the pattern for other conventions: ```typescript // Express/Hono route handlers /export\s+default\s+.*Router/ // React page components /export\s+default\s+function\s+\w+Page/ // Plugin modules /export\s+const\s+plugin\s*[:=]/ ``` ## When to use it When your architecture requires a specific export pattern from files in a directory. Pair this with the ADR's `files` frontmatter field to scope it to the right directory. ## When not to use it When the directory contains mixed file types that do not all need to follow the same export pattern. --- ## Examples: spdx-license-headers Source: https://cli.archgate.dev/examples/spdx-license-headers/ Ensure every source file declares its license with a machine-readable SPDX identifier. ## Rule details Open-source projects benefit from per-file license declarations. They survive file extraction, bundling, and copy-paste scenarios where the root LICENSE file is not present. This rule verifies that every TypeScript source file starts with the standard SPDX header comment. ## Examples of **incorrect** code ```typescript title="src/helpers/utils.ts" import { join } from "node:path"; export function resolvePath(base: string, rel: string): string { return join(base, rel); } ``` File is missing the SPDX license identifier header. ## Examples of **correct** code ```typescript title="src/helpers/utils.ts" // SPDX-License-Identifier: Apache-2.0 // Copyright 2026 Archgate import { join } from "node:path"; export function resolvePath(base: string, rel: string): string { return join(base, rel); } ``` For files with a shebang: ```typescript title="src/cli.ts" #!/usr/bin/env bun // SPDX-License-Identifier: Apache-2.0 // Copyright 2026 Archgate import { Command } from "commander"; ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "spdx-header-present": { description: "All TypeScript source files must have an SPDX-License-Identifier header", async check(ctx) { const results = await Promise.all( ctx.scopedFiles.map(async (file) => { const content = await ctx.readFile(file); return { file, content }; }) ); for (const { file, content } of results) { // Check first 5 lines for the SPDX identifier (allows for shebang) const lines = content.split("\n").slice(0, 5); const hasSpdx = lines.some((line) => line.includes("SPDX-License-Identifier: Apache-2.0") ); if (!hasSpdx) { ctx.report.violation({ message: "Missing SPDX-License-Identifier header.", file, line: 1, fix: 'Add "// SPDX-License-Identifier: Apache-2.0" as the first line of the file', }); } } }, }, }, } satisfies RuleSet; ``` ## Customization - **Change the license**: Replace `Apache-2.0` with your project's SPDX identifier (e.g., `MIT`, `BSD-3-Clause`) - **Change the scope**: Adjust the `files` glob in the ADR frontmatter to match your source directories - **Add copyright check**: Extend the rule to also verify the copyright line format ## When to use it When your project is open-source and you want unambiguous per-file license declarations that are recognized by compliance scanners (FOSSA, Snyk, Black Duck, npm license-checker). ## When not to use it In proprietary/closed-source projects where all files are implicitly "all rights reserved," or when your organization uses a different license-declaration mechanism like REUSE 3.0 `.dep5` files. --- ## Examples: test-file-coverage Source: https://cli.archgate.dev/examples/test-file-coverage/ Verify that every source file has a corresponding test file. ## Rule details Untested code is a liability. This rule enforces a structural convention: for every file in `src/`, a matching `.test.ts` file must exist in `tests/`. It uses `ctx.glob` to discover existing test files, builds a lookup set for fast matching, and reports any source file without a counterpart. ## Examples of **incorrect** code ``` src/ helpers/ log.ts ← has a test ✓ paths.ts ← no test file ✗ tests/ helpers/ log.test.ts ``` ## Examples of **correct** code ``` src/ helpers/ log.ts paths.ts tests/ helpers/ log.test.ts paths.test.ts ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> import { relative } from "node:path"; export default { rules: { "test-file-exists": { description: "Every source file should have a corresponding test file", severity: "warning", async check(ctx) { for (const file of ctx.scopedFiles) { const rel = relative(ctx.projectRoot, file); const testPath = rel .replace(/^src\//, "tests/") .replace(/\.ts$/, ".test.ts"); const testFiles = await ctx.glob(testPath); if (testFiles.length === 0) { ctx.report.warning({ message: `No test file found at ${testPath}`, file, fix: `Create a test file at ${testPath}`, }); } } }, }, }, } satisfies RuleSet; ``` To adapt for projects that colocate tests next to source files: ```typescript const testPath = rel.replace(/\.ts$/, ".test.ts"); // src/helpers/log.ts → src/helpers/log.test.ts ``` ## When to use it When your team follows a convention that every source module must have a corresponding test file, and you want to catch missing tests during code review. ## When not to use it When test coverage is tracked by other means (e.g., coverage thresholds in CI), or when some source files genuinely do not need tests (type-only files, constants). --- ## Examples: version-catalog Source: https://cli.archgate.dev/examples/version-catalog/ Enforce centralized version management in monorepos by requiring `catalog:` or `workspace:` notation for all dependencies. ## Rule details In monorepos with many packages, duplicating version strings across `package.json` files leads to version drift and upgrade pain. This rule ensures every dependency reference uses `catalog:` (resolved from a central catalog in the root `package.json`) or `workspace:` (for internal packages), never a raw semver string. The rule has two checks: `catalog-usage` verifies all dependencies use the correct notation, and `catalog-completeness` verifies that every `catalog:` reference resolves to an actual entry in the root catalog. ## Examples of **incorrect** code ```json title="packages/api/package.json" { "dependencies": { "zod": "^3.23.0", "hono": "^4.0.0" } } ``` Raw semver strings bypass the central catalog and will diverge across packages. ## Examples of **correct** code ```json title="packages/api/package.json" { "dependencies": { "zod": "catalog:", "hono": "catalog:", "@myorg/shared": "workspace:*" } } ``` ```json title="package.json (root)" { "catalog": { "zod": "^3.23.0", "hono": "^4.0.0" } } ``` All versions are managed centrally. Upgrading `zod` requires changing only the root catalog. ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "catalog-usage": { description: 'All workspace dependencies must use "catalog:" or "workspace:" notation', async check(ctx) { const packageJsonFiles = [ ...(await ctx.glob("packages/*/package.json")), ...(await ctx.glob("packages/*/*/package.json")), ]; for (const file of packageJsonFiles) { const pkg = (await ctx.readJSON(file)) as Record<string, unknown>; for (const depType of [ "dependencies", "devDependencies", "peerDependencies", ]) { const deps = pkg[depType] as Record<string, string> | undefined; if (!deps) continue; for (const [name, version] of Object.entries(deps)) { if ( typeof version === "string" && !version.startsWith("catalog:") && !version.startsWith("workspace:") ) { ctx.report.violation({ message: `${file}: ${depType}.${name} uses "${version}" instead of "catalog:" or "workspace:"`, file, fix: `Change to "catalog:" and ensure the package is listed in root package.json catalog`, }); } } } } }, }, "catalog-completeness": { description: "All catalog: references must resolve to entries in root package.json catalog", async check(ctx) { const rootPkg = (await ctx.readJSON("package.json")) as Record< string, unknown >; const catalog = (rootPkg.catalog ?? {}) as Record<string, string>; const catalogKeys = new Set(Object.keys(catalog)); const packageJsonFiles = [ ...(await ctx.glob("packages/*/package.json")), ...(await ctx.glob("packages/*/*/package.json")), ]; for (const file of packageJsonFiles) { const pkg = (await ctx.readJSON(file)) as Record<string, unknown>; for (const depType of [ "dependencies", "devDependencies", "peerDependencies", ]) { const deps = pkg[depType] as Record<string, string> | undefined; if (!deps) continue; for (const [name, version] of Object.entries(deps)) { if ( typeof version !== "string" || !version.startsWith("catalog:") ) continue; const catalogRef = version === "catalog:" ? name : version.slice("catalog:".length); if (!catalogKeys.has(catalogRef)) { ctx.report.violation({ message: `${file}: ${depType}.${name} references catalog:${catalogRef} but it is not in root catalog`, file, fix: `Add "${catalogRef}" to the catalog section in the root package.json`, }); } } } } }, }, }, } satisfies RuleSet; ``` ## When to use it In monorepos where multiple packages share dependencies and you want a single source of truth for versions (e.g., Bun workspaces with catalog support, or similar setups). ## When not to use it In single-package repositories or monorepos that use a different version management strategy like Renovate's group updates. --- ## Examples: wrapper-enforcement Source: https://cli.archgate.dev/examples/wrapper-enforcement/ Enforce use of a project wrapper instead of a raw platform API. ## Rule details When a project provides a helper that normalizes a low-level API (e.g., a `platform.ts` helper wrapping `process.platform`), direct usage of the raw API should be banned everywhere except in the wrapper itself. This rule scans scoped files for the raw API call while automatically excluding the helper file and non-production code. ## Examples of **incorrect** code ```typescript title="src/commands/build.ts" if (process.platform === "win32") { // Windows-specific logic } ``` ## Examples of **correct** code ```typescript title="src/commands/build.ts" import { isWindows } from "../helpers/platform"; if (isWindows()) { // Windows-specific logic } ``` ## Rule implementation ```typescript /// <reference path="../rules.d.ts" /> export default { rules: { "no-direct-process-platform": { description: "Platform detection must use src/helpers/platform.ts, not process.platform directly", async check(ctx) { const files = ctx.scopedFiles.filter( (f) => !f.includes("tests/") && !f.includes(".archgate/") && !f.endsWith("src/helpers/platform.ts") // Exclude the wrapper itself ); const matches = await Promise.all( files.map((file) => ctx.grep(file, /process\.platform/)) ); for (const fileMatches of matches) { for (const m of fileMatches) { ctx.report.violation({ message: "Do not access process.platform directly. Use isWindows(), isMacOS(), isLinux(), or getPlatformInfo() from src/helpers/platform.ts instead.", file: m.file, line: m.line, fix: 'Import { isWindows } from "../helpers/platform" and use it instead of process.platform', }); } } }, }, }, } satisfies RuleSet; ``` ## When to use it When your project has a wrapper or helper module that normalizes a raw API and you want to prevent direct access to the underlying API. Common examples: - `platform.ts` wrapping `process.platform` - `logger.ts` wrapping `console.log` / `console.error` - `fs.ts` wrapping `node:fs` with project-specific defaults - `env.ts` wrapping `process.env` with typed accessors ## When not to use it When the raw API is simple enough that a wrapper adds no value, or when the wrapper has not yet been adopted across the codebase (consider using `warning` severity during migration). ---