@pennyfarthing/core 10.1.0 → 10.3.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 (422) hide show
  1. package/README.md +22 -24
  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 +2 -2
  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 +2 -2
  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/theme.js +1 -1
  17. package/packages/core/dist/cli/commands/theme.js.map +1 -1
  18. package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
  19. package/packages/core/dist/cli/commands/uninstall.js +24 -13
  20. package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
  21. package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
  22. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  23. package/packages/core/dist/cli/commands/update.js.map +1 -1
  24. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  25. package/packages/core/dist/cli/theme-maker.test.js +64 -115
  26. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  27. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
  28. package/packages/core/dist/cli/utils/themes.js +3 -2
  29. package/packages/core/dist/cli/utils/themes.js.map +1 -1
  30. package/packages/core/dist/index.d.ts +1 -1
  31. package/packages/core/dist/index.d.ts.map +1 -1
  32. package/packages/core/dist/index.js +2 -2
  33. package/packages/core/dist/index.js.map +1 -1
  34. package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
  35. package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
  36. package/packages/core/dist/plugins/plugin-discovery.js +165 -0
  37. package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
  38. package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
  39. package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
  40. package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
  41. package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
  42. package/packages/core/dist/scripts/add-ocean-profiles.js +1 -1
  43. package/packages/core/dist/scripts/add-ocean-profiles.js.map +1 -1
  44. package/packages/core/dist/scripts/generate-all-spiders.js +2 -0
  45. package/packages/core/dist/scripts/generate-all-spiders.js.map +1 -1
  46. package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
  47. package/packages/core/dist/scripts/generate-report.js +2 -0
  48. package/packages/core/dist/scripts/generate-report.js.map +1 -1
  49. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  50. package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
  51. package/packages/core/dist/scripts/generate-spider.js +2 -0
  52. package/packages/core/dist/scripts/generate-spider.js.map +1 -1
  53. package/packages/core/dist/scripts/validate-ocean-profiles.js +1 -1
  54. package/packages/core/dist/scripts/validate-ocean-profiles.js.map +1 -1
  55. package/packages/core/dist/workflow/file-watch.d.ts +82 -0
  56. package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
  57. package/packages/core/dist/workflow/file-watch.js +198 -0
  58. package/packages/core/dist/workflow/file-watch.js.map +1 -0
  59. package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
  60. package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
  61. package/packages/core/dist/workflow/file-watch.test.js +469 -0
  62. package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
  63. package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
  64. package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
  65. package/packages/core/dist/workflow/observation-writer.js +97 -0
  66. package/packages/core/dist/workflow/observation-writer.js.map +1 -0
  67. package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
  68. package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
  69. package/packages/core/dist/workflow/observation-writer.test.js +424 -0
  70. package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
  71. package/packages/core/dist/workflow/output-path-normalizer.d.ts +47 -0
  72. package/packages/core/dist/workflow/output-path-normalizer.d.ts.map +1 -0
  73. package/packages/core/dist/workflow/output-path-normalizer.js +79 -0
  74. package/packages/core/dist/workflow/output-path-normalizer.js.map +1 -0
  75. package/packages/core/dist/workflow/output-path-normalizer.test.d.ts +16 -0
  76. package/packages/core/dist/workflow/output-path-normalizer.test.d.ts.map +1 -0
  77. package/packages/core/dist/workflow/output-path-normalizer.test.js +157 -0
  78. package/packages/core/dist/workflow/output-path-normalizer.test.js.map +1 -0
  79. package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
  80. package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
  81. package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
  82. package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
  83. package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
  84. package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
  85. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
  86. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
  87. package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
  88. package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
  89. package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
  90. package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
  91. package/packages/core/dist/workflow/tool-watch.js +166 -0
  92. package/packages/core/dist/workflow/tool-watch.js.map +1 -0
  93. package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
  94. package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
  95. package/packages/core/dist/workflow/tool-watch.test.js +717 -0
  96. package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
  97. package/packages/core/dist/workflow/variable-resolver.js +1 -1
  98. package/packages/core/dist/workflow/variable-resolver.js.map +1 -1
  99. package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
  100. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  101. package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
  102. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  103. package/packages/core/dist/workflow/workflow-schema.js +44 -0
  104. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  105. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  106. package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
  107. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  108. package/pennyfarthing-dist/agents/README.md +3 -1
  109. package/pennyfarthing-dist/agents/ba.md +165 -0
  110. package/pennyfarthing-dist/agents/handoff.md +18 -3
  111. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  112. package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
  113. package/pennyfarthing-dist/agents/sm.md +11 -5
  114. package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
  115. package/pennyfarthing-dist/commands/ba.md +17 -0
  116. package/pennyfarthing-dist/commands/setup.md +4 -0
  117. package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
  118. package/pennyfarthing-dist/guides/bikelane.md +3 -2
  119. package/pennyfarthing-dist/guides/scale-levels.md +4 -6
  120. package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
  121. package/pennyfarthing-dist/guides/workflow-schema.md +1 -1
  122. package/pennyfarthing-dist/personas/themes/a-team.yaml +30 -0
  123. package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +30 -0
  124. package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +30 -0
  125. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +30 -0
  126. package/pennyfarthing-dist/personas/themes/catch-22.yaml +30 -0
  127. package/pennyfarthing-dist/personas/themes/control.yaml +30 -0
  128. package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +31 -0
  129. package/pennyfarthing-dist/personas/themes/discworld.yaml +32 -1
  130. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +31 -0
  131. package/pennyfarthing-dist/personas/themes/dune.yaml +32 -0
  132. package/pennyfarthing-dist/personas/themes/fifth-element.yaml +327 -0
  133. package/pennyfarthing-dist/personas/themes/firefly.yaml +31 -0
  134. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +30 -0
  135. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +30 -0
  136. package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +30 -0
  137. package/pennyfarthing-dist/personas/themes/lord-of-the-rings.yaml +30 -0
  138. package/pennyfarthing-dist/personas/themes/mad-max.yaml +30 -0
  139. package/pennyfarthing-dist/personas/themes/mash.yaml +33 -0
  140. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +34 -0
  141. package/pennyfarthing-dist/personas/themes/sandman.yaml +33 -0
  142. package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +34 -0
  143. package/pennyfarthing-dist/personas/themes/star-wars.yaml +33 -0
  144. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +30 -0
  145. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +30 -0
  146. package/pennyfarthing-dist/personas/themes/watchmen.yaml +30 -0
  147. package/pennyfarthing-dist/personas/themes/west-wing.yaml +30 -0
  148. package/pennyfarthing-dist/personas/themes/x-files.yaml +30 -0
  149. package/pennyfarthing-dist/scripts/README.md +1 -1
  150. package/pennyfarthing-dist/scripts/core/agent-session.sh +1 -1
  151. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
  152. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +20 -10
  153. package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
  154. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +2 -2
  155. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +1 -0
  156. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  157. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -189
  158. package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
  159. package/pennyfarthing-dist/skills/skill-registry.yaml +1 -1
  160. package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
  161. package/pennyfarthing-dist/skills/theme/skill.md +1 -1
  162. package/pennyfarthing-dist/skills/workflow/skill.md +24 -1
  163. package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
  164. package/pennyfarthing-dist/workflows/architecture.yaml +2 -2
  165. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
  166. package/pennyfarthing-dist/workflows/epics-and-stories/workflow.yaml +2 -2
  167. package/pennyfarthing-dist/workflows/implementation-readiness/workflow.yaml +2 -2
  168. package/pennyfarthing-dist/workflows/prd/workflow.yaml +2 -2
  169. package/pennyfarthing-dist/workflows/product-brief/workflow.yaml +2 -2
  170. package/pennyfarthing-dist/workflows/project-context/workflow.yaml +2 -2
  171. package/pennyfarthing-dist/workflows/quick-dev/workflow.yaml +2 -2
  172. package/pennyfarthing-dist/workflows/research/workflow.yaml +2 -2
  173. package/pennyfarthing-dist/workflows/retrospective/workflow.yaml +1 -1
  174. package/pennyfarthing-dist/workflows/sprint-planning/workflow.yaml +3 -3
  175. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
  176. package/pennyfarthing-dist/workflows/ux-design/workflow.yaml +2 -2
  177. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/bellmode_hook.py +202 -47
  183. package/pennyfarthing_scripts/bikerack/__init__.py +36 -0
  184. package/pennyfarthing_scripts/bikerack/__main__.py +5 -0
  185. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/bikerack/cli.py +148 -0
  190. package/pennyfarthing_scripts/bikerack/launcher.py +181 -0
  191. package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
  192. package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
  193. package/pennyfarthing_scripts/brownfield/cli.py +0 -1
  194. package/pennyfarthing_scripts/brownfield/discover.py +1 -2
  195. package/pennyfarthing_scripts/cli.py +16 -6
  196. package/pennyfarthing_scripts/codemarkers/__init__.py +5 -1
  197. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/codemarkers/analyze.py +177 -2
  204. package/pennyfarthing_scripts/codemarkers/cli.py +50 -0
  205. package/pennyfarthing_scripts/codemarkers/formatters.py +0 -1
  206. package/pennyfarthing_scripts/codemarkers/models.py +15 -0
  207. package/pennyfarthing_scripts/common/__init__.py +8 -9
  208. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/common/config.py +1 -1
  211. package/pennyfarthing_scripts/complexity/__init__.py +1 -1
  212. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/complexity/analyze.py +1 -1
  219. package/pennyfarthing_scripts/complexity/cli.py +5 -1
  220. package/pennyfarthing_scripts/complexity/formatters.py +1 -1
  221. package/pennyfarthing_scripts/context.py +14 -15
  222. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/deadcode/analyze.py +3 -4
  229. package/pennyfarthing_scripts/deadcode/cli.py +2 -2
  230. package/pennyfarthing_scripts/dependencies/__init__.py +2 -2
  231. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/dependencies/analyze.py +1 -1
  238. package/pennyfarthing_scripts/dependencies/cli.py +8 -4
  239. package/pennyfarthing_scripts/dependencies/formatters.py +1 -1
  240. package/pennyfarthing_scripts/git/__init__.py +5 -5
  241. package/pennyfarthing_scripts/git/create_branches.py +3 -2
  242. package/pennyfarthing_scripts/git/status_all.py +1 -1
  243. package/pennyfarthing_scripts/healthscore/__init__.py +2 -2
  244. package/pennyfarthing_scripts/healthscore/__main__.py +8 -0
  245. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/healthscore/analyze.py +452 -21
  252. package/pennyfarthing_scripts/healthscore/cli.py +5 -1
  253. package/pennyfarthing_scripts/healthscore/models.py +0 -1
  254. package/pennyfarthing_scripts/hooks.py +8 -11
  255. package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
  256. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/hotspots/analyze.py +128 -14
  261. package/pennyfarthing_scripts/hotspots/cli.py +2 -2
  262. package/pennyfarthing_scripts/hotspots/models.py +0 -1
  263. package/pennyfarthing_scripts/jira/__init__.py +15 -17
  264. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/jira/bidirectional.py +2 -3
  275. package/pennyfarthing_scripts/jira/claim.py +21 -0
  276. package/pennyfarthing_scripts/jira/cli.py +2 -2
  277. package/pennyfarthing_scripts/jira/client.py +4 -4
  278. package/pennyfarthing_scripts/jira/create.py +45 -1
  279. package/pennyfarthing_scripts/jira/epic.py +3 -2
  280. package/pennyfarthing_scripts/jira/reconcile.py +0 -1
  281. package/pennyfarthing_scripts/jira/story.py +2 -0
  282. package/pennyfarthing_scripts/jira/sync.py +1 -1
  283. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/migration/skill.py +0 -1
  289. package/pennyfarthing_scripts/migration/step.py +0 -1
  290. package/pennyfarthing_scripts/migration/validate.py +8 -5
  291. package/pennyfarthing_scripts/patch_mode.py +2 -2
  292. package/pennyfarthing_scripts/preflight/__init__.py +1 -1
  293. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  294. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  295. package/pennyfarthing_scripts/preflight/finish.py +0 -1
  296. package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
  297. package/pennyfarthing_scripts/prime/__init__.py +2 -0
  298. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/prime/cli.py +18 -1
  304. package/pennyfarthing_scripts/prime/loader.py +72 -3
  305. package/pennyfarthing_scripts/prime/persona.py +4 -2
  306. package/pennyfarthing_scripts/prime/tiers.py +17 -4
  307. package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
  308. package/pennyfarthing_scripts/sprint/__init__.py +10 -12
  309. package/pennyfarthing_scripts/sprint/__main__.py +2 -2
  310. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  315. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  316. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  317. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  318. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  319. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  320. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  323. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  324. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/sprint/archive.py +0 -1
  326. package/pennyfarthing_scripts/sprint/archive_epic.py +1 -4
  327. package/pennyfarthing_scripts/sprint/cli.py +34 -28
  328. package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
  329. package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
  330. package/pennyfarthing_scripts/sprint/loader.py +6 -0
  331. package/pennyfarthing_scripts/sprint/status.py +1 -2
  332. package/pennyfarthing_scripts/sprint/story_add.py +2 -2
  333. package/pennyfarthing_scripts/sprint/story_finish.py +3 -5
  334. package/pennyfarthing_scripts/sprint/story_update.py +11 -3
  335. package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
  336. package/pennyfarthing_scripts/sprint/validator.py +120 -6
  337. package/pennyfarthing_scripts/sprint/work.py +1 -4
  338. package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
  339. package/pennyfarthing_scripts/story/__init__.py +14 -16
  340. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/story/size.py +0 -1
  344. package/pennyfarthing_scripts/story/template.py +0 -1
  345. package/pennyfarthing_scripts/swebench.py +1 -2
  346. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  347. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  348. package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/conftest.py +1 -2
  353. package/pennyfarthing_scripts/tests/test_bikerack.py +785 -0
  354. package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
  355. package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
  356. package/pennyfarthing_scripts/tests/test_codemarkers.py +13 -8
  357. package/pennyfarthing_scripts/tests/test_common.py +9 -4
  358. package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
  359. package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
  360. package/pennyfarthing_scripts/tests/test_healthscore.py +17 -25
  361. package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
  362. package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
  363. package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
  364. package/pennyfarthing_scripts/tests/test_prime.py +39 -21
  365. package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
  366. package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
  367. package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
  368. package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
  369. package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
  370. package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
  371. package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
  372. package/pennyfarthing_scripts/tests/test_topology_loader.py +620 -0
  373. package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
  374. package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
  375. package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
  376. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  377. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  378. package/pennyfarthing_scripts/theme/cli.py +3 -2
  379. package/pennyfarthing_scripts/validate/__init__.py +21 -0
  380. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  381. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  382. package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
  383. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  384. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  385. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  386. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  387. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  388. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  389. package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
  390. package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
  391. package/pennyfarthing_scripts/validate/adapters/skill_command.py +291 -0
  392. package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
  393. package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
  394. package/pennyfarthing_scripts/validate/cli.py +141 -0
  395. package/pennyfarthing_scripts/welcome_hook.py +2 -3
  396. package/pennyfarthing_scripts/workflow.py +3 -3
  397. package/scripts/README.md +3 -15
  398. package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
  399. package/pennyfarthing-dist/commands/benchmark.md +0 -485
  400. package/pennyfarthing-dist/commands/job-fair.md +0 -102
  401. package/pennyfarthing-dist/commands/solo.md +0 -447
  402. package/pennyfarthing-dist/guides/benchmarks.md +0 -62
  403. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
  404. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
  405. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
  406. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
  407. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
  408. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
  409. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
  410. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
  411. package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
  412. package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
  413. package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
  414. package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
  415. package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
  416. package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
  417. package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
  418. package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
  419. package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
  420. package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
  421. package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
  422. package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
@@ -225,9 +225,10 @@ def create(name: str, base: str | None, user: bool):
225
225
  Arguments:
226
226
  NAME - Name for the new theme (lowercase, hyphens allowed)
227
227
  """
228
- import yaml
229
228
  from pathlib import Path
230
229
 
230
+ import yaml
231
+
231
232
  from pennyfarthing_scripts.common.config import get_project_root
232
233
  from pennyfarthing_scripts.common.themes import (
233
234
  get_current_theme,
@@ -282,5 +283,5 @@ def create(name: str, base: str | None, user: bool):
282
283
  click.echo(f" File: {target_path}")
283
284
  click.echo()
284
285
  click.echo("Next steps:")
285
- click.echo(f" 1. Edit the theme file to customize your agents")
286
+ click.echo(" 1. Edit the theme file to customize your agents")
286
287
  click.echo(f" 2. Run 'pf theme set {name}' to activate")
@@ -0,0 +1,21 @@
1
+ """Top-level validate command — auto-discovers and runs project validators."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class ValidateReport:
10
+ """Unified report from a single validator domain."""
11
+
12
+ validator: str
13
+ passed: int = 0
14
+ warnings: int = 0
15
+ errors: int = 0
16
+ details: list[str] = field(default_factory=list)
17
+ fixed: bool = False
18
+
19
+ @property
20
+ def success(self) -> bool:
21
+ return self.errors == 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,291 @@
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
+
73
+ def _resolve_ref(ref: str) -> dict:
74
+ """Resolve a $ref pointer like #/definitions/skill."""
75
+ parts = ref.lstrip("#/").split("/")
76
+ result = schema
77
+ for part in parts:
78
+ result = result.get(part, {})
79
+ return result
80
+
81
+ def _validate_value(value, prop_schema: dict, path: str) -> None:
82
+ """Validate a single value against its schema."""
83
+ # Handle $ref
84
+ if "$ref" in prop_schema:
85
+ prop_schema = _resolve_ref(prop_schema["$ref"])
86
+
87
+ expected_type = prop_schema.get("type")
88
+
89
+ # Type checking
90
+ if expected_type == "object":
91
+ if not isinstance(value, dict):
92
+ errors.append(f"{path}: expected object, got {type(value).__name__}")
93
+ return
94
+
95
+ props = prop_schema.get("properties", {})
96
+ additional = prop_schema.get("additionalProperties")
97
+
98
+ # Required fields
99
+ for req in prop_schema.get("required", []):
100
+ if req not in value:
101
+ errors.append(f"{path}: missing required field '{req}'")
102
+
103
+ # Validate known properties
104
+ for key, val in value.items():
105
+ if key in props:
106
+ _validate_value(val, props[key], f"{path}.{key}")
107
+ elif additional is False:
108
+ errors.append(
109
+ f"{path}: additional property '{key}' not allowed"
110
+ )
111
+ elif isinstance(additional, dict):
112
+ _validate_value(val, additional, f"{path}.{key}")
113
+
114
+ elif expected_type == "string":
115
+ if not isinstance(value, str):
116
+ errors.append(f"{path}: expected string, got {type(value).__name__}")
117
+ return
118
+ pattern = prop_schema.get("pattern")
119
+ if pattern and not re.match(pattern, value):
120
+ errors.append(f"{path}: value '{value}' does not match pattern '{pattern}'")
121
+ enum_vals = prop_schema.get("enum")
122
+ if enum_vals and value not in enum_vals:
123
+ errors.append(
124
+ f"{path}: value '{value}' not in allowed values: "
125
+ f"{', '.join(enum_vals)}"
126
+ )
127
+
128
+ elif expected_type == "array":
129
+ if not isinstance(value, list):
130
+ errors.append(f"{path}: expected array, got {type(value).__name__}")
131
+ return
132
+ items_schema = prop_schema.get("items")
133
+ if items_schema:
134
+ for i, item in enumerate(value):
135
+ _validate_value(item, items_schema, f"{path}[{i}]")
136
+
137
+ elif expected_type == "boolean":
138
+ if not isinstance(value, bool):
139
+ errors.append(f"{path}: expected boolean, got {type(value).__name__}")
140
+
141
+ _validate_value(data, schema, "")
142
+ return errors
143
+
144
+
145
+ def validate_skill_registry(root: Path) -> tuple[list[str], list[str]]:
146
+ """Validate skill-registry.yaml against its JSON schema.
147
+
148
+ Returns:
149
+ (errors, warnings) — two lists of message strings.
150
+ """
151
+ errors: list[str] = []
152
+ warnings: list[str] = []
153
+
154
+ registry_path = discover_skill_registry(root)
155
+ if registry_path is None:
156
+ errors.append("skill-registry.yaml not found")
157
+ return errors, warnings
158
+
159
+ schema = _load_schema(root)
160
+ if schema is None:
161
+ errors.append("skill-registry.schema.json not found or invalid")
162
+ return errors, warnings
163
+
164
+ try:
165
+ content = registry_path.read_text()
166
+ data = yaml.safe_load(content)
167
+ except yaml.YAMLError:
168
+ errors.append("skill-registry.yaml: YAML parse error")
169
+ return errors, warnings
170
+
171
+ if not isinstance(data, dict):
172
+ errors.append("skill-registry.yaml: expected a YAML mapping")
173
+ return errors, warnings
174
+
175
+ schema_errors = _validate_against_schema(data, schema)
176
+ errors.extend(schema_errors)
177
+
178
+ return errors, warnings
179
+
180
+
181
+ def _has_frontmatter(content: str) -> bool:
182
+ """Check if content starts with YAML frontmatter (--- delimited)."""
183
+ return content.startswith("---\n")
184
+
185
+
186
+ def _parse_frontmatter(content: str) -> dict | None:
187
+ """Extract YAML frontmatter from content.
188
+
189
+ Returns:
190
+ Parsed dict, or None if no valid frontmatter.
191
+ """
192
+ if not _has_frontmatter(content):
193
+ return None
194
+ end = content.find("\n---", 3)
195
+ if end == -1:
196
+ return None
197
+ fm_text = content[4:end]
198
+ try:
199
+ return yaml.safe_load(fm_text) or {}
200
+ except yaml.YAMLError:
201
+ return None
202
+
203
+
204
+ def _get_body(content: str) -> str:
205
+ """Extract body content after frontmatter."""
206
+ if not _has_frontmatter(content):
207
+ return content
208
+ end = content.find("\n---", 3)
209
+ if end == -1:
210
+ return ""
211
+ return content[end + 4:].strip()
212
+
213
+
214
+ def validate_command_file(path: Path) -> tuple[list[str], list[str]]:
215
+ """Validate a command markdown file.
216
+
217
+ Checks:
218
+ - YAML frontmatter present
219
+ - description field in frontmatter (non-empty)
220
+ - Body content not empty (warning)
221
+
222
+ Returns:
223
+ (errors, warnings) — two lists of message strings.
224
+ """
225
+ errors: list[str] = []
226
+ warnings: list[str] = []
227
+ content = path.read_text()
228
+
229
+ fm = _parse_frontmatter(content)
230
+ if fm is None:
231
+ errors.append("Missing YAML frontmatter")
232
+ return errors, warnings
233
+
234
+ desc = fm.get("description")
235
+ if desc is None:
236
+ errors.append("Missing required frontmatter field: description")
237
+ elif not isinstance(desc, str) or not desc.strip():
238
+ errors.append("Frontmatter description must be a non-empty string")
239
+
240
+ body = _get_body(content)
241
+ if not body:
242
+ warnings.append("Command body is empty — consider adding content")
243
+
244
+ return errors, warnings
245
+
246
+
247
+ def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
248
+ """Validate skill registry and command files."""
249
+ report = ValidateReport(validator="skill-command")
250
+
251
+ # --- Skill registry validation ---
252
+ registry_errors, registry_warnings = validate_skill_registry(root)
253
+
254
+ for e in registry_errors:
255
+ report.errors += 1
256
+ report.details.append(f"[ERROR] skill-registry.yaml: {e}")
257
+
258
+ for w in registry_warnings:
259
+ if strict:
260
+ report.errors += 1
261
+ report.details.append(f"[ERROR] skill-registry.yaml: {w}")
262
+ else:
263
+ report.warnings += 1
264
+ report.details.append(f"[WARN] skill-registry.yaml: {w}")
265
+
266
+ if not registry_errors:
267
+ report.passed += 1
268
+
269
+ # --- Command file validation ---
270
+ commands_dir = root / "pennyfarthing-dist" / "commands"
271
+ command_files = discover_command_files(commands_dir)
272
+
273
+ for path in command_files:
274
+ file_errors, file_warnings = validate_command_file(path)
275
+
276
+ for e in file_errors:
277
+ report.errors += 1
278
+ report.details.append(f"[ERROR] {path.name}: {e}")
279
+
280
+ for w in file_warnings:
281
+ if strict:
282
+ report.errors += 1
283
+ report.details.append(f"[ERROR] {path.name}: {w}")
284
+ else:
285
+ report.warnings += 1
286
+ report.details.append(f"[WARN] {path.name}: {w}")
287
+
288
+ if not file_errors:
289
+ report.passed += 1
290
+
291
+ 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