> For the complete documentation index, see [llms.txt](https://gotts.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gotts.gitbook.io/docs/monorepo-infrastructure/monorepo/08-dx-ci.md).

# DX and CI

> **Part of**: [Monorepo Infrastructure PRD](/docs/monorepo-infrastructure/monorepo.md) | **Last Updated**: 2026-02-19

***

## VS Code Configuration

### `.vscode/extensions.json` (normative)

```jsonc
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss",
    "ms-vscode.vscode-typescript-next",
    "vitest.explorer",
    "juanblanco.solidity",
    "vercel.turbo-vscode",
  ],
}
```

### `.vscode/settings.json` (normative)

```jsonc
{
  // Format on save
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",

  // ESLint
  "eslint.useFlatConfig": true,
  "eslint.workingDirectories": [
    { "pattern": "packages/*" },
    { "pattern": "apps/*" },
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
  },

  // TypeScript
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,

  // Tailwind CSS
  "tailwindCSS.experimental.configFile": "apps/web/tailwind.css",

  // File associations
  "files.associations": {
    "*.css": "tailwindcss",
  },

  // Search exclusions
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true,
    "**/.turbo": true,
    "**/.next": true,
    "**/coverage": true,
    "pnpm-lock.yaml": true,
  },

  // File nesting for cleaner explorer
  "explorer.fileNesting.enabled": true,
  "explorer.fileNesting.patterns": {
    "package.json": "pnpm-lock.yaml, .npmrc, pnpm-workspace.yaml, turbo.json",
    "tsconfig.json": "tsconfig.*.json",
    ".gitignore": ".gitattributes, .prettierignore, .eslintignore",
  },
}
```

## GitHub Actions CI

### `.github/workflows/ci.yml` (normative)

```yaml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  ci:
    name: Build & Test
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Check formatting
        run: pnpm format:check

      - name: Lint
        run: pnpm lint

      - name: Type check
        run: pnpm check-types

      - name: Build
        run: pnpm build

      - name: Test
        run: pnpm test

      - name: publint
        run: |
          for pkg in packages/safe packages/vault packages/agent-proxy; do
            pnpm dlx publint "$pkg"
          done

  foundry:
    name: Solidity Tests
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1

      - name: Forge build (vault)
        working-directory: packages/vault
        run: forge build

      - name: Forge test (vault)
        working-directory: packages/vault
        run: forge test -vvv

      - name: Forge build (agent-proxy)
        working-directory: packages/agent-proxy
        run: forge build

      - name: Forge test (agent-proxy)
        working-directory: packages/agent-proxy
        run: forge test -vvv

      - name: Install Slither
        run: pip3 install slither-analyzer

      - name: Install Aderyn
        run: cargo install aderyn

      - name: Slither (vault)
        working-directory: packages/vault
        run: slither contracts/src/ --config-file contracts/slither.config.json

      - name: Slither (agent-proxy)
        working-directory: packages/agent-proxy
        run: slither contracts/src/ --config-file contracts/slither.config.json

      - name: Aderyn (vault)
        working-directory: packages/vault
        run: aderyn contracts/ --config aderyn.toml

      - name: Aderyn (agent-proxy)
        working-directory: packages/agent-proxy
        run: aderyn contracts/ --config aderyn.toml
```

**Key design decisions:**

1. **`pnpm/action-setup@v4`** — Reads `packageManager` from `package.json` for automatic version detection. No need to specify pnpm version in CI.
2. **`--frozen-lockfile`** — Fails if `pnpm-lock.yaml` is out of sync with `package.json`. Prevents accidental lockfile changes in CI.
3. **`concurrency` with `cancel-in-progress`** — Cancels redundant CI runs when new commits are pushed to the same PR.
4. **Separate `foundry` job** — Solidity tests run in parallel with TypeScript CI, not sequentially.
5. **`publint`** — Validates package.json exports configuration for all published packages.
6. **Static analysis in `foundry` job** — Slither and Aderyn run after `forge test` for both `vault` and `agent-proxy`. Zero high-severity findings required.

### `.github/workflows/release.yml` (normative)

```yaml
name: Release

on:
  push:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false

permissions:
  contents: write
  pull-requests: write
  id-token: write

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
          registry-url: "https://registry.npmjs.org"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Create Release Pull Request or Publish
        id: changesets
        uses: changesets/action@v1
        with:
          publish: pnpm release
          version: pnpm version-packages
          title: "chore(release): version packages"
          commit: "chore(release): version packages"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

**How the release workflow works:**

1. On every push to `main`, the Changesets action checks for pending changesets
2. If changesets exist, it creates a "Version Packages" PR that bumps versions and updates changelogs
3. When that PR is merged, the action publishes to npm with provenance attestation (`id-token: write`)
4. `concurrency` without `cancel-in-progress` ensures only one release runs at a time

## Knip — Unused Dependency Detection

[Knip](https://knip.dev/) finds unused files, exports, and dependencies across the monorepo.

### `knip.config.ts` (normative)

```typescript
import type { KnipConfig } from "knip";

const config: KnipConfig = {
  workspaces: {
    ".": {
      entry: ["scripts/*.ts"],
      ignoreDependencies: ["husky", "lint-staged"],
    },
    "packages/safe": {
      entry: ["src/index.ts"],
      ignoreDependencies: [],
    },
    "packages/vault": {
      entry: ["src/index.ts", "sdk/src/index.ts"],
      ignoreDependencies: [],
    },
    "packages/agent-proxy": {
      entry: ["src/index.ts", "sdk/src/index.ts"],
      ignoreDependencies: [],
    },
    "packages/ui": {
      entry: ["src/index.ts"],
      ignoreDependencies: [],
    },
    "apps/web": {
      entry: ["src/app/**/*.{ts,tsx}"],
      ignoreDependencies: [],
    },
  },
  ignore: ["**/*.d.ts", "**/*.config.{ts,js,mjs}"],
};

export default config;
```

### Running Knip

```bash
# Full analysis
pnpm knip

# Show only unused exports
pnpm knip --include exports

# Show only unused dependencies
pnpm knip --include dependencies
```

## Syncpack — Version Consistency

[Syncpack](https://jamiemason.github.io/syncpack/) ensures consistent dependency versions across workspaces.

### `syncpack.config.ts` (normative)

```typescript
import type { RcFile } from "syncpack";

const config: RcFile = {
  sortFirst: [
    "name",
    "version",
    "private",
    "type",
    "description",
    "license",
    "repository",
    "exports",
    "main",
    "types",
    "files",
    "scripts",
    "dependencies",
    "devDependencies",
    "peerDependencies",
    "engines",
    "publishConfig",
  ],
  versionGroups: [
    {
      label: "Use workspace protocol for internal packages",
      dependencies: ["@gotts.ai/*"],
      dependencyTypes: ["dev", "prod"],
      pinVersion: "workspace:*",
    },
  ],
  semverGroups: [
    {
      label: "All dependencies use caret ranges",
      range: "^",
      dependencies: ["**"],
      packages: ["**"],
    },
  ],
};

export default config;
```

### Running Syncpack

```bash
# Check for mismatches
pnpm syncpack:check

# Auto-fix mismatches
pnpm syncpack:fix

# Sort package.json fields
pnpm dlx syncpack format
```

## Renovate — Automated Dependency Updates

[Renovate](https://docs.renovatebot.com/) creates PRs for dependency updates automatically.

### `.github/renovate.json5` (normative)

```json5
{
  $schema: "https://docs.renovatebot.com/renovate-schema.json",
  extends: [
    "config:recommended",
    ":semanticCommits",
    ":automergeMinor",
    "group:monorepos",
    "group:recommended",
  ],
  labels: ["dependencies"],
  rangeStrategy: "bump",
  packageRules: [
    {
      description: "Group all non-major updates together",
      matchUpdateTypes: ["minor", "patch"],
      groupName: "all non-major dependencies",
      groupSlug: "all-minor-patch",
      schedule: ["before 6am on Monday"],
    },
    {
      description: "Auto-merge dev dependencies",
      matchDepTypes: ["devDependencies"],
      matchUpdateTypes: ["minor", "patch"],
      automerge: true,
    },
    {
      description: "Group Foundry updates",
      matchPackageNames: ["foundry-rs/**"],
      groupName: "foundry",
    },
  ],
  ignoreDeps: [],
}
```

**Key behaviors:**

1. **Weekly grouped PRs** — Minor and patch updates are grouped into a single weekly PR (Monday mornings)
2. **Auto-merge dev deps** — Dev dependency patches/minors merge automatically after CI passes
3. **Semantic commits** — PR titles follow conventional commit format (`chore(deps): ...`)
4. **Major updates** — Always separate PRs, never auto-merged

## Clean Scripts

Each package includes a `clean` script for removing build artifacts:

```jsonc
// Per-package
{
  "scripts": {
    "clean": "rm -rf dist .turbo node_modules/.cache",
  },
}
```

Root-level clean commands:

```bash
# Clean all build artifacts (keeps node_modules)
pnpm clean

# Clean Turborepo cache only
pnpm clean:turbo

# Nuclear clean (removes node_modules — requires re-install)
pnpm clean && rm -rf node_modules && pnpm install
```

## Development Workflow

### First-Time Setup

```bash
# 1. Enable Corepack (ensures correct pnpm version)
corepack enable

# 2. Install dependencies
pnpm install

# 3. Build all packages
pnpm build

# 5. Run tests to verify setup
pnpm test
```

### Daily Development

```bash
# Start dev servers (all packages in watch mode)
pnpm dev

# Start specific package dev
pnpm turbo run dev --filter=@gotts.ai/safe

# Run tests in watch mode
cd packages/safe && pnpm vitest

# Lint and format
pnpm lint
pnpm format
```

### Before Committing

Pre-commit hooks run automatically (lint-staged + commitlint). For manual checks:

```bash
pnpm format:check
pnpm lint
pnpm check-types
pnpm test
```
