Skip to content

Commit cae76b3

Browse files
feat(install): add writable graphify-extract subagent for vibe
Vibe's built-in explore subagent is read-only (enabled_tools omits write_file), so Task-dispatched semantic extraction inheriting it silently produced zero .graphify_chunk_NN.json files and aborted the pipeline. Register a graphify-extract subagent alongside the skill that enables write_file scoped to **/graphify-out/.graphify_chunk_*.json only (no unbounded write grant), reuses the explore system prompt, and is symmetrically installed/uninstalled by _install_vibe_extract_agent / _uninstall_vibe_extract_agent. Skill fragment updated so Task calls dispatch with subagent_type="graphify-extract" on vibe. uninstall_all sweeps the agent alongside the rest.
1 parent b3c42cc commit cae76b3

7 files changed

Lines changed: 309 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
217217
- Fix: an unresolved local JS/TS import (to a file absent from the scan) now emits a stable, portable `ref` target id instead of leaking a per-checkout absolute-path slug (#2457, thanks @rohit-jsfreaky).
218218
- Fix: `graphify benchmark` no longer crashes on a node whose label is `None` (#2674, thanks @Arthuro0103).
219219
- Feat: `graphify vibe install` adds full-parity Mistral Vibe (mistralai/mistral-vibe) support — skill file with `user-invocable: true` frontmatter (exposes `/graphify` as a native slash command in vibe's autocomplete), `AGENTS.md` always-on section, and two `pre_tool` hooks in `.vibe/hooks.toml` (matchers `grep` and `read_file` — vibe's snake-cased tool names per `vibe/core/tools/base.py:get_name`) that nudge toward `graphify query` for search/read operations. Global scope honors `VIBE_HOME`, hook ownership is `name`-based (never touches user-authored hooks that happen to shell out to graphify), and `hooks.toml` merges preserve hand-authored comments via `tomlkit`. Verified end-to-end against vibe 2.24.0.
220+
- Fix: `graphify vibe install` now also writes a writable `graphify-extract` subagent to `<scope>/agents/graphify-extract.toml` and the skill fragment dispatches Task calls with `subagent_type="graphify-extract"` (#2537 review, thanks @MusicalNinjaDad). Vibe's built-in `explore` subagent (`vibe/core/agents/models.py:EXPLORE`) is read-only — its `enabled_tools = ["grep", "read_file", "skill"]` cannot write chunk JSON — so semantic extraction inheriting it silently produced zero `.graphify_chunk_NN.json` files and the pipeline aborted. The new agent enables `write_file` scoped to `**/graphify-out/.graphify_chunk_*.json` only (no unbounded write grant), reuses the `explore` system prompt, and is registered/unregistered by `_install_vibe_extract_agent` / `_uninstall_vibe_extract_agent` symmetric to the hook flow. `uninstall_all` sweeps it alongside the rest.
220221

221222
## 0.9.40 (2026-08-11)
222223

‎graphify/__main__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@
9191
_install_vibe_hook,
9292
_vibe_hooks_path,
9393
_vibe_hook_entries,
94+
_install_vibe_extract_agent,
95+
_uninstall_vibe_extract_agent,
96+
_vibe_extract_agent_path,
97+
_vibe_agents_dir,
9498
claude_install,
9599
claude_uninstall,
96100
codebuddy_install,

‎graphify/install.py‎

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1815,23 +1815,115 @@ def _uninstall_vibe_agents_md(targets: list[Path]) -> None:
18151815
print(f"AGENTS.md was empty after removal - deleted {target.resolve()}")
18161816

18171817

1818+
_VIBE_EXTRACT_AGENT_NAME = "graphify-extract"
1819+
1820+
# TOML body for the writable graphify-extract subagent. Vibe's AgentRegistry
1821+
# scans `<scope>/agents/*.toml`; the file stem becomes the agent name
1822+
# (`AgentProfile.from_toml` sets `name=path.stem`, see mistralai/mistral-vibe
1823+
# vibe/core/agents/models.py), so this file MUST be named
1824+
# `graphify-extract.toml`. Only the four whitelisted tools are enabled, and
1825+
# `write_file` is scoped to graphify's own chunk output so nothing else in the
1826+
# workspace is writable. The system prompt id `explore` keeps it lean — vibe's
1827+
# read-only subagent uses the same prompt.
1828+
_VIBE_EXTRACT_AGENT_TOML = """\
1829+
# graphify-extract: writable subagent used by the graphify skill.
1830+
# Managed by `graphify vibe install` -- do not hand-edit; changes are overwritten.
1831+
# Named vs. the read-only `explore` builtin because Task-dispatched semantic
1832+
# extraction has to write one chunk JSON per invocation, which `explore`
1833+
# cannot do (its enabled_tools omit write_file).
1834+
display_name = "Graphify Extract"
1835+
description = "Writable subagent used by graphify's /graphify skill for semantic chunk extraction. Reads files, writes one graphify-out/.graphify_chunk_NN.json per Task, then exits."
1836+
safety = "safe"
1837+
agent_type = "subagent"
1838+
system_prompt_id = "explore"
1839+
enabled_tools = ["grep", "read_file", "write_file", "skill"]
1840+
1841+
[tools.write_file]
1842+
permission = "always"
1843+
allowlist = ["**/graphify-out/.graphify_chunk_*.json"]
1844+
1845+
[tools.read_file]
1846+
permission = "always"
1847+
1848+
[tools.grep]
1849+
permission = "always"
1850+
1851+
[tools.skill]
1852+
permission = "always"
1853+
"""
1854+
1855+
1856+
def _vibe_agents_dir(project_dir: Path, *, project: bool) -> Path:
1857+
"""Return the vibe agents dir for the requested scope (mirror hooks.toml layout)."""
1858+
if project:
1859+
return (project_dir or Path(".")) / ".vibe" / "agents"
1860+
return _vibe_home() / "agents"
1861+
1862+
1863+
def _vibe_extract_agent_path(project_dir: Path, *, project: bool) -> Path:
1864+
"""Full path of the graphify-extract.toml agent file for the requested scope."""
1865+
return _vibe_agents_dir(project_dir, project=project) / f"{_VIBE_EXTRACT_AGENT_NAME}.toml"
1866+
1867+
1868+
def _install_vibe_extract_agent(project_dir: Path, *, project: bool) -> None:
1869+
"""Write the writable graphify-extract subagent (idempotent).
1870+
1871+
Without this, the graphify skill dispatches Task subagents that inherit
1872+
vibe's read-only `explore` profile and cannot write chunk JSON files, so
1873+
Part B extraction silently produces zero chunks (see #2537 review).
1874+
"""
1875+
target = _vibe_extract_agent_path(project_dir, project=project)
1876+
target.parent.mkdir(parents=True, exist_ok=True)
1877+
body = _VIBE_EXTRACT_AGENT_TOML
1878+
if target.exists() and target.read_text(encoding="utf-8") == body:
1879+
print(f" {target} -> graphify-extract agent already registered (no change)")
1880+
return
1881+
if target.exists():
1882+
backup = target.with_name(target.name + ".graphify-bak")
1883+
shutil.copy2(target, backup)
1884+
target.write_text(body, encoding="utf-8")
1885+
print(f" {target} -> graphify-extract subagent registered")
1886+
1887+
1888+
def _uninstall_vibe_extract_agent(project_dir: Path, *, project: bool) -> None:
1889+
"""Remove the graphify-extract.toml agent file (leave user-authored agents alone)."""
1890+
target = _vibe_extract_agent_path(project_dir, project=project)
1891+
if not target.exists():
1892+
return
1893+
target.unlink()
1894+
print(f" {target} -> graphify-extract subagent removed")
1895+
# Prune the agents dir if empty and we own it (project scope only — the
1896+
# global VIBE_HOME/agents may hold unrelated user agents).
1897+
if project:
1898+
agents_dir = target.parent
1899+
try:
1900+
if agents_dir.is_dir() and not any(agents_dir.iterdir()):
1901+
agents_dir.rmdir()
1902+
except OSError:
1903+
pass
1904+
1905+
18181906
def _print_vibe_install_summary(strict: bool) -> None:
18191907
"""Post-install summary + optional strict-mode note."""
18201908
print()
18211909
print("Mistral Vibe will now check the knowledge graph before answering")
18221910
print("codebase questions and rebuild it after code changes.")
18231911
print("Use /graphify in vibe to build or update the graph.")
1912+
print("Semantic extraction dispatches to the `graphify-extract` subagent")
1913+
print("(installed alongside the skill; vibe's read-only `explore` cannot")
1914+
print("write chunk files).")
18241915
if strict:
18251916
print("Strict mode: the first raw file read per session is blocked until")
18261917
print("one `graphify query` runs (toggle with GRAPHIFY_HOOK_STRICT=0).")
18271918

18281919

18291920
def _vibe_install(project_dir: Path | None = None, *, project: bool = False, strict: bool = False) -> None:
1830-
"""Full-parity Vibe install: skill + AGENTS.md always-on + hooks.toml."""
1921+
"""Full-parity Vibe install: skill + AGENTS.md always-on + hooks.toml + graphify-extract agent."""
18311922
project_dir = project_dir or Path(".")
18321923
skill_dst = _copy_skill_file("vibe", project=project, project_dir=project_dir)
18331924
_install_vibe_agents_md(_vibe_agents_md_path(project_dir, project=project))
18341925
_install_vibe_hook(project_dir, project=project, strict=strict)
1926+
_install_vibe_extract_agent(project_dir, project=project)
18351927

18361928
if project:
18371929
_print_project_git_add_hint([
@@ -1856,10 +1948,12 @@ def _vibe_uninstall(project_dir: Path | None = None, *, project: bool = False, r
18561948
if project or (explicit_dir and not remove_user_skill):
18571949
_remove_skill_file("vibe", project=True, project_dir=project_dir)
18581950
_uninstall_vibe_hook(project_dir, project=True)
1951+
_uninstall_vibe_extract_agent(project_dir, project=True)
18591952
agents_md_targets.append(_vibe_agents_md_path(project_dir, project=True))
18601953
if remove_user_skill:
18611954
_remove_skill_file("vibe", project=False)
18621955
_uninstall_vibe_hook(project_dir, project=False)
1956+
_uninstall_vibe_extract_agent(project_dir, project=False)
18631957
agents_md_targets.append(_vibe_agents_md_path(project_dir, project=False))
18641958

18651959
_uninstall_vibe_agents_md(agents_md_targets)

‎graphify/skill-vibe.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,12 @@ Load files from `graphify-out/.graphify_uncached.txt`. Split into chunks of 20-2
261261

262262
> Uses the `Task` tool for parallel subagent dispatch.
263263
> Call `Task` once per chunk — ALL in the same response so they run in parallel.
264+
> **On Mistral Vibe, dispatch each `Task` with `subagent_type="graphify-extract"`.** This writable subagent is installed by `graphify vibe install` alongside the skill. Do NOT use vibe's built-in `explore` subagent — it is read-only (`enabled_tools = ["grep", "read_file", "skill"]`) and cannot write the chunk JSON files this pipeline needs. If `graphify-extract` is missing (older install, or you overrode `VIBE_HOME` after install), re-run `graphify vibe install` first.
264265
265266
Pass the extraction prompt as the task description:
266267

267268
```
268-
Task(description="Your task is to perform the following. Follow the instructions below exactly.\n\n<agent-instructions>\n[extraction prompt, with FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE substituted]\n</agent-instructions>\n\nExecute this now. Output ONLY the structured JSON response.")
269+
Task(subagent_type="graphify-extract", description="Your task is to perform the following. Follow the instructions below exactly.\n\n<agent-instructions>\n[extraction prompt, with FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE substituted]\n</agent-instructions>\n\nExecute this now. Output ONLY the structured JSON response.")
269270
```
270271

271272
Each subagent writes its result to its own `graphify-out/.graphify_chunk_NN.json`. Collect results as each `Task` completes and parse each as JSON.
@@ -285,10 +286,10 @@ See `references/extraction-spec.md` for the exact subagent prompt (JSON schema,
285286
Wait for all subagents. For each result:
286287
- Check that `graphify-out/.graphify_chunk_NN.json` exists on disk — this is the success signal
287288
- If the file exists and contains valid JSON with `nodes` and `edges`, include it and save to cache
288-
- If the file is missing, the subagent was likely dispatched as read-only (Explore type) — print a warning: "chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent." Do not silently skip.
289+
- If the file is missing, the subagent was likely dispatched as read-only (vibe's built-in `explore`, or Claude Code's Explore type) — print a warning: "chunk N missing from disk — subagent may have been read-only. On vibe, re-dispatch with `subagent_type=\"graphify-extract\"` (installed by `graphify vibe install`). On Claude Code, re-dispatch with the general-purpose agent." Do not silently skip.
289290
- If a subagent failed or returned invalid JSON, print a warning and skip that chunk - do not abort
290291

291-
If more than half the chunks failed or are missing, stop and tell the user to re-run and ensure `subagent_type="general-purpose"` is used.
292+
If more than half the chunks failed or are missing, stop and tell the user to re-run. On vibe, ensure `subagent_type="graphify-extract"` (installed by `graphify vibe install`) is passed to every `Task` call — the default `explore` subagent is read-only and will silently produce zero chunks. On Claude Code, ensure `subagent_type="general-purpose"` is used.
292293

293294
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:
294295
```bash

0 commit comments

Comments
 (0)