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:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
407a7f142d
commit
e3e4198038
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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'" },
|
||||
|
||||
Reference in New Issue
Block a user