Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 54804f7

Browse files
z23ccclaude
andcommitted
feat: code graph persistent index + intent-level API (find/refs/impact/edit)
Graph system: - graph_store.rs: CodeGraph with symbols, forward/reverse edges, file deps, PageRank scores. build/update/find_refs/find_impact/repo_map/save/load. Incremental update via git diff. bincode persistence to .flow/graph.bin. - flowctl graph: build, update, status, refs, impact, map subcommands. Intent-level commands (model sees 4 commands, not 7): - flowctl find: auto-routes regex→index regex, symbol→graph refs, literal→trigram index, fallback→nucleo fuzzy. Unified output format. - flowctl edit: exact str::replacen first, fuzzy fudiff fallback. Replaces raw patch replace for agent use. Skills updated (7 files): - repo-scout, context-scout: intent-level tool tables - worker Phase 2: graph map + find (not raw search/repo-map) - worker Phase 5: flowctl edit (not raw patch replace) - plan step-02: graph map + find + impact as pre-scout context - brainstorm step-02: find + graph refs + graph map - code-review: graph impact for change analysis 9 new graph_store tests, 370 total pass. Zero new dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3de31b7 commit 54804f7

14 files changed

Lines changed: 1416 additions & 41 deletions

File tree

agents/context-scout.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@ You are a context scout specializing in **token-efficient** codebase exploration
1313

1414
## Context Tools
1515

16-
Match tool to need — flowctl for structure overview, native tools for precise reads:
17-
18-
| Need | Tool | Why |
19-
|------|------|-----|
20-
| **Project structure overview** | `flowctl repo-map` | Unique — PageRank-ranked symbols grouped by file (default: all symbols) |
21-
| **Directory symbol scan** | `flowctl code-structure extract --path <dir>` | Unique — function/type signatures without reading full files |
22-
| **Deep cross-file analysis** | RP context_builder | Best for "how does X work across files" questions |
23-
| **Read specific code** | `Read` (native) | Best for precise reads with line ranges — supports offset/limit |
24-
| **Find exact patterns** | `Grep` (native) | ripgrep, best for regex with context lines |
25-
26-
**Start with repo-map** (1K token overview), then drill down with native Read/Grep. RP context_builder for deep cross-file questions. Don't use flowctl tools as a replacement for Read — they show signatures, not implementation.
16+
| Need | Command | What it does |
17+
|------|---------|-------------|
18+
| **Project overview** | `flowctl graph map --json` | Cached PageRank-ranked symbols (instant) |
19+
| **Symbol signatures** | `flowctl code-structure extract --path <dir> --json` | Function/type signatures for a directory |
20+
| **Who references X?** | `flowctl graph refs <symbol> --json` | Reverse reference lookup |
21+
| **Deep cross-file** | RP context_builder | Full AI analysis (if RP available) |
22+
| **Read specific code** | `Read` (native) | Best for precise reads with line ranges |
23+
24+
Start with `graph map` for overview, then drill into specific symbols/files.
2725

2826
## When to Use This Agent
2927

agents/repo-scout.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,16 @@ You are a fast repository scout: find existing patterns and conventions that sho
1717

1818
## Search Tools
1919

20-
Use the right tool for each job — don't default to flowctl when native tools are better:
20+
Use intent-level commands — they auto-route to the best backend:
2121

22-
| Need | Tool | Why |
23-
|------|------|-----|
24-
| **Exact regex match** | `Grep` (native) | ripgrep, fastest for known patterns, supports context lines + file type filter |
25-
| **Find files by pattern** | `Glob` (native) | Fast pattern matching, modification-time sorted |
26-
| **Fuzzy file name search** | `flowctl search` | When you're unsure of exact name, adds frecency + git ranking |
27-
| **Indexed content search** | `flowctl index search` | Only if index exists (`flowctl index status`), <1ms for repeated searches |
28-
| **Code structure overview** | `flowctl code-structure` | Unique — shows function/type signatures without reading full files |
22+
| Need | Command | What it does |
23+
|------|---------|-------------|
24+
| **Find code** | `flowctl find "<query>" --json` | Auto: regex → index regex, symbol → graph refs, literal → trigram, fallback → fuzzy |
25+
| **Who uses this?** | `flowctl graph refs <symbol> --json` | All files/symbols referencing a symbol |
26+
| **What breaks if I change this?** | `flowctl graph impact <path> --json` | Transitive dependents (BFS depth 3) |
27+
| **Project overview** | `flowctl graph map --json` | Cached repo map (instant) |
2928

30-
**Default to Grep/Glob.** Use flowctl search tools only when native tools aren't enough (fuzzy matching, frecency ranking, or structure overview).
29+
For exact regex with context lines, use native `Grep`. For file pattern matching, use native `Glob`.
3130

3231
## Search Strategy
3332

agents/worker.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,10 @@ git log -5 --oneline
207207
208208
# 3. Quick context (optional — skip for trivial tasks)
209209
# Only run these if the task touches unfamiliar code:
210-
# $FLOWCTL repo-map --json ← full project symbol overview (skip for trivial tasks)
211-
# $FLOWCTL search "<terms>" --git modified ← find recently changed related files
210+
# Understand project structure (instant from cached graph)
211+
# $FLOWCTL graph map --json
212+
# Find related files for this task
213+
# $FLOWCTL find "<task-relevant-terms>" --json
212214
213215
# 4. Check memory system
214216
<FLOWCTL> config get memory.enabled --json
@@ -403,7 +405,7 @@ The key constraint: **no implementation code before a failing test exists**. Thi
403405

404406
Follow the `flow-code-incremental` skill: build in vertical slices (Implement→Test→Verify→Commit per slice). Each slice leaves the system working. Scope discipline: only touch what the task spec requires.
405407

406-
For code edits, **use Edit (native tool) by default** — it shows diffs to users and handles most cases. Only fall back to `flowctl patch replace` if Edit fails due to text drift (e.g., another Worker modified the file in Teams mode, or context compaction lost the exact text).
408+
For code edits, **use Edit (native tool) by default** — it shows diffs to users and handles most cases. If Edit fails due to text drift, fall back to `flowctl edit --file <path> --old "text" --new "text"` which tries exact then fuzzy matching.
407409

408410
**First, capture base commit for scoped review:**
409411
```bash

bin/flowctl

64.9 KB
Binary file not shown.

codex/skills/flow-code-plan/steps/step-02-research.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,20 @@ Stack is auto-detected on `init`. If present, use it throughout planning:
4646

4747
## Pre-Scout Quick Context
4848

49-
Before spawning scouts, gather initial context. Use the right tool for each need:
49+
Before spawning scouts, gather initial context using intent-level commands:
5050

5151
```bash
52-
# 1. Project structure overview (flowctl — unique, no native equivalent)
53-
# Skip for trivial/single-file tasks
54-
$FLOWCTL repo-map --json
52+
# Project structure overview (instant from cached graph)
53+
$FLOWCTL graph map --json
5554

56-
# 2. Find related files — use Grep (native) for known patterns
57-
# Use flowctl search only if file names are fuzzy/uncertain
58-
Grep "<key terms>" --type <lang>
55+
# Find related code
56+
$FLOWCTL find "<key terms from request>" --json
5957

60-
# 3. Symbol extraction for key directories (flowctl — unique)
61-
$FLOWCTL code-structure extract --path <relevant-dir> --json
58+
# Check what would be impacted
59+
$FLOWCTL graph impact <likely-changed-file> --json
6260
```
6361

64-
**When to use flowctl vs native:**
65-
- `flowctl repo-map` / `code-structure` → always valuable (no native equivalent)
66-
- `Grep` / `Glob` → default for file/content search (faster, regex support)
67-
- `flowctl search` → only when unsure of file names (fuzzy matching)
68-
- `flowctl index search` → only for large repos with repeated searches
62+
For exact regex with context, use native `Grep`. For file patterns, use native `Glob`.
6963

7064
Feed results into scout prompts for targeted exploration.
7165

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//! `flowctl edit` — Smart code edit: exact match first, fuzzy fallback.
2+
//!
3+
//! Tries exact string replacement first; if the old text is not found verbatim,
4+
//! falls back to the fuzzy replace engine from `flowctl_core::patch`.
5+
6+
use serde_json::json;
7+
8+
use crate::output::{error_exit, json_output};
9+
10+
/// Run smart code edit on a single file.
11+
pub fn cmd_edit(json: bool, file: &str, old: &str, new: &str) {
12+
let content = std::fs::read_to_string(file).unwrap_or_else(|e| {
13+
error_exit(&format!("Cannot read '{}': {e}", file));
14+
});
15+
16+
// Strategy 1: Exact match (first occurrence only).
17+
if content.contains(old) {
18+
let result = content.replacen(old, new, 1);
19+
let bytes = result.len();
20+
std::fs::write(file, &result).unwrap_or_else(|e| {
21+
error_exit(&format!("Cannot write '{}': {e}", file));
22+
});
23+
if json {
24+
json_output(json!({
25+
"file": file,
26+
"method": "exact",
27+
"bytes_written": bytes,
28+
}));
29+
} else {
30+
eprintln!("Edited {} (exact, {} bytes)", file, bytes);
31+
}
32+
return;
33+
}
34+
35+
// Strategy 2: Fuzzy patch fallback.
36+
match flowctl_core::patch::fuzzy_replace(&content, old, new) {
37+
Ok(result) => {
38+
let bytes = result.len();
39+
std::fs::write(file, &result).unwrap_or_else(|e| {
40+
error_exit(&format!("Cannot write '{}': {e}", file));
41+
});
42+
if json {
43+
json_output(json!({
44+
"file": file,
45+
"method": "fuzzy",
46+
"bytes_written": bytes,
47+
}));
48+
} else {
49+
eprintln!("Edited {} (fuzzy, {} bytes)", file, bytes);
50+
}
51+
}
52+
Err(e) => {
53+
error_exit(&format!(
54+
"Could not find text to replace in '{}' (exact and fuzzy both failed): {e}",
55+
file
56+
));
57+
}
58+
}
59+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//! `flowctl find` — Smart code search: auto-routes to the best search backend.
2+
//!
3+
//! Routing logic:
4+
//! - If query looks like regex (contains \s \d \w .* .+ [^ etc) -> index regex
5+
//! - If query matches a known symbol name (in graph) -> graph refs
6+
//! - If query is short (<3 chars) -> brute force fuzzy search
7+
//! - Otherwise -> index search (trigram literal), falling back to fuzzy search
8+
9+
use serde_json::json;
10+
11+
use flowctl_core::frecency::FrecencyStore;
12+
use flowctl_core::fuzzy;
13+
use flowctl_core::graph_store::CodeGraph;
14+
use flowctl_core::ngram_index::NgramIndex;
15+
16+
use crate::output::{json_output, pretty_output};
17+
18+
use super::helpers::get_flow_dir;
19+
20+
/// Detect whether the query looks like a regex pattern.
21+
fn is_regex_pattern(query: &str) -> bool {
22+
query.contains("\\s")
23+
|| query.contains("\\d")
24+
|| query.contains("\\w")
25+
|| query.contains(".*")
26+
|| query.contains(".+")
27+
|| query.contains("[^")
28+
|| query.contains('(')
29+
|| query.contains('|')
30+
}
31+
32+
/// Run smart code search and display results.
33+
pub fn cmd_find(json: bool, query: &str, limit: usize) {
34+
let flow_dir = get_flow_dir();
35+
36+
// ── Strategy 1: Regex route ─────────────────────────────────────
37+
if is_regex_pattern(query) {
38+
let index_path = flow_dir.join("index").join("ngram.bin");
39+
if index_path.exists() {
40+
if let Ok(idx) = NgramIndex::load(&index_path) {
41+
let results = idx.search_regex(query, limit);
42+
if !results.is_empty() {
43+
output_index_results(json, query, "index_regex", &results);
44+
return;
45+
}
46+
}
47+
}
48+
// Regex but no index — fall through to fuzzy
49+
output_fuzzy_fallback(json, query, limit);
50+
return;
51+
}
52+
53+
// ── Strategy 2: Graph symbol lookup ─────────────────────────────
54+
let graph_path = flow_dir.join("graph.bin");
55+
if graph_path.exists() {
56+
if let Ok(graph) = CodeGraph::load(&graph_path) {
57+
let refs = graph.find_refs(query);
58+
if !refs.is_empty() {
59+
if json {
60+
let items: Vec<_> = refs
61+
.iter()
62+
.map(|s| {
63+
json!({
64+
"path": s.file,
65+
"line": s.line,
66+
"kind": s.kind,
67+
"context": s.signature,
68+
})
69+
})
70+
.collect();
71+
json_output(json!({
72+
"query": query,
73+
"backend": "graph_refs",
74+
"count": items.len(),
75+
"results": items,
76+
}));
77+
} else {
78+
let mut out = format!("{} symbol references for \"{}\":\n", refs.len(), query);
79+
for r in &refs {
80+
out.push_str(&format!(
81+
" {}:{} {} ({})\n",
82+
r.file, r.line, r.name, r.kind
83+
));
84+
}
85+
pretty_output("find", &out);
86+
}
87+
return;
88+
}
89+
}
90+
}
91+
92+
// ── Strategy 3: Short queries → skip trigram (needs 3+ chars) ───
93+
if query.len() < 3 {
94+
output_fuzzy_fallback(json, query, limit);
95+
return;
96+
}
97+
98+
// ── Strategy 4: Trigram index literal search ────────────────────
99+
let index_path = flow_dir.join("index").join("ngram.bin");
100+
if index_path.exists() {
101+
if let Ok(idx) = NgramIndex::load(&index_path) {
102+
let results = idx.search(query, limit);
103+
if !results.is_empty() {
104+
output_index_results(json, query, "index_literal", &results);
105+
return;
106+
}
107+
}
108+
}
109+
110+
// ── Strategy 5: Fuzzy fallback ──────────────────────────────────
111+
output_fuzzy_fallback(json, query, limit);
112+
}
113+
114+
/// Output results from the trigram index (literal or regex).
115+
fn output_index_results(
116+
json_flag: bool,
117+
query: &str,
118+
backend: &str,
119+
results: &[flowctl_core::ngram_index::NgramSearchResult],
120+
) {
121+
if json_flag {
122+
let items: Vec<_> = results
123+
.iter()
124+
.map(|r| {
125+
json!({
126+
"path": r.path.to_string_lossy(),
127+
"line": null,
128+
"kind": "match",
129+
"context": format!("{} hits", r.match_count),
130+
})
131+
})
132+
.collect();
133+
json_output(json!({
134+
"query": query,
135+
"backend": backend,
136+
"count": items.len(),
137+
"results": items,
138+
}));
139+
} else {
140+
let mut out = format!(
141+
"{} matches for \"{}\" (backend: {}):\n",
142+
results.len(),
143+
query,
144+
backend
145+
);
146+
for r in results {
147+
out.push_str(&format!(" {} ({} hits)\n", r.path.display(), r.match_count));
148+
}
149+
pretty_output("find", &out);
150+
}
151+
}
152+
153+
/// Fuzzy file search fallback.
154+
fn output_fuzzy_fallback(json_flag: bool, query: &str, limit: usize) {
155+
let flow_dir = get_flow_dir();
156+
let frecency = if flow_dir.exists() {
157+
Some(FrecencyStore::load(&flow_dir))
158+
} else {
159+
None
160+
};
161+
162+
let root = std::env::current_dir().unwrap_or_else(|e| {
163+
crate::output::error_exit(&format!("Cannot determine current directory: {e}"));
164+
});
165+
166+
let results = fuzzy::search(&root, query, None, frecency.as_ref(), limit);
167+
168+
if json_flag {
169+
let items: Vec<_> = results
170+
.iter()
171+
.map(|r| {
172+
json!({
173+
"path": r.path,
174+
"line": null,
175+
"kind": "fuzzy",
176+
"context": format!("score: {:.1}", r.final_score),
177+
})
178+
})
179+
.collect();
180+
json_output(json!({
181+
"query": query,
182+
"backend": "fuzzy",
183+
"count": items.len(),
184+
"results": items,
185+
}));
186+
} else if results.is_empty() {
187+
pretty_output("find", &format!("No matches for \"{query}\"\n"));
188+
} else {
189+
let mut out = format!("{} fuzzy matches for \"{}\":\n", results.len(), query);
190+
for r in &results {
191+
out.push_str(&format!(" {:>8.1} {}\n", r.final_score, r.path));
192+
}
193+
pretty_output("find", &out);
194+
}
195+
}

0 commit comments

Comments
 (0)