Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0a9bf22
Wire the no cluster and force flags into the build skill
ayushcodes10 Sep 16, 2026
c8c2fc3
Regenerate skill artifacts for the no cluster and force fix
ayushcodes10 Sep 16, 2026
ab14af6
Add regression tests for the no cluster and force wiring
ayushcodes10 Sep 16, 2026
f23b059
Add changelog entry for issue 1619
ayushcodes10 Sep 16, 2026
2f7f4dd
Substitute INPUT_PATH with forward slashes on Windows
ayushcodes10 Sep 16, 2026
9fab00f
Regenerate skill artifacts for the INPUT_PATH slash fix
ayushcodes10 Sep 16, 2026
0b561ab
Add a regression test for the INPUT_PATH slash guidance
ayushcodes10 Sep 16, 2026
a8b2f5d
Add changelog entry for the INPUT_PATH slash fix
ayushcodes10 Sep 16, 2026
58038d8
Add a failure gate to Step 1's interpreter install
ayushcodes10 Sep 16, 2026
3497d9b
Regenerate skill artifacts for the Step 1 failure gate
ayushcodes10 Sep 16, 2026
aac205b
Add a regression test for the Step 1 failure gate
ayushcodes10 Sep 16, 2026
b9229ac
Add changelog entry for the Step 1 failure gate
ayushcodes10 Sep 16, 2026
fa1eae4
Move the update backup instruction before the merge it backs up
ayushcodes10 Sep 16, 2026
e977600
Regenerate skill artifacts for the update backup reorder fix
ayushcodes10 Sep 16, 2026
58f1031
Add a regression test for the update backup ordering
ayushcodes10 Sep 16, 2026
27a4294
Add changelog entry for the update backup reorder fix
ayushcodes10 Sep 16, 2026
f154981
Quote the interpreter substitution everywhere it is used
ayushcodes10 Sep 17, 2026
58a3eab
Regenerate skill artifacts for the interpreter quoting fix
ayushcodes10 Sep 17, 2026
453c4d3
Add a regression test for the interpreter quoting fix
ayushcodes10 Sep 17, 2026
df17bb9
Add changelog entry for the interpreter quoting fix
ayushcodes10 Sep 17, 2026
9569a9c
Update the devin skill quoting assertion for the new form
ayushcodes10 Sep 17, 2026
e71b792
Pass community_labels through the no cluster Step 4 write
ayushcodes10 Sep 17, 2026
769b2d6
Sanction the no cluster label fix comment for the monolith guard
ayushcodes10 Sep 17, 2026
9a6187c
Regenerate skill artifacts for the no cluster label fix
ayushcodes10 Sep 17, 2026
4f15262
Add a regression test for the no cluster label fix
ayushcodes10 Sep 17, 2026
96f2d16
Update changelog entry for issue 1619 review finding
ayushcodes10 Sep 17, 2026
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Full release notes with details on each version: [GitHub Releases](https://githu

## 0.9.63 (2026-09-16)

- Fix: `$(cat graphify-out/.graphify_python)`, the `/graphify` skill's interpreter substitution used at 100+ call sites, was spliced in unquoted everywhere, unlike the already-quoted `"$PYTHON"` form Step 1 itself uses — an interpreter path containing a space (a venv under a user directory with a space in its name) word-split into multiple arguments and failed to exec. Every occurrence is now quoted (#1619, thanks @edtrackai).
- Fix: the `/graphify` skill's `--update` runbook told the agent to back up the old graph for the post-update diff AFTER the merge step and the diff block that reads the backup, so an agent following the runbook top to bottom never created it before the merge ran and the diff silently no-opped on every single update. The instruction now appears before the merge block it belongs with (#1619, thanks @edtrackai).
- Fix: `/graphify` skill Step 1 now stops with an actionable error when the interpreter install still fails after the retry, instead of silently writing a broken interpreter path that every later step then failed against with a cryptic error far removed from the real cause (#1619, thanks @edtrackai).
- Fix: a Windows path substituted into the `/graphify` skill's `INPUT_PATH` placeholder with backslashes corrupted the Python string literal it lands in (a lone `\t` becomes a tab, `\U` raises a `SyntaxError`). Every host now tells the agent to substitute forward slashes instead, and the PowerShell `Resolve-Path` call that saves the scan root is quoted so a path containing a space survives too (#1619, thanks @edtrackai).
- Fix: the `/graphify` skill's `--no-cluster` and `--force` flags are now actually implemented instead of being referenced but never wired through. Step 2 suggested `--no-cluster` for a flat corpus and the shrink-guard error message suggested `--force`, but Step 4 called `cluster()` unconditionally and `to_json()` never received `force=`, so neither flag could do anything. Step 4 now builds a single "Full Corpus" community instead of clustering when `--no-cluster` is given, both `to_json()` calls honor `force=`, and Step 5's labeling is skipped when there is nothing to label. Step 4's write also now passes through the placeholder "Full Corpus" community label it already computes, since it is the only write for that path and every node was silently ending up with no `community_name` at all without it (#1619, thanks @edtrackai).
- Feature: Elixir `alias`/`import`/`require`/`use` targets now resolve onto the module's `defmodule` node across files, so the internal module dependency graph is no longer dropped as dangling. Only top-level modules are indexed (a nested `defmodule`, labeled with its bare inner name, cannot capture an unrelated `use <Name>` from another file), and a same-file reference is left unresolved so it cannot clobber the structural `contains` edge (#3603, thanks @ayushcodes10).
- Feature: a Rust `self.method()` call now resolves to a method defined on the same type in another file (the common split-`impl`-block layout), pooling methods across every `impl` of one type and refusing to link when two unrelated types share a bare name (#3602, thanks @ayushcodes10).
- Feature: a Ruby member call `obj.foo` on a known-type receiver now resolves to a method `foo` inherited from a superclass, including across files, using the same conservative promotion as the implicit-self resolver — a single owning class, matching method kind, and one unambiguous ancestry chain, or it stays dangling (#3585, thanks @oleksii-tumanov).
Expand Down
66 changes: 47 additions & 19 deletions graphify/skill-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Turn any folder of files into a navigable knowledge graph with community detecti
/graphify <path> --directed # build directed graph (preserves edge direction: source→target)
/graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy
/graphify <path> --cluster-only # rerun clustering on existing graph
/graphify <path> --no-cluster # skip clustering, one "Full Corpus" community (flat/small corpora)
/graphify <path> --force # allow the rebuild to shrink graph.json (e.g. after deleting files)
/graphify <path> --no-viz # skip visualization, just report + JSON
/graphify <path> --html # (HTML is generated by default - this flag is a no-op)
/graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub)
Expand Down Expand Up @@ -54,6 +56,8 @@ If the user invoked `/graphify --help` or `/graphify -h` (with no other argument

If no path was given, use `.` (current directory). Do not ask the user for a path.

Every occurrence of `INPUT_PATH` below is a placeholder substituted with this resolved path, inside a Python string literal. On Windows, substitute it with forward slashes (`C:/Users/me/project`, not `C:\Users\me\project`) — a literal backslash in a Windows path splices a stray escape into the Python source (`\t` becomes a tab, `\U` raises a `SyntaxError`), silently or loudly corrupting every block that uses it.

If the path argument starts with `https://github.com/` or `http://github.com/`, treat it as a GitHub URL - run Step 0 before anything else, then continue with the resolved local path.

Follow these steps in order. Do not skip steps.
Expand Down Expand Up @@ -92,6 +96,17 @@ if ! "$PYTHON" -c "import graphify" 2>/dev/null; then
"$PYTHON" -m pip install graphifyy -q 2>/dev/null \
|| "$PYTHON" -m pip install graphifyy -q --break-system-packages 2>&1 | tail -3
fi
# #1619 B4: without this gate, a failed install left PYTHON pointing at an
# interpreter that still cannot import graphify. The step fell through
# silently, writing that interpreter's path anyway, and every later step
# then failed with a cryptic "-c: command not found" far from the real
# cause instead of a clear error here.
if ! "$PYTHON" -c "import graphify" 2>/dev/null; then
echo "ERROR: could not install or locate a Python interpreter with graphify. Try one of:" >&2
echo " uv tool install graphifyy" >&2
echo " python3 -m pip install graphifyy" >&2
exit 1
fi
fi
# Write interpreter path for all subsequent steps (persists across invocations)
mkdir -p graphify-out
Expand All @@ -100,14 +115,14 @@ mkdir -p graphify-out
echo "$(cd INPUT_PATH && pwd)" > graphify-out/.graphify_root
```

If the import succeeds, print nothing and move straight to Step 2.
If the import succeeds, print nothing and move straight to Step 2. If it prints the ERROR above, stop and tell the user what happened - do not proceed to Step 2.

**In every subsequent bash block, replace `python3` with `$(cat graphify-out/.graphify_python)` to use the correct interpreter.**
**In every subsequent bash block, replace `python3` with `"$(cat graphify-out/.graphify_python)"` to use the correct interpreter.**

### Step 2 - Detect files

```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import json
from graphify.detect import detect
from pathlib import Path
Expand Down Expand Up @@ -172,7 +187,7 @@ Note: Parallelizing AST + semantic saves 5-15s on large corpora. AST is determin
For any code files detected, run AST extraction in parallel with Part B subagents:

```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import sys, json
from graphify.extract import collect_files, extract
from pathlib import Path
Expand All @@ -198,7 +213,7 @@ else:
**Fast path:** If detection found zero docs, papers, and images (code-only corpus), skip Part B entirely and go straight to Part C. AST handles code - there is nothing for semantic subagents to do. **First write an empty semantic file** so Part C's merge has its input (it reads `.graphify_semantic.json` unconditionally; without this a code-only run hits `FileNotFoundError`):

```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import json
from pathlib import Path
Path('graphify-out/.graphify_semantic.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
Expand All @@ -220,7 +235,7 @@ Before dispatching any subagents, check which files already have cached extracti
SPEC_PATH below is the **absolute** path of the `references/extraction-spec.md` that ships beside this SKILL.md — the same file Step B2 loads and hands to every subagent. It is the extraction prompt, so cache entries are attributed to it: when a graphify upgrade changes the prompt, entries produced by the old one are re-extracted instead of replayed, and unchanged prompts keep their entries (#1939). Substitute the real path in both Step B0 and Step B3 — pass the same one to each, and do not drop the argument.

```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import json
from graphify.cache import check_semantic_cache
from pathlib import Path
Expand Down Expand Up @@ -285,7 +300,7 @@ If more than half the chunks failed or are missing, stop and tell the user to re

Merge all chunk files into `.graphify_semantic_new.json`. **After each Agent call completes, read the real token counts from the Agent tool result's `usage` field and write them back into the chunk JSON before merging** — the chunk JSON itself always has placeholder zeros. Then run:
```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import json, glob
from pathlib import Path

Expand All @@ -309,7 +324,7 @@ print(f'Merged {len(chunks)} chunks: {total_in:,} in / {total_out:,} out tokens'

Save new results to cache. Pass the same SPEC_PATH as Step B0 — it stamps each entry with the prompt that produced it, and a write under a different prompt than the read lands where the next run won't look (#1939):
```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import json
from graphify.cache import save_semantic_cache
from pathlib import Path
Expand All @@ -323,7 +338,7 @@ print(f'Cached {saved} files')

Merge cached + new results into `graphify-out/.graphify_semantic.json`:
```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import json
from pathlib import Path

Expand Down Expand Up @@ -356,7 +371,7 @@ Clean up temp files: `rm -f graphify-out/.graphify_cached.json graphify-out/.gra
#### Part C - Merge AST + semantic into final extraction

```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import sys, json
from pathlib import Path

Expand Down Expand Up @@ -391,9 +406,11 @@ print(f'Merged: {total} nodes, {edges} edges ({len(ast[\"nodes\"])} AST + {len(s

**Before starting:** the code blocks below pass `directed=IS_DIRECTED` to `build_from_json()`. Replace `IS_DIRECTED` with `True` if `--directed` was given (builds a `DiGraph` preserving edge direction source→target), otherwise `False` (the default undirected `Graph`). Substitute it the same way you substitute `INPUT_PATH` — do not leave the literal `IS_DIRECTED` in the code.

Two more substitutions, in this step's block and Step 5's: replace `IS_NO_CLUSTER` with `True` if `--no-cluster` was given (explicitly, or accepted after Step 2 suggested it for a flat corpus), otherwise `False`. Replace `IS_FORCE` with `True` if `--force` was given, otherwise `False`.

```bash
mkdir -p graphify-out
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import cluster, score_all
Expand All @@ -414,20 +431,29 @@ if G.number_of_nodes() == 0:
print('ERROR: Graph is empty - extraction produced no nodes.')
print('Possible causes: all files were skipped, binary-only corpus, or extraction failed.')
raise SystemExit(1)
communities = cluster(G)
if IS_NO_CLUSTER:
# Skip the expensive clustering step entirely - one placeholder community
# covering every node, per Step 2's flat-corpus suggestion.
communities = {0: list(G.nodes())}
else:
communities = cluster(G)
cohesion = score_all(G, communities)
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
gods = god_nodes(G)
surprises = surprising_connections(G, communities)
labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
labels = {0: 'Full Corpus'} if IS_NO_CLUSTER else {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5 (skipped for --no-cluster)
questions = suggest_questions(G, communities, labels)

# Export FIRST and honor the #479 shrink-guard: to_json returns False (writing
# nothing) when the new graph is smaller than the existing graph.json. Only write
# GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so
# they never describe a graph that graph.json doesn't contain (#1392).
wrote = to_json(G, communities, 'graphify-out/graph.json')
# community_labels=labels is passed here too, not just in Step 5's rewrite,
# because --no-cluster skips Step 5 entirely -- this is the only write for
# that path, so the 'Full Corpus' label computed above must reach graph.json
# now or every node silently loses its community_name.
wrote = to_json(G, communities, 'graphify-out/graph.json', community_labels=labels, force=IS_FORCE)
if not wrote:
print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).')
print('If this shrink is intentional (you deleted files), re-run a full build with --force.')
Expand Down Expand Up @@ -455,7 +481,7 @@ Replace INPUT_PATH with the actual path.
A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts.

```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import json
from pathlib import Path
from graphify.diagnostics import diagnose_extraction, format_diagnostic_report
Expand All @@ -478,12 +504,14 @@ Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNI

### Step 5 - Label communities

Skip this step entirely if `--no-cluster` was given in Step 4 (`IS_NO_CLUSTER` was `True`) — there is only one placeholder community ("Full Corpus"), nothing to label.

Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading").

Then regenerate the report and save the labels for the visualizer:

```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import score_all
Expand Down Expand Up @@ -514,7 +542,7 @@ Path('graphify-out/.graphify_labels.json').write_text(json.dumps({str(k): v for
# Re-export so graph.json nodes carry the curated community_name (#2490).
# Same extraction as Step 4, so the #479 shrink-guard passes on node count;
# if it still refuses, surface the guard message - do not force past it.
wrote = to_json(G, communities, 'graphify-out/graph.json', community_labels=labels)
wrote = to_json(G, communities, 'graphify-out/graph.json', community_labels=labels, force=IS_FORCE)
if not wrote:
print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).')
print('If this shrink is intentional (you deleted files), re-run a full build with --force.')
Expand Down Expand Up @@ -554,7 +582,7 @@ These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `
### Step 9 - Save manifest, update cost tracker, clean up, and report

```bash
$(cat graphify-out/.graphify_python) -c "
"$(cat graphify-out/.graphify_python)" -c "
import json
from pathlib import Path
from datetime import datetime, timezone
Expand Down
Loading
Loading