Skip to content
31 changes: 29 additions & 2 deletions docs/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3917,6 +3917,14 @@ Archgate plugin installed for Claude Code.

When `--editor cursor` is used, the output shows `.cursor/` instead of `.claude/`.

## 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

```
Expand Down Expand Up @@ -4424,14 +4432,15 @@ 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` (when custom domains are registered) or when you manually add configuration. It lives inside the `.archgate/` directory at your project root.
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" }
"paths": { "adrs": "docs/adrs", "rules": "docs/adrs" },
"baseBranch": "main"
}
```

Expand All @@ -4445,6 +4454,24 @@ Custom domain-to-prefix mappings. See [Custom Domains](/concepts/domains/#custom

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.

### `paths`

Override default directories for ADRs and rules.
Expand Down
8 changes: 8 additions & 0 deletions docs/src/content/docs/pt-br/reference/cli/init.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ Archgate plugin installed for Claude Code.

Quando `--editor cursor` é usado, a saída mostra `.cursor/` em vez de `.claude/`.

## Detecção de branch base

Quando executado dentro de um repositório git, `archgate init` detecta automaticamente o branch base e o salva em `.archgate/config.json` como o campo `baseBranch`. Isso permite que `archgate check` pule a detecção de branch a cada execução, economizando 1-4 chamadas de subprocesso git.

A detecção tenta `origin/HEAD`, `origin/main`, `origin/master`, `main` local e `master` local (primeiro encontrado vence). Se nenhum for encontrado (ex: não é um repositório git), nenhum `baseBranch` é gravado.

Re-executar `archgate init` **não** sobrescreve um `baseBranch` configurado manualmente. Veja [Configuração -- `baseBranch`](/reference/configuration/#basebranch) para detalhes.

## Estrutura gerada

```
Expand Down
23 changes: 21 additions & 2 deletions docs/src/content/docs/pt-br/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ description: Referência do arquivo de configuração .archgate/config.json. Con

O arquivo `.archgate/config.json` armazena configurações do projeto que são versionadas no controle de versão e compartilhadas com toda a equipe.

Este arquivo é criado automaticamente pelo `archgate init` (quando domínios customizados são registrados) ou quando você adiciona configurações manualmente. Ele fica dentro do diretório `.archgate/` na raiz do seu projeto.
Este arquivo é criado automaticamente pelo `archgate init` (para armazenar o branch base auto-detectado e quaisquer domínios customizados) ou quando você adiciona configurações manualmente. Ele fica dentro do diretório `.archgate/` na raiz do seu projeto.

## Schema

```json
{
"domains": { "security": "SEC", "compliance": "COMP" },
"paths": { "adrs": "docs/adrs", "rules": "docs/adrs" }
"paths": { "adrs": "docs/adrs", "rules": "docs/adrs" },
"baseBranch": "main"
}
```

Expand All @@ -26,6 +27,24 @@ Mapeamentos personalizados de domínio para prefixo. Veja [Domínios Personaliza

Esses são mesclados com os domínios built-in (`backend`, `frontend`, `data`, `architecture`, `general`) em tempo de leitura. Entradas personalizadas não podem sobrescrever nomes ou prefixos built-in.

### `baseBranch`

Branch base para detecção de mudanças no `archgate check`. Quando definido, `archgate check` pula as probes de auto-detecção e usa este valor diretamente para popular `ctx.changedFiles` via `git diff <baseBranch>...HEAD`.

| Tipo | Padrão | Descrição |
| -------- | --------------- | -------------------------------------------------------- |
| `string` | _(auto-detect)_ | Nome do branch ou ref remoto (ex: `main`, `origin/main`) |

Este campo é **preenchido automaticamente** pelo `archgate init` quando um repositório git é detectado. A auto-detecção tenta `origin/HEAD`, `origin/main`, `origin/master`, `main` local e `master` local (primeiro encontrado vence). Re-executar `archgate init` não sobrescreve um valor configurado manualmente.

Você também pode defini-lo manualmente:

```json
{ "baseBranch": "main" }
```

Veja [`archgate check` -- Detecção de arquivos alterados](/reference/cli/check/#changed-files-detection) para a prioridade completa de resolução.

### `paths`

Sobrescreve os diretórios padrão para ADRs e regras.
Expand Down
8 changes: 8 additions & 0 deletions docs/src/content/docs/reference/cli/init.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ Archgate plugin installed for Claude Code.

When `--editor cursor` is used, the output shows `.cursor/` instead of `.claude/`.

## 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

```
Expand Down
23 changes: 21 additions & 2 deletions docs/src/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ description: Reference for the .archgate/config.json project configuration file.

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` (when custom domains are registered) or when you manually add configuration. It lives inside the `.archgate/` directory at your project root.
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" }
"paths": { "adrs": "docs/adrs", "rules": "docs/adrs" },
"baseBranch": "main"
}
```

Expand All @@ -26,6 +27,24 @@ Custom domain-to-prefix mappings. See [Custom Domains](/concepts/domains/#custom

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.

### `paths`

Override default directories for ADRs and rules.
Expand Down
13 changes: 7 additions & 6 deletions src/engine/git-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/** Git file-listing utilities for ADR scope resolution and change detection. */

import { logDebug, logWarn } from "../helpers/log";
import { ensureBaseBranch } from "../helpers/project-config";

/** Warn when an ADR's resolved file scope exceeds this many files. */
export const SCOPE_FILE_WARN_THRESHOLD = 1000;
Expand Down Expand Up @@ -151,11 +152,10 @@ export async function getStagedFiles(projectRoot: string): Promise<string[]> {
/** Get all changed files (staged + unstaged). */
export async function getChangedFiles(projectRoot: string): Promise<string[]> {
try {
const staged = await runGit(
["diff", "--cached", "--name-only"],
projectRoot
);
const unstaged = await runGit(["diff", "--name-only"], projectRoot);
const [staged, unstaged] = await Promise.all([
runGit(["diff", "--cached", "--name-only"], projectRoot),
runGit(["diff", "--name-only"], projectRoot),
]);
const all = new Set([
...staged.trim().split("\n").filter(Boolean),
...unstaged.trim().split("\n").filter(Boolean),
Expand Down Expand Up @@ -250,7 +250,8 @@ export async function resolveBaseRef(
return options.configBase;
}

return (await detectBaseRef(projectRoot)) ?? undefined;
// Lazy-save: detect + persist to config.json so future runs skip detection.
return (await ensureBaseBranch(projectRoot, detectBaseRef)) ?? undefined;
}

/**
Expand Down
20 changes: 14 additions & 6 deletions src/engine/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,16 @@ export async function runChecks(
options: { staged?: boolean; files?: string[]; base?: string } = {}
): Promise<CheckResult> {
const startTime = performance.now();
const changedFiles = options.staged
? await getStagedFiles(projectRoot)

// Start git I/O concurrently — changedFiles and trackedFiles are independent
const changedFilesPromise = options.staged
? getStagedFiles(projectRoot)
: options.base
? await getFilesChangedSinceRef(projectRoot, options.base)
: [];
? getFilesChangedSinceRef(projectRoot, options.base)
: Promise.resolve([]);
const allTrackedFilesPromise = getGitTrackedFiles(projectRoot);

// Do synchronous work while git subprocesses run
const results: RuleResult[] = loadResults
.filter((lr) => lr.type === "blocked")
.map((lr) => blockedToRuleResult(projectRoot, lr.value));
Expand All @@ -224,8 +229,11 @@ export async function runChecks(
);
}

// Resolve tracked files once (cached per-process) for gitignore filtering
const allTrackedFiles = await getGitTrackedFiles(projectRoot);
// Await both git operations (started above, run concurrently)
const [changedFiles, allTrackedFiles] = await Promise.all([
changedFilesPromise,
allTrackedFilesPromise,
]);

// Run ADRs in parallel
const adrResults = await Promise.allSettled(
Expand Down
6 changes: 6 additions & 0 deletions src/helpers/init-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { existsSync, readdirSync } from "node:fs";
import { basename, join } from "node:path";

import { detectBaseRef } from "../engine/git-files";
import { generateExampleAdr } from "./adr-templates";
import { configureClaudeSettings } from "./claude-settings";
import { configureCopilotSettings } from "./copilot-settings";
Expand All @@ -13,6 +14,7 @@ import {
opencodeAgentsDir,
projectPaths,
} from "./paths";
import { ensureBaseBranch } from "./project-config";
import { writeRulesShim } from "./rules-shim";
import { configureVscodeSettings } from "./vscode-settings";

Expand Down Expand Up @@ -122,6 +124,10 @@ Archgate standardizes \`.archgate/lint/\` as the location for linter rules that
const editor = options?.editor ?? "claude";
const editorSettingsPath = await configureEditorSettings(projectRoot, editor);

// Auto-detect base branch and save to config.json when not already configured.
// Runs after directory creation so .archgate/ exists for saveProjectConfig.
await ensureBaseBranch(projectRoot, detectBaseRef);

// Plugin installation (optional — requires stored credentials)
let plugin: PluginResult | undefined;
if (options?.installPlugin) {
Expand Down
27 changes: 27 additions & 0 deletions src/helpers/project-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,33 @@ export function getConfiguredBaseBranch(projectRoot: string): string | null {
return config.baseBranch ?? null;
}

/**
* Detect the base branch and save it to `.archgate/config.json` when not
* already configured. Idempotent — skips if `baseBranch` is already set.
* Non-fatal — silently logs on failure (not a git repo, read-only fs, etc.).
*
* Used by both `archgate init` (eager) and `resolveBaseRef` (lazy on first check).
*/
export async function ensureBaseBranch(
projectRoot: string,
detectBaseRef: (root: string) => Promise<string | null>
): Promise<string | null> {
const config = loadProjectConfig(projectRoot);
if (config.baseBranch) return config.baseBranch;

try {
const detected = await detectBaseRef(projectRoot);
if (detected) {
await saveProjectConfig(projectRoot, { ...config, baseBranch: detected });
logDebug("Saved detected base branch to config:", detected);
}
return detected;
} catch {
logDebug("Base branch detection failed (not a git repo?)");
return null;
}
}

export function isDefaultDomain(domain: string): boolean {
return (DEFAULT_DOMAINS as readonly string[]).includes(domain);
}
Expand Down
5 changes: 3 additions & 2 deletions tests/engine/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

Expand All @@ -12,6 +12,7 @@ import {
buildReviewContext,
} from "../../src/engine/context";
import type { AdrDocument, AdrDomain } from "../../src/formats/adr";
import { safeRmSync } from "../test-utils";

function makeAdr(
overrides: Partial<AdrDocument["frontmatter"]> = {},
Expand Down Expand Up @@ -248,7 +249,7 @@ describe("buildReviewContext", () => {
});

afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
safeRmSync(tempDir);
});

function writeAdr(
Expand Down
Loading
Loading