@pennyfarthing/core 10.1.0 → 10.2.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 (407) hide show
  1. package/README.md +13 -18
  2. package/package.json +3 -1
  3. package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js +24 -0
  5. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +101 -15
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js +1 -1
  10. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js.map +1 -1
  11. package/packages/core/dist/cli/commands/e2e-upgrade.test.js +1 -1
  12. package/packages/core/dist/cli/commands/e2e-upgrade.test.js.map +1 -1
  13. package/packages/core/dist/cli/commands/hooks-consolidation.test.js +2 -2
  14. package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -1
  15. package/packages/core/dist/cli/commands/init-consolidation.test.js.map +1 -1
  16. package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
  17. package/packages/core/dist/cli/commands/uninstall.js +24 -13
  18. package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
  19. package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
  20. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  21. package/packages/core/dist/cli/commands/update.js.map +1 -1
  22. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  23. package/packages/core/dist/cli/theme-maker.test.js +64 -115
  24. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  25. package/packages/core/dist/index.d.ts +1 -1
  26. package/packages/core/dist/index.d.ts.map +1 -1
  27. package/packages/core/dist/index.js +2 -2
  28. package/packages/core/dist/index.js.map +1 -1
  29. package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
  30. package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
  31. package/packages/core/dist/plugins/plugin-discovery.js +165 -0
  32. package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
  33. package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
  34. package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
  35. package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
  36. package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
  37. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  38. package/packages/core/dist/workflow/context-watch.d.ts +80 -0
  39. package/packages/core/dist/workflow/context-watch.d.ts.map +1 -0
  40. package/packages/core/dist/workflow/context-watch.js +235 -0
  41. package/packages/core/dist/workflow/context-watch.js.map +1 -0
  42. package/packages/core/dist/workflow/context-watch.test.d.ts +1 -0
  43. package/packages/core/dist/workflow/context-watch.test.d.ts.map +1 -0
  44. package/packages/core/dist/workflow/context-watch.test.js +746 -0
  45. package/packages/core/dist/workflow/context-watch.test.js.map +1 -0
  46. package/packages/core/dist/workflow/file-watch.d.ts +82 -0
  47. package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
  48. package/packages/core/dist/workflow/file-watch.js +198 -0
  49. package/packages/core/dist/workflow/file-watch.js.map +1 -0
  50. package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
  51. package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
  52. package/packages/core/dist/workflow/file-watch.test.js +469 -0
  53. package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
  54. package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
  55. package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
  56. package/packages/core/dist/workflow/observation-writer.js +97 -0
  57. package/packages/core/dist/workflow/observation-writer.js.map +1 -0
  58. package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
  59. package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
  60. package/packages/core/dist/workflow/observation-writer.test.js +424 -0
  61. package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
  62. package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
  63. package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
  64. package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
  65. package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
  66. package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
  67. package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
  68. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
  69. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
  70. package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
  71. package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
  72. package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
  73. package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
  74. package/packages/core/dist/workflow/tool-watch.js +166 -0
  75. package/packages/core/dist/workflow/tool-watch.js.map +1 -0
  76. package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
  77. package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
  78. package/packages/core/dist/workflow/tool-watch.test.js +718 -0
  79. package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
  80. package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
  81. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  82. package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
  83. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  84. package/packages/core/dist/workflow/workflow-schema.js +44 -0
  85. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  86. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  87. package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
  88. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  89. package/pennyfarthing-dist/agents/handoff.md +18 -3
  90. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  91. package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
  92. package/pennyfarthing-dist/agents/sm.md +11 -5
  93. package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
  94. package/pennyfarthing-dist/commands/setup.md +4 -0
  95. package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
  96. package/pennyfarthing-dist/guides/bikelane.md +3 -2
  97. package/pennyfarthing-dist/guides/scale-levels.md +4 -6
  98. package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
  99. package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
  100. package/pennyfarthing-dist/personas/themes/fifth-element.yaml +295 -0
  101. package/pennyfarthing-dist/scripts/README.md +1 -1
  102. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
  103. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +20 -10
  104. package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
  105. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  106. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -189
  107. package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
  108. package/pennyfarthing-dist/skills/skill-registry.yaml +1 -1
  109. package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
  110. package/pennyfarthing-dist/skills/workflow/skill.md +24 -1
  111. package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
  112. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
  113. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
  114. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  122. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  123. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  124. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  125. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  126. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/bellmode_hook.py +202 -47
  128. package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
  129. package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
  130. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/brownfield/cli.py +0 -1
  135. package/pennyfarthing_scripts/brownfield/discover.py +1 -2
  136. package/pennyfarthing_scripts/cli.py +11 -6
  137. package/pennyfarthing_scripts/codemarkers/__init__.py +5 -1
  138. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/codemarkers/analyze.py +177 -2
  145. package/pennyfarthing_scripts/codemarkers/cli.py +50 -0
  146. package/pennyfarthing_scripts/codemarkers/formatters.py +0 -1
  147. package/pennyfarthing_scripts/codemarkers/models.py +15 -0
  148. package/pennyfarthing_scripts/common/__init__.py +8 -9
  149. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  150. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  151. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  152. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  153. package/pennyfarthing_scripts/common/config.py +1 -1
  154. package/pennyfarthing_scripts/complexity/__init__.py +1 -1
  155. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  156. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  157. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  158. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  159. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  160. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/complexity/analyze.py +1 -1
  162. package/pennyfarthing_scripts/complexity/cli.py +5 -1
  163. package/pennyfarthing_scripts/complexity/formatters.py +1 -1
  164. package/pennyfarthing_scripts/context.py +14 -15
  165. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  167. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/deadcode/analyze.py +3 -4
  172. package/pennyfarthing_scripts/deadcode/cli.py +2 -2
  173. package/pennyfarthing_scripts/dependencies/__init__.py +2 -2
  174. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/dependencies/analyze.py +1 -1
  181. package/pennyfarthing_scripts/dependencies/cli.py +8 -4
  182. package/pennyfarthing_scripts/dependencies/formatters.py +1 -1
  183. package/pennyfarthing_scripts/git/__init__.py +5 -5
  184. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  185. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/git/create_branches.py +3 -2
  188. package/pennyfarthing_scripts/git/status_all.py +1 -1
  189. package/pennyfarthing_scripts/healthscore/__init__.py +2 -2
  190. package/pennyfarthing_scripts/healthscore/__main__.py +8 -0
  191. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/healthscore/analyze.py +451 -21
  198. package/pennyfarthing_scripts/healthscore/cli.py +5 -1
  199. package/pennyfarthing_scripts/healthscore/models.py +0 -1
  200. package/pennyfarthing_scripts/hooks.py +8 -11
  201. package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
  202. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/hotspots/analyze.py +128 -14
  209. package/pennyfarthing_scripts/hotspots/cli.py +2 -2
  210. package/pennyfarthing_scripts/hotspots/models.py +0 -1
  211. package/pennyfarthing_scripts/jira/__init__.py +15 -17
  212. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  220. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  222. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/jira/bidirectional.py +2 -3
  225. package/pennyfarthing_scripts/jira/claim.py +21 -0
  226. package/pennyfarthing_scripts/jira/cli.py +2 -2
  227. package/pennyfarthing_scripts/jira/client.py +4 -4
  228. package/pennyfarthing_scripts/jira/create.py +45 -1
  229. package/pennyfarthing_scripts/jira/epic.py +3 -2
  230. package/pennyfarthing_scripts/jira/reconcile.py +0 -1
  231. package/pennyfarthing_scripts/jira/story.py +2 -0
  232. package/pennyfarthing_scripts/jira/sync.py +1 -1
  233. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  238. package/pennyfarthing_scripts/migration/skill.py +0 -1
  239. package/pennyfarthing_scripts/migration/step.py +0 -1
  240. package/pennyfarthing_scripts/migration/validate.py +8 -5
  241. package/pennyfarthing_scripts/patch_mode.py +2 -2
  242. package/pennyfarthing_scripts/preflight/__init__.py +1 -1
  243. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  244. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/preflight/finish.py +0 -1
  248. package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
  249. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/prime/cli.py +5 -1
  258. package/pennyfarthing_scripts/prime/loader.py +2 -3
  259. package/pennyfarthing_scripts/prime/persona.py +2 -1
  260. package/pennyfarthing_scripts/prime/tiers.py +4 -4
  261. package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
  262. package/pennyfarthing_scripts/sprint/__init__.py +10 -12
  263. package/pennyfarthing_scripts/sprint/__main__.py +2 -2
  264. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/sprint/archive.py +0 -1
  281. package/pennyfarthing_scripts/sprint/archive_epic.py +1 -4
  282. package/pennyfarthing_scripts/sprint/cli.py +34 -28
  283. package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
  284. package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
  285. package/pennyfarthing_scripts/sprint/loader.py +6 -0
  286. package/pennyfarthing_scripts/sprint/status.py +1 -2
  287. package/pennyfarthing_scripts/sprint/story_add.py +2 -2
  288. package/pennyfarthing_scripts/sprint/story_finish.py +3 -5
  289. package/pennyfarthing_scripts/sprint/story_update.py +11 -3
  290. package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
  291. package/pennyfarthing_scripts/sprint/validator.py +120 -6
  292. package/pennyfarthing_scripts/sprint/work.py +1 -4
  293. package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
  294. package/pennyfarthing_scripts/story/__init__.py +14 -16
  295. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  296. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  297. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  298. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/story/size.py +0 -1
  302. package/pennyfarthing_scripts/story/template.py +0 -1
  303. package/pennyfarthing_scripts/swebench.py +1 -2
  304. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  305. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  306. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  307. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  308. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  309. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  310. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  311. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  312. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  313. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  314. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  315. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  316. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  317. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  318. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  319. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  320. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  321. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  322. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  323. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  324. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  325. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  326. package/pennyfarthing_scripts/tests/conftest.py +1 -2
  327. package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
  328. package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
  329. package/pennyfarthing_scripts/tests/test_codemarkers.py +13 -8
  330. package/pennyfarthing_scripts/tests/test_common.py +9 -4
  331. package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
  332. package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
  333. package/pennyfarthing_scripts/tests/test_healthscore.py +17 -25
  334. package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
  335. package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
  336. package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
  337. package/pennyfarthing_scripts/tests/test_prime.py +39 -21
  338. package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
  339. package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
  340. package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
  341. package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
  342. package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
  343. package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
  344. package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
  345. package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
  346. package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
  347. package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
  348. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  349. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  350. package/pennyfarthing_scripts/theme/cli.py +3 -2
  351. package/pennyfarthing_scripts/validate/__init__.py +21 -0
  352. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  353. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  354. package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
  355. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  356. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  357. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  358. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  359. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  360. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  361. package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
  362. package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
  363. package/pennyfarthing_scripts/validate/adapters/skill_command.py +292 -0
  364. package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
  365. package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
  366. package/pennyfarthing_scripts/validate/cli.py +141 -0
  367. package/pennyfarthing_scripts/welcome_hook.py +2 -3
  368. package/pennyfarthing_scripts/workflow.py +3 -3
  369. package/scripts/README.md +3 -15
  370. package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
  371. package/pennyfarthing-dist/commands/benchmark.md +0 -485
  372. package/pennyfarthing-dist/commands/job-fair.md +0 -102
  373. package/pennyfarthing-dist/commands/solo.md +0 -447
  374. package/pennyfarthing-dist/guides/benchmarks.md +0 -62
  375. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  376. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
  377. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
  378. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
  379. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
  380. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
  381. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
  382. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
  383. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
  384. package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
  385. package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
  386. package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
  387. package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
  388. package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
  389. package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
  390. package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
  391. package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
  392. package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
  393. package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
  394. package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
  395. package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
  396. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  397. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  398. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  399. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  400. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  401. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  402. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  403. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  404. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  405. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  406. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  407. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -0,0 +1,239 @@
1
+ """Agent definition structural validator adapter.
2
+
3
+ Validates agent definition files in pennyfarthing-dist/agents/.
4
+ Checks required sections, model values, and subagent references.
5
+
6
+ Story: MSSCI-14710 (91-12)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+
14
+ import yaml
15
+
16
+ from pennyfarthing_scripts.validate import ValidateReport
17
+
18
+ VALID_MODELS = {"haiku", "sonnet", "opus"}
19
+ BUILTIN_AGENTS = {"Explore", "Plan"}
20
+
21
+ # Regex to find XML-like tags in markdown: <tagname> or <tag-name>
22
+ _TAG_RE = re.compile(r"<([a-zA-Z][a-zA-Z0-9_-]*)(?:\s[^>]*)?>", re.MULTILINE)
23
+
24
+ # Regex to extract **Model:** value from helpers section
25
+ _MODEL_RE = re.compile(r"\*\*Model:\*\*\s+(\S+)", re.IGNORECASE)
26
+
27
+ # Regex to extract subagent names from markdown table rows: | `name` | purpose |
28
+ _HELPER_TABLE_RE = re.compile(r"^\s*\|\s*`?([^`|]+)`?\s*\|", re.MULTILINE)
29
+
30
+
31
+ def _has_frontmatter(content: str) -> bool:
32
+ """Check if content starts with YAML frontmatter (--- delimited)."""
33
+ return content.startswith("---\n")
34
+
35
+
36
+ def _parse_frontmatter(content: str) -> dict:
37
+ """Extract YAML frontmatter from content."""
38
+ if not _has_frontmatter(content):
39
+ return {}
40
+ end = content.find("\n---", 3)
41
+ if end == -1:
42
+ return {}
43
+ fm_text = content[4:end]
44
+ try:
45
+ return yaml.safe_load(fm_text) or {}
46
+ except yaml.YAMLError:
47
+ return {}
48
+
49
+
50
+ def _find_tags(content: str) -> set[str]:
51
+ """Find all XML-like tag names in content."""
52
+ return {m.group(1) for m in _TAG_RE.finditer(content)}
53
+
54
+
55
+ def _extract_section(content: str, tag: str) -> str | None:
56
+ """Extract content between <tag> and </tag>."""
57
+ pattern = re.compile(rf"<{re.escape(tag)}[^>]*>(.*?)</{re.escape(tag)}>", re.DOTALL)
58
+ m = pattern.search(content)
59
+ return m.group(1) if m else None
60
+
61
+
62
+ def classify_agent_files(
63
+ agents_dir: Path,
64
+ ) -> tuple[list[Path], list[Path], list[Path]]:
65
+ """Classify agent files into main agents, subagents, and skipped.
66
+
67
+ Returns:
68
+ (main_agents, subagents, skipped) — three lists of Path objects.
69
+ """
70
+ main: list[Path] = []
71
+ sub: list[Path] = []
72
+ skipped: list[Path] = []
73
+
74
+ for f in sorted(agents_dir.glob("*.md")):
75
+ if f.name == "README.md":
76
+ skipped.append(f)
77
+ continue
78
+
79
+ content = f.read_text()
80
+ if _has_frontmatter(content):
81
+ sub.append(f)
82
+ else:
83
+ main.append(f)
84
+
85
+ return main, sub, skipped
86
+
87
+
88
+ def validate_main_agent(
89
+ path: Path, agents_dir: Path
90
+ ) -> tuple[list[str], list[str]]:
91
+ """Validate a main agent definition file.
92
+
93
+ Returns:
94
+ (errors, warnings) — two lists of message strings.
95
+ """
96
+ errors: list[str] = []
97
+ warnings: list[str] = []
98
+ content = path.read_text()
99
+ tags = _find_tags(content)
100
+
101
+ # Required sections
102
+ if "role" not in tags:
103
+ errors.append("Missing required <role> section")
104
+ if "critical" not in tags:
105
+ errors.append("Missing required <critical> section")
106
+ if "helpers" not in tags:
107
+ errors.append("Missing required <helpers> section")
108
+ if "skills" not in tags:
109
+ errors.append("Missing required <skills> section")
110
+
111
+ # Model validation (only if helpers exists)
112
+ if "helpers" in tags:
113
+ helpers_content = _extract_section(content, "helpers")
114
+ if helpers_content:
115
+ model_match = _MODEL_RE.search(helpers_content)
116
+ if model_match:
117
+ model_val = model_match.group(1).lower()
118
+ if model_val not in VALID_MODELS:
119
+ errors.append(
120
+ f"Invalid model '{model_match.group(1)}' in <helpers> "
121
+ f"(must be one of: {', '.join(sorted(VALID_MODELS))})"
122
+ )
123
+
124
+ # Subagent reference validation
125
+ existing_files = {f.stem for f in agents_dir.glob("*.md") if f.name != "README.md"}
126
+ # Parse table rows — skip header/separator rows
127
+ for line in helpers_content.splitlines():
128
+ m = _HELPER_TABLE_RE.match(line)
129
+ if not m:
130
+ continue
131
+ name = m.group(1).strip().strip("`")
132
+ # Skip table header and separator
133
+ if name.lower() in ("subagent", "---", "") or name.startswith("-"):
134
+ continue
135
+ if name in BUILTIN_AGENTS:
136
+ continue
137
+ if name not in existing_files:
138
+ warnings.append(
139
+ f"Helper references '{name}' but no matching agent file not found in agents/"
140
+ )
141
+
142
+ # Recommended sections (warnings)
143
+ if "on-activation" not in tags:
144
+ warnings.append("Missing recommended <on-activation> section")
145
+ if "exit" not in tags and "exit-sequence" not in tags:
146
+ warnings.append("Missing recommended <exit> or <exit-sequence> section")
147
+
148
+ return errors, warnings
149
+
150
+
151
+ def validate_subagent(path: Path) -> tuple[list[str], list[str]]:
152
+ """Validate a subagent definition file.
153
+
154
+ Returns:
155
+ (errors, warnings) — two lists of message strings.
156
+ """
157
+ errors: list[str] = []
158
+ warnings: list[str] = []
159
+ content = path.read_text()
160
+
161
+ # Frontmatter validation
162
+ fm = _parse_frontmatter(content)
163
+ if not fm:
164
+ errors.append("Missing or invalid YAML frontmatter")
165
+ return errors, warnings
166
+
167
+ for field in ("name", "description", "tools", "model"):
168
+ if field not in fm:
169
+ errors.append(f"Missing required frontmatter field: {field}")
170
+
171
+ # Model must be haiku for subagents
172
+ if "model" in fm:
173
+ model_val = str(fm["model"]).lower()
174
+ if model_val != "haiku":
175
+ errors.append(
176
+ f"Subagent model must be 'haiku', got '{fm['model']}'"
177
+ )
178
+
179
+ # Required tags
180
+ tags = _find_tags(content)
181
+ if "output" not in tags:
182
+ errors.append("Missing required <output> section")
183
+
184
+ # Recommended tags
185
+ if "arguments" not in tags:
186
+ warnings.append("Missing recommended <arguments> section")
187
+
188
+ return errors, warnings
189
+
190
+
191
+ def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
192
+ """Validate all agent definition files."""
193
+ report = ValidateReport(validator="agent")
194
+ agents_dir = root / "pennyfarthing-dist" / "agents"
195
+
196
+ if not agents_dir.is_dir():
197
+ report.details.append("[ERROR] agents directory not found")
198
+ report.errors += 1
199
+ return report
200
+
201
+ main_agents, subagents, _skipped = classify_agent_files(agents_dir)
202
+
203
+ for path in main_agents:
204
+ file_errors, file_warnings = validate_main_agent(path, agents_dir)
205
+
206
+ for e in file_errors:
207
+ report.errors += 1
208
+ report.details.append(f"[ERROR] {path.name}: {e}")
209
+
210
+ for w in file_warnings:
211
+ if strict:
212
+ report.errors += 1
213
+ report.details.append(f"[ERROR] {path.name}: {w}")
214
+ else:
215
+ report.warnings += 1
216
+ report.details.append(f"[WARN] {path.name}: {w}")
217
+
218
+ if not file_errors:
219
+ report.passed += 1
220
+
221
+ for path in subagents:
222
+ file_errors, file_warnings = validate_subagent(path)
223
+
224
+ for e in file_errors:
225
+ report.errors += 1
226
+ report.details.append(f"[ERROR] {path.name}: {e}")
227
+
228
+ for w in file_warnings:
229
+ if strict:
230
+ report.errors += 1
231
+ report.details.append(f"[ERROR] {path.name}: {w}")
232
+ else:
233
+ report.warnings += 1
234
+ report.details.append(f"[WARN] {path.name}: {w}")
235
+
236
+ if not file_errors:
237
+ report.passed += 1
238
+
239
+ return report
@@ -0,0 +1,30 @@
1
+ """XML schema validator adapter.
2
+
3
+ Delegates to migration/validate.validate_all() for session, skill, and workflow step files.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from pennyfarthing_scripts.validate import ValidateReport
11
+
12
+
13
+ def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
14
+ """Validate XML schema files (sessions, skills, workflow steps)."""
15
+ from pennyfarthing_scripts.migration.validate import validate_all
16
+
17
+ summary = validate_all(root, file_type="all", strict=strict)
18
+
19
+ report = ValidateReport(validator="schema")
20
+ report.passed = summary.passed
21
+ report.warnings = summary.warnings
22
+ report.errors = summary.errors
23
+
24
+ for result in summary.results:
25
+ for e in result.errors:
26
+ report.details.append(f"[ERROR] {result.file_path.name}: {e}")
27
+ for w in result.warnings:
28
+ report.details.append(f"[WARN] {result.file_path.name}: {w}")
29
+
30
+ return report
@@ -0,0 +1,292 @@
1
+ """Skill registry and command file structural validator adapter.
2
+
3
+ Validates skill-registry.yaml against skill-registry.schema.json and
4
+ command file structure in pennyfarthing-dist/commands/.
5
+
6
+ Story: MSSCI-14711 (91-13)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from pathlib import Path
14
+
15
+ import yaml
16
+
17
+ from pennyfarthing_scripts.validate import ValidateReport
18
+
19
+ _SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
20
+
21
+
22
+ def discover_skill_registry(root: Path) -> Path | None:
23
+ """Locate skill-registry.yaml in the project.
24
+
25
+ Returns:
26
+ Path to skill-registry.yaml, or None if not found.
27
+ """
28
+ path = root / "pennyfarthing-dist" / "skills" / "skill-registry.yaml"
29
+ return path if path.is_file() else None
30
+
31
+
32
+ def discover_command_files(commands_dir: Path) -> list[Path]:
33
+ """Discover all command markdown files.
34
+
35
+ Returns:
36
+ Sorted list of Path objects for command .md files.
37
+ """
38
+ if not commands_dir.is_dir():
39
+ return []
40
+ return sorted(f for f in commands_dir.glob("*.md"))
41
+
42
+
43
+ def _load_schema(root: Path) -> dict | None:
44
+ """Load the skill-registry.schema.json file."""
45
+ schema_path = root / "pennyfarthing-dist" / "skills" / "skill-registry.schema.json"
46
+ if not schema_path.is_file():
47
+ return None
48
+ try:
49
+ return json.loads(schema_path.read_text())
50
+ except json.JSONDecodeError:
51
+ return None
52
+
53
+
54
+ def _validate_against_schema(
55
+ data: dict, schema: dict
56
+ ) -> list[str]:
57
+ """Validate data against a JSON Schema (manual implementation).
58
+
59
+ Handles the subset of JSON Schema used by skill-registry.schema.json:
60
+ - type checking (object, string, array, boolean)
61
+ - required fields
62
+ - additionalProperties: false
63
+ - pattern (regex)
64
+ - enum
65
+ - $ref to #/definitions/*
66
+ - items (for arrays)
67
+
68
+ Returns:
69
+ List of error message strings.
70
+ """
71
+ errors: list[str] = []
72
+ definitions = schema.get("definitions", {})
73
+
74
+ def _resolve_ref(ref: str) -> dict:
75
+ """Resolve a $ref pointer like #/definitions/skill."""
76
+ parts = ref.lstrip("#/").split("/")
77
+ result = schema
78
+ for part in parts:
79
+ result = result.get(part, {})
80
+ return result
81
+
82
+ def _validate_value(value, prop_schema: dict, path: str) -> None:
83
+ """Validate a single value against its schema."""
84
+ # Handle $ref
85
+ if "$ref" in prop_schema:
86
+ prop_schema = _resolve_ref(prop_schema["$ref"])
87
+
88
+ expected_type = prop_schema.get("type")
89
+
90
+ # Type checking
91
+ if expected_type == "object":
92
+ if not isinstance(value, dict):
93
+ errors.append(f"{path}: expected object, got {type(value).__name__}")
94
+ return
95
+
96
+ props = prop_schema.get("properties", {})
97
+ additional = prop_schema.get("additionalProperties")
98
+
99
+ # Required fields
100
+ for req in prop_schema.get("required", []):
101
+ if req not in value:
102
+ errors.append(f"{path}: missing required field '{req}'")
103
+
104
+ # Validate known properties
105
+ for key, val in value.items():
106
+ if key in props:
107
+ _validate_value(val, props[key], f"{path}.{key}")
108
+ elif additional is False:
109
+ errors.append(
110
+ f"{path}: additional property '{key}' not allowed"
111
+ )
112
+ elif isinstance(additional, dict):
113
+ _validate_value(val, additional, f"{path}.{key}")
114
+
115
+ elif expected_type == "string":
116
+ if not isinstance(value, str):
117
+ errors.append(f"{path}: expected string, got {type(value).__name__}")
118
+ return
119
+ pattern = prop_schema.get("pattern")
120
+ if pattern and not re.match(pattern, value):
121
+ errors.append(f"{path}: value '{value}' does not match pattern '{pattern}'")
122
+ enum_vals = prop_schema.get("enum")
123
+ if enum_vals and value not in enum_vals:
124
+ errors.append(
125
+ f"{path}: value '{value}' not in allowed values: "
126
+ f"{', '.join(enum_vals)}"
127
+ )
128
+
129
+ elif expected_type == "array":
130
+ if not isinstance(value, list):
131
+ errors.append(f"{path}: expected array, got {type(value).__name__}")
132
+ return
133
+ items_schema = prop_schema.get("items")
134
+ if items_schema:
135
+ for i, item in enumerate(value):
136
+ _validate_value(item, items_schema, f"{path}[{i}]")
137
+
138
+ elif expected_type == "boolean":
139
+ if not isinstance(value, bool):
140
+ errors.append(f"{path}: expected boolean, got {type(value).__name__}")
141
+
142
+ _validate_value(data, schema, "")
143
+ return errors
144
+
145
+
146
+ def validate_skill_registry(root: Path) -> tuple[list[str], list[str]]:
147
+ """Validate skill-registry.yaml against its JSON schema.
148
+
149
+ Returns:
150
+ (errors, warnings) — two lists of message strings.
151
+ """
152
+ errors: list[str] = []
153
+ warnings: list[str] = []
154
+
155
+ registry_path = discover_skill_registry(root)
156
+ if registry_path is None:
157
+ errors.append("skill-registry.yaml not found")
158
+ return errors, warnings
159
+
160
+ schema = _load_schema(root)
161
+ if schema is None:
162
+ errors.append("skill-registry.schema.json not found or invalid")
163
+ return errors, warnings
164
+
165
+ try:
166
+ content = registry_path.read_text()
167
+ data = yaml.safe_load(content)
168
+ except yaml.YAMLError:
169
+ errors.append("skill-registry.yaml: YAML parse error")
170
+ return errors, warnings
171
+
172
+ if not isinstance(data, dict):
173
+ errors.append("skill-registry.yaml: expected a YAML mapping")
174
+ return errors, warnings
175
+
176
+ schema_errors = _validate_against_schema(data, schema)
177
+ errors.extend(schema_errors)
178
+
179
+ return errors, warnings
180
+
181
+
182
+ def _has_frontmatter(content: str) -> bool:
183
+ """Check if content starts with YAML frontmatter (--- delimited)."""
184
+ return content.startswith("---\n")
185
+
186
+
187
+ def _parse_frontmatter(content: str) -> dict | None:
188
+ """Extract YAML frontmatter from content.
189
+
190
+ Returns:
191
+ Parsed dict, or None if no valid frontmatter.
192
+ """
193
+ if not _has_frontmatter(content):
194
+ return None
195
+ end = content.find("\n---", 3)
196
+ if end == -1:
197
+ return None
198
+ fm_text = content[4:end]
199
+ try:
200
+ return yaml.safe_load(fm_text) or {}
201
+ except yaml.YAMLError:
202
+ return None
203
+
204
+
205
+ def _get_body(content: str) -> str:
206
+ """Extract body content after frontmatter."""
207
+ if not _has_frontmatter(content):
208
+ return content
209
+ end = content.find("\n---", 3)
210
+ if end == -1:
211
+ return ""
212
+ return content[end + 4:].strip()
213
+
214
+
215
+ def validate_command_file(path: Path) -> tuple[list[str], list[str]]:
216
+ """Validate a command markdown file.
217
+
218
+ Checks:
219
+ - YAML frontmatter present
220
+ - description field in frontmatter (non-empty)
221
+ - Body content not empty (warning)
222
+
223
+ Returns:
224
+ (errors, warnings) — two lists of message strings.
225
+ """
226
+ errors: list[str] = []
227
+ warnings: list[str] = []
228
+ content = path.read_text()
229
+
230
+ fm = _parse_frontmatter(content)
231
+ if fm is None:
232
+ errors.append("Missing YAML frontmatter")
233
+ return errors, warnings
234
+
235
+ desc = fm.get("description")
236
+ if desc is None:
237
+ errors.append("Missing required frontmatter field: description")
238
+ elif not isinstance(desc, str) or not desc.strip():
239
+ errors.append("Frontmatter description must be a non-empty string")
240
+
241
+ body = _get_body(content)
242
+ if not body:
243
+ warnings.append("Command body is empty — consider adding content")
244
+
245
+ return errors, warnings
246
+
247
+
248
+ def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
249
+ """Validate skill registry and command files."""
250
+ report = ValidateReport(validator="skill-command")
251
+
252
+ # --- Skill registry validation ---
253
+ registry_errors, registry_warnings = validate_skill_registry(root)
254
+
255
+ for e in registry_errors:
256
+ report.errors += 1
257
+ report.details.append(f"[ERROR] skill-registry.yaml: {e}")
258
+
259
+ for w in registry_warnings:
260
+ if strict:
261
+ report.errors += 1
262
+ report.details.append(f"[ERROR] skill-registry.yaml: {w}")
263
+ else:
264
+ report.warnings += 1
265
+ report.details.append(f"[WARN] skill-registry.yaml: {w}")
266
+
267
+ if not registry_errors:
268
+ report.passed += 1
269
+
270
+ # --- Command file validation ---
271
+ commands_dir = root / "pennyfarthing-dist" / "commands"
272
+ command_files = discover_command_files(commands_dir)
273
+
274
+ for path in command_files:
275
+ file_errors, file_warnings = validate_command_file(path)
276
+
277
+ for e in file_errors:
278
+ report.errors += 1
279
+ report.details.append(f"[ERROR] {path.name}: {e}")
280
+
281
+ for w in file_warnings:
282
+ if strict:
283
+ report.errors += 1
284
+ report.details.append(f"[ERROR] {path.name}: {w}")
285
+ else:
286
+ report.warnings += 1
287
+ report.details.append(f"[WARN] {path.name}: {w}")
288
+
289
+ if not file_errors:
290
+ report.passed += 1
291
+
292
+ return report
@@ -0,0 +1,69 @@
1
+ """Sprint YAML validator adapter.
2
+
3
+ Auto-discovers sprint files and delegates to sprint/validate_cmd.validate_sprint_yaml().
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from pennyfarthing_scripts.validate import ValidateReport
11
+
12
+
13
+ def _discover_files(root: Path) -> list[Path]:
14
+ """Find all validatable YAML in sprint/."""
15
+ sprint_dir = root / "sprint"
16
+ if not sprint_dir.is_dir():
17
+ return []
18
+
19
+ files: list[Path] = []
20
+
21
+ cs = sprint_dir / "current-sprint.yaml"
22
+ if cs.exists():
23
+ files.append(cs)
24
+
25
+ files.extend(sorted(sprint_dir.glob("epic-*.yaml")))
26
+ files.extend(sorted(sprint_dir.glob("initiative-*.yaml")))
27
+
28
+ future = sprint_dir / "future.yaml"
29
+ if future.exists():
30
+ files.append(future)
31
+
32
+ return files
33
+
34
+
35
+ def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
36
+ """Validate all sprint YAML files."""
37
+ from pennyfarthing_scripts.sprint.validate_cmd import validate_sprint_yaml
38
+
39
+ report = ValidateReport(validator="sprint")
40
+ files = _discover_files(root)
41
+
42
+ for path in files:
43
+ result = validate_sprint_yaml(path, fix=fix)
44
+
45
+ if result.errors:
46
+ report.errors += len(result.errors)
47
+ for err in result.errors:
48
+ line_info = f" (line {err.line})" if err.line else ""
49
+ report.details.append(
50
+ f"[{err.category.upper()}] {path.name}: {err.message}{line_info}"
51
+ )
52
+
53
+ if result.format_issues:
54
+ for issue in result.format_issues:
55
+ if strict:
56
+ report.errors += 1
57
+ else:
58
+ report.warnings += 1
59
+ report.details.append(
60
+ f"[FORMAT] {path.name}: {issue.message}"
61
+ )
62
+
63
+ if result.valid and not result.errors:
64
+ report.passed += 1
65
+
66
+ if result.fixed:
67
+ report.fixed = True
68
+
69
+ return report