#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:
co-authored by
Claude Opus 4.8
parent
f9174943a2
commit
54825b6a1c
@@ -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).
|
||||
|
||||
@@ -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
@@ -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,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."
|
||||
---
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
---
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user