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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ _exclude:

_tasks:
- "git init"
# Best effort: offline or without uv the project must still be generated.
- "sh -c 'uv lock || echo \"uv lock failed; run it later to enable the code graph\"'"
- "git checkout -b main"

# Questions
Expand Down
3 changes: 3 additions & 0 deletions template/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ data/**
poetry.lock
.isort.cfg

# graphify code graph (rebuilt by git hooks)
graphify-out/

# Claude code artifacts
**/.claude/settings.local.json

Expand Down
11 changes: 10 additions & 1 deletion template/.pre-commit-config.yaml.jinja
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
minimum_pre_commit_version: "3.0"
minimum_pre_commit_version: "3.2.0"
default_stages: [pre-commit]
default_install_hook_types: [pre-commit, post-commit, post-checkout, post-merge, post-rewrite]
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
Expand Down Expand Up @@ -29,6 +31,13 @@ repos:
language: system
pass_filenames: false
{%- endif %}
- id: graphify
name: graphify update
entry: sh scripts/graphify-update.sh
language: system
stages: [post-commit, post-checkout, post-merge, post-rewrite]
always_run: true
pass_filenames: false
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
Expand Down
15 changes: 15 additions & 0 deletions template/AGENTS.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ those blocks once you start modelling your own domain.
- `make lint` runs ruff and the `ty` type checker.
{%- endif %}

## Code Graph

The graph in `graphify-out/` is rebuilt by git hooks after commit, checkout,
merge, and rebase. It is AST only and can lag a moment behind. Each worktree
has its own graph, and it is not committed.

- Query it before broad searches: `graphify query "<question>"`,
`graphify explain "<node>"`, `graphify path "A" "B"`, and
`graphify-out/GRAPH_REPORT.md` for an overview.
- `uv run pre-commit install --allow-missing-config` installs all hook types,
including these. The flag keeps checkouts of old commits without a config
working.
- If the graph is missing, run `sh scripts/graphify-update.sh`. Do not call
`graphify update .` directly: it overwrites the global graphify skill files.

## Documentation and Comment Style

Brevity is not tidiness — it is what gets the text read. A docstring, comment,
Expand Down
5 changes: 4 additions & 1 deletion template/README.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ pip install {{ project_slug | replace('_', '-') }}

```bash
uv sync
uv run pre-commit install
uv run pre-commit install --allow-missing-config
```

The hooks keep a local code graph in `graphify-out/` current after commit,
checkout, merge and rebase. They need a POSIX `sh` (on Windows: Git Bash).

## Running tests

```bash
Expand Down
2 changes: 2 additions & 0 deletions template/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ type = "github"

[dependency-groups]
dev = [
"graphifyy>=0.9.92",
{%- if include_hexagonal %}
"import-linter>=2.0",
{%- endif %}
"pre-commit>=3.2",
"pytest>=8.0",
"ruff>=0.15",
"ty>=0.0.17",
Expand Down
102 changes: 102 additions & 0 deletions template/scripts/graphify-update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/bin/sh
# Rebuild the graphify code graph (graphify-out/) for this worktree.
#
# Called by the pre-commit "graphify" hook after commit, checkout, merge and
# rewrite. Needs a POSIX sh. Always exits 0 so it never breaks a git operation.
#
# Every call marks the worktree "dirty" and starts a detached worker. Only one
# worker per worktree holds the lock (a mkdir lock, portable to macOS); it
# rebuilds until nothing marked the tree dirty meanwhile, so the final graph
# always reflects the latest trigger.
#
# GRAPHIFY_UPDATE_FOREGROUND=1 runs the worker inline (for tests).

# Stops graphify from rewriting the global agent skill files with this
# project's pinned version.
export GRAPHIFY_NO_AUTO_REFRESH=1

# Work from the worktree root, whatever the caller's directory (kept as is
# outside a git repository).
self=$(cd "$(dirname "$0")" && pwd)/$(basename "$0")
root=$(git rev-parse --show-toplevel 2>/dev/null) && cd "$root"

out=graphify-out
lock=$out/.lock
dirty=$out/.dirty
log=$out/update.log
stale_minutes=10
max_log_bytes=100000

say() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$log"; }

trim_log() {
size=$(wc -c <"$log" 2>/dev/null || echo 0)
if [ "${size:-0}" -gt "$max_log_bytes" ]; then
tail -n 200 "$log" >"$log.tmp" 2>/dev/null && mv "$log.tmp" "$log"
fi
}

# Remove the lock only if this process still owns it.
release() {
[ "$(cat "$lock/pid" 2>/dev/null)" = "$$" ] && rm -rf "$lock"
return 0
}

# Take the lock. It is stale if its holder is dead, or if it has no pid file
# and has not been touched for a while. A live holder is never robbed.
acquire() {
mkdir "$lock" 2>/dev/null && { echo $$ >"$lock/pid"; return 0; }
pid=$(cat "$lock/pid" 2>/dev/null)
if { [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; } ||
{ [ -z "$pid" ] &&
[ -n "$(find "$lock" -maxdepth 0 -mmin +"$stale_minutes" 2>/dev/null)" ]; }; then
say "removing stale lock (holder ${pid:-unknown})"
rm -rf "$lock"
mkdir "$lock" 2>/dev/null && { echo $$ >"$lock/pid"; return 0; }
fi
return 1
}

worker() {
while acquire; do
trap release EXIT
while [ -f "$dirty" ]; do
touch "$lock" 2>/dev/null
rm -f "$dirty"
say "update start"
if uv run --frozen graphify update . >>"$log" 2>&1; then
say "update done"
else
say "update failed (exit $?)"
fi
trim_log
done
release
trap - EXIT
# A trigger may have arrived between the last check and the release.
[ -f "$dirty" ] || return 0
done
}

if [ "$1" = "--worker" ]; then
worker
exit 0
fi

command -v uv >/dev/null 2>&1 || exit 0
mkdir -p "$out" 2>/dev/null || exit 0

if [ ! -f uv.lock ]; then
say "no uv.lock in this checkout; skipping (run 'uv lock' to enable the graph)"
exit 0
fi

touch "$dirty"
if [ -n "$GRAPHIFY_UPDATE_FOREGROUND" ]; then
worker
elif command -v setsid >/dev/null 2>&1; then
setsid sh "$self" --worker >/dev/null 2>&1 </dev/null &
else
nohup sh "$self" --worker >/dev/null 2>&1 </dev/null &
fi
exit 0
Loading
Loading