Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
"url": "https://github.com/pickforge/pickforge-platform.git",
"path": "packages/review-tutor/claude-plugin"
}
},
{
"name": "complexity-gate",
"description": "Block completion when changed functions exceed complexity limits.",
"source": {
"source": "git-subdir",
"url": "https://github.com/pickforge/pickforge-platform.git",
"path": "packages/complexity-gate/claude-plugin"
}
}
]
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Shared platform packages for Pickforge desktop apps.
- `@pickforge/tauri-release`: signed Tauri release and updater-feed helpers.
- `@pickforge/brand`: CSS tokens, fonts, reset, and framework-neutral primitives.
- `@pickforge/auth`: UI-free Supabase Auth wrapper and entitlement reader.
- `@pickforge/complexity-gate`: cross-harness function complexity checks and stop gates.

Desktop apps keep updating from signed Tauri artifacts and signed `latest.json`
feeds. Stable releases stay tag-driven; nightly builds use a separate opt-in
Expand Down
43 changes: 34 additions & 9 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"packages/*"
],
"scripts": {
"build": "bun run --cwd packages/tauri-release build && bun run --cwd packages/tauri-updater build && bun run --cwd packages/auth build && bun run --cwd packages/brand build && bun run --cwd packages/flags build && bun run --cwd packages/billing build && bun run --cwd packages/edge-shared build && bun run --cwd packages/sync build && bun run --cwd packages/review-tutor build",
"build": "bun run --cwd packages/tauri-release build && bun run --cwd packages/tauri-updater build && bun run --cwd packages/auth build && bun run --cwd packages/brand build && bun run --cwd packages/flags build && bun run --cwd packages/billing build && bun run --cwd packages/edge-shared build && bun run --cwd packages/sync build && bun run --cwd packages/review-tutor build && bun run --cwd packages/complexity-gate build",
"test": "vitest run",
"test:supabase": "supabase test db supabase/tests/database --local && bun run supabase/tests/welcome-credits-concurrency.ts && bun test packages/billing/test/checkout-lifecycle.contract.test.ts && bun test packages/sync/test/lww.contract.test.ts && bun test packages/edge-shared/test/router-attempt.contract.test.ts",
"test:coverage": "vitest run --coverage",
Expand Down
50 changes: 50 additions & 0 deletions packages/complexity-gate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# @pickforge/complexity-gate

Function-complexity feedback and stop gates for Pi, Claude Code, and Codex. The npm package downloads the matching Rust binary, verifies its SHA-256 checksum, and keeps install non-fatal when a release or network is unavailable.

## Install

Requires Node 22 or newer. Set `COMPLEXITY_GATE_BIN` to an existing binary to skip the release download. Set `COMPLEXITY_GATE_VERSION` to a release tag (default `v0.1.0`).

```bash
npm install -g @pickforge/complexity-gate
complexity-gate-install --all
```

Choose one or more harnesses with `--harness claude,codex,pi`. With no flags, the installer prompts for a comma-separated list. `--print` prints configuration without writing, and `--home <dir>` changes the settings root.

### Pi

```bash
pi install npm:@pickforge/complexity-gate
```

The extension checks files after `edit` and `write` tool results. Violations are appended as tool feedback. At agent-turn completion it checks changed functions and queues up to three refactor follow-ups per session.

### Claude Code

```bash
claude plugin marketplace add pickforge/pickforge-platform
claude plugin install complexity-gate@pickforge
```

Alternatively, `complexity-gate-install --harness claude` merges the equivalent hooks into `~/.claude/settings.json` without replacing existing hooks.

### Codex

```bash
complexity-gate-install --harness codex
cp -r node_modules/@pickforge/complexity-gate/codex-skill/complexity-gate ~/.codex/skills/
```

The installer merges `codex-hooks.json` into `~/.codex/hooks.json`.

## Configure

Run `complexity-gate init` to create `.complexity-gate.json`. The defaults are complexity 15, depth 4, 100 nonblank/non-comment lines, and 6 parameters. See the installed skill for the refactoring workflow.

The wrapper resolves the executable in this order: `COMPLEXITY_GATE_BIN`, the verified binary under `vendor/`, then `complexity-gate` on `PATH`. stdin, stdout, stderr, argv, and exit status are inherited unchanged.

## Hook documentation

Hook formats and event names were checked against the Claude Code plugin/hooks and Codex hooks documentation on 2026-08-26. Both currently expose `PostToolUse` and `Stop`; both fragments invoke the Rust binary's harness-specific hook adapter.
40 changes: 40 additions & 0 deletions packages/complexity-gate/bin/complexity-gate
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env node
import { access, realpath } from "node:fs/promises";
import { constants } from "node:fs";
import { spawn } from "node:child_process";
import { delimiter, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const script = await realpath(fileURLToPath(import.meta.url));
const root = dirname(dirname(script));
const binaryName = process.platform === "win32" ? "complexity-gate.exe" : "complexity-gate";
const vendored = join(root, "vendor", binaryName);

async function usable(path) {
try { await access(path, constants.X_OK); return true; } catch { return false; }
}

async function pathBinary() {
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
const candidate = join(directory, binaryName);
if (!(await usable(candidate))) continue;
try { if (await realpath(candidate) === script) continue; } catch { continue; }
return candidate;
}
}

let selected;
for (const candidate of [process.env.COMPLEXITY_GATE_BIN, vendored]) {
if (candidate && await usable(candidate)) { selected = candidate; break; }
}
selected ??= await pathBinary();
if (!selected) {
console.error("complexity-gate: binary not found; set COMPLEXITY_GATE_BIN or run complexity-gate-install");
process.exit(127);
}
const child = spawn(selected, process.argv.slice(2), { stdio: "inherit" });
child.on("error", () => {
console.error("complexity-gate: binary not found; set COMPLEXITY_GATE_BIN or run complexity-gate-install");
process.exit(127);
});
child.on("exit", (code, signal) => signal ? process.kill(process.pid, signal) : process.exit(code ?? 1));
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "complexity-gate",
"version": "0.1.0",
"description": "Block agent completion when changed functions exceed complexity limits.",
"author": { "name": "Pickforge" },
"homepage": "https://github.com/pickforge/pickforge-platform/tree/main/packages/complexity-gate",
"repository": "https://github.com/pickforge/pickforge-platform",
"license": "MIT"
}
12 changes: 12 additions & 0 deletions packages/complexity-gate/claude-plugin/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"description": "Check edited and changed functions with complexity-gate.",
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{ "type": "command", "command": "complexity-gate hook claude" }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": "complexity-gate hook claude" }]
}]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: complexity-gate
description: Measure and reduce function complexity with the complexity-gate binary. Use when a complexity-gate hook reports FAIL lines, when the user asks to refactor, simplify, or clean up code, mentions complexity, maintainability, deeply nested logic, or god functions, or after writing any nontrivial branching code.
---

# Complexity gate

Never estimate complexity yourself. The only accepted numbers come from:

```bash
complexity-gate check <file> # one file
complexity-gate check --changed # every function you touched this session
```

Output: `FAIL path:line name metric value > limit`. Metrics: `complexity`
(cyclomatic), `depth` (nesting), `lines`, `params`. `UNVERIFIED path` means no
grammar for that language: say so in your report, do not count by hand.

The Stop hook re-runs `--changed` when you try to finish and blocks while any
FAIL remains. Fix the listed functions; do not suppress, rename, or move them to
escape the diff.

## Refactor tactics, in order of preference

1. **Guard clauses.** Invert conditions, return early, kill nesting.
2. **Extract function.** Each piece gets a name that says what, not how.
3. **Lookup table / map** instead of if-else or switch chains.
4. **Named predicates.** `if (isEligibleForRefund(order))` beats a 4-clause boolean.
5. **Polymorphism / strategy** for switch-on-type, only when the switch appears in 2+ places.
6. **Flatten loops.** Extract the loop body; use `continue` instead of nested `if`.

## Hard rules

- Preserve behavior. Run tests before and after. No tests: say so, refactor conservatively.
- Don't game the metric. A dense one-liner hiding six branches is worse than the
honest if-chain it replaced. Complexity moves into well-named units, it does not
disappear into cleverness.
- Don't break public APIs or exported signatures without asking.
- One responsibility per function. If the name needs "and", split.
- Never raise a limit in `.complexity-gate.json` to get green. Legacy code you
did not touch is not your problem; the gate only checks changed functions.

## Workflow

1. Run `complexity-gate check --changed`; rank FAILs by value descending.
2. Refactor worst first, one function at a time.
3. Re-run the check. End with:

```
## Complexity report
| Function | Metric | Before | After |
|----------|--------|--------|-------|
| parseOrder | complexity | 18 | 6 |

Extracted: validateHeader, resolveDiscount
Behavior verified: <tests run / how>
```
12 changes: 12 additions & 0 deletions packages/complexity-gate/codex-hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"description": "Check edited and changed functions with complexity-gate.",
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{ "type": "command", "command": "complexity-gate hook codex" }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": "complexity-gate hook codex" }]
}]
}
}
57 changes: 57 additions & 0 deletions packages/complexity-gate/codex-skill/complexity-gate/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: complexity-gate
description: Measure and reduce function complexity with the complexity-gate binary. Use when a complexity-gate hook reports FAIL lines, when the user asks to refactor, simplify, or clean up code, mentions complexity, maintainability, deeply nested logic, or god functions, or after writing any nontrivial branching code.
---

# Complexity gate

Never estimate complexity yourself. The only accepted numbers come from:

```bash
complexity-gate check <file> # one file
complexity-gate check --changed # every function you touched this session
```

Output: `FAIL path:line name metric value > limit`. Metrics: `complexity`
(cyclomatic), `depth` (nesting), `lines`, `params`. `UNVERIFIED path` means no
grammar for that language: say so in your report, do not count by hand.

The Stop hook re-runs `--changed` when you try to finish and blocks while any
FAIL remains. Fix the listed functions; do not suppress, rename, or move them to
escape the diff.

## Refactor tactics, in order of preference

1. **Guard clauses.** Invert conditions, return early, kill nesting.
2. **Extract function.** Each piece gets a name that says what, not how.
3. **Lookup table / map** instead of if-else or switch chains.
4. **Named predicates.** `if (isEligibleForRefund(order))` beats a 4-clause boolean.
5. **Polymorphism / strategy** for switch-on-type, only when the switch appears in 2+ places.
6. **Flatten loops.** Extract the loop body; use `continue` instead of nested `if`.

## Hard rules

- Preserve behavior. Run tests before and after. No tests: say so, refactor conservatively.
- Don't game the metric. A dense one-liner hiding six branches is worse than the
honest if-chain it replaced. Complexity moves into well-named units, it does not
disappear into cleverness.
- Don't break public APIs or exported signatures without asking.
- One responsibility per function. If the name needs "and", split.
- Never raise a limit in `.complexity-gate.json` to get green. Legacy code you
did not touch is not your problem; the gate only checks changed functions.

## Workflow

1. Run `complexity-gate check --changed`; rank FAILs by value descending.
2. Refactor worst first, one function at a time.
3. Re-run the check. End with:

```
## Complexity report
| Function | Metric | Before | After |
|----------|--------|--------|-------|
| parseOrder | complexity | 18 | 6 |

Extracted: validateHeader, resolveDiscount
Behavior verified: <tests run / how>
```
Loading