@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.
Files changed (273) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +139 -0
  2. package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
  3. package/.agents/skills/audit-skills/SKILL.md +145 -0
  4. package/.agents/skills/board-to-waves/SKILL.md +135 -0
  5. package/.agents/skills/code-review/SKILL.md +64 -0
  6. package/.agents/skills/codebase-design/DEEPENING.md +37 -0
  7. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  8. package/.agents/skills/codebase-design/SKILL.md +114 -0
  9. package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
  10. package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
  11. package/.agents/skills/decision-gate/SKILL.md +67 -0
  12. package/.agents/skills/diagnose/SKILL.md +136 -0
  13. package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
  14. package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  15. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  16. package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  17. package/.agents/skills/domain-modeling/SKILL.md +76 -0
  18. package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
  19. package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
  20. package/.agents/skills/grill-me/SKILL.md +54 -0
  21. package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
  22. package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  23. package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  24. package/.agents/skills/grill-with-docs/SKILL.md +137 -0
  25. package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
  26. package/.agents/skills/implement/SKILL.md +15 -0
  27. package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
  28. package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
  29. package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
  30. package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  31. package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  32. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
  33. package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  34. package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
  35. package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
  36. package/.agents/skills/kit-release/SKILL.md +56 -0
  37. package/.agents/skills/local-ci/SKILL.md +94 -0
  38. package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
  39. package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
  40. package/.agents/skills/prototype/LOGIC.md +79 -0
  41. package/.agents/skills/prototype/SKILL.md +30 -0
  42. package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
  43. package/.agents/skills/prototype/UI.md +112 -0
  44. package/.agents/skills/research/SKILL.md +12 -0
  45. package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
  46. package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
  47. package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
  48. package/.agents/skills/retro/SKILL.md +209 -0
  49. package/.agents/skills/scale-check/SKILL.md +141 -0
  50. package/.agents/skills/security-audit/SKILL.md +67 -0
  51. package/.agents/skills/setup-workflow/SKILL.md +187 -0
  52. package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
  53. package/.agents/skills/setup-workflow/board-sync.md +111 -0
  54. package/.agents/skills/setup-workflow/code-review.md +26 -0
  55. package/.agents/skills/setup-workflow/domain.md +51 -0
  56. package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
  57. package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
  58. package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
  59. package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
  60. package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
  61. package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
  62. package/.agents/skills/setup-workflow/triage-labels.md +18 -0
  63. package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
  64. package/.agents/skills/spec-self-critique/SKILL.md +152 -0
  65. package/.agents/skills/spec-self-critique/scenarios.md +82 -0
  66. package/.agents/skills/tdd/SKILL.md +120 -0
  67. package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
  68. package/.agents/skills/tdd/deep-modules.md +33 -0
  69. package/.agents/skills/tdd/interface-design.md +31 -0
  70. package/.agents/skills/tdd/mocking.md +59 -0
  71. package/.agents/skills/tdd/refactoring.md +10 -0
  72. package/.agents/skills/tdd/tests.md +61 -0
  73. package/.agents/skills/to-issues/SKILL.md +325 -0
  74. package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
  75. package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
  76. package/.agents/skills/to-prd/SKILL.md +180 -0
  77. package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
  78. package/.agents/skills/to-waves/SKILL.md +260 -0
  79. package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
  80. package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
  81. package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
  82. package/.agents/skills/triage/SKILL.md +122 -0
  83. package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
  84. package/.agents/skills/verify-spike/SKILL.md +39 -0
  85. package/.agents/skills/wayfinder/SKILL.md +127 -0
  86. package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
  87. package/.agents/skills/wrapup/SKILL.md +71 -0
  88. package/.agents/skills/wrapup/SOURCES.txt +6 -0
  89. package/.agents/skills/zoom-out/SKILL.md +7 -0
  90. package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
  91. package/.claude/hooks/_hook_utils.py +184 -0
  92. package/.claude/hooks/drift-guard.py +176 -0
  93. package/.claude/hooks/skill-drift-hint.py +122 -0
  94. package/.claude/hooks/sync-board-status.py +191 -0
  95. package/.claude/skills/ask-matt/SKILL.md +139 -0
  96. package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
  97. package/.claude/skills/audit-skills/SKILL.md +145 -0
  98. package/.claude/skills/board-to-waves/SKILL.md +148 -0
  99. package/.claude/skills/code-review/SKILL.md +64 -0
  100. package/.claude/skills/codebase-design/DEEPENING.md +37 -0
  101. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  102. package/.claude/skills/codebase-design/SKILL.md +114 -0
  103. package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
  104. package/.claude/skills/codex-build/SKILL.md +126 -0
  105. package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
  106. package/.claude/skills/codex-review/SKILL.md +145 -0
  107. package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
  108. package/.claude/skills/decision-gate/SKILL.md +67 -0
  109. package/.claude/skills/diagnose/SKILL.md +136 -0
  110. package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
  111. package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  112. package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
  113. package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  114. package/.claude/skills/domain-modeling/SKILL.md +76 -0
  115. package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
  116. package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
  117. package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
  118. package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
  119. package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
  120. package/.claude/skills/grill-me/SKILL.md +54 -0
  121. package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
  122. package/.claude/skills/grill-me-codex/SKILL.md +158 -0
  123. package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
  124. package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  125. package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  126. package/.claude/skills/grill-with-docs/SKILL.md +137 -0
  127. package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
  128. package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
  129. package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
  130. package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
  131. package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
  132. package/.claude/skills/implement/SKILL.md +15 -0
  133. package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
  134. package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
  135. package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
  136. package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  137. package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  138. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
  139. package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  140. package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
  141. package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
  142. package/.claude/skills/kit-release/SKILL.md +56 -0
  143. package/.claude/skills/local-ci/SKILL.md +94 -0
  144. package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
  145. package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
  146. package/.claude/skills/prototype/LOGIC.md +79 -0
  147. package/.claude/skills/prototype/SKILL.md +30 -0
  148. package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
  149. package/.claude/skills/prototype/UI.md +112 -0
  150. package/.claude/skills/research/SKILL.md +12 -0
  151. package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
  152. package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
  153. package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
  154. package/.claude/skills/retro/SKILL.md +209 -0
  155. package/.claude/skills/scale-check/SKILL.md +141 -0
  156. package/.claude/skills/security-audit/SKILL.md +67 -0
  157. package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
  158. package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
  159. package/.claude/skills/setup-workflow/SKILL.md +187 -0
  160. package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
  161. package/.claude/skills/setup-workflow/board-sync.md +111 -0
  162. package/.claude/skills/setup-workflow/code-review.md +26 -0
  163. package/.claude/skills/setup-workflow/domain.md +51 -0
  164. package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
  165. package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
  166. package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
  167. package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
  168. package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
  169. package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
  170. package/.claude/skills/setup-workflow/triage-labels.md +18 -0
  171. package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
  172. package/.claude/skills/skill-manifest.json +389 -0
  173. package/.claude/skills/spec-self-critique/SKILL.md +152 -0
  174. package/.claude/skills/spec-self-critique/scenarios.md +82 -0
  175. package/.claude/skills/tdd/SKILL.md +120 -0
  176. package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
  177. package/.claude/skills/tdd/deep-modules.md +33 -0
  178. package/.claude/skills/tdd/interface-design.md +31 -0
  179. package/.claude/skills/tdd/mocking.md +59 -0
  180. package/.claude/skills/tdd/refactoring.md +10 -0
  181. package/.claude/skills/tdd/tests.md +61 -0
  182. package/.claude/skills/to-issues/SKILL.md +325 -0
  183. package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
  184. package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
  185. package/.claude/skills/to-prd/SKILL.md +180 -0
  186. package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
  187. package/.claude/skills/to-waves/SKILL.md +260 -0
  188. package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
  189. package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
  190. package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
  191. package/.claude/skills/triage/SKILL.md +122 -0
  192. package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
  193. package/.claude/skills/verify-spike/SKILL.md +39 -0
  194. package/.claude/skills/wayfinder/SKILL.md +127 -0
  195. package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
  196. package/.claude/skills/wrapup/SKILL.md +81 -0
  197. package/.claude/skills/wrapup/SOURCES.txt +6 -0
  198. package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
  199. package/.claude/skills/write-a-skill/SKILL.md +88 -0
  200. package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
  201. package/.claude/skills/zoom-out/SKILL.md +7 -0
  202. package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
  203. package/LICENSE +28 -0
  204. package/PROVENANCE.md +39 -0
  205. package/README.md +619 -0
  206. package/agent-workflow-kit.package.json +1939 -0
  207. package/docs/agents/board-sync.md +71 -0
  208. package/docs/agents/code-review.md +14 -0
  209. package/docs/agents/domain.md +7 -0
  210. package/docs/agents/issue-tracker.md +12 -0
  211. package/docs/agents/security-audit-runbook-template.md +205 -0
  212. package/docs/agents/skills/orchestrate-wave.md +13 -0
  213. package/docs/agents/skills/spec-self-critique.md +18 -0
  214. package/docs/agents/triage-labels.md +10 -0
  215. package/docs/agents/wave-anchor-template.md +88 -0
  216. package/docs/conventions/spec-completeness.md +23 -0
  217. package/docs/index.html +56 -0
  218. package/docs/methodology.html +488 -0
  219. package/docs/methodology.svg +93 -0
  220. package/docs/workflow.png +0 -0
  221. package/package.json +40 -0
  222. package/scripts/anchor_table.py +343 -0
  223. package/scripts/board-sync.py +1232 -0
  224. package/scripts/board_config.py +147 -0
  225. package/scripts/board_fields.py +192 -0
  226. package/scripts/build-kit.mjs +116 -0
  227. package/scripts/build-kit.test.mjs +59 -0
  228. package/scripts/check-kit-staleness.mjs +45 -0
  229. package/scripts/check-kit-staleness.test.mjs +19 -0
  230. package/scripts/execute-ready-check.py +516 -0
  231. package/scripts/issue_deps.py +79 -0
  232. package/scripts/kit-release.mjs +100 -0
  233. package/scripts/kit-release.test.mjs +67 -0
  234. package/scripts/lib/audit-refs.mjs +146 -0
  235. package/scripts/lib/scrub.mjs +113 -0
  236. package/scripts/lib/scrub.test.mjs +187 -0
  237. package/scripts/loc_offender_core.py +196 -0
  238. package/scripts/loc_offender_gate.py +182 -0
  239. package/scripts/node_kind.py +68 -0
  240. package/scripts/portability_profile_scan.py +53 -0
  241. package/scripts/pr-body-check.py +263 -0
  242. package/scripts/program_graph.py +125 -0
  243. package/scripts/program_graph_parse.py +275 -0
  244. package/scripts/program_graph_validate.py +256 -0
  245. package/scripts/program_sync.py +160 -0
  246. package/scripts/release-delta-guard.mjs +96 -0
  247. package/scripts/release-delta-guard.test.mjs +53 -0
  248. package/scripts/test_board_sync_create_idempotency.py +104 -0
  249. package/scripts/test_dist_kit_smoke.py +199 -0
  250. package/scripts/test_kit_docs_language_census.py +178 -0
  251. package/scripts/test_skill_code_review_seed.py +89 -0
  252. package/scripts/test_skill_frontmatter_lint.py +226 -0
  253. package/scripts/test_skill_gh_lint.py +124 -0
  254. package/scripts/test_skill_language_census.py +254 -0
  255. package/scripts/test_skill_portability_lint.py +773 -0
  256. package/scripts/test_skill_publish_audit.py +300 -0
  257. package/scripts/test_skill_selfcontainment_lint.py +255 -0
  258. package/scripts/test_skill_setup_workflow_seeds.py +200 -0
  259. package/scripts/test_skill_stale_name_lint.py +126 -0
  260. package/scripts/test_skill_surface_refs.py +300 -0
  261. package/scripts/test_skill_trailing_artifact_lint.py +71 -0
  262. package/scripts/wrapup-land.py +573 -0
  263. package/src/cli.mjs +70 -0
  264. package/src/commands/diff.mjs +6 -0
  265. package/src/commands/init.mjs +59 -0
  266. package/src/commands/uninstall.mjs +43 -0
  267. package/src/commands/update.mjs +95 -0
  268. package/src/lib/atomicWrite.mjs +49 -0
  269. package/src/lib/bundle.mjs +133 -0
  270. package/src/lib/hash.mjs +13 -0
  271. package/src/lib/manifest.mjs +58 -0
  272. package/src/lib/sentinel.mjs +20 -0
  273. 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)