Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
22 changes: 0 additions & 22 deletions .claude/hookify.block-double-push.md

This file was deleted.

13 changes: 0 additions & 13 deletions .claude/hookify.block-pr-create.md

This file was deleted.

23 changes: 0 additions & 23 deletions .claude/hookify.enforce-parallel-tests.md

This file was deleted.

15 changes: 0 additions & 15 deletions .claude/hookify.function-length.md

This file was deleted.

36 changes: 0 additions & 36 deletions .claude/hookify.missing-logger.md

This file was deleted.

20 changes: 0 additions & 20 deletions .claude/hookify.no-cd-prefix.md

This file was deleted.

14 changes: 0 additions & 14 deletions .claude/hookify.no-future-annotations.md

This file was deleted.

19 changes: 0 additions & 19 deletions .claude/hookify.no-local-coverage.md

This file was deleted.

11 changes: 0 additions & 11 deletions .claude/hookify.pep758-except.md

This file was deleted.

24 changes: 17 additions & 7 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,25 @@
"type": "command",
"command": "python3 scripts/check_no_bulk_edit.py",
"timeout": 5000
}
]
},
{
"matcher": "Edit",
"hooks": [
},
{
"type": "command",
"command": "python3 scripts/check_no_bulk_edit.py",
"command": "bash scripts/check_no_pr_create.sh",
"timeout": 5000
},
{
"type": "command",
"command": "bash scripts/check_no_cd_prefix.sh",
"timeout": 5000
},
{
"type": "command",
"command": "bash scripts/check_no_local_coverage.sh",
"timeout": 5000
},
{
"type": "command",
"command": "bash scripts/check_enforce_parallel_tests.sh",
"timeout": 5000
}
]
Expand Down
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ src/synthorg/persistence/postgres/schema.sql text eol=lf
# Shell scripts must use LF (bash can't parse CRLF).
*.sh text eol=lf

# Committed git hook wrappers are extensionless (git only invokes a
# hook file named exactly pre-commit / pre-push / commit-msg), so the
# *.sh rule above misses them. They run via `#!/usr/bin/env bash`; a
# CRLF checkout makes the kernel look for `bash\r` and every hook
# fails silently. Pin to LF regardless of platform / core.autocrlf.
scripts/git-hooks/* text eol=lf

# ZAP DAST rules file is parsed on Linux runners; CRLF was observed
# leaking in from Windows-side edits, leaving the file with mixed
# endings that some TSV consumers cannot handle.
Expand Down
67 changes: 29 additions & 38 deletions .opencode/plugins/synthorg-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
* PreToolUse (Bash): scripts/check_no_baseline_update.sh
* PreToolUse (Bash): scripts/check_bash_no_write.sh
* PreToolUse (Bash): scripts/check_git_c_cwd.sh
* PreToolUse (Bash | Edit): scripts/check_no_bulk_edit.py
* PreToolUse (Bash): scripts/check_no_pr_create.sh
* PreToolUse (Bash): scripts/check_no_cd_prefix.sh
* PreToolUse (Bash): scripts/check_no_local_coverage.sh
* PreToolUse (Bash): scripts/check_enforce_parallel_tests.sh
* PreToolUse (Bash): scripts/check_no_bulk_edit.py (shell in-place only)
* PreToolUse (Edit|Write): scripts/check_mock_spec_ratchet.py
* PreToolUse (Edit|Write): scripts/check_no_edit_migration.sh
* PreToolUse (Edit|Write): scripts/check_no_edit_baseline.sh
Expand All @@ -23,11 +27,12 @@
* PostToolUse (Edit|Write): scripts/check_backend_regional_defaults.py
* PostToolUse (Bash): scripts/record_push_throttle.sh
*
* Hookify rules enforced via this plugin (from .claude/hookify.*.md):
* block-pr-create: blocks direct `gh pr create`
* enforce-parallel-tests: enforces `-n 8` with pytest
* no-cd-prefix: blocks `cd` prefix in Bash commands
* no-local-coverage: blocks `--cov` flags locally
* These committed scripts are the single source of truth for the
* shared hook rules, so OpenCode (this plugin) and Claude Code
* (.claude/settings.json) enforce identical gates: block-pr-create via
* check_no_pr_create.sh, no-cd-prefix via check_no_cd_prefix.sh,
* no-local-coverage via check_no_local_coverage.sh, and
* enforce-parallel-tests via check_enforce_parallel_tests.sh.
*/

import type { Plugin } from "@opencode-ai/plugin";
Expand Down Expand Up @@ -328,38 +333,24 @@ export const SynthOrgHooks: Plugin = async ({ client, $, app }) => {
throw new Error(bulkDeny);
}

// block-pr-create: block direct gh pr create
if (/gh\s+pr\s+create/i.test(command)) {
throw new Error(
"PR creation blocked. Use `/pre-pr-review` instead; it runs automated checks + review agents + fixes before creating the PR. For trivial or docs-only changes: `/pre-pr-review quick` skips agents but still runs automated checks.",
);
}

// enforce-parallel-tests: enforce -n 8 with pytest
if (
/(?:^|\s)(?:pytest|run\s+pytest|python\s+-m\s+pytest)\b/i.test(command) &&
!/-n 8/.test(command)
) {
throw new Error(
"Always use `-n 8` with pytest for parallel execution. Add `-n 8` to your pytest command. Never run tests sequentially or with `-n auto` (32 workers causes crashes and is slower due to contention).",
);
}

// no-cd-prefix: block cd prefix in Bash commands (with optional leading whitespace)
if (/^\s*cd\s+/i.test(command)) {
throw new Error(
"BLOCKED: Do not use `cd` in Bash commands; it poisons the cwd for all subsequent calls. The working directory is ALREADY set to the project root. Run commands directly. For Go commands: use `go -C cli <command>`. For subdir tools without a `-C`/`--prefix` equivalent: use `bash -c \"cd <dir> && <cmd>\"`.",
);
}

// no-local-coverage: block --cov flags locally
if (
/(?:^|\s)(?:pytest|run\s+pytest|python\s+-m\s+pytest)\b/i.test(command) &&
/--cov\b/.test(command)
) {
throw new Error(
"Do not run pytest with coverage locally; CI handles it. Coverage adds 20-40% overhead. Remove `--cov`, `--cov-report`, and `--cov-fail-under` from your command.",
);
// block-pr-create / no-cd-prefix / no-local-coverage /
// enforce-parallel-tests: defer to the committed scripts so the
// rule lives in one place and OpenCode stays in lockstep with
// .claude/settings.json. The previous inline regexes had drifted
// (the enforce-parallel-tests variant blocked the documented
// `pytest -m unit` even though pyproject addopts already pins
// -n=8 --dist=loadfile).
for (const script of [
"scripts/check_no_pr_create.sh",
"scripts/check_no_cd_prefix.sh",
"scripts/check_no_local_coverage.sh",
"scripts/check_enforce_parallel_tests.sh",
]) {
const outcome = runHookScript(script, { command }, 5000, "Bash");
const denyReason = denyReasonFromOutcome(outcome);
if (denyReason) {
throw new Error(denyReason);
}
}

// check_push_throttle.sh + check_ci_before_push.sh + check_push_rebased.sh:
Expand Down
15 changes: 14 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ ci:
# local-only pre-push hook surface) so pre-commit.ci does not pick
# them up; their CI counterpart is the ``Lint`` job in ``ci.yml``
# which runs the same scripts on every PR.
skip: [commitizen, gitleaks, hadolint-docker, caddy-validate, zizmor, no-em-dashes, no-redundant-timeout, mypy, pytest-unit, golangci-lint, go-vet, go-test, eslint-web, check-push-rebased, check-single-migration-per-pr, check-no-modify-migration, forbidden-literals, persistence-boundary, persistence-protocol-uniformity, dependency-inversion, provider-complete-chokepoint, no-new-logger-exception-str-exc, otlp-span-redaction, orphan-fixtures, doc-drift-counts, boundary-typed, setting-to-startup-trace, long-running-loop-kill-switch, list-pagination, domain-error-hierarchy, dead-api-endpoints, dual-backend-test-parity, schema-drift, no-magic-numbers, convention-gate-inventory, mcp-admin-guardrail, runtime-stats-freshness, dto-types-ts-in-sync]
skip: [commitizen, gitleaks, hadolint-docker, caddy-validate, zizmor, no-em-dashes, no-redundant-timeout, mypy, pytest-unit, golangci-lint, go-vet, go-test, eslint-web, check-push-rebased, check-single-migration-per-pr, check-no-modify-migration, forbidden-literals, persistence-boundary, persistence-protocol-uniformity, dependency-inversion, provider-complete-chokepoint, no-new-logger-exception-str-exc, otlp-span-redaction, orphan-fixtures, doc-drift-counts, boundary-typed, setting-to-startup-trace, long-running-loop-kill-switch, list-pagination, domain-error-hierarchy, dead-api-endpoints, dual-backend-test-parity, schema-drift, no-magic-numbers, convention-gate-inventory, mcp-admin-guardrail, runtime-stats-freshness, dto-types-ts-in-sync, no-stdlib-logging]

default_install_hook_types: [pre-commit, commit-msg, pre-push]

Expand Down Expand Up @@ -209,6 +209,19 @@ repos:
language: system
files: ^src/synthorg/.*\.py$

- id: no-stdlib-logging
name: no stdlib logging in application code (use get_logger)
entry: uv run python scripts/check_no_stdlib_logging.py
language: system
# Trigger on any src/synthorg Python file, the gate script
# itself, or this config so a PR that introduces an
# ``import logging`` (or weakens the gate by editing the
# script) cannot bypass the check. The observability package
# is allowlisted inside the script, not here.
files: ^(src/synthorg/.*\.py|scripts/check_no_stdlib_logging\.py|\.pre-commit-config\.yaml)$
pass_filenames: false
stages: [pre-push]

- id: no-loop-bound-init
name: block new asyncio primitives in __init__ of lifecycle-managed classes
entry: uv
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Web: see `web/CLAUDE.md`. CLI: see `cli/CLAUDE.md` (use `go -C cli`, never `cd c
- **No Hardcoded Values (MANDATORY)**: numerics live in `settings/definitions/`; allowlist 0/1/-1, HTTP codes, hex masks, powers-of-2, and module-level annotated named constants of the form `NAME: int|float|Final|Final[int]|Final[float] = literal`. Enforced by `scripts/check_no_magic_numbers.py`.
- **Doc Numeric Claims (MANDATORY)**: numerics in README + public docs sourced from `data/runtime_stats.yaml` via `<!--RS:NAME-->` markers. See `data/README.md`.
- **Test Regression (MANDATORY)**: timeout/slow failures = source-code regression; never edit `tests/baselines/unit_timing.json` or any `scripts/*_baseline.{txt,json}` / `scripts/_*_baseline.py`. Both families are PreToolUse-blocked. Per-invocation bypass for gate baselines: `ALLOW_BASELINE_GROWTH=1 git commit ...` (requires explicit user approval).
- **Post-Implementation + Pre-PR Review (MANDATORY)**: after issue: branch + commit + push (no auto-PR); use `/pre-pr-review` (gh pr create is hookify-blocked). After PR: `/aurelio-review-pr` for external feedback. Fix EVERYTHING valid; no deferring.
- **Post-Implementation + Pre-PR Review (MANDATORY)**: after issue: branch + commit + push (no auto-PR); use `/pre-pr-review` (`gh pr create` is blocked by `scripts/check_no_pr_create.sh`). After PR: `/aurelio-review-pr` for external feedback. Fix EVERYTHING valid; no deferring.

## Quick Commands

Expand All @@ -34,6 +34,7 @@ uv run python -m pytest tests/ --ignore=tests/benchmarks/ --cov=synthorg --cov-f
uv run python -m pytest tests/benchmarks/ --codspeed -n0
HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -k properties
HYPOTHESIS_PROFILE=fuzz uv run python -m pytest tests/ -m unit --timeout=0
bash scripts/install_git_hooks.sh # one-time per clone: wire core.hooksPath -> scripts/git-hooks (NOT pre-commit install)
uv run pre-commit run --all-files
uv run python scripts/check_schema_drift_revisions.py --backend sqlite # or --backend postgres
PYTHONPATH=. uv run zensical build # docs
Expand Down Expand Up @@ -103,7 +104,7 @@ PYTHONPATH=. uv run zensical build # docs
- Commits: `<type>: <description>` (feat/fix/refactor/docs/test/chore/perf/ci); commitizen-enforced.
- Signed commits required on protected refs (GPG/SSH or GitHub App via `synthorg-repo-bot`).
- Branches: `<type>/<slug>` from main.
- Pre-commit/pre-push hooks: `.pre-commit-config.yaml`. Hookify rules: `.claude/hookify.*.md`.
- Pre-commit/pre-push hooks: `.pre-commit-config.yaml`. Tool-call gates: `.claude/settings.json` PreToolUse (`scripts/check_*.sh`/`.py`).
- Squash merge. PR body becomes squash commit; trailers (`Release-As`, `Closes #N`) must be in PR body.
- GitHub queries: `gh issue list` via Bash, NOT MCP `list_issues`.

Expand Down
Loading
Loading