Repository navigation
[Bug]: Skill detect()/detect_incremental() calls ignore the persisted --no-gitignore and --exclude settings (.graphify_build.json) #4240
Description
Pre-flight checks
- I have checked the Troubleshooting section in the README
What happened?
#1886, #2027 and #1971 made --exclude and --no-gitignore persist in graphify-out/.graphify_build.json. graphify update, --watch and the hook read them back (_read_build_excludes / _read_build_gitignore in watch.py).
The skill pipeline does not. skill.md Step 2 calls detect(Path('INPUT_PATH')), and references/update.md calls detect_incremental(Path('INPUT_PATH')). Both use the defaults: gitignore=True and no extra_excludes. I checked this on v8 at 6478eb7.
So when a corpus was built with --no-gitignore, a skill-driven --update:
- Drops every opted-in git-ignored file from the scan.
detect_incrementalputs them inexcluded_files, because they still exist on disk. The runbook prunes onlydeleted_files, so their nodes stay ingraph.json. Butsave_manifest(..., scan_corpus=...)drops their manifest rows. Their nodes then go stale, and the next CLI run re-extracts all of them, which costs LLM tokens for docs. - Re-includes paths excluded with
--exclude. This is--excludepatterns only apply to the initial scan — update/watch/hook rebuilds silently re-include excluded paths #1886 again, on the skill path.
Expected: the skill honors the same persisted options as the CLI.
Our case: an Obsidian knowledge base whose wiki/ folder is deliberately git-ignored (not versioned) but belongs in the graph. The README says --no-gitignore is the way to do this, and it works for the CLI.
Steps to reproduce
mkdir -p /tmp/gfy-repro/notes /tmp/gfy-repro/docs && cd /tmp/gfy-repro && git init -q
printf 'notes/\n' > .gitignore
echo '# A note' > notes/a.md; echo '# B doc' > docs/b.md; echo '# Skip me' > docs/skip.md
python -I - <<'EOF' # the graphifyy 0.9.80 environment
from pathlib import Path
from graphify.watch import _write_build_config, _read_build_gitignore, _read_build_excludes
from graphify.detect import detect, detect_incremental, save_manifest
root, out = Path('.'), Path('graphify-out')
# What `graphify extract . --no-gitignore --exclude docs/skip.md` persists:
_write_build_config(out, excludes=['docs/skip.md'], gitignore=False)
print('persisted:', (out / '.graphify_build.json').read_text())
rel = lambda fs: sorted(str(Path(f).resolve().relative_to(Path.cwd().resolve())) for f in fs)
# CLI update/watch/hook path (watch.py) honors the persisted options:
cli = detect(root, gitignore=_read_build_gitignore(out), extra_excludes=_read_build_excludes(out) or None)
print('CLI-path detect(): ', rel(f for v in cli['files'].values() for f in v))
save_manifest(cli['files'], root=str(root.resolve()))
# Skill path, verbatim from skill.md Step 2 / references/update.md:
sk = detect(Path('.'))
print('skill detect(): ', rel(f for v in sk['files'].values() for f in v))
inc = detect_incremental(Path('.'))
print('skill detect_incremental: new =', rel(f for v in inc['new_files'].values() for f in v),
'| excluded_files =', rel(inc['excluded_files']), '| deleted_files =', inc['deleted_files'])
EOFError output or graph output
persisted: {"excludes": ["docs/skip.md"], "gitignore": false}
CLI-path detect(): ['docs/b.md', 'notes/a.md']
skill detect(): ['docs/b.md', 'docs/skip.md']
skill detect_incremental: new = ['docs/skip.md'] | excluded_files = ['notes/a.md'] | deleted_files = []
The skill path drops notes/a.md (git-ignored, but opted in) and re-includes docs/skip.md (excluded).
Graphify version
0.9.80
Operating System
macOS
Python Version
3.13
Installation Method
uv tool install (recommended)
Additional context
Two possible fixes:
- In
skill.mdStep 2 andreferences/update.md, for every platform copy, read the persisted options and pass them:from graphify.watch import _read_build_gitignore, _read_build_excludes out = Path('graphify-out') detect(Path('INPUT_PATH'), gitignore=_read_build_gitignore(out), extra_excludes=_read_build_excludes(out) or None)
- Make
detect()anddetect_incremental()fall back to the persisted values when the caller passes nothing, for example withgitignore: bool | None = None. That fixes every caller in one place, including third-party scripts that follow the runbook.
Option 2 looks safer, because the skill has many platform copies of these calls.
Activity
github-actions commentedon Oct 8, 2026
Thanks for opening this issue, @soulmachine. A maintainer will take a look soon.
If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.
To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.
fix(skill): preserve persisted scan options across skill runs
test(skill): point persisted-scan-options guard at the pipeline steps (…
test(skill): point persisted-scan-options guard at the pipeline steps (…
fix(skill): carry the v8 fixes into the pipeline steps after rebase (G…
test(skill): point persisted-scan-options guard at the pipeline steps (…
fix(skill): carry the v8 fixes into the pipeline steps after rebase (G…