@pennyfarthing/core 10.0.3 → 10.1.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 (282) hide show
  1. package/README.md +9 -7
  2. package/package.json +7 -1
  3. package/packages/core/dist/cli/commands/cyclist.d.ts +5 -1
  4. package/packages/core/dist/cli/commands/cyclist.d.ts.map +1 -1
  5. package/packages/core/dist/cli/commands/cyclist.js +4 -4
  6. package/packages/core/dist/cli/commands/cyclist.js.map +1 -1
  7. package/packages/core/dist/cli/commands/cyclist.test.js +2 -2
  8. package/packages/core/dist/cli/commands/cyclist.test.js.map +1 -1
  9. package/packages/core/dist/cli/commands/doctor-legacy.test.js +17 -16
  10. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  11. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  12. package/packages/core/dist/cli/commands/doctor.js +251 -4
  13. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  14. package/packages/core/dist/cli/commands/init.d.ts +7 -0
  15. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  16. package/packages/core/dist/cli/commands/init.js +43 -7
  17. package/packages/core/dist/cli/commands/init.js.map +1 -1
  18. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  19. package/packages/core/dist/cli/commands/update.js +26 -0
  20. package/packages/core/dist/cli/commands/update.js.map +1 -1
  21. package/packages/core/dist/cli/index.js +1 -1
  22. package/packages/core/dist/cli/index.js.map +1 -1
  23. package/packages/core/dist/cli/ocean-profiles.test.js +1 -1
  24. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  25. package/packages/core/dist/cli/utils/files.d.ts +10 -0
  26. package/packages/core/dist/cli/utils/files.d.ts.map +1 -1
  27. package/packages/core/dist/cli/utils/files.js +35 -0
  28. package/packages/core/dist/cli/utils/files.js.map +1 -1
  29. package/packages/core/dist/cli/utils/python.d.ts +22 -0
  30. package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
  31. package/packages/core/dist/cli/utils/python.js +102 -0
  32. package/packages/core/dist/cli/utils/python.js.map +1 -0
  33. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  34. package/packages/core/dist/cli/utils/settings.js +10 -0
  35. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  36. package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
  37. package/packages/core/dist/scripts/generate-report.js +11 -7
  38. package/packages/core/dist/scripts/generate-report.js.map +1 -1
  39. package/packages/core/dist/scripts/generate-spider-report.d.ts.map +1 -1
  40. package/packages/core/dist/scripts/generate-spider-report.js +12 -8
  41. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  42. package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
  43. package/packages/core/dist/scripts/generate-spider.js +6 -4
  44. package/packages/core/dist/scripts/generate-spider.js.map +1 -1
  45. package/packages/core/dist/scripts/generate-spider.test.js +2 -2
  46. package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
  47. package/pennyfarthing-dist/agents/README.md +1 -3
  48. package/pennyfarthing-dist/agents/architect.md +0 -6
  49. package/pennyfarthing-dist/agents/devops.md +0 -6
  50. package/pennyfarthing-dist/agents/orchestrator.md +0 -6
  51. package/pennyfarthing-dist/agents/pm.md +1 -7
  52. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  53. package/pennyfarthing-dist/agents/sm-setup.md +2 -2
  54. package/pennyfarthing-dist/agents/sm.md +4 -11
  55. package/pennyfarthing-dist/commands/architect.md +11 -3
  56. package/pennyfarthing-dist/commands/close-epic.md +24 -131
  57. package/pennyfarthing-dist/commands/create-theme.md +14 -24
  58. package/pennyfarthing-dist/commands/dev.md +11 -3
  59. package/pennyfarthing-dist/commands/devops.md +11 -3
  60. package/pennyfarthing-dist/commands/health-check.md +1 -3
  61. package/pennyfarthing-dist/commands/help.md +8 -12
  62. package/pennyfarthing-dist/commands/list-themes.md +14 -16
  63. package/pennyfarthing-dist/commands/orchestrator.md +11 -3
  64. package/pennyfarthing-dist/commands/parallel-work.md +1 -3
  65. package/pennyfarthing-dist/commands/pm.md +11 -3
  66. package/pennyfarthing-dist/commands/prime.md +6 -6
  67. package/pennyfarthing-dist/commands/repo-status.md +2 -2
  68. package/pennyfarthing-dist/commands/reviewer.md +11 -3
  69. package/pennyfarthing-dist/commands/run-ci.md +1 -1
  70. package/pennyfarthing-dist/commands/set-theme.md +14 -51
  71. package/pennyfarthing-dist/commands/setup.md +1 -1
  72. package/pennyfarthing-dist/commands/show-theme.md +14 -16
  73. package/pennyfarthing-dist/commands/sm.md +11 -3
  74. package/pennyfarthing-dist/commands/sprint.md +8 -8
  75. package/pennyfarthing-dist/commands/tea.md +11 -3
  76. package/pennyfarthing-dist/commands/tech-writer.md +11 -3
  77. package/pennyfarthing-dist/commands/theme-maker.md +14 -671
  78. package/pennyfarthing-dist/commands/theme.md +95 -0
  79. package/pennyfarthing-dist/commands/ux-designer.md +11 -3
  80. package/pennyfarthing-dist/commands/work.md +3 -5
  81. package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
  82. package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
  83. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
  84. package/pennyfarthing-dist/guides/hooks.md +5 -5
  85. package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
  86. package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
  87. package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
  88. package/pennyfarthing-dist/guides/prime.md +2 -2
  89. package/pennyfarthing-dist/guides/skill-schema.md +25 -26
  90. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  91. package/pennyfarthing-dist/scripts/README.md +2 -2
  92. package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
  93. package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
  94. package/pennyfarthing-dist/scripts/git/git-status-all.sh +1 -1
  95. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
  96. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +3 -3
  97. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +14 -12
  98. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
  99. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
  100. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +1 -1
  101. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  102. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +3 -3
  103. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
  104. package/pennyfarthing-dist/scripts/sprint/README.md +32 -17
  105. package/pennyfarthing-dist/scripts/story/README.md +1 -1
  106. package/pennyfarthing-dist/scripts/test/test-setup.sh +1 -1
  107. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
  108. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
  109. package/pennyfarthing-dist/scripts/theme/README.md +1 -1
  110. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
  111. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +62 -17
  112. package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
  113. package/pennyfarthing-dist/skills/skill-registry.yaml +41 -28
  114. package/pennyfarthing-dist/skills/sprint/skill.md +386 -68
  115. package/pennyfarthing-dist/skills/story/skill.md +14 -206
  116. package/pennyfarthing-dist/skills/theme/skill.md +290 -75
  117. package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
  118. package/pennyfarthing-dist/skills/workflow/skill.md +4 -4
  119. package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
  120. package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
  121. package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
  122. package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
  123. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +42 -25
  124. package/pennyfarthing-dist/workflows/git-cleanup.yaml +1 -1
  125. package/pennyfarthing-dist/workflows/project-setup/steps/step-10-complete.md +1 -1
  126. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/cli.py +15 -0
  131. package/pennyfarthing_scripts/codemarkers/__init__.py +19 -0
  132. package/pennyfarthing_scripts/codemarkers/__main__.py +6 -0
  133. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  135. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  136. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  137. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  138. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/codemarkers/analyze.py +326 -0
  140. package/pennyfarthing_scripts/codemarkers/cli.py +129 -0
  141. package/pennyfarthing_scripts/codemarkers/formatters.py +89 -0
  142. package/pennyfarthing_scripts/codemarkers/models.py +45 -0
  143. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  145. package/pennyfarthing_scripts/common/config.py +2 -1
  146. package/pennyfarthing_scripts/complexity/__init__.py +15 -0
  147. package/pennyfarthing_scripts/complexity/__main__.py +6 -0
  148. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  149. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  150. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  151. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  152. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  153. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  154. package/pennyfarthing_scripts/complexity/analyze.py +207 -0
  155. package/pennyfarthing_scripts/complexity/cli.py +78 -0
  156. package/pennyfarthing_scripts/complexity/formatters.py +64 -0
  157. package/pennyfarthing_scripts/complexity/models.py +32 -0
  158. package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
  159. package/pennyfarthing_scripts/deadcode/__main__.py +6 -0
  160. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  162. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  163. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  164. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  165. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/deadcode/analyze.py +323 -0
  167. package/pennyfarthing_scripts/deadcode/cli.py +163 -0
  168. package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
  169. package/pennyfarthing_scripts/deadcode/models.py +54 -0
  170. package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
  171. package/pennyfarthing_scripts/dependencies/__main__.py +5 -0
  172. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/dependencies/analyze.py +155 -0
  179. package/pennyfarthing_scripts/dependencies/cli.py +72 -0
  180. package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
  181. package/pennyfarthing_scripts/dependencies/models.py +39 -0
  182. package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
  183. package/pennyfarthing_scripts/healthscore/__main__.py +6 -0
  184. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  185. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  190. package/pennyfarthing_scripts/healthscore/analyze.py +161 -0
  191. package/pennyfarthing_scripts/healthscore/cli.py +76 -0
  192. package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
  193. package/pennyfarthing_scripts/healthscore/models.py +44 -0
  194. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/hotspots/analyze.py +28 -1
  201. package/pennyfarthing_scripts/hotspots/cli.py +11 -9
  202. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/jira/bidirectional.py +42 -15
  210. package/pennyfarthing_scripts/jira/cli.py +78 -1
  211. package/pennyfarthing_scripts/jira/client.py +28 -0
  212. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/prime/workflow.py +5 -3
  218. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  220. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  222. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/sprint/archive.py +63 -6
  231. package/pennyfarthing_scripts/sprint/archive_epic.py +198 -85
  232. package/pennyfarthing_scripts/sprint/cli.py +1565 -65
  233. package/pennyfarthing_scripts/sprint/epic_add.py +173 -0
  234. package/pennyfarthing_scripts/sprint/loader.py +46 -2
  235. package/pennyfarthing_scripts/sprint/story_add.py +202 -27
  236. package/pennyfarthing_scripts/sprint/story_finish.py +211 -0
  237. package/pennyfarthing_scripts/sprint/validate_cmd.py +44 -5
  238. package/pennyfarthing_scripts/sprint/validator.py +13 -3
  239. package/pennyfarthing_scripts/sprint/work.py +43 -3
  240. package/pennyfarthing_scripts/sprint/yaml_io.py +124 -15
  241. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  242. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  243. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  244. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  245. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  246. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  247. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  248. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  249. package/pennyfarthing_scripts/tests/test_codemarkers.py +682 -0
  250. package/pennyfarthing_scripts/tests/test_healthscore.py +524 -0
  251. package/pennyfarthing_scripts/tests/test_sprint_package.py +166 -0
  252. package/pennyfarthing_scripts/tests/test_yaml_io.py +117 -0
  253. package/pennyfarthing_scripts/theme/__init__.py +5 -0
  254. package/pennyfarthing_scripts/theme/__main__.py +6 -0
  255. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/theme/cli.py +286 -0
  258. package/scripts/README.md +53 -0
  259. package/scripts/postinstall.cjs +34 -0
  260. package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
  261. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +0 -133
  262. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +0 -91
  263. package/pennyfarthing-dist/scripts/sprint/check-story.sh +0 -158
  264. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +0 -52
  265. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +0 -63
  266. package/pennyfarthing-dist/scripts/sprint/list-future.sh +0 -145
  267. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +0 -110
  268. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +0 -148
  269. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +0 -415
  270. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +0 -33
  271. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +0 -230
  272. package/pennyfarthing-dist/scripts/sprint/sprint-status.sh +0 -134
  273. package/pennyfarthing-dist/scripts/sprint/validate-sprint-yaml.sh +0 -139
  274. package/pennyfarthing-dist/skills/sprint/scripts/archive-story.sh +0 -101
  275. package/pennyfarthing-dist/skills/sprint/scripts/available-stories.sh +0 -97
  276. package/pennyfarthing-dist/skills/sprint/scripts/check-story.sh +0 -164
  277. package/pennyfarthing-dist/skills/sprint/scripts/create-jira-epic.sh +0 -23
  278. package/pennyfarthing-dist/skills/sprint/scripts/new-sprint.sh +0 -116
  279. package/pennyfarthing-dist/skills/sprint/scripts/promote-epic.sh +0 -164
  280. package/pennyfarthing-dist/skills/sprint/scripts/sprint-info.sh +0 -39
  281. package/pennyfarthing-dist/skills/sprint/scripts/sprint-status.sh +0 -147
  282. package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +0 -23
@@ -0,0 +1,155 @@
1
+ """
2
+ Core dependency analysis engine.
3
+
4
+ Wraps npm outdated --json and npm audit --json.
5
+ Parses output into models following ADR-0008 result pattern.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import shutil
13
+ from collections import Counter
14
+ from pathlib import Path
15
+
16
+ from pennyfarthing_scripts.dependencies.models import (
17
+ OutdatedPackage,
18
+ SecurityAdvisory,
19
+ DependenciesResult,
20
+ )
21
+
22
+
23
+ def _find_npm(target_path: Path) -> Path | None:
24
+ """Find npm binary via shutil.which."""
25
+ npm = shutil.which("npm")
26
+ return Path(npm) if npm else None
27
+
28
+
29
+ def _check_package_json(target_path: Path) -> bool:
30
+ """Check if package.json exists in target directory."""
31
+ return (target_path / "package.json").exists()
32
+
33
+
34
+ def _parse_outdated_output(output: str) -> list[OutdatedPackage]:
35
+ """Parse npm outdated --json output into OutdatedPackage models.
36
+
37
+ npm outdated --json returns: {pkg_name: {current, wanted, latest, type, ...}}
38
+ """
39
+ if not output:
40
+ return []
41
+ try:
42
+ data = json.loads(output)
43
+ except (json.JSONDecodeError, TypeError):
44
+ return []
45
+
46
+ if not data or not isinstance(data, dict):
47
+ return []
48
+
49
+ packages = []
50
+ for name, info in data.items():
51
+ packages.append(OutdatedPackage(
52
+ name=name,
53
+ current=info.get("current", ""),
54
+ wanted=info.get("wanted", ""),
55
+ latest=info.get("latest", ""),
56
+ type=info.get("type", ""),
57
+ ))
58
+ return packages
59
+
60
+
61
+ def _parse_audit_output(output: str) -> list[SecurityAdvisory]:
62
+ """Parse npm audit --json output into SecurityAdvisory models.
63
+
64
+ npm audit --json returns: {vulnerabilities: {name: {severity, ...}}, metadata: ...}
65
+ Aggregates by severity level.
66
+ """
67
+ if not output:
68
+ return []
69
+ try:
70
+ data = json.loads(output)
71
+ except (json.JSONDecodeError, TypeError):
72
+ return []
73
+
74
+ vulns = data.get("vulnerabilities", {})
75
+ if not vulns:
76
+ return []
77
+
78
+ severity_counts: Counter[str] = Counter()
79
+ for info in vulns.values():
80
+ sev = info.get("severity", "unknown")
81
+ severity_counts[sev] += 1
82
+
83
+ return [
84
+ SecurityAdvisory(severity=sev, count=count)
85
+ for sev, count in severity_counts.items()
86
+ ]
87
+
88
+
89
+ async def _run_npm_outdated(npm_bin: Path, target_path: Path) -> tuple[str, str, int]:
90
+ """Run npm outdated --json subprocess."""
91
+ proc = await asyncio.create_subprocess_exec(
92
+ str(npm_bin),
93
+ "outdated",
94
+ "--json",
95
+ cwd=str(target_path),
96
+ stdout=asyncio.subprocess.PIPE,
97
+ stderr=asyncio.subprocess.PIPE,
98
+ )
99
+ stdout, stderr = await proc.communicate()
100
+ return (
101
+ stdout.decode("utf-8", errors="replace"),
102
+ stderr.decode("utf-8", errors="replace"),
103
+ proc.returncode or 0,
104
+ )
105
+
106
+
107
+ async def _run_npm_audit(npm_bin: Path, target_path: Path) -> tuple[str, str, int]:
108
+ """Run npm audit --json subprocess."""
109
+ proc = await asyncio.create_subprocess_exec(
110
+ str(npm_bin),
111
+ "audit",
112
+ "--json",
113
+ cwd=str(target_path),
114
+ stdout=asyncio.subprocess.PIPE,
115
+ stderr=asyncio.subprocess.PIPE,
116
+ )
117
+ stdout, stderr = await proc.communicate()
118
+ return (
119
+ stdout.decode("utf-8", errors="replace"),
120
+ stderr.decode("utf-8", errors="replace"),
121
+ proc.returncode or 0,
122
+ )
123
+
124
+
125
+ async def analyze_dependencies(target_path: Path) -> DependenciesResult:
126
+ """Analyze dependencies of a Node.js project."""
127
+ resolved = target_path.resolve()
128
+
129
+ npm_bin = _find_npm(resolved)
130
+ if npm_bin is None:
131
+ return DependenciesResult(
132
+ success=False,
133
+ target_path=str(resolved),
134
+ error="npm not found. Install Node.js to use dependency analysis.",
135
+ )
136
+
137
+ if not _check_package_json(resolved):
138
+ return DependenciesResult(
139
+ success=False,
140
+ target_path=str(resolved),
141
+ error="No package.json found in target directory.",
142
+ )
143
+
144
+ outdated_stdout, _, _ = await _run_npm_outdated(npm_bin, resolved)
145
+ audit_stdout, _, _ = await _run_npm_audit(npm_bin, resolved)
146
+
147
+ outdated = _parse_outdated_output(outdated_stdout)
148
+ advisories = _parse_audit_output(audit_stdout)
149
+
150
+ return DependenciesResult(
151
+ success=True,
152
+ target_path=str(resolved),
153
+ outdated=outdated,
154
+ advisories=advisories,
155
+ )
@@ -0,0 +1,72 @@
1
+ """
2
+ CLI commands for dependency analysis.
3
+
4
+ Usage:
5
+ python -m pennyfarthing_scripts.dependencies analyze [OPTIONS]
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from pathlib import Path
12
+
13
+ import click
14
+
15
+
16
+ @click.group()
17
+ def dependencies():
18
+ """Dependency staleness and security analysis.
19
+
20
+ \b
21
+ Commands:
22
+ analyze - Analyze dependency health
23
+ """
24
+ pass
25
+
26
+
27
+ def _run_analysis(target_path: str | None) -> "DependenciesResult":
28
+ """Run analysis and return result."""
29
+ from pennyfarthing_scripts.dependencies.analyze import analyze_dependencies
30
+
31
+ p = Path(target_path).resolve() if target_path else Path(".").resolve()
32
+ return asyncio.run(analyze_dependencies(p))
33
+
34
+
35
+ def _output_result(result, fmt: str, output_file: str | None):
36
+ """Format and output the analysis result."""
37
+ from pennyfarthing_scripts.dependencies.formatters import (
38
+ format_outdated_table,
39
+ format_audit_table,
40
+ export_json,
41
+ export_csv,
42
+ )
43
+
44
+ if fmt == "json":
45
+ text = export_json(result)
46
+ elif fmt == "csv":
47
+ text = export_csv(result.outdated)
48
+ else:
49
+ parts = [format_outdated_table(result.outdated)]
50
+ if result.advisories:
51
+ parts.append("")
52
+ parts.append(format_audit_table(result.advisories))
53
+ text = "\n".join(parts)
54
+
55
+ if output_file:
56
+ Path(output_file).write_text(text)
57
+ click.echo(f"Output written to {output_file}", err=True)
58
+ else:
59
+ click.echo(text)
60
+
61
+
62
+ @dependencies.command()
63
+ @click.option("--path", "target_path", type=click.Path(exists=True),
64
+ help="Directory to analyze")
65
+ @click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]),
66
+ default="table", show_default=True)
67
+ @click.option("--output", "output_file", type=click.Path(),
68
+ help="Write output to file")
69
+ def analyze(target_path, fmt, output_file):
70
+ """Analyze dependency health."""
71
+ result = _run_analysis(target_path)
72
+ _output_result(result, fmt, output_file)
@@ -0,0 +1,63 @@
1
+ """
2
+ Output formatters for dependency analysis results.
3
+
4
+ Supports table, JSON, and CSV output — no external dependencies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import csv
10
+ import io
11
+ import json
12
+ from dataclasses import asdict
13
+
14
+ from pennyfarthing_scripts.dependencies.models import (
15
+ OutdatedPackage,
16
+ SecurityAdvisory,
17
+ DependenciesResult,
18
+ )
19
+
20
+
21
+ def format_outdated_table(packages: list[OutdatedPackage]) -> str:
22
+ """Format outdated packages as column-aligned table."""
23
+ if not packages:
24
+ return " No outdated packages found."
25
+
26
+ hdr = f"{'Package':<30} {'Current':<12} {'Wanted':<12} {'Latest':<12} Type"
27
+ sep = f"{'-' * 30} {'-' * 12} {'-' * 12} {'-' * 12} ----"
28
+
29
+ lines = [hdr, sep]
30
+ for p in packages:
31
+ lines.append(
32
+ f"{p.name:<30} {p.current:<12} {p.wanted:<12} {p.latest:<12} {p.type}"
33
+ )
34
+ return "\n".join(lines)
35
+
36
+
37
+ def format_audit_table(advisories: list[SecurityAdvisory]) -> str:
38
+ """Format security advisories as table."""
39
+ if not advisories:
40
+ return " No vulnerabilities found."
41
+
42
+ hdr = f"{'Severity':<12} {'Count':>6}"
43
+ sep = f"{'-' * 12} {'-' * 6}"
44
+
45
+ lines = [hdr, sep]
46
+ for a in advisories:
47
+ lines.append(f"{a.severity:<12} {a.count:>6}")
48
+ return "\n".join(lines)
49
+
50
+
51
+ def export_json(result: DependenciesResult) -> str:
52
+ """Serialize result to JSON string."""
53
+ return json.dumps(asdict(result), indent=2, default=str)
54
+
55
+
56
+ def export_csv(packages: list[OutdatedPackage]) -> str:
57
+ """Export outdated packages as CSV."""
58
+ buf = io.StringIO()
59
+ writer = csv.writer(buf)
60
+ writer.writerow(["name", "current", "wanted", "latest", "type"])
61
+ for p in packages:
62
+ writer.writerow([p.name, p.current, p.wanted, p.latest, p.type])
63
+ return buf.getvalue()
@@ -0,0 +1,39 @@
1
+ """
2
+ Data models for dependency analysis results.
3
+
4
+ Follows ADR-0008 result pattern — structured dataclasses with success/error fields.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class OutdatedPackage:
14
+ """A package with available updates."""
15
+
16
+ name: str = ""
17
+ current: str = ""
18
+ wanted: str = ""
19
+ latest: str = ""
20
+ type: str = ""
21
+
22
+
23
+ @dataclass
24
+ class SecurityAdvisory:
25
+ """Aggregated vulnerability count per severity level."""
26
+
27
+ severity: str = ""
28
+ count: int = 0
29
+
30
+
31
+ @dataclass
32
+ class DependenciesResult:
33
+ """Analysis result following ADR-0008 pattern."""
34
+
35
+ success: bool = False
36
+ target_path: str = ""
37
+ outdated: list[OutdatedPackage] = field(default_factory=list)
38
+ advisories: list[SecurityAdvisory] = field(default_factory=list)
39
+ error: str | None = None
@@ -0,0 +1,21 @@
1
+ """
2
+ Composite health score for codebase analysis.
3
+
4
+ Aggregates 8 dimensions into a single 0-100 weighted score:
5
+ churn, TODO density, complexity, test gaps, dead code,
6
+ deprecation debt, dependency freshness, agent context efficiency.
7
+ """
8
+
9
+ from pennyfarthing_scripts.healthscore.models import (
10
+ DimensionScore,
11
+ HealthscoreResult,
12
+ DEFAULT_WEIGHTS,
13
+ )
14
+ from pennyfarthing_scripts.healthscore.analyze import analyze_healthscore
15
+
16
+ __all__ = [
17
+ "DimensionScore",
18
+ "HealthscoreResult",
19
+ "DEFAULT_WEIGHTS",
20
+ "analyze_healthscore",
21
+ ]
@@ -0,0 +1,6 @@
1
+ """Allow running as: python -m pennyfarthing_scripts.healthscore"""
2
+
3
+ from pennyfarthing_scripts.healthscore.cli import healthscore
4
+
5
+ if __name__ == "__main__":
6
+ healthscore()
@@ -0,0 +1,161 @@
1
+ """
2
+ Core health score analysis engine.
3
+
4
+ Aggregates lightweight dimension scores into a composite 0-100 score.
5
+ Supports caching with a configurable TTL (default 5 minutes).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from pennyfarthing_scripts.healthscore.models import (
16
+ DEFAULT_WEIGHTS,
17
+ DimensionScore,
18
+ HealthscoreResult,
19
+ )
20
+
21
+
22
+ async def analyze_healthscore(
23
+ target_path: Path,
24
+ weights: dict[str, float] | None = None,
25
+ cache_ttl: int = 300,
26
+ ) -> HealthscoreResult:
27
+ """Analyze codebase health across all dimensions.
28
+
29
+ Args:
30
+ target_path: Directory to analyze.
31
+ weights: Custom dimension weights (must sum to 1.0). Uses defaults if None.
32
+ cache_ttl: Cache time-to-live in seconds (default 300 = 5 minutes).
33
+
34
+ Returns:
35
+ HealthscoreResult with composite score and per-dimension breakdown.
36
+ """
37
+ w = weights if weights is not None else DEFAULT_WEIGHTS
38
+ resolved = target_path.resolve()
39
+
40
+ cache_dir = get_cache_path(resolved)
41
+ any_cached = False
42
+ raw_scores: dict[str, float | None] = {}
43
+ dimensions: list[DimensionScore] = []
44
+
45
+ for dim_name, dim_weight in w.items():
46
+ score: float | None = None
47
+ error: str | None = None
48
+
49
+ # Try cache if ttl > 0
50
+ if cache_ttl > 0:
51
+ cached = read_cached_score(cache_dir, dim_name, cache_ttl)
52
+ if cached is not None:
53
+ score = cached
54
+ any_cached = True
55
+
56
+ # If no cached value, run lightweight probe
57
+ if score is None:
58
+ score = _probe_dimension(dim_name, resolved)
59
+ # Cache result if we got one and caching is enabled
60
+ if score is not None and cache_ttl > 0:
61
+ cache_dir.mkdir(parents=True, exist_ok=True)
62
+ write_cached_score(cache_dir, dim_name, score)
63
+
64
+ if score is None:
65
+ error = f"{dim_name} not available"
66
+
67
+ raw_scores[dim_name] = score
68
+ dimensions.append(DimensionScore(
69
+ name=dim_name,
70
+ score=score,
71
+ weight=dim_weight,
72
+ error=error,
73
+ ))
74
+
75
+ composite = compute_composite_score(raw_scores, w)
76
+
77
+ return HealthscoreResult(
78
+ success=True,
79
+ composite_score=composite,
80
+ target_path=str(resolved),
81
+ dimensions=dimensions,
82
+ cached=any_cached,
83
+ )
84
+
85
+
86
+ def _probe_dimension(name: str, target_path: Path) -> float | None:
87
+ """Run a lightweight probe for a single dimension.
88
+
89
+ Returns a score 0-100 or None if the dimension cannot be assessed.
90
+ These are intentionally simple heuristics — full analysis is deferred
91
+ to each dimension's own module when available.
92
+ """
93
+ # For now, return None for all dimensions.
94
+ # Each dimension will be wired to its respective analyzer in future stories.
95
+ return None
96
+
97
+
98
+ def compute_composite_score(
99
+ dimension_scores: dict[str, float | None],
100
+ weights: dict[str, float],
101
+ ) -> float:
102
+ """Compute weighted average from dimension scores.
103
+
104
+ Dimensions with None scores are excluded and remaining weights
105
+ are renormalized.
106
+
107
+ Args:
108
+ dimension_scores: Map of dimension name to score (0-100 or None).
109
+ weights: Map of dimension name to weight.
110
+
111
+ Returns:
112
+ Composite score 0-100.
113
+ """
114
+ total_weight = 0.0
115
+ weighted_sum = 0.0
116
+
117
+ for name, score in dimension_scores.items():
118
+ if score is not None:
119
+ w = weights.get(name, 0.0)
120
+ weighted_sum += score * w
121
+ total_weight += w
122
+
123
+ if total_weight == 0.0:
124
+ return 0.0
125
+
126
+ return weighted_sum / total_weight
127
+
128
+
129
+ def get_cache_path(target_path: Path) -> Path:
130
+ """Return the cache directory for a given target path."""
131
+ path_hash = hashlib.md5(str(target_path).encode()).hexdigest()[:12]
132
+ return target_path / ".pennyfarthing" / ".cache" / "healthscore" / path_hash
133
+
134
+
135
+ def read_cached_score(cache_dir: Path, dimension: str, ttl: int) -> float | None:
136
+ """Read a cached dimension score if still valid.
137
+
138
+ Returns None if cache miss or expired.
139
+ """
140
+ cache_file = cache_dir / f"{dimension}.json"
141
+ if not cache_file.exists():
142
+ return None
143
+
144
+ try:
145
+ data = json.loads(cache_file.read_text())
146
+ except (json.JSONDecodeError, OSError):
147
+ return None
148
+
149
+ ts = data.get("timestamp", 0)
150
+ if ttl <= 0 or (time.time() - ts) > ttl:
151
+ return None
152
+
153
+ return data.get("score")
154
+
155
+
156
+ def write_cached_score(cache_dir: Path, dimension: str, score: float) -> None:
157
+ """Write a dimension score to cache."""
158
+ cache_dir.mkdir(parents=True, exist_ok=True)
159
+ cache_file = cache_dir / f"{dimension}.json"
160
+ data = {"score": score, "timestamp": time.time()}
161
+ cache_file.write_text(json.dumps(data))
@@ -0,0 +1,76 @@
1
+ """
2
+ CLI commands for health score analysis.
3
+
4
+ Usage:
5
+ pf healthscore analyze [OPTIONS]
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from pathlib import Path
12
+
13
+ import click
14
+
15
+
16
+ @click.group()
17
+ def healthscore():
18
+ """Composite codebase health score.
19
+
20
+ \b
21
+ Commands:
22
+ analyze - Compute health score across all dimensions
23
+ """
24
+ pass
25
+
26
+
27
+ def _common_options(fn):
28
+ """Shared options for healthscore commands."""
29
+ fn = click.option("--path", "target_path", type=click.Path(exists=True),
30
+ help="Directory to analyze")(fn)
31
+ fn = click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]),
32
+ default="table", show_default=True)(fn)
33
+ fn = click.option("--output", "output_file", type=click.Path(),
34
+ help="Write output to file")(fn)
35
+ fn = click.option("--no-cache", is_flag=True,
36
+ help="Bypass cache, force fresh analysis")(fn)
37
+ return fn
38
+
39
+
40
+ def _run_analysis(target_path: str | None, no_cache: bool) -> "HealthscoreResult":
41
+ """Run analysis and return result."""
42
+ from pennyfarthing_scripts.healthscore.analyze import analyze_healthscore
43
+
44
+ p = Path(target_path).resolve() if target_path else Path(".").resolve()
45
+ cache_ttl = 0 if no_cache else 300
46
+ return asyncio.run(analyze_healthscore(p, cache_ttl=cache_ttl))
47
+
48
+
49
+ def _output_result(result, fmt: str, output_file: str | None):
50
+ """Format and output the analysis result."""
51
+ from pennyfarthing_scripts.healthscore.formatters import (
52
+ export_csv,
53
+ export_json,
54
+ format_table,
55
+ )
56
+
57
+ if fmt == "json":
58
+ text = export_json(result)
59
+ elif fmt == "csv":
60
+ text = export_csv(result)
61
+ else:
62
+ text = format_table(result)
63
+
64
+ if output_file:
65
+ Path(output_file).write_text(text)
66
+ click.echo(f"Output written to {output_file}", err=True)
67
+ else:
68
+ click.echo(text)
69
+
70
+
71
+ @healthscore.command()
72
+ @_common_options
73
+ def analyze(target_path, fmt, output_file, no_cache):
74
+ """Compute composite health score."""
75
+ result = _run_analysis(target_path, no_cache)
76
+ _output_result(result, fmt, output_file)
@@ -0,0 +1,46 @@
1
+ """
2
+ Output formatters for health score results.
3
+
4
+ Supports table, JSON, and CSV output — no external dependencies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import csv
10
+ import io
11
+ import json
12
+ from dataclasses import asdict
13
+
14
+ from pennyfarthing_scripts.healthscore.models import HealthscoreResult
15
+
16
+
17
+ def format_table(result: HealthscoreResult) -> str:
18
+ """Format health score results as column-aligned table."""
19
+ lines = [
20
+ f"Health Score: {result.composite_score:.1f} / 100",
21
+ "",
22
+ f"{'Dimension':<30} {'Score':>6} {'Weight':>6}",
23
+ f"{'-' * 30} {'------':>6} {'------':>6}",
24
+ ]
25
+
26
+ for dim in result.dimensions:
27
+ score_str = f"{dim.score:.1f}" if dim.score is not None else "N/A"
28
+ weight_pct = f"{dim.weight * 100:.0f}%"
29
+ lines.append(f"{dim.name:<30} {score_str:>6} {weight_pct:>6}")
30
+
31
+ return "\n".join(lines)
32
+
33
+
34
+ def export_json(result: HealthscoreResult) -> str:
35
+ """Serialize result to JSON string."""
36
+ return json.dumps(asdict(result), indent=2, default=str)
37
+
38
+
39
+ def export_csv(result: HealthscoreResult) -> str:
40
+ """Export dimension scores as CSV."""
41
+ buf = io.StringIO()
42
+ writer = csv.writer(buf)
43
+ writer.writerow(["dimension", "score", "weight", "error"])
44
+ for dim in result.dimensions:
45
+ writer.writerow([dim.name, dim.score, dim.weight, dim.error or ""])
46
+ return buf.getvalue()