Skip to content

feat: initial pgsql-coding plugin - #1

Merged
sebastian-iancu merged 20 commits into
mainfrom
feat/initial-plugin
Oct 27, 2026
Merged

sebastian-iancu merged 20 commits into
mainfrom
feat/initial-plugin

Conversation

@sebastian-iancu

Copy link
Copy Markdown
Contributor

Summary

The first version of pgsql-coding: PostgreSQL coding standards for Claude Code and Cursor, laid out like go-coding and php-coding. It targets PostgreSQL 18 (19 additions appear only as labelled hints) and is written for SQL called from Go (pgx v5) and PHP (PDO), with depth on jsonb, indexing, and plan analysis.

Component What it does
Skill pgsql-coding Router: the tool that settles each question, and the skill that owns it. versions.md lists what needs PostgreSQL 17, 18 or 19.
Skills pgsql-query, pgsql-jsonb, pgsql-schema, pgsql-indexing, pgsql-performance, pgsql-migrations Short rules in SKILL.md; 25 reference files that load only when a task needs them. Every rule cites its source, mostly the PostgreSQL 18 manual.
Skill /pgsql-lint-setup Writes the reference .squawk.toml (identical to references/squawk.toml).
pgsql-performance/scripts/plan-summary.py Standard-library summariser for TEXT or JSON EXPLAIN plans: where the time goes, the node where an estimate error starts, spills, loops, filter waste, JIT share.
Agent pgsql-reviewer Report-only SQL review of .sql files, migrations, and Go or PHP query strings.
Agent pgsql-plan-analyst Report-only analysis of a long plan or a before-and-after pair, in its own context.
Hooks session-start, lint-on-save (squawk, opt-in through .squawk.toml), skill-nudge; wired for both hosts.
Cursor rule rules/pgsql-context.mdc for **/*.sql.

How it was checked

  • scripts/validate.py and its 14 self-tests, 40 hook tests, 21 plan-summary tests, claude plugin validate ., and --check-links: all pass. All 255 cited URLs resolved.
  • The SQL examples were run on a postgres:18 (18.6) container. The jsonb driver claims were run on pgx v5.11.0 and PHP 8.4 with pdo_pgsql. The plan-summary fixtures are real 18.6 plans.
  • lint-on-save was run against a real squawk 2.70.0.
  • An independent review pass found and fixed a migration recipe that added NOT NULL ... NOT VALID before the backfill. It would have failed every update to an old row on a live table. The same pass fixed two wrong idioms and rules that two skills both stated in full.

Eval results

The first full run had 7 cases and 42 runs, and cost $5.45:

Case With Without Δ Skill fired
indexing-orders-table 1.00 1.00 0.00 1 of 3
jsonb-gin-predicate-shape 1.00 1.00 0.00 0 of 3
migrations-tenant-column 0.67 0.00 +0.67 3 of 3
performance-correlated-estimate 1.00 1.00 0.00 0 of 3
performance-plan-before-after 1.00 1.00 0.00 0 of 3; agent dispatched 0 of 3
query-inactive-customers 1.00 1.00 0.00 0 of 3
schema-orders-ddl 1.00 1.00 0.00 2 of 3

What the run showed:

  1. It found a real defect. The migrations recipe set lock_timeout = '1s' before CREATE INDEX CONCURRENTLY. On 18.6, any older open transaction makes that build fail and leave an INVALID index. A waiting concurrent build does not block plain DML, so no short timeout is needed there. This is fixed in the last commit. The migrations case now scores with 1.00, without 0.67 (Δ +0.33), and the skill fired and its reference was read in 3 of 3 runs.
  2. Six of seven cases do not measure the plugin. The model without the plugin already passes them, so their rubrics test knowledge it has anyway.
  3. Skills seldom fire from a bare prompt in an empty sandbox. The sandbox has no .sql files, no go.mod, and so no session-start line or nudges. In a real repository the hooks add those signals, but the descriptions still need a triggering pass.

Follow-ups (not in this PR)

  • A triggering pass on the skill descriptions, plus harder eval cases on things the base model gets wrong (PostgreSQL 18 specifics, pgx statement caching, plan diagnosis).
  • A marketplace entry in Cadasto/plugin-marketplace (pgsql-coding@cadasto), pinned to the first tag.
  • A plugin icon, the only claude plugin validate warning.

Checklist

  • ./scripts/validate.sh, python3 scripts/validate.py --selftest and ./scripts/hooks-test.sh pass
  • claude plugin validate . passes
  • Every rule cites a source read for this change; each reference's ## Sources carries the checked date
  • python3 scripts/validate.py --check-links passes
  • Both manifests in sync; Cursor rule and path map in step with the components
  • Skill and agent triggering tested locally in a real repository (see docs/testing.md)
  • Cursor install tested locally

🤖 Generated with Claude Code

sebastian-iancu and others added 9 commits October 27, 2026 11:06
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first eval run showed the migrations recipe set lock_timeout = '1s' in the
CREATE INDEX CONCURRENTLY file. Tested on PostgreSQL 18.6: with any older
transaction open, the build fails with a lock timeout and leaves an INVALID
index, while a waiting concurrent build does not block plain DML. The recipe now
sets lock_timeout = 0 there; the router, Cursor rule and reviewer say the same,
and the eval rubric no longer requires a timeout on that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sebastian-iancu sebastian-iancu left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

The eight skills and both agents are ready. Descriptions name real asks and point at the sibling that owns the neighbouring job, no rule is copied in full across two SKILL.md files, and the router, the migrations skill, lock-levels.md, patterns.md and migration-tools.md agree that CREATE INDEX CONCURRENTLY must not use a short lock_timeout. Both agents match the house shape used by go-reviewer: model: inherit, a description that says when to invoke and when not to, a "When to invoke" section, a fixed output format, and a Bash grant the body treats as a contract. pgsql-reviewer is blue and pgsql-plan-analyst is cyan. The plan analyst loads pgsql-performance and runs plan-summary.py instead of restating its thresholds.

The summariser, which that agent is told to trust, mis-states several findings and crashes on a legal plan. The migration timeout rule has already drifted in the reviewer checklist and the eval grader.

Important

  1. skills/pgsql-performance/scripts/plan-summary.py — silent wrong summaries and one crash. Inline comment on misestimate.
    • :172 clamps the smaller row count up to 1, so a fractional estimate (an index probe at 0.01 that returns 0.50, 10,000 times) never reaches the tenfold test.
    • :189 reports a LIMIT early stop as the node where an overestimate starts. explain.md says that stop is not an estimation error.
    • :205 formats n.total for a node with loops >= 1000. EXPLAIN (ANALYZE, TIMING OFF) omits per-node time, and the handler at :235 does not catch TypeError, so the script prints a partial summary and then crashes. explain.md tells the reader to use TIMING OFF when only row counts matter.
    • :207 reads Rows Removed by Filter and ignores Rows Removed by Join Filter, which both parsers store.
    • :212 multiplies Heap Fetches by loops. That counter is already a total. Rows Removed by Filter is a per-loop average, so the multiply is right only for the filter.
    • :125 builds a JSON title from Node Type alone, so TEXT Hash Anti Join and JSON Hash Join are different names for the same node. Incremental-sort and tuplestore spills (Average Disk, Storage: Disk) are not read.
  2. agents/pgsql-reviewer.md:64 restates the migration rule and drops SHARE and SET lock_timeout = 0. Inline comment.
  3. evals/migrations-tenant-column/graders/safe-sequence.md:8 still passes the bug the last commit fixes: a concurrent-index file with SET lock_timeout = '1s' is allowed when some other file sets a timeout. Inline comment.
  4. scripts/validate.py:326 returns success when either hook config is missing, and :313 accepts a Cursor command that contains ${CLAUDE_PLUGIN_ROOT} because the regex only captures hooks/<script>.sh. :165 measures a folded description: as the single token >-. The shipped tree is valid, so CI is green. The checks do not guard the shapes docs/testing.md names.

Suggestions

  • skills/pgsql-migrations/SKILL.md:18 sets lock_timeout = 0 and does not mention statement_timeout. lock-levels.md:50 also clears statement_timeout, "or a generous bound". A file that only follows the skill can keep statement_timeout = '5s' from the strong-lock template and abort a long build. Use one sentence: a concurrent index, drop or reindex file sets both to 0.
  • pgsql-jsonb and pgsql-query both state that an aggregate over zero rows returns NULL. Keep the jsonb_agg / ORDER BY prescription in jsonb and point the null-over-no-rows fact at pgsql-query.
  • README.md:69, docs/install.md:72 and AGENTS.md:59 say the skill nudge reads the added text on both hosts. hooks/skill-nudge.sh:26-28 does that only when the payload has tool_input. Cursor's afterFileEdit reads the whole file.
  • The plan-summary tests lock the TEXT findings on join-spill and never assert exclusive time, the JSON twin of subplan-loops (JIT under 10% must not print JIT took), heap fetches, a join filter, or TIMING OFF. hooks-test.sh never checks session-start's exit code.
  • pgsql-query and pgsql-performance descriptions are at 988 and 998 characters of the 1,024 limit.

Strengths

Sibling boundaries are real: index choice versus a live build, plan reading versus index design, squawk setup versus a squawk finding. Hook scripts exit 0, quote paths, and do not expand ${CLAUDE_PLUGIN_ROOT} on the Cursor side. The lock_timeout = '1s' recipe is gone from every CREATE INDEX CONCURRENTLY example. validate.py passes on the tree that shipped, including reference links, hook parity and the doc inventory.

Fix the summariser's row math and the crash, and make the reviewer checklist and the migrations grader require lock_timeout = 0 on a concurrent index file. The skills and the agent structure do not need a change.

if not n.analyzed or n.never:
return None
est, rows, loops = n.est or 0.0, n.rows or 0.0, n.loops or 1
hi, lo = max(est, rows), max(min(est, rows), 1.0)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lo = max(min(est, rows), 1.0) makes the ratio hi / 1. Any node whose larger per-loop row count is under 10 can never reach the tenfold test, and a fractional estimate disappears: an index probe estimated at 0.01 rows that returns 0.50, run 10,000 times (50×, 5,000 rows), produces no finding. The hi * loops >= 1000 guard never runs.

Divide by the real smaller count. If that count is 0, treat the ratio as large. Keep hi * loops >= 1000 as the only noise filter.

Same function, nearby:

  • :189 treats a LIMIT early stop as the node where an overestimate starts. explain.md says that stop is not an estimation error. Skip over when an ancestor is Limit.
  • :205 formats n.total whenever loops >= 1000. EXPLAIN (ANALYZE, TIMING OFF) omits per-node time, work_ms treats that as 0, and :235 does not catch TypeError, so the script prints a partial ranking and crashes. explain.md tells the reader to use TIMING OFF when only row counts matter. Skip the exclusive-time ranking when timing was not collected, and omit the per-loop time in the loops finding when it is missing.
  • :207 ignores Rows Removed by Join Filter, which both parsers store (:103, :135). Apply the same threshold.
  • :212 multiplies Heap Fetches by loops. That counter is already a total. Rows Removed by Filter is a per-loop average, so the multiply belongs only on the filter.
  • :125 titles a JSON node from Node Type alone, so TEXT Hash Anti Join and JSON Hash Join differ. Insert Join Type when it is not Inner, and fold Partial Mode plus Strategy into Aggregate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7894023. Each point is checked against a plan captured on 18.6 (scripts/testdata/plans/), and each new test fails on a5079a7's script.

  • Ratio. It now divides by the real smaller count, and a node that returned nothing counts as one row over all its loops. One exception, based on evidence: an estimate of exactly 1 is not called an overestimate. clamp_row_est forces every estimate to at least one row, so a fractional estimate such as 0.01 never appears in a plan. Dividing by a fractional actual below that floor moves the origin off the node that is wrong: in clamp-probe.txt the probe is estimated at 1 and returns 0.04, while the error is the Nested Loop's (estimated 20,000, got 750). An estimate above 1 that returns a fraction is now reported (fractional-probe.txt: estimated 8, got 0.38, ×21). The old code missed it.
  • LIMIT. over is skipped below a Limit, but the early stop ends at a node that reads all its input first (Sort, Hash, a plain or hashed aggregate). "Any Limit ancestor" would hide a real error: in limit-sort.txt, LIMIT over Sort over Seq Scan still runs the scan to the end, and its ×5,103 overestimate is real. The same rule covers the inner side of a nested-loop semi or anti join, which stops at the first match. The old code reported ×30,000 on a plain EXISTS (semi-stop.txt). explain.md now says so.
  • TIMING OFF. There is no ranking by time, and the loops finding leaves out the time. Both formats crashed before.
  • Join filter. It is now reported. The rows it passed are the rows kept plus the rows the node's own Filter removed afterwards, because the join filter runs first.
  • Heap Fetches. It is no longer multiplied. explain.c prints ntuples2 as a raw total, while both Rows Removed counters are nfiltered / nloops.
  • Names and spills. Join Type, Strategy and Partial Mode are folded into the JSON name, and every TEXT/JSON fixture pair is tested for identical node names. The script now also reads incremental-sort group spills and PG 18 tuplestore Storage: Disk. A HashAggregate spill was read from TEXT but not from JSON (HashAgg Batches); that's fixed too.

Mutation checks: removing the one-row floor, the stop at a Sort, or the semi-join rule each fails its own test.

Comment thread agents/pgsql-reviewer.md Outdated
- **Indexing** (`pgsql-indexing`): an unindexed foreign key; a range column before the equality columns; prefix `LIKE` without `text_pattern_ops` or `"C"`; a partial or expression index the query cannot match (bind parameter, different expression, non-IMMUTABLE cast); an index that blocks HOT updates; a duplicate index; a drop on `idx_scan = 0` without checking the reset time, replicas and uniqueness.
- **Performance evidence** (`pgsql-performance`): a fix with no plan behind it; `enable_*` as a fix; a query per row in a loop (N+1, `pgsql-query`); `SELECT *` over large jsonb or text (`pgsql-jsonb`); an exact `COUNT(*)` for a UI total; a CTE used twice that blocks an outer filter; timeouts in `postgresql.conf`; `auto_explain.log_analyze` in production unsampled.
- **Schema and security** (`pgsql-schema`): `serial`; an identity column with no primary key; `money`, `float` or `timestamp` misused; a `varchar(n)` limit with no rule; an enum whose values will be retired; a missing `NOT NULL`; a falsely `IMMUTABLE` function; `SECURITY DEFINER` without `SET search_path` and a `REVOKE` from `PUBLIC`; row-level security tested as the owner; a view over a protected table without `security_invoker`.
- **Migrations** (`pgsql-migrations`; name the squawk rule): no `lock_timeout` before DDL that takes `ACCESS EXCLUSIVE` or `SHARE ROW EXCLUSIVE`, or a short one before a concurrent index build (it aborts the build and leaves an INVALID index); an index built or dropped without `CONCURRENTLY`, or `CONCURRENTLY` inside a transaction (goose without `NO TRANSACTION`); a concurrent build retried without dropping an INVALID leftover; a constraint without `NOT VALID`, or `NOT VALID` and `VALIDATE` in one file; a bare `SET NOT NULL` on a populated column; a rewriting type change; a rename or drop shipped with the code change; a one-statement backfill.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checklist restates pgsql-migrations and has already drifted. The skill (SKILL.md:14 and :18) requires a timeout before ACCESS EXCLUSIVE, SHARE ROW EXCLUSIVE or SHARE, and requires lock_timeout = 0 on the concurrent-index file. This line omits SHARE and treats "not a short timeout" as enough.

lock-levels.md:50 also clears statement_timeout, because a SET from the previous file stays on the session. Name SHARE, and require SET lock_timeout = 0 and SET statement_timeout = 0 on a concurrent index, drop or reindex file. The plan analyst does not have this problem: it points at explain.md instead of copying the thresholds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a0cd09e. The line now names SHARE and flags a concurrent index build, drop or reindex whose file does not set lock_timeout = 0 and statement_timeout = 0. The same rule now reads identically in pgsql-migrations (ad306fd), in lock-levels.md ("or a generous bound" is dropped), and in patterns.md, the router checklist and the Cursor rule (83eaa20).


PASS if all of these hold:
- The answer says the teammate's migration is unsafe and names at least two concrete reasons, such as: goose runs the whole file in one transaction, so the `ACCESS EXCLUSIVE` lock from `ADD COLUMN` is held through the full-table `UPDATE`; `SET NOT NULL` scans the table under `ACCESS EXCLUSIVE`; the foreign key validates every row while blocking writes; the plain `CREATE INDEX` blocks writes for the whole build.
- Every rewritten migration file whose DDL takes `ACCESS EXCLUSIVE` or `SHARE ROW EXCLUSIVE` (adding the column, adding the foreign key or not-null constraint) sets `lock_timeout` before its first DDL statement. A file holding only concurrent index statements is not required to set it; setting `lock_timeout = 0` there, or saying that a short `lock_timeout` would abort the concurrent build, is correct.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A file of only concurrent index statements "is not required to set" lock_timeout, and the FAIL line at :15 only fails when the rewrite sets no lock_timeout anywhere. SET lock_timeout = '1s' directly above CREATE INDEX CONCURRENTLY still passes, which is the bug a5079a7 fixes.

FAIL when a concurrent index file sets a non-zero lock_timeout or statement_timeout. Treat an explicit 0 as required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9be360b. To pass, the file with the concurrent index statements must now set lock_timeout = 0 and statement_timeout = 0. A file with CREATE INDEX CONCURRENTLY or DROP INDEX CONCURRENTLY fails if either value is non-zero or unset.

I re-ran the case: 3 runs per arm, $1.87. With the plugin it scores 1.00 (3 of 3 pass). Without it, 0.00 (3 of 3 fail). None of the three no-plugin answers sets either timeout before CREATE INDEX CONCURRENTLY. Before this change, the no-plugin arm scored 0.67, so the case now tells the two arms apart on exactly this rule.

sebastian-iancu and others added 10 commits October 27, 2026 12:28
Both manifests' repository field, the issue-template README link and the
--plugin-dir examples in AGENTS.md, README.md, docs/install.md and
docs/versioning.md named Cadasto/pgsql-coding, which only resolved
through GitHub's rename redirect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CONCURRENTLY file sets statement_timeout = 0 as well as
lock_timeout = 0: a timeout set by an earlier migration file stays on
the session and would abort the long build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lters

plan-summary.py:
- does not report a misestimate on a node that stopped early: below a
  Limit, or on the inner side of a nested-loop semi or anti join, unless
  a Sort, Hash or hashed aggregate in between read all its input first;
- reports spills from incremental sort groups, HashAggregate batches and
  PG 18 tuplestores (Material, WindowAgg, CTE Scan), and filters that
  discard most rows through a join filter;
- names JSON nodes as TEXT does (aggregate strategy, partial mode, join
  type), and says when a plan ran with TIMING OFF.

explain.md gains the semi/anti-join early stop, the per-loop and total
counters (checked on 18.6, cited to explain.c) and the new spill lines.
plan-summary-test.py covers each case against real PostgreSQL 18 plans
added under scripts/testdata/plans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lock-levels.md drops "or a generous bound", patterns.md names statement_timeout beside lock_timeout, and the router and Cursor rule checklists say a concurrent build runs in a file that sets both to 0, matching pgsql-migrations and the migration-tools.md example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checklist named only ACCESS EXCLUSIVE and SHARE ROW EXCLUSIVE and accepted any concurrent build without a short lock_timeout. It now names SHARE and flags a concurrent index build, drop or reindex whose file does not set lock_timeout = 0 and statement_timeout = 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ENTLY

SET lock_timeout = '1s' above CREATE INDEX CONCURRENTLY passed whenever another file set a timeout. The concurrent file must now set lock_timeout = 0 and statement_timeout = 0, and a non-zero or missing value fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olded descriptions

A missing hooks.json or cursor-hooks.json passed silently; it is now an error. Claude Code commands must reach their script through ${CLAUDE_PLUGIN_ROOT} and Cursor commands must not use it. A folded or literal description block is measured whole instead of as '>-'. Each check has a --selftest case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cursor sends edits[].new_string, conversation_id and hook_event_name "afterFileEdit". skill-nudge matched the whole file instead of the added text, fell back to a per-process session id (so it nudged on every edit), and both hooks printed Claude's JSON because any hook_event_name counted. The hooks now read the added text and conversation_id, and print JSON only for PostToolUse. The tests use Cursor's documented payload and check session-start's exit code. The docs say Cursor documents no afterFileEdit output, so delivery there is unverified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pgsql-query

pgsql-jsonb keeps the COALESCE(jsonb_agg(x ORDER BY x), '[]') prescription and points at pgsql-query for why an aggregate over zero rows is NULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sebastian-iancu

Copy link
Copy Markdown
Contributor Author

Answer to the review of a5079a7. Everything is pushed; the head is ce0bc72 and both checks pass.

Important

  1. plan-summary.py: fixed in 7894023. Details are in the inline thread, including two places where I didn't follow the suggestion as written: the one-row estimate floor, and where a LIMIT's early stop ends.

  2. The reviewer checklist: fixed in a0cd09e (see the thread).

  3. The migrations grader: fixed in 9be360b (see the thread). In the re-run the case scores 1.00 with the plugin and 0.00 without.

  4. The validator, fixed in e544a2d:

    • a missing hook config is now an error;
    • a Claude Code hook command must use ${CLAUDE_PLUGIN_ROOT}, and a Cursor command must not;
    • a folded >- description is measured in full.

    Each check has its own --selftest case; there are now 19.

Suggestions

  • statement_timeout: done in ad306fd and 83eaa20. The skill, lock-levels.md ("or a generous bound" is gone), patterns.md, the router, the Cursor rule and the reviewer now state one rule: the file of a concurrent index build, drop or reindex sets both timeouts to 0.

  • The empty-aggregate NULL rule: done in ebca5a1. pgsql-jsonb keeps the jsonb_agg pattern and points to pgsql-query for the rule.

  • The skill nudge on Cursor: the docs' claim was wrong, and Cursor's hook docs (cursor.com/docs/agent/hooks) showed more problems:

    • Cursor documents no output for afterFileEdit, so neither the nudge nor the squawk findings reached Cursor's agent.
    • The nudge fired on every edit, because Cursor names the session conversation_id.
    • session-start ran from the plugin's install folder, so it looked for PostgreSQL there instead of in the project.
    • It also printed plain text where Cursor reads JSON.

    c91318d and ce0bc72 move Cursor to postToolUse (matcher Write) and sessionStart, reply with additional_context, and add the required "version": 1. Claude Code's hooks behave as before. One limit: Cursor doesn't document the input fields of its Write tool, so on Cursor the nudge matches the whole file and the scripts accept either file_path or path. The open Cursor install item in the checklist will confirm this works.

  • Tests:

    • The plan-summary tests went from 21 to 68. New checks cover exclusive time, the JSON subplan-loops plan with JIT under 10%, heap fetches, the join filter and TIMING OFF. Every TEXT/JSON pair must also give the same node names and findings.
    • The hook tests went from 40 to 48. They now check session-start's exit code and use Cursor's documented payloads.
  • Description lengths (988 and 998 characters): left for the planned rewrite of the skill descriptions, which will change them anyway.

The repository URL also now points at Cadasto/pgsql-coding-plugin (194a3ff).

🤖 Generated with Claude Code

…racts

Cursor documents no output for afterFileEdit, so lint findings and skill nudges never reached the Cursor agent. The Cursor config now carries "version": 1 and wires lint-on-save and skill-nudge to postToolUse with matcher Write; both answer with additional_context. session-start runs from the plugin's install directory under Cursor, so it inspects CURSOR_PROJECT_DIR and answers with JSON additional_context. Cursor documents no Write input fields, so the scripts accept file_path or path and the nudge matches the whole file there. The Claude Code path is unchanged; the validator maps PostToolUse to postToolUse and requires the version field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sebastian-iancu
sebastian-iancu merged commit 1243362 into main Oct 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant