From 54825b6a1ca03f70ff68b8e17b670ffa56978d07 Mon Sep 17 00:00:00 2001 From: safishamsi Date: Sat, 4 Jul 2026 22:43:31 +0100 Subject: [PATCH] fix: windows skill name, opencode plugin separator, doc-corpus report noise (#1635, #1646, #1657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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) --- CHANGELOG.md | 3 ++ graphify/__main__.py | 10 ++++- graphify/report.py | 38 ++++++++++++------- graphify/skill-windows.md | 2 +- tests/test_install.py | 12 ++++++ tests/test_report.py | 30 +++++++++++++++ tests/test_skillgen.py | 7 +++- .../expected/graphify__skill-windows.md | 2 +- tools/skillgen/platforms.toml | 8 +++- 9 files changed, 90 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33fa21a..0006c4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/graphify/__main__.py b/graphify/__main__.py index 59dcd70..e620d97 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -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; } diff --git a/graphify/report.py b/graphify/report.py index 1a1d9a3..5bac06d 100644 --- a/graphify/report.py +++ b/graphify/report.py @@ -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: diff --git a/graphify/skill-windows.md b/graphify/skill-windows.md index 6ab0e02..5a55c57 100644 --- a/graphify/skill-windows.md +++ b/graphify/skill-windows.md @@ -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." --- diff --git a/tests/test_install.py b/tests/test_install.py index fc2c165..fc1a4e9 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -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") diff --git a/tests/test_report.py b/tests/test_report.py index d8c4ad4..00be0f3 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -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 diff --git a/tests/test_skillgen.py b/tests/test_skillgen.py index be404c6..282aedf 100644 --- a/tests/test_skillgen.py +++ b/tests/test_skillgen.py @@ -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 diff --git a/tools/skillgen/expected/graphify__skill-windows.md b/tools/skillgen/expected/graphify__skill-windows.md index 6ab0e02..5a55c57 100644 --- a/tools/skillgen/expected/graphify__skill-windows.md +++ b/tools/skillgen/expected/graphify__skill-windows.md @@ -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." --- diff --git a/tools/skillgen/platforms.toml b/tools/skillgen/platforms.toml index d33a8c8..0856d22 100644 --- a/tools/skillgen/platforms.toml +++ b/tools/skillgen/platforms.toml @@ -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"