From 0da71de28777f83813fcbfbcfadbc11a54a998c7 Mon Sep 17 00:00:00 2001 From: SunsetDrifter Date: Tue, 21 Jul 2026 23:39:31 +0200 Subject: [PATCH 1/5] proto: skills-dispatch wrappers for homelab workflows Thin .claude/skills//SKILL.md wrappers, one per workflow, each pointing at its workflows/.md; workflows stay the in-bundle, linted source of truth. New skills_dir engine knob (default None) and check_skills, which errors on missing wrappers, orphan wrappers, and wrappers that don't reference their workflow. Homelab only; variant parity tests carry a documented divergence allowlist. --- codebase-large/wikilint/checks.py | 44 +++++++++++++++++++ codebase-large/wikilint/cli.py | 1 + codebase-large/wikilint/settings.py | 12 +++++ codebase/wikilint/checks.py | 44 +++++++++++++++++++ codebase/wikilint/cli.py | 1 + codebase/wikilint/settings.py | 12 +++++ generic/wikilint/checks.py | 44 +++++++++++++++++++ generic/wikilint/cli.py | 1 + generic/wikilint/settings.py | 12 +++++ homelab/.claude/skills/document/SKILL.md | 6 +++ homelab/.claude/skills/incident/SKILL.md | 6 +++ homelab/.claude/skills/lint/SKILL.md | 6 +++ homelab/.claude/skills/maintain/SKILL.md | 6 +++ homelab/.claude/skills/query/SKILL.md | 6 +++ homelab/.claude/skills/reconcile/SKILL.md | 6 +++ homelab/.claude/skills/triage/SKILL.md | 6 +++ homelab/.claude/skills/verify/SKILL.md | 6 +++ homelab/CLAUDE.md | 2 + homelab/lint.py | 4 +- homelab/wikilint/checks.py | 44 +++++++++++++++++++ homelab/wikilint/cli.py | 1 + homelab/wikilint/settings.py | 12 +++++ tests/test_checks.py | 53 +++++++++++++++++++++++ tests/test_variants.py | 13 +++++- 24 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 homelab/.claude/skills/document/SKILL.md create mode 100644 homelab/.claude/skills/incident/SKILL.md create mode 100644 homelab/.claude/skills/lint/SKILL.md create mode 100644 homelab/.claude/skills/maintain/SKILL.md create mode 100644 homelab/.claude/skills/query/SKILL.md create mode 100644 homelab/.claude/skills/reconcile/SKILL.md create mode 100644 homelab/.claude/skills/triage/SKILL.md create mode 100644 homelab/.claude/skills/verify/SKILL.md diff --git a/codebase-large/wikilint/checks.py b/codebase-large/wikilint/checks.py index 0051044..eae3883 100644 --- a/codebase-large/wikilint/checks.py +++ b/codebase-large/wikilint/checks.py @@ -313,6 +313,50 @@ def check_types(pages, report, root): ) +def check_skills(pages, report, root): + """Harness skill wrappers must pair 1:1 with workflows: every + workflows/.md needs //SKILL.md whose body points + back at the workflow file, and no wrapper may point at nothing. Errors, + not warnings: the wrappers are dispatch surface, and silent drift there + means a slash command runs a stale or missing procedure.""" + if CONFIG["skills_dir"] is None: + return + skills_root = root / CONFIG["skills_dir"] + workflows_root = root / "workflows" + wf_stems = ( + {p.stem for p in workflows_root.glob("*.md")} + if workflows_root.is_dir() else set() + ) + skill_names = ( + {d.name for d in skills_root.iterdir() if d.is_dir()} + if skills_root.is_dir() else set() + ) + sdir = CONFIG["skills_dir"] + for stem in sorted(wf_stems - skill_names): + report.error( + "skills", f"workflows/{stem}.md", + f"no skill wrapper; add {sdir}/{stem}/SKILL.md pointing at it", + ) + for name in sorted(skill_names - wf_stems): + report.error( + "skills", f"{sdir}/{name}/SKILL.md", + f"orphan wrapper; workflows/{name}.md does not exist", + ) + for name in sorted(wf_stems & skill_names): + skill_md = skills_root / name / "SKILL.md" + rel = f"{sdir}/{name}/SKILL.md" + if not skill_md.is_file(): + report.error("skills", rel, "missing SKILL.md in wrapper directory") + continue + body = skill_md.read_text(encoding="utf-8", errors="replace") + if f"workflows/{name}.md" not in body: + report.error( + "skills", rel, + f"wrapper does not reference workflows/{name}.md; " + "the body must point at its workflow file", + ) + + def check_contested_age(pages, report): today = date.today() for p in pages: diff --git a/codebase-large/wikilint/cli.py b/codebase-large/wikilint/cli.py index 671bace..48a18bf 100644 --- a/codebase-large/wikilint/cli.py +++ b/codebase-large/wikilint/cli.py @@ -29,6 +29,7 @@ def gather_report(root): checks.check_claude_size(root, report) checks.check_tags(pages, report, root) checks.check_types(pages, report, root) + checks.check_skills(pages, report, root) checks.check_contested_age(pages, report) checks.check_inferred_markers(pages, report) checks.check_inbox(root, report) diff --git a/codebase-large/wikilint/settings.py b/codebase-large/wikilint/settings.py index d177406..0ef8758 100644 --- a/codebase-large/wikilint/settings.py +++ b/codebase-large/wikilint/settings.py @@ -38,6 +38,10 @@ # schema type with a one-line meaning, so the bundle self-describes its # type vocabulary to OKF consumers. Off for non-wiki markdown trees. "types_glossary": True, + # Directory of harness skill wrappers (e.g. ".claude/skills"), each a + # /SKILL.md whose body points at workflows/.md. When set, + # check_skills errors on any drift between the two sets. None disables. + "skills_dir": None, # Append-only operations log checked by check_log; None disables. "log_file": "log.md", # Callables(pages, report, root) run at the end of every check pass. @@ -80,6 +84,14 @@ def _validate(cfg): raise ConfigError("log_file must be None or a relative path string") if not isinstance(cfg["types_glossary"], bool): raise ConfigError("types_glossary must be a bool") + skills_dir = cfg["skills_dir"] + if skills_dir is not None: + if not isinstance(skills_dir, str) or not skills_dir.strip(): + raise ConfigError("skills_dir must be None or a non-empty relative path") + posix = PurePosixPath(skills_dir) + if posix.is_absolute() or ".." in posix.parts: + raise ConfigError( + f"skills_dir must stay within the wiki root: {skills_dir!r}") if cfg["index_body_fn"] is not None and not callable(cfg["index_body_fn"]): raise ConfigError("index_body_fn must be None or callable") for fn in cfg["extra_checks"]: diff --git a/codebase/wikilint/checks.py b/codebase/wikilint/checks.py index 0051044..eae3883 100644 --- a/codebase/wikilint/checks.py +++ b/codebase/wikilint/checks.py @@ -313,6 +313,50 @@ def check_types(pages, report, root): ) +def check_skills(pages, report, root): + """Harness skill wrappers must pair 1:1 with workflows: every + workflows/.md needs //SKILL.md whose body points + back at the workflow file, and no wrapper may point at nothing. Errors, + not warnings: the wrappers are dispatch surface, and silent drift there + means a slash command runs a stale or missing procedure.""" + if CONFIG["skills_dir"] is None: + return + skills_root = root / CONFIG["skills_dir"] + workflows_root = root / "workflows" + wf_stems = ( + {p.stem for p in workflows_root.glob("*.md")} + if workflows_root.is_dir() else set() + ) + skill_names = ( + {d.name for d in skills_root.iterdir() if d.is_dir()} + if skills_root.is_dir() else set() + ) + sdir = CONFIG["skills_dir"] + for stem in sorted(wf_stems - skill_names): + report.error( + "skills", f"workflows/{stem}.md", + f"no skill wrapper; add {sdir}/{stem}/SKILL.md pointing at it", + ) + for name in sorted(skill_names - wf_stems): + report.error( + "skills", f"{sdir}/{name}/SKILL.md", + f"orphan wrapper; workflows/{name}.md does not exist", + ) + for name in sorted(wf_stems & skill_names): + skill_md = skills_root / name / "SKILL.md" + rel = f"{sdir}/{name}/SKILL.md" + if not skill_md.is_file(): + report.error("skills", rel, "missing SKILL.md in wrapper directory") + continue + body = skill_md.read_text(encoding="utf-8", errors="replace") + if f"workflows/{name}.md" not in body: + report.error( + "skills", rel, + f"wrapper does not reference workflows/{name}.md; " + "the body must point at its workflow file", + ) + + def check_contested_age(pages, report): today = date.today() for p in pages: diff --git a/codebase/wikilint/cli.py b/codebase/wikilint/cli.py index 671bace..48a18bf 100644 --- a/codebase/wikilint/cli.py +++ b/codebase/wikilint/cli.py @@ -29,6 +29,7 @@ def gather_report(root): checks.check_claude_size(root, report) checks.check_tags(pages, report, root) checks.check_types(pages, report, root) + checks.check_skills(pages, report, root) checks.check_contested_age(pages, report) checks.check_inferred_markers(pages, report) checks.check_inbox(root, report) diff --git a/codebase/wikilint/settings.py b/codebase/wikilint/settings.py index d177406..0ef8758 100644 --- a/codebase/wikilint/settings.py +++ b/codebase/wikilint/settings.py @@ -38,6 +38,10 @@ # schema type with a one-line meaning, so the bundle self-describes its # type vocabulary to OKF consumers. Off for non-wiki markdown trees. "types_glossary": True, + # Directory of harness skill wrappers (e.g. ".claude/skills"), each a + # /SKILL.md whose body points at workflows/.md. When set, + # check_skills errors on any drift between the two sets. None disables. + "skills_dir": None, # Append-only operations log checked by check_log; None disables. "log_file": "log.md", # Callables(pages, report, root) run at the end of every check pass. @@ -80,6 +84,14 @@ def _validate(cfg): raise ConfigError("log_file must be None or a relative path string") if not isinstance(cfg["types_glossary"], bool): raise ConfigError("types_glossary must be a bool") + skills_dir = cfg["skills_dir"] + if skills_dir is not None: + if not isinstance(skills_dir, str) or not skills_dir.strip(): + raise ConfigError("skills_dir must be None or a non-empty relative path") + posix = PurePosixPath(skills_dir) + if posix.is_absolute() or ".." in posix.parts: + raise ConfigError( + f"skills_dir must stay within the wiki root: {skills_dir!r}") if cfg["index_body_fn"] is not None and not callable(cfg["index_body_fn"]): raise ConfigError("index_body_fn must be None or callable") for fn in cfg["extra_checks"]: diff --git a/generic/wikilint/checks.py b/generic/wikilint/checks.py index 0051044..eae3883 100644 --- a/generic/wikilint/checks.py +++ b/generic/wikilint/checks.py @@ -313,6 +313,50 @@ def check_types(pages, report, root): ) +def check_skills(pages, report, root): + """Harness skill wrappers must pair 1:1 with workflows: every + workflows/.md needs //SKILL.md whose body points + back at the workflow file, and no wrapper may point at nothing. Errors, + not warnings: the wrappers are dispatch surface, and silent drift there + means a slash command runs a stale or missing procedure.""" + if CONFIG["skills_dir"] is None: + return + skills_root = root / CONFIG["skills_dir"] + workflows_root = root / "workflows" + wf_stems = ( + {p.stem for p in workflows_root.glob("*.md")} + if workflows_root.is_dir() else set() + ) + skill_names = ( + {d.name for d in skills_root.iterdir() if d.is_dir()} + if skills_root.is_dir() else set() + ) + sdir = CONFIG["skills_dir"] + for stem in sorted(wf_stems - skill_names): + report.error( + "skills", f"workflows/{stem}.md", + f"no skill wrapper; add {sdir}/{stem}/SKILL.md pointing at it", + ) + for name in sorted(skill_names - wf_stems): + report.error( + "skills", f"{sdir}/{name}/SKILL.md", + f"orphan wrapper; workflows/{name}.md does not exist", + ) + for name in sorted(wf_stems & skill_names): + skill_md = skills_root / name / "SKILL.md" + rel = f"{sdir}/{name}/SKILL.md" + if not skill_md.is_file(): + report.error("skills", rel, "missing SKILL.md in wrapper directory") + continue + body = skill_md.read_text(encoding="utf-8", errors="replace") + if f"workflows/{name}.md" not in body: + report.error( + "skills", rel, + f"wrapper does not reference workflows/{name}.md; " + "the body must point at its workflow file", + ) + + def check_contested_age(pages, report): today = date.today() for p in pages: diff --git a/generic/wikilint/cli.py b/generic/wikilint/cli.py index 671bace..48a18bf 100644 --- a/generic/wikilint/cli.py +++ b/generic/wikilint/cli.py @@ -29,6 +29,7 @@ def gather_report(root): checks.check_claude_size(root, report) checks.check_tags(pages, report, root) checks.check_types(pages, report, root) + checks.check_skills(pages, report, root) checks.check_contested_age(pages, report) checks.check_inferred_markers(pages, report) checks.check_inbox(root, report) diff --git a/generic/wikilint/settings.py b/generic/wikilint/settings.py index d177406..0ef8758 100644 --- a/generic/wikilint/settings.py +++ b/generic/wikilint/settings.py @@ -38,6 +38,10 @@ # schema type with a one-line meaning, so the bundle self-describes its # type vocabulary to OKF consumers. Off for non-wiki markdown trees. "types_glossary": True, + # Directory of harness skill wrappers (e.g. ".claude/skills"), each a + # /SKILL.md whose body points at workflows/.md. When set, + # check_skills errors on any drift between the two sets. None disables. + "skills_dir": None, # Append-only operations log checked by check_log; None disables. "log_file": "log.md", # Callables(pages, report, root) run at the end of every check pass. @@ -80,6 +84,14 @@ def _validate(cfg): raise ConfigError("log_file must be None or a relative path string") if not isinstance(cfg["types_glossary"], bool): raise ConfigError("types_glossary must be a bool") + skills_dir = cfg["skills_dir"] + if skills_dir is not None: + if not isinstance(skills_dir, str) or not skills_dir.strip(): + raise ConfigError("skills_dir must be None or a non-empty relative path") + posix = PurePosixPath(skills_dir) + if posix.is_absolute() or ".." in posix.parts: + raise ConfigError( + f"skills_dir must stay within the wiki root: {skills_dir!r}") if cfg["index_body_fn"] is not None and not callable(cfg["index_body_fn"]): raise ConfigError("index_body_fn must be None or callable") for fn in cfg["extra_checks"]: diff --git a/homelab/.claude/skills/document/SKILL.md b/homelab/.claude/skills/document/SKILL.md new file mode 100644 index 0000000..e7d0b3c --- /dev/null +++ b/homelab/.claude/skills/document/SKILL.md @@ -0,0 +1,6 @@ +--- +name: document +description: Document a homelab component, config, or change in the wiki. Use when the human says "document the new VM", "document this OPNsense config", or similar. +--- + +Read `workflows/document.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/homelab/.claude/skills/incident/SKILL.md b/homelab/.claude/skills/incident/SKILL.md new file mode 100644 index 0000000..0c07143 --- /dev/null +++ b/homelab/.claude/skills/incident/SKILL.md @@ -0,0 +1,6 @@ +--- +name: incident +description: File an incident page for something that broke. Use when the human says "log incident: " or describes an outage to record. +--- + +Read `workflows/incident.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/homelab/.claude/skills/lint/SKILL.md b/homelab/.claude/skills/lint/SKILL.md new file mode 100644 index 0000000..bdb1cb3 --- /dev/null +++ b/homelab/.claude/skills/lint/SKILL.md @@ -0,0 +1,6 @@ +--- +name: lint +description: Run the deterministic lint plus the judgment-only review pass. Use when the human says "lint the wiki". +--- + +Read `workflows/lint.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/homelab/.claude/skills/maintain/SKILL.md b/homelab/.claude/skills/maintain/SKILL.md new file mode 100644 index 0000000..e733d95 --- /dev/null +++ b/homelab/.claude/skills/maintain/SKILL.md @@ -0,0 +1,6 @@ +--- +name: maintain +description: Run or review the unattended maintenance pass on the maintenance branch. Use when the human says "maintenance pass" or "review maintenance". +--- + +Read `workflows/maintain.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/homelab/.claude/skills/query/SKILL.md b/homelab/.claude/skills/query/SKILL.md new file mode 100644 index 0000000..430b97e --- /dev/null +++ b/homelab/.claude/skills/query/SKILL.md @@ -0,0 +1,6 @@ +--- +name: query +description: Answer a question from the wiki, scanning frontmatter and the index before opening page bodies. Use for any question answerable from the wiki. +--- + +Read `workflows/query.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/homelab/.claude/skills/reconcile/SKILL.md b/homelab/.claude/skills/reconcile/SKILL.md new file mode 100644 index 0000000..4bb4a3c --- /dev/null +++ b/homelab/.claude/skills/reconcile/SKILL.md @@ -0,0 +1,6 @@ +--- +name: reconcile +description: Resolve a contested page: weigh the disagreeing sources and rewrite latest-evidence-first. Use when the human says "reconcile ". +--- + +Read `workflows/reconcile.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/homelab/.claude/skills/triage/SKILL.md b/homelab/.claude/skills/triage/SKILL.md new file mode 100644 index 0000000..bc05351 --- /dev/null +++ b/homelab/.claude/skills/triage/SKILL.md @@ -0,0 +1,6 @@ +--- +name: triage +description: Triage the raw inbox of this homelab wiki. Use when the human says "triage the inbox" or drops files into raw/inbox/ and asks to sort them. +--- + +Read `workflows/triage.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/homelab/.claude/skills/verify/SKILL.md b/homelab/.claude/skills/verify/SKILL.md new file mode 100644 index 0000000..4264a11 --- /dev/null +++ b/homelab/.claude/skills/verify/SKILL.md @@ -0,0 +1,6 @@ +--- +name: verify +description: Reality-check wiki pages against the actual infrastructure. Use when the human says "verify components" or "verify ". +--- + +Read `workflows/verify.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/homelab/CLAUDE.md b/homelab/CLAUDE.md index 26acb84..022a9bc 100644 --- a/homelab/CLAUDE.md +++ b/homelab/CLAUDE.md @@ -110,6 +110,8 @@ When the human triggers an operation, read the matching file and follow it exact | "reconcile " | `workflows/reconcile.md` | | "maintenance pass" / "review maintenance" | `workflows/maintain.md` | +In Claude Code each workflow is also a skill wrapper under `.claude/skills/`, so `/document`, `/triage`, etc. dispatch the same files deterministically; lint errors when a wrapper and its workflow drift apart. + ## Deterministic checks `python3 lint.py check` handles every mechanical health check: frontmatter validity, broken links, orphans (with unlinked-mention hints), dangling references, topology membership of active components (prime directive 3), tag taxonomy, stale contested pages, inbox health, secrets, `last_verified` staleness, Mermaid presence on topology pages, index drift, log format, OKF conformance. Run it instead of checking these by hand, and fix errors it reports before finishing any operation. A pre-commit hook (installed via `git config core.hooksPath .githooks`) lints the staged snapshot and makes errors uncommittable; never bypass it with `--no-verify`. diff --git a/homelab/lint.py b/homelab/lint.py index 261bf54..2c35421 100644 --- a/homelab/lint.py +++ b/homelab/lint.py @@ -16,8 +16,10 @@ # Top-level entries that are allowed to exist but are not wiki pages. "non_page_allowed": [ "CLAUDE.md", "index.md", "log.md", "lint.py", "wikilint", "taxonomy.md", "raw", "workflows", - ".git", ".gitignore", ".githooks", + ".git", ".gitignore", ".githooks", ".claude", ], + # Claude Code skill wrappers pair 1:1 with workflows/ (check_skills). + "skills_dir": ".claude/skills", "raw_dir": "raw", "inbox_dir": "raw/inbox", "inbox_warn_count": 10, diff --git a/homelab/wikilint/checks.py b/homelab/wikilint/checks.py index 0051044..eae3883 100644 --- a/homelab/wikilint/checks.py +++ b/homelab/wikilint/checks.py @@ -313,6 +313,50 @@ def check_types(pages, report, root): ) +def check_skills(pages, report, root): + """Harness skill wrappers must pair 1:1 with workflows: every + workflows/.md needs //SKILL.md whose body points + back at the workflow file, and no wrapper may point at nothing. Errors, + not warnings: the wrappers are dispatch surface, and silent drift there + means a slash command runs a stale or missing procedure.""" + if CONFIG["skills_dir"] is None: + return + skills_root = root / CONFIG["skills_dir"] + workflows_root = root / "workflows" + wf_stems = ( + {p.stem for p in workflows_root.glob("*.md")} + if workflows_root.is_dir() else set() + ) + skill_names = ( + {d.name for d in skills_root.iterdir() if d.is_dir()} + if skills_root.is_dir() else set() + ) + sdir = CONFIG["skills_dir"] + for stem in sorted(wf_stems - skill_names): + report.error( + "skills", f"workflows/{stem}.md", + f"no skill wrapper; add {sdir}/{stem}/SKILL.md pointing at it", + ) + for name in sorted(skill_names - wf_stems): + report.error( + "skills", f"{sdir}/{name}/SKILL.md", + f"orphan wrapper; workflows/{name}.md does not exist", + ) + for name in sorted(wf_stems & skill_names): + skill_md = skills_root / name / "SKILL.md" + rel = f"{sdir}/{name}/SKILL.md" + if not skill_md.is_file(): + report.error("skills", rel, "missing SKILL.md in wrapper directory") + continue + body = skill_md.read_text(encoding="utf-8", errors="replace") + if f"workflows/{name}.md" not in body: + report.error( + "skills", rel, + f"wrapper does not reference workflows/{name}.md; " + "the body must point at its workflow file", + ) + + def check_contested_age(pages, report): today = date.today() for p in pages: diff --git a/homelab/wikilint/cli.py b/homelab/wikilint/cli.py index 671bace..48a18bf 100644 --- a/homelab/wikilint/cli.py +++ b/homelab/wikilint/cli.py @@ -29,6 +29,7 @@ def gather_report(root): checks.check_claude_size(root, report) checks.check_tags(pages, report, root) checks.check_types(pages, report, root) + checks.check_skills(pages, report, root) checks.check_contested_age(pages, report) checks.check_inferred_markers(pages, report) checks.check_inbox(root, report) diff --git a/homelab/wikilint/settings.py b/homelab/wikilint/settings.py index d177406..0ef8758 100644 --- a/homelab/wikilint/settings.py +++ b/homelab/wikilint/settings.py @@ -38,6 +38,10 @@ # schema type with a one-line meaning, so the bundle self-describes its # type vocabulary to OKF consumers. Off for non-wiki markdown trees. "types_glossary": True, + # Directory of harness skill wrappers (e.g. ".claude/skills"), each a + # /SKILL.md whose body points at workflows/.md. When set, + # check_skills errors on any drift between the two sets. None disables. + "skills_dir": None, # Append-only operations log checked by check_log; None disables. "log_file": "log.md", # Callables(pages, report, root) run at the end of every check pass. @@ -80,6 +84,14 @@ def _validate(cfg): raise ConfigError("log_file must be None or a relative path string") if not isinstance(cfg["types_glossary"], bool): raise ConfigError("types_glossary must be a bool") + skills_dir = cfg["skills_dir"] + if skills_dir is not None: + if not isinstance(skills_dir, str) or not skills_dir.strip(): + raise ConfigError("skills_dir must be None or a non-empty relative path") + posix = PurePosixPath(skills_dir) + if posix.is_absolute() or ".." in posix.parts: + raise ConfigError( + f"skills_dir must stay within the wiki root: {skills_dir!r}") if cfg["index_body_fn"] is not None and not callable(cfg["index_body_fn"]): raise ConfigError("index_body_fn must be None or callable") for fn in cfg["extra_checks"]: diff --git a/tests/test_checks.py b/tests/test_checks.py index df91691..a44b809 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -715,6 +715,59 @@ def test_glossary_gate_off(self): self.assertEqual(findings(report, "types"), []) +class TestSkillsPairing(WikiTest): + WORKFLOW = "---\ntype: workflow\n---\n\n# Document\n\nSteps.\n" + WRAPPER = ( + "---\nname: document\ndescription: Document a thing.\n---\n\n" + "Read `workflows/document.md` and follow it exactly.\n" + ) + + def wiki(self, files=None): + root = make_wiki(self.tmp, files=files) + use_variant_with("generic", skills_dir=".claude/skills") + return root + + def test_disabled_by_default(self): + root = make_wiki(self.tmp, files={"workflows/document.md": self.WORKFLOW}) + report = gather(root) + self.assertEqual(findings(report, "skills"), []) + + def test_paired_wrapper_passes(self): + root = self.wiki(files={ + "workflows/document.md": self.WORKFLOW, + ".claude/skills/document/SKILL.md": self.WRAPPER, + }) + report = gather(root) + self.assertEqual(findings(report, "skills"), []) + + def test_workflow_without_wrapper_errors(self): + root = self.wiki(files={"workflows/document.md": self.WORKFLOW}) + report = gather(root) + hits = findings(report, "skills", "ERROR") + self.assertEqual(len(hits), 1) + self.assertIn("no skill wrapper", hits[0][3]) + + def test_orphan_wrapper_errors(self): + root = self.wiki(files={ + ".claude/skills/ghost/SKILL.md": self.WRAPPER, + }) + report = gather(root) + hits = findings(report, "skills", "ERROR") + self.assertEqual(len(hits), 1) + self.assertIn("orphan wrapper", hits[0][3]) + + def test_wrapper_must_reference_its_workflow(self): + root = self.wiki(files={ + "workflows/document.md": self.WORKFLOW, + ".claude/skills/document/SKILL.md": + "---\nname: document\ndescription: d\n---\n\nJust do it from memory.\n", + }) + report = gather(root) + hits = findings(report, "skills", "ERROR") + self.assertEqual(len(hits), 1) + self.assertIn("does not reference", hits[0][3]) + + class TestOkfConformance(WikiTest): def test_stray_markdown_needs_frontmatter_and_type(self): root = make_wiki(self.tmp, files={ diff --git a/tests/test_variants.py b/tests/test_variants.py index 3e6da63..9c208ad 100644 --- a/tests/test_variants.py +++ b/tests/test_variants.py @@ -28,13 +28,22 @@ def test_everything_compiles(self): py_compile.compile(str(p), doraise=True) +# Knobs a variant may set differently from the others, each with a reason. +# Anything not listed here must stay uniform across variants. +INTENTIONAL_DIVERGENCES = { + # skills-dispatch prototype: homelab pairs workflows/ with .claude/skills/ + # wrappers; other variants adopt it only if the prototype graduates. + "skills_dir", +} + + class TestConfigSanity(unittest.TestCase): def test_configs_load_and_share_keys(self): key_sets = [] for variant in VARIANTS: config, extra = load_variant_config(variant) self.assertTrue(callable(extra), variant) - key_sets.append((variant, set(config))) + key_sets.append((variant, set(config) - INTENTIONAL_DIVERGENCES)) _, reference = key_sets[0] for variant, keys in key_sets[1:]: self.assertEqual(keys, reference, f"{variant} CONFIG keys diverge from generic") @@ -68,6 +77,8 @@ def test_extension_defaults_present_and_consistent(self): configure(config, extra) for key in DEFAULTS: self.assertIn(key, CONFIG, f"{variant} missing extension key {key}") + if key in INTENTIONAL_DIVERGENCES: + continue seen.setdefault(key, CONFIG[key]) self.assertEqual(CONFIG[key], seen[key], f"{variant} diverges on extension key {key}") From 75e0dc0f23400c76e5e6d28e30b73af46ba9f114 Mon Sep 17 00:00:00 2001 From: SunsetDrifter Date: Wed, 22 Jul 2026 09:34:11 +0200 Subject: [PATCH 2/5] proto: generic-variant skill wrappers for the okf-wiki sandbox Add .claude/skills//SKILL.md wrappers for the six generic workflows (triage, ingest, query, lint, reconcile, maintain), enable skills_dir in generic/lint.py, and document the dispatch in generic/CLAUDE.md, mirroring the homelab pattern. Also fix tests/test_checks.py:TestSkillsPairing.test_disabled_by_default, which hardcoded variant="generic" to prove check_skills is opt-in; now that generic enables skills_dir, the test exercises the codebase variant instead, which still leaves the knob unset. --- generic/.claude/skills/ingest/SKILL.md | 6 ++++++ generic/.claude/skills/lint/SKILL.md | 6 ++++++ generic/.claude/skills/maintain/SKILL.md | 6 ++++++ generic/.claude/skills/query/SKILL.md | 6 ++++++ generic/.claude/skills/reconcile/SKILL.md | 6 ++++++ generic/.claude/skills/triage/SKILL.md | 6 ++++++ generic/CLAUDE.md | 2 ++ generic/lint.py | 4 +++- tests/test_checks.py | 5 ++++- 9 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 generic/.claude/skills/ingest/SKILL.md create mode 100644 generic/.claude/skills/lint/SKILL.md create mode 100644 generic/.claude/skills/maintain/SKILL.md create mode 100644 generic/.claude/skills/query/SKILL.md create mode 100644 generic/.claude/skills/reconcile/SKILL.md create mode 100644 generic/.claude/skills/triage/SKILL.md diff --git a/generic/.claude/skills/ingest/SKILL.md b/generic/.claude/skills/ingest/SKILL.md new file mode 100644 index 0000000..70986d4 --- /dev/null +++ b/generic/.claude/skills/ingest/SKILL.md @@ -0,0 +1,6 @@ +--- +name: ingest +description: Ingest one raw source into the wiki: source page, affected pages, new pages where warranted. Use when the human says "ingest ". +--- + +Read `workflows/ingest.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/generic/.claude/skills/lint/SKILL.md b/generic/.claude/skills/lint/SKILL.md new file mode 100644 index 0000000..bdb1cb3 --- /dev/null +++ b/generic/.claude/skills/lint/SKILL.md @@ -0,0 +1,6 @@ +--- +name: lint +description: Run the deterministic lint plus the judgment-only review pass. Use when the human says "lint the wiki". +--- + +Read `workflows/lint.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/generic/.claude/skills/maintain/SKILL.md b/generic/.claude/skills/maintain/SKILL.md new file mode 100644 index 0000000..e733d95 --- /dev/null +++ b/generic/.claude/skills/maintain/SKILL.md @@ -0,0 +1,6 @@ +--- +name: maintain +description: Run or review the unattended maintenance pass on the maintenance branch. Use when the human says "maintenance pass" or "review maintenance". +--- + +Read `workflows/maintain.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/generic/.claude/skills/query/SKILL.md b/generic/.claude/skills/query/SKILL.md new file mode 100644 index 0000000..430b97e --- /dev/null +++ b/generic/.claude/skills/query/SKILL.md @@ -0,0 +1,6 @@ +--- +name: query +description: Answer a question from the wiki, scanning frontmatter and the index before opening page bodies. Use for any question answerable from the wiki. +--- + +Read `workflows/query.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/generic/.claude/skills/reconcile/SKILL.md b/generic/.claude/skills/reconcile/SKILL.md new file mode 100644 index 0000000..4bb4a3c --- /dev/null +++ b/generic/.claude/skills/reconcile/SKILL.md @@ -0,0 +1,6 @@ +--- +name: reconcile +description: Resolve a contested page: weigh the disagreeing sources and rewrite latest-evidence-first. Use when the human says "reconcile ". +--- + +Read `workflows/reconcile.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/generic/.claude/skills/triage/SKILL.md b/generic/.claude/skills/triage/SKILL.md new file mode 100644 index 0000000..ceeb659 --- /dev/null +++ b/generic/.claude/skills/triage/SKILL.md @@ -0,0 +1,6 @@ +--- +name: triage +description: Triage the raw inbox of this wiki. Use when the human says "triage the inbox" or "process the inbox", or drops files into raw/inbox/ and asks to sort them. +--- + +Read `workflows/triage.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/generic/CLAUDE.md b/generic/CLAUDE.md index d6beec0..783617a 100644 --- a/generic/CLAUDE.md +++ b/generic/CLAUDE.md @@ -85,6 +85,8 @@ When the human triggers an operation, read the matching file and follow it exact | "reconcile " | `workflows/reconcile.md` | | "maintenance pass" / "review maintenance" | `workflows/maintain.md` | +In Claude Code each workflow is also a skill wrapper under `.claude/skills/`, so `/ingest`, `/triage`, etc. dispatch the same files deterministically; lint errors when a wrapper and its workflow drift apart. + ## Deterministic checks `python3 lint.py check` handles every mechanical health check: frontmatter validity, broken links, orphans (with unlinked-mention hints), dangling references, tag taxonomy, stale contested pages, inbox health, secrets, index drift, log format, OKF conformance. Run it instead of checking these by hand, and fix errors it reports before finishing any operation. A pre-commit hook (installed via `git config core.hooksPath .githooks`) lints the staged snapshot and makes errors uncommittable; never bypass it with `--no-verify`. diff --git a/generic/lint.py b/generic/lint.py index a4260e8..fbeeaae 100644 --- a/generic/lint.py +++ b/generic/lint.py @@ -13,8 +13,10 @@ # Top-level entries that are allowed to exist but are not wiki pages. "non_page_allowed": [ "CLAUDE.md", "index.md", "log.md", "lint.py", "wikilint", "taxonomy.md", - "raw", "workflows", ".git", ".gitignore", ".githooks", + "raw", "workflows", ".git", ".gitignore", ".githooks", ".claude", ], + # Claude Code skill wrappers pair 1:1 with workflows/ (check_skills). + "skills_dir": ".claude/skills", "raw_dir": "raw", "inbox_dir": "raw/inbox", "inbox_warn_count": 10, diff --git a/tests/test_checks.py b/tests/test_checks.py index a44b809..c6b95e4 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -728,7 +728,10 @@ def wiki(self, files=None): return root def test_disabled_by_default(self): - root = make_wiki(self.tmp, files={"workflows/document.md": self.WORKFLOW}) + # "generic" now ships skills_dir (task 1: generic-variant wrappers), so + # this exercises a variant that still leaves the knob unset. + root = make_wiki(self.tmp, variant="codebase", + files={"workflows/document.md": self.WORKFLOW}) report = gather(root) self.assertEqual(findings(report, "skills"), []) From ce6e227e118908834324721e72e322117fa05350 Mon Sep 17 00:00:00 2001 From: SunsetDrifter Date: Wed, 22 Jul 2026 22:30:51 +0200 Subject: [PATCH 3/5] proto: namespace skill wrappers as wiki-* via skills_prefix knob Field finding from the okf-wiki seeding session: a bare /triage collided with a globally installed triage skill. Wrappers are now wiki-; check_skills pairs workflows/.md with // and workflows keep their natural names. --- codebase-large/wikilint/checks.py | 20 +++++++++------- codebase-large/wikilint/settings.py | 12 ++++++++-- codebase/wikilint/checks.py | 20 +++++++++------- codebase/wikilint/settings.py | 12 ++++++++-- .../skills/{ingest => wiki-ingest}/SKILL.md | 2 +- .../skills/{lint => wiki-lint}/SKILL.md | 2 +- .../{maintain => wiki-maintain}/SKILL.md | 2 +- .../.claude/skills/wiki-query}/SKILL.md | 2 +- .../{reconcile => wiki-reconcile}/SKILL.md | 2 +- .../skills/{triage => wiki-triage}/SKILL.md | 2 +- generic/CLAUDE.md | 2 +- generic/lint.py | 1 + generic/wikilint/checks.py | 20 +++++++++------- generic/wikilint/settings.py | 12 ++++++++-- .../{document => wiki-document}/SKILL.md | 2 +- .../{incident => wiki-incident}/SKILL.md | 2 +- .../skills/{lint => wiki-lint}/SKILL.md | 2 +- .../{maintain => wiki-maintain}/SKILL.md | 2 +- .../.claude/skills/wiki-query}/SKILL.md | 2 +- .../{reconcile => wiki-reconcile}/SKILL.md | 2 +- .../skills/{triage => wiki-triage}/SKILL.md | 2 +- .../skills/{verify => wiki-verify}/SKILL.md | 2 +- homelab/CLAUDE.md | 2 +- homelab/lint.py | 1 + homelab/wikilint/checks.py | 20 +++++++++------- homelab/wikilint/settings.py | 12 ++++++++-- tests/test_checks.py | 23 +++++++++++++++---- tests/test_variants.py | 7 ++++-- 28 files changed, 129 insertions(+), 63 deletions(-) rename generic/.claude/skills/{ingest => wiki-ingest}/SKILL.md (96%) rename generic/.claude/skills/{lint => wiki-lint}/SKILL.md (96%) rename generic/.claude/skills/{maintain => wiki-maintain}/SKILL.md (95%) rename {homelab/.claude/skills/query => generic/.claude/skills/wiki-query}/SKILL.md (96%) rename generic/.claude/skills/{reconcile => wiki-reconcile}/SKILL.md (95%) rename generic/.claude/skills/{triage => wiki-triage}/SKILL.md (96%) rename homelab/.claude/skills/{document => wiki-document}/SKILL.md (95%) rename homelab/.claude/skills/{incident => wiki-incident}/SKILL.md (95%) rename homelab/.claude/skills/{lint => wiki-lint}/SKILL.md (96%) rename homelab/.claude/skills/{maintain => wiki-maintain}/SKILL.md (95%) rename {generic/.claude/skills/query => homelab/.claude/skills/wiki-query}/SKILL.md (96%) rename homelab/.claude/skills/{reconcile => wiki-reconcile}/SKILL.md (95%) rename homelab/.claude/skills/{triage => wiki-triage}/SKILL.md (96%) rename homelab/.claude/skills/{verify => wiki-verify}/SKILL.md (96%) diff --git a/codebase-large/wikilint/checks.py b/codebase-large/wikilint/checks.py index eae3883..e0f3d1e 100644 --- a/codebase-large/wikilint/checks.py +++ b/codebase-large/wikilint/checks.py @@ -327,32 +327,36 @@ def check_skills(pages, report, root): {p.stem for p in workflows_root.glob("*.md")} if workflows_root.is_dir() else set() ) + prefix = CONFIG["skills_prefix"] skill_names = ( {d.name for d in skills_root.iterdir() if d.is_dir()} if skills_root.is_dir() else set() ) + expected = {prefix + stem: stem for stem in wf_stems} sdir = CONFIG["skills_dir"] - for stem in sorted(wf_stems - skill_names): + for name in sorted(set(expected) - skill_names): report.error( - "skills", f"workflows/{stem}.md", - f"no skill wrapper; add {sdir}/{stem}/SKILL.md pointing at it", + "skills", f"workflows/{expected[name]}.md", + f"no skill wrapper; add {sdir}/{name}/SKILL.md pointing at it", ) - for name in sorted(skill_names - wf_stems): + for name in sorted(skill_names - set(expected)): report.error( "skills", f"{sdir}/{name}/SKILL.md", - f"orphan wrapper; workflows/{name}.md does not exist", + f"orphan wrapper; no workflow pairs with it " + f"(expected form: {prefix})", ) - for name in sorted(wf_stems & skill_names): + for name in sorted(set(expected) & skill_names): + stem = expected[name] skill_md = skills_root / name / "SKILL.md" rel = f"{sdir}/{name}/SKILL.md" if not skill_md.is_file(): report.error("skills", rel, "missing SKILL.md in wrapper directory") continue body = skill_md.read_text(encoding="utf-8", errors="replace") - if f"workflows/{name}.md" not in body: + if f"workflows/{stem}.md" not in body: report.error( "skills", rel, - f"wrapper does not reference workflows/{name}.md; " + f"wrapper does not reference workflows/{stem}.md; " "the body must point at its workflow file", ) diff --git a/codebase-large/wikilint/settings.py b/codebase-large/wikilint/settings.py index 0ef8758..98b8009 100644 --- a/codebase-large/wikilint/settings.py +++ b/codebase-large/wikilint/settings.py @@ -39,9 +39,15 @@ # type vocabulary to OKF consumers. Off for non-wiki markdown trees. "types_glossary": True, # Directory of harness skill wrappers (e.g. ".claude/skills"), each a - # /SKILL.md whose body points at workflows/.md. When set, - # check_skills errors on any drift between the two sets. None disables. + # /SKILL.md whose body points at workflows/.md. When + # set, check_skills errors on any drift between the two sets. None + # disables. "skills_dir": None, + # Wrapper-name prefix, namespacing wiki skills away from a user's global + # skills (field finding, 2026-07-22: a bare /triage collided with a + # global triage skill). "wiki-" pairs workflows/triage.md with + # /wiki-triage/SKILL.md. + "skills_prefix": "", # Append-only operations log checked by check_log; None disables. "log_file": "log.md", # Callables(pages, report, root) run at the end of every check pass. @@ -92,6 +98,8 @@ def _validate(cfg): if posix.is_absolute() or ".." in posix.parts: raise ConfigError( f"skills_dir must stay within the wiki root: {skills_dir!r}") + if not isinstance(cfg["skills_prefix"], str): + raise ConfigError("skills_prefix must be a string") if cfg["index_body_fn"] is not None and not callable(cfg["index_body_fn"]): raise ConfigError("index_body_fn must be None or callable") for fn in cfg["extra_checks"]: diff --git a/codebase/wikilint/checks.py b/codebase/wikilint/checks.py index eae3883..e0f3d1e 100644 --- a/codebase/wikilint/checks.py +++ b/codebase/wikilint/checks.py @@ -327,32 +327,36 @@ def check_skills(pages, report, root): {p.stem for p in workflows_root.glob("*.md")} if workflows_root.is_dir() else set() ) + prefix = CONFIG["skills_prefix"] skill_names = ( {d.name for d in skills_root.iterdir() if d.is_dir()} if skills_root.is_dir() else set() ) + expected = {prefix + stem: stem for stem in wf_stems} sdir = CONFIG["skills_dir"] - for stem in sorted(wf_stems - skill_names): + for name in sorted(set(expected) - skill_names): report.error( - "skills", f"workflows/{stem}.md", - f"no skill wrapper; add {sdir}/{stem}/SKILL.md pointing at it", + "skills", f"workflows/{expected[name]}.md", + f"no skill wrapper; add {sdir}/{name}/SKILL.md pointing at it", ) - for name in sorted(skill_names - wf_stems): + for name in sorted(skill_names - set(expected)): report.error( "skills", f"{sdir}/{name}/SKILL.md", - f"orphan wrapper; workflows/{name}.md does not exist", + f"orphan wrapper; no workflow pairs with it " + f"(expected form: {prefix})", ) - for name in sorted(wf_stems & skill_names): + for name in sorted(set(expected) & skill_names): + stem = expected[name] skill_md = skills_root / name / "SKILL.md" rel = f"{sdir}/{name}/SKILL.md" if not skill_md.is_file(): report.error("skills", rel, "missing SKILL.md in wrapper directory") continue body = skill_md.read_text(encoding="utf-8", errors="replace") - if f"workflows/{name}.md" not in body: + if f"workflows/{stem}.md" not in body: report.error( "skills", rel, - f"wrapper does not reference workflows/{name}.md; " + f"wrapper does not reference workflows/{stem}.md; " "the body must point at its workflow file", ) diff --git a/codebase/wikilint/settings.py b/codebase/wikilint/settings.py index 0ef8758..98b8009 100644 --- a/codebase/wikilint/settings.py +++ b/codebase/wikilint/settings.py @@ -39,9 +39,15 @@ # type vocabulary to OKF consumers. Off for non-wiki markdown trees. "types_glossary": True, # Directory of harness skill wrappers (e.g. ".claude/skills"), each a - # /SKILL.md whose body points at workflows/.md. When set, - # check_skills errors on any drift between the two sets. None disables. + # /SKILL.md whose body points at workflows/.md. When + # set, check_skills errors on any drift between the two sets. None + # disables. "skills_dir": None, + # Wrapper-name prefix, namespacing wiki skills away from a user's global + # skills (field finding, 2026-07-22: a bare /triage collided with a + # global triage skill). "wiki-" pairs workflows/triage.md with + # /wiki-triage/SKILL.md. + "skills_prefix": "", # Append-only operations log checked by check_log; None disables. "log_file": "log.md", # Callables(pages, report, root) run at the end of every check pass. @@ -92,6 +98,8 @@ def _validate(cfg): if posix.is_absolute() or ".." in posix.parts: raise ConfigError( f"skills_dir must stay within the wiki root: {skills_dir!r}") + if not isinstance(cfg["skills_prefix"], str): + raise ConfigError("skills_prefix must be a string") if cfg["index_body_fn"] is not None and not callable(cfg["index_body_fn"]): raise ConfigError("index_body_fn must be None or callable") for fn in cfg["extra_checks"]: diff --git a/generic/.claude/skills/ingest/SKILL.md b/generic/.claude/skills/wiki-ingest/SKILL.md similarity index 96% rename from generic/.claude/skills/ingest/SKILL.md rename to generic/.claude/skills/wiki-ingest/SKILL.md index 70986d4..5726a1f 100644 --- a/generic/.claude/skills/ingest/SKILL.md +++ b/generic/.claude/skills/wiki-ingest/SKILL.md @@ -1,5 +1,5 @@ --- -name: ingest +name: wiki-ingest description: Ingest one raw source into the wiki: source page, affected pages, new pages where warranted. Use when the human says "ingest ". --- diff --git a/generic/.claude/skills/lint/SKILL.md b/generic/.claude/skills/wiki-lint/SKILL.md similarity index 96% rename from generic/.claude/skills/lint/SKILL.md rename to generic/.claude/skills/wiki-lint/SKILL.md index bdb1cb3..0f3dbd6 100644 --- a/generic/.claude/skills/lint/SKILL.md +++ b/generic/.claude/skills/wiki-lint/SKILL.md @@ -1,5 +1,5 @@ --- -name: lint +name: wiki-lint description: Run the deterministic lint plus the judgment-only review pass. Use when the human says "lint the wiki". --- diff --git a/generic/.claude/skills/maintain/SKILL.md b/generic/.claude/skills/wiki-maintain/SKILL.md similarity index 95% rename from generic/.claude/skills/maintain/SKILL.md rename to generic/.claude/skills/wiki-maintain/SKILL.md index e733d95..5aad0f5 100644 --- a/generic/.claude/skills/maintain/SKILL.md +++ b/generic/.claude/skills/wiki-maintain/SKILL.md @@ -1,5 +1,5 @@ --- -name: maintain +name: wiki-maintain description: Run or review the unattended maintenance pass on the maintenance branch. Use when the human says "maintenance pass" or "review maintenance". --- diff --git a/homelab/.claude/skills/query/SKILL.md b/generic/.claude/skills/wiki-query/SKILL.md similarity index 96% rename from homelab/.claude/skills/query/SKILL.md rename to generic/.claude/skills/wiki-query/SKILL.md index 430b97e..7810d2d 100644 --- a/homelab/.claude/skills/query/SKILL.md +++ b/generic/.claude/skills/wiki-query/SKILL.md @@ -1,5 +1,5 @@ --- -name: query +name: wiki-query description: Answer a question from the wiki, scanning frontmatter and the index before opening page bodies. Use for any question answerable from the wiki. --- diff --git a/generic/.claude/skills/reconcile/SKILL.md b/generic/.claude/skills/wiki-reconcile/SKILL.md similarity index 95% rename from generic/.claude/skills/reconcile/SKILL.md rename to generic/.claude/skills/wiki-reconcile/SKILL.md index 4bb4a3c..8c4e35d 100644 --- a/generic/.claude/skills/reconcile/SKILL.md +++ b/generic/.claude/skills/wiki-reconcile/SKILL.md @@ -1,5 +1,5 @@ --- -name: reconcile +name: wiki-reconcile description: Resolve a contested page: weigh the disagreeing sources and rewrite latest-evidence-first. Use when the human says "reconcile ". --- diff --git a/generic/.claude/skills/triage/SKILL.md b/generic/.claude/skills/wiki-triage/SKILL.md similarity index 96% rename from generic/.claude/skills/triage/SKILL.md rename to generic/.claude/skills/wiki-triage/SKILL.md index ceeb659..789a9ed 100644 --- a/generic/.claude/skills/triage/SKILL.md +++ b/generic/.claude/skills/wiki-triage/SKILL.md @@ -1,5 +1,5 @@ --- -name: triage +name: wiki-triage description: Triage the raw inbox of this wiki. Use when the human says "triage the inbox" or "process the inbox", or drops files into raw/inbox/ and asks to sort them. --- diff --git a/generic/CLAUDE.md b/generic/CLAUDE.md index 783617a..b2505d7 100644 --- a/generic/CLAUDE.md +++ b/generic/CLAUDE.md @@ -85,7 +85,7 @@ When the human triggers an operation, read the matching file and follow it exact | "reconcile " | `workflows/reconcile.md` | | "maintenance pass" / "review maintenance" | `workflows/maintain.md` | -In Claude Code each workflow is also a skill wrapper under `.claude/skills/`, so `/ingest`, `/triage`, etc. dispatch the same files deterministically; lint errors when a wrapper and its workflow drift apart. +In Claude Code each workflow is also a skill wrapper under `.claude/skills/`, so `/wiki-ingest`, `/wiki-triage`, etc. dispatch the same files deterministically (the wiki- prefix avoids collisions with globally installed skills); lint errors when a wrapper and its workflow drift apart. ## Deterministic checks diff --git a/generic/lint.py b/generic/lint.py index fbeeaae..f2081a4 100644 --- a/generic/lint.py +++ b/generic/lint.py @@ -17,6 +17,7 @@ ], # Claude Code skill wrappers pair 1:1 with workflows/ (check_skills). "skills_dir": ".claude/skills", + "skills_prefix": "wiki-", "raw_dir": "raw", "inbox_dir": "raw/inbox", "inbox_warn_count": 10, diff --git a/generic/wikilint/checks.py b/generic/wikilint/checks.py index eae3883..e0f3d1e 100644 --- a/generic/wikilint/checks.py +++ b/generic/wikilint/checks.py @@ -327,32 +327,36 @@ def check_skills(pages, report, root): {p.stem for p in workflows_root.glob("*.md")} if workflows_root.is_dir() else set() ) + prefix = CONFIG["skills_prefix"] skill_names = ( {d.name for d in skills_root.iterdir() if d.is_dir()} if skills_root.is_dir() else set() ) + expected = {prefix + stem: stem for stem in wf_stems} sdir = CONFIG["skills_dir"] - for stem in sorted(wf_stems - skill_names): + for name in sorted(set(expected) - skill_names): report.error( - "skills", f"workflows/{stem}.md", - f"no skill wrapper; add {sdir}/{stem}/SKILL.md pointing at it", + "skills", f"workflows/{expected[name]}.md", + f"no skill wrapper; add {sdir}/{name}/SKILL.md pointing at it", ) - for name in sorted(skill_names - wf_stems): + for name in sorted(skill_names - set(expected)): report.error( "skills", f"{sdir}/{name}/SKILL.md", - f"orphan wrapper; workflows/{name}.md does not exist", + f"orphan wrapper; no workflow pairs with it " + f"(expected form: {prefix})", ) - for name in sorted(wf_stems & skill_names): + for name in sorted(set(expected) & skill_names): + stem = expected[name] skill_md = skills_root / name / "SKILL.md" rel = f"{sdir}/{name}/SKILL.md" if not skill_md.is_file(): report.error("skills", rel, "missing SKILL.md in wrapper directory") continue body = skill_md.read_text(encoding="utf-8", errors="replace") - if f"workflows/{name}.md" not in body: + if f"workflows/{stem}.md" not in body: report.error( "skills", rel, - f"wrapper does not reference workflows/{name}.md; " + f"wrapper does not reference workflows/{stem}.md; " "the body must point at its workflow file", ) diff --git a/generic/wikilint/settings.py b/generic/wikilint/settings.py index 0ef8758..98b8009 100644 --- a/generic/wikilint/settings.py +++ b/generic/wikilint/settings.py @@ -39,9 +39,15 @@ # type vocabulary to OKF consumers. Off for non-wiki markdown trees. "types_glossary": True, # Directory of harness skill wrappers (e.g. ".claude/skills"), each a - # /SKILL.md whose body points at workflows/.md. When set, - # check_skills errors on any drift between the two sets. None disables. + # /SKILL.md whose body points at workflows/.md. When + # set, check_skills errors on any drift between the two sets. None + # disables. "skills_dir": None, + # Wrapper-name prefix, namespacing wiki skills away from a user's global + # skills (field finding, 2026-07-22: a bare /triage collided with a + # global triage skill). "wiki-" pairs workflows/triage.md with + # /wiki-triage/SKILL.md. + "skills_prefix": "", # Append-only operations log checked by check_log; None disables. "log_file": "log.md", # Callables(pages, report, root) run at the end of every check pass. @@ -92,6 +98,8 @@ def _validate(cfg): if posix.is_absolute() or ".." in posix.parts: raise ConfigError( f"skills_dir must stay within the wiki root: {skills_dir!r}") + if not isinstance(cfg["skills_prefix"], str): + raise ConfigError("skills_prefix must be a string") if cfg["index_body_fn"] is not None and not callable(cfg["index_body_fn"]): raise ConfigError("index_body_fn must be None or callable") for fn in cfg["extra_checks"]: diff --git a/homelab/.claude/skills/document/SKILL.md b/homelab/.claude/skills/wiki-document/SKILL.md similarity index 95% rename from homelab/.claude/skills/document/SKILL.md rename to homelab/.claude/skills/wiki-document/SKILL.md index e7d0b3c..6a72469 100644 --- a/homelab/.claude/skills/document/SKILL.md +++ b/homelab/.claude/skills/wiki-document/SKILL.md @@ -1,5 +1,5 @@ --- -name: document +name: wiki-document description: Document a homelab component, config, or change in the wiki. Use when the human says "document the new VM", "document this OPNsense config", or similar. --- diff --git a/homelab/.claude/skills/incident/SKILL.md b/homelab/.claude/skills/wiki-incident/SKILL.md similarity index 95% rename from homelab/.claude/skills/incident/SKILL.md rename to homelab/.claude/skills/wiki-incident/SKILL.md index 0c07143..f91473f 100644 --- a/homelab/.claude/skills/incident/SKILL.md +++ b/homelab/.claude/skills/wiki-incident/SKILL.md @@ -1,5 +1,5 @@ --- -name: incident +name: wiki-incident description: File an incident page for something that broke. Use when the human says "log incident: " or describes an outage to record. --- diff --git a/homelab/.claude/skills/lint/SKILL.md b/homelab/.claude/skills/wiki-lint/SKILL.md similarity index 96% rename from homelab/.claude/skills/lint/SKILL.md rename to homelab/.claude/skills/wiki-lint/SKILL.md index bdb1cb3..0f3dbd6 100644 --- a/homelab/.claude/skills/lint/SKILL.md +++ b/homelab/.claude/skills/wiki-lint/SKILL.md @@ -1,5 +1,5 @@ --- -name: lint +name: wiki-lint description: Run the deterministic lint plus the judgment-only review pass. Use when the human says "lint the wiki". --- diff --git a/homelab/.claude/skills/maintain/SKILL.md b/homelab/.claude/skills/wiki-maintain/SKILL.md similarity index 95% rename from homelab/.claude/skills/maintain/SKILL.md rename to homelab/.claude/skills/wiki-maintain/SKILL.md index e733d95..5aad0f5 100644 --- a/homelab/.claude/skills/maintain/SKILL.md +++ b/homelab/.claude/skills/wiki-maintain/SKILL.md @@ -1,5 +1,5 @@ --- -name: maintain +name: wiki-maintain description: Run or review the unattended maintenance pass on the maintenance branch. Use when the human says "maintenance pass" or "review maintenance". --- diff --git a/generic/.claude/skills/query/SKILL.md b/homelab/.claude/skills/wiki-query/SKILL.md similarity index 96% rename from generic/.claude/skills/query/SKILL.md rename to homelab/.claude/skills/wiki-query/SKILL.md index 430b97e..7810d2d 100644 --- a/generic/.claude/skills/query/SKILL.md +++ b/homelab/.claude/skills/wiki-query/SKILL.md @@ -1,5 +1,5 @@ --- -name: query +name: wiki-query description: Answer a question from the wiki, scanning frontmatter and the index before opening page bodies. Use for any question answerable from the wiki. --- diff --git a/homelab/.claude/skills/reconcile/SKILL.md b/homelab/.claude/skills/wiki-reconcile/SKILL.md similarity index 95% rename from homelab/.claude/skills/reconcile/SKILL.md rename to homelab/.claude/skills/wiki-reconcile/SKILL.md index 4bb4a3c..8c4e35d 100644 --- a/homelab/.claude/skills/reconcile/SKILL.md +++ b/homelab/.claude/skills/wiki-reconcile/SKILL.md @@ -1,5 +1,5 @@ --- -name: reconcile +name: wiki-reconcile description: Resolve a contested page: weigh the disagreeing sources and rewrite latest-evidence-first. Use when the human says "reconcile ". --- diff --git a/homelab/.claude/skills/triage/SKILL.md b/homelab/.claude/skills/wiki-triage/SKILL.md similarity index 96% rename from homelab/.claude/skills/triage/SKILL.md rename to homelab/.claude/skills/wiki-triage/SKILL.md index bc05351..7adbd9c 100644 --- a/homelab/.claude/skills/triage/SKILL.md +++ b/homelab/.claude/skills/wiki-triage/SKILL.md @@ -1,5 +1,5 @@ --- -name: triage +name: wiki-triage description: Triage the raw inbox of this homelab wiki. Use when the human says "triage the inbox" or drops files into raw/inbox/ and asks to sort them. --- diff --git a/homelab/.claude/skills/verify/SKILL.md b/homelab/.claude/skills/wiki-verify/SKILL.md similarity index 96% rename from homelab/.claude/skills/verify/SKILL.md rename to homelab/.claude/skills/wiki-verify/SKILL.md index 4264a11..f2a4c7b 100644 --- a/homelab/.claude/skills/verify/SKILL.md +++ b/homelab/.claude/skills/wiki-verify/SKILL.md @@ -1,5 +1,5 @@ --- -name: verify +name: wiki-verify description: Reality-check wiki pages against the actual infrastructure. Use when the human says "verify components" or "verify ". --- diff --git a/homelab/CLAUDE.md b/homelab/CLAUDE.md index 022a9bc..767acd9 100644 --- a/homelab/CLAUDE.md +++ b/homelab/CLAUDE.md @@ -110,7 +110,7 @@ When the human triggers an operation, read the matching file and follow it exact | "reconcile " | `workflows/reconcile.md` | | "maintenance pass" / "review maintenance" | `workflows/maintain.md` | -In Claude Code each workflow is also a skill wrapper under `.claude/skills/`, so `/document`, `/triage`, etc. dispatch the same files deterministically; lint errors when a wrapper and its workflow drift apart. +In Claude Code each workflow is also a skill wrapper under `.claude/skills/`, so `/wiki-document`, `/wiki-triage`, etc. dispatch the same files deterministically (the wiki- prefix avoids collisions with globally installed skills); lint errors when a wrapper and its workflow drift apart. ## Deterministic checks diff --git a/homelab/lint.py b/homelab/lint.py index 2c35421..0b11c99 100644 --- a/homelab/lint.py +++ b/homelab/lint.py @@ -20,6 +20,7 @@ ], # Claude Code skill wrappers pair 1:1 with workflows/ (check_skills). "skills_dir": ".claude/skills", + "skills_prefix": "wiki-", "raw_dir": "raw", "inbox_dir": "raw/inbox", "inbox_warn_count": 10, diff --git a/homelab/wikilint/checks.py b/homelab/wikilint/checks.py index eae3883..e0f3d1e 100644 --- a/homelab/wikilint/checks.py +++ b/homelab/wikilint/checks.py @@ -327,32 +327,36 @@ def check_skills(pages, report, root): {p.stem for p in workflows_root.glob("*.md")} if workflows_root.is_dir() else set() ) + prefix = CONFIG["skills_prefix"] skill_names = ( {d.name for d in skills_root.iterdir() if d.is_dir()} if skills_root.is_dir() else set() ) + expected = {prefix + stem: stem for stem in wf_stems} sdir = CONFIG["skills_dir"] - for stem in sorted(wf_stems - skill_names): + for name in sorted(set(expected) - skill_names): report.error( - "skills", f"workflows/{stem}.md", - f"no skill wrapper; add {sdir}/{stem}/SKILL.md pointing at it", + "skills", f"workflows/{expected[name]}.md", + f"no skill wrapper; add {sdir}/{name}/SKILL.md pointing at it", ) - for name in sorted(skill_names - wf_stems): + for name in sorted(skill_names - set(expected)): report.error( "skills", f"{sdir}/{name}/SKILL.md", - f"orphan wrapper; workflows/{name}.md does not exist", + f"orphan wrapper; no workflow pairs with it " + f"(expected form: {prefix})", ) - for name in sorted(wf_stems & skill_names): + for name in sorted(set(expected) & skill_names): + stem = expected[name] skill_md = skills_root / name / "SKILL.md" rel = f"{sdir}/{name}/SKILL.md" if not skill_md.is_file(): report.error("skills", rel, "missing SKILL.md in wrapper directory") continue body = skill_md.read_text(encoding="utf-8", errors="replace") - if f"workflows/{name}.md" not in body: + if f"workflows/{stem}.md" not in body: report.error( "skills", rel, - f"wrapper does not reference workflows/{name}.md; " + f"wrapper does not reference workflows/{stem}.md; " "the body must point at its workflow file", ) diff --git a/homelab/wikilint/settings.py b/homelab/wikilint/settings.py index 0ef8758..98b8009 100644 --- a/homelab/wikilint/settings.py +++ b/homelab/wikilint/settings.py @@ -39,9 +39,15 @@ # type vocabulary to OKF consumers. Off for non-wiki markdown trees. "types_glossary": True, # Directory of harness skill wrappers (e.g. ".claude/skills"), each a - # /SKILL.md whose body points at workflows/.md. When set, - # check_skills errors on any drift between the two sets. None disables. + # /SKILL.md whose body points at workflows/.md. When + # set, check_skills errors on any drift between the two sets. None + # disables. "skills_dir": None, + # Wrapper-name prefix, namespacing wiki skills away from a user's global + # skills (field finding, 2026-07-22: a bare /triage collided with a + # global triage skill). "wiki-" pairs workflows/triage.md with + # /wiki-triage/SKILL.md. + "skills_prefix": "", # Append-only operations log checked by check_log; None disables. "log_file": "log.md", # Callables(pages, report, root) run at the end of every check pass. @@ -92,6 +98,8 @@ def _validate(cfg): if posix.is_absolute() or ".." in posix.parts: raise ConfigError( f"skills_dir must stay within the wiki root: {skills_dir!r}") + if not isinstance(cfg["skills_prefix"], str): + raise ConfigError("skills_prefix must be a string") if cfg["index_body_fn"] is not None and not callable(cfg["index_body_fn"]): raise ConfigError("index_body_fn must be None or callable") for fn in cfg["extra_checks"]: diff --git a/tests/test_checks.py b/tests/test_checks.py index c6b95e4..9374b07 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -718,7 +718,7 @@ def test_glossary_gate_off(self): class TestSkillsPairing(WikiTest): WORKFLOW = "---\ntype: workflow\n---\n\n# Document\n\nSteps.\n" WRAPPER = ( - "---\nname: document\ndescription: Document a thing.\n---\n\n" + "---\nname: wiki-document\ndescription: Document a thing.\n---\n\n" "Read `workflows/document.md` and follow it exactly.\n" ) @@ -727,6 +727,19 @@ def wiki(self, files=None): use_variant_with("generic", skills_dir=".claude/skills") return root + def test_unprefixed_wrapper_is_orphan(self): + # Field finding 2026-07-22: bare skill names collide with global + # skills, so pairing requires the configured prefix. + root = self.wiki(files={ + "workflows/document.md": self.WORKFLOW, + ".claude/skills/document/SKILL.md": self.WRAPPER, + }) + report = gather(root) + messages = [f[3] for f in findings(report, "skills", "ERROR")] + self.assertEqual(len(messages), 2) + self.assertTrue(any("no skill wrapper" in m for m in messages)) + self.assertTrue(any("orphan wrapper" in m for m in messages)) + def test_disabled_by_default(self): # "generic" now ships skills_dir (task 1: generic-variant wrappers), so # this exercises a variant that still leaves the knob unset. @@ -738,7 +751,7 @@ def test_disabled_by_default(self): def test_paired_wrapper_passes(self): root = self.wiki(files={ "workflows/document.md": self.WORKFLOW, - ".claude/skills/document/SKILL.md": self.WRAPPER, + ".claude/skills/wiki-document/SKILL.md": self.WRAPPER, }) report = gather(root) self.assertEqual(findings(report, "skills"), []) @@ -752,7 +765,7 @@ def test_workflow_without_wrapper_errors(self): def test_orphan_wrapper_errors(self): root = self.wiki(files={ - ".claude/skills/ghost/SKILL.md": self.WRAPPER, + ".claude/skills/wiki-ghost/SKILL.md": self.WRAPPER, }) report = gather(root) hits = findings(report, "skills", "ERROR") @@ -762,8 +775,8 @@ def test_orphan_wrapper_errors(self): def test_wrapper_must_reference_its_workflow(self): root = self.wiki(files={ "workflows/document.md": self.WORKFLOW, - ".claude/skills/document/SKILL.md": - "---\nname: document\ndescription: d\n---\n\nJust do it from memory.\n", + ".claude/skills/wiki-document/SKILL.md": + "---\nname: wiki-document\ndescription: d\n---\n\nJust do it from memory.\n", }) report = gather(root) hits = findings(report, "skills", "ERROR") diff --git a/tests/test_variants.py b/tests/test_variants.py index 9c208ad..388db16 100644 --- a/tests/test_variants.py +++ b/tests/test_variants.py @@ -31,9 +31,12 @@ def test_everything_compiles(self): # Knobs a variant may set differently from the others, each with a reason. # Anything not listed here must stay uniform across variants. INTENTIONAL_DIVERGENCES = { - # skills-dispatch prototype: homelab pairs workflows/ with .claude/skills/ - # wrappers; other variants adopt it only if the prototype graduates. + # skills-dispatch prototype: generic and homelab pair workflows/ with + # .claude/skills/ wrappers; other variants adopt it only if the prototype + # graduates. skills_prefix namespaces the wrappers (wiki-*) away from + # globally installed skills (field finding, 2026-07-22). "skills_dir", + "skills_prefix", } From df1ba422802df0821ece148c4e019111307cc33e Mon Sep 17 00:00:00 2001 From: SunsetDrifter Date: Wed, 22 Jul 2026 22:45:08 +0200 Subject: [PATCH 4/5] fix: self-contained check_types docstring (mirrors main) --- codebase-large/wikilint/checks.py | 3 +-- codebase/wikilint/checks.py | 3 +-- generic/wikilint/checks.py | 3 +-- homelab/wikilint/checks.py | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/codebase-large/wikilint/checks.py b/codebase-large/wikilint/checks.py index e0f3d1e..cfdec49 100644 --- a/codebase-large/wikilint/checks.py +++ b/codebase-large/wikilint/checks.py @@ -276,8 +276,7 @@ def check_tags(pages, report, root): def check_types(pages, report, root): """The type glossary: taxonomy.md's `## Page types` section must describe every schema type with a one-line meaning, so the bundle self-describes - its type vocabulary to foreign OKF consumers (docs/research - 2026-07-22 position note).""" + its type vocabulary to foreign OKF consumers.""" if CONFIG["taxonomy_file"] is None or not CONFIG["types_glossary"]: return taxonomy = load_taxonomy(root) diff --git a/codebase/wikilint/checks.py b/codebase/wikilint/checks.py index e0f3d1e..cfdec49 100644 --- a/codebase/wikilint/checks.py +++ b/codebase/wikilint/checks.py @@ -276,8 +276,7 @@ def check_tags(pages, report, root): def check_types(pages, report, root): """The type glossary: taxonomy.md's `## Page types` section must describe every schema type with a one-line meaning, so the bundle self-describes - its type vocabulary to foreign OKF consumers (docs/research - 2026-07-22 position note).""" + its type vocabulary to foreign OKF consumers.""" if CONFIG["taxonomy_file"] is None or not CONFIG["types_glossary"]: return taxonomy = load_taxonomy(root) diff --git a/generic/wikilint/checks.py b/generic/wikilint/checks.py index e0f3d1e..cfdec49 100644 --- a/generic/wikilint/checks.py +++ b/generic/wikilint/checks.py @@ -276,8 +276,7 @@ def check_tags(pages, report, root): def check_types(pages, report, root): """The type glossary: taxonomy.md's `## Page types` section must describe every schema type with a one-line meaning, so the bundle self-describes - its type vocabulary to foreign OKF consumers (docs/research - 2026-07-22 position note).""" + its type vocabulary to foreign OKF consumers.""" if CONFIG["taxonomy_file"] is None or not CONFIG["types_glossary"]: return taxonomy = load_taxonomy(root) diff --git a/homelab/wikilint/checks.py b/homelab/wikilint/checks.py index e0f3d1e..cfdec49 100644 --- a/homelab/wikilint/checks.py +++ b/homelab/wikilint/checks.py @@ -276,8 +276,7 @@ def check_tags(pages, report, root): def check_types(pages, report, root): """The type glossary: taxonomy.md's `## Page types` section must describe every schema type with a one-line meaning, so the bundle self-describes - its type vocabulary to foreign OKF consumers (docs/research - 2026-07-22 position note).""" + its type vocabulary to foreign OKF consumers.""" if CONFIG["taxonomy_file"] is None or not CONFIG["types_glossary"]: return taxonomy = load_taxonomy(root) From 4e6c1458609027ff6cbd0898739e0ade5710ba7d Mon Sep 17 00:00:00 2001 From: SunsetDrifter Date: Wed, 22 Jul 2026 22:56:56 +0200 Subject: [PATCH 5/5] feat: graduate skills-dispatch to all variants Wrappers for codebase (10) and codebase-large (11), parity allowlist dropped now that all four variants ship skills_dir/skills_prefix, README documents the wrapper convention. --- README.md | 1 + codebase-large/.claude/skills/wiki-adr/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-document/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-lint/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-maintain/SKILL.md | 6 ++++++ .../.claude/skills/wiki-owner-review/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-postmortem/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-query/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-reconcile/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-sync/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-triage/SKILL.md | 6 ++++++ codebase-large/.claude/skills/wiki-verify/SKILL.md | 6 ++++++ codebase-large/CLAUDE.md | 2 ++ codebase-large/lint.py | 5 ++++- codebase/.claude/skills/wiki-adr/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-document/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-lint/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-maintain/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-postmortem/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-query/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-reconcile/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-sync/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-triage/SKILL.md | 6 ++++++ codebase/.claude/skills/wiki-verify/SKILL.md | 6 ++++++ codebase/CLAUDE.md | 2 ++ codebase/lint.py | 5 ++++- tests/test_checks.py | 10 +++++----- tests/test_variants.py | 9 +-------- 28 files changed, 145 insertions(+), 15 deletions(-) create mode 100644 codebase-large/.claude/skills/wiki-adr/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-document/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-lint/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-maintain/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-owner-review/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-postmortem/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-query/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-reconcile/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-sync/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-triage/SKILL.md create mode 100644 codebase-large/.claude/skills/wiki-verify/SKILL.md create mode 100644 codebase/.claude/skills/wiki-adr/SKILL.md create mode 100644 codebase/.claude/skills/wiki-document/SKILL.md create mode 100644 codebase/.claude/skills/wiki-lint/SKILL.md create mode 100644 codebase/.claude/skills/wiki-maintain/SKILL.md create mode 100644 codebase/.claude/skills/wiki-postmortem/SKILL.md create mode 100644 codebase/.claude/skills/wiki-query/SKILL.md create mode 100644 codebase/.claude/skills/wiki-reconcile/SKILL.md create mode 100644 codebase/.claude/skills/wiki-sync/SKILL.md create mode 100644 codebase/.claude/skills/wiki-triage/SKILL.md create mode 100644 codebase/.claude/skills/wiki-verify/SKILL.md diff --git a/README.md b/README.md index 3da7c3b..f1d544f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ The schemas follow a few rules, informed by how agent-maintained wikis actually - **Hot/cold split.** `CLAUDE.md` stays around a hundred lines (directives, layout, frontmatter schemas, a dispatch table). Step-by-step workflows live in `workflows/` and are read when triggered. Always-loaded context is a cost paid on every turn; verbose context files measurably hurt. - **If a rule can be a script, it is a script.** `lint.py check` covers frontmatter validation, broken links, orphans, dangling references, staleness, secrets, ADR integrity, index drift, log format, and OKF conformance. The LLM lint workflow keeps only what needs judgment: contradictions, stale claims, concept gaps. - **Every relationship is stored once.** Pages declare their forward edges (`depends_on`, `defined_in`, `consumes`, `producers`, `consumers`, ...); `lint.py reverse-deps` derives the reverse map of every one of them. Agents reliably fail to keep hand-maintained symmetric fields consistent, so the schemas don't ask them to. +- **Workflows double as skills.** Each workflow has a thin `.claude/skills/wiki-/SKILL.md` wrapper, so `/wiki-triage`, `/wiki-document`, etc. dispatch deterministically in Claude Code (the prefix avoids collisions with globally installed skills). Wrappers contain no procedure text, only a pointer; `check_skills` makes wrapper/workflow drift a lint error. - **The index is a derived artifact.** Every page carries a one-line `description:`; `lint.py rebuild-index` generates `index.md` from frontmatter. Agents scan frontmatter first and open page bodies only on a hit. - **Trust is structural, not remembered.** Immutable `raw/`, append-only `log.md`, a git commit per operation, a pre-commit hook that lints the staged snapshot (exactly the bytes that will land) and makes lint errors uncommittable, and commit-pinned verification fields. Agent-maintained judgment metadata decays, so `confidence:` is reduced to the two states a lint can actually check (`low` = uncited, `contested` = sources disagree and the body must explain), inferred claims are marked inline with `(inferred)`, and tags are validated against a `taxonomy.md`, whose `## Page types` section also describes every allowed page type with a one-line meaning, so each bundle self-describes its type vocabulary to OKF consumers. - **Contested is a state to exit.** The documented failure mode of agent wikis is contradictions accumulating faster than they resolve. Lint flags contested pages older than 30 days; the reconcile workflow rewrites in place, moving losing claims to a dated "Superseded claims" section instead of deleting them. diff --git a/codebase-large/.claude/skills/wiki-adr/SKILL.md b/codebase-large/.claude/skills/wiki-adr/SKILL.md new file mode 100644 index 0000000..8601945 --- /dev/null +++ b/codebase-large/.claude/skills/wiki-adr/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-adr +description: Draft an architecture decision record. Use when the human says "draft an ADR for ". +--- + +Read `workflows/adr.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-document/SKILL.md b/codebase-large/.claude/skills/wiki-document/SKILL.md new file mode 100644 index 0000000..b0a6934 --- /dev/null +++ b/codebase-large/.claude/skills/wiki-document/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-document +description: Document code in the wiki: module, service, api, or data-model pages. Use when the human says "document " or "document the service". +--- + +Read `workflows/document.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-lint/SKILL.md b/codebase-large/.claude/skills/wiki-lint/SKILL.md new file mode 100644 index 0000000..0f3dbd6 --- /dev/null +++ b/codebase-large/.claude/skills/wiki-lint/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-lint +description: Run the deterministic lint plus the judgment-only review pass. Use when the human says "lint the wiki". +--- + +Read `workflows/lint.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-maintain/SKILL.md b/codebase-large/.claude/skills/wiki-maintain/SKILL.md new file mode 100644 index 0000000..5aad0f5 --- /dev/null +++ b/codebase-large/.claude/skills/wiki-maintain/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-maintain +description: Run or review the unattended maintenance pass on the maintenance branch. Use when the human says "maintenance pass" or "review maintenance". +--- + +Read `workflows/maintain.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-owner-review/SKILL.md b/codebase-large/.claude/skills/wiki-owner-review/SKILL.md new file mode 100644 index 0000000..eee4497 --- /dev/null +++ b/codebase-large/.claude/skills/wiki-owner-review/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-owner-review +description: Run the owner-review staleness pass for a subsystem. Use when the human says "owner review for ". +--- + +Read `workflows/owner-review.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-postmortem/SKILL.md b/codebase-large/.claude/skills/wiki-postmortem/SKILL.md new file mode 100644 index 0000000..ad6546a --- /dev/null +++ b/codebase-large/.claude/skills/wiki-postmortem/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-postmortem +description: File a postmortem page. Use when the human says "log postmortem: ". +--- + +Read `workflows/postmortem.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-query/SKILL.md b/codebase-large/.claude/skills/wiki-query/SKILL.md new file mode 100644 index 0000000..8f65d58 --- /dev/null +++ b/codebase-large/.claude/skills/wiki-query/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-query +description: Answer a question from the wiki or the code, scanning frontmatter and the index before opening page bodies. Use for any question answerable from the wiki. +--- + +Read `workflows/query.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-reconcile/SKILL.md b/codebase-large/.claude/skills/wiki-reconcile/SKILL.md new file mode 100644 index 0000000..d35a638 --- /dev/null +++ b/codebase-large/.claude/skills/wiki-reconcile/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-reconcile +description: Resolve a contested page: weigh the disagreeing evidence and rewrite latest-evidence-first. Use when the human says "reconcile ". +--- + +Read `workflows/reconcile.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-sync/SKILL.md b/codebase-large/.claude/skills/wiki-sync/SKILL.md new file mode 100644 index 0000000..e33abca --- /dev/null +++ b/codebase-large/.claude/skills/wiki-sync/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-sync +description: Sync the wiki to the current state of the code repository. Use when the human says "sync to current". +--- + +Read `workflows/sync.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-triage/SKILL.md b/codebase-large/.claude/skills/wiki-triage/SKILL.md new file mode 100644 index 0000000..499cf3c --- /dev/null +++ b/codebase-large/.claude/skills/wiki-triage/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-triage +description: Triage the raw inbox of this wiki. Use when the human says "triage the inbox" or drops files into raw/inbox/ and asks to sort them. +--- + +Read `workflows/triage.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/.claude/skills/wiki-verify/SKILL.md b/codebase-large/.claude/skills/wiki-verify/SKILL.md new file mode 100644 index 0000000..816b3e2 --- /dev/null +++ b/codebase-large/.claude/skills/wiki-verify/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-verify +description: Reality-check wiki pages against the code. Use when the human says "verify modules" or "verify ". +--- + +Read `workflows/verify.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase-large/CLAUDE.md b/codebase-large/CLAUDE.md index e1f28c4..f198a52 100644 --- a/codebase-large/CLAUDE.md +++ b/codebase-large/CLAUDE.md @@ -125,6 +125,8 @@ When the human triggers an operation, read the matching file and follow it exact | "reconcile " | `workflows/reconcile.md` | | "maintenance pass" / "review maintenance" | `workflows/maintain.md` | +In Claude Code each workflow is also a skill wrapper under `.claude/skills/`, so `/wiki-document`, `/wiki-triage`, etc. dispatch the same files deterministically (the wiki- prefix avoids collisions with globally installed skills); lint errors when a wrapper and its workflow drift apart. + ## Deterministic checks `python3 lint.py check` handles every mechanical health check: frontmatter validity, broken links, orphans (with unlinked-mention hints), dangling references, tag taxonomy, stale contested pages, criticality-weighted sync drift, owner-review staleness, mermaid presence and node counts, ADR numbering in global and subsystem directories, inbox, secrets, index drift, log format, OKF conformance. Run it instead of checking by hand; fix errors before finishing any operation. A pre-commit hook (installed via `git config core.hooksPath .githooks`) lints the staged snapshot and makes errors uncommittable; never bypass it with `--no-verify`. diff --git a/codebase-large/lint.py b/codebase-large/lint.py index 0069fce..95a668c 100644 --- a/codebase-large/lint.py +++ b/codebase-large/lint.py @@ -16,8 +16,11 @@ # Top-level entries that are allowed to exist but are not wiki pages. "non_page_allowed": [ "CLAUDE.md", "index.md", "log.md", "coverage.md", "glossary.md", - "lint.py", "wikilint", "taxonomy.md", "raw", "workflows", ".git", ".gitignore", ".githooks", + "lint.py", "wikilint", "taxonomy.md", "raw", "workflows", ".git", ".gitignore", ".githooks", ".claude", ], + # Claude Code skill wrappers pair 1:1 with workflows/ (check_skills). + "skills_dir": ".claude/skills", + "skills_prefix": "wiki-", "raw_dir": "raw", "inbox_dir": "raw/inbox", "inbox_warn_count": 10, diff --git a/codebase/.claude/skills/wiki-adr/SKILL.md b/codebase/.claude/skills/wiki-adr/SKILL.md new file mode 100644 index 0000000..8601945 --- /dev/null +++ b/codebase/.claude/skills/wiki-adr/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-adr +description: Draft an architecture decision record. Use when the human says "draft an ADR for ". +--- + +Read `workflows/adr.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-document/SKILL.md b/codebase/.claude/skills/wiki-document/SKILL.md new file mode 100644 index 0000000..b0a6934 --- /dev/null +++ b/codebase/.claude/skills/wiki-document/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-document +description: Document code in the wiki: module, service, api, or data-model pages. Use when the human says "document " or "document the service". +--- + +Read `workflows/document.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-lint/SKILL.md b/codebase/.claude/skills/wiki-lint/SKILL.md new file mode 100644 index 0000000..0f3dbd6 --- /dev/null +++ b/codebase/.claude/skills/wiki-lint/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-lint +description: Run the deterministic lint plus the judgment-only review pass. Use when the human says "lint the wiki". +--- + +Read `workflows/lint.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-maintain/SKILL.md b/codebase/.claude/skills/wiki-maintain/SKILL.md new file mode 100644 index 0000000..5aad0f5 --- /dev/null +++ b/codebase/.claude/skills/wiki-maintain/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-maintain +description: Run or review the unattended maintenance pass on the maintenance branch. Use when the human says "maintenance pass" or "review maintenance". +--- + +Read `workflows/maintain.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-postmortem/SKILL.md b/codebase/.claude/skills/wiki-postmortem/SKILL.md new file mode 100644 index 0000000..ad6546a --- /dev/null +++ b/codebase/.claude/skills/wiki-postmortem/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-postmortem +description: File a postmortem page. Use when the human says "log postmortem: ". +--- + +Read `workflows/postmortem.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-query/SKILL.md b/codebase/.claude/skills/wiki-query/SKILL.md new file mode 100644 index 0000000..8f65d58 --- /dev/null +++ b/codebase/.claude/skills/wiki-query/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-query +description: Answer a question from the wiki or the code, scanning frontmatter and the index before opening page bodies. Use for any question answerable from the wiki. +--- + +Read `workflows/query.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-reconcile/SKILL.md b/codebase/.claude/skills/wiki-reconcile/SKILL.md new file mode 100644 index 0000000..d35a638 --- /dev/null +++ b/codebase/.claude/skills/wiki-reconcile/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-reconcile +description: Resolve a contested page: weigh the disagreeing evidence and rewrite latest-evidence-first. Use when the human says "reconcile ". +--- + +Read `workflows/reconcile.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-sync/SKILL.md b/codebase/.claude/skills/wiki-sync/SKILL.md new file mode 100644 index 0000000..e33abca --- /dev/null +++ b/codebase/.claude/skills/wiki-sync/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-sync +description: Sync the wiki to the current state of the code repository. Use when the human says "sync to current". +--- + +Read `workflows/sync.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-triage/SKILL.md b/codebase/.claude/skills/wiki-triage/SKILL.md new file mode 100644 index 0000000..499cf3c --- /dev/null +++ b/codebase/.claude/skills/wiki-triage/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-triage +description: Triage the raw inbox of this wiki. Use when the human says "triage the inbox" or drops files into raw/inbox/ and asks to sort them. +--- + +Read `workflows/triage.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/.claude/skills/wiki-verify/SKILL.md b/codebase/.claude/skills/wiki-verify/SKILL.md new file mode 100644 index 0000000..816b3e2 --- /dev/null +++ b/codebase/.claude/skills/wiki-verify/SKILL.md @@ -0,0 +1,6 @@ +--- +name: wiki-verify +description: Reality-check wiki pages against the code. Use when the human says "verify modules" or "verify ". +--- + +Read `workflows/verify.md` from the wiki root and follow it exactly, step by step. Do not run the procedure from memory; the workflow file is the source of truth and may have changed since you last read it. `python3 lint.py check` verifies this wrapper stays paired with its workflow. diff --git a/codebase/CLAUDE.md b/codebase/CLAUDE.md index ccf0e9d..8e3321f 100644 --- a/codebase/CLAUDE.md +++ b/codebase/CLAUDE.md @@ -100,6 +100,8 @@ When the human triggers an operation, read the matching file and follow it exact | "reconcile " | `workflows/reconcile.md` | | "maintenance pass" / "review maintenance" | `workflows/maintain.md` | +In Claude Code each workflow is also a skill wrapper under `.claude/skills/`, so `/wiki-document`, `/wiki-triage`, etc. dispatch the same files deterministically (the wiki- prefix avoids collisions with globally installed skills); lint errors when a wrapper and its workflow drift apart. + ## Deterministic checks `python3 lint.py check` handles every mechanical health check: frontmatter validity, broken links, orphans (with unlinked-mention hints), dangling references (including `defined_in` and `includes`), architecture membership of active modules (prime directive 3), tag taxonomy, stale contested pages, sync drift against the pinning block, missing Mermaid diagrams on architecture pages, ADR numbering gaps and superseded ADRs without forward links, inbox health, secrets, index drift, log format, OKF conformance. Run it instead of checking these by hand, and fix errors it reports before finishing any operation. A pre-commit hook (installed via `git config core.hooksPath .githooks`) lints the staged snapshot and makes errors uncommittable; never bypass it with `--no-verify`. diff --git a/codebase/lint.py b/codebase/lint.py index 6e8f01e..ed26a13 100644 --- a/codebase/lint.py +++ b/codebase/lint.py @@ -16,8 +16,11 @@ # Top-level entries that are allowed to exist but are not wiki pages. "non_page_allowed": [ "CLAUDE.md", "index.md", "log.md", "lint.py", "wikilint", "taxonomy.md", "raw", "workflows", - ".git", ".gitignore", ".githooks", + ".git", ".gitignore", ".githooks", ".claude", ], + # Claude Code skill wrappers pair 1:1 with workflows/ (check_skills). + "skills_dir": ".claude/skills", + "skills_prefix": "wiki-", "raw_dir": "raw", "inbox_dir": "raw/inbox", "inbox_warn_count": 10, diff --git a/tests/test_checks.py b/tests/test_checks.py index 9374b07..596ed81 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -740,11 +740,11 @@ def test_unprefixed_wrapper_is_orphan(self): self.assertTrue(any("no skill wrapper" in m for m in messages)) self.assertTrue(any("orphan wrapper" in m for m in messages)) - def test_disabled_by_default(self): - # "generic" now ships skills_dir (task 1: generic-variant wrappers), so - # this exercises a variant that still leaves the knob unset. - root = make_wiki(self.tmp, variant="codebase", - files={"workflows/document.md": self.WORKFLOW}) + def test_disabled_when_knob_unset(self): + # Every variant now ships skills_dir, so exercise the engine default + # (None) by overriding it off: no pairing checks run. + root = make_wiki(self.tmp, files={"workflows/document.md": self.WORKFLOW}) + use_variant_with("generic", skills_dir=None) report = gather(root) self.assertEqual(findings(report, "skills"), []) diff --git a/tests/test_variants.py b/tests/test_variants.py index 388db16..2af8caf 100644 --- a/tests/test_variants.py +++ b/tests/test_variants.py @@ -30,14 +30,7 @@ def test_everything_compiles(self): # Knobs a variant may set differently from the others, each with a reason. # Anything not listed here must stay uniform across variants. -INTENTIONAL_DIVERGENCES = { - # skills-dispatch prototype: generic and homelab pair workflows/ with - # .claude/skills/ wrappers; other variants adopt it only if the prototype - # graduates. skills_prefix namespaces the wrappers (wiki-*) away from - # globally installed skills (field finding, 2026-07-22). - "skills_dir", - "skills_prefix", -} +INTENTIONAL_DIVERGENCES = set() class TestConfigSanity(unittest.TestCase):