From b6dfdeebf8c733a71f958b34565b34912d940bce Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Fri, 22 May 2026 22:56:21 -0700 Subject: [PATCH 01/13] feat(ansible): deploy Claude settings and Cursor rules via symlink Adds an agent_hooks task that links the in-repo `claude/settings.json` into `~/.claude/` and the `cursor/rules` directory into `~/.cursor/`, so both agents pick up shared config from this dotfiles checkout. Includes the initial set of Cursor rules (planning, dbt). Co-Authored-By: Claude Opus 4.7 (1M context) --- ansible/dotfiles.yml | 11 +++++++ ansible/tasks/agent_hooks.yml | 32 +++++++++++++++++++ cursor/README.md | 46 +++++++++++++++++++++++++++ cursor/rules/dbt.mdc | 51 ++++++++++++++++++++++++++++++ cursor/rules/planning.mdc | 59 +++++++++++++++++++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 ansible/tasks/agent_hooks.yml create mode 100644 cursor/README.md create mode 100644 cursor/rules/dbt.mdc create mode 100644 cursor/rules/planning.mdc diff --git a/ansible/dotfiles.yml b/ansible/dotfiles.yml index 1d4adc5..8e17781 100644 --- a/ansible/dotfiles.yml +++ b/ansible/dotfiles.yml @@ -65,6 +65,17 @@ - skills - setup + - name: Include agent hooks and Cursor rules deployment + ansible.builtin.include_tasks: + file: tasks/agent_hooks.yml + apply: + tags: + - hooks + - setup + tags: + - hooks + - setup + - name: Include paperless setup tasks include_tasks: tasks/paperless.yml tags: diff --git a/ansible/tasks/agent_hooks.yml b/ansible/tasks/agent_hooks.yml new file mode 100644 index 0000000..5e8f902 --- /dev/null +++ b/ansible/tasks/agent_hooks.yml @@ -0,0 +1,32 @@ +--- +- name: Ensure agent config directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ ansible_user_dir }}/.claude" + - "{{ ansible_user_dir }}/.cursor" + tags: + - hooks + - setup + +- name: Link Claude settings (global prompt hooks) + ansible.builtin.file: + src: "{{ dotfiles_dir }}/claude/settings.json" + dest: "{{ ansible_user_dir }}/.claude/settings.json" + state: link + force: true + tags: + - hooks + - setup + +- name: Link Cursor rules + ansible.builtin.file: + src: "{{ dotfiles_dir }}/cursor/rules" + dest: "{{ ansible_user_dir }}/.cursor/rules" + state: link + force: true + tags: + - hooks + - setup diff --git a/cursor/README.md b/cursor/README.md new file mode 100644 index 0000000..be45b8c --- /dev/null +++ b/cursor/README.md @@ -0,0 +1,46 @@ +# Cursor and Claude agent configuration + +User-level **prompt hooks** (via Claude settings) and **Cursor rules** apply across all projects when deployed. + +## Deployment + +Symlink into your home directory: + +```bash +ln -sf ~/.dotfiles/claude/settings.json ~/.claude/settings.json +ln -sf ~/.dotfiles/cursor/rules ~/.cursor/rules +``` + +Or run Ansible with the `hooks` or `setup` tag: + +```bash +./apply_ansible hooks +``` + +### Cursor: enable third-party hooks + +Hooks live in `~/.claude/settings.json` (Claude Code format). Cursor loads them when **Settings → Features → Third-party skills** is enabled. + +Project-level `.cursor/hooks.json` in a repo can still add project-specific hooks; global hooks come from `~/.claude/settings.json`. + +## What's included + +### Hooks (`~/.claude/settings.json`) + +| Hook | Event | What it does | +|------|-------|--------------| +| Plan quality gate | `PostToolUse` (Write/Edit) | Checks plans under `.cursor/plans/` or `.claude/plans/` for exit criteria, invariants, failure modes (+ premortem), assumptions & unknowns, outside view; MVC and substitution checks; reversibility + System 2 triggers for high-risk plans | +| Verify build | `Stop` | Reminds the agent to run build/render if `.sql`, `.yml`, or `.qmd` files were modified | + +dbt layer boundaries and other SQL conventions are enforced via **rules** (`dbt.mdc`), not hooks. + +### Rules (`~/.cursor/rules/`) + +| Rule | Scope | What it does | +|------|-------|--------------| +| `planning.mdc` | Always applied | Plan structure (including TFaS-inspired sections), minimal viable change, three-tier boundaries, visualization confirmation | +| `dbt.mdc` | `*.sql`, `*.yml` | Layer boundaries, grain docstrings, testing conventions, anti-patterns | + +## Project-specific extensions + +Add `.cursor/rules/` or `.cursor/hooks.json` in a project repo for domain-specific policy. Project hooks override user hooks where Cursor merges configs. diff --git a/cursor/rules/dbt.mdc b/cursor/rules/dbt.mdc new file mode 100644 index 0000000..163888d --- /dev/null +++ b/cursor/rules/dbt.mdc @@ -0,0 +1,51 @@ +--- +description: dbt modelling conventions +globs: "*.sql,*.yml" +alwaysApply: false +--- + +## Layer boundaries + +``` +staging -> intermediate -> mart/core (fct/dim) -> mart/ (consumer views) +``` + +- Staging models do minimal transformation (type casting, renaming). +- Intermediate models contain business logic. They reference staging or + other intermediates. **Intermediates NEVER reference mart models.** +- mart/core models (`fct_`, `dim_`) assemble intermediates into + business-facing tables with minimal additional logic. +- mart/ (top-level) models are thin views joining core models for specific + consumers. Consumers should reference these, not core tables directly. +- If a staging model is getting complex, push logic to an intermediate. + +## Grain and docstrings + +- Each model's SQL docstring starts with `Grain: ...` describing what one + row represents. +- Avoid stating implementation details obvious from the SQL itself. + +## Testing + +- Models should always have data tests: at minimum `not_null` and `unique` + on columns where expected. +- Use `dbt_utils` and `dbt_expectations` for integrity tests. +- When an ID column is a hash of dimension columns, add + `dbt_expectations.expect_column_distinct_count_to_equal` with `value: 1` + and `group_by` on those dimension columns. +- Catch data integrity issues as early as possible in the DAG. + +## Anti-patterns + +- Never hardcode thresholds as magic numbers. Compute dynamically from the + data or reference upstream constants. +- `SELECT DISTINCT` as a band-aid for duplicates is a code smell. Fix the + grain or join that produced them. +- `SELECT *` should be avoided (redundant in DuckDB, hides column changes + elsewhere). +- Avoid unnecessary `CROSS JOIN`s that produce redundant zero-filled rows. + +## Verification + +- After modifying any `.sql` or `.yml` model file, run the project's build + command and confirm success before reporting completion. diff --git a/cursor/rules/planning.mdc b/cursor/rules/planning.mdc new file mode 100644 index 0000000..82d9c97 --- /dev/null +++ b/cursor/rules/planning.mdc @@ -0,0 +1,59 @@ +--- +description: Planning and task execution rules +alwaysApply: true +--- + +## Planning + +Every plan MUST include these sections (bullets, reviewable in under 60 seconds): + +- **Exit criteria**: How we know the task is done. Always includes: + - Build succeeds (e.g. `just build`, `dbt build`) + - Rendered output is correct if applicable (e.g. `quarto render`) + - For refactors: before/after data snapshots with expected zero-row diffs +- **Invariants**: What must not change. Examples: + - "row counts in table X unchanged" + - "no new test failures" + - "downstream consumers produce identical output" +- **Failure modes**: What could go wrong. Examples: + - "join fan-out from a 1:N relationship" + - "grain change in intermediate breaks downstream" + - "database lock from stale process" + - **Premortem** (one line): "It is 3 months later and this failed because …" (top 2–3 concrete reasons) +- **Assumptions & unknowns**: What we treat as true but have not verified; what this plan does *not* cover; what would invalidate the plan ("if X is wrong, we stop") +- **Outside view**: Reference class — how similar work in this repo/class usually goes; what typically breaks; buffer vs best-case estimate (e.g. "add 2× for grain changes") + +For plans that change **grain, schema, deletes, or production data**, also include: + +- **Reversibility**: Rollback path, feature flag, or migration back-out +- **System 2 triggers**: Steps that need explicit human approval before the agent runs them (see "Ask first" below) + +### Three-tier boundaries for actions + +- **Always do**: run builds after SQL changes, add schema tests for new + models, verify renders after document changes. +- **Ask first**: adding new models, changing a model's grain, modifying mart + schema, adding dependencies. +- **Never do**: intermediate referencing a mart, hardcoded numeric + thresholds, `SELECT DISTINCT` to mask a grain problem. + +### Minimal viable change + +Propose the simplest design that works. One new column on an existing table +before a new table. One new table before a new model chain. Let the operator +request more complexity. + +### Confirm before building visualizations + +Before writing code for any figure, state and confirm: + (a) chart type, + (b) data universe / source table, + (c) x-axis metric, + (d) y-axis metric, + (e) grouping / faceting. +If uncertain about any of these, ask. Do not guess. + +### Plan format + +Plans should be concise and scannable. Use bullet points, not paragraphs. +The operator should be able to review a plan in under 60 seconds. From f195e9866ffbd7e145b10ddc80acbd925edfe822 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Fri, 22 May 2026 22:56:42 -0700 Subject: [PATCH 02/13] feat(claude): add path-gated PostToolUse and Stop hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two command-type hooks replace earlier prompt-type hooks that would narrate past their own stated gate (e.g. flagging a JSON config edit as "a YAML build artifact"). Command hooks make the path filter deterministic in shell — the gate either matches or the script exits silently, no model interpretation involved. - `plan-review.sh` (PostToolUse on Write|Edit): only fires when the edited file is under `.cursor/plans/` or `.claude/plans/`. On a match, injects a strict plan-quality checklist as additional context. - `stop-check.sh` (Stop): only fires when the current turn edited a `.sql` file inside a dbt project (has a `dbt_project.yml` ancestor) or any `.qmd` file, AND no build/render command ran. Suggests the matching command per file group. Both scripts are referenced via `$HOME/...` so the deployment is portable across machines and accounts. Co-Authored-By: Claude Opus 4.7 (1M context) --- claude/hooks/plan-review.sh | 37 +++++++++++++++ claude/hooks/stop-check.sh | 94 +++++++++++++++++++++++++++++++++++++ claude/settings.json | 38 +++++++++++++++ 3 files changed, 169 insertions(+) create mode 100755 claude/hooks/plan-review.sh create mode 100755 claude/hooks/stop-check.sh create mode 100644 claude/settings.json diff --git a/claude/hooks/plan-review.sh b/claude/hooks/plan-review.sh new file mode 100755 index 0000000..773d4f7 --- /dev/null +++ b/claude/hooks/plan-review.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# PostToolUse(Write|Edit) hook: review plan documents only. +# Path-gates first so non-plan edits exit silently, then injects a strict +# review checklist as additional_context for the main agent. + +set -euo pipefail + +input=$(cat) +file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty') + +case "$file_path" in + */.cursor/plans/*|*/.claude/plans/*) ;; + *) exit 0 ;; +esac + +cat <<'PROMPT' +You just edited a plan document. Before any implementation, audit the plan against the gates below. Be strict: a gate passes ONLY if you can quote the line(s) that satisfy it. Paraphrases, "implied somewhere," or "this is obvious" do NOT count. + +REQUIRED GATES (every plan): +1. Exit criteria — at least one falsifiable check (specific command succeeds, diff matches, render produces expected output). Reject vibes like "works as expected" or "looks right." +2. Invariants — explicit list of what must not change (schemas, public APIs, file paths, observed behavior). +3. Failure modes — concrete risks AND a Premortem in the form: "It is 3 months later and this failed because …" with top 2–3 reasons. Both required. +4. Assumptions & unknowns — what is assumed true, what is unverified, what would invalidate the plan. +5. Outside view — reference class (similar tasks or repo history), what usually breaks for that class, realistic buffer vs best case. +6. Minimal viable change — simplest design described before any optional extras. +7. Substitution check — plan addresses the user's actual ask, not an easier adjacent problem. + +CONDITIONAL GATES (required if the plan changes grain, schema, deletes data, or touches production): +8. Reversibility — concrete rollback path. +9. System 2 triggers — steps that require explicit human approval before execution. + +OUTPUT FORMAT: +- If every applicable gate passes: output exactly "PLAN OK" on a single line and nothing else. +- Otherwise: output a bulleted list. Each bullet: "Gate N (): ". Then stop and ask the user to fill the gaps before implementing. + +Do not begin implementation until the plan passes. +PROMPT diff --git a/claude/hooks/stop-check.sh b/claude/hooks/stop-check.sh new file mode 100755 index 0000000..5498192 --- /dev/null +++ b/claude/hooks/stop-check.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Stop hook: nag about build/render only when, in the current turn: +# - a .sql file was edited inside a dbt project (dbt_project.yml ancestor), or +# - a .qmd file was edited (any location — quarto renders standalone files) +# AND no build/render command was run this turn. + +set -euo pipefail + +input=$(cat) +transcript=$(printf '%s' "$input" | jq -r '.transcript_path // empty') + +if [ -z "$transcript" ] || [ ! -f "$transcript" ]; then + exit 0 +fi + +last_user_line=$(grep -n '"type":"user"' "$transcript" | tail -1 | cut -d: -f1) +last_user_line=${last_user_line:-0} +recent=$(tail -n +"$((last_user_line + 1))" "$transcript") + +edits=$(printf '%s' "$recent" | jq -r ' + select(.type == "assistant") | + .message.content[]? | + select(.type == "tool_use") | + select(.name == "Write" or .name == "Edit") | + .input.file_path // empty +' 2>/dev/null | grep -E '\.(sql|qmd)$' || true) + +if [ -z "$edits" ]; then + exit 0 +fi + +# Walk up from a directory looking for any of the given marker filenames. +# Returns 0 if any marker is found, 1 otherwise. +has_marker() { + local dir="$1" + shift + while [ -n "$dir" ] && [ "$dir" != "/" ]; do + for marker in "$@"; do + [ -f "$dir/$marker" ] && return 0 + done + dir=$(dirname "$dir") + done + return 1 +} + +dbt_files="" +quarto_files="" + +while IFS= read -r path; do + [ -z "$path" ] && continue + case "$path" in + *.sql) + if has_marker "$(dirname "$path")" dbt_project.yml; then + dbt_files="${dbt_files}${path}"$'\n' + fi + ;; + *.qmd) + quarto_files="${quarto_files}${path}"$'\n' + ;; + esac +done <<< "$edits" + +if [ -z "$dbt_files" ] && [ -z "$quarto_files" ]; then + exit 0 +fi + +bashes=$(printf '%s' "$recent" | jq -r ' + select(.type == "assistant") | + .message.content[]? | + select(.type == "tool_use" and .name == "Bash") | + .input.command // empty +' 2>/dev/null || true) + +if printf '%s' "$bashes" | grep -qE '(just (build|html|render)|dbt (build|run|test)|quarto render)'; then + exit 0 +fi + +{ + echo "You edited build-relevant files this turn but did not run a build/render command:" + if [ -n "$dbt_files" ]; then + echo + echo "dbt project files (dbt_project.yml ancestor found):" + printf '%s' "$dbt_files" | sed 's/^/ /' + echo " → run \`dbt build\` (or \`just build\` if the project has a justfile target)." + fi + if [ -n "$quarto_files" ]; then + echo + echo "quarto documents:" + printf '%s' "$quarto_files" | sed 's/^/ /' + echo " → run \`quarto render \` (or \`just html\`/\`just render\` if the project has it)." + fi + echo + echo "Verify before stopping." +} diff --git a/claude/settings.json b/claude/settings.json new file mode 100644 index 0000000..08e99c9 --- /dev/null +++ b/claude/settings.json @@ -0,0 +1,38 @@ +{ + "permissions": { + "allow": [ + "Bash(uv run *)", + "Bash(just *)", + "Bash(duckdb *)" + ], + "defaultMode": "auto" + }, + "model": "sonnet[1m]", + "theme": "auto", + "skipAutoPermissionPrompt": true, + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "$HOME/.dotfiles/claude/hooks/plan-review.sh", + "timeout": 5 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "$HOME/.dotfiles/claude/hooks/stop-check.sh", + "timeout": 5 + } + ] + } + ] + } +} From 813ab59ec2409c85970e371250756405af28c3eb Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Tue, 26 May 2026 08:53:47 -0700 Subject: [PATCH 03/13] refactor(claude): move plan-review prompt to claude/prompts/ Extracts the inline heredoc out of `plan-review.sh` into a dedicated markdown file so the prompt picks up editor/formatter support and so future prompts can sit alongside it under `claude/prompts/`. The script resolves the file relative to its own location, so it remains portable across machines. Co-Authored-By: Claude Opus 4.7 (1M context) --- claude/hooks/plan-review.sh | 31 ++++++++------------------ claude/prompts/plan-review.md | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 claude/prompts/plan-review.md diff --git a/claude/hooks/plan-review.sh b/claude/hooks/plan-review.sh index 773d4f7..a3fb3c1 100755 --- a/claude/hooks/plan-review.sh +++ b/claude/hooks/plan-review.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # PostToolUse(Write|Edit) hook: review plan documents only. -# Path-gates first so non-plan edits exit silently, then injects a strict -# review checklist as additional_context for the main agent. +# Path-gates first so non-plan edits exit silently, then prints the prompt +# from ../prompts/plan-review.md as additional_context for the main agent. set -euo pipefail @@ -13,25 +13,12 @@ case "$file_path" in *) exit 0 ;; esac -cat <<'PROMPT' -You just edited a plan document. Before any implementation, audit the plan against the gates below. Be strict: a gate passes ONLY if you can quote the line(s) that satisfy it. Paraphrases, "implied somewhere," or "this is obvious" do NOT count. +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +prompt_file="$script_dir/../prompts/plan-review.md" -REQUIRED GATES (every plan): -1. Exit criteria — at least one falsifiable check (specific command succeeds, diff matches, render produces expected output). Reject vibes like "works as expected" or "looks right." -2. Invariants — explicit list of what must not change (schemas, public APIs, file paths, observed behavior). -3. Failure modes — concrete risks AND a Premortem in the form: "It is 3 months later and this failed because …" with top 2–3 reasons. Both required. -4. Assumptions & unknowns — what is assumed true, what is unverified, what would invalidate the plan. -5. Outside view — reference class (similar tasks or repo history), what usually breaks for that class, realistic buffer vs best case. -6. Minimal viable change — simplest design described before any optional extras. -7. Substitution check — plan addresses the user's actual ask, not an easier adjacent problem. +if [ ! -f "$prompt_file" ]; then + echo "plan-review hook: missing prompt file at $prompt_file" >&2 + exit 1 +fi -CONDITIONAL GATES (required if the plan changes grain, schema, deletes data, or touches production): -8. Reversibility — concrete rollback path. -9. System 2 triggers — steps that require explicit human approval before execution. - -OUTPUT FORMAT: -- If every applicable gate passes: output exactly "PLAN OK" on a single line and nothing else. -- Otherwise: output a bulleted list. Each bullet: "Gate N (): ". Then stop and ask the user to fill the gaps before implementing. - -Do not begin implementation until the plan passes. -PROMPT +cat "$prompt_file" diff --git a/claude/prompts/plan-review.md b/claude/prompts/plan-review.md new file mode 100644 index 0000000..516c718 --- /dev/null +++ b/claude/prompts/plan-review.md @@ -0,0 +1,42 @@ +You just edited a plan document. Before any implementation, audit the plan +against the gates below. Be strict: a gate passes ONLY if you can quote the +line(s) that satisfy it. Paraphrases, "implied somewhere," or "this is +obvious" do NOT count. + +## Required gates (every plan) + +1. **Exit criteria** — at least one falsifiable check (specific command + succeeds, diff matches, render produces expected output). Reject vibes + like "works as expected" or "looks right." +2. **Invariants** — explicit list of what must not change (schemas, public + APIs, file paths, observed behavior). +3. **Failure modes** — concrete risks AND a Premortem in the form: "It is + 3 months later and this failed because …" with top 2–3 reasons. Both + required. +4. **Assumptions & unknowns** — what is assumed true, what is unverified, + what would invalidate the plan. +5. **Outside view** — reference class (similar tasks or repo history), + what usually breaks for that class, realistic buffer vs best case. +6. **Minimal viable change** — simplest design described before any + optional extras. +7. **Substitution check** — plan addresses the user's actual ask, not an + easier adjacent problem. + +## Conditional gates + +Required if the plan changes grain, schema, deletes data, or touches +production: + +8. **Reversibility** — concrete rollback path. +9. **System 2 triggers** — steps that require explicit human approval + before execution. + +## Output format + +- If every applicable gate passes: output exactly `PLAN OK` on a single + line and nothing else. +- Otherwise: output a bulleted list. Each bullet: + `Gate N (): `. Then stop and ask the user to + fill the gaps before implementing. + +Do not begin implementation until the plan passes. From d432a9f3f17cb6a51556cce7dd3fbefe2d5071bf Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Tue, 26 May 2026 08:57:11 -0700 Subject: [PATCH 04/13] refactor: make planning.mdc the single source for plan gates Previously `plan-review.md` and `planning.mdc` each restated the same five core gates (exit criteria, invariants, failure modes + premortem, assumptions & unknowns, outside view) plus the conditional reversibility and System 2 gates. Editing one and forgetting the other would silently drift the contract. The hook script now reads `cursor/rules/planning.mdc` (strips its YAML frontmatter) and substitutes its body into the `{{PLANNING_RULE}}` placeholder in `plan-review.md`. `plan-review.md` keeps only the audit framing and the `PLAN OK` / gap-list output contract. Co-Authored-By: Claude Opus 4.7 (1M context) --- claude/hooks/plan-review.sh | 30 +++++++++++++++------ claude/prompts/plan-review.md | 49 ++++++++++------------------------- cursor/README.md | 2 +- 3 files changed, 36 insertions(+), 45 deletions(-) diff --git a/claude/hooks/plan-review.sh b/claude/hooks/plan-review.sh index a3fb3c1..dde9b21 100755 --- a/claude/hooks/plan-review.sh +++ b/claude/hooks/plan-review.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# PostToolUse(Write|Edit) hook: review plan documents only. -# Path-gates first so non-plan edits exit silently, then prints the prompt -# from ../prompts/plan-review.md as additional_context for the main agent. +# PostToolUse(Write|Edit) hook: review plan documents. +# Path-gates first so non-plan edits exit silently, then assembles the prompt +# from ../prompts/plan-review.md with {{PLANNING_RULE}} substituted by the +# body of cursor/rules/planning.mdc (the canonical gate definitions). set -euo pipefail @@ -15,10 +16,23 @@ esac script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) prompt_file="$script_dir/../prompts/plan-review.md" +planning_rule="$script_dir/../../cursor/rules/planning.mdc" -if [ ! -f "$prompt_file" ]; then - echo "plan-review hook: missing prompt file at $prompt_file" >&2 - exit 1 -fi +for f in "$prompt_file" "$planning_rule"; do + if [ ! -f "$f" ]; then + echo "plan-review hook: missing file $f" >&2 + exit 1 + fi +done -cat "$prompt_file" +# Strip the YAML frontmatter (between the first two `---` lines) from the +# planning rule so only the rule body is injected. +planning_body=$(awk 'BEGIN{f=0} /^---$/{f++; next} f>=2{print}' "$planning_rule") + +while IFS= read -r line || [ -n "$line" ]; do + if [ "$line" = "{{PLANNING_RULE}}" ]; then + printf '%s\n' "$planning_body" + else + printf '%s\n' "$line" + fi +done < "$prompt_file" diff --git a/claude/prompts/plan-review.md b/claude/prompts/plan-review.md index 516c718..64301ee 100644 --- a/claude/prompts/plan-review.md +++ b/claude/prompts/plan-review.md @@ -1,42 +1,19 @@ -You just edited a plan document. Before any implementation, audit the plan -against the gates below. Be strict: a gate passes ONLY if you can quote the -line(s) that satisfy it. Paraphrases, "implied somewhere," or "this is -obvious" do NOT count. +You just edited a plan document. Before any implementation, audit it +against the planning rule below (the canonical gate definitions). Be +strict: a gate passes ONLY if you can quote the line(s) that satisfy it. +Paraphrases, "implied somewhere," or "this is obvious" do NOT count. -## Required gates (every plan) +--- -1. **Exit criteria** — at least one falsifiable check (specific command - succeeds, diff matches, render produces expected output). Reject vibes - like "works as expected" or "looks right." -2. **Invariants** — explicit list of what must not change (schemas, public - APIs, file paths, observed behavior). -3. **Failure modes** — concrete risks AND a Premortem in the form: "It is - 3 months later and this failed because …" with top 2–3 reasons. Both - required. -4. **Assumptions & unknowns** — what is assumed true, what is unverified, - what would invalidate the plan. -5. **Outside view** — reference class (similar tasks or repo history), - what usually breaks for that class, realistic buffer vs best case. -6. **Minimal viable change** — simplest design described before any - optional extras. -7. **Substitution check** — plan addresses the user's actual ask, not an - easier adjacent problem. +{{PLANNING_RULE}} -## Conditional gates +--- -Required if the plan changes grain, schema, deletes data, or touches -production: +## Audit output -8. **Reversibility** — concrete rollback path. -9. **System 2 triggers** — steps that require explicit human approval - before execution. +If every applicable gate passes: output exactly `PLAN OK` on a single +line and nothing else. -## Output format - -- If every applicable gate passes: output exactly `PLAN OK` on a single - line and nothing else. -- Otherwise: output a bulleted list. Each bullet: - `Gate N (): `. Then stop and ask the user to - fill the gaps before implementing. - -Do not begin implementation until the plan passes. +Otherwise: output a bulleted gap list. Each bullet: +`Gate : `. Then stop and ask the user to fill the +gaps before implementing. diff --git a/cursor/README.md b/cursor/README.md index be45b8c..89f0bdd 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -29,7 +29,7 @@ Project-level `.cursor/hooks.json` in a repo can still add project-specific hook | Hook | Event | What it does | |------|-------|--------------| -| Plan quality gate | `PostToolUse` (Write/Edit) | Checks plans under `.cursor/plans/` or `.claude/plans/` for exit criteria, invariants, failure modes (+ premortem), assumptions & unknowns, outside view; MVC and substitution checks; reversibility + System 2 triggers for high-risk plans | +| Plan quality gate | `PostToolUse` (Write/Edit) | When a plan under `.cursor/plans/` or `.claude/plans/` is edited, audits it against the gates defined in `planning.mdc` (single source of truth). The hook script materializes the prompt from `claude/prompts/plan-review.md` with `planning.mdc` inlined. | | Verify build | `Stop` | Reminds the agent to run build/render if `.sql`, `.yml`, or `.qmd` files were modified | dbt layer boundaries and other SQL conventions are enforced via **rules** (`dbt.mdc`), not hooks. From ce40cede9dcf88ad6f10d80bcef24b0dbe18d800 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Tue, 26 May 2026 09:01:06 -0700 Subject: [PATCH 05/13] refactor(claude): inline plan-review framing into the hook script The `claude/prompts/plan-review.md` template only held ~10 lines of audit framing + an output contract wrapping a `{{PLANNING_RULE}}` placeholder. Claude Code has no convention for a `prompts/` directory, and the substitution loop was more indirection than the content deserves. Inlining the framing as two small heredocs around the canonical `planning.mdc` body keeps the single-source-of-truth property and drops a file. Co-Authored-By: Claude Opus 4.7 (1M context) --- claude/hooks/plan-review.sh | 53 ++++++++++++++++++++--------------- claude/prompts/plan-review.md | 19 ------------- cursor/README.md | 2 +- 3 files changed, 32 insertions(+), 42 deletions(-) delete mode 100644 claude/prompts/plan-review.md diff --git a/claude/hooks/plan-review.sh b/claude/hooks/plan-review.sh index dde9b21..f264e15 100755 --- a/claude/hooks/plan-review.sh +++ b/claude/hooks/plan-review.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash # PostToolUse(Write|Edit) hook: review plan documents. -# Path-gates first so non-plan edits exit silently, then assembles the prompt -# from ../prompts/plan-review.md with {{PLANNING_RULE}} substituted by the -# body of cursor/rules/planning.mdc (the canonical gate definitions). +# Path-gates first so non-plan edits exit silently, then prints an audit +# prompt that wraps the canonical planning rule (cursor/rules/planning.mdc). set -euo pipefail @@ -15,24 +14,34 @@ case "$file_path" in esac script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) -prompt_file="$script_dir/../prompts/plan-review.md" planning_rule="$script_dir/../../cursor/rules/planning.mdc" -for f in "$prompt_file" "$planning_rule"; do - if [ ! -f "$f" ]; then - echo "plan-review hook: missing file $f" >&2 - exit 1 - fi -done - -# Strip the YAML frontmatter (between the first two `---` lines) from the -# planning rule so only the rule body is injected. -planning_body=$(awk 'BEGIN{f=0} /^---$/{f++; next} f>=2{print}' "$planning_rule") - -while IFS= read -r line || [ -n "$line" ]; do - if [ "$line" = "{{PLANNING_RULE}}" ]; then - printf '%s\n' "$planning_body" - else - printf '%s\n' "$line" - fi -done < "$prompt_file" +if [ ! -f "$planning_rule" ]; then + echo "plan-review hook: missing planning rule at $planning_rule" >&2 + exit 1 +fi + +cat <<'PREAMBLE' +You just edited a plan document. Before any implementation, audit it +against the planning rule below (the canonical gate definitions). Be +strict: a gate passes ONLY if you can quote the line(s) that satisfy it. +Paraphrases, "implied somewhere," or "this is obvious" do NOT count. + +--- +PREAMBLE + +# Strip the YAML frontmatter (between the first two `---` lines). +awk 'BEGIN{f=0} /^---$/{f++; next} f>=2{print}' "$planning_rule" + +cat <<'POSTAMBLE' +--- + +## Audit output + +If every applicable gate passes: output exactly `PLAN OK` on a single +line and nothing else. + +Otherwise: output a bulleted gap list. Each bullet: +`Gate : `. Then stop and ask the user to fill the +gaps before implementing. +POSTAMBLE diff --git a/claude/prompts/plan-review.md b/claude/prompts/plan-review.md deleted file mode 100644 index 64301ee..0000000 --- a/claude/prompts/plan-review.md +++ /dev/null @@ -1,19 +0,0 @@ -You just edited a plan document. Before any implementation, audit it -against the planning rule below (the canonical gate definitions). Be -strict: a gate passes ONLY if you can quote the line(s) that satisfy it. -Paraphrases, "implied somewhere," or "this is obvious" do NOT count. - ---- - -{{PLANNING_RULE}} - ---- - -## Audit output - -If every applicable gate passes: output exactly `PLAN OK` on a single -line and nothing else. - -Otherwise: output a bulleted gap list. Each bullet: -`Gate : `. Then stop and ask the user to fill the -gaps before implementing. diff --git a/cursor/README.md b/cursor/README.md index 89f0bdd..cb579b5 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -29,7 +29,7 @@ Project-level `.cursor/hooks.json` in a repo can still add project-specific hook | Hook | Event | What it does | |------|-------|--------------| -| Plan quality gate | `PostToolUse` (Write/Edit) | When a plan under `.cursor/plans/` or `.claude/plans/` is edited, audits it against the gates defined in `planning.mdc` (single source of truth). The hook script materializes the prompt from `claude/prompts/plan-review.md` with `planning.mdc` inlined. | +| Plan quality gate | `PostToolUse` (Write/Edit) | When a plan under `.cursor/plans/` or `.claude/plans/` is edited, audits it against the gates defined in `planning.mdc` (single source of truth). The hook script inlines the rule body and wraps it with audit framing + `PLAN OK` / gap-list output contract. | | Verify build | `Stop` | Reminds the agent to run build/render if `.sql`, `.yml`, or `.qmd` files were modified | dbt layer boundaries and other SQL conventions are enforced via **rules** (`dbt.mdc`), not hooks. From fa05c986a5225fb9efa8b8e9793c8daceaa310b6 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Tue, 26 May 2026 09:17:54 -0700 Subject: [PATCH 06/13] refactor(planning): drop production-eng gates for analytical work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes from `cursor/rules/planning.mdc`: - "Reversibility" and "System 2 triggers" conditional gates — these are geared at production deploys / data migrations rather than the analytical/dbt work that's the actual day-to-day. - "Three-tier boundaries for actions" — overly prescriptive, and the "Never do" list in particular bakes in dbt-shop-specific anti-patterns that don't generalize. Five core gates (exit criteria, invariants, failure modes + premortem, assumptions & unknowns, outside view), minimal viable change, viz confirmation, and plan format all retained. Co-Authored-By: Claude Opus 4.7 (1M context) --- cursor/README.md | 2 +- cursor/rules/planning.mdc | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/cursor/README.md b/cursor/README.md index cb579b5..bcdba82 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -38,7 +38,7 @@ dbt layer boundaries and other SQL conventions are enforced via **rules** (`dbt. | Rule | Scope | What it does | |------|-------|--------------| -| `planning.mdc` | Always applied | Plan structure (including TFaS-inspired sections), minimal viable change, three-tier boundaries, visualization confirmation | +| `planning.mdc` | Always applied | Plan structure (exit criteria, invariants, failure modes + premortem, assumptions & unknowns, outside view), minimal viable change, visualization confirmation | | `dbt.mdc` | `*.sql`, `*.yml` | Layer boundaries, grain docstrings, testing conventions, anti-patterns | ## Project-specific extensions diff --git a/cursor/rules/planning.mdc b/cursor/rules/planning.mdc index 82d9c97..83016e1 100644 --- a/cursor/rules/planning.mdc +++ b/cursor/rules/planning.mdc @@ -23,20 +23,6 @@ Every plan MUST include these sections (bullets, reviewable in under 60 seconds) - **Assumptions & unknowns**: What we treat as true but have not verified; what this plan does *not* cover; what would invalidate the plan ("if X is wrong, we stop") - **Outside view**: Reference class — how similar work in this repo/class usually goes; what typically breaks; buffer vs best-case estimate (e.g. "add 2× for grain changes") -For plans that change **grain, schema, deletes, or production data**, also include: - -- **Reversibility**: Rollback path, feature flag, or migration back-out -- **System 2 triggers**: Steps that need explicit human approval before the agent runs them (see "Ask first" below) - -### Three-tier boundaries for actions - -- **Always do**: run builds after SQL changes, add schema tests for new - models, verify renders after document changes. -- **Ask first**: adding new models, changing a model's grain, modifying mart - schema, adding dependencies. -- **Never do**: intermediate referencing a mart, hardcoded numeric - thresholds, `SELECT DISTINCT` to mask a grain problem. - ### Minimal viable change Propose the simplest design that works. One new column on an existing table From 355d285a92241dd12e0c729aec9982450514107a Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Thu, 11 Jun 2026 15:41:56 -0700 Subject: [PATCH 07/13] Fix quarto formatting --- ansible/tasks/neovim.yml | 1 + nvim/ftplugin/quarto.lua | 9 +++++ nvim/lua/plugins/language.lua | 63 +++++++++++++++++++++++------------ 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/ansible/tasks/neovim.yml b/ansible/tasks/neovim.yml index efb9fd6..dd437b3 100644 --- a/ansible/tasks/neovim.yml +++ b/ansible/tasks/neovim.yml @@ -9,6 +9,7 @@ - debugpy - isort - mdformat + - mdformat-frontmatter - neovim - pynvim - ruff diff --git a/nvim/ftplugin/quarto.lua b/nvim/ftplugin/quarto.lua index fd04efa..b63182c 100644 --- a/nvim/ftplugin/quarto.lua +++ b/nvim/ftplugin/quarto.lua @@ -10,6 +10,15 @@ vim.b.completion = false -- don't run vim ftplugin on top vim.api.nvim_buf_set_var(0, "did_ftplugin", true) +-- LSP attaches after the ftplugin and re-sets formatexpr; clear it again so +-- gq uses vim's built-in paragraph reflower instead of the LSP code formatter. +vim.api.nvim_create_autocmd("LspAttach", { + buffer = 0, + callback = function() + vim.opt_local.formatexpr = "" + end, +}) + -- markdown vs. quarto hacks local ns = vim.api.nvim_create_namespace("QuartoHighlight") vim.api.nvim_set_hl(ns, "@markup.strikethrough", { strikethrough = false }) diff --git a/nvim/lua/plugins/language.lua b/nvim/lua/plugins/language.lua index f8912b0..b064ff3 100644 --- a/nvim/lua/plugins/language.lua +++ b/nvim/lua/plugins/language.lua @@ -41,12 +41,16 @@ return { "stevearc/conform.nvim", opts = { formatters_by_ft = { - quarto = { "injected" }, + -- injected formats code cells via treesitter language injections; + -- prettier then hard-wraps prose. prettier is used over mdformat for + -- quarto because it preserves {{< >}} shortcodes, which mdformat + -- escapes. Caveat: ::: div content written without surrounding blank + -- lines gets joined into one paragraph by both tools. + quarto = { "injected", "prettier" }, sql = { "sqlfmt" }, markdown = { "mdformat" }, vimwiki = { "mdformat" }, }, - -- Configure mdformat with text wrapping and formatting options formatters = { mdformat = { prepend_args = { @@ -54,27 +58,42 @@ return { "--number", -- Use numbered lists consistently }, }, - }, - -- See: - -- https://github.com/jmbuhr/quarto-nvim-kickstarter/blob/382b050e13eada7180ad048842386be37e820660/lua/plugins/editing.lua#L29-L81 - injected = { - -- Set the options field - options = { - -- Set to true to ignore errors - ignore_errors = false, - -- Map of treesitter language to file extension - -- A temporary file name with this extension will be generated during formatting - -- because some formatters care about the filename. - lang_to_ext = { - bash = "sh", - latex = "tex", - markdown = "md", - python = "py", - vimwiki = "md", + prettier = { + prepend_args = { "--prose-wrap", "always", "--print-width", "80" }, + options = { + -- prettier can't infer a parser from the .qmd extension + ft_parsers = { quarto = "markdown" }, + }, + }, + -- Use the ansible-managed venv ruff rather than mason's, which sits + -- first on PATH inside nvim but lags behind: preserving quarto's `#|` + -- cell-option comments (instead of rewriting them to `# |`) needs + -- ruff >= 0.15.17. + ruff_format = { + command = vim.fn.expand("~/.venvs/nvim/bin/ruff"), + }, + -- See: + -- https://github.com/jmbuhr/quarto-nvim-kickstarter/blob/382b050e13eada7180ad048842386be37e820660/lua/plugins/editing.lua#L29-L81 + injected = { + options = { + ignore_errors = false, + -- Map of treesitter language to file extension + -- A temporary file name with this extension will be generated during formatting + -- because some formatters care about the filename. + lang_to_ext = { + bash = "sh", + latex = "tex", + markdown = "md", + python = "py", + vimwiki = "md", + }, + -- Code cells need an explicit formatter entry here: python files + -- are normally formatted by the ruff LSP, which the injected + -- formatter cannot call. + lang_to_formatters = { + python = { "ruff_format" }, + }, }, - -- Map of treesitter language to formatters to use - -- (defaults to the value from formatters_by_ft) - lang_to_formatters = {}, }, }, }, From cd7bc580ceffd4530c045d25857377eac1037668 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Thu, 11 Jun 2026 15:42:58 -0700 Subject: [PATCH 08/13] refactor(claude): reorganize settings.json, add effortLevel and tui Co-Authored-By: Claude Sonnet 4.6 --- claude/settings.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/claude/settings.json b/claude/settings.json index 08e99c9..31559b5 100644 --- a/claude/settings.json +++ b/claude/settings.json @@ -7,9 +7,6 @@ ], "defaultMode": "auto" }, - "model": "sonnet[1m]", - "theme": "auto", - "skipAutoPermissionPrompt": true, "hooks": { "PostToolUse": [ { @@ -34,5 +31,9 @@ ] } ] - } + }, + "effortLevel": "xhigh", + "tui": "fullscreen", + "theme": "auto", + "skipAutoPermissionPrompt": true } From f2b346ecf10091bef283ee48a5ca41510a89f8da Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Fri, 12 Jun 2026 10:17:09 -0700 Subject: [PATCH 09/13] refactor(claude): harden hook prompts; add turn tracking for stop-check Plan-review audit prompt now follows judge-prompt best practice: named gates, per-gate PASS/FAIL/N-A verdicts with quoted evidence, strict default on uncertainty, XML-delimited rule body, and the full rule injected every edit (session marker went stale after compaction). Stop-check nag spells out what verification means and gives an honest exit for false positives. New track-tool-use.sh records this turn's edits/commands to a state file so stop-check can target dbt/quarto files without parsing the transcript. Co-Authored-By: Claude Fable 5 --- claude/hooks/plan-review.sh | 63 ++++++++++++++++++++++---------- claude/hooks/stop-check.sh | 67 +++++++++++++++++++++------------- claude/hooks/track-tool-use.sh | 24 ++++++++++++ claude/settings.json | 13 ++++++- cursor/README.md | 7 +++- 5 files changed, 127 insertions(+), 47 deletions(-) create mode 100755 claude/hooks/track-tool-use.sh diff --git a/claude/hooks/plan-review.sh b/claude/hooks/plan-review.sh index f264e15..d14d53c 100755 --- a/claude/hooks/plan-review.sh +++ b/claude/hooks/plan-review.sh @@ -1,7 +1,12 @@ #!/usr/bin/env bash # PostToolUse(Write|Edit) hook: review plan documents. -# Path-gates first so non-plan edits exit silently, then prints an audit -# prompt that wraps the canonical planning rule (cursor/rules/planning.mdc). +# Path-gates first so non-plan edits exit silently, then emits an audit +# prompt wrapping the canonical planning rule (cursor/rules/planning.mdc) +# as PostToolUse additionalContext JSON — plain stdout at exit 0 never +# reaches the model. The full rule (~300 tokens) is injected on every +# plan edit: a once-per-session marker would go stale after context +# compaction, leaving the model auditing against a rubric it no longer +# has. set -euo pipefail @@ -9,10 +14,15 @@ input=$(cat) file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty') case "$file_path" in - */.cursor/plans/*|*/.claude/plans/*) ;; + */.cursor/plans/* | */.claude/plans/*) ;; *) exit 0 ;; esac +emit() { + jq -n --arg ctx "$1" \ + '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}}' +} + script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) planning_rule="$script_dir/../../cursor/rules/planning.mdc" @@ -21,27 +31,42 @@ if [ ! -f "$planning_rule" ]; then exit 1 fi -cat <<'PREAMBLE' +# Strip the YAML frontmatter (between the first two `---` lines). +rule_body=$(awk 'BEGIN{f=0} /^---$/{f++; next} f>=2{print}' "$planning_rule") + +prompt=$( + cat < below. The gates are: ---- -PREAMBLE +1. Exit criteria +2. Invariants +3. Failure modes (including the one-line premortem) +4. Assumptions & unknowns +5. Outside view +6. Minimal viable change +7. Visualization confirmation (only when the plan involves figures) -# Strip the YAML frontmatter (between the first two `---` lines). -awk 'BEGIN{f=0} /^---$/{f++; next} f>=2{print}' "$planning_rule" +A gate passes ONLY if you can quote the plan line(s) that satisfy it. +Paraphrases, "implied somewhere," or "this is obvious" do NOT count. +If you are unsure whether a quote satisfies a gate, the gate FAILS. -cat <<'POSTAMBLE' ---- + +$rule_body + ## Audit output -If every applicable gate passes: output exactly `PLAN OK` on a single -line and nothing else. +Output exactly one line per gate, in the order listed above: + +\`: PASS — ""\` +\`: FAIL — \` +\`: N/A — \` + +If there are no FAIL lines, end with \`PLAN OK\` on its own line and +begin implementing. Otherwise stop and ask the user to fill the gaps +before implementing. +PROMPT +) -Otherwise: output a bulleted gap list. Each bullet: -`Gate : `. Then stop and ask the user to fill the -gaps before implementing. -POSTAMBLE +emit "$prompt" diff --git a/claude/hooks/stop-check.sh b/claude/hooks/stop-check.sh index 5498192..e4de0c2 100755 --- a/claude/hooks/stop-check.sh +++ b/claude/hooks/stop-check.sh @@ -1,31 +1,45 @@ #!/usr/bin/env bash -# Stop hook: nag about build/render only when, in the current turn: +# Stop hook: nag about build/render when, in the current turn: # - a .sql file was edited inside a dbt project (dbt_project.yml ancestor), or # - a .qmd file was edited (any location — quarto renders standalone files) # AND no build/render command was run this turn. +# +# Reads the per-session state file written by track-tool-use.sh instead of +# parsing the transcript. A nag is delivered as {"decision": "block", +# "reason": ...} — the only output Claude Code feeds back to the model. +# +# Robustness invariants (this is a reminder, not an enforcement gate): +# - Blocks at most ONCE per turn: when stop_hook_active is set (Claude is +# already continuing because of this hook), clear state and allow the +# stop unconditionally. We deliberately do not re-check whether the +# build actually ran — re-checking is how agents get stuck in loops. +# - Every error path fails open (exit 0, no block): missing/malformed +# state file or fields, jq failures, unreadable input. -set -euo pipefail +set -uo pipefail # no -e: error paths must fall through to allow the stop -input=$(cat) -transcript=$(printf '%s' "$input" | jq -r '.transcript_path // empty') +input=$(cat) || exit 0 +session_id=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null) || exit 0 +[ -z "$session_id" ] && exit 0 +case "$session_id" in + *[!A-Za-z0-9_-]*) exit 0 ;; +esac -if [ -z "$transcript" ] || [ ! -f "$transcript" ]; then +state_file="${TMPDIR:-/tmp}/claude-turn-${session_id}.jsonl" + +stop_hook_active=$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null) || stop_hook_active=false +if [ "$stop_hook_active" = "true" ]; then + rm -f "$state_file" exit 0 fi -last_user_line=$(grep -n '"type":"user"' "$transcript" | tail -1 | cut -d: -f1) -last_user_line=${last_user_line:-0} -recent=$(tail -n +"$((last_user_line + 1))" "$transcript") +[ -f "$state_file" ] || exit 0 -edits=$(printf '%s' "$recent" | jq -r ' - select(.type == "assistant") | - .message.content[]? | - select(.type == "tool_use") | - select(.name == "Write" or .name == "Edit") | - .input.file_path // empty -' 2>/dev/null | grep -E '\.(sql|qmd)$' || true) +edits=$(jq -r 'select(.tool == "Write" or .tool == "Edit") | .path // empty' \ + "$state_file" 2>/dev/null | grep -E '\.(sql|qmd)$') || edits="" if [ -z "$edits" ]; then + rm -f "$state_file" exit 0 fi @@ -58,24 +72,23 @@ while IFS= read -r path; do quarto_files="${quarto_files}${path}"$'\n' ;; esac -done <<< "$edits" +done <<<"$edits" if [ -z "$dbt_files" ] && [ -z "$quarto_files" ]; then + rm -f "$state_file" exit 0 fi -bashes=$(printf '%s' "$recent" | jq -r ' - select(.type == "assistant") | - .message.content[]? | - select(.type == "tool_use" and .name == "Bash") | - .input.command // empty -' 2>/dev/null || true) +bashes=$(jq -r 'select(.tool == "Bash") | .cmd // empty' "$state_file" 2>/dev/null) || bashes="" if printf '%s' "$bashes" | grep -qE '(just (build|html|render)|dbt (build|run|test)|quarto render)'; then + rm -f "$state_file" exit 0 fi -{ +# Keep the state file: the post-block stop is allowed via stop_hook_active, +# which also cleans it up. +reason=$( echo "You edited build-relevant files this turn but did not run a build/render command:" if [ -n "$dbt_files" ]; then echo @@ -90,5 +103,9 @@ fi echo " → run \`quarto render \` (or \`just html\`/\`just render\` if the project has it)." fi echo - echo "Verify before stopping." -} + echo "Run the build/render now; if it fails, fix the failures before stopping, and report the result either way." + echo "If a build is genuinely not applicable (e.g. the edit was comment-only or the file was deleted later in the turn), say why in one line instead." +) + +jq -n --arg reason "$reason" '{decision: "block", reason: $reason}' 2>/dev/null || true +exit 0 diff --git a/claude/hooks/track-tool-use.sh b/claude/hooks/track-tool-use.sh new file mode 100755 index 0000000..a1127f5 --- /dev/null +++ b/claude/hooks/track-tool-use.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# PostToolUse(Write|Edit|Bash) hook: append this turn's tool uses to a +# per-session state file so the Stop hook (stop-check.sh) can see which +# files were edited and which commands ran without parsing the transcript. +# Never blocks anything: no output, always exits 0. + +set -uo pipefail + +input=$(cat) || exit 0 +session_id=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null) || exit 0 +[ -z "$session_id" ] && exit 0 + +# Session id becomes part of a filename; accept only safe characters. +case "$session_id" in + *[!A-Za-z0-9_-]*) exit 0 ;; +esac + +printf '%s' "$input" | jq -c '{ + tool: .tool_name, + path: (.tool_input.file_path // null), + cmd: (.tool_input.command // null) +}' >>"${TMPDIR:-/tmp}/claude-turn-${session_id}.jsonl" 2>/dev/null || true + +exit 0 diff --git a/claude/settings.json b/claude/settings.json index 31559b5..c2c202b 100644 --- a/claude/settings.json +++ b/claude/settings.json @@ -18,6 +18,16 @@ "timeout": 5 } ] + }, + { + "matcher": "Write|Edit|Bash", + "hooks": [ + { + "type": "command", + "command": "$HOME/.dotfiles/claude/hooks/track-tool-use.sh", + "timeout": 5 + } + ] } ], "Stop": [ @@ -35,5 +45,6 @@ "effortLevel": "xhigh", "tui": "fullscreen", "theme": "auto", - "skipAutoPermissionPrompt": true + "skipAutoPermissionPrompt": true, + "model": "claude-fable-5[1m]" } diff --git a/cursor/README.md b/cursor/README.md index bcdba82..738e5bc 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -29,11 +29,14 @@ Project-level `.cursor/hooks.json` in a repo can still add project-specific hook | Hook | Event | What it does | |------|-------|--------------| -| Plan quality gate | `PostToolUse` (Write/Edit) | When a plan under `.cursor/plans/` or `.claude/plans/` is edited, audits it against the gates defined in `planning.mdc` (single source of truth). The hook script inlines the rule body and wraps it with audit framing + `PLAN OK` / gap-list output contract. | -| Verify build | `Stop` | Reminds the agent to run build/render if `.sql`, `.yml`, or `.qmd` files were modified | +| Plan quality gate (`plan-review.sh`) | `PostToolUse` (Write/Edit) | When a plan under `.cursor/plans/` or `.claude/plans/` is edited, injects an audit prompt (via `additionalContext`) wrapping the gates defined in `planning.mdc` (single source of truth), with a per-gate PASS/FAIL/N-A verdict contract ending in `PLAN OK` when clean. The full rule is injected on every plan edit — a once-per-session marker would go stale after context compaction. | +| Turn tracker (`track-tool-use.sh`) | `PostToolUse` (Write/Edit/Bash) | Appends edited file paths and Bash commands to a per-session state file in `$TMPDIR` so the Stop hook knows what happened this turn without parsing the transcript. No output. | +| Verify build (`stop-check.sh`) | `Stop` | If `.sql` files inside a dbt project or `.qmd` files were edited this turn and no build/render command ran, blocks the stop once (`decision: block`) with a reminder to run `dbt build` / `quarto render`. Loop-safe by design: allows the stop unconditionally when `stop_hook_active` is set (max one nag per turn) and fails open on any error. | dbt layer boundaries and other SQL conventions are enforced via **rules** (`dbt.mdc`), not hooks. +Hook output follows the **Claude Code** JSON contract (`hookSpecificOutput.additionalContext`, `decision: block`); plain stdout from a hook never reaches the model. Cursor's own hook protocol differs — these scripts are written against Claude Code semantics. + ### Rules (`~/.cursor/rules/`) | Rule | Scope | What it does | From 1e06c3cd74929bb1996c69842647a2ee7a9d10bf Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Fri, 12 Jun 2026 10:40:40 -0700 Subject: [PATCH 10/13] feat(rules): grain-as-spec gates; inject dbt.mdc into Claude Code sessions planning.mdc: plans for new/changed dbt models must declare the grain (operator-reviewed spec, not implementation-discovered) and exit via a passing uniqueness test on it. dbt.mdc: grain test required per model, failing grain tests are findings (never widen keys or dedup to pass), all dedup must be plan-sanctioned, marts need a reconciliation test against an externally controlled number. New dbt-rules.sh hook mirrors Cursor's glob-scoped rule loading for Claude Code: injects dbt.mdc on the first .sql/.yml edit inside a dbt project per session, then a compressed reminder of the load-bearing rules on later edits. Co-Authored-By: Claude Fable 5 --- claude/hooks/dbt-rules.sh | 73 +++++++++++++++++++++++++++++++++++++++ claude/settings.json | 5 +++ cursor/README.md | 1 + cursor/rules/dbt.mdc | 16 +++++++++ cursor/rules/planning.mdc | 5 +++ 5 files changed, 100 insertions(+) create mode 100755 claude/hooks/dbt-rules.sh diff --git a/claude/hooks/dbt-rules.sh b/claude/hooks/dbt-rules.sh new file mode 100755 index 0000000..c65afca --- /dev/null +++ b/claude/hooks/dbt-rules.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# PostToolUse(Write|Edit) hook: inject dbt conventions (cursor/rules/dbt.mdc) +# when a .sql/.yml file inside a dbt project (dbt_project.yml ancestor) is +# edited. Claude Code has no equivalent of Cursor's glob-scoped rule loading, +# so this mirrors it: full rule on the first qualifying edit per session, +# then a one-line reminder of the load-bearing rules on later edits — cheap +# insurance against context compaction without re-paying ~700 tokens per +# edit. + +set -euo pipefail + +input=$(cat) +file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty') + +case "$file_path" in + *.sql | *.yml) ;; + *) exit 0 ;; +esac + +dir=$(dirname "$file_path") +found="" +while [ -n "$dir" ] && [ "$dir" != "/" ]; do + if [ -f "$dir/dbt_project.yml" ]; then + found=1 + break + fi + dir=$(dirname "$dir") +done +[ -n "$found" ] || exit 0 + +emit() { + jq -n --arg ctx "$1" \ + '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}}' +} + +session_id=$(printf '%s' "$input" | jq -r '.session_id // empty') +marker="" +case "$session_id" in + "" | *[!A-Za-z0-9_-]*) ;; + *) marker="${TMPDIR:-/tmp}/claude-dbt-rules-${session_id}" ;; +esac + +if [ -n "$marker" ] && [ -e "$marker" ]; then + emit "Reminder — dbt conventions apply (full rules injected earlier this session): every model has a uniqueness test on its declared grain; a failing grain test is a finding, NEVER fixed by widening the key or deduplicating — stop and report the join that fanned out; all dedup (DISTINCT/QUALIFY/row_number) must be called for in the plan; marts carry a reconciliation test against a number the model does not control." + exit 0 +fi + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +dbt_rule="$script_dir/../../cursor/rules/dbt.mdc" + +if [ ! -f "$dbt_rule" ]; then + echo "dbt-rules hook: missing dbt rule at $dbt_rule" >&2 + exit 1 +fi + +# Strip the YAML frontmatter (between the first two `---` lines). +rule_body=$(awk 'BEGIN{f=0} /^---$/{f++; next} f>=2{print}' "$dbt_rule") + +prompt=$( + cat < +$rule_body + +PROMPT +) + +if [ -n "$marker" ]; then + touch "$marker" +fi +emit "$prompt" diff --git a/claude/settings.json b/claude/settings.json index c2c202b..990deca 100644 --- a/claude/settings.json +++ b/claude/settings.json @@ -16,6 +16,11 @@ "type": "command", "command": "$HOME/.dotfiles/claude/hooks/plan-review.sh", "timeout": 5 + }, + { + "type": "command", + "command": "$HOME/.dotfiles/claude/hooks/dbt-rules.sh", + "timeout": 5 } ] }, diff --git a/cursor/README.md b/cursor/README.md index 738e5bc..061f354 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -30,6 +30,7 @@ Project-level `.cursor/hooks.json` in a repo can still add project-specific hook | Hook | Event | What it does | |------|-------|--------------| | Plan quality gate (`plan-review.sh`) | `PostToolUse` (Write/Edit) | When a plan under `.cursor/plans/` or `.claude/plans/` is edited, injects an audit prompt (via `additionalContext`) wrapping the gates defined in `planning.mdc` (single source of truth), with a per-gate PASS/FAIL/N-A verdict contract ending in `PLAN OK` when clean. The full rule is injected on every plan edit — a once-per-session marker would go stale after context compaction. | +| dbt rules (`dbt-rules.sh`) | `PostToolUse` (Write/Edit) | When a `.sql`/`.yml` file inside a dbt project (`dbt_project.yml` ancestor) is edited, injects `dbt.mdc` (via `additionalContext`) — Claude Code's equivalent of Cursor's glob-scoped rule loading. Full rule on the first qualifying edit per session; later edits get a one-line reminder of the load-bearing rules (grain test, no repair-loop, plan-sanctioned dedup, reconciliation). | | Turn tracker (`track-tool-use.sh`) | `PostToolUse` (Write/Edit/Bash) | Appends edited file paths and Bash commands to a per-session state file in `$TMPDIR` so the Stop hook knows what happened this turn without parsing the transcript. No output. | | Verify build (`stop-check.sh`) | `Stop` | If `.sql` files inside a dbt project or `.qmd` files were edited this turn and no build/render command ran, blocks the stop once (`decision: block`) with a reminder to run `dbt build` / `quarto render`. Loop-safe by design: allows the stop unconditionally when `stop_hook_active` is set (max one nag per turn) and fails open on any error. | diff --git a/cursor/rules/dbt.mdc b/cursor/rules/dbt.mdc index 163888d..8f61257 100644 --- a/cursor/rules/dbt.mdc +++ b/cursor/rules/dbt.mdc @@ -23,12 +23,25 @@ staging -> intermediate -> mart/core (fct/dim) -> mart/ (consumer views) - Each model's SQL docstring starts with `Grain: ...` describing what one row represents. +- The grain comes from the plan (operator-reviewed), not from inspecting + what the SQL happens to produce. Docstring, uniqueness test, and plan + must all state the same grain. - Avoid stating implementation details obvious from the SQL itself. ## Testing - Models should always have data tests: at minimum `not_null` and `unique` on columns where expected. +- Every model has a uniqueness test on its declared grain (single-column + `unique` or `dbt_utils.unique_combination_of_columns`). +- A failing grain/uniqueness test is a finding, not a bug to fix. NEVER + make it pass by widening the key, adding `DISTINCT`/`QUALIFY`, or + deduplicating. Stop, identify which join produced the duplicates, and + report it to the operator. +- Marts should carry at least one reconciliation test tying a row count or + total to a number the model does not control: an upstream entity count + (e.g. rows == `count(distinct sample_id)` in the source) or a known + external figure. - Use `dbt_utils` and `dbt_expectations` for integrity tests. - When an ID column is a hash of dimension columns, add `dbt_expectations.expect_column_distinct_count_to_equal` with `value: 1` @@ -41,6 +54,9 @@ staging -> intermediate -> mart/core (fct/dim) -> mart/ (consumer views) data or reference upstream constants. - `SELECT DISTINCT` as a band-aid for duplicates is a code smell. Fix the grain or join that produced them. +- All deduplication (`DISTINCT`, `QUALIFY`, `row_number() = 1`) must be + called for in the plan. Unplanned dedup is treated as hidden join + fan-out until shown otherwise. - `SELECT *` should be avoided (redundant in DuckDB, hides column changes elsewhere). - Avoid unnecessary `CROSS JOIN`s that produce redundant zero-filled rows. diff --git a/cursor/rules/planning.mdc b/cursor/rules/planning.mdc index 83016e1..d549cff 100644 --- a/cursor/rules/planning.mdc +++ b/cursor/rules/planning.mdc @@ -11,10 +11,15 @@ Every plan MUST include these sections (bullets, reviewable in under 60 seconds) - Build succeeds (e.g. `just build`, `dbt build`) - Rendered output is correct if applicable (e.g. `quarto render`) - For refactors: before/after data snapshots with expected zero-row diffs + - For new/changed dbt models: a uniqueness test on the declared grain passes - **Invariants**: What must not change. Examples: - "row counts in table X unchanged" - "no new test failures" - "downstream consumers produce identical output" + - For every new or changed dbt model: its grain, stated as "one row per …". + The grain is a spec the operator reviews, not something the implementation + discovers — if the built model disagrees with the declared grain, the + declaration wins and the implementation is wrong. - **Failure modes**: What could go wrong. Examples: - "join fan-out from a 1:N relationship" - "grain change in intermediate breaks downstream" From abae25492642654de927468e199dab47c4f55410 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Fri, 12 Jun 2026 14:22:33 -0700 Subject: [PATCH 11/13] Tidy up claude settings --- claude/settings.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/claude/settings.json b/claude/settings.json index 990deca..bf317b4 100644 --- a/claude/settings.json +++ b/claude/settings.json @@ -1,7 +1,7 @@ { "permissions": { "allow": [ - "Bash(uv run *)", + "Bash(uv *)", "Bash(just *)", "Bash(duckdb *)" ], @@ -47,9 +47,7 @@ } ] }, - "effortLevel": "xhigh", "tui": "fullscreen", "theme": "auto", - "skipAutoPermissionPrompt": true, - "model": "claude-fable-5[1m]" + "skipAutoPermissionPrompt": true } From 0bffdda279ee0fa208fe3989c95e01d8746da9b1 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Fri, 12 Jun 2026 14:32:43 -0700 Subject: [PATCH 12/13] Fix markdown formatting --- Makefile | 4 +- cursor/README.md | 41 +++++++++++++-------- dbt/dbt_deep_analysis.md | 77 ++++++++++++++++++++++++--------------- dbt/dbt_quick_analysis.md | 6 +-- llm/PROMPT.md | 11 +++--- nvim/README.md | 4 +- skills/explain.md | 11 ++++-- tmux/cheatsheet.md | 68 +++++++++++++++++----------------- 8 files changed, 126 insertions(+), 96 deletions(-) diff --git a/Makefile b/Makefile index cb6260a..44cf052 100644 --- a/Makefile +++ b/Makefile @@ -5,14 +5,14 @@ apply: fmt: npx --loglevel error --yes prettier --write **/*.yml - uvx mdformat --wrap 80 --number *.md + uvx --with mdformat-gfm --with mdformat-frontmatter mdformat --wrap 80 --number **/*.md uvx ruff format --line-length=100 **/*.py uvx ruff check --fix --line-length=100 **/*.py npx --loglevel error --yes @johnnymorganz/stylua-bin -- **/*.lua fmt_check: npx --loglevel error --yes prettier --check **/*.yml - uvx mdformat --check --wrap 80 --number *.md + uvx --with mdformat-gfm --with mdformat-frontmatter mdformat --check --wrap 80 --number **/*.md uvx ruff format --check --line-length=100 **/*.py uvx ruff check --line-length=100 **/*.py npx --loglevel error --yes @johnnymorganz/stylua-bin --check -- **/*.lua diff --git a/cursor/README.md b/cursor/README.md index 061f354..58891a8 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -1,6 +1,7 @@ # Cursor and Claude agent configuration -User-level **prompt hooks** (via Claude settings) and **Cursor rules** apply across all projects when deployed. +User-level **prompt hooks** (via Claude settings) and **Cursor rules** apply +across all projects when deployed. ## Deployment @@ -19,32 +20,40 @@ Or run Ansible with the `hooks` or `setup` tag: ### Cursor: enable third-party hooks -Hooks live in `~/.claude/settings.json` (Claude Code format). Cursor loads them when **Settings → Features → Third-party skills** is enabled. +Hooks live in `~/.claude/settings.json` (Claude Code format). Cursor loads them +when **Settings → Features → Third-party skills** is enabled. -Project-level `.cursor/hooks.json` in a repo can still add project-specific hooks; global hooks come from `~/.claude/settings.json`. +Project-level `.cursor/hooks.json` in a repo can still add project-specific +hooks; global hooks come from `~/.claude/settings.json`. ## What's included ### Hooks (`~/.claude/settings.json`) -| Hook | Event | What it does | -|------|-------|--------------| -| Plan quality gate (`plan-review.sh`) | `PostToolUse` (Write/Edit) | When a plan under `.cursor/plans/` or `.claude/plans/` is edited, injects an audit prompt (via `additionalContext`) wrapping the gates defined in `planning.mdc` (single source of truth), with a per-gate PASS/FAIL/N-A verdict contract ending in `PLAN OK` when clean. The full rule is injected on every plan edit — a once-per-session marker would go stale after context compaction. | -| dbt rules (`dbt-rules.sh`) | `PostToolUse` (Write/Edit) | When a `.sql`/`.yml` file inside a dbt project (`dbt_project.yml` ancestor) is edited, injects `dbt.mdc` (via `additionalContext`) — Claude Code's equivalent of Cursor's glob-scoped rule loading. Full rule on the first qualifying edit per session; later edits get a one-line reminder of the load-bearing rules (grain test, no repair-loop, plan-sanctioned dedup, reconciliation). | -| Turn tracker (`track-tool-use.sh`) | `PostToolUse` (Write/Edit/Bash) | Appends edited file paths and Bash commands to a per-session state file in `$TMPDIR` so the Stop hook knows what happened this turn without parsing the transcript. No output. | -| Verify build (`stop-check.sh`) | `Stop` | If `.sql` files inside a dbt project or `.qmd` files were edited this turn and no build/render command ran, blocks the stop once (`decision: block`) with a reminder to run `dbt build` / `quarto render`. Loop-safe by design: allows the stop unconditionally when `stop_hook_active` is set (max one nag per turn) and fails open on any error. | +| Hook | Event | What it does | +| ------------------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Plan quality gate (`plan-review.sh`) | `PostToolUse` (Write/Edit) | When a plan under `.cursor/plans/` or `.claude/plans/` is edited, injects an audit prompt (via `additionalContext`) wrapping the gates defined in `planning.mdc` (single source of truth), with a per-gate PASS/FAIL/N-A verdict contract ending in `PLAN OK` when clean. The full rule is injected on every plan edit — a once-per-session marker would go stale after context compaction. | +| dbt rules (`dbt-rules.sh`) | `PostToolUse` (Write/Edit) | When a `.sql`/`.yml` file inside a dbt project (`dbt_project.yml` ancestor) is edited, injects `dbt.mdc` (via `additionalContext`) — Claude Code's equivalent of Cursor's glob-scoped rule loading. Full rule on the first qualifying edit per session; later edits get a one-line reminder of the load-bearing rules (grain test, no repair-loop, plan-sanctioned dedup, reconciliation). | +| Turn tracker (`track-tool-use.sh`) | `PostToolUse` (Write/Edit/Bash) | Appends edited file paths and Bash commands to a per-session state file in `$TMPDIR` so the Stop hook knows what happened this turn without parsing the transcript. No output. | +| Verify build (`stop-check.sh`) | `Stop` | If `.sql` files inside a dbt project or `.qmd` files were edited this turn and no build/render command ran, blocks the stop once (`decision: block`) with a reminder to run `dbt build` / `quarto render`. Loop-safe by design: allows the stop unconditionally when `stop_hook_active` is set (max one nag per turn) and fails open on any error. | -dbt layer boundaries and other SQL conventions are enforced via **rules** (`dbt.mdc`), not hooks. +dbt layer boundaries and other SQL conventions are enforced via **rules** +(`dbt.mdc`), not hooks. -Hook output follows the **Claude Code** JSON contract (`hookSpecificOutput.additionalContext`, `decision: block`); plain stdout from a hook never reaches the model. Cursor's own hook protocol differs — these scripts are written against Claude Code semantics. +Hook output follows the **Claude Code** JSON contract +(`hookSpecificOutput.additionalContext`, `decision: block`); plain stdout from a +hook never reaches the model. Cursor's own hook protocol differs — these scripts +are written against Claude Code semantics. ### Rules (`~/.cursor/rules/`) -| Rule | Scope | What it does | -|------|-------|--------------| -| `planning.mdc` | Always applied | Plan structure (exit criteria, invariants, failure modes + premortem, assumptions & unknowns, outside view), minimal viable change, visualization confirmation | -| `dbt.mdc` | `*.sql`, `*.yml` | Layer boundaries, grain docstrings, testing conventions, anti-patterns | +| Rule | Scope | What it does | +| -------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `planning.mdc` | Always applied | Plan structure (exit criteria, invariants, failure modes + premortem, assumptions & unknowns, outside view), minimal viable change, visualization confirmation | +| `dbt.mdc` | `*.sql`, `*.yml` | Layer boundaries, grain docstrings, testing conventions, anti-patterns | ## Project-specific extensions -Add `.cursor/rules/` or `.cursor/hooks.json` in a project repo for domain-specific policy. Project hooks override user hooks where Cursor merges configs. +Add `.cursor/rules/` or `.cursor/hooks.json` in a project repo for +domain-specific policy. Project hooks override user hooks where Cursor merges +configs. diff --git a/dbt/dbt_deep_analysis.md b/dbt/dbt_deep_analysis.md index fbc10d4..257ab77 100644 --- a/dbt/dbt_deep_analysis.md +++ b/dbt/dbt_deep_analysis.md @@ -1,70 +1,89 @@ -You have access to a duckdb database. You are auditing a dbt model for data quality, correctness, and best practices. Interrogate the database to validate every claim you make — do not speculate without running a query first. +You have access to a duckdb database. You are auditing a dbt model for data +quality, correctness, and best practices. Interrogate the database to validate +every claim you make — do not speculate without running a query first. ## Audit checklist Work through each section. For every finding, run a query to confirm it. ### 1. Schema & types -- Are column types appropriate (e.g. dates stored as DATE not VARCHAR, monetary values as DECIMAL not FLOAT)? -- Are there implicit casts in joins or WHERE clauses that could silently drop rows or change values? + +- Are column types appropriate (e.g. dates stored as DATE not VARCHAR, monetary + values as DECIMAL not FLOAT)? +- Are there implicit casts in joins or WHERE clauses that could silently drop + rows or change values? - Do any columns contain mixed types or unexpected NULLs? ### 2. Join correctness -- Is every join relationship correct (1:1, 1:N, M:N)? Run a query: does the join **fan out** (produce more rows than the driving table)? -- Are there orphaned rows (LEFT JOIN misses)? What fraction of rows have NULL foreign keys after the join? -- Are join keys unique on the side that should be unique? Query `COUNT(*) vs COUNT(DISTINCT key)`. + +- Is every join relationship correct (1:1, 1:N, M:N)? Run a query: does the join + **fan out** (produce more rows than the driving table)? +- Are there orphaned rows (LEFT JOIN misses)? What fraction of rows have NULL + foreign keys after the join? +- Are join keys unique on the side that should be unique? Query + `COUNT(*) vs COUNT(DISTINCT key)`. ### 3. Filters & business logic -- Are there WHERE / HAVING filters that could silently exclude valid records (e.g. filtering on a column that is sometimes NULL)? -- Is there business logic (CASE statements, date arithmetic, aggregations) that could produce wrong results on edge cases? + +- Are there WHERE / HAVING filters that could silently exclude valid records + (e.g. filtering on a column that is sometimes NULL)? +- Is there business logic (CASE statements, date arithmetic, aggregations) that + could produce wrong results on edge cases? - Are date boundaries inclusive/exclusive as intended? ### 4. Grain & uniqueness -- What is the intended grain of this model? Verify with `COUNT(*) vs COUNT(DISTINCT )`. + +- What is the intended grain of this model? Verify with + `COUNT(*) vs COUNT(DISTINCT )`. - Could the model produce duplicate rows under any upstream data condition? ### 5. Data quality -- What percentage of each column is NULL? Flag any column where the NULL rate is suspicious. -- Are there unexpected duplicate values, negative numbers, future dates, or empty strings where there shouldn't be? -- Do value distributions look reasonable (run MIN, MAX, AVG, percentiles for numeric columns)? + +- What percentage of each column is NULL? Flag any column where the NULL rate is + suspicious. +- Are there unexpected duplicate values, negative numbers, future dates, or + empty strings where there shouldn't be? +- Do value distributions look reasonable (run MIN, MAX, AVG, percentiles for + numeric columns)? ### 6. Performance & best practices + - Are there SELECT * or unnecessary columns being carried through? - Could CTEs be simplified or combined? -- Are there window functions that could be replaced with simpler aggregations, or vice versa? -- Is the model incremental where it should be, or full-refresh where incremental would be better? +- Are there window functions that could be replaced with simpler aggregations, + or vice versa? +- Is the model incremental where it should be, or full-refresh where incremental + would be better? ### 7. Test coverage gaps -{{#if existing_tests}} -The following dbt tests are already defined for this model: -{{existing_tests}} + +{{#if existing_tests}} The following dbt tests are already defined for this +model: {{existing_tests}} Identify what is NOT covered by existing tests. Focus recommendations on gaps. -{{/if}} -{{^if existing_tests}} -No dbt tests were found for this model. Recommend the most important tests to add. -{{/if}} +{{/if}} {{^if existing_tests}} No dbt tests were found for this model. Recommend +the most important tests to add. {{/if}} ### 8. Upstream dependency risks -{{#if lineage}} -Model lineage (immediate upstream/downstream): -{{lineage}} -Consider: if an upstream model delivers late, delivers duplicates, or changes its grain, how does this model behave? Are there defensive checks? -{{/if}} +{{#if lineage}} Model lineage (immediate upstream/downstream): {{lineage}} + +Consider: if an upstream model delivers late, delivers duplicates, or changes +its grain, how does this model behave? Are there defensive checks? {{/if}} ## Context ### Compiled SQL + {{compiled_sql}} ### Sample rows + {{sample_rows}} ### Data profile -{{#if data_profile}} -{{data_profile}} -{{/if}} + +{{#if data_profile}} {{data_profile}} {{/if}} ## Output format diff --git a/dbt/dbt_quick_analysis.md b/dbt/dbt_quick_analysis.md index 6eb4511..391998b 100644 --- a/dbt/dbt_quick_analysis.md +++ b/dbt/dbt_quick_analysis.md @@ -1,4 +1,4 @@ Output the complete SQL file with inline comments added as SQL comments (-- ). -Add brief comments suggesting improvements, potential issues, or best-practice violations. -Where appropriate, include a short explanation of why the suggestion matters. -Output ONLY the SQL with comments, no markdown fences, no preamble. +Add brief comments suggesting improvements, potential issues, or best-practice +violations. Where appropriate, include a short explanation of why the suggestion +matters. Output ONLY the SQL with comments, no markdown fences, no preamble. diff --git a/llm/PROMPT.md b/llm/PROMPT.md index 6cbf6c7..617c8ca 100644 --- a/llm/PROMPT.md +++ b/llm/PROMPT.md @@ -5,9 +5,8 @@ sensibilities. We are working on a project I need your assistance with updates or debugging. I will provide background in this message for you and then in the following message provide context of the problem I would like to work on today. For you first message I would like you to just respond with "understood". After -my next message where I outline what I want to work on today I would like you -to ask questions, and in response you will provide a technical spec what we're -planning to work on today. Then only when I write "build" you will implement -the solution. Please remember to add detailed comments and explanations -throughout your generated code. - +my next message where I outline what I want to work on today I would like you to +ask questions, and in response you will provide a technical spec what we're +planning to work on today. Then only when I write "build" you will implement the +solution. Please remember to add detailed comments and explanations throughout +your generated code. diff --git a/nvim/README.md b/nvim/README.md index 185280b..f5c479d 100644 --- a/nvim/README.md +++ b/nvim/README.md @@ -1,4 +1,4 @@ # 💤 LazyVim -A starter template for [LazyVim](https://github.com/LazyVim/LazyVim). -Refer to the [documentation](https://lazyvim.github.io/installation) to get started. +A starter template for [LazyVim](https://github.com/LazyVim/LazyVim). Refer to +the [documentation](https://lazyvim.github.io/installation) to get started. diff --git a/skills/explain.md b/skills/explain.md index 1cefbef..1cf8bcf 100644 --- a/skills/explain.md +++ b/skills/explain.md @@ -1,6 +1,6 @@ --- name: explain -description: > +description: >- Use this skill when the user wants to understand a file — whether they ask to "explain," "walk me through," "what does this do," or simply point at a file and ask about it. Produces a two-part report: an intuitive overview of @@ -26,7 +26,8 @@ Given a file path, read the file and produce a report with exactly two sections: ### Part 1 — Intuitive Explanation -Explain what this file does as if describing it to a colleague who understands software but has no context on this project. Cover: +Explain what this file does as if describing it to a colleague who understands +software but has no context on this project. Cover: - What problem it solves or what role it plays - How it fits into the broader system @@ -41,6 +42,8 @@ Walk through the implementation with precision. Cover: - Structure: key functions, classes, or sections and how they relate - Data flow: what comes in, what goes out, what gets transformed - Dependencies: what it relies on and what relies on it -- Notable decisions: non-obvious implementation choices, trade-offs, or constraints +- Notable decisions: non-obvious implementation choices, trade-offs, or + constraints -Reference specific line numbers and identifiers. Do not restate Part 1 in technical language — add new information. +Reference specific line numbers and identifiers. Do not restate Part 1 in +technical language — add new information. diff --git a/tmux/cheatsheet.md b/tmux/cheatsheet.md index 5d8b30c..21ab00b 100644 --- a/tmux/cheatsheet.md +++ b/tmux/cheatsheet.md @@ -2,56 +2,56 @@ Prefix key: `Ctrl-a` -All window and tab management is handled by tmux. Ghostty's `Cmd-T` and -`Cmd-N` are unbound to avoid conflicts. +All window and tab management is handled by tmux. Ghostty's `Cmd-T` and `Cmd-N` +are unbound to avoid conflicts. ## tmux (prefix = Ctrl-a) ### Sessions -| Keys | Action | -| --- | --- | -| `prefix d` | Detach from session | -| `prefix s` | List / switch sessions | -| `prefix $` | Rename session | -| `tmux new -s name` | New named session | +| Keys | Action | +| ------------------ | ---------------------- | +| `prefix d` | Detach from session | +| `prefix s` | List / switch sessions | +| `prefix $` | Rename session | +| `tmux new -s name` | New named session | ### Windows (tabs inside tmux) -| Keys | Action | -| --- | --- | -| `prefix c` | New window | -| `prefix ,` | Rename window | -| `prefix n` / `prefix p` | Next / previous window | -| `prefix 1-9` | Jump to window by number | -| `prefix Ctrl-a` | Toggle last window | -| `prefix &` | Kill window | +| Keys | Action | +| ----------------------- | ------------------------ | +| `prefix c` | New window | +| `prefix ,` | Rename window | +| `prefix n` / `prefix p` | Next / previous window | +| `prefix 1-9` | Jump to window by number | +| `prefix Ctrl-a` | Toggle last window | +| `prefix &` | Kill window | ### Panes (splits inside a window) -| Keys | Action | -| --- | --- | -| `prefix i` | Split horizontal | -| `prefix u` | Split vertical | +| Keys | Action | +| -------------- | ----------------------------------- | +| `prefix i` | Split horizontal | +| `prefix u` | Split vertical | | `Ctrl-h/j/k/l` | Navigate panes (vim-tmux-navigator) | -| `prefix z` | Zoom / unzoom pane | -| `prefix x` | Kill pane | -| `prefix Space` | Enter copy mode (vi keys) | +| `prefix z` | Zoom / unzoom pane | +| `prefix x` | Kill pane | +| `prefix Space` | Enter copy mode (vi keys) | ### Copy mode (vi) -| Keys | Action | -| --- | --- | -| `/` | Search forward | -| `?` | Search backward | -| `v` | Begin selection | -| `y` | Yank selection | -| `q` | Exit copy mode | +| Keys | Action | +| ---- | --------------- | +| `/` | Search forward | +| `?` | Search backward | +| `v` | Begin selection | +| `y` | Yank selection | +| `q` | Exit copy mode | ## Ghostty -| Keys | Action | -| --- | --- | +| Keys | Action | +| ----------------- | ------------- | | `Cmd-+` / `Cmd--` | Zoom in / out | -| `Cmd-Shift-,` | Open config | -| `Cmd-K` | Clear screen | +| `Cmd-Shift-,` | Open config | +| `Cmd-K` | Clear screen | From 4e36fc571e71d5f076e04617e7c02aca52ee7063 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Fri, 12 Jun 2026 14:34:25 -0700 Subject: [PATCH 13/13] Drop generic prompt --- llm/PROMPT.md | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 llm/PROMPT.md diff --git a/llm/PROMPT.md b/llm/PROMPT.md deleted file mode 100644 index 617c8ca..0000000 --- a/llm/PROMPT.md +++ /dev/null @@ -1,12 +0,0 @@ -# Background - -Hello, for this session you are a world class software engineer, with pragmatic -sensibilities. We are working on a project I need your assistance with updates -or debugging. I will provide background in this message for you and then in the -following message provide context of the problem I would like to work on today. -For you first message I would like you to just respond with "understood". After -my next message where I outline what I want to work on today I would like you to -ask questions, and in response you will provide a technical spec what we're -planning to work on today. Then only when I write "build" you will implement the -solution. Please remember to add detailed comments and explanations throughout -your generated code.