# Pygienium

Code hygiene for [pi](https://github.com/earendil-works/pi-coding-agent) — isolated
sub-agent checks that scan a target, apply fixes, and emit a findings + changes
report. Inspired by piolium's sub-agent loops.

Pygienium runs **highly-structured hygiene passes** over a repo to clean up the
common quality issues LLM-generated code accumulates: restating comments,
shallow pass-through modules, dead exports/files, and redundant defensive
guards. Each check is an isolated, resumable sub-agent run whose progress lands
in a single inspectable run-state file.

## Install

```sh
pi install npm:@mikefreno/pygienium
```

On load it emits a TUI notification `Pygienium loaded. Run /pygienium-help for available checks and flags.` (only when a dialog-capable UI is available).

## Configuration

Pygienium reads its chat-rendering style from pi's `settings.json` (`~/.pi/agent/settings.json`) under a `pygienium` key:

```json
{
  "pygienium": {
    "chatStyle": "verbose"
  }
}
```

| Setting | Default | Values | Description |
| --- | --- | --- | --- |
| `pygienium.chatStyle` | `"verbose"` | `"verbose"` \| `"compact"` | Chat rendering for sub-agent tool calls. **verbose** (piolium-style) streams each tool event live as its own chat line (`[Comments: Scanning] → bash ...` / `← (ok)`). **compact** (ralpi-style) suppresses the per-event stream and shows only the final completion message with its expandable phase tree. |

No entry means `"verbose"` (the default). An unreadable or missing `settings.json` also falls back to `"verbose"`.

Environment variable (read at run time):

| Env var | Default | Description |
| --- | --- | --- |
| `PYGIENIUM_AGENT_TIMEOUT_MS` | `3600000` (60 min) | Per-agent-phase settle deadline. A sub-agent session that never settles (stalled provider stream, hung retry/auto-compaction after its last tool call) is aborted and the phase fails with a visible error instead of hanging the run mid-transition with no state update and no completion message. Lower it (e.g. `1200000`) to fail fast on flaky providers; raise it for very large repos.

## Commands

Every command accepts a `[path]` target (default: the current directory) and is
resumable — progress is persisted to `<cwd>/.pygienium/run-state.json`.

| Command | What it does |
| --- | --- |
| `/pygienium-help` | Print every command, shipped check, and flag. |
| `/pygienium-<check> [path] [--fix]` | Run one isolated sub-agent that scans a target, applies fixes with `--fix`, and emits a findings + changes report. |
| `/pygienium-all [path] [--fix]` | Run every registered check in sequence under one resumable run-state. |
| `/pygienium-status [path]` | Show per-check progress, artifact line counts, and errors for the latest run. |
| `/pygienium-resume [path] [--fresh]` | Resume the latest in-progress/failed/partial run; complete/skipped checks skip unless `--fresh`. |
| `/pygienium-export [path] [--check=] [--status=] [--out=md\|json]` | Bundle every check's `findings.md` + `changes.md` into `pygienium/export.{md\|json}`. |

## Flags

| Flag | Scope | Description |
| --- | --- | --- |
| `[path]` | all check commands | Target file or directory to scan (default: current dir). |
| `--fix` | `<check>`, `all`, `resume` | Apply fixes (default: scan-only; emits findings only). |
| `--fresh` | `resume` | Re-dispatch completed checks too — reset their run-state entries and re-run. |
| `--check=` | `export` | Comma-separated check names to include in the bundle. |
| `--status=` | `export` | Comma-separated statuses to include (e.g. `complete,failed,skipped`). |
| `--out=` | `export` | Bundle format: `md` (default) or `json`. |

## Checks

The five shipped checks live in [`src/checks/`](./src/checks/) and self-register
on load. `/pygienium-help` lists whichever checks are currently registered, so
this table and the live help always agree on the registered set.

| Command | Check | Agent | What it fixes |
| --- | --- | --- | --- |
| `/pygienium-comments` | comments | `scanner` / `fixer` | Remove low-value/restating comments, tighten verbose ones, keep "why" comments. |
| `/pygienium-deep-modules` | deep-modules | `deep-modules` | Detect shallow modules (pass-throughs, trivial wrappers, re-export barrels) and consolidate the safe ones. |
| `/pygienium-dead-code` | dead-code | `scanner` / `fixer` | Find unreferenced exports, dead files, obsolete compat shims, migration helpers, and unused dependencies; remove clearly-dead items and flag dynamic ones. |
| `/pygienium-defensive-guards` | defensive-guards | `defensive-guards` | Remove redundant defensive guarding (null checks on non-nullable types, swallowing try/catch, masking fallbacks) while keeping boundary guards (IO, parsing, untrusted input). |
| `/pygienium-todos` | todos | `todos` / `fixer` | Inventory TODO/FIXME markers and stub implementations; with `--fix`, convert silent stubs (placeholder returns, empty bodies) into loud failures — never implementing TODOs or deleting markers. |

## Artifacts

Every check writes its reports under `<cwd>/.pygienium/checks/<name>/` (run
state lives at `<cwd>/.pygienium/run-state.json`):

- `findings.md` — what the scan found (per-file line refs).
- `changes.md` — what the fix phase changed + anything deferred for human review.

`/pygienium-export` merges every check's artifacts into one
`.pygienium/export.md` (or `export.json`).

On first run in a git work tree, pygienium appends `.pygienium/` to the
target repo's `.gitignore` so a run never stages its own state/artifacts into
git (opt out with `--no-gitignore`).

## Adding a check

One file + one `registerCheck()` call. **No `index.ts` command-wiring changes.**
`index.ts` auto-discovers every `checks/*.ts` (except the registry barrel) at
startup, so a new file self-registers and `/pygienium-<name>` appears
automatically.

1. Create `src/checks/<name>.ts` from the template below.
2. Edit the `name`, `label`, `description`, the rubric in the scan/fix task
   builders, and the `gate` precondition.
3. Keep the trailing `registerCheck(<name>Check)`. Done.

```ts
import { registerCheck, type CheckScope } from "./registry.js";

export const myCheck = {
  name: "my-check",
  label: "My check",
  description: "What it fixes (shown in /pygienium-help).",
  agentName: "scanner",       // reuse a shipped agent, or add agents/<name>.md
  fixAgentName: "fixer",
  phaseId: "my-check",
  buildScanTask: (_cwd: string, scope: CheckScope) => `# Task: my-check scan\n…`,
  buildFixTask: (_cwd: string, scope: CheckScope, findings: string) => `# Task: my-check fix\n…`,
  gate: (cwd: string) => undefined,
} as const;

registerCheck(myCheck);
```

Reload pi (or `/reload`) and run `/pygienium-help` — `/pygienium-my-check` is
listed and runnable. A `CheckDefinition` supplies the task builders and gate;
the generic check-runner wires the phases together, so a new check never
touches command plumbing.

## Architecture

Each check is a "mode" running a fixed phase pipeline:

```
registerCheck(def)            ← checks/*.ts self-register on load
        │
        ▼
/pygienium-<check> ─► runCheck(def)        (src/modes/check-runner.ts)
        │
        ├─ Q0 recon (shared, run once per run)   (src/recon.ts)
        │     git state + source-file inventory → .pygienium/recon.json
        ├─ analysis sub-agent  (buildScanTask)    ← scanner/<check> agent
        │     writes .pygienium/checks/<name>/findings.md
        ├─ fix sub-agent       (buildFixTask)     ← fixer, only with --fix
        │     writes .pygienium/checks/<name>/changes.md
        ├─ verify gate         (re-runs check.gate)
        └─ cleanup             (drops transient scratch artifacts)
```

- **Sub-agents** are isolated in-memory `AgentSession`s scoped to the target
  `cwd` (see `src/agent-runner.ts`), with the agent definition's system prompt
  and tool allowlist applied. Agent definitions are plain editable markdown in
  [`agents/`](./agents/) (frontmatter `name` + `allowedTools`, body = system
  prompt) — tuning a sub-agent never needs TypeScript changes. A scanned
  project can ship its own `agents/*.md` at its root: those are loaded as
  overrides (repo agent wins on name collision), so teams can tune prompts or
  add project-specific agents without touching the extension.
- **Run-state** is a single JSON file at `<cwd>/.pygienium/run-state.json`
  (`src/run-state.ts`): per-check phase progress, captured findings/changes
  text, and recon status. `/pygienium-status`, `/pygienium-resume`, and
  `/pygienium-export` are pure reads over it; `/pygienium-all` shares one
  `RunState` across every check so phases accumulate in one record.
- **The registry** (`src/checks/registry.ts`) is the extensibility seam: a
  module-level `Map` of `CheckDefinition`s. `index.ts` iterates it and binds
  one `/pygienium-<name>` command per entry, so adding a check is a file +
  one `registerCheck()` line.
- **The footer** (`src/footer.ts`) is the piolium-style pipeline-overview
  status strip: a single static line in the TUI footer (via
  `ui.setStatus(key, text)`) listing the full ordered pipeline with the cursor
  on the current phase and what's to come. For a single check the items are
  the phases; for `/pygienium-all` they're the checks (the full todo list),
  and the per-check footer is suppressed so two overviews never compete over
  the same status slot. The chat widget (`phases.ts`) remains the animated
  detail view (spinner + tool-call tree + completion tree); the footer is the
  overview — the two never overlap. In print/JSON mode the footer is a no-op.

## Layout

```
pygienium/
├─ src/
│  ├─ index.ts            ← entry: auto-discover checks, bind commands
│  ├─ commands.ts          ← slash-command handlers (thin binders)
│  ├─ help.ts              ← COMMANDS + CLI_FLAGS → /pygienium-help output
│  ├─ agent-runner.ts      ← isolated sub-agent sessions (injectable for tests)
│  ├─ agents.ts            ← markdown agent-definition loader
│  ├─ recon.ts             ← shared Q0 reconnaissance snapshot
│  ├─ run-state.ts         ← persistent, resumable run-state model
│  ├─ status.ts            ← /pygienium-status formatter (pure)
│  ├─ export.ts            ← /pygienium-export gatherer + md/json renderer
│  ├─ phases.ts            ← live chat progress widget + completion-tree helpers
│  ├─ footer.ts            ← pipeline-overview status strip (TUI footer)
│  ├─ modes/check-runner.ts ← the per-check phase pipeline
│  └─ checks/              ← one file per check, self-registering
│     ├─ registry.ts       ← CheckDefinition + registerCheck
│     ├─ comments.ts  deep-modules.ts  dead-code.ts
│     ├─ defensive-guards.ts
└─ agents/                 ← scanner.md  fixer.md  deep-modules.md  defensive-guards.md
```

## Development

The omp port at `~/.omp/agent/extensions/pygienium` is regenerated **only by CI
on push** (`.gitea/workflows/port-to-omp.yml`) — never by hand.

The source repo's own `tsconfig.json` extends the host harness tsconfig, so the
reliable typecheck target is the regenerated port (self-contained tsconfig +
pinned `@oh-my-pi` SDK devDependency). A committed pre-commit hook runs exactly
what CI runs — regenerate the port into a temp dir and `tsc --noEmit` it — and
fails the commit on any error:

```sh
git config core.hooksPath .githooks
```

## License

MIT
