regenerate omp port from pi base via port-to-omp
Checks now self-register through the static barrel (src/checks/all.ts) like the pi base: the ?mtime registry-split workaround is gone, check modules and tests are byte-identical to the base, and registration happens in one module graph instance under omp's extension loader.
This commit is contained in:
41
README.md
41
README.md
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
Code hygiene for [omp](https://github.com/oh-my-pi) — isolated
|
Code hygiene for [omp](https://github.com/oh-my-pi) — isolated
|
||||||
sub-agent checks that scan a target, apply fixes, and emit a findings + changes
|
sub-agent checks that scan a target, apply fixes, and emit a findings + changes
|
||||||
report. Port of the pi extension (was `~/.pi/agent/extensions/pygienium/`);
|
report. Inspired by piolium's sub-agent loops.
|
||||||
inspired by piolium's sub-agent loops.
|
|
||||||
|
|
||||||
Pygienium runs **highly-structured hygiene passes** over a repo to clean up the
|
Pygienium runs **highly-structured hygiene passes** over a repo to clean up the
|
||||||
common quality issues LLM-generated code accumulates: restating comments,
|
common quality issues LLM-generated code accumulates: restating comments,
|
||||||
@@ -70,9 +69,9 @@ resumable — progress is persisted to `<cwd>/.pygienium/run-state.json`.
|
|||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
The five shipped checks live in [`src/checks/`](./src/checks/) and are
|
The five shipped checks live in [`src/checks/`](./src/checks/) and self-register
|
||||||
auto-discovered on load. `/pygienium-help` lists whichever checks are currently
|
on load. `/pygienium-help` lists whichever checks are currently registered, so
|
||||||
registered, so this table and the live help always agree on the registered set.
|
this table and the live help always agree on the registered set.
|
||||||
|
|
||||||
| Command | Check | Agent | What it fixes |
|
| Command | Check | Agent | What it fixes |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
@@ -99,27 +98,20 @@ git (opt out with `--no-gitignore`).
|
|||||||
|
|
||||||
## Adding a check
|
## Adding a check
|
||||||
|
|
||||||
One file exporting a `check` definition. **No `index.ts` command-wiring changes.**
|
One file + one `registerCheck()` call. **No `index.ts` command-wiring changes.**
|
||||||
`index.ts` auto-discovers every `checks/*.ts` (except the registry barrel) at
|
`index.ts` auto-discovers every `checks/*.ts` (except the registry barrel) at
|
||||||
startup, registers each file's `check` export, and `/pygienium-<name>` appears
|
startup, so a new file self-registers and `/pygienium-<name>` appears
|
||||||
automatically.
|
automatically.
|
||||||
|
|
||||||
Check files are **pure data modules** — they export a definition and never
|
|
||||||
import the registry at runtime. Registration happens in `index.ts` from the
|
|
||||||
entry's own registry instance, which keeps a single registry even under omp's
|
|
||||||
extension loader (it cache-busts lazily imported graph modules with an
|
|
||||||
`?mtime` suffix, which would otherwise split the registry into two module
|
|
||||||
instances).
|
|
||||||
|
|
||||||
1. Create `src/checks/<name>.ts` from the template below.
|
1. Create `src/checks/<name>.ts` from the template below.
|
||||||
2. Edit the `name`, `label`, `description`, the rubric in the scan/fix task
|
2. Edit the `name`, `label`, `description`, the rubric in the scan/fix task
|
||||||
builders, and the `gate` precondition.
|
builders, and the `gate` precondition.
|
||||||
3. Export it as `check`. Done.
|
3. Keep the trailing `registerCheck(<name>Check)`. Done.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import type { CheckDefinition, CheckScope } from "./registry.js";
|
import { registerCheck, type CheckScope } from "./registry.js";
|
||||||
|
|
||||||
export const check = {
|
export const myCheck = {
|
||||||
name: "my-check",
|
name: "my-check",
|
||||||
label: "My check",
|
label: "My check",
|
||||||
description: "What it fixes (shown in /pygienium-help).",
|
description: "What it fixes (shown in /pygienium-help).",
|
||||||
@@ -129,7 +121,9 @@ export const check = {
|
|||||||
buildScanTask: (_cwd: string, scope: CheckScope) => `# Task: my-check scan\n…`,
|
buildScanTask: (_cwd: string, scope: CheckScope) => `# Task: my-check scan\n…`,
|
||||||
buildFixTask: (_cwd: string, scope: CheckScope, findings: string) => `# Task: my-check fix\n…`,
|
buildFixTask: (_cwd: string, scope: CheckScope, findings: string) => `# Task: my-check fix\n…`,
|
||||||
gate: (cwd: string) => undefined,
|
gate: (cwd: string) => undefined,
|
||||||
} as const satisfies CheckDefinition;
|
} as const;
|
||||||
|
|
||||||
|
registerCheck(myCheck);
|
||||||
```
|
```
|
||||||
|
|
||||||
Reload omp (or `/reload`) and run `/pygienium-help` — `/pygienium-my-check` is
|
Reload omp (or `/reload`) and run `/pygienium-help` — `/pygienium-my-check` is
|
||||||
@@ -142,7 +136,7 @@ touches command plumbing.
|
|||||||
Each check is a "mode" running a fixed phase pipeline:
|
Each check is a "mode" running a fixed phase pipeline:
|
||||||
|
|
||||||
```
|
```
|
||||||
registerCheck(def) ← index.ts discovers checks/*.ts `check` exports
|
registerCheck(def) ← checks/*.ts self-register on load
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
/pygienium-<check> ─► runCheck(def) (src/modes/check-runner.ts)
|
/pygienium-<check> ─► runCheck(def) (src/modes/check-runner.ts)
|
||||||
@@ -171,10 +165,9 @@ registerCheck(def) ← index.ts discovers checks/*.ts `check` exports
|
|||||||
`/pygienium-export` are pure reads over it; `/pygienium-all` shares one
|
`/pygienium-export` are pure reads over it; `/pygienium-all` shares one
|
||||||
`RunState` across every check so phases accumulate in one record.
|
`RunState` across every check so phases accumulate in one record.
|
||||||
- **The registry** (`src/checks/registry.ts`) is the extensibility seam: a
|
- **The registry** (`src/checks/registry.ts`) is the extensibility seam: a
|
||||||
module-level `Map` of `CheckDefinition`s. `index.ts` discovers every
|
module-level `Map` of `CheckDefinition`s. `index.ts` iterates it and binds
|
||||||
`checks/*.ts` file, registers its `check` export, then iterates the map and
|
one `/pygienium-<name>` command per entry, so adding a check is a file +
|
||||||
binds one `/pygienium-<name>` command per entry — adding a check is a file
|
one `registerCheck()` line.
|
||||||
with a `check` export, nothing else.
|
|
||||||
- **The footer** (`src/footer.ts`) is the piolium-style pipeline-overview
|
- **The footer** (`src/footer.ts`) is the piolium-style pipeline-overview
|
||||||
status strip: a single static line in the TUI footer (via
|
status strip: a single static line in the TUI footer (via
|
||||||
`ui.setStatus(key, text)`) listing the full ordered pipeline with the cursor
|
`ui.setStatus(key, text)`) listing the full ordered pipeline with the cursor
|
||||||
@@ -202,7 +195,7 @@ pygienium/
|
|||||||
│ ├─ phases.ts ← live chat progress widget + completion-tree helpers
|
│ ├─ phases.ts ← live chat progress widget + completion-tree helpers
|
||||||
│ ├─ footer.ts ← pipeline-overview status strip (TUI footer)
|
│ ├─ footer.ts ← pipeline-overview status strip (TUI footer)
|
||||||
│ ├─ modes/check-runner.ts ← the per-check phase pipeline
|
│ ├─ modes/check-runner.ts ← the per-check phase pipeline
|
||||||
│ └─ checks/ ← one file per check, exporting `check`
|
│ └─ checks/ ← one file per check, self-registering
|
||||||
│ ├─ registry.ts ← CheckDefinition + registerCheck
|
│ ├─ registry.ts ← CheckDefinition + registerCheck
|
||||||
│ ├─ comments.ts deep-modules.ts dead-code.ts
|
│ ├─ comments.ts deep-modules.ts dead-code.ts
|
||||||
│ ├─ defensive-guards.ts
|
│ ├─ defensive-guards.ts
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@mikefreno/omp-pygenium",
|
"name": "@mikefreno/omp-pygienium",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Code hygiene extension for omp (port of the pi extension) — isolated sub-agent checks that scan a target, apply fixes, and emit a findings+changes report.",
|
"description": "Code hygiene extension for omp (port of the pi extension) — isolated sub-agent checks that scan a target, apply fixes, and emit a findings+changes report.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -30,4 +30,4 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/checks/all.ts
Normal file
15
src/checks/all.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Static barrel for shipped checks.
|
||||||
|
*
|
||||||
|
* Every check module self-registers on import (top-level `registerCheck`), so
|
||||||
|
* importing all of them here — once, statically, from the entry graph —
|
||||||
|
* registers every check before commands bind. Adding a check means dropping
|
||||||
|
* the file in and adding one import line here.
|
||||||
|
*/
|
||||||
|
import "./comments.js";
|
||||||
|
import "./complexity.js";
|
||||||
|
import "./dead-code.js";
|
||||||
|
import "./deep-modules.js";
|
||||||
|
import "./defensive-guards.js";
|
||||||
|
import "./scope.js";
|
||||||
|
import "./todos.js";
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
* @module pygienium/checks/comments
|
* @module pygienium/checks/comments
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CheckDefinition, CheckScope } from "./registry.js";
|
import { registerCheck, type CheckScope } from "./registry.js";
|
||||||
import { scopeRulesMarkdown } from "./scope.js";
|
import { scopeRulesMarkdown } from "./scope.js";
|
||||||
|
|
||||||
/** Phase-strip phase this check belongs to. */
|
/** Phase-strip phase this check belongs to. */
|
||||||
@@ -215,7 +215,7 @@ async function commentsVerify(scope: CheckScope): Promise<string | undefined> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The comments hygiene check definition. */
|
/** The comments hygiene check definition. */
|
||||||
export const check = {
|
export const commentsCheck = {
|
||||||
name: "comments",
|
name: "comments",
|
||||||
label: "Comments",
|
label: "Comments",
|
||||||
description:
|
description:
|
||||||
@@ -227,7 +227,7 @@ export const check = {
|
|||||||
buildFixTask: buildCommentsFixTask,
|
buildFixTask: buildCommentsFixTask,
|
||||||
gate: commentsGate,
|
gate: commentsGate,
|
||||||
verify: commentsVerify,
|
verify: commentsVerify,
|
||||||
} as const satisfies CheckDefinition;
|
} as const;
|
||||||
|
|
||||||
// No self-registration here: `index.ts` auto-discovers every `checks/*.ts`
|
// Self-register on import so index.ts auto-discovery picks it up.
|
||||||
// that exports `check` and registers it — a new check is still one file.
|
registerCheck(commentsCheck);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* @module pygienium/checks/complexity
|
* @module pygienium/checks/complexity
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CheckDefinition, CheckScope } from "./registry.js";
|
import { registerCheck, type CheckScope } from "./registry.js";
|
||||||
import { scopeRulesMarkdown } from "./scope.js";
|
import { scopeRulesMarkdown } from "./scope.js";
|
||||||
|
|
||||||
/** Phase-strip phase this check belongs to. */
|
/** Phase-strip phase this check belongs to. */
|
||||||
@@ -299,7 +299,7 @@ async function complexityVerify(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The excessive complexity check definition. */
|
/** The excessive complexity check definition. */
|
||||||
export const check = {
|
export const complexityCheck = {
|
||||||
name: "complexity",
|
name: "complexity",
|
||||||
label: "Complexity",
|
label: "Complexity",
|
||||||
description:
|
description:
|
||||||
@@ -311,7 +311,7 @@ export const check = {
|
|||||||
buildFixTask: buildComplexityFixTask,
|
buildFixTask: buildComplexityFixTask,
|
||||||
gate: complexityGate,
|
gate: complexityGate,
|
||||||
verify: complexityVerify,
|
verify: complexityVerify,
|
||||||
} as const satisfies CheckDefinition;
|
} as const;
|
||||||
|
|
||||||
// No self-registration here: `index.ts` auto-discovers every `checks/*.ts`
|
// Self-register on import so index.ts auto-discovery picks it up.
|
||||||
// that exports `check` and registers it — a new check is still one file.
|
registerCheck(complexityCheck);
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ import {
|
|||||||
rm,
|
rm,
|
||||||
} from "node:fs/promises";
|
} from "node:fs/promises";
|
||||||
import { basename, dirname, join, relative, resolve } from "node:path";
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
||||||
import type { CheckDefinition, CheckScope } from "./registry.js";
|
import {
|
||||||
|
registerCheck,
|
||||||
|
type CheckDefinition,
|
||||||
|
type CheckScope,
|
||||||
|
} from "./registry.js";
|
||||||
import {
|
import {
|
||||||
isScopeSource,
|
isScopeSource,
|
||||||
SCOPE_EXCLUDE_DIRS,
|
SCOPE_EXCLUDE_DIRS,
|
||||||
@@ -1126,7 +1130,7 @@ async function deadCodeVerify(scope: CheckScope): Promise<string | undefined> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The registered `CheckDefinition` (module self-registers on import). */
|
/** The registered `CheckDefinition` (module self-registers on import). */
|
||||||
export const check: CheckDefinition = {
|
export const deadCodeCheck: CheckDefinition = {
|
||||||
name: "dead-code",
|
name: "dead-code",
|
||||||
label: "Dead code",
|
label: "Dead code",
|
||||||
description:
|
description:
|
||||||
@@ -1140,5 +1144,5 @@ export const check: CheckDefinition = {
|
|||||||
verify: deadCodeVerify,
|
verify: deadCodeVerify,
|
||||||
};
|
};
|
||||||
|
|
||||||
// No self-registration here: `index.ts` auto-discovers every `checks/*.ts`
|
// Self-register so `index.ts` auto-discovers this check with zero wiring edits.
|
||||||
// that exports `check` and registers it.
|
registerCheck(deadCodeCheck);
|
||||||
|
|||||||
@@ -23,7 +23,11 @@
|
|||||||
|
|
||||||
import { readdirSync } from "node:fs";
|
import { readdirSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { CheckDefinition, CheckScope } from "./registry.js";
|
import {
|
||||||
|
registerCheck,
|
||||||
|
type CheckDefinition,
|
||||||
|
type CheckScope,
|
||||||
|
} from "./registry.js";
|
||||||
import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
||||||
|
|
||||||
/** Output directory for this check's persistent reports. */
|
/** Output directory for this check's persistent reports. */
|
||||||
@@ -168,7 +172,7 @@ function buildDeepFixTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The check definition; registers itself on import. */
|
/** The check definition; registers itself on import. */
|
||||||
export const check: CheckDefinition = {
|
const deepModulesCheck: CheckDefinition = {
|
||||||
name: "deep-modules",
|
name: "deep-modules",
|
||||||
label: "Deep modules",
|
label: "Deep modules",
|
||||||
description:
|
description:
|
||||||
@@ -181,5 +185,6 @@ export const check: CheckDefinition = {
|
|||||||
verify: deepModulesVerify,
|
verify: deepModulesVerify,
|
||||||
};
|
};
|
||||||
|
|
||||||
// No self-registration here: `index.ts` auto-discovers every `checks/*.ts`
|
registerCheck(deepModulesCheck);
|
||||||
// that exports `check` and registers it.
|
|
||||||
|
export { deepModulesCheck };
|
||||||
|
|||||||
@@ -43,7 +43,11 @@
|
|||||||
|
|
||||||
import { readdirSync } from "node:fs";
|
import { readdirSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { CheckDefinition, CheckScope } from "./registry.js";
|
import {
|
||||||
|
registerCheck,
|
||||||
|
type CheckDefinition,
|
||||||
|
type CheckScope,
|
||||||
|
} from "./registry.js";
|
||||||
import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
||||||
|
|
||||||
/** Output directory for this check's persistent reports. */
|
/** Output directory for this check's persistent reports. */
|
||||||
@@ -198,7 +202,7 @@ function buildDefensiveGuardsFixTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The check definition; registers itself on import. */
|
/** The check definition; registers itself on import. */
|
||||||
export const check: CheckDefinition = {
|
const defensiveGuardsCheck: CheckDefinition = {
|
||||||
name: "defensive-guards",
|
name: "defensive-guards",
|
||||||
label: "Defensive guards",
|
label: "Defensive guards",
|
||||||
description:
|
description:
|
||||||
@@ -211,5 +215,6 @@ export const check: CheckDefinition = {
|
|||||||
verify: defensiveGuardsVerify,
|
verify: defensiveGuardsVerify,
|
||||||
};
|
};
|
||||||
|
|
||||||
// No self-registration here: `index.ts` auto-discovers every `checks/*.ts`
|
registerCheck(defensiveGuardsCheck);
|
||||||
// that exports `check` and registers it.
|
|
||||||
|
export { defensiveGuardsCheck };
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const SCOPE_EXTENSIONS: ReadonlySet<string> = new Set([
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Directories pygienium never descends into — build output, dependency caches,
|
* Directories pygienium never descends into — build output, dependency caches,
|
||||||
* tooling state, and VCS metadata. When walking the tree with `glob`/`grep`/
|
* tooling state, and VCS metadata. When walking the tree with `find`/`grep`/
|
||||||
* `readdir`, skip these by name to avoid wasting tokens on vendored code and
|
* `readdir`, skip these by name to avoid wasting tokens on vendored code and
|
||||||
* generated artifacts the user can't act on.
|
* generated artifacts the user can't act on.
|
||||||
*/
|
*/
|
||||||
@@ -104,7 +104,7 @@ export function isScopeSource(path: string): boolean {
|
|||||||
* exactly what to inspect and what to skip — stated once here, not copy-pasted
|
* exactly what to inspect and what to skip — stated once here, not copy-pasted
|
||||||
* into each task builder.
|
* into each task builder.
|
||||||
*
|
*
|
||||||
* Agents that use `glob`/`grep`/`readdir` for their own file discovery read
|
* Agents that use `find`/`grep`/`readdir` for their own file discovery read
|
||||||
* this before exploring, so the exclusion list governs their search too.
|
* this before exploring, so the exclusion list governs their search too.
|
||||||
*/
|
*/
|
||||||
export function scopeRulesMarkdown(): string {
|
export function scopeRulesMarkdown(): string {
|
||||||
@@ -139,7 +139,7 @@ noise the user cannot act on.
|
|||||||
exists — it is the authoritative source inventory (git-tracked, extension-
|
exists — it is the authoritative source inventory (git-tracked, extension-
|
||||||
filtered, exclude-aware). Read its \`fileCounts\` for the quick picture.
|
filtered, exclude-aware). Read its \`fileCounts\` for the quick picture.
|
||||||
2. Otherwise enumerate files yourself, applying the rules above.
|
2. Otherwise enumerate files yourself, applying the rules above.
|
||||||
3. When using \`glob\`/\`grep\`, add ignore patterns for the skip directories
|
3. When using \`find\`/\`grep\`, add prune clauses for the skip directories
|
||||||
(e.g. exclude \`**/node_modules/**\` from your scans).
|
(e.g. \`find . -type d -name node_modules -prune -o -name '*.ts' -print\`).
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ import { readdirSync } from "node:fs";
|
|||||||
import { readFile, readdir, stat } from "node:fs/promises";
|
import { readFile, readdir, stat } from "node:fs/promises";
|
||||||
import { join, relative } from "node:path";
|
import { join, relative } from "node:path";
|
||||||
import { loadRunState } from "../run-state.js";
|
import { loadRunState } from "../run-state.js";
|
||||||
import type { CheckDefinition, CheckScope } from "./registry.js";
|
import {
|
||||||
|
registerCheck,
|
||||||
|
type CheckDefinition,
|
||||||
|
type CheckScope,
|
||||||
|
} from "./registry.js";
|
||||||
import {
|
import {
|
||||||
isScopeSource,
|
isScopeSource,
|
||||||
SCOPE_EXCLUDE_DIRS,
|
SCOPE_EXCLUDE_DIRS,
|
||||||
@@ -541,7 +545,7 @@ async function todosVerify(scope: CheckScope): Promise<string | undefined> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The todos check definition; registers itself on import. */
|
/** The todos check definition; registers itself on import. */
|
||||||
export const check: CheckDefinition = {
|
const todosCheck: CheckDefinition = {
|
||||||
name: "todos",
|
name: "todos",
|
||||||
label: "TODOs & stubs",
|
label: "TODOs & stubs",
|
||||||
description:
|
description:
|
||||||
@@ -554,5 +558,6 @@ export const check: CheckDefinition = {
|
|||||||
verify: todosVerify,
|
verify: todosVerify,
|
||||||
};
|
};
|
||||||
|
|
||||||
// No self-registration here: `index.ts` auto-discovers every `checks/*.ts`
|
registerCheck(todosCheck);
|
||||||
// that exports `check` and registers it.
|
|
||||||
|
export { todosCheck };
|
||||||
|
|||||||
52
src/index.ts
52
src/index.ts
@@ -3,11 +3,15 @@
|
|||||||
*
|
*
|
||||||
* Entry point. Registers `/pygienium-help`, auto-registers one
|
* Entry point. Registers `/pygienium-help`, auto-registers one
|
||||||
* `/pygienium-<check>` command per registered `CheckDefinition`, plus the
|
* `/pygienium-<check>` command per registered `CheckDefinition`, plus the
|
||||||
* `all`/`resume`/`status`/`export` commands. Adding a check requires ONLY a new
|
* `all`/`resume`/`status`/`export` commands. Adding a check requires a new
|
||||||
* file in `src/checks/` plus one `registerCheck(def)` call — no changes here.
|
* file in `src/checks/` plus one import line in `src/checks/all.ts` — no
|
||||||
|
* changes here.
|
||||||
*
|
*
|
||||||
* Check files in `src/checks/` are auto-discovered (every `.ts` except the
|
* Check modules self-register on import (top-level `registerCheck`); the
|
||||||
* registry barrel), so they self-register at load time before commands bind.
|
* static barrel `src/checks/all.ts` imports every shipped check so they all
|
||||||
|
* register before commands bind. A static import list (rather than readdir +
|
||||||
|
* dynamic import) keeps the whole check graph resolvable up front, which
|
||||||
|
* matters for bundlers and module-loaders that tag extension graphs.
|
||||||
*
|
*
|
||||||
* Pi loads this file via jiti at runtime (see `pi.extensions` in package.json).
|
* Pi loads this file via jiti at runtime (see `pi.extensions` in package.json).
|
||||||
* The default export runs once per session; the factory is async so check
|
* The default export runs once per session; the factory is async so check
|
||||||
@@ -16,10 +20,9 @@
|
|||||||
* @module pygienium/index
|
* @module pygienium/index
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readdir, readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { dirname, join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import type {
|
import type {
|
||||||
ExtensionAPI,
|
ExtensionAPI,
|
||||||
ExtensionCommandContext,
|
ExtensionCommandContext,
|
||||||
@@ -28,7 +31,7 @@ import type {
|
|||||||
SessionStartEvent,
|
SessionStartEvent,
|
||||||
} from "@oh-my-pi/pi-coding-agent";
|
} from "@oh-my-pi/pi-coding-agent";
|
||||||
import { Box, Text } from "@oh-my-pi/pi-tui";
|
import { Box, Text } from "@oh-my-pi/pi-tui";
|
||||||
import { registerCheck, type CheckDefinition } from "./checks/registry.js";
|
import "./checks/all.js";
|
||||||
import { registerPygieniumCommands, type PygieniumCtx } from "./commands.js";
|
import { registerPygieniumCommands, type PygieniumCtx } from "./commands.js";
|
||||||
import {
|
import {
|
||||||
type SendChatMessage,
|
type SendChatMessage,
|
||||||
@@ -244,34 +247,6 @@ function makeStreamForwarder(pi: ExtensionAPI): StreamForwarder {
|
|||||||
return { onAgentEvent, sendPhaseLine };
|
return { onAgentEvent, sendPhaseLine };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Import every `checks/*.ts` module (except the registry barrel) and register
|
|
||||||
* each file's `check` export. Check files are pure data modules — they no
|
|
||||||
* longer self-register on import, because omp's extension loader cache-busts
|
|
||||||
* lazily imported graph modules with an `?mtime` suffix, which would split
|
|
||||||
* the registry into two module instances (static entry-graph imports resolve
|
|
||||||
* to the clean file, lazy imports to the `?mtime` copy). Registering here —
|
|
||||||
* from the entry's own registry instance — keeps one registry and still makes
|
|
||||||
* adding a check a drop-a-file operation.
|
|
||||||
*/
|
|
||||||
async function loadCheckModules(): Promise<void> {
|
|
||||||
const dir = join(dirname(fileURLToPath(import.meta.url)), "checks");
|
|
||||||
let entries: string[];
|
|
||||||
try {
|
|
||||||
entries = await readdir(dir);
|
|
||||||
} catch {
|
|
||||||
return; // no checks dir (e.g. minimal install)
|
|
||||||
}
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.endsWith(".ts")) continue;
|
|
||||||
if (entry === "registry.ts" || entry === "load.ts") continue;
|
|
||||||
const mod = (await import(`./checks/${entry}`)) as {
|
|
||||||
check?: CheckDefinition;
|
|
||||||
};
|
|
||||||
if (mod.check) registerCheck(mod.check);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a callback to send completion messages to the main chat window.
|
* Create a callback to send completion messages to the main chat window.
|
||||||
*/
|
*/
|
||||||
@@ -291,9 +266,8 @@ function makeSendChatMessage(pi: ExtensionAPI): SendChatMessage {
|
|||||||
export default async function pygieniumExtension(
|
export default async function pygieniumExtension(
|
||||||
pi: ExtensionAPI,
|
pi: ExtensionAPI,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Self-register every shipped check before wiring commands.
|
// Checks self-register on import (see ./checks/all.js), which runs before
|
||||||
await loadCheckModules();
|
// this factory — commands below bind against a fully populated registry.
|
||||||
|
|
||||||
const sendChatMessage = makeSendChatMessage(pi);
|
const sendChatMessage = makeSendChatMessage(pi);
|
||||||
|
|
||||||
// Register custom message renderer for pygienium progress messages.
|
// Register custom message renderer for pygienium progress messages.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function fakeCheck(name: string): CheckDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Capture process.stdout.write lines for the duration of `fn`. */
|
/** Capture process.stdout.write lines for the duration of `fn`. */
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function fakeCheck(name: string): CheckDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tracker: records dispatched agent tasks then delegates to the fake runner. */
|
/** Tracker: records dispatched agent tasks then delegates to the fake runner. */
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function smokeCheck(): CheckDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("check-runner integration", () => {
|
describe("check-runner integration", () => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
|
import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
|
||||||
import { loadRunState } from "../src/run-state.js";
|
import { loadRunState } from "../src/run-state.js";
|
||||||
import {
|
import {
|
||||||
check as commentsCheck,
|
commentsCheck,
|
||||||
findingsPath,
|
findingsPath,
|
||||||
changesPath,
|
changesPath,
|
||||||
} from "../src/checks/comments.js";
|
} from "../src/checks/comments.js";
|
||||||
@@ -48,6 +48,7 @@ const FILENAME = "sample.ts";
|
|||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return {
|
return {
|
||||||
cwd,
|
cwd,
|
||||||
|
mode: "print",
|
||||||
hasUI: false,
|
hasUI: false,
|
||||||
ui: undefined,
|
ui: undefined,
|
||||||
} as PygieniumCtx;
|
} as PygieniumCtx;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import * as fs from "node:fs/promises";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import {
|
import {
|
||||||
check as complexityCheck,
|
complexityCheck,
|
||||||
buildComplexityScanTask,
|
buildComplexityScanTask,
|
||||||
buildComplexityFixTask,
|
buildComplexityFixTask,
|
||||||
} from "../src/checks/complexity.js";
|
} from "../src/checks/complexity.js";
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
getCheck,
|
getCheck,
|
||||||
} from "../src/checks/registry.js";
|
} from "../src/checks/registry.js";
|
||||||
import {
|
import {
|
||||||
check as deadCodeCheck,
|
deadCodeCheck,
|
||||||
detectDeadCode,
|
detectDeadCode,
|
||||||
findingsPath,
|
findingsPath,
|
||||||
changesPath,
|
changesPath,
|
||||||
@@ -39,7 +39,7 @@ import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
|
|||||||
import { loadRunState } from "../src/run-state.js";
|
import { loadRunState } from "../src/run-state.js";
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeFixture(root: string): Promise<void> {
|
async function writeFixture(root: string): Promise<void> {
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ import { loadRunState } from "../src/run-state.js";
|
|||||||
import {
|
import {
|
||||||
findingsPath,
|
findingsPath,
|
||||||
changesPath,
|
changesPath,
|
||||||
check as deepModulesCheck,
|
deepModulesCheck,
|
||||||
} from "../src/checks/deep-modules.js";
|
} from "../src/checks/deep-modules.js";
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Drop a pass-through wrapper module that forwards a single lib call. */
|
/** Drop a pass-through wrapper module that forwards a single lib call. */
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ import { loadRunState } from "../src/run-state.js";
|
|||||||
import {
|
import {
|
||||||
findingsPath,
|
findingsPath,
|
||||||
changesPath,
|
changesPath,
|
||||||
check as defensiveGuardsCheck,
|
defensiveGuardsCheck,
|
||||||
} from "../src/checks/defensive-guards.js";
|
} from "../src/checks/defensive-guards.js";
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
|
|||||||
import { loadRunState } from "../src/run-state.js";
|
import { loadRunState } from "../src/run-state.js";
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Capture process.stdout.write lines for the duration of `fn`. */
|
/** Capture process.stdout.write lines for the duration of `fn`. */
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function fakeCheck(name: string): CheckDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tracker: records dispatched agent tasks then delegates to the fake runner. */
|
/** Tracker: records dispatched agent tasks then delegates to the fake runner. */
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ import { loadRunState } from "../src/run-state.js";
|
|||||||
import {
|
import {
|
||||||
findingsPath,
|
findingsPath,
|
||||||
changesPath,
|
changesPath,
|
||||||
check as todosCheck,
|
todosCheck,
|
||||||
detectTodoStubs,
|
detectTodoStubs,
|
||||||
todosPriorCounts,
|
todosPriorCounts,
|
||||||
buildTodosScanTask,
|
buildTodosScanTask,
|
||||||
} from "../src/checks/todos.js";
|
} from "../src/checks/todos.js";
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ import {
|
|||||||
} from "../src/agent-runner.js";
|
} from "../src/agent-runner.js";
|
||||||
import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
|
import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
|
||||||
import { loadRunState } from "../src/run-state.js";
|
import { loadRunState } from "../src/run-state.js";
|
||||||
import { check as complexityCheck } from "../src/checks/complexity.js";
|
import { complexityCheck } from "../src/checks/complexity.js";
|
||||||
import { check as deadCodeCheck } from "../src/checks/dead-code.js";
|
import { deadCodeCheck } from "../src/checks/dead-code.js";
|
||||||
import { check as deepModulesCheck } from "../src/checks/deep-modules.js";
|
import { deepModulesCheck } from "../src/checks/deep-modules.js";
|
||||||
import { check as defensiveGuardsCheck } from "../src/checks/defensive-guards.js";
|
import { defensiveGuardsCheck } from "../src/checks/defensive-guards.js";
|
||||||
|
|
||||||
/** Agent runner that simulates the MagniFluo bug: ok, empty, no writes. */
|
/** Agent runner that simulates the MagniFluo bug: ok, empty, no writes. */
|
||||||
const noopRunner: AgentRunner = async () => ({
|
const noopRunner: AgentRunner = async () => ({
|
||||||
@@ -41,7 +41,7 @@ const noopRunner: AgentRunner = async () => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function stubCtx(cwd: string): PygieniumCtx {
|
function stubCtx(cwd: string): PygieniumCtx {
|
||||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("verify hooks fail loudly on empty agent output", () => {
|
describe("verify hooks fail loudly on empty agent output", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user