# pi-code-quality

Declarative Pi extension that automatically formats and lints files after successful `write` / `edit` tool calls.

## Why

`pi-code-quality` lets users configure formatters and linters with JSON instead of installing a separate Pi plugin for every language.

It is intentionally separate from [`pi-lsp`](https://www.npmjs.com/package/pi-lsp): formatters/linters are short-lived command-line tools, while LSP servers are long-lived JSON-RPC processes.

## Install

```bash
pi install npm:pi-code-quality
```

For local development:

```bash
pi install /absolute/path/to/pi-code-quality
```

## Configuration

Global config, trusted automatically:

```text
~/.pi/agent/code-quality.json
```

Project-local config, requires trust:

```text
.pi/code-quality.json
```

Project entries override global entries with the same `id`. A project entry with `"enabled": false` disables the global entry with that `id`.

## Trust and security

Project-local config can auto-run binaries on your machine. For that reason:

- project-local config content is hashed;
- unknown hashes prompt for `Trust once`, `Trust always`, or `Reject`;
- the prompt shows every configured auto-run binary;
- `Trust always` stores the hash in `~/.pi/agent/trust/code-quality.json`;
- changing the config changes the hash and asks again;
- non-interactive mode rejects project-local config by default;
- commands run as `bin` + `args[]`, never as shell strings.

Global config is considered trusted because it is user-owned agent configuration.

## Example config

```json
{
  "version": 1,
  "tools": [
    {
      "id": "go",
      "enabled": true,
      "include": ["**/*.go"],
      "exclude": ["vendor/**"],
      "rootMarkers": ["go.mod"],
      "formatter": {
        "bin": "gofmt",
        "args": ["-w", "{file}"],
        "cwd": "{root}",
        "timeoutMs": 25000
      },
      "linter": {
        "bin": "golangci-lint",
        "args": ["run", "--new-from-rev=HEAD", "--timeout=25s", "./{relDir}"],
        "cwd": "{root}",
        "timeoutMs": 30000,
        "diagnosticExitCodes": [1]
      }
    },
    {
      "id": "python",
      "enabled": true,
      "include": ["**/*.py"],
      "rootMarkers": ["pyproject.toml", "ruff.toml"],
      "formatter": {
        "bin": "uv",
        "args": ["run", "ruff", "format", "{file}"],
        "cwd": "{root}",
        "timeoutMs": 25000
      },
      "linter": {
        "bin": "uv",
        "args": ["run", "ruff", "check", "{file}"],
        "cwd": "{root}",
        "timeoutMs": 30000,
        "diagnosticExitCodes": [1]
      }
    },
    {
      "id": "typescript",
      "enabled": true,
      "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
      "exclude": ["node_modules/**", "dist/**"],
      "rootMarkers": ["package.json"],
      "formatter": {
        "bin": "node_modules/.bin/prettier",
        "args": ["--write", "{file}"],
        "cwd": "{root}",
        "timeoutMs": 25000
      },
      "linter": {
        "bin": "node_modules/.bin/eslint",
        "args": ["{file}"],
        "cwd": "{root}",
        "timeoutMs": 30000,
        "diagnosticExitCodes": [1]
      }
    }
  ]
}
```

## Behavior

After a successful `write` or `edit`:

1. finds tools matching `include` / `exclude`;
2. finds `{root}` using `rootMarkers`;
3. skips files above `maxFileSizeBytes`;
4. runs formatter first;
5. runs linter second;
6. appends a compact summary to the original tool result.

Lint diagnostic exit codes do **not** make `write` / `edit` fail. Diagnostics are extra context for the model. Summaries that contain issues are also sent as a user-visible, tool-styled diagnostic notice.

No slash commands are registered.

## Relative paths and placeholders

Path resolution:

1. absolute paths are used as-is;
2. relative `bin`, `config`, and `cwd` values with `/` are resolved relative to `{root}`;
3. bare binary names are resolved through `PATH`.

Placeholders:

- `{workspace}` — Pi working directory or directory containing project `.pi` config
- `{root}` — nearest directory containing one of `rootMarkers`
- `{file}` — absolute file path
- `{relFile}` — file path relative to `{root}`
- `{dir}` — absolute file directory
- `{relDir}` — file directory relative to `{root}`
- `{config}` — resolved command config path
- `{configDir}` — directory containing `{config}`

## Output example

```text
Code quality:

✅ gofmt: formatted internal/service/foo.go
✅ golangci-lint: no issues in internal/service
```

```text
Code quality:

✅ gofmt: formatted internal/service/foo.go
⚠️ golangci-lint found issues:
internal/service/foo.go:42:13: unchecked error
```

## Disable a tool

```json
{
  "version": 1,
  "tools": [{ "id": "go", "enabled": false }]
}
```

## Development

```bash
npm install
npm run verify
npm pack --dry-run
```
