Graft v3.6: Find All References and the First Bug in 24 Rounds
Graft is a graph-native language for AI agent harness engineering. It compiles .gft source files into execution harnesses — declaring how agents communicate, what context they share, and how token budgets flow through a pipeline. The compiler is built with a multi-agent adversarial debate process where 2-4 AI agents independently analyze and converge on implementations.
v3.6 completes the LSP feature set with find-all-references, and breaks a 24-round streak of zero bugs caught by review.
What's New
Find All References
The last major LSP feature. Right-click any context, node, memory, graph, or produces name and see every reference across your workspace:
context TaskSpec(max_tokens: 1k) { // declaration
description: String
}
node Analyzer(model: sonnet, budget: 5k/2k) {
reads: [TaskSpec] // reference
produces Analysis { issues: List<String> }
}
graph Pipeline(input: TaskSpec, ...) { // reference
Analyzer -> done
}
The implementation reuses collectRenameLocations from the rename feature — same word-boundary regex, same comment/string/import-path filtering. A new isReferable() function gates which symbols get references, and it's broader than isRenameable: it also checks producesNodeMap, so produces names like Analysis above are findable too.
Cross-file references scan all workspace .gft files (not just importers like rename does) with a fast string.includes() pre-filter before running the full regex pipeline.
The Produces Name Gap
This was the most interesting finding from A3-Skeptic's analysis. The rename feature uses isRenameable() to gate which symbols can be renamed — but isRenameable only checks 4 ProgramIndex maps (contextMap, nodeMap, memoryMap, graphMap). It doesn't check producesNodeMap.
If find-all-references had reused isRenameable as the gate (the obvious minimal approach), produces names would silently return zero results. A3 caught this before implementation began, thanks to R-PROC-14 (enforce analysis completion before convergence) — the process improvement we proposed in v3.4's retro and first tested here.
Keyword Unification
A tech debt item from v3.5: GRAFT_KEYWORDS was a hand-maintained Set of 26 strings, separate from the lexer's KEYWORDS Record of 38+ entries. Now derived automatically:
export const GRAFT_KEYWORDS = new Set(
Object.keys(KEYWORDS).filter(k => !TYPE_KEYWORDS.has(k))
);
Type keywords (String, Int, Float, etc.) and the contextual keyword output are excluded — they're valid as identifiers in some positions.
Parse-Based Conflict Detection
Cross-file rename conflict checking previously used a regex (\b(context|node|memory|graph)\s+NewName\b) which could false-positive on declarations inside comments. Now it parses each workspace file and checks ProgramIndex maps directly.
The 24-Round Streak Ends
v3.6-R3's reviewer caught a bug: selectionRange in document symbols started at the keyword position instead of the name position. For context TaskSpec, the selectionRange covered "context " instead of "TaskSpec". The tests passed because they only verified the width, not the absolute position.
This is the first NEEDS_CHANGES verdict since v3.1 — breaking a streak of 24 consecutive rounds across 5 versions with zero bugs caught by review. The fix was one line.
Try It
npm install -g @graft-lang/graft@3.6.0
graft compile pipeline.gft
graft check pipeline.gft
graft run pipeline.gft --input '{"query": "test"}' --dry-run
Stats
| Metric | v3.5 | v3.6 | Delta | |--------|------|------|-------| | Tests | 739 | 790 | +51 | | Ratchet decisions | ~210 | ~220 | +8 | | Agent calls | ~8 | ~11 | +3 | | Rounds | 4 | 4 | 0 | | NEEDS_CHANGES | 0 | 1 | +1 | | Debated rounds | 0 | 1 | +1 | | LSP features | 7 | 8 (+ references) | +1 |
Built with Claude Opus 4.6 via Claude Code. April 2026.