@ikon85/agent-workflow-kit 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/ask-matt/SKILL.md +139 -0
- package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/audit-skills/SKILL.md +145 -0
- package/.agents/skills/board-to-waves/SKILL.md +135 -0
- package/.agents/skills/code-review/SKILL.md +64 -0
- package/.agents/skills/codebase-design/DEEPENING.md +37 -0
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.agents/skills/codebase-design/SKILL.md +114 -0
- package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
- package/.agents/skills/decision-gate/SKILL.md +67 -0
- package/.agents/skills/diagnose/SKILL.md +136 -0
- package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +76 -0
- package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
- package/.agents/skills/grill-me/SKILL.md +54 -0
- package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/grill-with-docs/SKILL.md +137 -0
- package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/implement/SKILL.md +15 -0
- package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/kit-release/SKILL.md +56 -0
- package/.agents/skills/local-ci/SKILL.md +94 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
- package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.agents/skills/prototype/LOGIC.md +79 -0
- package/.agents/skills/prototype/SKILL.md +30 -0
- package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.agents/skills/research/SKILL.md +12 -0
- package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/retro/SKILL.md +209 -0
- package/.agents/skills/scale-check/SKILL.md +141 -0
- package/.agents/skills/security-audit/SKILL.md +67 -0
- package/.agents/skills/setup-workflow/SKILL.md +187 -0
- package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/setup-workflow/board-sync.md +111 -0
- package/.agents/skills/setup-workflow/code-review.md +26 -0
- package/.agents/skills/setup-workflow/domain.md +51 -0
- package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.agents/skills/setup-workflow/triage-labels.md +18 -0
- package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
- package/.agents/skills/spec-self-critique/SKILL.md +152 -0
- package/.agents/skills/spec-self-critique/scenarios.md +82 -0
- package/.agents/skills/tdd/SKILL.md +120 -0
- package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/tdd/deep-modules.md +33 -0
- package/.agents/skills/tdd/interface-design.md +31 -0
- package/.agents/skills/tdd/mocking.md +59 -0
- package/.agents/skills/tdd/refactoring.md +10 -0
- package/.agents/skills/tdd/tests.md +61 -0
- package/.agents/skills/to-issues/SKILL.md +325 -0
- package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.agents/skills/to-prd/SKILL.md +180 -0
- package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-waves/SKILL.md +260 -0
- package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
- package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.agents/skills/triage/SKILL.md +122 -0
- package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/verify-spike/SKILL.md +39 -0
- package/.agents/skills/wayfinder/SKILL.md +127 -0
- package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/wrapup/SKILL.md +71 -0
- package/.agents/skills/wrapup/SOURCES.txt +6 -0
- package/.agents/skills/zoom-out/SKILL.md +7 -0
- package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/hooks/_hook_utils.py +184 -0
- package/.claude/hooks/drift-guard.py +176 -0
- package/.claude/hooks/skill-drift-hint.py +122 -0
- package/.claude/hooks/sync-board-status.py +191 -0
- package/.claude/skills/ask-matt/SKILL.md +139 -0
- package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/audit-skills/SKILL.md +145 -0
- package/.claude/skills/board-to-waves/SKILL.md +148 -0
- package/.claude/skills/code-review/SKILL.md +64 -0
- package/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.claude/skills/codebase-design/SKILL.md +114 -0
- package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/codex-build/SKILL.md +126 -0
- package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
- package/.claude/skills/codex-review/SKILL.md +145 -0
- package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
- package/.claude/skills/decision-gate/SKILL.md +67 -0
- package/.claude/skills/diagnose/SKILL.md +136 -0
- package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/domain-modeling/SKILL.md +76 -0
- package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
- package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
- package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
- package/.claude/skills/grill-me/SKILL.md +54 -0
- package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-me-codex/SKILL.md +158 -0
- package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
- package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs/SKILL.md +137 -0
- package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
- package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
- package/.claude/skills/implement/SKILL.md +15 -0
- package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/kit-release/SKILL.md +56 -0
- package/.claude/skills/local-ci/SKILL.md +94 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
- package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.claude/skills/prototype/LOGIC.md +79 -0
- package/.claude/skills/prototype/SKILL.md +30 -0
- package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/prototype/UI.md +112 -0
- package/.claude/skills/research/SKILL.md +12 -0
- package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/retro/SKILL.md +209 -0
- package/.claude/skills/scale-check/SKILL.md +141 -0
- package/.claude/skills/security-audit/SKILL.md +67 -0
- package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
- package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
- package/.claude/skills/setup-workflow/SKILL.md +187 -0
- package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/setup-workflow/board-sync.md +111 -0
- package/.claude/skills/setup-workflow/code-review.md +26 -0
- package/.claude/skills/setup-workflow/domain.md +51 -0
- package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.claude/skills/setup-workflow/triage-labels.md +18 -0
- package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
- package/.claude/skills/skill-manifest.json +389 -0
- package/.claude/skills/spec-self-critique/SKILL.md +152 -0
- package/.claude/skills/spec-self-critique/scenarios.md +82 -0
- package/.claude/skills/tdd/SKILL.md +120 -0
- package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/tdd/deep-modules.md +33 -0
- package/.claude/skills/tdd/interface-design.md +31 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/refactoring.md +10 -0
- package/.claude/skills/tdd/tests.md +61 -0
- package/.claude/skills/to-issues/SKILL.md +325 -0
- package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.claude/skills/to-prd/SKILL.md +180 -0
- package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-waves/SKILL.md +260 -0
- package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
- package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.claude/skills/triage/SKILL.md +122 -0
- package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/verify-spike/SKILL.md +39 -0
- package/.claude/skills/wayfinder/SKILL.md +127 -0
- package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/wrapup/SKILL.md +81 -0
- package/.claude/skills/wrapup/SOURCES.txt +6 -0
- package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
- package/.claude/skills/write-a-skill/SKILL.md +88 -0
- package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/zoom-out/SKILL.md +7 -0
- package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/LICENSE +28 -0
- package/PROVENANCE.md +39 -0
- package/README.md +619 -0
- package/agent-workflow-kit.package.json +1939 -0
- package/docs/agents/board-sync.md +71 -0
- package/docs/agents/code-review.md +14 -0
- package/docs/agents/domain.md +7 -0
- package/docs/agents/issue-tracker.md +12 -0
- package/docs/agents/security-audit-runbook-template.md +205 -0
- package/docs/agents/skills/orchestrate-wave.md +13 -0
- package/docs/agents/skills/spec-self-critique.md +18 -0
- package/docs/agents/triage-labels.md +10 -0
- package/docs/agents/wave-anchor-template.md +88 -0
- package/docs/conventions/spec-completeness.md +23 -0
- package/docs/index.html +56 -0
- package/docs/methodology.html +488 -0
- package/docs/methodology.svg +93 -0
- package/docs/workflow.png +0 -0
- package/package.json +40 -0
- package/scripts/anchor_table.py +343 -0
- package/scripts/board-sync.py +1232 -0
- package/scripts/board_config.py +147 -0
- package/scripts/board_fields.py +192 -0
- package/scripts/build-kit.mjs +116 -0
- package/scripts/build-kit.test.mjs +59 -0
- package/scripts/check-kit-staleness.mjs +45 -0
- package/scripts/check-kit-staleness.test.mjs +19 -0
- package/scripts/execute-ready-check.py +516 -0
- package/scripts/issue_deps.py +79 -0
- package/scripts/kit-release.mjs +100 -0
- package/scripts/kit-release.test.mjs +67 -0
- package/scripts/lib/audit-refs.mjs +146 -0
- package/scripts/lib/scrub.mjs +113 -0
- package/scripts/lib/scrub.test.mjs +187 -0
- package/scripts/loc_offender_core.py +196 -0
- package/scripts/loc_offender_gate.py +182 -0
- package/scripts/node_kind.py +68 -0
- package/scripts/portability_profile_scan.py +53 -0
- package/scripts/pr-body-check.py +263 -0
- package/scripts/program_graph.py +125 -0
- package/scripts/program_graph_parse.py +275 -0
- package/scripts/program_graph_validate.py +256 -0
- package/scripts/program_sync.py +160 -0
- package/scripts/release-delta-guard.mjs +96 -0
- package/scripts/release-delta-guard.test.mjs +53 -0
- package/scripts/test_board_sync_create_idempotency.py +104 -0
- package/scripts/test_dist_kit_smoke.py +199 -0
- package/scripts/test_kit_docs_language_census.py +178 -0
- package/scripts/test_skill_code_review_seed.py +89 -0
- package/scripts/test_skill_frontmatter_lint.py +226 -0
- package/scripts/test_skill_gh_lint.py +124 -0
- package/scripts/test_skill_language_census.py +254 -0
- package/scripts/test_skill_portability_lint.py +773 -0
- package/scripts/test_skill_publish_audit.py +300 -0
- package/scripts/test_skill_selfcontainment_lint.py +255 -0
- package/scripts/test_skill_setup_workflow_seeds.py +200 -0
- package/scripts/test_skill_stale_name_lint.py +126 -0
- package/scripts/test_skill_surface_refs.py +300 -0
- package/scripts/test_skill_trailing_artifact_lint.py +71 -0
- package/scripts/wrapup-land.py +573 -0
- package/src/cli.mjs +70 -0
- package/src/commands/diff.mjs +6 -0
- package/src/commands/init.mjs +59 -0
- package/src/commands/uninstall.mjs +43 -0
- package/src/commands/update.mjs +95 -0
- package/src/lib/atomicWrite.mjs +49 -0
- package/src/lib/bundle.mjs +133 -0
- package/src/lib/hash.mjs +13 -0
- package/src/lib/manifest.mjs +58 -0
- package/src/lib/sentinel.mjs +20 -0
- package/src/lib/settings.mjs +23 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""setup-workflow seed + idempotency-rule spec (Welle 26 / Slice 5 / #989).
|
|
3
|
+
|
|
4
|
+
`/setup-workflow` is a prompt-driven skill, so its interactive run is verified
|
|
5
|
+
live (mock fresh-fill + in-repo idempotent re-run). This file pins the two parts
|
|
6
|
+
that ARE machine-checkable:
|
|
7
|
+
|
|
8
|
+
1. The seed templates it ships are structurally valid for their consumers, so a
|
|
9
|
+
fresh fill cannot produce a project layer that makes a downstream skill warn.
|
|
10
|
+
2. The sentinel-based idempotency rule, as a reference implementation + the
|
|
11
|
+
canonical example table the skill's prose must satisfy (Codex R1 #12 strict
|
|
12
|
+
`state=` header; R2 #2 terminal `not-applicable`; R3 legacy no-sentinel skip).
|
|
13
|
+
|
|
14
|
+
It does NOT test Claude's execution of the prose — that is the live-verify.
|
|
15
|
+
|
|
16
|
+
Run: python3 scripts/test_skill_setup_workflow_seeds.py
|
|
17
|
+
"""
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import unittest
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
24
|
+
SKILL = REPO / ".claude/skills/setup-workflow"
|
|
25
|
+
|
|
26
|
+
SENTINEL_RE = re.compile(
|
|
27
|
+
r"^<!--\s*setup-workflow:\s*state=(stub|filled|not-applicable)"
|
|
28
|
+
r"(?:;\s*mode=(github-projects-v2|none))?\s*-->\s*$"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---- Reference implementation of the documented idempotency rule -------------
|
|
33
|
+
def classify(first_line, is_empty):
|
|
34
|
+
"""Return the action the skill must take for one target file.
|
|
35
|
+
|
|
36
|
+
first_line: the file's first line, or None if the file does not exist.
|
|
37
|
+
is_empty: True if the file is empty/whitespace-only.
|
|
38
|
+
"""
|
|
39
|
+
if first_line is None:
|
|
40
|
+
return "create"
|
|
41
|
+
m = SENTINEL_RE.match(first_line.strip())
|
|
42
|
+
if m:
|
|
43
|
+
return "fill" if m.group(1) == "stub" else "skip"
|
|
44
|
+
# No sentinel: non-empty legacy file is left alone; empty is fillable.
|
|
45
|
+
return "create" if is_empty else "skip"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class IdempotencyRule(unittest.TestCase):
|
|
49
|
+
CASES = [
|
|
50
|
+
# (first_line, is_empty, expected)
|
|
51
|
+
(None, False, "create"), # missing
|
|
52
|
+
("<!-- setup-workflow: state=stub -->", False, "fill"), # stub
|
|
53
|
+
("<!-- setup-workflow: state=filled -->", False, "skip"), # filled
|
|
54
|
+
("<!-- setup-workflow: state=stub; mode=github-projects-v2 -->", False, "fill"),
|
|
55
|
+
("<!-- setup-workflow: state=filled; mode=github-projects-v2 -->", False, "skip"),
|
|
56
|
+
("<!-- setup-workflow: state=not-applicable; mode=none -->", False, "skip"),
|
|
57
|
+
("# Board sync — GitHub Projects field-IDs", False, "skip"), # legacy non-empty
|
|
58
|
+
("", True, "create"), # empty no-sentinel
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def test_canonical_examples(self):
|
|
62
|
+
for first, empty, expected in self.CASES:
|
|
63
|
+
self.assertEqual(
|
|
64
|
+
classify(first, empty), expected, f"{first!r} empty={empty}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def test_filled_file_mentioning_marker_in_body_is_not_refilled(self):
|
|
68
|
+
# Only the FIRST line decides; a body mention must not flip it (Codex R1 #12).
|
|
69
|
+
first = "<!-- setup-workflow: state=filled -->"
|
|
70
|
+
self.assertEqual(classify(first, False), "skip")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SeedTemplatesValid(unittest.TestCase):
|
|
74
|
+
def test_spec_completeness_seed_has_valid_self_critique_block(self):
|
|
75
|
+
"""A convention without a valid Trigger/Check/Korrektur block makes
|
|
76
|
+
spec-self-critique point 8 warn — the seed must carry one (Codex R1 #14)."""
|
|
77
|
+
t = (SKILL / "spec-completeness-seed.md").read_text(encoding="utf-8")
|
|
78
|
+
self.assertIn("## Self-Critique-Check", t)
|
|
79
|
+
for kw in ("Trigger", "Check", "Korrektur"):
|
|
80
|
+
self.assertRegex(t, rf"\*\*{kw}", f"missing {kw} in Self-Critique-Check")
|
|
81
|
+
|
|
82
|
+
def test_spec_self_critique_seed_has_12_point_anchors(self):
|
|
83
|
+
"""Stable retro-append anchors for points 1..12 (Codex R1 #15)."""
|
|
84
|
+
t = (SKILL / "spec-self-critique-seed.md").read_text(encoding="utf-8")
|
|
85
|
+
anchors = sorted(int(n) for n in re.findall(r"^## Point (\d+)\b", t, re.M))
|
|
86
|
+
self.assertEqual(anchors, list(range(1, 13)))
|
|
87
|
+
|
|
88
|
+
def test_orchestrate_wave_seed_has_section_contract(self):
|
|
89
|
+
"""The orchestrate-wave project-layer seed carries the exact §-section
|
|
90
|
+
headings the skill's Phase-0 probe matches (#1958). If a heading is
|
|
91
|
+
renamed here without updating the skeleton (or vice versa) the probe
|
|
92
|
+
stops finding the section — this pins the contract on both sides."""
|
|
93
|
+
seed = (SKILL / "orchestrate-wave-seed.md").read_text(encoding="utf-8")
|
|
94
|
+
skeleton = (REPO / ".claude/skills/orchestrate-wave/SKILL.md").read_text(encoding="utf-8")
|
|
95
|
+
sections = [
|
|
96
|
+
"§Setup", "§Builder Commands", "§Builder Hard Rules",
|
|
97
|
+
"§Integration Suites", "§Verify Recipe", "§Headless Login", "§Landing",
|
|
98
|
+
]
|
|
99
|
+
for sec in sections:
|
|
100
|
+
self.assertRegex(seed, rf"(?m)^#+ {re.escape(sec)}\b", f"seed missing heading {sec!r}")
|
|
101
|
+
self.assertIn(sec, skeleton, f"skeleton no longer refers to {sec!r}")
|
|
102
|
+
|
|
103
|
+
def test_board_sync_seed_documents_states_and_profile(self):
|
|
104
|
+
t = (SKILL / "board-sync.md").read_text(encoding="utf-8")
|
|
105
|
+
for token in ("state=filled", "state=stub", "state=not-applicable", "Status"):
|
|
106
|
+
self.assertIn(token, t)
|
|
107
|
+
|
|
108
|
+
def test_workflow_overview_seed_is_generic_and_linked_from_skill(self):
|
|
109
|
+
"""Fresh repos should get an entry-point map without Testreporter-local lore.
|
|
110
|
+
|
|
111
|
+
The expected skill set is derived from the manifest's `entryPoint` flag
|
|
112
|
+
(publish:true skills only) rather than a hand-maintained token list —
|
|
113
|
+
it used to hardcode 5 names while the manifest already carried 13
|
|
114
|
+
publish:true entry-point-worthy skills, so the seed silently drifted
|
|
115
|
+
behind (#1890: `ask-matt`, the router, was missing entirely)."""
|
|
116
|
+
manifest = json.loads((REPO / ".claude/skills/skill-manifest.json").read_text(encoding="utf-8"))
|
|
117
|
+
expected = sorted(
|
|
118
|
+
name for name, meta in manifest["skills"].items()
|
|
119
|
+
if meta.get("publish") and meta.get("entryPoint")
|
|
120
|
+
)
|
|
121
|
+
self.assertGreaterEqual(len(expected), 10, "manifest entryPoint set looks too small")
|
|
122
|
+
|
|
123
|
+
seed = (SKILL / "workflow-overview.md").read_text(encoding="utf-8")
|
|
124
|
+
skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
|
|
125
|
+
|
|
126
|
+
self.assertIn("# Workflow", seed)
|
|
127
|
+
for name in expected:
|
|
128
|
+
self.assertIn(name, seed, f"entry-point skill {name!r} (manifest entryPoint:true) missing from seed")
|
|
129
|
+
self.assertIn("ask-matt", seed, "router entry must be named explicitly")
|
|
130
|
+
self.assertNotIn("Testreporter", seed)
|
|
131
|
+
self.assertIn("workflow-overview.md", skill)
|
|
132
|
+
self.assertIn("## Workflow", skill)
|
|
133
|
+
|
|
134
|
+
def test_triage_labels_seed_matches_two_label_reality(self):
|
|
135
|
+
"""The seed presented at setup-workflow fill-time must not contradict
|
|
136
|
+
the triage skill's actual label vocabulary: it used to list five
|
|
137
|
+
canonical roles as if all were active labels, but the triage skill
|
|
138
|
+
only ever applies two — the rest live in board status. A fresh fill
|
|
139
|
+
from the old seed taught agents to assign three dead labels (#1879)."""
|
|
140
|
+
seed = (SKILL / "triage-labels.md").read_text(encoding="utf-8")
|
|
141
|
+
triage_skill = (REPO / ".claude/skills/triage/SKILL.md").read_text(encoding="utf-8")
|
|
142
|
+
|
|
143
|
+
for label in ("needs-info", "ready-for-agent"):
|
|
144
|
+
self.assertIn(f"`{label}`", seed)
|
|
145
|
+
self.assertIn(f"`{label}`", triage_skill)
|
|
146
|
+
|
|
147
|
+
# The retired roles must not appear in the table portion (before the
|
|
148
|
+
# board-status explainer) as if they were still active labels.
|
|
149
|
+
table, sep, explainer = seed.partition("Board status is authoritative")
|
|
150
|
+
self.assertTrue(sep, "seed must explain that board status is authoritative")
|
|
151
|
+
for retired in ("needs-triage", "ready-for-human", "wontfix"):
|
|
152
|
+
self.assertNotIn(f"`{retired}`", table)
|
|
153
|
+
|
|
154
|
+
self.assertIn("Board status is authoritative", seed)
|
|
155
|
+
self.assertIn("Board status is authoritative", triage_skill)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SentinelProtectsFilledProfile(unittest.TestCase):
|
|
159
|
+
"""A consumer's filled board profile must never be rewritten on rerun."""
|
|
160
|
+
|
|
161
|
+
def test_filled_public_profile_is_left_unchanged_on_rerun(self):
|
|
162
|
+
real_profile = REPO / "docs/agents/board-sync.md"
|
|
163
|
+
text = real_profile.read_text(encoding="utf-8")
|
|
164
|
+
first_line = text.splitlines()[0]
|
|
165
|
+
self.assertEqual(
|
|
166
|
+
classify(first_line, False), "skip",
|
|
167
|
+
"a filled project profile must classify as skip, never re-fillable",
|
|
168
|
+
)
|
|
169
|
+
# Prove this is genuinely filled (not an accidental stub), otherwise
|
|
170
|
+
# the "skip" assertion above would be vacuous.
|
|
171
|
+
self.assertIn('"repo": "iKon85/agent-workflow-kit"', text)
|
|
172
|
+
self.assertIn('"vorBau": "Clarify Before Build"', text)
|
|
173
|
+
|
|
174
|
+
def test_unfilled_stub_gets_english_defaults(self):
|
|
175
|
+
stub_first_line = "<!-- setup-workflow: state=stub; mode=github-projects-v2 -->"
|
|
176
|
+
self.assertEqual(classify(stub_first_line, False), "fill")
|
|
177
|
+
|
|
178
|
+
seed = (SKILL / "board-sync.md").read_text(encoding="utf-8")
|
|
179
|
+
self.assertIn('"retroValues": ["ran", "skipped"]', seed)
|
|
180
|
+
self.assertIn('"vorBau": "Clarify Before Build"', seed)
|
|
181
|
+
self.assertNotIn("gefahren", seed)
|
|
182
|
+
self.assertNotIn("Vor Bau zu klären", seed)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class FreshFillProducesValidSentinels(unittest.TestCase):
|
|
186
|
+
"""Simulate the write step: prepend the documented sentinel to a template
|
|
187
|
+
body and assert the result is a well-formed project-layer file."""
|
|
188
|
+
|
|
189
|
+
def test_prepended_sentinel_parses(self):
|
|
190
|
+
body = (SKILL / "spec-self-critique-seed.md").read_text(encoding="utf-8")
|
|
191
|
+
for state, mode in (("stub", None), ("filled", "github-projects-v2"),
|
|
192
|
+
("not-applicable", "none")):
|
|
193
|
+
sentinel = f"<!-- setup-workflow: state={state}" + (
|
|
194
|
+
f"; mode={mode}" if mode else "") + " -->"
|
|
195
|
+
written = sentinel + "\n" + body
|
|
196
|
+
self.assertRegex(written.splitlines()[0], SENTINEL_RE)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stale-skill-name lint (Welle 26 / Slice 5 / #989).
|
|
3
|
+
|
|
4
|
+
When a skill is renamed (here: `setup-matt-pocock-skills` -> `setup-workflow`),
|
|
5
|
+
every ACTIVE reference must move with it, or consumer skills point at a command
|
|
6
|
+
that no longer exists (Codex R1 #1: `to-issues/SKILL.md:10` still invoked the
|
|
7
|
+
deleted name). This lint fails on any active reference to a retired skill name.
|
|
8
|
+
|
|
9
|
+
ACTIVE reference = a form that would actually misroute: a slash invocation
|
|
10
|
+
(`/<name>`), a frontmatter `name:` value, a `.../skills/<name>` dir/path reference,
|
|
11
|
+
or a manifest JSON key (`"<name>":`).
|
|
12
|
+
|
|
13
|
+
EXPLICITLY NOT a violation (Codex R2 #1): legitimate *attribution prose* that names
|
|
14
|
+
the upstream skill — the provenance upstream-path `engineering/<name>/`, a manifest
|
|
15
|
+
`note`, or a SKILL.md description "adapted from <name>". Those credit the upstream,
|
|
16
|
+
they don't route anywhere. The active-form patterns below match none of them. A
|
|
17
|
+
per-line `stale-name-lint: ok` marker also exempts a single line.
|
|
18
|
+
|
|
19
|
+
Run: python3 scripts/test_skill_stale_name_lint.py
|
|
20
|
+
"""
|
|
21
|
+
import re
|
|
22
|
+
import unittest
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
26
|
+
|
|
27
|
+
# Retired skill names that must have zero active references anywhere we scan.
|
|
28
|
+
RETIRED_NAMES = ["setup-matt-pocock-skills"]
|
|
29
|
+
|
|
30
|
+
LINT_OK = "stale-name-lint: ok"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _active_patterns(name):
|
|
34
|
+
"""Forms that would actually misroute to a non-existent skill.
|
|
35
|
+
|
|
36
|
+
Boundary-guarded so neither the provenance path `engineering/<name>/` nor
|
|
37
|
+
attribution prose (`adapted from <name>`, a manifest note) matches.
|
|
38
|
+
"""
|
|
39
|
+
n = re.escape(name)
|
|
40
|
+
return [
|
|
41
|
+
re.compile(rf"(?<![\w/])/{n}(?![\w/-])"), # slash invocation /<name>
|
|
42
|
+
re.compile(rf"name:\s*{n}\b"), # frontmatter name:
|
|
43
|
+
re.compile(rf"skills/{n}\b"), # .claude|.agents/skills/<name>
|
|
44
|
+
re.compile(rf'"{n}"\s*:'), # manifest JSON key
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _scan_files():
|
|
49
|
+
"""Files where an active reference would actually mislead a consumer."""
|
|
50
|
+
files = []
|
|
51
|
+
for root in ("CLAUDE.md", "AGENTS.md"):
|
|
52
|
+
p = REPO / root
|
|
53
|
+
if p.is_file():
|
|
54
|
+
files.append(p)
|
|
55
|
+
for d in (".claude/skills", ".agents/skills", "docs"):
|
|
56
|
+
base = REPO / d
|
|
57
|
+
if base.is_dir():
|
|
58
|
+
files.extend(base.rglob("*.md"))
|
|
59
|
+
manifest = REPO / ".claude/skills/skill-manifest.json"
|
|
60
|
+
if manifest.is_file():
|
|
61
|
+
files.append(manifest)
|
|
62
|
+
return files
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _violations_for(name):
|
|
66
|
+
"""Return list of (file, lineno, line) with an active reference to `name`."""
|
|
67
|
+
patterns = _active_patterns(name)
|
|
68
|
+
out = []
|
|
69
|
+
for f in _scan_files():
|
|
70
|
+
try:
|
|
71
|
+
text = f.read_text(encoding="utf-8")
|
|
72
|
+
except (UnicodeDecodeError, OSError):
|
|
73
|
+
continue
|
|
74
|
+
for i, line in enumerate(text.splitlines(), start=1):
|
|
75
|
+
if name not in line or LINT_OK in line:
|
|
76
|
+
continue
|
|
77
|
+
if any(p.search(line) for p in patterns):
|
|
78
|
+
out.append((f.relative_to(REPO), i, line.strip()))
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class StaleSkillName(unittest.TestCase):
|
|
83
|
+
def test_no_active_references_to_retired_skill_names(self):
|
|
84
|
+
all_v = []
|
|
85
|
+
for name in RETIRED_NAMES:
|
|
86
|
+
all_v.extend(_violations_for(name))
|
|
87
|
+
msg = "\n".join(f" {f}:{n}: {ln}" for f, n, ln in all_v)
|
|
88
|
+
self.assertEqual(
|
|
89
|
+
all_v,
|
|
90
|
+
[],
|
|
91
|
+
f"\nActive references to retired skill name(s) found "
|
|
92
|
+
f"(rename them or add `{LINT_OK}`):\n{msg}",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def test_attribution_prose_is_not_flagged(self):
|
|
96
|
+
"""Credit forms that name the upstream must NOT be treated as violations."""
|
|
97
|
+
for name in RETIRED_NAMES:
|
|
98
|
+
patterns = _active_patterns(name)
|
|
99
|
+
for benign in (
|
|
100
|
+
f"| `setup-workflow` | generic | `engineering/{name}/` |",
|
|
101
|
+
f"adapted from Matt Pocock's `{name}` (MIT)",
|
|
102
|
+
f'"note": "evolved from {name} (Welle 26)"',
|
|
103
|
+
):
|
|
104
|
+
self.assertFalse(
|
|
105
|
+
any(p.search(benign) for p in patterns),
|
|
106
|
+
f"attribution prose should be exempt: {benign!r}",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def test_active_forms_are_flagged(self):
|
|
110
|
+
"""The misrouting forms must be caught."""
|
|
111
|
+
for name in RETIRED_NAMES:
|
|
112
|
+
patterns = _active_patterns(name)
|
|
113
|
+
for active in (
|
|
114
|
+
f"run `/{name}` if not",
|
|
115
|
+
f"name: {name}",
|
|
116
|
+
f".claude/skills/{name}/SKILL.md",
|
|
117
|
+
f'"{name}": {{ "class": "vendored" }}',
|
|
118
|
+
):
|
|
119
|
+
self.assertTrue(
|
|
120
|
+
any(p.search(active) for p in patterns),
|
|
121
|
+
f"active form should be flagged: {active!r}",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Skill surface-reference lint (Welle 49 / Slice 2 / #1873).
|
|
3
|
+
|
|
4
|
+
Every `/skill-name` or backtick `` `skill-name` `` a SKILL.md names must
|
|
5
|
+
resolve to a skill that exists as a directory on the SAME surface the
|
|
6
|
+
referencing file lives on. `skill-manifest.json`'s `surfaces` array is the
|
|
7
|
+
SSOT for which trees (`.claude/skills`, `.agents/skills`) host a skill.
|
|
8
|
+
|
|
9
|
+
Two independent checks:
|
|
10
|
+
|
|
11
|
+
MISMATCH — an exact backtick reference to a REAL manifest skill NOT hosted
|
|
12
|
+
on the referencing file's surface (a Claude-only skill named from a
|
|
13
|
+
Codex-surface file, or vice-versa). This is the class behind the
|
|
14
|
+
Codex-escalation dangling-target finding and the `/code-review` dangling:
|
|
15
|
+
a PUBLISHED skill can pass the existing publish audit
|
|
16
|
+
(`auditSkillNameRefs` in `scripts/lib/audit-refs.mjs`, which only checks
|
|
17
|
+
*publish status*) while still dangling on ONE surface — that audit has no
|
|
18
|
+
concept of `surfaces` at all.
|
|
19
|
+
|
|
20
|
+
NONEXISTENT — a `/name` slash-token styled as a catalog entry (a list item
|
|
21
|
+
opening with an optionally-bold backtick slash-token, the convention every
|
|
22
|
+
genuine skill listing in this repo uses, e.g. `- **`/tdd`** — ...`) that
|
|
23
|
+
names NO manifest skill anywhere — a typo'd or dead name in a router list.
|
|
24
|
+
|
|
25
|
+
`class: "adapter"` skills (e.g. `codex-adapter-sync`) are exempt from
|
|
26
|
+
MISMATCH: their purpose is to be invoked FROM the opposite surface —
|
|
27
|
+
Claude-side prose runs the Codex-mirror-sync tool that itself lives only
|
|
28
|
+
under `.agents/skills` (manifest note: "published skills reference it as the
|
|
29
|
+
Claude→Codex mirror step"). That's the intended direction, not a dangler.
|
|
30
|
+
|
|
31
|
+
Deliberate exceptions: an inline marker on the line (`Claude Code only`, or
|
|
32
|
+
the generic `skill-surface-lint: ok`), or `PROSE_ALLOWLIST` below for
|
|
33
|
+
skill-system PROSE in files this slice doesn't touch (each entry carries its
|
|
34
|
+
own reason — an explicit, auditable allowlist, not a blanket exemption).
|
|
35
|
+
|
|
36
|
+
Run: python3 scripts/test_skill_surface_refs.py
|
|
37
|
+
"""
|
|
38
|
+
import json
|
|
39
|
+
import re
|
|
40
|
+
import unittest
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
from test_skill_portability_lint import load_manifest, SKILL_DIRS
|
|
44
|
+
|
|
45
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
46
|
+
TREE_SURFACE = {".claude/skills": "claude", ".agents/skills": "codex"}
|
|
47
|
+
|
|
48
|
+
EXEMPT = "skill-surface-lint: ok"
|
|
49
|
+
CLAUDE_ONLY_MARKER = "Claude Code only"
|
|
50
|
+
|
|
51
|
+
# Slash-tokens a catalog bullet can legitimately name without matching any
|
|
52
|
+
# manifest skill: either a real command OUTSIDE this repo's manifest (a
|
|
53
|
+
# Claude Code CLI built-in, or — like `handoff` — a skill living in the
|
|
54
|
+
# user's global `~/.claude/skills`, never a repo skill directory; ask-matt
|
|
55
|
+
# names both, formatted exactly like a real catalog entry, `/compact` even
|
|
56
|
+
# self-annotates "(built-in)"), or a documented TEMPLATE PLACEHOLDER in
|
|
57
|
+
# third-party (vendored, not ours to rewrite) skill prose — `impeccable`'s
|
|
58
|
+
# recommended-actions template repeats `/command-name` as a fill-in-the-blank,
|
|
59
|
+
# not a specific skill.
|
|
60
|
+
EXTERNAL_COMMANDS = {"compact", "handoff", "command-name"}
|
|
61
|
+
|
|
62
|
+
# Deliberate skill-system PROSE that NAMES a skill without inviting an
|
|
63
|
+
# operator on THIS surface to invoke it — not a routing reference. Each is a
|
|
64
|
+
# file this slice does not edit (see #1873 scope); (relpath, distinctive
|
|
65
|
+
# line substring, reason).
|
|
66
|
+
PROSE_ALLOWLIST = [
|
|
67
|
+
(".agents/skills/ask-matt/SKILL.md",
|
|
68
|
+
"Folder↔upstream-name note:",
|
|
69
|
+
"identity blockquote names its own upstream-name mapping (Matt "
|
|
70
|
+
"Pocock's write-a-skill == upstream writing-great-skills), not a "
|
|
71
|
+
"routing invocation"),
|
|
72
|
+
(".agents/skills/sync-upstream-skills/SKILL.md",
|
|
73
|
+
"write-a-skill` = upstream `writing-great-skills`",
|
|
74
|
+
"folder↔upstream naming-convention example, not an invocation"),
|
|
75
|
+
(".agents/skills/codex-adapter-sync/SKILL.md",
|
|
76
|
+
"grill-with-docs-codex`, `grill-me-codex`, Chase AI's cross-model",
|
|
77
|
+
"rule-doc prose naming the escalation-target-rewrite pattern by "
|
|
78
|
+
"example, describing the very class this lint catches"),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# A markdown list item that OPENS with an (optionally bold) backtick
|
|
82
|
+
# slash-token — the sole convention every genuine skill-catalog entry in
|
|
83
|
+
# this repo uses (`- **`/tdd`** — ...`). Deliberately excludes inline/prose
|
|
84
|
+
# slash mentions (route examples, `/tmp`, upstream-repo slugs) which never
|
|
85
|
+
# open a bullet this way.
|
|
86
|
+
BULLET_ENTRY = re.compile(r"^\s*(?:[-*]|\d+\.)\s+\*{0,2}`([^`\n]+)`")
|
|
87
|
+
NAME_SHAPE = re.compile(r"^(/?)([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def escape_name(name: str) -> str:
|
|
91
|
+
return re.escape(name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def name_pattern(name: str) -> re.Pattern:
|
|
95
|
+
"""A backtick-wrapped exact reference (`` `/name` `` or `` `name` ``) OR a
|
|
96
|
+
boundary-guarded bare slash-token — mirrors the proven construction in
|
|
97
|
+
`scripts/lib/audit-refs.mjs`'s `auditSkillNameRefs` (same repo, same
|
|
98
|
+
problem shape: a name match that ignores doc paths / repo slugs, since
|
|
99
|
+
`owner/name` and `skills/name` never satisfy either alternative — the
|
|
100
|
+
char immediately before the match is excluded by the boundary guard)."""
|
|
101
|
+
e = escape_name(name)
|
|
102
|
+
return re.compile(rf"`/?{e}`|(?<![\w/-])/{e}(?![\w-])")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_allowlisted(relpath: str, line: str) -> bool:
|
|
106
|
+
return any(relpath == path and substr in line
|
|
107
|
+
for path, substr, _reason in PROSE_ALLOWLIST)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def find_mismatches(surface: str, relpath: str, text: str, manifest: dict) -> list[str]:
|
|
111
|
+
"""Lines on `surface` naming a real manifest skill not hosted there."""
|
|
112
|
+
problems = []
|
|
113
|
+
for lineno, line in enumerate(text.splitlines(), 1):
|
|
114
|
+
if EXEMPT in line or CLAUDE_ONLY_MARKER in line or is_allowlisted(relpath, line):
|
|
115
|
+
continue
|
|
116
|
+
for name, entry in manifest.items():
|
|
117
|
+
if entry.get("class") == "adapter":
|
|
118
|
+
continue
|
|
119
|
+
if surface in entry.get("surfaces", []):
|
|
120
|
+
continue
|
|
121
|
+
if name_pattern(name).search(line):
|
|
122
|
+
problems.append(
|
|
123
|
+
f"{relpath}:{lineno}: `{name}` not hosted on '{surface}' "
|
|
124
|
+
f"(hosted={entry.get('surfaces', [])}): {line.strip()}")
|
|
125
|
+
return problems
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def find_nonexistent(relpath: str, text: str, manifest: dict) -> list[str]:
|
|
129
|
+
"""Catalog-style bullets naming a slash-token that matches NO manifest
|
|
130
|
+
skill anywhere and is not a known external command."""
|
|
131
|
+
problems = []
|
|
132
|
+
for lineno, line in enumerate(text.splitlines(), 1):
|
|
133
|
+
if EXEMPT in line:
|
|
134
|
+
continue
|
|
135
|
+
m = BULLET_ENTRY.match(line)
|
|
136
|
+
if not m:
|
|
137
|
+
continue
|
|
138
|
+
shape = NAME_SHAPE.match(m.group(1))
|
|
139
|
+
if not shape or not shape.group(1):
|
|
140
|
+
continue
|
|
141
|
+
name = shape.group(2)
|
|
142
|
+
if name in manifest or name in EXTERNAL_COMMANDS:
|
|
143
|
+
continue
|
|
144
|
+
problems.append(f"{relpath}:{lineno}: `/{name}` names no skill anywhere: {line.strip()}")
|
|
145
|
+
return problems
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def iter_skill_markdown():
|
|
149
|
+
"""Yield (surface, relpath-as-posix-string, text) for every distributed
|
|
150
|
+
*.md under every skill directory on both trees."""
|
|
151
|
+
for tree in SKILL_DIRS:
|
|
152
|
+
surface = TREE_SURFACE[tree]
|
|
153
|
+
base = REPO_ROOT / tree
|
|
154
|
+
if not base.is_dir():
|
|
155
|
+
continue
|
|
156
|
+
for skill_dir in sorted(p for p in base.iterdir() if p.is_dir()):
|
|
157
|
+
for md in sorted(skill_dir.rglob("*.md")):
|
|
158
|
+
relpath = md.relative_to(REPO_ROOT).as_posix()
|
|
159
|
+
yield surface, relpath, md.read_text(encoding="utf-8")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class PatternDetectorFixtures(unittest.TestCase):
|
|
163
|
+
"""Fixture (a)+(c): a manufactured Codex-surface skill naming a
|
|
164
|
+
Claude-only manifest skill is red unmarked, green once marked."""
|
|
165
|
+
|
|
166
|
+
MANIFEST = {
|
|
167
|
+
"claude-only-skill": {"class": "vendored", "surfaces": ["claude"]},
|
|
168
|
+
"dual-skill": {"class": "generic", "surfaces": ["claude", "codex"]},
|
|
169
|
+
"the-adapter": {"class": "adapter", "surfaces": ["codex"]},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def test_unmarked_claude_only_ref_on_codex_surface_is_red(self):
|
|
173
|
+
text = "- **`/claude-only-skill`** — does a thing."
|
|
174
|
+
problems = find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST)
|
|
175
|
+
self.assertEqual(len(problems), 1)
|
|
176
|
+
self.assertIn("claude-only-skill", problems[0])
|
|
177
|
+
|
|
178
|
+
def test_marked_claude_only_ref_is_green(self):
|
|
179
|
+
text = "- **`/claude-only-skill`** (Claude Code only) — does a thing."
|
|
180
|
+
self.assertEqual(find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
181
|
+
|
|
182
|
+
def test_dual_surface_ref_is_always_green(self):
|
|
183
|
+
text = "- **`/dual-skill`** — does a thing."
|
|
184
|
+
self.assertEqual(find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
185
|
+
|
|
186
|
+
def test_adapter_class_target_is_exempt_both_directions(self):
|
|
187
|
+
text = "run **`/the-adapter`** after editing a skill."
|
|
188
|
+
self.assertEqual(find_mismatches("claude", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
189
|
+
|
|
190
|
+
def test_doc_path_and_repo_slug_are_not_matched(self):
|
|
191
|
+
text = ("see `docs/agents/skills/claude-only-skill.md` or the upstream "
|
|
192
|
+
"repo `someorg/claude-only-skill`")
|
|
193
|
+
self.assertEqual(find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
194
|
+
|
|
195
|
+
def test_generic_exempt_marker_is_line_scoped(self):
|
|
196
|
+
text = "- **`/claude-only-skill`** <!-- skill-surface-lint: ok -->"
|
|
197
|
+
self.assertEqual(find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class NonexistentFixtures(unittest.TestCase):
|
|
201
|
+
"""Fixture (b): a catalog bullet naming a skill that exists nowhere."""
|
|
202
|
+
|
|
203
|
+
MANIFEST = {"tdd": {"class": "vendored", "surfaces": ["claude", "codex"]}}
|
|
204
|
+
|
|
205
|
+
def test_wholly_nonexistent_skill_bullet_is_red(self):
|
|
206
|
+
text = "- **`/frobnicate-widget`** — does a thing that never existed."
|
|
207
|
+
problems = find_nonexistent("fixture/SKILL.md", text, self.MANIFEST)
|
|
208
|
+
self.assertEqual(len(problems), 1)
|
|
209
|
+
self.assertIn("frobnicate-widget", problems[0])
|
|
210
|
+
|
|
211
|
+
def test_known_skill_bullet_is_green(self):
|
|
212
|
+
text = "- **`/tdd`** — test-driven development."
|
|
213
|
+
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
214
|
+
|
|
215
|
+
def test_external_builtin_bullet_is_green(self):
|
|
216
|
+
text = "- **`/handoff`** — compacts the conversation."
|
|
217
|
+
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
218
|
+
|
|
219
|
+
def test_inline_prose_slash_mention_is_not_a_catalog_entry(self):
|
|
220
|
+
text = "falling back to `/tmp` when $TMPDIR is unset."
|
|
221
|
+
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
222
|
+
|
|
223
|
+
def test_route_example_list_is_not_flagged(self):
|
|
224
|
+
text = "- Product signals: `/app/*`, `/dashboard`, `/settings`, forms."
|
|
225
|
+
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
226
|
+
|
|
227
|
+
def test_exempt_marker_line_scoped(self):
|
|
228
|
+
text = "- **`/frobnicate-widget`** <!-- skill-surface-lint: ok -->"
|
|
229
|
+
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class AllowlistFixtures(unittest.TestCase):
|
|
233
|
+
"""Fixture (d): the deliberate skill-system-prose exceptions stay green."""
|
|
234
|
+
|
|
235
|
+
MANIFEST = {"write-a-skill": {"class": "vendored", "surfaces": ["claude"]}}
|
|
236
|
+
|
|
237
|
+
def test_ask_matt_identity_blockquote_is_allowlisted(self):
|
|
238
|
+
text = ("> Homage. Folder↔upstream-name note: `/diagnose` = upstream "
|
|
239
|
+
"`diagnosing-bugs`, `/write-a-skill` = upstream `writing-great-skills`.")
|
|
240
|
+
problems = find_mismatches(
|
|
241
|
+
"codex", ".agents/skills/ask-matt/SKILL.md", text, self.MANIFEST)
|
|
242
|
+
self.assertEqual(problems, [])
|
|
243
|
+
|
|
244
|
+
def test_sync_upstream_skills_naming_note_is_allowlisted(self):
|
|
245
|
+
text = ("e.g. `diagnose` = upstream `diagnosing-bugs`, "
|
|
246
|
+
"`write-a-skill` = upstream `writing-great-skills`")
|
|
247
|
+
problems = find_mismatches(
|
|
248
|
+
"codex", ".agents/skills/sync-upstream-skills/SKILL.md", text, self.MANIFEST)
|
|
249
|
+
self.assertEqual(problems, [])
|
|
250
|
+
|
|
251
|
+
def test_codex_adapter_sync_rule_doc_example_is_allowlisted(self):
|
|
252
|
+
text = ("has no `.agents` mirror. Any Codex-side reference to that skill "
|
|
253
|
+
"name is a dangling target (e.g. `grill-with-docs-codex`, "
|
|
254
|
+
"`grill-me-codex`, Chase AI's cross-model Act-2 variants).")
|
|
255
|
+
manifest = {"grill-with-docs-codex": {"class": "vendored", "surfaces": ["claude"]},
|
|
256
|
+
"grill-me-codex": {"class": "vendored", "surfaces": ["claude"]}}
|
|
257
|
+
problems = find_mismatches(
|
|
258
|
+
"codex", ".agents/skills/codex-adapter-sync/SKILL.md", text, manifest)
|
|
259
|
+
self.assertEqual(problems, [])
|
|
260
|
+
|
|
261
|
+
def test_allowlist_does_not_leak_to_other_files(self):
|
|
262
|
+
"""The allowlist is (path, substring)-scoped — the SAME prose in a
|
|
263
|
+
DIFFERENT file must still be caught (regression guard)."""
|
|
264
|
+
text = ("e.g. `diagnose` = upstream `diagnosing-bugs`, "
|
|
265
|
+
"`write-a-skill` = upstream `writing-great-skills`")
|
|
266
|
+
problems = find_mismatches("codex", "fixture/other-skill/SKILL.md", text, self.MANIFEST)
|
|
267
|
+
self.assertEqual(len(problems), 1)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class RealRepoSurfaceRefs(unittest.TestCase):
|
|
271
|
+
"""The actual repo, end to end: every skill-name reference in every
|
|
272
|
+
SKILL.md resolves on its own surface (AC: green on HEAD)."""
|
|
273
|
+
|
|
274
|
+
def test_no_cross_surface_dangling_references(self):
|
|
275
|
+
manifest = load_manifest()["skills"]
|
|
276
|
+
problems = []
|
|
277
|
+
for surface, relpath, text in iter_skill_markdown():
|
|
278
|
+
problems.extend(find_mismatches(surface, relpath, text, manifest))
|
|
279
|
+
self.assertEqual(
|
|
280
|
+
problems, [],
|
|
281
|
+
"skill reference dangles on its own surface — either the target "
|
|
282
|
+
"doesn't ship there (mark `(Claude Code only)` / port a Codex "
|
|
283
|
+
"mirror) or this is deliberate prose (add a PROSE_ALLOWLIST "
|
|
284
|
+
"entry with a reason):\n" + "\n".join(problems))
|
|
285
|
+
|
|
286
|
+
def test_no_wholly_nonexistent_catalog_entries(self):
|
|
287
|
+
manifest = load_manifest()["skills"]
|
|
288
|
+
problems = []
|
|
289
|
+
for _surface, relpath, text in iter_skill_markdown():
|
|
290
|
+
problems.extend(find_nonexistent(relpath, text, manifest))
|
|
291
|
+
self.assertEqual(
|
|
292
|
+
problems, [],
|
|
293
|
+
"catalog bullet names a skill that exists on no surface — fix "
|
|
294
|
+
"the name, remove the stale entry, or add it to "
|
|
295
|
+
"EXTERNAL_COMMANDS if it's a real command outside this repo's "
|
|
296
|
+
"manifest:\n" + "\n".join(problems))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
unittest.main(verbosity=2)
|