Coverage for scripts/check_prompt_wiring.py: 100%

67 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-31 06:18 +0000

1#!/usr/bin/env python3 

2"""Check that the plugin's internal procedures under ``prompts/`` stay wired up. 

3 

4``commands/*.md`` are slash commands the user invokes. ``prompts/*.md`` are 

5**internal procedures** a command reaches with the ``Read`` tool — deliberately 

6outside ``commands/`` so they cannot be invoked directly. Nothing at runtime 

7verifies that arrangement, so a rename or a stray file would only surface as a 

8command failing mid-run, in front of a user. This is that check. 

9 

10It enforces five rules: 

11 

121. every procedure announces itself as **not a slash command**, so a reader (human 

13 or model) opening one mid-task knows it isn't user-facing; 

142. no procedure carries command frontmatter (``allowed-tools``/``argument-hint``), 

15 which would be misleading and hints the file belongs in ``commands/``; 

163. no procedure name collides with a command name; 

174. every ``prompts/<name>.md`` path mentioned in the repo's prose resolves — a 

18 dangling reference is a command that breaks when it reaches that step; 

195. no procedure is orphaned — each is referenced by at least one command or 

20 procedure, and none is invoked "via the Skill tool", which only works for real 

21 commands. 

22 

23Usage: 

24 uv run --python 3.12 --no-project python \ 

25 scripts/check_prompt_wiring.py [--root DIR] 

26 

27Exits 0 when the wiring is sound, 1 (listing every violation) otherwise. 

28""" 

29 

30from __future__ import annotations 

31 

32import argparse 

33import re 

34import sys 

35from pathlib import Path 

36 

37_NOT_A_COMMAND = "Not a slash command" 

38_FRONTMATTER_KEYS = ("allowed-tools:", "argument-hint:") 

39_PROMPT_REF = re.compile(r"prompts/([a-zA-Z0-9_-]+)\.md") 

40_SKILL_INVOCATION = re.compile(r"`([a-zA-Z0-9_-]+)` command via the Skill tool") 

41 

42 

43def _names(directory: Path) -> list[str]: 

44 """Return the sorted stems of the markdown files directly in *directory*.""" 

45 if not directory.is_dir(): 

46 return [] 

47 return sorted(p.stem for p in directory.glob("*.md")) 

48 

49 

50def _prose_files(root: Path) -> list[Path]: 

51 """Return every markdown file whose prompt references should resolve.""" 

52 return sorted( 

53 [ 

54 *(root / "commands").glob("*.md"), 

55 *(root / "prompts").glob("*.md"), 

56 *root.glob("*.md"), 

57 ] 

58 ) 

59 

60 

61def check_declares_internal(prompts_dir: Path) -> list[str]: 

62 """Rule 1: each procedure states that it is not a slash command.""" 

63 return [ 

64 f"prompts/{name}.md does not say {_NOT_A_COMMAND!r}" 

65 for name in _names(prompts_dir) 

66 if _NOT_A_COMMAND not in (prompts_dir / f"{name}.md").read_text() 

67 ] 

68 

69 

70def check_no_command_frontmatter(prompts_dir: Path) -> list[str]: 

71 """Rule 2: procedures carry no command frontmatter.""" 

72 violations = [] 

73 for name in _names(prompts_dir): 

74 text = (prompts_dir / f"{name}.md").read_text() 

75 if text.startswith("---"): 

76 violations.append(f"prompts/{name}.md opens with command frontmatter") 

77 violations += [ 

78 f"prompts/{name}.md declares {key!r}, which only applies to commands" 

79 for key in _FRONTMATTER_KEYS 

80 if key in text 

81 ] 

82 return violations 

83 

84 

85def check_no_name_collisions(commands_dir: Path, prompts_dir: Path) -> list[str]: 

86 """Rule 3: a name is either a command or a procedure, never both.""" 

87 both = sorted(set(_names(commands_dir)) & set(_names(prompts_dir))) 

88 return [f"{name!r} exists as both a command and a procedure" for name in both] 

89 

90 

91def check_references_resolve(root: Path) -> list[str]: 

92 """Rule 4: every ``prompts/<name>.md`` mentioned in prose exists.""" 

93 violations = [] 

94 for path in _prose_files(root): 

95 for name in sorted(set(_PROMPT_REF.findall(path.read_text()))): 

96 if not (root / "prompts" / f"{name}.md").is_file(): 

97 rel = path.relative_to(root) 

98 violations.append(f"{rel} references missing prompts/{name}.md") 

99 return violations 

100 

101 

102def check_no_orphans_and_no_skill_calls(root: Path) -> list[str]: 

103 """Rule 5: every procedure is referenced, and none is invoked as a command.""" 

104 prompts = set(_names(root / "prompts")) 

105 referenced: set[str] = set() 

106 violations = [] 

107 

108 for path in _prose_files(root): 

109 text = path.read_text() 

110 # A file referencing itself doesn't make it reachable. 

111 own = path.stem if path.parent.name == "prompts" else None 

112 referenced |= {name for name in _PROMPT_REF.findall(text) if name != own} 

113 for name in _SKILL_INVOCATION.findall(text): 

114 if name in prompts: 

115 rel = path.relative_to(root) 

116 violations.append( 

117 f"{rel} invokes {name!r} via the Skill tool, but it is a procedure, " 

118 "not a command — reach it with Read" 

119 ) 

120 

121 violations += [ 

122 f"prompts/{name}.md is never referenced — no command can reach it" 

123 for name in sorted(prompts - referenced) 

124 ] 

125 return violations 

126 

127 

128def check_wiring(root: Path) -> list[str]: 

129 """Run every rule against *root*; return all violations.""" 

130 commands_dir, prompts_dir = root / "commands", root / "prompts" 

131 return [ 

132 *check_declares_internal(prompts_dir), 

133 *check_no_command_frontmatter(prompts_dir), 

134 *check_no_name_collisions(commands_dir, prompts_dir), 

135 *check_references_resolve(root), 

136 *check_no_orphans_and_no_skill_calls(root), 

137 ] 

138 

139 

140def main(argv: list[str] | None = None) -> int: 

141 """Entry point: check the wiring and return an exit code.""" 

142 parser = argparse.ArgumentParser(description="Check the plugin's prompt wiring.") 

143 parser.add_argument("--root", default=".", help="Plugin root (default: current directory).") 

144 args = parser.parse_args(argv) 

145 

146 root = Path(args.root).resolve() 

147 violations = check_wiring(root) 

148 if violations: 

149 print("Prompt-wiring check failed:", file=sys.stderr) 

150 for violation in violations: 

151 print(f"{violation}", file=sys.stderr) 

152 return 1 

153 

154 count = len(_names(root / "prompts")) 

155 print(f"prompt wiring is sound ({count} internal procedure(s))") 

156 return 0 

157 

158 

159if __name__ == "__main__": 

160 raise SystemExit(main())