diff --git a/.archgate/adrs/LEGAL-001-spdx-license-headers.md b/.archgate/adrs/LEGAL-001-spdx-license-headers.md
new file mode 100644
index 00000000..ffa2cebe
--- /dev/null
+++ b/.archgate/adrs/LEGAL-001-spdx-license-headers.md
@@ -0,0 +1,89 @@
+---
+id: LEGAL-001
+title: SPDX License Headers
+domain: legal
+rules: true
+files: ["src/**/*.ts", "tests/**/*.ts"]
+---
+
+## Context
+
+The Archgate CLI is licensed under Apache-2.0. SPDX (Software Package Data Exchange) license identifiers provide a machine-readable, unambiguous way to declare the license of each source file. This eliminates ambiguity about which license applies to any given file and enables automated compliance tooling to verify license declarations at scale.
+
+Without per-file license identifiers, downstream consumers (enterprises, redistributors, compliance scanners) must infer the license from the root LICENSE.md file — a fragile assumption that breaks when files are extracted, copied, or bundled outside the original repository context.
+
+**Alternatives considered:**
+
+- **Root LICENSE.md only** — Covers the project as a whole but provides no per-file signal. Files extracted in isolation (e.g., copy-pasted utilities, bundled snippets) lose their license provenance.
+- **Full license header blocks** — Includes the entire license notice in every file. Verbose, creates noise, and adds maintenance burden if the copyright year or holder changes.
+- **REUSE 3.0 specification** — A comprehensive approach using `.reuse/dep5` files for bulk declarations. More complex than needed for a single-license project where all files share the same license.
+
+SPDX-License-Identifier comments are the lightest-weight solution that provides per-file legal clarity, is recognized by all major compliance scanners (FOSSA, Snyk, Black Duck, npm license-checker), and is trivial to maintain.
+
+## Decision
+
+Every TypeScript source file in `src/` and `tests/` must begin with an SPDX license identifier and copyright notice:
+
+```typescript
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
+```
+
+For files with a shebang line (`#!/usr/bin/env bun`), the SPDX header must appear immediately after the shebang:
+
+```typescript
+#!/usr/bin/env bun
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
+```
+
+## Do's and Don'ts
+
+### Do
+
+- Add the SPDX header as the first two lines of every new `.ts` file in `src/` or `tests/`
+- Place the header after the shebang line when one is present (only `src/cli.ts`)
+- Use the exact format: `// SPDX-License-Identifier: Apache-2.0` followed by `// Copyright 2026 Archgate`
+- Run `bun run scripts/add-spdx-headers.ts` to bulk-add headers to new files
+
+### Don't
+
+- Don't use block comments (`/* */`) for the SPDX header — scanners expect `//` single-line format
+- Don't add SPDX headers to non-TypeScript files (JSON, YAML, Markdown) — they are covered by the root LICENSE.md
+- Don't modify the copyright year without a project-wide decision
+- Don't add additional license identifiers (e.g., dual licensing) without updating this ADR
+
+## Consequences
+
+### Positive
+
+- **Unambiguous per-file licensing** — Every source file declares its license in a machine-readable format
+- **Compliance scanner compatibility** — FOSSA, Snyk, and npm license-checker recognize SPDX identifiers automatically
+- **Downstream safety** — Files extracted or bundled independently retain their license provenance
+- **Apache-2.0 Section 4(a) alignment** — "You must give any other recipients of the Work a copy of this License" — per-file identifiers serve as a lightweight form of notice
+
+### Negative
+
+- **Two extra lines per file** — Minor visual noise; mitigated by being at the very top (above imports)
+- **Year maintenance** — If copyright year changes, all files need updating (mitigated by the bulk script)
+
+### Risks
+
+- **Developers forget on new files** — The `LEGAL-001/spdx-header-present` rule catches this automatically in CI.
+ - **Mitigation:** Automated rule blocks PRs that add `.ts` files without the header.
+
+## Compliance and Enforcement
+
+### Automated Enforcement
+
+- **Archgate rule** `LEGAL-001/spdx-header-present`: Scans all scoped `.ts` files and verifies the SPDX identifier is present in the first 3 lines. Severity: `error` (hard blocker).
+
+### Manual Enforcement
+
+None required — fully automated.
+
+## References
+
+- [SPDX License List](https://spdx.org/licenses/)
+- [REUSE Software recommendations](https://reuse.software/spec/)
+- [Apache-2.0 License, Section 4](https://www.apache.org/licenses/LICENSE-2.0#redistribution)
diff --git a/.archgate/adrs/LEGAL-001-spdx-license-headers.rules.ts b/.archgate/adrs/LEGAL-001-spdx-license-headers.rules.ts
new file mode 100644
index 00000000..db3acf0b
--- /dev/null
+++ b/.archgate/adrs/LEGAL-001-spdx-license-headers.rules.ts
@@ -0,0 +1,38 @@
+///
+
+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) {
+ if (content === null) continue;
+
+ // 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. Add `// SPDX-License-Identifier: Apache-2.0` as the first line.",
+ file,
+ line: 1,
+ fix: 'Add "// SPDX-License-Identifier: Apache-2.0\\n// Copyright 2026 Archgate\\n" at the top of the file',
+ });
+ }
+ }
+ },
+ },
+ },
+} satisfies RuleSet;
diff --git a/.archgate/adrs/LEGAL-002-dependency-license-compatibility.md b/.archgate/adrs/LEGAL-002-dependency-license-compatibility.md
new file mode 100644
index 00000000..1930b7f9
--- /dev/null
+++ b/.archgate/adrs/LEGAL-002-dependency-license-compatibility.md
@@ -0,0 +1,95 @@
+---
+id: LEGAL-002
+title: Dependency License Compatibility
+domain: legal
+rules: true
+files: ["package.json"]
+---
+
+## Context
+
+The Archgate CLI is licensed under Apache-2.0, a permissive open-source license. When the CLI is compiled into a binary via `bun build --compile`, all runtime dependencies are bundled into the executable. This means the binary is a combined work that must comply with the license terms of every bundled dependency.
+
+Copyleft licenses (GPL, AGPL, LGPL) impose "share-alike" requirements that could force the entire CLI to be distributed under copyleft terms — incompatible with the project's Apache-2.0 licensing. Even for devDependencies (which are not bundled), copyleft test frameworks or build tools could create legal ambiguity about the project's overall license posture.
+
+**Alternatives considered:**
+
+- **No automated checking** — Rely on manual review during dependency additions. Error-prone; a single copyleft transitive dependency could slip in unnoticed.
+- **FOSSA or Snyk integration** — Third-party SaaS license scanners. Adds external dependency, cost, and requires API tokens in CI. Overkill for a project with only 3 production dependencies.
+- **npm license-checker package** — Adds a devDependency for something achievable with a simple script. Counter to ARCH-006 (minimize dependencies).
+
+A lightweight, self-contained script that reads `node_modules/*/package.json` license fields provides the same coverage without external dependencies or API tokens.
+
+## Decision
+
+All dependencies (production and development) must use licenses compatible with Apache-2.0. The project maintains an allowlist of approved permissive licenses:
+
+| License | SPDX Identifier |
+| ---------------------------- | -------------------- |
+| MIT License | MIT |
+| Apache License 2.0 | Apache-2.0 |
+| ISC License | ISC |
+| BSD 2-Clause | BSD-2-Clause |
+| BSD 3-Clause | BSD-3-Clause |
+| Zero-Clause BSD | 0BSD |
+| Creative Commons Zero | CC0-1.0 |
+| The Unlicense | Unlicense |
+| Blue Oak Model License | BlueOak-1.0.0 |
+| Creative Commons Attribution | CC-BY-4.0, CC-BY-3.0 |
+| Python Software Foundation | Python-2.0 |
+
+SPDX OR expressions (e.g., `MIT OR Apache-2.0`) are allowed if at least one alternative is on the allowlist.
+
+**Prohibited licenses include:** GPL-2.0, GPL-3.0, AGPL-3.0, LGPL-2.1, LGPL-3.0, SSPL-1.0, and any other copyleft or source-available license.
+
+## Do's and Don'ts
+
+### Do
+
+- Run `archgate check` (or `bun run validate`) before adding any new dependency
+- Verify transitive dependencies — a permissively-licensed package may pull in a copyleft transitive
+- Add newly-encountered permissive licenses to the allowlist in the LEGAL-002 `.rules.ts` file
+- Prefer dependencies with clear SPDX license identifiers in their `package.json`
+
+### Don't
+
+- Don't add dependencies with GPL, AGPL, LGPL, or SSPL licenses
+- Don't add dependencies with no license field (`UNLICENSED` or missing) — these are "all rights reserved" by default
+- Don't assume a package is permissive based on its README — always check the `license` field in `package.json`
+- Don't add packages with `Custom` or proprietary license fields without explicit legal review
+
+## Consequences
+
+### Positive
+
+- **Legal certainty** — Every dependency in the compiled binary is confirmed Apache-2.0-compatible
+- **Distribution safety** — Users, enterprises, and downstream redistributors can rely on the project's Apache-2.0 license without hidden copyleft obligations
+- **Automated enforcement** — License violations are caught in CI before merge
+
+### Negative
+
+- **May reject useful packages** — Some high-quality libraries use copyleft licenses (e.g., readline-sync is GPL). These cannot be used regardless of utility.
+- **Allowlist maintenance** — Rare or exotic permissive licenses require manual addition to the allowlist
+
+### Risks
+
+- **Transitive dependency license change** — A previously-permissive dependency may change its license in a new version (e.g., the Elasticsearch SSPL relicensing).
+ - **Mitigation:** The `LEGAL-002/no-copyleft-deps` rule scans the installed `node_modules` tree on every `archgate check`, catching license changes on any version update.
+- **Missing license field in package.json** — Some packages declare their license only in a LICENSE file, not in the `license` field. The scanner may flag these as "no license."
+ - **Mitigation:** If a package is clearly permissive (LICENSE file exists) but lacks a `package.json` license field, add it to the allowlist with a comment explaining the override.
+
+## Compliance and Enforcement
+
+### Automated Enforcement
+
+- **Archgate rule** `LEGAL-002/no-copyleft-deps`: Scans **all** packages in `node_modules/` (direct and transitive) via glob, reads each `package.json` license field, and flags any package not on the permissive allowlist. Severity: `error` (hard blocker). Runs as part of `archgate check` (included in `bun run validate`).
+
+### Manual Enforcement
+
+- Dependency additions in PRs should include a note confirming license compatibility
+
+## References
+
+- [SPDX License List](https://spdx.org/licenses/)
+- [Apache-2.0 License Compatibility](https://www.apache.org/legal/resolved.html)
+- [ARCH-006 — Dependency Policy](./ARCH-006-dependency-policy.md) — Governs which dependencies are allowed; this ADR governs their license compatibility
diff --git a/.archgate/adrs/LEGAL-002-dependency-license-compatibility.rules.ts b/.archgate/adrs/LEGAL-002-dependency-license-compatibility.rules.ts
new file mode 100644
index 00000000..2ed7fae3
--- /dev/null
+++ b/.archgate/adrs/LEGAL-002-dependency-license-compatibility.rules.ts
@@ -0,0 +1,95 @@
+///
+
+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",
+ "CC-BY-4.0",
+ "CC-BY-3.0",
+ "Python-2.0",
+ "(MIT OR Apache-2.0)",
+ "MIT OR Apache-2.0",
+ "(MIT AND Zlib)",
+ "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "(MIT OR CC0-1.0)",
+ "BlueOak-1.0.0 OR MIT OR Apache-2.0",
+]);
+
+function isAllowed(license: string | undefined): boolean {
+ if (!license) return false;
+ if (ALLOWED_LICENSES.has(license)) return true;
+
+ const normalized = license.trim().replace(/^\(/u, "").replace(/\)$/u, "");
+ if (ALLOWED_LICENSES.has(normalized)) return true;
+ if (ALLOWED_LICENSES.has(`(${normalized})`)) return true;
+
+ // OR expressions: at least one alternative must be allowed
+ if (normalized.includes(" OR ")) {
+ return normalized.split(" OR ").some((l) => ALLOWED_LICENSES.has(l.trim()));
+ }
+
+ return false;
+}
+
+/**
+ * Extract the package name from a node_modules package.json path.
+ * Handles both regular (node_modules/foo/package.json) and scoped
+ * (node_modules/@scope/foo/package.json) 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);
+ // Scoped package: @scope/name
+ 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 Apache-2.0-compatible (permissive) licenses",
+ async check(ctx) {
+ // Scan ALL packages in node_modules — direct AND transitive.
+ // Brace expansion covers both regular (zod) and scoped (@sentry/node-core) 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);
+ const name = extractPackageName(pkgPath);
+ return {
+ dep: name,
+ license: depPkg.license as string | undefined,
+ };
+ } 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)"}". Only permissive licenses (MIT, Apache-2.0, ISC, BSD, etc.) are allowed.`,
+ file: "package.json",
+ fix: `Remove "${result.dep}" or find an alternative with a permissive license. See LEGAL-002 for the approved license list.`,
+ });
+ }
+ }
+ },
+ },
+ },
+} satisfies RuleSet;
diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt
index 2958048b..c8e004bd 100644
--- a/docs/public/llms-full.txt
+++ b/docs/public/llms-full.txt
@@ -4039,15 +4039,31 @@ For the full Archgate Privacy Policy, please visit:
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) — 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). See the [full privacy policy](https://archgate.dev/privacy-policy) for complete details.
+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
@@ -4061,10 +4077,20 @@ 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
@@ -4492,11 +4518,32 @@ 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
-- **Analytics**: PostHog Cloud (EU region). Data retained per PostHog's standard retention policy.
-- **Error tracking**: Sentry Cloud (EU region). Error events retained for 90 days.
-- **Local config**: `~/.archgate/config.json` stores your telemetry preference and anonymous install ID.
+| 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
@@ -4982,6 +5029,143 @@ When your project intentionally mixes naming conventions (e.g., PascalCase for R
---
+## 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
+///
+
+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.
+- **Check only production deps**: Remove `devDependencies` from `allDeps` if you only care about bundled/shipped code.
+- **Scan transitives**: Use `ctx.glob("node_modules/{*,@*/*}/package.json")` to scan all installed packages (including scoped) recursively, not just direct dependencies.
+
+## 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/
@@ -6015,6 +6199,105 @@ When the directory contains mixed file types that do not all need to follow the
---
+## 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"
+
+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
+
+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
+
+```
+
+## Rule implementation
+
+```typescript
+///
+
+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) {
+ if (content === null) continue;
+
+ // 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/
diff --git a/docs/src/components/Analytics.astro b/docs/src/components/Analytics.astro
index c51eb288..7afaa33c 100644
--- a/docs/src/components/Analytics.astro
+++ b/docs/src/components/Analytics.astro
@@ -5,5 +5,6 @@
ui_host: "https://eu.posthog.com",
defaults: "2026-01-30",
person_profiles: "identified_only",
+ persistence: "memory",
});
diff --git a/docs/src/content/docs/examples/license-compatibility.mdx b/docs/src/content/docs/examples/license-compatibility.mdx
new file mode 100644
index 00000000..1dfe3ca5
--- /dev/null
+++ b/docs/src/content/docs/examples/license-compatibility.mdx
@@ -0,0 +1,135 @@
+---
+title: license-compatibility
+description: Verify all dependencies use licenses compatible with your project's license to prevent copyleft contamination.
+---
+
+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
+///
+
+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.
+- **Check only production deps**: Remove `devDependencies` from `allDeps` if you only care about bundled/shipped code.
+- **Scan transitives**: Use `ctx.glob("node_modules/{*,@*/*}/package.json")` to scan all installed packages (including scoped) recursively, not just direct dependencies.
+
+## 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.
diff --git a/docs/src/content/docs/examples/spdx-license-headers.mdx b/docs/src/content/docs/examples/spdx-license-headers.mdx
new file mode 100644
index 00000000..3dd1c007
--- /dev/null
+++ b/docs/src/content/docs/examples/spdx-license-headers.mdx
@@ -0,0 +1,99 @@
+---
+title: spdx-license-headers
+description: Enforce SPDX-License-Identifier headers in all source files for per-file license clarity and compliance scanner compatibility.
+---
+
+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
+///
+
+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) {
+ if (content === null) continue;
+
+ // 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.
diff --git a/docs/src/content/docs/pt-br/examples/license-compatibility.mdx b/docs/src/content/docs/pt-br/examples/license-compatibility.mdx
new file mode 100644
index 00000000..531110d4
--- /dev/null
+++ b/docs/src/content/docs/pt-br/examples/license-compatibility.mdx
@@ -0,0 +1,135 @@
+---
+title: license-compatibility
+description: Verificar se todas as dependências usam licenças compatíveis com a licença do seu projeto para prevenir contaminação copyleft.
+---
+
+Previna que licenças copyleft ou incompatíveis entrem na sua árvore de dependências.
+
+## Detalhes da regra
+
+Quando você compila ou empacota dependências na sua aplicação, os termos de licença delas se aplicam à obra combinada. Uma única dependência GPL em um projeto permissivo (MIT/Apache-2.0) pode forçar o projeto inteiro a adotar termos copyleft. Esta regra verifica todas as dependências diretas contra uma allowlist de licenças permissivas.
+
+## Exemplos de código **incorreto**
+
+```json title="package.json"
+{
+ "dependencies": { "zod": "^3.23.0" },
+ "devDependencies": { "readline-sync": "^1.4.10" }
+}
+```
+
+Se `readline-sync` usa GPL-3.0, dispara uma violação mesmo como devDependency.
+
+## Exemplos de código **correto**
+
+```json title="package.json"
+{
+ "dependencies": { "zod": "^3.23.0" },
+ "devDependencies": { "fast-check": "^4.7.0" }
+}
+```
+
+Tanto `zod` (MIT) quanto `fast-check` (MIT) estão na allowlist de licenças permissivas.
+
+## Implementação da regra
+
+```typescript
+///
+
+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;
+}
+
+/**
+ * Extrai o nome do pacote de um caminho node_modules.
+ * Trata pacotes regulares e escopados (@scope/name).
+ */
+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) {
+ // Escaneia TODOS os pacotes em node_modules — diretos E transitivos.
+ // Brace expansion cobre pacotes regulares e escopados.
+ 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;
+```
+
+## Personalização
+
+- **Alterar a allowlist**: Adicione ou remova identificadores de licença baseado na licença do seu projeto. Projetos GPL podem permitir dependências GPL; projetos Apache-2.0 devem bloqueá-las.
+- **Verificar apenas deps de produção**: Remova `devDependencies` de `allDeps` se você só se preocupa com código empacotado/distribuído.
+- **Escanear transitivas**: Use `ctx.glob("node_modules/{*,@*/*}/package.json")` para escanear todos os pacotes instalados (incluindo escopados) recursivamente, não apenas dependências diretas.
+
+## Quando usar
+
+Quando seu projeto usa uma licença permissiva (MIT, Apache-2.0, ISC, BSD) e você quer prevenir contaminação copyleft, especialmente em distribuições compiladas/empacotadas.
+
+## Quando não usar
+
+Em projetos licenciados sob GPL (onde dependências copyleft são compatíveis), ou quando você tem uma ferramenta SaaS dedicada de scanning de licenças (FOSSA, Snyk) que já bloqueia seu pipeline de CI.
diff --git a/docs/src/content/docs/pt-br/examples/spdx-license-headers.mdx b/docs/src/content/docs/pt-br/examples/spdx-license-headers.mdx
new file mode 100644
index 00000000..a267ae7e
--- /dev/null
+++ b/docs/src/content/docs/pt-br/examples/spdx-license-headers.mdx
@@ -0,0 +1,99 @@
+---
+title: spdx-license-headers
+description: Garantir cabeçalhos SPDX-License-Identifier em todos os arquivos fonte para clareza de licença por arquivo e compatibilidade com scanners de conformidade.
+---
+
+Garanta que cada arquivo fonte declare sua licença com um identificador SPDX legível por máquina.
+
+## Detalhes da regra
+
+Projetos open-source se beneficiam de declarações de licença por arquivo — elas sobrevivem à extração de arquivos, bundling e cenários de cópia onde o arquivo LICENSE raiz não está presente. Esta regra verifica que cada arquivo TypeScript fonte começa com o comentário de cabeçalho SPDX padrão.
+
+## Exemplos de código **incorreto**
+
+```typescript title="src/helpers/utils.ts"
+import { join } from "node:path";
+
+export function resolvePath(base: string, rel: string): string {
+ return join(base, rel);
+}
+```
+
+O arquivo está sem o cabeçalho de identificador de licença SPDX.
+
+## Exemplos de código **correto**
+
+```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);
+}
+```
+
+Para arquivos com shebang:
+
+```typescript title="src/cli.ts"
+#!/usr/bin/env bun
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
+import { Command } from "commander";
+```
+
+## Implementação da regra
+
+```typescript
+///
+
+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) {
+ if (content === null) continue;
+
+ // 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;
+```
+
+## Personalização
+
+- **Alterar a licença**: Substitua `Apache-2.0` pelo identificador SPDX do seu projeto (ex: `MIT`, `BSD-3-Clause`)
+- **Alterar o escopo**: Ajuste o glob `files` no frontmatter do ADR para corresponder aos seus diretórios fonte
+- **Adicionar verificação de copyright**: Estenda a regra para também verificar o formato da linha de copyright
+
+## Quando usar
+
+Quando seu projeto é open-source e você deseja declarações de licença por arquivo inequívocas reconhecidas por scanners de conformidade (FOSSA, Snyk, Black Duck, npm license-checker).
+
+## Quando não usar
+
+Em projetos proprietários/closed-source onde todos os arquivos são implicitamente "todos os direitos reservados", ou quando sua organização usa um mecanismo diferente de declaração de licença como arquivos `.dep5` do REUSE 3.0.
diff --git a/docs/src/content/docs/reference/privacy-policy.mdx b/docs/src/content/docs/reference/privacy-policy.mdx
index dafee47a..fa5c5f7c 100644
--- a/docs/src/content/docs/reference/privacy-policy.mdx
+++ b/docs/src/content/docs/reference/privacy-policy.mdx
@@ -9,15 +9,31 @@ For the full Archgate Privacy Policy, please visit:
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) — 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). See the [full privacy policy](https://archgate.dev/privacy-policy) for complete details.
+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
@@ -31,6 +47,16 @@ 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.
diff --git a/docs/src/content/docs/reference/telemetry.mdx b/docs/src/content/docs/reference/telemetry.mdx
index 2396ca8d..dab0338f 100644
--- a/docs/src/content/docs/reference/telemetry.mdx
+++ b/docs/src/content/docs/reference/telemetry.mdx
@@ -112,11 +112,32 @@ 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
-- **Analytics**: PostHog Cloud (EU region). Data retained per PostHog's standard retention policy.
-- **Error tracking**: Sentry Cloud (EU region). Error events retained for 90 days.
-- **Local config**: `~/.archgate/config.json` stores your telemetry preference and anonymous install ID.
+| 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
diff --git a/scripts/add-spdx-headers.ts b/scripts/add-spdx-headers.ts
new file mode 100644
index 00000000..bccae9ad
--- /dev/null
+++ b/scripts/add-spdx-headers.ts
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
+/**
+ * add-spdx-headers.ts — One-time script to add SPDX license headers to all
+ * TypeScript source files that don't already have them.
+ *
+ * Run: bun run scripts/add-spdx-headers.ts
+ */
+
+import { readFileSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+
+import { Glob } from "bun";
+
+const HEADER = `// SPDX-License-Identifier: Apache-2.0\n// Copyright 2026 Archgate\n`;
+const ROOT = join(import.meta.dir, "..");
+
+const patterns = ["src/**/*.ts", "tests/**/*.ts"];
+let updated = 0;
+let skipped = 0;
+
+for (const pattern of patterns) {
+ const glob = new Glob(pattern);
+ for (const match of glob.scanSync({ cwd: ROOT, absolute: true })) {
+ const content = readFileSync(match, "utf-8");
+
+ // Skip if already has SPDX header
+ if (content.includes("SPDX-License-Identifier")) {
+ skipped++;
+ continue;
+ }
+
+ // Handle shebang lines — insert after them
+ let newContent: string;
+ if (content.startsWith("#!")) {
+ const firstNewline = content.indexOf("\n");
+ newContent =
+ content.slice(0, firstNewline + 1) +
+ HEADER +
+ content.slice(firstNewline + 1);
+ } else {
+ newContent = HEADER + content;
+ }
+
+ writeFileSync(match, newContent);
+ updated++;
+ }
+}
+
+console.log(
+ `Added SPDX headers to ${updated} files. Skipped ${skipped} (already had headers).`
+);
diff --git a/src/cli.ts b/src/cli.ts
index c7791547..9072778e 100755
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,4 +1,6 @@
#!/usr/bin/env bun
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { Command, Option } from "@commander-js/extra-typings";
import { semver } from "bun";
@@ -31,6 +33,7 @@ import {
initTelemetry,
trackCommand,
} from "./helpers/telemetry";
+import { showFirstRunNoticeIfNeeded } from "./helpers/telemetry-config";
import { checkForUpdatesIfNeeded } from "./helpers/update-check";
// Pre-main environment guards — these are user-facing errors (exit 1), not bugs.
@@ -94,6 +97,9 @@ async function main() {
// the second arg is the actual subcommand being executed. We use the action
// command so `adr create` etc. resolves correctly instead of always "root".
program.hook("preAction", async (_hookedCommand, actionCommand) => {
+ // Show one-time privacy disclosure before the first real command.
+ showFirstRunNoticeIfNeeded();
+
// Ensure telemetry SDKs are initialized before emitting any events.
// By the time a real command's preAction fires, the init promises have
// usually already resolved (they started before command registration),
diff --git a/src/commands/adr/create.ts b/src/commands/adr/create.ts
index 05665612..5547a3d4 100644
--- a/src/commands/adr/create.ts
+++ b/src/commands/adr/create.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { cursorTo } from "node:readline";
import type { Command } from "@commander-js/extra-typings";
diff --git a/src/commands/adr/domain/add.ts b/src/commands/adr/domain/add.ts
index d3588888..8da3c0cb 100644
--- a/src/commands/adr/domain/add.ts
+++ b/src/commands/adr/domain/add.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { exitWith } from "../../../helpers/exit";
diff --git a/src/commands/adr/domain/index.ts b/src/commands/adr/domain/index.ts
index e305eb35..273b1bfe 100644
--- a/src/commands/adr/domain/index.ts
+++ b/src/commands/adr/domain/index.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { registerDomainAddCommand } from "./add";
diff --git a/src/commands/adr/domain/list.ts b/src/commands/adr/domain/list.ts
index ef1f227e..77bf5850 100644
--- a/src/commands/adr/domain/list.ts
+++ b/src/commands/adr/domain/list.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { styleText } from "node:util";
import type { Command } from "@commander-js/extra-typings";
diff --git a/src/commands/adr/domain/remove.ts b/src/commands/adr/domain/remove.ts
index e3a9cd4f..8ed4bb41 100644
--- a/src/commands/adr/domain/remove.ts
+++ b/src/commands/adr/domain/remove.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { exitWith } from "../../../helpers/exit";
diff --git a/src/commands/adr/index.ts b/src/commands/adr/index.ts
index 4f802e56..f2e1d113 100644
--- a/src/commands/adr/index.ts
+++ b/src/commands/adr/index.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { registerAdrCreateCommand } from "./create";
diff --git a/src/commands/adr/list.ts b/src/commands/adr/list.ts
index e6d8cc7d..a69d9b6a 100644
--- a/src/commands/adr/list.ts
+++ b/src/commands/adr/list.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync } from "node:fs";
import { styleText } from "node:util";
diff --git a/src/commands/adr/show.ts b/src/commands/adr/show.ts
index ba1810de..e580a569 100644
--- a/src/commands/adr/show.ts
+++ b/src/commands/adr/show.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { findAdrFileById } from "../../helpers/adr-writer";
diff --git a/src/commands/adr/update.ts b/src/commands/adr/update.ts
index 121e6d4d..72b3dee6 100644
--- a/src/commands/adr/update.ts
+++ b/src/commands/adr/update.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { updateAdrFile } from "../../helpers/adr-writer";
diff --git a/src/commands/check.ts b/src/commands/check.ts
index 30b55a18..d909ac33 100644
--- a/src/commands/check.ts
+++ b/src/commands/check.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { loadRuleAdrs } from "../engine/loader";
diff --git a/src/commands/clean.ts b/src/commands/clean.ts
index b18fbe82..cc423272 100644
--- a/src/commands/clean.ts
+++ b/src/commands/clean.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync, readdirSync, rmSync } from "node:fs";
import { join } from "node:path";
diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts
index 924bc678..c84c1969 100644
--- a/src/commands/doctor.ts
+++ b/src/commands/doctor.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { styleText } from "node:util";
import type { Command } from "@commander-js/extra-typings";
diff --git a/src/commands/init.ts b/src/commands/init.ts
index 33033405..535f8497 100644
--- a/src/commands/init.ts
+++ b/src/commands/init.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync } from "node:fs";
import { join } from "node:path";
import { cursorTo } from "node:readline";
diff --git a/src/commands/login.ts b/src/commands/login.ts
index aa80bf38..4a06a467 100644
--- a/src/commands/login.ts
+++ b/src/commands/login.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { styleText } from "node:util";
import type { Command } from "@commander-js/extra-typings";
diff --git a/src/commands/plugin/index.ts b/src/commands/plugin/index.ts
index caab5c6d..73af4e0b 100644
--- a/src/commands/plugin/index.ts
+++ b/src/commands/plugin/index.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { registerPluginInstallCommand } from "./install";
diff --git a/src/commands/plugin/install.ts b/src/commands/plugin/install.ts
index 8817702a..a8e456e7 100644
--- a/src/commands/plugin/install.ts
+++ b/src/commands/plugin/install.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { styleText } from "node:util";
import type { Command } from "@commander-js/extra-typings";
diff --git a/src/commands/plugin/url.ts b/src/commands/plugin/url.ts
index 89e2ca85..0ef10284 100644
--- a/src/commands/plugin/url.ts
+++ b/src/commands/plugin/url.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
diff --git a/src/commands/review-context.ts b/src/commands/review-context.ts
index 413affe2..83d2ee02 100644
--- a/src/commands/review-context.ts
+++ b/src/commands/review-context.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { buildReviewContext } from "../engine/context";
diff --git a/src/commands/session-context/claude-code.ts b/src/commands/session-context/claude-code.ts
index 19be7c8b..7e7fea01 100644
--- a/src/commands/session-context/claude-code.ts
+++ b/src/commands/session-context/claude-code.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
diff --git a/src/commands/session-context/copilot.ts b/src/commands/session-context/copilot.ts
index 108886f4..bb99881e 100644
--- a/src/commands/session-context/copilot.ts
+++ b/src/commands/session-context/copilot.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
diff --git a/src/commands/session-context/cursor.ts b/src/commands/session-context/cursor.ts
index 205c7f51..31e11383 100644
--- a/src/commands/session-context/cursor.ts
+++ b/src/commands/session-context/cursor.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
diff --git a/src/commands/session-context/index.ts b/src/commands/session-context/index.ts
index da14d7cc..66d527ef 100644
--- a/src/commands/session-context/index.ts
+++ b/src/commands/session-context/index.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { registerClaudeCodeSessionContextCommand } from "./claude-code";
diff --git a/src/commands/session-context/opencode.ts b/src/commands/session-context/opencode.ts
index 60705cb9..f8e98e9a 100644
--- a/src/commands/session-context/opencode.ts
+++ b/src/commands/session-context/opencode.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
diff --git a/src/commands/telemetry.ts b/src/commands/telemetry.ts
index 183fefa7..5d138101 100644
--- a/src/commands/telemetry.ts
+++ b/src/commands/telemetry.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { Command } from "@commander-js/extra-typings";
import { exitWith } from "../helpers/exit";
@@ -37,6 +39,9 @@ export function registerTelemetryCommand(program: Command) {
console.log(
"\nTo disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0"
);
+ console.log(
+ "\nLegal basis: https://archgate.dev/legitimate-interest-assessment"
+ );
console.log("Learn more: https://cli.archgate.dev/reference/telemetry");
} else {
console.log("Telemetry is disabled.");
diff --git a/src/commands/upgrade.ts b/src/commands/upgrade.ts
index cee36d67..1a5e2adb 100644
--- a/src/commands/upgrade.ts
+++ b/src/commands/upgrade.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { clearLine, cursorTo } from "node:readline";
diff --git a/src/engine/context.ts b/src/engine/context.ts
index 810092df..0c8b4bde 100644
--- a/src/engine/context.ts
+++ b/src/engine/context.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { AdrDocument, AdrDomain } from "../formats/adr";
import { getChangedFiles, getStagedFiles } from "./git-files";
import { loadRuleAdrs, parseAllAdrs } from "./loader";
diff --git a/src/engine/git-files.ts b/src/engine/git-files.ts
index 1fcf2ab5..6475ed23 100644
--- a/src/engine/git-files.ts
+++ b/src/engine/git-files.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/** Git file-listing utilities for ADR scope resolution and change detection. */
import { logDebug } from "../helpers/log";
diff --git a/src/engine/loader.ts b/src/engine/loader.ts
index acaa2934..db48bf89 100644
--- a/src/engine/loader.ts
+++ b/src/engine/loader.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { readdirSync } from "node:fs";
import { join, basename } from "node:path";
import { pathToFileURL } from "node:url";
diff --git a/src/engine/reporter.ts b/src/engine/reporter.ts
index cd3ad82d..d7b4f249 100644
--- a/src/engine/reporter.ts
+++ b/src/engine/reporter.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { styleText } from "node:util";
import type { Severity } from "../formats/rules";
diff --git a/src/engine/rule-scanner.ts b/src/engine/rule-scanner.ts
index f6fba1ca..ba6283ed 100644
--- a/src/engine/rule-scanner.ts
+++ b/src/engine/rule-scanner.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { parseModule } from "meriyah";
/**
diff --git a/src/engine/runner.ts b/src/engine/runner.ts
index 2cd03a20..dc176e09 100644
--- a/src/engine/runner.ts
+++ b/src/engine/runner.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { lstatSync } from "node:fs";
import { relative, resolve, isAbsolute } from "node:path";
diff --git a/src/engine/source-positions.ts b/src/engine/source-positions.ts
index 19fa968d..672d329c 100644
--- a/src/engine/source-positions.ts
+++ b/src/engine/source-positions.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* Utilities for mapping violation positions from transpiled JS back to
* original TypeScript source, skipping matches in comments and strings.
diff --git a/src/formats/adr.ts b/src/formats/adr.ts
index e666a171..5c72656d 100644
--- a/src/formats/adr.ts
+++ b/src/formats/adr.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { z } from "zod";
import { DOMAIN_NAME_PATTERN } from "./project-config";
diff --git a/src/formats/project-config.ts b/src/formats/project-config.ts
index eeb74ccc..6ec1b3f8 100644
--- a/src/formats/project-config.ts
+++ b/src/formats/project-config.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { z } from "zod";
export const DOMAIN_NAME_PATTERN = /^[a-z][a-z0-9-]*$/u;
diff --git a/src/formats/rules.ts b/src/formats/rules.ts
index c36cc375..45cc26bc 100644
--- a/src/formats/rules.ts
+++ b/src/formats/rules.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
// --- Severity ---
export type Severity = "error" | "warning" | "info";
diff --git a/src/helpers/adr-templates.ts b/src/helpers/adr-templates.ts
index 0f35b874..410464d8 100644
--- a/src/helpers/adr-templates.ts
+++ b/src/helpers/adr-templates.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { AdrDomain } from "../formats/adr";
export function generateExampleAdr(projectName: string): string {
diff --git a/src/helpers/adr-writer.ts b/src/helpers/adr-writer.ts
index 636c84be..515489cf 100644
--- a/src/helpers/adr-writer.ts
+++ b/src/helpers/adr-writer.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync, readdirSync } from "node:fs";
import { join, basename } from "node:path";
diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts
index 8fe09ec8..7b1766f7 100644
--- a/src/helpers/auth.ts
+++ b/src/helpers/auth.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* auth.ts — GitHub Device Flow authentication and archgate token management.
*
diff --git a/src/helpers/binary-upgrade.ts b/src/helpers/binary-upgrade.ts
index 8909a4cb..e6ff7a6e 100644
--- a/src/helpers/binary-upgrade.ts
+++ b/src/helpers/binary-upgrade.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { createHash } from "node:crypto";
import { chmodSync, mkdtempSync, renameSync, unlinkSync } from "node:fs";
import { unlink } from "node:fs/promises";
diff --git a/src/helpers/claude-settings.ts b/src/helpers/claude-settings.ts
index 5d8a7bc5..203e88c0 100644
--- a/src/helpers/claude-settings.ts
+++ b/src/helpers/claude-settings.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync, mkdirSync } from "node:fs";
import { join } from "node:path";
diff --git a/src/helpers/copilot-settings.ts b/src/helpers/copilot-settings.ts
index b625af86..7e3aafda 100644
--- a/src/helpers/copilot-settings.ts
+++ b/src/helpers/copilot-settings.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync, mkdirSync } from "node:fs";
import { join } from "node:path";
diff --git a/src/helpers/credential-store.ts b/src/helpers/credential-store.ts
index 1b81e1dc..849b5dc1 100644
--- a/src/helpers/credential-store.ts
+++ b/src/helpers/credential-store.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* credential-store.ts — Secure credential storage using git's native credential helpers.
*
diff --git a/src/helpers/cursor-settings.ts b/src/helpers/cursor-settings.ts
index d9ab0058..a5b18693 100644
--- a/src/helpers/cursor-settings.ts
+++ b/src/helpers/cursor-settings.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* Cursor editor integration.
*
diff --git a/src/helpers/doctor.ts b/src/helpers/doctor.ts
index b4bcaf2c..a85dc863 100644
--- a/src/helpers/doctor.ts
+++ b/src/helpers/doctor.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* doctor.ts — Gathers system diagnostic information for debugging.
*
diff --git a/src/helpers/editor-detect.ts b/src/helpers/editor-detect.ts
index a800176d..52e2d367 100644
--- a/src/helpers/editor-detect.ts
+++ b/src/helpers/editor-detect.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* editor-detect.ts — Detects available editor CLIs and prompts the user to select.
*
diff --git a/src/helpers/exit.ts b/src/helpers/exit.ts
index b16d6994..61b97f01 100644
--- a/src/helpers/exit.ts
+++ b/src/helpers/exit.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* exit.ts — Centralized process-exit helper for the CLI.
*
diff --git a/src/helpers/git.ts b/src/helpers/git.ts
index 10b7c200..abbcdbe2 100644
--- a/src/helpers/git.ts
+++ b/src/helpers/git.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { logDebug, logInfo } from "./log";
import { isWindows, isMacOS, resolveCommand } from "./platform";
diff --git a/src/helpers/init-project.ts b/src/helpers/init-project.ts
index ef955864..62285f47 100644
--- a/src/helpers/init-project.ts
+++ b/src/helpers/init-project.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync, readdirSync } from "node:fs";
import { basename, join } from "node:path";
diff --git a/src/helpers/install-info.ts b/src/helpers/install-info.ts
index 910afa0c..541b3bd5 100644
--- a/src/helpers/install-info.ts
+++ b/src/helpers/install-info.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* install-info.ts — Detects the CLI installation method and project context.
*
diff --git a/src/helpers/log.ts b/src/helpers/log.ts
index 65e83288..2bfc8034 100644
--- a/src/helpers/log.ts
+++ b/src/helpers/log.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { styleText } from "node:util";
// ---------------------------------------------------------------------------
diff --git a/src/helpers/login-flow.ts b/src/helpers/login-flow.ts
index 6a674ff2..743f5772 100644
--- a/src/helpers/login-flow.ts
+++ b/src/helpers/login-flow.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* login-flow.ts — Shared GitHub device flow + signup logic
* used by both `login` and `init` commands.
@@ -49,6 +51,9 @@ export interface LoginFlowResult {
export async function runLoginFlow(
options?: LoginFlowOptions
): Promise {
+ console.log("By signing up, you agree to the Archgate Terms of Service:");
+ console.log("https://archgate.dev/terms-of-service\n");
+
logInfo("Authenticating with GitHub...\n");
logDebug("Starting login flow");
diff --git a/src/helpers/output.ts b/src/helpers/output.ts
index 9c0f7033..c4e84cf7 100644
--- a/src/helpers/output.ts
+++ b/src/helpers/output.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* Output format detection for agent vs human contexts.
*
diff --git a/src/helpers/paths.ts b/src/helpers/paths.ts
index 828a20e0..5ca65b88 100644
--- a/src/helpers/paths.ts
+++ b/src/helpers/paths.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join, dirname } from "node:path";
diff --git a/src/helpers/platform.ts b/src/helpers/platform.ts
index 284dcb32..46c03a9b 100644
--- a/src/helpers/platform.ts
+++ b/src/helpers/platform.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { readFileSync } from "node:fs";
// ---------------------------------------------------------------------------
diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts
index 44477f9a..b6afb944 100644
--- a/src/helpers/plugin-install.ts
+++ b/src/helpers/plugin-install.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/** Download and install the archgate plugin for supported editors. */
import { mkdirSync, unlinkSync } from "node:fs";
diff --git a/src/helpers/project-config.ts b/src/helpers/project-config.ts
index 0ad52df9..1436a674 100644
--- a/src/helpers/project-config.ts
+++ b/src/helpers/project-config.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* project-config.ts — Read/write `.archgate/config.json` at project root.
*
diff --git a/src/helpers/repo-probe.ts b/src/helpers/repo-probe.ts
index 198e39f8..5addec49 100644
--- a/src/helpers/repo-probe.ts
+++ b/src/helpers/repo-probe.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* repo-probe.ts — Unauthenticated API probes to determine whether a Git
* repository is public on its host (GitHub, GitLab, Bitbucket, Azure DevOps).
diff --git a/src/helpers/repo.ts b/src/helpers/repo.ts
index 52d63f81..5338783f 100644
--- a/src/helpers/repo.ts
+++ b/src/helpers/repo.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* repo.ts — Detect the git repository context for telemetry enrichment.
*
diff --git a/src/helpers/rules-shim.ts b/src/helpers/rules-shim.ts
index 2deb0260..4a933e14 100644
--- a/src/helpers/rules-shim.ts
+++ b/src/helpers/rules-shim.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { logDebug } from "./log";
import { projectPath } from "./paths";
diff --git a/src/helpers/sentry.ts b/src/helpers/sentry.ts
index 2fd4e569..959efbb7 100644
--- a/src/helpers/sentry.ts
+++ b/src/helpers/sentry.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* sentry.ts — Error tracking via @sentry/node-core light mode.
*
diff --git a/src/helpers/session-context-copilot.ts b/src/helpers/session-context-copilot.ts
index 04cb96c6..73e926f8 100644
--- a/src/helpers/session-context-copilot.ts
+++ b/src/helpers/session-context-copilot.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { readdirSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
diff --git a/src/helpers/session-context-opencode.ts b/src/helpers/session-context-opencode.ts
index 52f1cef5..fbc0d9fd 100644
--- a/src/helpers/session-context-opencode.ts
+++ b/src/helpers/session-context-opencode.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { Database } from "bun:sqlite";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
diff --git a/src/helpers/session-context.ts b/src/helpers/session-context.ts
index 293a8778..9cbe44e8 100644
--- a/src/helpers/session-context.ts
+++ b/src/helpers/session-context.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { readdirSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { basename, join } from "node:path";
diff --git a/src/helpers/signup.ts b/src/helpers/signup.ts
index 2e95a325..ca23acc8 100644
--- a/src/helpers/signup.ts
+++ b/src/helpers/signup.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* signup.ts — Archgate plugins platform signup for unregistered users.
*/
diff --git a/src/helpers/telemetry-config.ts b/src/helpers/telemetry-config.ts
index ec08e46f..7d233402 100644
--- a/src/helpers/telemetry-config.ts
+++ b/src/helpers/telemetry-config.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* telemetry-config.ts — Manages telemetry preferences in ~/.archgate/config.json.
*
@@ -26,6 +28,8 @@ export interface TelemetryConfig {
installId: string;
/** ISO date of first telemetry config creation. */
createdAt: string;
+ /** Whether the first-run privacy notice has been shown. */
+ noticeShown?: boolean;
}
// ---------------------------------------------------------------------------
@@ -145,6 +149,37 @@ function saveTelemetryConfigAsync(config: TelemetryConfig): void {
});
}
+// ---------------------------------------------------------------------------
+// First-run privacy notice
+// ---------------------------------------------------------------------------
+
+/**
+ * Show a one-time privacy notice on first interactive CLI session.
+ * Only displays when: TTY, not CI, telemetry enabled, notice not yet shown.
+ * Non-blocking — saves the "shown" flag asynchronously.
+ */
+export function showFirstRunNoticeIfNeeded(): void {
+ if (!process.stdout.isTTY) return;
+ if (Bun.env.CI) return;
+ if (isEnvTelemetryDisabled()) return;
+
+ const config = loadTelemetryConfig();
+ if (!config.telemetry) return;
+ if (config.noticeShown) return;
+
+ logDebug("Showing first-run privacy notice");
+ process.stdout.write(
+ "\nArchgate collects anonymous usage data to improve the tool.\n" +
+ "Details: https://cli.archgate.dev/reference/telemetry\n" +
+ "Disable: archgate telemetry disable\n\n"
+ );
+
+ // Persist the flag so the notice is shown only once
+ config.noticeShown = true;
+ cachedConfig = config;
+ saveTelemetryConfigAsync(config);
+}
+
// ---------------------------------------------------------------------------
// Testing helpers
// ---------------------------------------------------------------------------
diff --git a/src/helpers/telemetry.ts b/src/helpers/telemetry.ts
index c3ae8573..8fbf4c0a 100644
--- a/src/helpers/telemetry.ts
+++ b/src/helpers/telemetry.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* telemetry.ts — Anonymous usage analytics via PostHog Node SDK.
*
diff --git a/src/helpers/tls.ts b/src/helpers/tls.ts
index 230870f1..157406ff 100644
--- a/src/helpers/tls.ts
+++ b/src/helpers/tls.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* tls.ts — Detect TLS/SSL interception errors common in corporate environments
* and provide actionable guidance to the user.
diff --git a/src/helpers/update-check.ts b/src/helpers/update-check.ts
index a5362e75..9fc99811 100644
--- a/src/helpers/update-check.ts
+++ b/src/helpers/update-check.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { semver } from "bun";
import { fetchLatestGitHubVersion } from "./binary-upgrade";
diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts
index f130f9f7..b9c6c379 100644
--- a/src/helpers/vscode-settings.ts
+++ b/src/helpers/vscode-settings.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
diff --git a/tests/commands/adr.test.ts b/tests/commands/adr.test.ts
index 3b6e6e96..06bbf811 100644
--- a/tests/commands/adr.test.ts
+++ b/tests/commands/adr.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, readdirSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/adr/create.test.ts b/tests/commands/adr/create.test.ts
index c5fded77..3d138a81 100644
--- a/tests/commands/adr/create.test.ts
+++ b/tests/commands/adr/create.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/adr/domain/add.test.ts b/tests/commands/adr/domain/add.test.ts
index 7f6d51fa..ce9432cb 100644
--- a/tests/commands/adr/domain/add.test.ts
+++ b/tests/commands/adr/domain/add.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/adr/domain/list.test.ts b/tests/commands/adr/domain/list.test.ts
index a38f1f88..d0df2910 100644
--- a/tests/commands/adr/domain/list.test.ts
+++ b/tests/commands/adr/domain/list.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/adr/domain/remove.test.ts b/tests/commands/adr/domain/remove.test.ts
index 8f022951..f6785038 100644
--- a/tests/commands/adr/domain/remove.test.ts
+++ b/tests/commands/adr/domain/remove.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/adr/index.test.ts b/tests/commands/adr/index.test.ts
index 9cfb5de0..615b8c24 100644
--- a/tests/commands/adr/index.test.ts
+++ b/tests/commands/adr/index.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/adr/list.test.ts b/tests/commands/adr/list.test.ts
index 07f9cc2c..5eb152e1 100644
--- a/tests/commands/adr/list.test.ts
+++ b/tests/commands/adr/list.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/adr/show.test.ts b/tests/commands/adr/show.test.ts
index 11aaa096..d6b347e5 100644
--- a/tests/commands/adr/show.test.ts
+++ b/tests/commands/adr/show.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/adr/update.test.ts b/tests/commands/adr/update.test.ts
index 8dbf73bd..92c6e8ac 100644
--- a/tests/commands/adr/update.test.ts
+++ b/tests/commands/adr/update.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/check-security.test.ts b/tests/commands/check-security.test.ts
index f85b764c..1debcded 100644
--- a/tests/commands/check-security.test.ts
+++ b/tests/commands/check-security.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import {
mkdtempSync,
diff --git a/tests/commands/check.test.ts b/tests/commands/check.test.ts
index 5e9d0466..cd1fed85 100644
--- a/tests/commands/check.test.ts
+++ b/tests/commands/check.test.ts
@@ -1,3 +1,5 @@
+// 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 { tmpdir } from "node:os";
diff --git a/tests/commands/clean.test.ts b/tests/commands/clean.test.ts
index 788f2f3a..931a1e6f 100644
--- a/tests/commands/clean.test.ts
+++ b/tests/commands/clean.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import {
mkdtempSync,
diff --git a/tests/commands/init.test.ts b/tests/commands/init.test.ts
index 8c4fcbc6..57aa779d 100644
--- a/tests/commands/init.test.ts
+++ b/tests/commands/init.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/commands/login.test.ts b/tests/commands/login.test.ts
index 3f5d0cc5..58fbc202 100644
--- a/tests/commands/login.test.ts
+++ b/tests/commands/login.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/plugin/index.test.ts b/tests/commands/plugin/index.test.ts
index 737fc688..54c71abe 100644
--- a/tests/commands/plugin/index.test.ts
+++ b/tests/commands/plugin/index.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/plugin/install.test.ts b/tests/commands/plugin/install.test.ts
index 6916b7bd..e4b63b8b 100644
--- a/tests/commands/plugin/install.test.ts
+++ b/tests/commands/plugin/install.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/plugin/url.test.ts b/tests/commands/plugin/url.test.ts
index 47847b9a..b942fd45 100644
--- a/tests/commands/plugin/url.test.ts
+++ b/tests/commands/plugin/url.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/review-context.test.ts b/tests/commands/review-context.test.ts
index 966e6f9b..33f0c203 100644
--- a/tests/commands/review-context.test.ts
+++ b/tests/commands/review-context.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/session-context.test.ts b/tests/commands/session-context.test.ts
index 95cd683e..f4158f3f 100644
--- a/tests/commands/session-context.test.ts
+++ b/tests/commands/session-context.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/session-context/claude-code.test.ts b/tests/commands/session-context/claude-code.test.ts
index 97196fb6..2faf4961 100644
--- a/tests/commands/session-context/claude-code.test.ts
+++ b/tests/commands/session-context/claude-code.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/session-context/copilot.test.ts b/tests/commands/session-context/copilot.test.ts
index 5ba2ad17..d4a3b7d9 100644
--- a/tests/commands/session-context/copilot.test.ts
+++ b/tests/commands/session-context/copilot.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/session-context/cursor.test.ts b/tests/commands/session-context/cursor.test.ts
index 3b715158..c31fa89f 100644
--- a/tests/commands/session-context/cursor.test.ts
+++ b/tests/commands/session-context/cursor.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/session-context/opencode.test.ts b/tests/commands/session-context/opencode.test.ts
index da3c1560..af12145a 100644
--- a/tests/commands/session-context/opencode.test.ts
+++ b/tests/commands/session-context/opencode.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
diff --git a/tests/commands/upgrade.test.ts b/tests/commands/upgrade.test.ts
index 033d87b9..77acccc9 100644
--- a/tests/commands/upgrade.test.ts
+++ b/tests/commands/upgrade.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/engine/context.test.ts b/tests/engine/context.test.ts
index f0a14000..568aea8b 100644
--- a/tests/engine/context.test.ts
+++ b/tests/engine/context.test.ts
@@ -1,3 +1,5 @@
+// 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 { tmpdir } from "node:os";
diff --git a/tests/engine/git-files.test.ts b/tests/engine/git-files.test.ts
index 5133a484..71a66711 100644
--- a/tests/engine/git-files.test.ts
+++ b/tests/engine/git-files.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/engine/loader-security.test.ts b/tests/engine/loader-security.test.ts
index 6100d68d..dd122064 100644
--- a/tests/engine/loader-security.test.ts
+++ b/tests/engine/loader-security.test.ts
@@ -1,3 +1,5 @@
+// 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 { tmpdir } from "node:os";
diff --git a/tests/engine/loader.test.ts b/tests/engine/loader.test.ts
index e72e78b4..9f2dc3fb 100644
--- a/tests/engine/loader.test.ts
+++ b/tests/engine/loader.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import {
mkdtempSync,
diff --git a/tests/engine/reporter.test.ts b/tests/engine/reporter.test.ts
index 23206040..d953345b 100644
--- a/tests/engine/reporter.test.ts
+++ b/tests/engine/reporter.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import {
diff --git a/tests/engine/rule-scanner-adversarial.test.ts b/tests/engine/rule-scanner-adversarial.test.ts
index cd9e61c2..bfdd6801 100644
--- a/tests/engine/rule-scanner-adversarial.test.ts
+++ b/tests/engine/rule-scanner-adversarial.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { scanRuleSource } from "../../src/engine/rule-scanner";
diff --git a/tests/engine/rule-scanner-positions.test.ts b/tests/engine/rule-scanner-positions.test.ts
index cc018f88..3a1b82d3 100644
--- a/tests/engine/rule-scanner-positions.test.ts
+++ b/tests/engine/rule-scanner-positions.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { scanRuleSource } from "../../src/engine/rule-scanner";
diff --git a/tests/engine/rule-scanner.test.ts b/tests/engine/rule-scanner.test.ts
index d841d591..c4220232 100644
--- a/tests/engine/rule-scanner.test.ts
+++ b/tests/engine/rule-scanner.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { scanRuleSource } from "../../src/engine/rule-scanner";
diff --git a/tests/engine/runner-security.test.ts b/tests/engine/runner-security.test.ts
index 2dd05339..fc4d5ff1 100644
--- a/tests/engine/runner-security.test.ts
+++ b/tests/engine/runner-security.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/engine/runner.test.ts b/tests/engine/runner.test.ts
index b0943b30..c14afd93 100644
--- a/tests/engine/runner.test.ts
+++ b/tests/engine/runner.test.ts
@@ -1,3 +1,5 @@
+// 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 { tmpdir } from "node:os";
diff --git a/tests/engine/source-positions.test.ts b/tests/engine/source-positions.test.ts
index 7d68526e..b169d7c9 100644
--- a/tests/engine/source-positions.test.ts
+++ b/tests/engine/source-positions.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import {
diff --git a/tests/fixtures/rules/TEST-001-sample.rules.ts b/tests/fixtures/rules/TEST-001-sample.rules.ts
index 4a261c72..23e37593 100644
--- a/tests/fixtures/rules/TEST-001-sample.rules.ts
+++ b/tests/fixtures/rules/TEST-001-sample.rules.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import type { RuleSet } from "../../../src/formats/rules";
export default {
diff --git a/tests/formats/adr-fuzz.test.ts b/tests/formats/adr-fuzz.test.ts
index 372e7a6a..2c564497 100644
--- a/tests/formats/adr-fuzz.test.ts
+++ b/tests/formats/adr-fuzz.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import fc from "fast-check";
diff --git a/tests/formats/adr.test.ts b/tests/formats/adr.test.ts
index b6430b81..c2b70cb0 100644
--- a/tests/formats/adr.test.ts
+++ b/tests/formats/adr.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { join } from "node:path";
diff --git a/tests/formats/project-config-fuzz.test.ts b/tests/formats/project-config-fuzz.test.ts
index 9dae50b8..3583dfff 100644
--- a/tests/formats/project-config-fuzz.test.ts
+++ b/tests/formats/project-config-fuzz.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import fc from "fast-check";
diff --git a/tests/formats/rules.test.ts b/tests/formats/rules.test.ts
index cdb6ced0..845267ae 100644
--- a/tests/formats/rules.test.ts
+++ b/tests/formats/rules.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import type { RuleSet } from "../../src/formats/rules";
diff --git a/tests/helpers/adr-templates.test.ts b/tests/helpers/adr-templates.test.ts
index 710a286c..869a84bb 100644
--- a/tests/helpers/adr-templates.test.ts
+++ b/tests/helpers/adr-templates.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { parseAdr } from "../../src/formats/adr";
diff --git a/tests/helpers/adr-writer.test.ts b/tests/helpers/adr-writer.test.ts
index 7c5f9655..58e79a5c 100644
--- a/tests/helpers/adr-writer.test.ts
+++ b/tests/helpers/adr-writer.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import {
mkdtempSync,
diff --git a/tests/helpers/auth-poll.test.ts b/tests/helpers/auth-poll.test.ts
index d6476a1a..cf45aefd 100644
--- a/tests/helpers/auth-poll.test.ts
+++ b/tests/helpers/auth-poll.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, mock } from "bun:test";
/** Type-safe fetch mock — Bun's fetch type includes `preconnect` which mock() doesn't provide. */
diff --git a/tests/helpers/auth.test.ts b/tests/helpers/auth.test.ts
index a56da959..a83f9687 100644
--- a/tests/helpers/auth.test.ts
+++ b/tests/helpers/auth.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/binary-upgrade.test.ts b/tests/helpers/binary-upgrade.test.ts
index 60ab1265..bd3cee4c 100644
--- a/tests/helpers/binary-upgrade.test.ts
+++ b/tests/helpers/binary-upgrade.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, mock, beforeEach, afterEach } from "bun:test";
import {
existsSync,
diff --git a/tests/helpers/claude-settings.test.ts b/tests/helpers/claude-settings.test.ts
index 36ba425f..7288558f 100644
--- a/tests/helpers/claude-settings.test.ts
+++ b/tests/helpers/claude-settings.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, existsSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/copilot-settings.test.ts b/tests/helpers/copilot-settings.test.ts
index 0a5f67af..86eaf044 100644
--- a/tests/helpers/copilot-settings.test.ts
+++ b/tests/helpers/copilot-settings.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/credential-store.test.ts b/tests/helpers/credential-store.test.ts
index 50a7e61a..4f8a8eea 100644
--- a/tests/helpers/credential-store.test.ts
+++ b/tests/helpers/credential-store.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/cursor-settings.test.ts b/tests/helpers/cursor-settings.test.ts
index 7c48b8b8..d6b018e6 100644
--- a/tests/helpers/cursor-settings.test.ts
+++ b/tests/helpers/cursor-settings.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/doctor.test.ts b/tests/helpers/doctor.test.ts
index 7aaebc5f..ba559604 100644
--- a/tests/helpers/doctor.test.ts
+++ b/tests/helpers/doctor.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { runDoctor } from "../../src/helpers/doctor";
diff --git a/tests/helpers/editor-detect.test.ts b/tests/helpers/editor-detect.test.ts
index 41c6cc94..8ba818f3 100644
--- a/tests/helpers/editor-detect.test.ts
+++ b/tests/helpers/editor-detect.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
// ---------------------------------------------------------------------------
diff --git a/tests/helpers/exit.test.ts b/tests/helpers/exit.test.ts
index c834c3ba..02ab86d7 100644
--- a/tests/helpers/exit.test.ts
+++ b/tests/helpers/exit.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import {
diff --git a/tests/helpers/git.test.ts b/tests/helpers/git.test.ts
index 7aca5829..ac7997bb 100644
--- a/tests/helpers/git.test.ts
+++ b/tests/helpers/git.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { installGit } from "../../src/helpers/git";
diff --git a/tests/helpers/init-project.test.ts b/tests/helpers/init-project.test.ts
index 0a69ff34..1eab1dde 100644
--- a/tests/helpers/init-project.test.ts
+++ b/tests/helpers/init-project.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, existsSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/install-info.test.ts b/tests/helpers/install-info.test.ts
index c78e803b..057b6bb4 100644
--- a/tests/helpers/install-info.test.ts
+++ b/tests/helpers/install-info.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, afterEach } from "bun:test";
import {
diff --git a/tests/helpers/log.test.ts b/tests/helpers/log.test.ts
index 5372d13b..afefbbf3 100644
--- a/tests/helpers/log.test.ts
+++ b/tests/helpers/log.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { logDebug, logInfo, logError, logWarn } from "../../src/helpers/log";
diff --git a/tests/helpers/login-flow.test.ts b/tests/helpers/login-flow.test.ts
index 67bd2ba3..975d3a79 100644
--- a/tests/helpers/login-flow.test.ts
+++ b/tests/helpers/login-flow.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { runLoginFlow } from "../../src/helpers/login-flow";
diff --git a/tests/helpers/output.test.ts b/tests/helpers/output.test.ts
index b1f133be..b05fffcb 100644
--- a/tests/helpers/output.test.ts
+++ b/tests/helpers/output.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { isAgentContext, formatJSON } from "../../src/helpers/output";
diff --git a/tests/helpers/paths.test.ts b/tests/helpers/paths.test.ts
index 2e24e35d..798ba1d9 100644
--- a/tests/helpers/paths.test.ts
+++ b/tests/helpers/paths.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/platform.test.ts b/tests/helpers/platform.test.ts
index 05d3a3df..c7944001 100644
--- a/tests/helpers/platform.test.ts
+++ b/tests/helpers/platform.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import {
diff --git a/tests/helpers/plugin-install.test.ts b/tests/helpers/plugin-install.test.ts
index 709a02d6..1a314a67 100644
--- a/tests/helpers/plugin-install.test.ts
+++ b/tests/helpers/plugin-install.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import {
diff --git a/tests/helpers/project-config.test.ts b/tests/helpers/project-config.test.ts
index 7a6159b1..056e9983 100644
--- a/tests/helpers/project-config.test.ts
+++ b/tests/helpers/project-config.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { existsSync, mkdtempSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/repo-probe.test.ts b/tests/helpers/repo-probe.test.ts
index 26e6d8c3..cbc63b41 100644
--- a/tests/helpers/repo-probe.test.ts
+++ b/tests/helpers/repo-probe.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, afterEach, beforeEach } from "bun:test";
import {
diff --git a/tests/helpers/repo.test.ts b/tests/helpers/repo.test.ts
index f4dffa3a..785ff5d1 100644
--- a/tests/helpers/repo.test.ts
+++ b/tests/helpers/repo.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, afterEach, beforeEach } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/rules-shim.test.ts b/tests/helpers/rules-shim.test.ts
index 92739a4a..87f4fbb6 100644
--- a/tests/helpers/rules-shim.test.ts
+++ b/tests/helpers/rules-shim.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/sentry.test.ts b/tests/helpers/sentry.test.ts
index 78336e4b..1f5c8e32 100644
--- a/tests/helpers/sentry.test.ts
+++ b/tests/helpers/sentry.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, test, beforeEach, afterEach, mock } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/session-context-copilot.test.ts b/tests/helpers/session-context-copilot.test.ts
index 8fc8f0e8..d35abd99 100644
--- a/tests/helpers/session-context-copilot.test.ts
+++ b/tests/helpers/session-context-copilot.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
diff --git a/tests/helpers/session-context-cursor.test.ts b/tests/helpers/session-context-cursor.test.ts
index 95f4d1d1..5493d194 100644
--- a/tests/helpers/session-context-cursor.test.ts
+++ b/tests/helpers/session-context-cursor.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
diff --git a/tests/helpers/session-context-opencode.test.ts b/tests/helpers/session-context-opencode.test.ts
index 816d7b18..54c4e37c 100644
--- a/tests/helpers/session-context-opencode.test.ts
+++ b/tests/helpers/session-context-opencode.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { Database } from "bun:sqlite";
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, rmSync } from "node:fs";
diff --git a/tests/helpers/session-context.test.ts b/tests/helpers/session-context.test.ts
index 31cb2985..78fd87a0 100644
--- a/tests/helpers/session-context.test.ts
+++ b/tests/helpers/session-context.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
diff --git a/tests/helpers/signup.test.ts b/tests/helpers/signup.test.ts
index baf28b97..e34fbaa1 100644
--- a/tests/helpers/signup.test.ts
+++ b/tests/helpers/signup.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, mock } from "bun:test";
import {
diff --git a/tests/helpers/telemetry-config.test.ts b/tests/helpers/telemetry-config.test.ts
index 966d0fb1..f3f1fc80 100644
--- a/tests/helpers/telemetry-config.test.ts
+++ b/tests/helpers/telemetry-config.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/telemetry.test.ts b/tests/helpers/telemetry.test.ts
index 556e542c..2be1cd05 100644
--- a/tests/helpers/telemetry.test.ts
+++ b/tests/helpers/telemetry.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/tls.test.ts b/tests/helpers/tls.test.ts
index 9f0c097d..09a767cd 100644
--- a/tests/helpers/tls.test.ts
+++ b/tests/helpers/tls.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test } from "bun:test";
import { isTlsError, tlsHintMessage } from "../../src/helpers/tls";
diff --git a/tests/helpers/update-check.test.ts b/tests/helpers/update-check.test.ts
index d011d690..88c31753 100644
--- a/tests/helpers/update-check.test.ts
+++ b/tests/helpers/update-check.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts
index 5cbf1497..baf5f539 100644
--- a/tests/helpers/vscode-settings.test.ts
+++ b/tests/helpers/vscode-settings.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync, existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
diff --git a/tests/integration/adr-show-update.test.ts b/tests/integration/adr-show-update.test.ts
index dac1b76a..34bc6bb4 100644
--- a/tests/integration/adr-show-update.test.ts
+++ b/tests/integration/adr-show-update.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { join } from "node:path";
diff --git a/tests/integration/adr.test.ts b/tests/integration/adr.test.ts
index 90fb14d3..fe73274e 100644
--- a/tests/integration/adr.test.ts
+++ b/tests/integration/adr.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { readdirSync } from "node:fs";
import { join } from "node:path";
diff --git a/tests/integration/check.test.ts b/tests/integration/check.test.ts
index 841f0f31..1b439a38 100644
--- a/tests/integration/check.test.ts
+++ b/tests/integration/check.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
diff --git a/tests/integration/clean.test.ts b/tests/integration/clean.test.ts
index 267ffab1..bb5ee319 100644
--- a/tests/integration/clean.test.ts
+++ b/tests/integration/clean.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdirSync, writeFileSync } from "node:fs";
import { existsSync } from "node:fs";
diff --git a/tests/integration/cli-harness.ts b/tests/integration/cli-harness.ts
index 50f579b6..dbfcd6e4 100644
--- a/tests/integration/cli-harness.ts
+++ b/tests/integration/cli-harness.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* Integration test harness — runs the real CLI via Bun.spawn
* against isolated temp project directories.
diff --git a/tests/integration/cli-perf.test.ts b/tests/integration/cli-perf.test.ts
index ad6a31eb..65506660 100644
--- a/tests/integration/cli-perf.test.ts
+++ b/tests/integration/cli-perf.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* Performance regression tests for CLI startup and exit latency.
*
diff --git a/tests/integration/init.test.ts b/tests/integration/init.test.ts
index 4e2d8a21..a7722b90 100644
--- a/tests/integration/init.test.ts
+++ b/tests/integration/init.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
diff --git a/tests/integration/review-context.test.ts b/tests/integration/review-context.test.ts
index 8409ff8e..8d683379 100644
--- a/tests/integration/review-context.test.ts
+++ b/tests/integration/review-context.test.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdirSync, writeFileSync } from "node:fs";
import { mkdtempSync } from "node:fs";
diff --git a/tests/preload.ts b/tests/preload.ts
index e98b5c5a..a21e8c41 100644
--- a/tests/preload.ts
+++ b/tests/preload.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
/**
* Test preload — sets environment for all test runs.
*
diff --git a/tests/test-utils.ts b/tests/test-utils.ts
index d53483a0..cfa78f66 100644
--- a/tests/test-utils.ts
+++ b/tests/test-utils.ts
@@ -1,3 +1,5 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2026 Archgate
import { rmSync } from "node:fs";
/**