@pennyfarthing/core 10.1.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. package/README.md +13 -18
  2. package/package.json +3 -1
  3. package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js +24 -0
  5. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +101 -15
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js +1 -1
  10. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js.map +1 -1
  11. package/packages/core/dist/cli/commands/e2e-upgrade.test.js +1 -1
  12. package/packages/core/dist/cli/commands/e2e-upgrade.test.js.map +1 -1
  13. package/packages/core/dist/cli/commands/hooks-consolidation.test.js +2 -2
  14. package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -1
  15. package/packages/core/dist/cli/commands/init-consolidation.test.js.map +1 -1
  16. package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
  17. package/packages/core/dist/cli/commands/uninstall.js +24 -13
  18. package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
  19. package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
  20. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  21. package/packages/core/dist/cli/commands/update.js.map +1 -1
  22. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  23. package/packages/core/dist/cli/theme-maker.test.js +64 -115
  24. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  25. package/packages/core/dist/index.d.ts +1 -1
  26. package/packages/core/dist/index.d.ts.map +1 -1
  27. package/packages/core/dist/index.js +2 -2
  28. package/packages/core/dist/index.js.map +1 -1
  29. package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
  30. package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
  31. package/packages/core/dist/plugins/plugin-discovery.js +165 -0
  32. package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
  33. package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
  34. package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
  35. package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
  36. package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
  37. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  38. package/packages/core/dist/workflow/context-watch.d.ts +80 -0
  39. package/packages/core/dist/workflow/context-watch.d.ts.map +1 -0
  40. package/packages/core/dist/workflow/context-watch.js +235 -0
  41. package/packages/core/dist/workflow/context-watch.js.map +1 -0
  42. package/packages/core/dist/workflow/context-watch.test.d.ts +1 -0
  43. package/packages/core/dist/workflow/context-watch.test.d.ts.map +1 -0
  44. package/packages/core/dist/workflow/context-watch.test.js +746 -0
  45. package/packages/core/dist/workflow/context-watch.test.js.map +1 -0
  46. package/packages/core/dist/workflow/file-watch.d.ts +82 -0
  47. package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
  48. package/packages/core/dist/workflow/file-watch.js +198 -0
  49. package/packages/core/dist/workflow/file-watch.js.map +1 -0
  50. package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
  51. package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
  52. package/packages/core/dist/workflow/file-watch.test.js +469 -0
  53. package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
  54. package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
  55. package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
  56. package/packages/core/dist/workflow/observation-writer.js +97 -0
  57. package/packages/core/dist/workflow/observation-writer.js.map +1 -0
  58. package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
  59. package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
  60. package/packages/core/dist/workflow/observation-writer.test.js +424 -0
  61. package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
  62. package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
  63. package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
  64. package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
  65. package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
  66. package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
  67. package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
  68. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
  69. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
  70. package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
  71. package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
  72. package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
  73. package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
  74. package/packages/core/dist/workflow/tool-watch.js +166 -0
  75. package/packages/core/dist/workflow/tool-watch.js.map +1 -0
  76. package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
  77. package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
  78. package/packages/core/dist/workflow/tool-watch.test.js +718 -0
  79. package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
  80. package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
  81. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  82. package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
  83. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  84. package/packages/core/dist/workflow/workflow-schema.js +44 -0
  85. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  86. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  87. package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
  88. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  89. package/pennyfarthing-dist/agents/handoff.md +18 -3
  90. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  91. package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
  92. package/pennyfarthing-dist/agents/sm.md +11 -5
  93. package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
  94. package/pennyfarthing-dist/commands/setup.md +4 -0
  95. package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
  96. package/pennyfarthing-dist/guides/bikelane.md +3 -2
  97. package/pennyfarthing-dist/guides/scale-levels.md +4 -6
  98. package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
  99. package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
  100. package/pennyfarthing-dist/personas/themes/fifth-element.yaml +295 -0
  101. package/pennyfarthing-dist/scripts/README.md +1 -1
  102. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
  103. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +20 -10
  104. package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
  105. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  106. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -189
  107. package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
  108. package/pennyfarthing-dist/skills/skill-registry.yaml +1 -1
  109. package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
  110. package/pennyfarthing-dist/skills/workflow/skill.md +24 -1
  111. package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
  112. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
  113. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
  114. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  122. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  123. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  124. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  125. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  126. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/bellmode_hook.py +202 -47
  128. package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
  129. package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
  130. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/brownfield/cli.py +0 -1
  135. package/pennyfarthing_scripts/brownfield/discover.py +1 -2
  136. package/pennyfarthing_scripts/cli.py +11 -6
  137. package/pennyfarthing_scripts/codemarkers/__init__.py +5 -1
  138. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/codemarkers/analyze.py +177 -2
  145. package/pennyfarthing_scripts/codemarkers/cli.py +50 -0
  146. package/pennyfarthing_scripts/codemarkers/formatters.py +0 -1
  147. package/pennyfarthing_scripts/codemarkers/models.py +15 -0
  148. package/pennyfarthing_scripts/common/__init__.py +8 -9
  149. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  150. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  151. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  152. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  153. package/pennyfarthing_scripts/common/config.py +1 -1
  154. package/pennyfarthing_scripts/complexity/__init__.py +1 -1
  155. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  156. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  157. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  158. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  159. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  160. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/complexity/analyze.py +1 -1
  162. package/pennyfarthing_scripts/complexity/cli.py +5 -1
  163. package/pennyfarthing_scripts/complexity/formatters.py +1 -1
  164. package/pennyfarthing_scripts/context.py +14 -15
  165. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  167. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/deadcode/analyze.py +3 -4
  172. package/pennyfarthing_scripts/deadcode/cli.py +2 -2
  173. package/pennyfarthing_scripts/dependencies/__init__.py +2 -2
  174. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/dependencies/analyze.py +1 -1
  181. package/pennyfarthing_scripts/dependencies/cli.py +8 -4
  182. package/pennyfarthing_scripts/dependencies/formatters.py +1 -1
  183. package/pennyfarthing_scripts/git/__init__.py +5 -5
  184. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  185. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/git/create_branches.py +3 -2
  188. package/pennyfarthing_scripts/git/status_all.py +1 -1
  189. package/pennyfarthing_scripts/healthscore/__init__.py +2 -2
  190. package/pennyfarthing_scripts/healthscore/__main__.py +8 -0
  191. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/healthscore/analyze.py +451 -21
  198. package/pennyfarthing_scripts/healthscore/cli.py +5 -1
  199. package/pennyfarthing_scripts/healthscore/models.py +0 -1
  200. package/pennyfarthing_scripts/hooks.py +8 -11
  201. package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
  202. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/hotspots/analyze.py +128 -14
  209. package/pennyfarthing_scripts/hotspots/cli.py +2 -2
  210. package/pennyfarthing_scripts/hotspots/models.py +0 -1
  211. package/pennyfarthing_scripts/jira/__init__.py +15 -17
  212. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  220. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  222. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/jira/bidirectional.py +2 -3
  225. package/pennyfarthing_scripts/jira/claim.py +21 -0
  226. package/pennyfarthing_scripts/jira/cli.py +2 -2
  227. package/pennyfarthing_scripts/jira/client.py +4 -4
  228. package/pennyfarthing_scripts/jira/create.py +45 -1
  229. package/pennyfarthing_scripts/jira/epic.py +3 -2
  230. package/pennyfarthing_scripts/jira/reconcile.py +0 -1
  231. package/pennyfarthing_scripts/jira/story.py +2 -0
  232. package/pennyfarthing_scripts/jira/sync.py +1 -1
  233. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  238. package/pennyfarthing_scripts/migration/skill.py +0 -1
  239. package/pennyfarthing_scripts/migration/step.py +0 -1
  240. package/pennyfarthing_scripts/migration/validate.py +8 -5
  241. package/pennyfarthing_scripts/patch_mode.py +2 -2
  242. package/pennyfarthing_scripts/preflight/__init__.py +1 -1
  243. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  244. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/preflight/finish.py +0 -1
  248. package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
  249. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/prime/cli.py +5 -1
  258. package/pennyfarthing_scripts/prime/loader.py +2 -3
  259. package/pennyfarthing_scripts/prime/persona.py +2 -1
  260. package/pennyfarthing_scripts/prime/tiers.py +4 -4
  261. package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
  262. package/pennyfarthing_scripts/sprint/__init__.py +10 -12
  263. package/pennyfarthing_scripts/sprint/__main__.py +2 -2
  264. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/sprint/archive.py +0 -1
  281. package/pennyfarthing_scripts/sprint/archive_epic.py +1 -4
  282. package/pennyfarthing_scripts/sprint/cli.py +34 -28
  283. package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
  284. package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
  285. package/pennyfarthing_scripts/sprint/loader.py +6 -0
  286. package/pennyfarthing_scripts/sprint/status.py +1 -2
  287. package/pennyfarthing_scripts/sprint/story_add.py +2 -2
  288. package/pennyfarthing_scripts/sprint/story_finish.py +3 -5
  289. package/pennyfarthing_scripts/sprint/story_update.py +11 -3
  290. package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
  291. package/pennyfarthing_scripts/sprint/validator.py +120 -6
  292. package/pennyfarthing_scripts/sprint/work.py +1 -4
  293. package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
  294. package/pennyfarthing_scripts/story/__init__.py +14 -16
  295. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  296. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  297. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  298. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/story/size.py +0 -1
  302. package/pennyfarthing_scripts/story/template.py +0 -1
  303. package/pennyfarthing_scripts/swebench.py +1 -2
  304. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  305. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  306. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  307. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  308. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  309. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  310. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  311. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  312. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  313. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  314. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  315. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  316. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  317. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  318. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  319. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  320. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  321. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  322. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  323. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  324. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  325. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  326. package/pennyfarthing_scripts/tests/conftest.py +1 -2
  327. package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
  328. package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
  329. package/pennyfarthing_scripts/tests/test_codemarkers.py +13 -8
  330. package/pennyfarthing_scripts/tests/test_common.py +9 -4
  331. package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
  332. package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
  333. package/pennyfarthing_scripts/tests/test_healthscore.py +17 -25
  334. package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
  335. package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
  336. package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
  337. package/pennyfarthing_scripts/tests/test_prime.py +39 -21
  338. package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
  339. package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
  340. package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
  341. package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
  342. package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
  343. package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
  344. package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
  345. package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
  346. package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
  347. package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
  348. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  349. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  350. package/pennyfarthing_scripts/theme/cli.py +3 -2
  351. package/pennyfarthing_scripts/validate/__init__.py +21 -0
  352. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  353. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  354. package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
  355. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  356. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  357. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  358. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  359. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  360. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  361. package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
  362. package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
  363. package/pennyfarthing_scripts/validate/adapters/skill_command.py +292 -0
  364. package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
  365. package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
  366. package/pennyfarthing_scripts/validate/cli.py +141 -0
  367. package/pennyfarthing_scripts/welcome_hook.py +2 -3
  368. package/pennyfarthing_scripts/workflow.py +3 -3
  369. package/scripts/README.md +3 -15
  370. package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
  371. package/pennyfarthing-dist/commands/benchmark.md +0 -485
  372. package/pennyfarthing-dist/commands/job-fair.md +0 -102
  373. package/pennyfarthing-dist/commands/solo.md +0 -447
  374. package/pennyfarthing-dist/guides/benchmarks.md +0 -62
  375. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  376. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
  377. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
  378. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
  379. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
  380. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
  381. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
  382. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
  383. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
  384. package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
  385. package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
  386. package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
  387. package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
  388. package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
  389. package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
  390. package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
  391. package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
  392. package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
  393. package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
  394. package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
  395. package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
  396. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  397. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  398. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  399. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  400. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  401. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  402. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  403. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  404. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  405. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  406. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  407. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -1,17 +1,18 @@
1
1
  """
2
2
  Core hotspot analysis engine.
3
3
 
4
- Parses git log history, computes per-file metrics, and produces scored hotspot results.
5
- Reuses async git subprocess pattern from git/status_all.py.
4
+ Uses PyDriller to mine git history, computes per-file metrics, and produces
5
+ scored hotspot results. Falls back to raw git log parsing if PyDriller is unavailable.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
10
  import asyncio
11
11
  import fnmatch
12
+ import logging
12
13
  import re
13
14
  from collections import defaultdict
14
- from datetime import datetime, timezone
15
+ from datetime import UTC, datetime, timedelta
15
16
  from pathlib import Path
16
17
 
17
18
  from pennyfarthing_scripts.hotspots.models import (
@@ -21,6 +22,8 @@ from pennyfarthing_scripts.hotspots.models import (
21
22
  MultiRepoHotspotResult,
22
23
  )
23
24
 
25
+ logger = logging.getLogger("hotspots")
26
+
24
27
  # Scoring weights (must sum to 1.0)
25
28
  WEIGHT_BUG_FIXES = 0.35
26
29
  WEIGHT_CHANGES = 0.30
@@ -59,6 +62,30 @@ DEFAULT_EXCLUDES = [
59
62
  "*.d.ts.map",
60
63
  # CI config
61
64
  ".github/*",
65
+ # Sprint/session operational files (not code quality signals)
66
+ "sprint/*",
67
+ ".session/*",
68
+ # Config/manifest files — high churn but not code quality signals
69
+ "package.json",
70
+ "*/package.json",
71
+ "tsconfig.json",
72
+ "*/tsconfig.json",
73
+ "tsconfig.*.json",
74
+ "*/tsconfig.*.json",
75
+ # Documentation — frequently edited but not code hotspots
76
+ "*.md",
77
+ "CLAUDE.md",
78
+ "*/CLAUDE.md",
79
+ "CLAUDE-*.md",
80
+ "*/CLAUDE-*.md",
81
+ "README.md",
82
+ "*/README.md",
83
+ "CHANGELOG.md",
84
+ "*/CHANGELOG.md",
85
+ "docs/*",
86
+ # YAML config (sprint, workflow, etc.)
87
+ "*.yaml",
88
+ "*.yml",
62
89
  ]
63
90
 
64
91
  # Regex for identifying bug-fix commits
@@ -286,6 +313,8 @@ async def analyze_repo(
286
313
  ) -> HotspotResult:
287
314
  """Analyze a single repository for code hotspots.
288
315
 
316
+ Uses PyDriller for git mining when available, falls back to raw git log parsing.
317
+
289
318
  Args:
290
319
  name: Display name for the repository
291
320
  path: Path to the git repository
@@ -308,6 +337,87 @@ async def analyze_repo(
308
337
  error=f"Path not found: {resolved}",
309
338
  )
310
339
 
340
+ try:
341
+ return await _analyze_repo_pydriller(name, resolved, days, all_excludes)
342
+ except ImportError:
343
+ logger.info("[hotspots] PyDriller not available, using git log fallback")
344
+ return await _analyze_repo_gitlog(name, resolved, days, all_excludes, branch)
345
+
346
+
347
+ async def _analyze_repo_pydriller(
348
+ name: str,
349
+ resolved: Path,
350
+ days: int,
351
+ all_excludes: list[str],
352
+ ) -> HotspotResult:
353
+ """PyDriller-backed hotspot analysis."""
354
+ from pydriller import Repository
355
+
356
+ since = datetime.now(UTC) - timedelta(days=days)
357
+ now = datetime.now(UTC)
358
+
359
+ file_metrics: dict[str, dict] = defaultdict(
360
+ lambda: {
361
+ "change_count": 0,
362
+ "bug_fix_count": 0,
363
+ "authors": set(),
364
+ "lines_added": 0,
365
+ "lines_deleted": 0,
366
+ "last_changed": "",
367
+ }
368
+ )
369
+
370
+ def _collect() -> int:
371
+ commit_count = 0
372
+ repo = Repository(str(resolved), since=since)
373
+ for commit in repo.traverse_commits():
374
+ commit_count += 1
375
+ is_fix = is_bug_fix_commit(commit.msg)
376
+ commit_date = commit.committer_date.isoformat()
377
+
378
+ for mod in commit.modified_files:
379
+ fpath = mod.new_path or mod.old_path
380
+ if not fpath:
381
+ continue
382
+ if _should_exclude(fpath, all_excludes):
383
+ continue
384
+
385
+ m = file_metrics[fpath]
386
+ m["change_count"] += 1
387
+ if is_fix:
388
+ m["bug_fix_count"] += 1
389
+ m["authors"].add(commit.author.name)
390
+ m["lines_added"] += mod.added_lines
391
+ m["lines_deleted"] += mod.deleted_lines
392
+ if not m["last_changed"] or commit_date > m["last_changed"]:
393
+ m["last_changed"] = commit_date
394
+ return commit_count
395
+
396
+ loop = asyncio.get_event_loop()
397
+ commit_count = await loop.run_in_executor(None, _collect)
398
+ logger.info("[hotspots] PyDriller: %d commits, %d files after filtering",
399
+ commit_count, len(file_metrics))
400
+
401
+ if not file_metrics:
402
+ return HotspotResult(
403
+ success=True,
404
+ repo_name=name,
405
+ repo_path=str(resolved),
406
+ time_window_days=days,
407
+ commit_count=commit_count,
408
+ )
409
+
410
+ return _build_hotspot_result(name, resolved, days, commit_count, file_metrics, now)
411
+
412
+
413
+ async def _analyze_repo_gitlog(
414
+ name: str,
415
+ resolved: Path,
416
+ days: int,
417
+ all_excludes: list[str],
418
+ branch: str,
419
+ ) -> HotspotResult:
420
+ """Fallback: raw git log parsing."""
311
421
  stdout, stderr, rc = await _run_git_log(resolved, days, branch)
312
422
 
313
423
  if rc != 0:
@@ -330,7 +440,6 @@ async def analyze_repo(
330
440
  commit_count=0,
331
441
  )
332
442
 
333
- # Aggregate per-file metrics
334
443
  file_metrics: dict[str, dict] = defaultdict(
335
444
  lambda: {
336
445
  "change_count": 0,
@@ -342,7 +451,7 @@ async def analyze_repo(
342
451
  }
343
452
  )
344
453
 
345
- now = datetime.now(timezone.utc)
454
+ now = datetime.now(UTC)
346
455
 
347
456
  for commit in commits:
348
457
  is_fix = is_bug_fix_commit(commit["message"])
@@ -358,7 +467,6 @@ async def analyze_repo(
358
467
  m["authors"].add(commit["author"])
359
468
  m["lines_added"] += file_info["added"]
360
469
  m["lines_deleted"] += file_info["deleted"]
361
- # Track most recent change date
362
470
  if not m["last_changed"] or commit["date"] > m["last_changed"]:
363
471
  m["last_changed"] = commit["date"]
364
472
 
@@ -371,7 +479,18 @@ async def analyze_repo(
371
479
  commit_count=len(commits),
372
480
  )
373
481
 
374
- # Compute normalization maximums
482
+ return _build_hotspot_result(name, resolved, days, len(commits), file_metrics, now)
483
+
484
+
485
+ def _build_hotspot_result(
486
+ name: str,
487
+ resolved: Path,
488
+ days: int,
489
+ commit_count: int,
490
+ file_metrics: dict[str, dict],
491
+ now: datetime,
492
+ ) -> HotspotResult:
493
+ """Build scored HotspotResult from aggregated file metrics."""
375
494
  max_changes = max(m["change_count"] for m in file_metrics.values())
376
495
  max_bugs = max(m["bug_fix_count"] for m in file_metrics.values()) or 1
377
496
  max_authors = max(len(m["authors"]) for m in file_metrics.values())
@@ -379,13 +498,11 @@ async def analyze_repo(
379
498
  m["lines_added"] + m["lines_deleted"] for m in file_metrics.values()
380
499
  ) or 1
381
500
 
382
- # Build FileHotspot list with scores
383
501
  file_hotspots = []
384
502
  for fpath, m in file_metrics.items():
385
503
  churn = m["lines_added"] + m["lines_deleted"]
386
504
 
387
- # Compute age in days
388
- age_days = days # default to full window
505
+ age_days = days
389
506
  if m["last_changed"]:
390
507
  try:
391
508
  last_dt = datetime.fromisoformat(m["last_changed"])
@@ -420,10 +537,7 @@ async def analyze_repo(
420
537
  )
421
538
  )
422
539
 
423
- # Sort by score descending
424
540
  file_hotspots.sort(key=lambda h: h.hotspot_score, reverse=True)
425
-
426
- # Aggregate directories
427
541
  directory_hotspots = _aggregate_by_directory(file_hotspots)
428
542
 
429
543
  return HotspotResult(
@@ -431,7 +545,7 @@ async def analyze_repo(
431
545
  repo_name=name,
432
546
  repo_path=str(resolved),
433
547
  time_window_days=days,
434
- commit_count=len(commits),
548
+ commit_count=commit_count,
435
549
  file_hotspots=file_hotspots,
436
550
  directory_hotspots=directory_hotspots,
437
551
  )
@@ -44,8 +44,8 @@ def _common_options(fn):
44
44
 
45
45
  def _run_analysis(repo: str | None, repo_path: str | None, days: int, exclude: tuple, branch: str, skip_type: tuple = ()):
46
46
  """Run analysis and return result."""
47
- from pennyfarthing_scripts.hotspots.analyze import analyze_all_repos, analyze_repo
48
47
  from pennyfarthing_scripts.common.config import get_project_root
48
+ from pennyfarthing_scripts.hotspots.analyze import analyze_all_repos, analyze_repo
49
49
 
50
50
  excludes = list(exclude) if exclude else None
51
51
  skip_types = list(skip_type) if skip_type else None
@@ -90,7 +90,7 @@ def _output_result(result, fmt: str, output_file: str | None, top: int, mode: st
90
90
  format_file_table,
91
91
  format_summary,
92
92
  )
93
- from pennyfarthing_scripts.hotspots.models import HotspotResult, MultiRepoHotspotResult
93
+ from pennyfarthing_scripts.hotspots.models import MultiRepoHotspotResult
94
94
 
95
95
  # Collect all repo results
96
96
  if isinstance(result, MultiRepoHotspotResult):
@@ -7,7 +7,6 @@ Follows ADR-0008 result pattern — structured dataclasses with success/error fi
7
7
  from __future__ import annotations
8
8
 
9
9
  from dataclasses import dataclass, field
10
- from datetime import datetime
11
10
 
12
11
 
13
12
  @dataclass
@@ -20,6 +20,21 @@ Usage:
20
20
  """
21
21
 
22
22
  # Re-export from client for backwards compatibility
23
+ # Import submodules to make them accessible
24
+ # CLI entry point - import module, not function, so "from jira import cli" gets the module
25
+ from pennyfarthing_scripts.jira import (
26
+ bidirectional,
27
+ claim,
28
+ cli,
29
+ client,
30
+ create,
31
+ epic,
32
+ operations,
33
+ reconcile,
34
+ story,
35
+ sync,
36
+ )
37
+ from pennyfarthing_scripts.jira.cli import main
23
38
  from pennyfarthing_scripts.jira.client import (
24
39
  # Constants
25
40
  GITHUB_TO_JIRA_MAP,
@@ -44,23 +59,6 @@ from pennyfarthing_scripts.jira.client import (
44
59
  update_issue_status,
45
60
  )
46
61
 
47
- # Import submodules to make them accessible
48
- from pennyfarthing_scripts.jira import (
49
- bidirectional,
50
- claim,
51
- client,
52
- create,
53
- epic,
54
- operations,
55
- reconcile,
56
- story,
57
- sync,
58
- )
59
-
60
- # CLI entry point - import module, not function, so "from jira import cli" gets the module
61
- from pennyfarthing_scripts.jira import cli
62
- from pennyfarthing_scripts.jira.cli import main
63
-
64
62
  __all__ = [
65
63
  # Constants
66
64
  "GITHUB_TO_JIRA_MAP",
@@ -28,11 +28,10 @@ from dataclasses import dataclass, field
28
28
  from pathlib import Path
29
29
  from typing import Any, Literal
30
30
 
31
- from pennyfarthing_scripts.jira.client import JiraClient, map_jira_to_status, map_status_to_jira
32
31
  from pennyfarthing_scripts.common.output import error, info, success, warn
32
+ from pennyfarthing_scripts.jira.client import JiraClient, map_jira_to_status, map_status_to_jira
33
33
  from pennyfarthing_scripts.sprint.loader import get_all_stories, load_sprint
34
34
 
35
-
36
35
  # =============================================================================
37
36
  # Data Classes
38
37
  # =============================================================================
@@ -520,7 +519,7 @@ async def async_main(args: argparse.Namespace) -> int:
520
519
  tasks = [client.get_issue_async(key) for key in jira_keys]
521
520
  results = await asyncio.gather(*tasks, return_exceptions=True)
522
521
 
523
- for key, result in zip(jira_keys, results):
522
+ for key, result in zip(jira_keys, results, strict=False):
524
523
  if isinstance(result, Exception):
525
524
  warn(f"Failed to fetch {key}: {result}")
526
525
  elif result:
@@ -162,6 +162,27 @@ def claim_story(issue_key: str) -> dict[str, Any]:
162
162
  "exit_code": 3,
163
163
  }
164
164
 
165
+ # Update sprint YAML assigned_to field
166
+ try:
167
+ from pennyfarthing_scripts.common.config import get_project_root
168
+ from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
169
+
170
+ root = get_project_root()
171
+ sprint_path = root / "sprint" / "current-sprint.yaml"
172
+ if sprint_path.exists():
173
+ data = read_sprint(sprint_path)
174
+ for epic in data.get("epics", []):
175
+ if not isinstance(epic, dict):
176
+ continue
177
+ for story in epic.get("stories", []):
178
+ if story.get("jira") == issue_key:
179
+ story["assigned_to"] = current_user
180
+ write_sprint(sprint_path, data)
181
+ actions.append("Sprint YAML assigned_to set")
182
+ break
183
+ except Exception:
184
+ pass # Best-effort — Jira claim already succeeded
185
+
165
186
  return {
166
187
  "success": True,
167
188
  "actions": actions,
@@ -206,7 +206,7 @@ def create_standalone(title, points, description, dry_run):
206
206
  click.echo(f"[DRY-RUN] Would create: {title}")
207
207
  click.echo(f" Points: {points}")
208
208
  click.echo(f" Sprint: {sprint_id}")
209
- click.echo(f" Actions: create -> add to sprint -> transition to Done")
209
+ click.echo(" Actions: create -> add to sprint -> transition to Done")
210
210
  return
211
211
 
212
212
  client = get_client()
@@ -241,7 +241,7 @@ def create_standalone(title, points, description, dry_run):
241
241
  # 4. Transition to Done
242
242
  result = client.transition_sync(jira_key, "Done")
243
243
  if result.get("success"):
244
- click.echo(f"Transitioned to Done")
244
+ click.echo("Transitioned to Done")
245
245
  else:
246
246
  click.echo(f"Warning: could not transition to Done: {result.get('reason')}")
247
247
 
@@ -515,7 +515,7 @@ class JiraClient:
515
515
  f"Available: {available}",
516
516
  }
517
517
 
518
- result = self._call_api_sync(
518
+ self._call_api_sync(
519
519
  "POST",
520
520
  f"/rest/api/3/issue/{issue_key}/transitions",
521
521
  {"transition": {"id": transition_id}},
@@ -553,7 +553,7 @@ class JiraClient:
553
553
  account_id = None
554
554
 
555
555
  payload = {"accountId": account_id}
556
- result = self._call_api_sync(
556
+ self._call_api_sync(
557
557
  "PUT", f"/rest/api/3/issue/{issue_key}/assignee", payload
558
558
  )
559
559
  # Assign PUT returns empty body on success (204)
@@ -569,7 +569,7 @@ class JiraClient:
569
569
  Returns:
570
570
  Result dict with success status
571
571
  """
572
- result = self._call_api_sync(
572
+ self._call_api_sync(
573
573
  "POST",
574
574
  f"/rest/agile/1.0/sprint/{sprint_id}/issue",
575
575
  {"issues": [issue_key]},
@@ -619,7 +619,7 @@ class JiraClient:
619
619
  Returns:
620
620
  Result dict with success status
621
621
  """
622
- result = self._call_api_sync(
622
+ self._call_api_sync(
623
623
  "POST",
624
624
  "/rest/api/3/issueLink",
625
625
  {
@@ -16,7 +16,7 @@ import sys
16
16
  from pathlib import Path
17
17
  from typing import Any
18
18
 
19
- from pennyfarthing_scripts.jira.client import JIRA_PROJECT, JiraClient, get_client
19
+ from pennyfarthing_scripts.jira.client import JIRA_PROJECT, get_client
20
20
  from pennyfarthing_scripts.sprint.loader import find_epic, find_story, load_sprint
21
21
  from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
22
22
 
@@ -150,6 +150,7 @@ def create_epic_in_jira(
150
150
  *,
151
151
  sprint_path: Path | None = None,
152
152
  dry_run: bool = False,
153
+ force: bool = False,
153
154
  ) -> dict[str, Any]:
154
155
  """Create a Jira epic and its child stories from sprint YAML.
155
156
 
@@ -157,6 +158,7 @@ def create_epic_in_jira(
157
158
  epic_id: Epic ID from sprint YAML (e.g., "epic-63" or "63")
158
159
  sprint_path: Path to sprint YAML (defaults to auto-detect)
159
160
  dry_run: If True, preview without creating
161
+ force: If True, create even if duplicate title detected
160
162
 
161
163
  Returns:
162
164
  {success, epic_key?, stories: [{id, jira_key}], error?}
@@ -184,6 +186,13 @@ def create_epic_in_jira(
184
186
  if not epic_ruamel:
185
187
  return {"success": False, "error": f"Epic '{epic_id}' not found in ruamel data"}
186
188
 
189
+ # Validate epic shard before any Jira operations (ADR-0022)
190
+ from pennyfarthing_scripts.sprint.validator import validate_epic_shard
191
+ validation = validate_epic_shard(dict(epic_ruamel))
192
+ if not validation.valid:
193
+ error_msgs = "; ".join(e.message for e in validation.errors)
194
+ return {"success": False, "error": f"Epic validation failed: {error_msgs}"}
195
+
187
196
  title = epic.get("title", f"Epic {epic_id}")
188
197
  description = epic.get("description", "")
189
198
  epic_jira_key = epic.get("jira")
@@ -200,6 +209,41 @@ def create_epic_in_jira(
200
209
  epic_jira_key = "MSSCI-XXXXX"
201
210
  else:
202
211
  client = get_client()
212
+
213
+ # Idempotency check: search for existing epic with same title (ADR-0022)
214
+ try:
215
+ existing = client.search_issues_sync(
216
+ f'project = {JIRA_PROJECT} AND issuetype = Epic AND summary ~ "{title}"'
217
+ )
218
+ if existing and not force:
219
+ existing_key = existing[0]["key"]
220
+ print(f"Found existing epic with same title: {existing_key}")
221
+ epic_jira_key = existing_key
222
+
223
+ # Update YAML with existing key
224
+ epic_ruamel["jira"] = epic_jira_key
225
+ write_sprint(path, data)
226
+
227
+ return {
228
+ "success": True,
229
+ "epic_key": epic_jira_key,
230
+ "duplicate_detected": True,
231
+ "stories": [],
232
+ }
233
+ elif existing and force:
234
+ import warnings
235
+ warnings.warn(
236
+ f"Duplicate epic title detected ({existing[0]['key']}), "
237
+ "proceeding with --force",
238
+ stacklevel=2,
239
+ )
240
+ except Exception as exc:
241
+ import warnings
242
+ warnings.warn(
243
+ f"Jira search for duplicate titles failed: {exc}",
244
+ stacklevel=2,
245
+ )
246
+
203
247
  payload = {
204
248
  "fields": {
205
249
  "project": {"key": JIRA_PROJECT},
@@ -17,8 +17,9 @@ import json
17
17
  import sys
18
18
  from typing import Any
19
19
 
20
- from pennyfarthing_scripts.jira.client import JiraClient, JIRA_PROJECT
21
- from pennyfarthing_scripts.sprint.loader import find_epic, load_sprint as load_current_sprint
20
+ from pennyfarthing_scripts.jira.client import JIRA_PROJECT, JiraClient
21
+ from pennyfarthing_scripts.sprint.loader import find_epic
22
+ from pennyfarthing_scripts.sprint.loader import load_sprint as load_current_sprint
22
23
 
23
24
 
24
25
  def parse_args(args: list[str] | None = None) -> argparse.Namespace:
@@ -23,7 +23,6 @@ from pennyfarthing_scripts.jira.client import (
23
23
  JIRA_PROJECT,
24
24
  get_client,
25
25
  get_jira_field,
26
- map_jira_to_status,
27
26
  map_status_to_jira,
28
27
  )
29
28
  from pennyfarthing_scripts.sprint.loader import load_sprint
@@ -24,6 +24,8 @@ from pennyfarthing_scripts.sprint.loader import (
24
24
  find_epic,
25
25
  find_story,
26
26
  get_story_by_id,
27
+ )
28
+ from pennyfarthing_scripts.sprint.loader import (
27
29
  load_sprint as load_current_sprint,
28
30
  )
29
31
 
@@ -17,13 +17,13 @@ from dataclasses import dataclass, field
17
17
  from typing import Any
18
18
 
19
19
  from pennyfarthing_scripts.common.config import get_project_root
20
+ from pennyfarthing_scripts.common.output import error, info, success, warn
20
21
  from pennyfarthing_scripts.jira.client import (
21
22
  JiraClient,
22
23
  extract_jira_key,
23
24
  get_jira_field,
24
25
  map_status_to_jira,
25
26
  )
26
- from pennyfarthing_scripts.common.output import error, info, success, warn
27
27
  from pennyfarthing_scripts.sprint.loader import find_epic, load_sprint
28
28
 
29
29
 
@@ -10,7 +10,6 @@ import re
10
10
  from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
12
 
13
-
14
13
  # Tag requirements
15
14
  REQUIRED_TAGS = ["run", "output"]
16
15
  RECOMMENDED_TAGS = ["args", "example", "when"]
@@ -10,7 +10,6 @@ import re
10
10
  from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
12
 
13
-
14
13
  # Tag requirements
15
14
  REQUIRED_TAGS = ["step-meta", "purpose", "instructions", "output"]
16
15
  RECOMMENDED_TAGS = ["prerequisites", "actions", "collaboration-menu", "next-step"]
@@ -20,12 +20,20 @@ from pennyfarthing_scripts.migration.session import (
20
20
  )
21
21
  from pennyfarthing_scripts.migration.skill import (
22
22
  RECOMMENDED_TAGS as SKILL_RECOMMENDED,
23
+ )
24
+ from pennyfarthing_scripts.migration.skill import (
23
25
  REQUIRED_TAGS as SKILL_REQUIRED,
26
+ )
27
+ from pennyfarthing_scripts.migration.skill import (
24
28
  find_skill_files,
25
29
  )
26
30
  from pennyfarthing_scripts.migration.step import (
27
31
  RECOMMENDED_TAGS as STEP_RECOMMENDED,
32
+ )
33
+ from pennyfarthing_scripts.migration.step import (
28
34
  REQUIRED_TAGS as STEP_REQUIRED,
35
+ )
36
+ from pennyfarthing_scripts.migration.step import (
29
37
  STEP_META_FIELDS,
30
38
  find_step_files,
31
39
  )
@@ -119,7 +127,6 @@ def validate_skill_file(file_path: Path) -> ValidationResult:
119
127
  Returns:
120
128
  ValidationResult with errors/warnings
121
129
  """
122
- skill_name = file_path.parent.name
123
130
  result = ValidationResult(file_path=file_path, file_type="skill")
124
131
  content = file_path.read_text()
125
132
 
@@ -157,10 +164,6 @@ def validate_step_file(file_path: Path) -> ValidationResult:
157
164
  result = ValidationResult(file_path=file_path, file_type="step")
158
165
  content = file_path.read_text()
159
166
 
160
- # Determine workflow/step names from path
161
- workflow_name = file_path.parent.parent.name
162
- step_name = file_path.stem
163
-
164
167
  # Check required tags
165
168
  for tag in STEP_REQUIRED:
166
169
  if not _has_tag(content, tag):
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
  import re
14
14
  import subprocess
15
15
  import time
16
- from dataclasses import dataclass, asdict
16
+ from dataclasses import asdict, dataclass
17
17
  from datetime import datetime
18
18
  from pathlib import Path
19
19
  from typing import Any
@@ -38,7 +38,7 @@ class PatchState:
38
38
  return yaml.dump(asdict(self), default_flow_style=False)
39
39
 
40
40
  @classmethod
41
- def from_yaml(cls, yaml_str: str) -> "PatchState":
41
+ def from_yaml(cls, yaml_str: str) -> PatchState:
42
42
  """Deserialize state from YAML string."""
43
43
  data = yaml.safe_load(yaml_str)
44
44
  return cls(**data)
@@ -5,8 +5,8 @@ to validate story completion before finishing.
5
5
  """
6
6
 
7
7
  from pennyfarthing_scripts.preflight.finish import (
8
- PreflightResult,
9
8
  PreflightIssue,
9
+ PreflightResult,
10
10
  run_finish_preflight,
11
11
  )
12
12