fix: windows skill name, opencode plugin separator, doc-corpus report noise (#1635, #1646, #1657)

#1635: the windows skill variant declared `name: graphify-windows`, but
`graphify install --platform windows` writes it to ~/.claude/skills/graphify/
SKILL.md and Claude Code requires the folder name to equal the frontmatter
`name` — the suffix broke discovery. platforms.toml now sets name = "graphify"
(regenerated + re-blessed).

#1646: the OpenCode (and Kilo) plugin prepended its reminder with `&&`, which
Windows PowerShell 5.1 rejects as a statement separator, breaking the first
bash command of every session. Switched to `;` (valid in PowerShell 5.1, Bash,
POSIX).

#1657: the GRAPH_REPORT.md "Import Cycles" section printed "None detected" on
documents-only corpora where imports don't exist — now gated on code nodes /
import edges being present. The other two items in that issue (mojibake in
manifest/report, stdout encoding) are already handled on current v8: both files
are written UTF-8 and main() reconfigures stdout/stderr to UTF-8.

Full suite: 2909 passed, 3 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-07-04 22:43:31 +01:00
co-authored by Claude Opus 4.8
parent f9174943a2
commit 54825b6a1c
9 changed files with 90 additions and 22 deletions
+3
View File
@@ -4,6 +4,9 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## Unreleased
- Fix: the Windows skill variant now declares `name: graphify` instead of `name: graphify-windows` (#1635, thanks @ray8875). `graphify install --platform windows` writes the variant to `~/.claude/skills/graphify/SKILL.md`, but Claude Code requires the skill folder name to equal the frontmatter `name`, so the `-windows` suffix broke discovery/validation. The variant suffix is a packaging detail, not part of the skill's identity.
- Fix: the OpenCode plugin joins its reminder to the user's command with `;` instead of `&&` (#1646, thanks @gonaik). Windows PowerShell 5.1 rejects `&&` as a statement separator (`not a valid statement separator`), so the first bash command of every OpenCode session on Windows failed. `;` works in PowerShell 5.1, Bash, and POSIX shells. (Both the OpenCode and Kilo plugin templates are fixed.)
- Fix: the `GRAPH_REPORT.md` "Import Cycles" section is now emitted only when the graph contains code (#1657, thanks @Ns2384-star). On a documents-only corpus there are no imports, so the section was pure noise ("None detected") on every run; it is now conditioned on code nodes or import edges being present. (The same report also confirms the mojibake and stdout-encoding items in that issue are already addressed on the current branch: manifest.json and `GRAPH_REPORT.md` are written UTF-8, and the CLI reconfigures stdout/stderr to UTF-8 with `errors="replace"`.)
- Fix: a modified `.docx`/`.xlsx` now re-enters `--update` (#1649, thanks @Ns2384-star). `detect_incremental` tracks the converted markdown sidecar, and `convert_office_file` early-returned whenever the sidecar already existed — so an Office source edited after its first conversion never updated its sidecar and was reported "unchanged" forever, freezing the graph on a living docs corpus. The sidecar is now re-converted when the source is newer than it (which bumps the sidecar's mtime/content so the incremental hash check picks it up); an unchanged source still skips the rewrite so it never churns (#1226).
- Fix: files whose absolute path exceeds Windows' 260-char limit are now hashed (#1655, thanks @Ns2384-star). `_md5_file`/`save_manifest`/`count_words` used plain `open()`/`stat()`, which the Windows file APIs reject for long paths unless prefixed with the extended-length marker `\\?\` — so deeply-nested files (accented, deep folders) never hashed, their manifest entry never stabilized, and `detect_incremental` re-flagged them as changed on every run. Change-detection I/O now prefixes long absolute paths on win32 (mirroring the normalization `cache.py` already applied to cache keys). No-op on other platforms.
- Perf: word counts are cached against each file's stat signature (#1656, thanks @Ns2384-star). `detect()` counted words in every PDF/docx/text file to size the corpus, re-opening and re-parsing every binary on each run — minutes on a large docs corpus even when only a few files changed. Counts are now memoized in the existing content-hash stat index (keyed by size + mtime), so an unchanged file is parsed once and read from the index thereafter; incremental detection drops from O(corpus) parsing to O(changed).
+8 -2
View File
@@ -1325,8 +1325,12 @@ export const GraphifyPlugin = async ({ directory }) => {
if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;
if (input.tool === "bash") {
// Separate with ';' not '&&' — Windows PowerShell 5.1 rejects '&&' as a
// statement separator ("not a valid statement separator"), which broke
// the first bash command in every OpenCode session on Windows (#1646).
// ';' works in PowerShell 5.1, Bash, and POSIX shells alike.
output.args.command =
'echo "[graphify] Knowledge graph available. Read graphify-out/GRAPH_REPORT.md for god nodes and architecture context before searching files." && ' +
'echo "[graphify] Knowledge graph available. Read graphify-out/GRAPH_REPORT.md for god nodes and architecture context before searching files." ; ' +
output.args.command;
reminded = true;
}
@@ -1502,8 +1506,10 @@ export const GraphifyPlugin = async ({ directory }) => {
if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;
if (input.tool === "bash") {
// ';' not '&&' — Windows PowerShell 5.1 rejects '&&' as a statement
// separator, breaking the first bash command of the session (#1646).
output.args.command =
'echo "[graphify] knowledge graph at graphify-out/. For focused questions, run graphify query with your question (scoped subgraph, usually much smaller than GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only for broad architecture context." && ' +
'echo "[graphify] knowledge graph at graphify-out/. For focused questions, run graphify query with your question (scoped subgraph, usually much smaller than GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only for broad architecture context." ; ' +
output.args.command;
reminded = true;
}
+24 -14
View File
@@ -176,20 +176,30 @@ def generate(
else:
lines.append("- None detected - all connections are within the same source files.")
# Circular imports surfaced from file-level dependency graph.
from .analyze import find_import_cycles
cycles = find_import_cycles(G)
lines += ["", "## Import Cycles"]
if cycles:
for c in cycles:
cycle = c.get("cycle", [])
length = c.get("length", len(cycle))
if not cycle:
continue
cycle_path = " -> ".join(cycle + [cycle[0]])
lines.append(f"- {length}-file cycle: `{cycle_path}`")
else:
lines.append("- None detected.")
# Circular imports surfaced from file-level dependency graph. Only meaningful
# for code — a documents-only corpus has no imports, so the section is pure
# noise there ("None detected" on every run). Emit it only when the graph
# actually contains code (#1657).
_has_code = any(
d.get("file_type") == "code" for _, d in G.nodes(data=True)
) or any(
d.get("relation") in ("imports", "imports_from")
for *_e, d in G.edges(data=True)
)
if _has_code:
from .analyze import find_import_cycles
cycles = find_import_cycles(G)
lines += ["", "## Import Cycles"]
if cycles:
for c in cycles:
cycle = c.get("cycle", [])
length = c.get("length", len(cycle))
if not cycle:
continue
cycle_path = " -> ".join(cycle + [cycle[0]])
lines.append(f"- {length}-file cycle: `{cycle_path}`")
else:
lines.append("- None detected.")
hyperedges = G.graph.get("hyperedges", [])
if hyperedges:
+1 -1
View File
@@ -1,5 +1,5 @@
---
name: graphify-windows
name: graphify
description: "Use for any question about a codebase, its architecture, file relationships, or project content — especially when graphify-out/ exists, where the question should be treated as a graphify query first. Turns any input (code, docs, papers, images, videos) into a persistent knowledge graph with god nodes, community detection, and query/path/explain tools."
---
+12
View File
@@ -621,6 +621,18 @@ def test_opencode_plugin_reminder_has_no_backticks(tmp_path):
assert "$(" not in reminder, f"$() in reminder would trigger command substitution: {reminder!r}"
def test_opencode_plugin_uses_semicolon_not_ampersand(tmp_path):
"""The reminder must be joined to the user's command with ';', not '&&'
(#1646). Windows PowerShell 5.1 rejects '&&' as a statement separator, which
broke the first bash command of every OpenCode session on Windows. ';' works
in PowerShell 5.1, Bash, and POSIX shells."""
_agents_install(tmp_path, "opencode")
body = (tmp_path / ".opencode" / "plugins" / "graphify.js").read_text()
# The prepend line ends with the separator before `' +`.
assert '" ; \' +' in body or '." ; \' +' in body, "reminder should join with ';'"
assert '" && \' +' not in body, "'&&' breaks PowerShell 5.1 (#1646)"
def test_opencode_agents_install_registers_plugin_in_config(tmp_path):
"""opencode install registers the plugin in .opencode/opencode.json."""
_agents_install(tmp_path, "opencode")
+30
View File
@@ -105,3 +105,33 @@ def test_report_work_memory_section_absent_without_overlay():
tokens, "./project", learning={"overlay": {}, "dead_ends": []})
assert "## Work-memory lessons" not in empty
assert before == empty
def test_import_cycles_section_present_for_code_corpus():
# #1657: the fixture is a code corpus, so the Import Cycles section shows.
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "## Import Cycles" in report
def test_import_cycles_section_absent_for_documents_only_corpus():
# #1657: a documents-only corpus has no imports; the section is pure noise
# ("None detected") and must be suppressed.
extraction = {
"nodes": [
{"id": "d1", "label": "intro.md", "file_type": "document"},
{"id": "d2", "label": "guide.md", "file_type": "document"},
],
"edges": [{"source": "d1", "target": "d2", "relation": "references"}],
"input_tokens": 0, "output_tokens": 0,
}
G = build_from_json(extraction)
communities = cluster(G)
cohesion = score_all(G, communities)
labels = {cid: f"Community {cid}" for cid in communities}
gods = god_nodes(G)
surprises = surprising_connections(G)
detection = {"total_files": 2, "total_words": 100, "needs_graph": True, "warning": None}
tokens = {"input": 0, "output": 0}
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "## Import Cycles" not in report
+5 -2
View File
@@ -285,9 +285,12 @@ def test_descriptions_are_unified():
def test_windows_frontmatter_name_and_shell_and_extra():
"""windows: graphify-windows name, powershell install, troubleshooting tail."""
"""windows: name must be `graphify` (folder-name rule, #1635), powershell
install, troubleshooting tail."""
core, _ = _platform_artifacts("windows")
assert core.startswith("---\nname: graphify-windows\n")
# Claude Code requires the frontmatter name to equal the install folder
# (graphify); a `graphify-windows` name broke skill discovery (#1635).
assert core.startswith("---\nname: graphify\n")
assert "```powershell" in core
assert "function Find-GraphifyPython" in core
assert "## Troubleshooting" in core
@@ -1,5 +1,5 @@
---
name: graphify-windows
name: graphify
description: "Use for any question about a codebase, its architecture, file relationships, or project content — especially when graphify-out/ exists, where the question should be treated as a graphify query first. Turns any input (code, docs, papers, images, videos) into a persistent knowledge graph with god nodes, community detection, and query/path/explain tools."
---
+6 -2
View File
@@ -16,7 +16,7 @@
# core core template basename under fragments/core/ (split only).
# skill_dst rendered SKILL.md path, relative to the repo root.
# refs_dst rendered references/ dir, relative to the repo root (split only).
# name frontmatter name (default "graphify"; graphify-windows for windows).
# name frontmatter name (default "graphify"; must equal the install folder name).
# description frontmatter description, PRESERVED VERBATIM per platform. Required.
# dispatch Part-B dispatch fragment basename under fragments/dispatch/.
# extraction verbose | compact. Selects the extraction-spec reference body.
@@ -58,7 +58,11 @@ bucket = "split"
core = "core"
skill_dst = "graphify/skill-windows.md"
refs_dst = "graphify/skills/windows/references"
name = "graphify-windows"
# Must equal the install folder name (graphify). Claude Code requires the skill
# folder name to match the frontmatter `name`, and `graphify install` writes the
# windows variant to ~/.claude/skills/graphify/SKILL.md — a `graphify-windows`
# name broke discovery/validation (#1635). The variant is a packaging detail.
name = "graphify"
description = "Use for any question about a codebase, its architecture, file relationships, or project content — especially when graphify-out/ exists, where the question should be treated as a graphify query first. Turns any input (code, docs, papers, images, videos) into a persistent knowledge graph with god nodes, community detection, and query/path/explain tools."
dispatch = "agent-tool-disk-powershell"
extraction = "verbose"