Metadata-Version: 2.4
Name: misscat
Version: 1.7.0
Summary: Watch a GitHub repository and run an AI review on every new PR HEAD (plus BadCat, a review-state watcher with opt-in merge, and badcat-host, its read-only Native Messaging host)
Author: KJ Kim
License-Expression: MIT
Project-URL: Homepage, https://github.com/genonfire/misscat
Project-URL: Repository, https://github.com/genonfire/misscat
Project-URL: Issues, https://github.com/genonfire/misscat/issues
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0
Requires-Dist: prompt_toolkit<4,>=3.0
Dynamic: license-file

# MissCat
[![PyPI](https://img.shields.io/pypi/v/misscat.svg)](https://pypi.org/project/misscat/)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/misscat?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/misscat)

**Never miss a single commit.**

> *I’ll catch ’em all, meow.*

MissCat is a small CLI that watches a GitHub repository and automatically runs an AI code review whenever a new pull request or commit appears.

It watches the **repository**, not a single PR.

## Why MissCat?

AI reviewers are useful, but somebody still has to notice that a PR changed and ask them to review it again.

MissCat does that part.

```text
new PR
  → review

new commit
  → review again

nothing changed
  → do nothing
```

No webhook server.  
No API keys stored by MissCat.  
No repeated review of the same commit with the same profile.

## Prerequisites

Before running MissCat, ensure you have:

- **Python**: Python 3.9 or newer
- **Git**: Configured with credentials to clone and fetch the target repository (HTTPS users using GitHub CLI can run `gh auth setup-git`)
- **GitHub CLI (`gh`)**: Installed and authenticated (`gh auth login`)
- **At least one AI reviewer CLI**: Installed and authenticated:
  - [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code) (`claude`)
  - [Codex CLI](https://github.com/openai/codex) (`codex`)
  - [Antigravity CLI](https://github.com/google/antigravity) (`agy`) for Gemini

## Installation

Install MissCat with [pipx](https://pypa.github.io/pipx/):

```bash
pipx install misscat
```

To upgrade MissCat to the latest version:

```bash
pipx upgrade misscat --pip-args="--no-cache-dir"
```

## Releasing (maintainers)

Releases are published to PyPI by `.github/workflows/publish.yml` when a `vMAJOR.MINOR.PATCH` tag is pushed. It uses PyPI [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC), so no PyPI token is stored in GitHub secrets.

### One-time setup

1. On PyPI, add a trusted publisher for the `misscat` project (Project → Publishing; for a first release use "Add a pending publisher"):
   - Owner: `genonfire`
   - Repository: `misscat`
   - Workflow name: `publish.yml`
   - Environment name: `pypi`
2. In the GitHub repository, create an environment named `pypi` (Settings → Environments). Recommended: add required reviewers so every release needs a manual approval, and add a tag ruleset (Settings → Rules) so only maintainers can create `v*` tags. A deployment tag rule alone is not enough, since it matches the tag name, not the commit.

### Release procedure

1. Check that the version is not already on PyPI (versions are immutable and cannot be re-uploaded): <https://pypi.org/project/misscat/#history>
2. Bump `version` in `pyproject.toml` **and** `__version__` in `src/misscat/__init__.py` (the workflow fails if they differ), then merge to `master`.
3. Tag the merge commit and push the tag (the tag must equal `v` + the `pyproject.toml` version):

   ```bash
   git tag v1.0.1
   git push origin v1.0.1
   ```

4. The workflow fails before publishing if the tagged commit is not on `master`, the tag is malformed, the tag does not match `pyproject.toml` or `__version__`, or `python -m build` / `twine check` fails.
5. Confirm the release:

   ```bash
   pipx upgrade misscat
   misscat --version
   pip index versions misscat
   ```

## Quick Start / First Run

On the first normal run, if `~/.config/misscat/` does not exist, MissCat creates it and copies the bundled reviewer profiles (`luna.yml`, `sol.yml`, `sonnet.yml`, `opus.yml`, `gemini.yml`, `opencode.yml`) into it before loading your profile. Edit them freely. If the directory already exists, nothing is copied automatically, so existing configs are preserved across upgrades. `--help` and `--version` never touch the filesystem.

Watch a repository and review new PRs using the default profile:

```bash
misscat <owner/repo>
```

Example:

```bash
misscat genonfire/misscat
```

Use a specific reviewer profile:

```bash
misscat genonfire/misscat sol
```

MissCat resolves `sol` as:

```text
~/.config/misscat/sol.yml
```

Running `misscat` with no arguments (or `misscat --help`) shows usage help:

```bash
misscat --help
```

Repository names are canonicalized to lowercase, so `Genonfire/MissCat` and `genonfire/misscat` refer to the same local workspace and state.

### Log output and `--loud`

By default MissCat logs review status only: review start/done/failed with elapsed time and `[cat-N]`, reviewer progress and usage (`reviewer: working`, `reviewer: turn done (input=…, output=…)`, `reviewer: claude done (turns=…, cost=…)`), the `nothing to review …` polling lines, and every warning and error. Failed reviewer tool activity (`exec: failed`, a non-zero exit code, a failed MCP or Claude tool call) is a warning and is always shown.

```bash
misscat . sol --cat 2 --loud
```

`--loud` additionally shows successful reviewer tool activity (`exec: …`, `exec: completed (0)`, MCP calls, web searches, Claude `tool: …` lines, Gemini tool steps) and raw, non-JSON reviewer CLI output at `DEBUG`.

### Concurrent reviews: `--cat N`

```bash
misscat . sol --cat 3
```

`--cat N` (default 1) runs up to **N** reviews at the same time, each in its own persistent `cat-N` worktree. The cats are independent:

- A finished review is verified against GitHub and recorded **immediately**, without waiting for the other cats (`review done in … [cat-K]`).
- New work is assigned **only at the regular poll**. A cat that just finished stays idle until the next poll; a completion neither lists PRs nor moves the next poll time.
- The poll uses the `active` schedule while at least one cat is reviewing and the `idle` schedule when none is; changing between the two restarts the schedule.
- A PR that is being reviewed is not assigned again, even if its HEAD changes meanwhile; the new HEAD is picked up by the first poll after the run was recorded.
- A HEAD whose review failed is not retried for 5 minutes, and then goes after the other waiting PRs of its priority. A failed workspace sync delays the next poll by at least 5 minutes without penalizing any HEAD; running cats are unaffected.

### Using `.` for the current repository

`.` **always means the GitHub repository of the current Git checkout's `origin` remote**; `upstream` and other remotes are never used.

```bash
misscat .           # watch origin's repository with the default profile
misscat . luna      # same repository, luna profile
misscat state .     # manage that repository's review state
```

MissCat prints `resolved . -> owner/repo` and then behaves exactly as if you had typed that name (same state, lock and workspace). It works from subdirectories and worktrees, and understands `git@github.com:owner/repo.git`, `https://github.com/owner/repo(.git)` and `ssh://git@github.com/owner/repo(.git)`. It fails, before touching any state, when outside a Git repository, when `origin` is missing, or when `origin` is not on `github.com` (SSH host aliases are not interpreted).

## Bundled profiles: `misscat init`

```bash
misscat init          # create the config dir if needed; copy only missing bundled profiles
misscat init --force  # overwrite files named like bundled profiles with fresh copies
```

- `init` never modifies existing files. `init --force` replaces only `luna.yml`, `sol.yml`, `sonnet.yml`, `opus.yml`, `gemini.yml` and `opencode.yml`; custom profiles, per-repository state JSONs and the review cache are never touched.
- After upgrading MissCat, new or updated bundled profiles are not applied automatically: run `misscat init` for new ones, or `misscat init --force` to reset the bundled ones (this discards your edits to them).
- If the automatic first-run copy is interrupted, `misscat init` restores the missing profiles.
- `default.yml` is the internal base configuration and is not copied. Each bundled profile carries its own editable copy of the default `prompt`, so you can customize a reviewer's prompt in place; `default.yml` remains the fallback.

## Review state manager

Stop the watcher before opening the terminal state manager:

```bash
misscat state genonfire/typewriter
```

The list shows the most recently completed reviews first, across all PRs and profiles (sorted by `reviewed_at`; the stored State order is unchanged). Use **↑/↓** to select any completed review, **Enter** for its full HEAD SHA, profile, and local review completion time, and **Esc/Q** to exit. **Delete** or **Backspace** only operates on the *most recently reviewed HEAD of that PR* (by `reviewed_at`), with a `[y/N]` prompt that names how many records (one per profile that recorded the HEAD) are removed; reply history is kept. A HEAD with an unsettled review attempt cannot be removed until the watcher has settled it. Older HEADs are read-only until newer ones in that PR have been removed. Nothing is deleted on Enter or when you decline confirmation.

The manager works from local State JSON; it does not call GitHub. Removing a HEAD's records (all profiles) makes that PR/HEAD eligible for review again, **but only when the HEAD is the PR's current open HEAD** at the next watcher run. Removing historical HEADs does not trigger review of historical commits.

### State v2 and process safety

- Every successful review now stores `reviewed_at` as an ISO 8601 UTC timestamp. Display uses local time. The review identity is PR + HEAD (the row `profile` is metadata only); the JSON sorting convention is unchanged.
- **Breaking state reset:** **stop all previously running MissCat watchers before upgrading to this version**; older watchers do not participate in the new OS locking scheme and may restore v1 records from memory. On first State access after upgrade, any **v1** reviewed history is intentionally discarded and the file is rewritten as an empty **v2** State. Consequently, open PR HEADs previously marked complete become eligible for a new AI review. This may consume additional reviewer tokens. Unknown versions and corrupt State files cause errors instead of being discarded.
- A per-repository OS file lock prevents a watcher and a state manager—or two watchers—from running on the same repository simultaneously, even with different profiles. Stop the watcher before editing state. The lock file lives under `~/.cache/misscat/locks/` (never in the config directory) and may remain on disk after exit; the OS releases its lock when the process exits. Do not use the file's existence as an indication of an active watcher.
- State manager needs an interactive TTY. It does not touch profile YAML files or cache workspaces.

## Configuration

MissCat ships with a bundled `default.yml` inside the package, so installed copies do not depend on the source checkout or current working directory.

User configuration and persistent data are kept entirely outside the package:

- **Profiles**: `~/.config/misscat/<profile>.yml`
- **Per-repository state**: `~/.config/misscat/<owner>__<repo>.json`
- **Persistent workspace**: `~/.cache/misscat/repos/<owner>/<repo>/`

Directory layout:

```text
~/.config/misscat/
├── sol.yml
├── sonnet.yml
├── opus.yml
├── genonfire__typewriter.json
└── genonfire__misscat.json
```

Example `default.yml`:

```yaml
reviewer:
  # provider: claude
  # model: claude-sonnet-5-5
  # args:
  #   - --allowedTools
  #   - "Bash(gh *)"

  provider: codex
  model: gpt-6-luna
  args:
    - --sandbox
    - workspace-write
    - -c
    - sandbox_workspace_write.network_access=true
    - -c
    - apps._default.enabled=false
    - -c
    - model_reasoning_effort=max

prompt: |
  Read REVIEW.md if exist and act as the 1st reviewer.
  Use the gh CLI for GitHub operations, including posting the review.

watch:
  idle: [60, 120, 180, 240, 300]
  active: [300, 240, 180, 120, 60]

review:
  include_drafts: true
```

Example `~/.config/misscat/sol.yml`:

```yaml
reviewer:
  provider: codex
  model: gpt-6.1-sol
  args:
    - --sandbox
    - workspace-write
    - -c
    - sandbox_workspace_write.network_access=true
    - -c
    - apps._default.enabled=false
    - -c
    - model_reasoning_effort=medium
```

The selected local profile overrides the bundled defaults.

When creating a local profile, use `default.yml` as the reference and specify `provider`, `model`, and `args` explicitly for that reviewer CLI. Do not rely on `args` inherited from a different provider.

The `args` field under `reviewer` is passed to the selected reviewer CLI unchanged.

Reviewed state is stored per repository and keyed by PR and HEAD SHA, whichever profile ran the review (the profile is kept in each record as metadata only). A HEAD reviewed with `luna` is therefore not reviewed again by `sol` or `opencode`; a new HEAD is.

### Gemini (Antigravity CLI)

Allow `gh` and `git` in `~/.gemini/antigravity-cli/settings.json` (keep your other settings):

```json
{
  "toolPermission": "proceed-in-sandbox",
  "enableTerminalSandbox": true,
  "permissions": { "allow": ["command(gh)", "command(git)"] }
}
```

```yaml
reviewer:
  provider: gemini
  model: gemini-3.8-flash-low
  args: [--print-timeout, 30m]
```

In headless mode a denied permission can end the run without posting a review; use `--loud` to inspect.

### OpenCode

`opencode` is a regular provider. Run it with your already configured [OpenCode](https://opencode.ai/docs/cli/) CLI like any other profile (`opencode.yml` is bundled and installed by `misscat init`). It targets the **OpenCode native V2 v2.0.32** interface (V1 flags and events are not supported).

```bash
misscat . opencode
misscat owner/repo opencode --cat 2 --loud
```

`~/.config/misscat/opencode.yml` is read by MissCat itself (not an OpenCode-native config); it has the usual `reviewer` (`provider: opencode`, `model`, `args`) and `prompt` fields, and MissCat runs, inside the exact-HEAD `cat-N` worktree:

```text
opencode run --standalone --format json <reviewer.args> --model <reviewer.model> <prompt>
```

Every review starts a fresh OpenCode session, `--standalone` gives each run a private OpenCode server (never your managed background one), and the child gets `PWD` set to the worktree. `reviewer.model` is passed unchanged as `provider/model#variant` (default `openai/gpt-6-luna#xhigh`; the variant must exist in your OpenCode).

- **Completion:** same as every provider: a submitted `COMMENT` review with the verdict, the exact HEAD and the run's `MissCat-Run:` line, never exit status alone. A review submitted before OpenCode fails is still settled.
- **Output:** compact progress, tool failures and, when OpenCode reports them, token usage and a cost estimate (summed per step; missing data is omitted, never shown as zero).
- **Fatal errors:** clearly non-retryable errors (`provider.auth`, `provider.no-route`) assign nothing more, let the reviews already running finish and be recorded, then stop the watch with exit code 2 and advice (`opencode auth login`, or check `reviewer.model` in the profile). Other failures follow the normal retry wait.

### Upgrading to 1.7.0

- `--cat N` still runs up to N reviews concurrently, but reviews are now independent: each result is recorded as soon as it finishes, and idle cats receive new work at the next regular poll. The batch barrier has been removed (see [Concurrent reviews](#concurrent-reviews---cat-n)). Profiles, state files, locks and worktree paths are unchanged.

- The separate `mscat` command is gone: use `misscat . opencode`. Run `misscat init` once to install `opencode.yml` (an existing file is never overwritten). Your edited `~/.config/misscat/opencode.yml` keeps working unchanged.
- A review is now complete per PR + HEAD, not per profile. Existing State v2 files are read as they are: records written under any profile (including `mscat/opencode`) count, so switching profiles does not review the same HEAD again. Nothing in the files is rewritten or discarded.

## BadCat

`pipx install misscat` also installs `badcat`, a deterministic watcher of submitted PR reviews (no AI). By default it only reports; it merges (squash) only with `--merge`.

```bash
badcat owner/repo                          # report only, no GitHub writes
badcat . --merge                           # merge after +1 (same as --merge=1)
badcat . --merge=2                         # merge after +1 then +2 (batch-labeled PRs at +1)
badcat . --merge --trusted-reviewer luna   # trust reviews by this login (default: your gh user)
```

A PR labeled exactly `no-review` is never eligible: BadCat reports no "ready to merge" for it and never merges it, whatever its reviews or `batch`/`urgent` labels. Labels are re-read from GitHub just before a merge, so adding `no-review` after the PR was listed still blocks the merge; removing the label restores eligibility. The read-only `badcat-host` Chrome host likewise emits no `+1` event for a `no-review` PR (like a draft), and emits one again once the label is removed. MissCat skips `no-review` PRs too.

A review counts only if it is a submitted `COMMENTED` review by a trusted login whose body starts with:

```text
+1
HEAD: <40-hex sha of the PR's current HEAD>

<review evidence>
```

## What MissCat does

- Watches all open PRs in a repository
- Discovers PRs created after MissCat starts
- Detects changes by PR HEAD SHA
- Reviews every new PR HEAD once, whichever profile ran it
- Avoids duplicate reviews
- Remembers reviewed commits across restarts
- Uses an isolated persistent review workspace
- Supports multiple reviewer backends

MissCat does **not** modify code, push commits, or merge PRs.

It watches. It catches. It reviews.

(BadCat is the separate, opt-in merge operator described above.)
