fix(skillgen): host-generic /graphify install guidance (#1530)

Generated install/skill guidance told agents to invoke a literal `skill`
tool with `skill: "graphify"`, which is host-specific and not valid in
every environment. The always-on AGENTS fragment, packaged artifact,
expected snapshot, and _skill_registration() output now use host-generic
wording: "use the installed graphify skill or instructions". Also decodes
skillgen git blob reads as UTF-8 for Windows and replaces stale English
code-block examples in the translated READMEs.

The always-on roundtrip guard deliberately freezes the v8 baseline, so an
intentional wording change would otherwise fail it. Rather than only
patching the pytest mirror (which left the blocking CLI guard
--always-on-roundtrip red, as the original PR did), this adds an explicit,
reviewable ALWAYS_ON_SANCTIONED_EDITS registry: the guard applies the
approved old->new substitution to the baseline before the byte-for-byte
compare, so this exact sentence is allowed while any other drift still
fails. CLI guard and pytest test now agree and CI passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ariadne Mitophane
2026-06-29 10:01:06 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 407a7f142d
commit e3e4198038
11 changed files with 84 additions and 12 deletions
+1 -1
View File
@@ -114,7 +114,7 @@ curl -fsSL https://raw.githubusercontent.com/safishamsi/graphify/v3/graphify/ski
```
- **graphify** (`~/.claude/skills/graphify/SKILL.md`) - any input to knowledge graph. Trigger: `/graphify`
When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` before doing anything else.
When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
```
</details>
+1 -1
View File
@@ -150,7 +150,7 @@ curl -fsSL https://raw.githubusercontent.com/safishamsi/graphify/v3/graphify/ski
```
- **graphify** (`~/.claude/skills/graphify/SKILL.md`) - any input to knowledge graph. Trigger: `/graphify`
When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` before doing anything else.
When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
```
</details>
+1 -1
View File
@@ -110,7 +110,7 @@ curl -fsSL https://raw.githubusercontent.com/safishamsi/graphify/v3/graphify/ski
```
- **graphify** (`~/.claude/skills/graphify/SKILL.md`) - any input to knowledge graph. Trigger: `/graphify`
When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` before doing anything else.
When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
```
</details>
+2 -2
View File
@@ -462,8 +462,8 @@ def _skill_registration(skill_path: str = "~/.claude/skills/graphify/SKILL.md")
"\n# graphify\n"
f"- **graphify** (`{skill_path}`) "
"- any input to knowledge graph. Trigger: `/graphify`\n"
"When the user types `/graphify`, invoke the Skill tool "
"with `skill: \"graphify\"` before doing anything else.\n"
"When the user types `/graphify`, use the installed graphify skill "
"or instructions before doing anything else.\n"
)
+1 -1
View File
@@ -2,7 +2,7 @@
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else.
When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
+14
View File
@@ -14,6 +14,7 @@ import json
from graphify.__main__ import (
_SETTINGS_HOOK,
_READ_SETTINGS_HOOK,
_skill_registration,
_CLAUDE_MD_SECTION,
_AGENTS_MD_SECTION,
_GEMINI_MD_SECTION,
@@ -127,6 +128,19 @@ def test_agents_section_does_not_skip_dirty_graph_output():
assert "not a reason to skip graphify" in _AGENTS_MD_SECTION
def test_agents_section_uses_generic_graphify_instruction():
assert "`skill` tool" not in _AGENTS_MD_SECTION
assert 'skill: "graphify"' not in _AGENTS_MD_SECTION
assert "use the installed graphify skill" in _AGENTS_MD_SECTION
def test_skill_registration_uses_host_generic_instruction():
reg = _skill_registration()
assert 'skill: "graphify"' not in reg
assert "Skill tool" not in reg
assert "use the installed graphify skill or instructions" in reg
def test_how_it_works_clarifies_code_only_semantic_extraction():
from pathlib import Path
doc = (Path(__file__).parent.parent / "docs" / "how-it-works.md").read_text(encoding="utf-8")
+29 -1
View File
@@ -601,8 +601,36 @@ def test_always_on_roundtrip_is_byte_faithful():
graphify.__main__, so the packaged markdown must round-trip exactly or those
contracts silently change.
"""
# The guard passes with zero problems: every always-on block reproduces its
# frozen baseline, with the agents-md block allowed exactly the #1530
# sanctioned substitution recorded in gen.ALWAYS_ON_SANCTIONED_EDITS.
problems = gen.always_on_roundtrip()
assert problems == [], "\n".join(problems)
assert problems == []
rendered_agents = next(
a.content
for a in gen.render_always_on()
if a.path == "graphify/always_on/agents-md.md"
)
old_instruction = (
"When the user types `/graphify`, invoke the `skill` tool with "
'`skill: "graphify"` before doing anything else.'
)
new_instruction = (
"When the user types `/graphify`, use the installed graphify skill or instructions "
"before doing anything else."
)
# The sanctioned-edit registry holds exactly this single old->new substitution.
assert gen.ALWAYS_ON_SANCTIONED_EDITS["_AGENTS_MD_SECTION"] == (
(old_instruction, new_instruction),
)
baseline_agents = gen._always_on_constants(gen.ALWAYS_ON_BASELINE_REF)["_AGENTS_MD_SECTION"]
# The ONLY divergence from the frozen baseline is the sanctioned sentence —
# any other byte drift would have surfaced as a problem above.
assert old_instruction in baseline_agents
assert baseline_agents.replace(old_instruction, new_instruction) == rendered_agents
assert "`skill` tool" not in rendered_agents
assert 'skill: "graphify"' not in rendered_agents
def test_extracted_constants_equal_the_packaged_always_on_files():
@@ -2,7 +2,7 @@
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else.
When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
@@ -2,7 +2,7 @@
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else.
When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
+32 -2
View File
@@ -92,6 +92,27 @@ ALWAYS_ON_BLOCKS = {
"kiro-steering": "_KIRO_STEERING",
}
# Sanctioned divergences from the frozen always-on baseline above. The roundtrip
# guard deliberately does NOT track HEAD, so any *intentional* change to an
# always-on instruction block must be recorded here as an explicit, reviewable
# old -> new substitution keyed by the baseline constant. The guard applies these
# to the baseline before the byte-for-byte comparison; anything not covered here
# still fails the guard, so unrelated drift cannot slip through. Each entry is a
# one-time, audited edit to the otherwise-immutable v8 baseline.
ALWAYS_ON_SANCTIONED_EDITS: dict[str, tuple[tuple[str, str], ...]] = {
# #1530: install guidance must stay host-generic — do not tell agents to
# invoke a literal `skill` tool with `skill: "graphify"`, which is
# host-specific and not valid in every environment.
"_AGENTS_MD_SECTION": (
(
"When the user types `/graphify`, invoke the `skill` tool with "
'`skill: "graphify"` before doing anything else.',
"When the user types `/graphify`, use the installed graphify skill or instructions "
"before doing anything else.",
),
),
}
# The full six-value file_type enum (Decision A). Every rendered platform — split
# or monolith — must carry exactly this enum, byte for byte. schema-singleton
# guards it.
@@ -561,6 +582,7 @@ def _git_show(ref: str) -> str:
cwd=REPO_ROOT,
capture_output=True,
text=True,
encoding="utf-8",
)
if result.returncode != 0:
raise SystemExit(f"error: could not read {ref}: {result.stderr.strip()}")
@@ -949,10 +971,18 @@ def always_on_roundtrip() -> list[str]:
if const_name not in baseline:
problems.append(f"could not find constant {const_name} in {ALWAYS_ON_BASELINE_REF}")
continue
if rendered[path] != baseline[const_name]:
expected = baseline[const_name]
for old, new in ALWAYS_ON_SANCTIONED_EDITS.get(const_name, ()):
if old not in expected:
problems.append(
f"sanctioned edit for {const_name} no longer applies: "
f"old text not found in {ALWAYS_ON_BASELINE_REF}"
)
expected = expected.replace(old, new)
if rendered[path] != expected:
problems.append(
f"always_on/{basename}.md does not reproduce {const_name} byte for byte "
f"(rendered {len(rendered[path])} chars vs baseline {len(baseline[const_name])} chars)"
f"(rendered {len(rendered[path])} chars vs baseline {len(expected)} chars)"
)
return problems
Generated
+1 -1
View File
@@ -1150,7 +1150,7 @@ wheels = [
[[package]]
name = "graphifyy"
version = "0.9.0"
version = "0.9.1"
source = { editable = "." }
dependencies = [
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },