Archgate is a CLI tool for AI governance via Architecture Decision Records (ADRs) — combining human-readable docs with machine-checkable rules. The CLI dogfoods itself via ADRs in .archgate/adrs/. AI features are delivered as a Claude Code plugin (../plugins/claude-code), not via direct API calls.
- Runtime: Bun — not Node.js compatible (minimum version enforced in
src/cli.ts) - Language: TypeScript (strict mode, ESNext, ES modules)
- CLI framework: Commander.js (
@commander-js/extra-typings) - Linter: Oxlint | Formatter: Oxfmt | Dead exports: Knip | Commits: Conventional Commits
bun run src/cli.ts <command> # run CLI locally
bun run lint # oxlint
bun run typecheck # tsc --build
bun run format # oxfmt --write
bun run format:check # oxfmt --check
bun run test # all tests (not bare `bun test` — picks up --timeout; see GEN-003)
bun run knip # dead export detection
bun run validate # MANDATORY: lint + typecheck + format:check + test + ADR check + knip + build check
bun run build:check # verify build compiles (CI builds binaries via release workflow)
bun run commit # conventional commit wizardbun run validate must pass before any task is considered complete. Fail-fast pipeline: lint → typecheck → format:check → test → ADR check → knip → build check. Mirrors CI in .github/workflows/code-pull-request.yml.
Config-based hooks in .githooks run validation locally before commits and pushes:
- pre-commit: lint + typecheck + format:check (~15s)
- pre-push: full
bun run validate(~60s, mirrors CI)
Activate once per clone:
git config --local include.path ../.githooksOpt out of a specific hook: git config --local hook.<name>.enabled false. Skip all hooks for a single commit: git commit --no-verify.
Claude Code sessions in this repo keep a small persistent memory at .claude/agent-memory/archgate-developer/ (index: MEMORY.md).
It is deliberately small. Governance lives in ADRs, companion .rules.ts, the oxlint plugins in lint/, and the test suite — all re-validated on every bun run validate. Memory holds only what no check can reach: Claude Code harness behaviour, GitHub state, external-tool quirks, and the agent's own working process. Anything a rule could enforce belongs in the rule, which stays true; a memory entry is validated by nobody and rots silently.
Before adding an entry, ask which layer should carry it instead: a static syntax invariant belongs in a custom oxlint rule, executable behaviour in a test, cross-file governance in an ADR companion .rules.ts, and behaviour every archgate user shares in a built-in CheckResult diagnostic. Only when none of those can reach it does it belong here. Nothing here is required reading for contributors or tools without access to it.
Entry point: src/cli.ts (shebang #!/usr/bin/env bun). Commands registered via register*Command(program). See src/commands/ for all command implementations — each file exports a register*Command(program) function.
~/.archgate/— CLI cache;~/.archgate/bin/— binary install location.archgate/adrs/— ADRs;.archgate/lint/— linter rulessrc/formats/— Zod schemas + types (adr.ts,rules.ts)src/helpers/— utilities (paths, log, git, templates, adr-writer, etc.)tests/— mirrorssrc/; fixtures intests/fixtures/
Zod schemas are the single source of truth. Types derived via z.infer<> — never define separate interfaces. Use safeParse(). Reuse AdrFrontmatterSchema.shape.* to avoid duplicating enums.
Archgate ships as a compiled binary (bun build --compile) published to GitHub Releases, plus thin shim packages for each supported ecosystem — npm (shims/npm/archgate.cjs), PyPI, RubyGems, Maven, NuGet, and Go (all under shims/). A shim bundles no binary: on first run it detects the platform, downloads the matching binary from GitHub Releases, verifies its SHA256 checksum, caches it to ~/.archgate/bin/, and forwards to it (see ARCH-017). Runtime dependencies (commander, inquirer, zod) are bundled into the compiled binary, so they belong in devDependencies, not dependencies.
- Commands export
register*Command(program), handle I/O only — no business logic - OS: macOS, Linux, and Windows
- Output:
styleText()fromnode:util;--jsonfor machine-readable; auto-compact JSON in agent contexts (non-TTY, non-CI); no emoji - Exit codes: 0 = success, 1 = violation, 2 = internal error, 130 = user cancellation (SIGINT)
- Deps: minimal; prefer Bun built-ins (see ARCH-006)
See .prototools for pinned tool versions. Minimum user-facing Bun version is enforced in src/cli.ts.
The CLI dogfoods itself — see .archgate/adrs/ for the full list of ADRs and their companion .rules.ts files. Read them before making architectural changes.
YAML frontmatter (id, title, domain, rules, optional files). Sections: Context, Decision, Do's and Don'ts, Consequences, Compliance, References. Companion .rules.ts exports a plain object satisfies RuleSet.
Editor integrations share the EditorTarget union. Adding a new editor requires coordinated edits — missing any one breaks detection, init, or tests:
src/helpers/init-project.ts— extendEditorTargetunion,EDITOR_LABELS, theconfigureEditorSettingsswitch, and (when authenticated install applies) thetryInstallPluginbranchsrc/helpers/plugin-install.ts— addis<Editor>CliAvailable()and any install/download helper. For tarball-based editors (no marketplace CLI), useinstallEditorPluginBundle()— it handles directory creation, old-file cleanup, and tarball extraction in one call. If the editor also ships a GUI/Desktop distribution with no CLI binary at all, add a broaderis<Editor>Available()that OR's in a shared-state fallback (e.g., the editor's user-scope config directory already existing) — see the opencode Desktop note belowsrc/helpers/editor-detect.ts— append to thePromise.alland the returned arraysrc/commands/init.ts— extendEDITOR_DIRS,SIGNUP_EDITORS, the--editor.choices([...] as const), andprintManualInstructionssrc/commands/plugin/install.ts— extend.choices([...] as const)and add a case toinstallForEditor+ the manual-instructionscatchsrc/commands/plugin/url.ts— extend.choices([...] as const)and branch before the URL ternary- Tests that assert the exact choice list:
tests/commands/plugin/install.test.ts,tests/commands/plugin/url.test.ts, andtests/helpers/editor-detect.test.ts(length + id order)
User-scope editors (e.g., opencode) write to a path resolved in paths.ts rather than the project tree — configureEditorSettings returns that path for the init summary and the real work happens in tryInstallPlugin.
Match the target editor's actual path resolution — don't assume Windows conventions. opencode uses xdg-basedir, which falls back to ~/.config on all platforms (Windows: C:\Users\<user>\.config\…, not %APPDATA%\…). opencodeAgentsDir() must mirror that exactly. Verify the editor's own path helper before writing a resolver for a new user-scope editor.
opencode ships two distributions — CLI detection alone misses the Desktop app. The Electron-based Desktop app (@opencode-aidesktop on Windows) ships no CLI binary, so isOpencodeCliAvailable() (PATH check) can't detect it. Both distributions share opencodeConfigDir() (~/.config/opencode/), so isOpencodeAvailable() in plugin-install.ts also treats that directory's existence as installed. All three call sites (editor-detect.ts, init-project.ts, commands/plugin/install.ts) use the broader isOpencodeAvailable() — use it for any new opencode-gated behavior too.