@pennyfarthing/core 10.0.2 → 10.0.5

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 (722) hide show
  1. package/README.md +287 -0
  2. package/package.json +29 -41
  3. package/{dist → packages/core/dist}/cli/commands/cyclist.d.ts +5 -1
  4. package/packages/core/dist/cli/commands/cyclist.d.ts.map +1 -0
  5. package/{dist → packages/core/dist}/cli/commands/cyclist.js +4 -4
  6. package/packages/core/dist/cli/commands/cyclist.js.map +1 -0
  7. package/{dist → packages/core/dist}/cli/commands/cyclist.test.js +2 -2
  8. package/packages/core/dist/cli/commands/cyclist.test.js.map +1 -0
  9. package/packages/core/dist/cli/commands/doctor-file-layout.test.d.ts +13 -0
  10. package/packages/core/dist/cli/commands/doctor-file-layout.test.d.ts.map +1 -0
  11. package/packages/core/dist/cli/commands/doctor-file-layout.test.js +234 -0
  12. package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -0
  13. package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.js +17 -16
  14. package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.js.map +1 -1
  15. package/{dist → packages/core/dist}/cli/commands/doctor.d.ts +8 -0
  16. package/{dist → packages/core/dist}/cli/commands/doctor.d.ts.map +1 -1
  17. package/{dist → packages/core/dist}/cli/commands/doctor.js +224 -3
  18. package/packages/core/dist/cli/commands/doctor.js.map +1 -0
  19. package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.js +1 -1
  20. package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.js.map +1 -1
  21. package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.js +1 -1
  22. package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.js.map +1 -1
  23. package/packages/core/dist/cli/commands/hooks-consolidation.test.d.ts +19 -0
  24. package/packages/core/dist/cli/commands/hooks-consolidation.test.d.ts.map +1 -0
  25. package/packages/core/dist/cli/commands/hooks-consolidation.test.js +358 -0
  26. package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -0
  27. package/{dist → packages/core/dist}/cli/commands/init.d.ts.map +1 -1
  28. package/{dist → packages/core/dist}/cli/commands/init.js +3 -0
  29. package/packages/core/dist/cli/commands/init.js.map +1 -0
  30. package/{dist → packages/core/dist}/cli/commands/update.d.ts.map +1 -1
  31. package/{dist → packages/core/dist}/cli/commands/update.js +53 -1
  32. package/{dist → packages/core/dist}/cli/commands/update.js.map +1 -1
  33. package/{dist → packages/core/dist}/cli/ocean-profiles.test.js +1 -1
  34. package/{dist → packages/core/dist}/cli/ocean-profiles.test.js.map +1 -1
  35. package/{dist → packages/core/dist}/cli/utils/files.d.ts +10 -0
  36. package/{dist → packages/core/dist}/cli/utils/files.d.ts.map +1 -1
  37. package/{dist → packages/core/dist}/cli/utils/files.js +35 -0
  38. package/{dist → packages/core/dist}/cli/utils/files.js.map +1 -1
  39. package/{dist → packages/core/dist}/cli/utils/settings.d.ts.map +1 -1
  40. package/{dist → packages/core/dist}/cli/utils/settings.js +24 -0
  41. package/packages/core/dist/cli/utils/settings.js.map +1 -0
  42. package/{dist → packages/core/dist}/cli/utils/themes.d.ts +1 -0
  43. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -0
  44. package/{dist → packages/core/dist}/cli/utils/themes.js.map +1 -1
  45. package/{dist → packages/core/dist}/scripts/generate-report.d.ts.map +1 -1
  46. package/{dist → packages/core/dist}/scripts/generate-report.js +11 -7
  47. package/packages/core/dist/scripts/generate-report.js.map +1 -0
  48. package/{dist → packages/core/dist}/scripts/generate-spider-report.d.ts.map +1 -1
  49. package/{dist → packages/core/dist}/scripts/generate-spider-report.js +12 -8
  50. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -0
  51. package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -0
  52. package/{dist → packages/core/dist}/scripts/generate-spider.js +6 -4
  53. package/packages/core/dist/scripts/generate-spider.js.map +1 -0
  54. package/{dist → packages/core/dist}/scripts/generate-spider.test.js +2 -2
  55. package/packages/core/dist/scripts/generate-spider.test.js.map +1 -0
  56. package/pennyfarthing-dist/agents/pm.md +1 -1
  57. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  58. package/pennyfarthing-dist/agents/sm-setup.md +6 -6
  59. package/pennyfarthing-dist/agents/sm.md +12 -6
  60. package/pennyfarthing-dist/agents/workflow-status-check.md +1 -1
  61. package/pennyfarthing-dist/commands/repo-status.md +2 -2
  62. package/pennyfarthing-dist/commands/sprint.md +8 -8
  63. package/pennyfarthing-dist/guides/bell-mode.md +65 -0
  64. package/pennyfarthing-dist/guides/benchmarks.md +62 -0
  65. package/pennyfarthing-dist/guides/bikelane.md +86 -0
  66. package/pennyfarthing-dist/guides/prime.md +72 -0
  67. package/pennyfarthing-dist/guides/reflector.md +59 -0
  68. package/pennyfarthing-dist/guides/relay-mode.md +53 -0
  69. package/pennyfarthing-dist/guides/skill-schema.md +25 -26
  70. package/pennyfarthing-dist/guides/tirepump.md +54 -0
  71. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  72. package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +59 -58
  73. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +10 -10
  74. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +10 -10
  75. package/pennyfarthing-dist/personas/themes/dune.yaml +64 -69
  76. package/pennyfarthing-dist/personas/themes/firefly.yaml +60 -73
  77. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +60 -69
  78. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +59 -73
  79. package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +45 -57
  80. package/pennyfarthing-dist/personas/themes/mad-max.yaml +5 -11
  81. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +53 -63
  82. package/pennyfarthing-dist/personas/themes/sandman.yaml +59 -59
  83. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +61 -62
  84. package/pennyfarthing-dist/personas/themes/west-wing.yaml +8 -9
  85. package/pennyfarthing-dist/scripts/README.md +2 -2
  86. package/pennyfarthing-dist/scripts/git/git-status-all.sh +1 -1
  87. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +3 -3
  88. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +32 -0
  89. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -7
  90. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +1 -1
  91. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +12 -91
  92. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +11 -86
  93. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +11 -255
  94. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +3 -3
  95. package/pennyfarthing-dist/scripts/sprint/README.md +32 -17
  96. package/pennyfarthing-dist/scripts/story/README.md +1 -1
  97. package/pennyfarthing-dist/scripts/test/test-setup.sh +1 -1
  98. package/pennyfarthing-dist/skills/jira/SKILL.md +107 -408
  99. package/pennyfarthing-dist/skills/skill-registry.yaml +21 -12
  100. package/pennyfarthing-dist/skills/sprint/skill.md +386 -68
  101. package/pennyfarthing-dist/skills/story/skill.md +14 -206
  102. package/pennyfarthing-dist/templates/settings.local.json.template +9 -1
  103. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +1 -1
  104. package/pennyfarthing-dist/workflows/git-cleanup.yaml +1 -1
  105. package/pennyfarthing-dist/workflows/project-setup/steps/step-10-complete.md +1 -1
  106. package/pennyfarthing_scripts/README.md +66 -0
  107. package/pennyfarthing_scripts/__init__.py +17 -0
  108. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  109. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  110. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  111. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  112. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  113. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  114. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  122. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  123. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  124. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  125. package/pennyfarthing_scripts/bellmode_hook.py +154 -0
  126. package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
  127. package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
  128. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/brownfield/cli.py +131 -0
  133. package/pennyfarthing_scripts/brownfield/discover.py +753 -0
  134. package/pennyfarthing_scripts/cli.py +184 -0
  135. package/pennyfarthing_scripts/common/__init__.py +49 -0
  136. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  137. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  138. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/common/config.py +92 -0
  141. package/pennyfarthing_scripts/common/output.py +180 -0
  142. package/pennyfarthing_scripts/common/themes.py +253 -0
  143. package/pennyfarthing_scripts/config.py +21 -0
  144. package/pennyfarthing_scripts/context.py +414 -0
  145. package/pennyfarthing_scripts/git/__init__.py +29 -0
  146. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  147. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  148. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  149. package/pennyfarthing_scripts/git/create_branches.py +439 -0
  150. package/pennyfarthing_scripts/git/status_all.py +310 -0
  151. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +7 -0
  152. package/pennyfarthing_scripts/hooks.py +454 -0
  153. package/pennyfarthing_scripts/hotspots/__init__.py +31 -0
  154. package/pennyfarthing_scripts/hotspots/__main__.py +6 -0
  155. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  156. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  157. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  158. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  159. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  160. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/hotspots/analyze.py +472 -0
  162. package/pennyfarthing_scripts/hotspots/cli.py +152 -0
  163. package/pennyfarthing_scripts/hotspots/formatters.py +109 -0
  164. package/pennyfarthing_scripts/hotspots/models.py +60 -0
  165. package/pennyfarthing_scripts/jira/__init__.py +99 -0
  166. package/pennyfarthing_scripts/jira/__main__.py +10 -0
  167. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  172. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
  183. package/pennyfarthing_scripts/jira/claim.py +211 -0
  184. package/pennyfarthing_scripts/jira/cli.py +351 -0
  185. package/pennyfarthing_scripts/jira/client.py +762 -0
  186. package/pennyfarthing_scripts/jira/create.py +267 -0
  187. package/pennyfarthing_scripts/jira/epic.py +176 -0
  188. package/pennyfarthing_scripts/jira/operations.py +124 -0
  189. package/pennyfarthing_scripts/jira/reconcile.py +277 -0
  190. package/pennyfarthing_scripts/jira/story.py +219 -0
  191. package/pennyfarthing_scripts/jira/sync.py +350 -0
  192. package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
  193. package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
  194. package/pennyfarthing_scripts/jira_sync.py +36 -0
  195. package/pennyfarthing_scripts/jira_sync_story.py +30 -0
  196. package/pennyfarthing_scripts/migration/__init__.py +39 -0
  197. package/pennyfarthing_scripts/migration/__main__.py +10 -0
  198. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/migration/cli.py +304 -0
  206. package/pennyfarthing_scripts/migration/session.py +384 -0
  207. package/pennyfarthing_scripts/migration/skill.py +188 -0
  208. package/pennyfarthing_scripts/migration/step.py +229 -0
  209. package/pennyfarthing_scripts/migration/validate.py +282 -0
  210. package/pennyfarthing_scripts/output.py +37 -0
  211. package/pennyfarthing_scripts/patch_mode.py +449 -0
  212. package/pennyfarthing_scripts/preflight/__init__.py +17 -0
  213. package/pennyfarthing_scripts/preflight/__main__.py +10 -0
  214. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/preflight/cli.py +141 -0
  219. package/pennyfarthing_scripts/preflight/finish.py +382 -0
  220. package/pennyfarthing_scripts/pretooluse_hook.py +193 -0
  221. package/pennyfarthing_scripts/prime/__init__.py +125 -0
  222. package/pennyfarthing_scripts/prime/__main__.py +8 -0
  223. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/prime/cli.py +645 -0
  233. package/pennyfarthing_scripts/prime/loader.py +239 -0
  234. package/pennyfarthing_scripts/prime/models.py +206 -0
  235. package/pennyfarthing_scripts/prime/persona.py +309 -0
  236. package/pennyfarthing_scripts/prime/session.py +183 -0
  237. package/pennyfarthing_scripts/prime/tiers.py +201 -0
  238. package/pennyfarthing_scripts/prime/workflow.py +277 -0
  239. package/pennyfarthing_scripts/schema_validation_hook.py +306 -0
  240. package/pennyfarthing_scripts/sprint/__init__.py +66 -0
  241. package/pennyfarthing_scripts/sprint/__main__.py +10 -0
  242. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  243. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  244. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/sprint/archive.py +165 -0
  257. package/pennyfarthing_scripts/sprint/archive_epic.py +408 -0
  258. package/pennyfarthing_scripts/sprint/cli.py +1863 -0
  259. package/pennyfarthing_scripts/sprint/epic_add.py +173 -0
  260. package/pennyfarthing_scripts/sprint/import_epic.py +431 -0
  261. package/pennyfarthing_scripts/sprint/loader.py +237 -0
  262. package/pennyfarthing_scripts/sprint/status.py +122 -0
  263. package/pennyfarthing_scripts/sprint/story_add.py +187 -0
  264. package/pennyfarthing_scripts/sprint/story_update.py +181 -0
  265. package/pennyfarthing_scripts/sprint/validate_cmd.py +307 -0
  266. package/pennyfarthing_scripts/sprint/validator.py +580 -0
  267. package/pennyfarthing_scripts/sprint/work.py +208 -0
  268. package/pennyfarthing_scripts/sprint/yaml_io.py +367 -0
  269. package/pennyfarthing_scripts/story/__init__.py +67 -0
  270. package/pennyfarthing_scripts/story/__main__.py +10 -0
  271. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/story/cli.py +105 -0
  278. package/pennyfarthing_scripts/story/create.py +167 -0
  279. package/pennyfarthing_scripts/story/size.py +113 -0
  280. package/pennyfarthing_scripts/story/template.py +151 -0
  281. package/pennyfarthing_scripts/swebench.py +216 -0
  282. package/pennyfarthing_scripts/tests/__init__.py +1 -0
  283. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  285. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  286. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  287. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  288. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  289. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  290. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  291. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  292. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  293. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  294. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  295. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  296. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  297. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  298. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  299. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  300. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  301. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  302. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  303. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  304. package/pennyfarthing_scripts/tests/conftest.py +106 -0
  305. package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
  306. package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
  307. package/pennyfarthing_scripts/tests/test_common.py +180 -0
  308. package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
  309. package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
  310. package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
  311. package/pennyfarthing_scripts/tests/test_patch_mode.py +830 -0
  312. package/pennyfarthing_scripts/tests/test_prime.py +1050 -0
  313. package/pennyfarthing_scripts/tests/test_sprint_package.py +402 -0
  314. package/pennyfarthing_scripts/tests/test_sprint_validator.py +731 -0
  315. package/pennyfarthing_scripts/tests/test_story_add.py +921 -0
  316. package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
  317. package/pennyfarthing_scripts/tests/test_story_update.py +769 -0
  318. package/pennyfarthing_scripts/tests/test_tiers.py +1090 -0
  319. package/pennyfarthing_scripts/tests/test_token_counting.py +559 -0
  320. package/pennyfarthing_scripts/tests/test_validate_cmd.py +500 -0
  321. package/pennyfarthing_scripts/tests/test_workflow_check.py +341 -0
  322. package/pennyfarthing_scripts/tests/test_yaml_io.py +815 -0
  323. package/pennyfarthing_scripts/welcome_hook.py +157 -0
  324. package/pennyfarthing_scripts/workflow.py +287 -0
  325. package/scripts/postinstall.cjs +34 -0
  326. package/dist/cli/commands/cyclist.d.ts.map +0 -1
  327. package/dist/cli/commands/cyclist.js.map +0 -1
  328. package/dist/cli/commands/cyclist.test.js.map +0 -1
  329. package/dist/cli/commands/doctor.js.map +0 -1
  330. package/dist/cli/commands/init.js.map +0 -1
  331. package/dist/cli/utils/settings.js.map +0 -1
  332. package/dist/cli/utils/themes.d.ts.map +0 -1
  333. package/dist/scripts/generate-report.js.map +0 -1
  334. package/dist/scripts/generate-spider-report.js.map +0 -1
  335. package/dist/scripts/generate-spider.d.ts.map +0 -1
  336. package/dist/scripts/generate-spider.js.map +0 -1
  337. package/dist/scripts/generate-spider.test.js.map +0 -1
  338. package/pennyfarthing-dist/scripts/jira/jira-lib.sh +0 -464
  339. package/pennyfarthing-dist/scripts/jira/jira-sync.sh +0 -16
  340. package/pennyfarthing-dist/scripts/jira/sync-epic-to-jira.sh +0 -16
  341. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +0 -133
  342. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +0 -91
  343. package/pennyfarthing-dist/scripts/sprint/check-story.sh +0 -158
  344. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +0 -52
  345. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +0 -63
  346. package/pennyfarthing-dist/scripts/sprint/list-future.sh +0 -145
  347. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +0 -110
  348. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +0 -148
  349. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +0 -415
  350. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +0 -33
  351. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +0 -230
  352. package/pennyfarthing-dist/scripts/sprint/sprint-status.sh +0 -134
  353. package/pennyfarthing-dist/scripts/sprint/validate-sprint-yaml.sh +0 -139
  354. package/pennyfarthing-dist/skills/sprint/scripts/archive-story.sh +0 -101
  355. package/pennyfarthing-dist/skills/sprint/scripts/available-stories.sh +0 -97
  356. package/pennyfarthing-dist/skills/sprint/scripts/check-story.sh +0 -164
  357. package/pennyfarthing-dist/skills/sprint/scripts/create-jira-epic.sh +0 -101
  358. package/pennyfarthing-dist/skills/sprint/scripts/new-sprint.sh +0 -116
  359. package/pennyfarthing-dist/skills/sprint/scripts/promote-epic.sh +0 -164
  360. package/pennyfarthing-dist/skills/sprint/scripts/sprint-info.sh +0 -39
  361. package/pennyfarthing-dist/skills/sprint/scripts/sprint-status.sh +0 -147
  362. package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +0 -93
  363. /package/{bin → packages/core/bin}/pennyfarthing.js +0 -0
  364. /package/{dist → packages/core/dist}/bmad/context-reader.d.ts +0 -0
  365. /package/{dist → packages/core/dist}/bmad/context-reader.d.ts.map +0 -0
  366. /package/{dist → packages/core/dist}/bmad/context-reader.js +0 -0
  367. /package/{dist → packages/core/dist}/bmad/context-reader.js.map +0 -0
  368. /package/{dist → packages/core/dist}/bmad/context-reader.test.d.ts +0 -0
  369. /package/{dist → packages/core/dist}/bmad/context-reader.test.d.ts.map +0 -0
  370. /package/{dist → packages/core/dist}/bmad/context-reader.test.js +0 -0
  371. /package/{dist → packages/core/dist}/bmad/context-reader.test.js.map +0 -0
  372. /package/{dist → packages/core/dist}/bmad/epics-parser.d.ts +0 -0
  373. /package/{dist → packages/core/dist}/bmad/epics-parser.d.ts.map +0 -0
  374. /package/{dist → packages/core/dist}/bmad/epics-parser.js +0 -0
  375. /package/{dist → packages/core/dist}/bmad/epics-parser.js.map +0 -0
  376. /package/{dist → packages/core/dist}/bmad/epics-parser.test.d.ts +0 -0
  377. /package/{dist → packages/core/dist}/bmad/epics-parser.test.d.ts.map +0 -0
  378. /package/{dist → packages/core/dist}/bmad/epics-parser.test.js +0 -0
  379. /package/{dist → packages/core/dist}/bmad/epics-parser.test.js.map +0 -0
  380. /package/{dist → packages/core/dist}/bmad/index.d.ts +0 -0
  381. /package/{dist → packages/core/dist}/bmad/index.d.ts.map +0 -0
  382. /package/{dist → packages/core/dist}/bmad/index.js +0 -0
  383. /package/{dist → packages/core/dist}/bmad/index.js.map +0 -0
  384. /package/{dist → packages/core/dist}/bmad/status-sync.d.ts +0 -0
  385. /package/{dist → packages/core/dist}/bmad/status-sync.d.ts.map +0 -0
  386. /package/{dist → packages/core/dist}/bmad/status-sync.js +0 -0
  387. /package/{dist → packages/core/dist}/bmad/status-sync.js.map +0 -0
  388. /package/{dist → packages/core/dist}/bmad/status-sync.test.d.ts +0 -0
  389. /package/{dist → packages/core/dist}/bmad/status-sync.test.d.ts.map +0 -0
  390. /package/{dist → packages/core/dist}/bmad/status-sync.test.js +0 -0
  391. /package/{dist → packages/core/dist}/bmad/status-sync.test.js.map +0 -0
  392. /package/{dist → packages/core/dist}/bmad/story-exporter.d.ts +0 -0
  393. /package/{dist → packages/core/dist}/bmad/story-exporter.d.ts.map +0 -0
  394. /package/{dist → packages/core/dist}/bmad/story-exporter.js +0 -0
  395. /package/{dist → packages/core/dist}/bmad/story-exporter.js.map +0 -0
  396. /package/{dist → packages/core/dist}/bmad/story-exporter.test.d.ts +0 -0
  397. /package/{dist → packages/core/dist}/bmad/story-exporter.test.d.ts.map +0 -0
  398. /package/{dist → packages/core/dist}/bmad/story-exporter.test.js +0 -0
  399. /package/{dist → packages/core/dist}/bmad/story-exporter.test.js.map +0 -0
  400. /package/{dist → packages/core/dist}/bmad/story-parser.d.ts +0 -0
  401. /package/{dist → packages/core/dist}/bmad/story-parser.d.ts.map +0 -0
  402. /package/{dist → packages/core/dist}/bmad/story-parser.js +0 -0
  403. /package/{dist → packages/core/dist}/bmad/story-parser.js.map +0 -0
  404. /package/{dist → packages/core/dist}/bmad/story-parser.test.d.ts +0 -0
  405. /package/{dist → packages/core/dist}/bmad/story-parser.test.d.ts.map +0 -0
  406. /package/{dist → packages/core/dist}/bmad/story-parser.test.js +0 -0
  407. /package/{dist → packages/core/dist}/bmad/story-parser.test.js.map +0 -0
  408. /package/{dist → packages/core/dist}/cli/commands/command.d.ts +0 -0
  409. /package/{dist → packages/core/dist}/cli/commands/command.d.ts.map +0 -0
  410. /package/{dist → packages/core/dist}/cli/commands/command.js +0 -0
  411. /package/{dist → packages/core/dist}/cli/commands/command.js.map +0 -0
  412. /package/{dist → packages/core/dist}/cli/commands/cyclist.test.d.ts +0 -0
  413. /package/{dist → packages/core/dist}/cli/commands/cyclist.test.d.ts.map +0 -0
  414. /package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.d.ts +0 -0
  415. /package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.d.ts.map +0 -0
  416. /package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.d.ts +0 -0
  417. /package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.d.ts.map +0 -0
  418. /package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.d.ts +0 -0
  419. /package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.d.ts.map +0 -0
  420. /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.d.ts +0 -0
  421. /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.d.ts.map +0 -0
  422. /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.js +0 -0
  423. /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.js.map +0 -0
  424. /package/{dist → packages/core/dist}/cli/commands/init.d.ts +0 -0
  425. /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.d.ts +0 -0
  426. /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.d.ts.map +0 -0
  427. /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.js +0 -0
  428. /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.js.map +0 -0
  429. /package/{dist → packages/core/dist}/cli/commands/skill.d.ts +0 -0
  430. /package/{dist → packages/core/dist}/cli/commands/skill.d.ts.map +0 -0
  431. /package/{dist → packages/core/dist}/cli/commands/skill.js +0 -0
  432. /package/{dist → packages/core/dist}/cli/commands/skill.js.map +0 -0
  433. /package/{dist → packages/core/dist}/cli/commands/theme.d.ts +0 -0
  434. /package/{dist → packages/core/dist}/cli/commands/theme.d.ts.map +0 -0
  435. /package/{dist → packages/core/dist}/cli/commands/theme.js +0 -0
  436. /package/{dist → packages/core/dist}/cli/commands/theme.js.map +0 -0
  437. /package/{dist → packages/core/dist}/cli/commands/uninstall.d.ts +0 -0
  438. /package/{dist → packages/core/dist}/cli/commands/uninstall.d.ts.map +0 -0
  439. /package/{dist → packages/core/dist}/cli/commands/uninstall.js +0 -0
  440. /package/{dist → packages/core/dist}/cli/commands/uninstall.js.map +0 -0
  441. /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.d.ts +0 -0
  442. /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.d.ts.map +0 -0
  443. /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.js +0 -0
  444. /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.js.map +0 -0
  445. /package/{dist → packages/core/dist}/cli/commands/update.d.ts +0 -0
  446. /package/{dist → packages/core/dist}/cli/commands/version.d.ts +0 -0
  447. /package/{dist → packages/core/dist}/cli/commands/version.d.ts.map +0 -0
  448. /package/{dist → packages/core/dist}/cli/commands/version.js +0 -0
  449. /package/{dist → packages/core/dist}/cli/commands/version.js.map +0 -0
  450. /package/{dist → packages/core/dist}/cli/customization.test.d.ts +0 -0
  451. /package/{dist → packages/core/dist}/cli/customization.test.d.ts.map +0 -0
  452. /package/{dist → packages/core/dist}/cli/customization.test.js +0 -0
  453. /package/{dist → packages/core/dist}/cli/customization.test.js.map +0 -0
  454. /package/{dist → packages/core/dist}/cli/cyclist-migration.test.d.ts +0 -0
  455. /package/{dist → packages/core/dist}/cli/cyclist-migration.test.d.ts.map +0 -0
  456. /package/{dist → packages/core/dist}/cli/cyclist-migration.test.js +0 -0
  457. /package/{dist → packages/core/dist}/cli/cyclist-migration.test.js.map +0 -0
  458. /package/{dist → packages/core/dist}/cli/index.d.ts +0 -0
  459. /package/{dist → packages/core/dist}/cli/index.d.ts.map +0 -0
  460. /package/{dist → packages/core/dist}/cli/index.js +0 -0
  461. /package/{dist → packages/core/dist}/cli/index.js.map +0 -0
  462. /package/{dist → packages/core/dist}/cli/ocean-profiles.test.d.ts +0 -0
  463. /package/{dist → packages/core/dist}/cli/ocean-profiles.test.d.ts.map +0 -0
  464. /package/{dist → packages/core/dist}/cli/theme-maker.test.d.ts +0 -0
  465. /package/{dist → packages/core/dist}/cli/theme-maker.test.d.ts.map +0 -0
  466. /package/{dist → packages/core/dist}/cli/theme-maker.test.js +0 -0
  467. /package/{dist → packages/core/dist}/cli/theme-maker.test.js.map +0 -0
  468. /package/{dist → packages/core/dist}/cli/utils/constants.d.ts +0 -0
  469. /package/{dist → packages/core/dist}/cli/utils/constants.d.ts.map +0 -0
  470. /package/{dist → packages/core/dist}/cli/utils/constants.js +0 -0
  471. /package/{dist → packages/core/dist}/cli/utils/constants.js.map +0 -0
  472. /package/{dist → packages/core/dist}/cli/utils/logger.d.ts +0 -0
  473. /package/{dist → packages/core/dist}/cli/utils/logger.d.ts.map +0 -0
  474. /package/{dist → packages/core/dist}/cli/utils/logger.js +0 -0
  475. /package/{dist → packages/core/dist}/cli/utils/logger.js.map +0 -0
  476. /package/{dist → packages/core/dist}/cli/utils/manifest.d.ts +0 -0
  477. /package/{dist → packages/core/dist}/cli/utils/manifest.d.ts.map +0 -0
  478. /package/{dist → packages/core/dist}/cli/utils/manifest.js +0 -0
  479. /package/{dist → packages/core/dist}/cli/utils/manifest.js.map +0 -0
  480. /package/{dist → packages/core/dist}/cli/utils/node-modules.d.ts +0 -0
  481. /package/{dist → packages/core/dist}/cli/utils/node-modules.d.ts.map +0 -0
  482. /package/{dist → packages/core/dist}/cli/utils/node-modules.js +0 -0
  483. /package/{dist → packages/core/dist}/cli/utils/node-modules.js.map +0 -0
  484. /package/{dist → packages/core/dist}/cli/utils/prompts.d.ts +0 -0
  485. /package/{dist → packages/core/dist}/cli/utils/prompts.d.ts.map +0 -0
  486. /package/{dist → packages/core/dist}/cli/utils/prompts.js +0 -0
  487. /package/{dist → packages/core/dist}/cli/utils/prompts.js.map +0 -0
  488. /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.d.ts +0 -0
  489. /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.d.ts.map +0 -0
  490. /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.js +0 -0
  491. /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.js.map +0 -0
  492. /package/{dist → packages/core/dist}/cli/utils/settings.d.ts +0 -0
  493. /package/{dist → packages/core/dist}/cli/utils/symlinks.d.ts +0 -0
  494. /package/{dist → packages/core/dist}/cli/utils/symlinks.d.ts.map +0 -0
  495. /package/{dist → packages/core/dist}/cli/utils/symlinks.js +0 -0
  496. /package/{dist → packages/core/dist}/cli/utils/symlinks.js.map +0 -0
  497. /package/{dist → packages/core/dist}/cli/utils/themes.js +0 -0
  498. /package/{dist → packages/core/dist}/cli/utils/themes.test.d.ts +0 -0
  499. /package/{dist → packages/core/dist}/cli/utils/themes.test.d.ts.map +0 -0
  500. /package/{dist → packages/core/dist}/cli/utils/themes.test.js +0 -0
  501. /package/{dist → packages/core/dist}/cli/utils/themes.test.js.map +0 -0
  502. /package/{dist → packages/core/dist}/cli/utils/version.d.ts +0 -0
  503. /package/{dist → packages/core/dist}/cli/utils/version.d.ts.map +0 -0
  504. /package/{dist → packages/core/dist}/cli/utils/version.js +0 -0
  505. /package/{dist → packages/core/dist}/cli/utils/version.js.map +0 -0
  506. /package/{dist → packages/core/dist}/cli/workspace.test.d.ts +0 -0
  507. /package/{dist → packages/core/dist}/cli/workspace.test.d.ts.map +0 -0
  508. /package/{dist → packages/core/dist}/cli/workspace.test.js +0 -0
  509. /package/{dist → packages/core/dist}/cli/workspace.test.js.map +0 -0
  510. /package/{dist → packages/core/dist}/index.d.ts +0 -0
  511. /package/{dist → packages/core/dist}/index.d.ts.map +0 -0
  512. /package/{dist → packages/core/dist}/index.js +0 -0
  513. /package/{dist → packages/core/dist}/index.js.map +0 -0
  514. /package/{dist → packages/core/dist}/jira/jira-epic-creation.d.ts +0 -0
  515. /package/{dist → packages/core/dist}/jira/jira-epic-creation.d.ts.map +0 -0
  516. /package/{dist → packages/core/dist}/jira/jira-epic-creation.js +0 -0
  517. /package/{dist → packages/core/dist}/jira/jira-epic-creation.js.map +0 -0
  518. /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.d.ts +0 -0
  519. /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.d.ts.map +0 -0
  520. /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.js +0 -0
  521. /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.js.map +0 -0
  522. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.d.ts +0 -0
  523. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.d.ts.map +0 -0
  524. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.js +0 -0
  525. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.js.map +0 -0
  526. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.d.ts +0 -0
  527. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.d.ts.map +0 -0
  528. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.js +0 -0
  529. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.js.map +0 -0
  530. /package/{dist → packages/core/dist}/permissions/index.d.ts +0 -0
  531. /package/{dist → packages/core/dist}/permissions/index.d.ts.map +0 -0
  532. /package/{dist → packages/core/dist}/permissions/index.js +0 -0
  533. /package/{dist → packages/core/dist}/permissions/index.js.map +0 -0
  534. /package/{dist → packages/core/dist}/permissions/permission-schema.d.ts +0 -0
  535. /package/{dist → packages/core/dist}/permissions/permission-schema.d.ts.map +0 -0
  536. /package/{dist → packages/core/dist}/permissions/permission-schema.js +0 -0
  537. /package/{dist → packages/core/dist}/permissions/permission-schema.js.map +0 -0
  538. /package/{dist → packages/core/dist}/permissions/permission-schema.test.d.ts +0 -0
  539. /package/{dist → packages/core/dist}/permissions/permission-schema.test.d.ts.map +0 -0
  540. /package/{dist → packages/core/dist}/permissions/permission-schema.test.js +0 -0
  541. /package/{dist → packages/core/dist}/permissions/permission-schema.test.js.map +0 -0
  542. /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.d.ts +0 -0
  543. /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.d.ts.map +0 -0
  544. /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.js +0 -0
  545. /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.js.map +0 -0
  546. /package/{dist → packages/core/dist}/scripts/benchmark-integration.d.ts +0 -0
  547. /package/{dist → packages/core/dist}/scripts/benchmark-integration.d.ts.map +0 -0
  548. /package/{dist → packages/core/dist}/scripts/benchmark-integration.js +0 -0
  549. /package/{dist → packages/core/dist}/scripts/benchmark-integration.js.map +0 -0
  550. /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.d.ts +0 -0
  551. /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.d.ts.map +0 -0
  552. /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.js +0 -0
  553. /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.js.map +0 -0
  554. /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.d.ts +0 -0
  555. /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.d.ts.map +0 -0
  556. /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.js +0 -0
  557. /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.js.map +0 -0
  558. /package/{dist → packages/core/dist}/scripts/generate-all-spiders.d.ts +0 -0
  559. /package/{dist → packages/core/dist}/scripts/generate-all-spiders.d.ts.map +0 -0
  560. /package/{dist → packages/core/dist}/scripts/generate-all-spiders.js +0 -0
  561. /package/{dist → packages/core/dist}/scripts/generate-all-spiders.js.map +0 -0
  562. /package/{dist → packages/core/dist}/scripts/generate-report.d.ts +0 -0
  563. /package/{dist → packages/core/dist}/scripts/generate-report.test.d.ts +0 -0
  564. /package/{dist → packages/core/dist}/scripts/generate-report.test.d.ts.map +0 -0
  565. /package/{dist → packages/core/dist}/scripts/generate-report.test.js +0 -0
  566. /package/{dist → packages/core/dist}/scripts/generate-report.test.js.map +0 -0
  567. /package/{dist → packages/core/dist}/scripts/generate-spider-report.d.ts +0 -0
  568. /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.d.ts +0 -0
  569. /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.d.ts.map +0 -0
  570. /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.js +0 -0
  571. /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.js.map +0 -0
  572. /package/{dist → packages/core/dist}/scripts/generate-spider.d.ts +0 -0
  573. /package/{dist → packages/core/dist}/scripts/generate-spider.test.d.ts +0 -0
  574. /package/{dist → packages/core/dist}/scripts/generate-spider.test.d.ts.map +0 -0
  575. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.d.ts +0 -0
  576. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.d.ts.map +0 -0
  577. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.js +0 -0
  578. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.js.map +0 -0
  579. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.d.ts +0 -0
  580. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.d.ts.map +0 -0
  581. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.js +0 -0
  582. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.js.map +0 -0
  583. /package/{dist → packages/core/dist}/scripts/run-ci.test.d.ts +0 -0
  584. /package/{dist → packages/core/dist}/scripts/run-ci.test.d.ts.map +0 -0
  585. /package/{dist → packages/core/dist}/scripts/run-ci.test.js +0 -0
  586. /package/{dist → packages/core/dist}/scripts/run-ci.test.js.map +0 -0
  587. /package/{dist → packages/core/dist}/scripts/theme-detail.test.d.ts +0 -0
  588. /package/{dist → packages/core/dist}/scripts/theme-detail.test.d.ts.map +0 -0
  589. /package/{dist → packages/core/dist}/scripts/theme-detail.test.js +0 -0
  590. /package/{dist → packages/core/dist}/scripts/theme-detail.test.js.map +0 -0
  591. /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.d.ts +0 -0
  592. /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.d.ts.map +0 -0
  593. /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.js +0 -0
  594. /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.js.map +0 -0
  595. /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.d.ts +0 -0
  596. /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.d.ts.map +0 -0
  597. /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.js +0 -0
  598. /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.js.map +0 -0
  599. /package/{dist → packages/core/dist}/workflow/gate-handler.d.ts +0 -0
  600. /package/{dist → packages/core/dist}/workflow/gate-handler.d.ts.map +0 -0
  601. /package/{dist → packages/core/dist}/workflow/gate-handler.js +0 -0
  602. /package/{dist → packages/core/dist}/workflow/gate-handler.js.map +0 -0
  603. /package/{dist → packages/core/dist}/workflow/gate-handler.test.d.ts +0 -0
  604. /package/{dist → packages/core/dist}/workflow/gate-handler.test.d.ts.map +0 -0
  605. /package/{dist → packages/core/dist}/workflow/gate-handler.test.js +0 -0
  606. /package/{dist → packages/core/dist}/workflow/gate-handler.test.js.map +0 -0
  607. /package/{dist → packages/core/dist}/workflow/generic-sm-finish.d.ts +0 -0
  608. /package/{dist → packages/core/dist}/workflow/generic-sm-finish.d.ts.map +0 -0
  609. /package/{dist → packages/core/dist}/workflow/generic-sm-finish.js +0 -0
  610. /package/{dist → packages/core/dist}/workflow/generic-sm-finish.js.map +0 -0
  611. /package/{dist → packages/core/dist}/workflow/generic-sm-setup.d.ts +0 -0
  612. /package/{dist → packages/core/dist}/workflow/generic-sm-setup.d.ts.map +0 -0
  613. /package/{dist → packages/core/dist}/workflow/generic-sm-setup.js +0 -0
  614. /package/{dist → packages/core/dist}/workflow/generic-sm-setup.js.map +0 -0
  615. /package/{dist → packages/core/dist}/workflow/handoff.d.ts +0 -0
  616. /package/{dist → packages/core/dist}/workflow/handoff.d.ts.map +0 -0
  617. /package/{dist → packages/core/dist}/workflow/handoff.js +0 -0
  618. /package/{dist → packages/core/dist}/workflow/handoff.js.map +0 -0
  619. /package/{dist → packages/core/dist}/workflow/handoff.test.d.ts +0 -0
  620. /package/{dist → packages/core/dist}/workflow/handoff.test.d.ts.map +0 -0
  621. /package/{dist → packages/core/dist}/workflow/handoff.test.js +0 -0
  622. /package/{dist → packages/core/dist}/workflow/handoff.test.js.map +0 -0
  623. /package/{dist → packages/core/dist}/workflow/index.d.ts +0 -0
  624. /package/{dist → packages/core/dist}/workflow/index.d.ts.map +0 -0
  625. /package/{dist → packages/core/dist}/workflow/index.js +0 -0
  626. /package/{dist → packages/core/dist}/workflow/index.js.map +0 -0
  627. /package/{dist → packages/core/dist}/workflow/session-state.d.ts +0 -0
  628. /package/{dist → packages/core/dist}/workflow/session-state.d.ts.map +0 -0
  629. /package/{dist → packages/core/dist}/workflow/session-state.js +0 -0
  630. /package/{dist → packages/core/dist}/workflow/session-state.js.map +0 -0
  631. /package/{dist → packages/core/dist}/workflow/session-state.test.d.ts +0 -0
  632. /package/{dist → packages/core/dist}/workflow/session-state.test.d.ts.map +0 -0
  633. /package/{dist → packages/core/dist}/workflow/session-state.test.js +0 -0
  634. /package/{dist → packages/core/dist}/workflow/session-state.test.js.map +0 -0
  635. /package/{dist → packages/core/dist}/workflow/sm-subagents.test.d.ts +0 -0
  636. /package/{dist → packages/core/dist}/workflow/sm-subagents.test.d.ts.map +0 -0
  637. /package/{dist → packages/core/dist}/workflow/sm-subagents.test.js +0 -0
  638. /package/{dist → packages/core/dist}/workflow/sm-subagents.test.js.map +0 -0
  639. /package/{dist → packages/core/dist}/workflow/step-parser.d.ts +0 -0
  640. /package/{dist → packages/core/dist}/workflow/step-parser.d.ts.map +0 -0
  641. /package/{dist → packages/core/dist}/workflow/step-parser.js +0 -0
  642. /package/{dist → packages/core/dist}/workflow/step-parser.js.map +0 -0
  643. /package/{dist → packages/core/dist}/workflow/step-parser.test.d.ts +0 -0
  644. /package/{dist → packages/core/dist}/workflow/step-parser.test.d.ts.map +0 -0
  645. /package/{dist → packages/core/dist}/workflow/step-parser.test.js +0 -0
  646. /package/{dist → packages/core/dist}/workflow/step-parser.test.js.map +0 -0
  647. /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.d.ts +0 -0
  648. /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.d.ts.map +0 -0
  649. /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.js +0 -0
  650. /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.js.map +0 -0
  651. /package/{dist → packages/core/dist}/workflow/test-cache.d.ts +0 -0
  652. /package/{dist → packages/core/dist}/workflow/test-cache.d.ts.map +0 -0
  653. /package/{dist → packages/core/dist}/workflow/test-cache.js +0 -0
  654. /package/{dist → packages/core/dist}/workflow/test-cache.js.map +0 -0
  655. /package/{dist → packages/core/dist}/workflow/test-cache.test.d.ts +0 -0
  656. /package/{dist → packages/core/dist}/workflow/test-cache.test.d.ts.map +0 -0
  657. /package/{dist → packages/core/dist}/workflow/test-cache.test.js +0 -0
  658. /package/{dist → packages/core/dist}/workflow/test-cache.test.js.map +0 -0
  659. /package/{dist → packages/core/dist}/workflow/trimodal.d.ts +0 -0
  660. /package/{dist → packages/core/dist}/workflow/trimodal.d.ts.map +0 -0
  661. /package/{dist → packages/core/dist}/workflow/trimodal.js +0 -0
  662. /package/{dist → packages/core/dist}/workflow/trimodal.js.map +0 -0
  663. /package/{dist → packages/core/dist}/workflow/trimodal.test.d.ts +0 -0
  664. /package/{dist → packages/core/dist}/workflow/trimodal.test.d.ts.map +0 -0
  665. /package/{dist → packages/core/dist}/workflow/trimodal.test.js +0 -0
  666. /package/{dist → packages/core/dist}/workflow/trimodal.test.js.map +0 -0
  667. /package/{dist → packages/core/dist}/workflow/variable-resolver.d.ts +0 -0
  668. /package/{dist → packages/core/dist}/workflow/variable-resolver.d.ts.map +0 -0
  669. /package/{dist → packages/core/dist}/workflow/variable-resolver.js +0 -0
  670. /package/{dist → packages/core/dist}/workflow/variable-resolver.js.map +0 -0
  671. /package/{dist → packages/core/dist}/workflow/variable-resolver.test.d.ts +0 -0
  672. /package/{dist → packages/core/dist}/workflow/variable-resolver.test.d.ts.map +0 -0
  673. /package/{dist → packages/core/dist}/workflow/variable-resolver.test.js +0 -0
  674. /package/{dist → packages/core/dist}/workflow/variable-resolver.test.js.map +0 -0
  675. /package/{dist → packages/core/dist}/workflow/workflow-executor.d.ts +0 -0
  676. /package/{dist → packages/core/dist}/workflow/workflow-executor.d.ts.map +0 -0
  677. /package/{dist → packages/core/dist}/workflow/workflow-executor.js +0 -0
  678. /package/{dist → packages/core/dist}/workflow/workflow-executor.js.map +0 -0
  679. /package/{dist → packages/core/dist}/workflow/workflow-executor.test.d.ts +0 -0
  680. /package/{dist → packages/core/dist}/workflow/workflow-executor.test.d.ts.map +0 -0
  681. /package/{dist → packages/core/dist}/workflow/workflow-executor.test.js +0 -0
  682. /package/{dist → packages/core/dist}/workflow/workflow-executor.test.js.map +0 -0
  683. /package/{dist → packages/core/dist}/workflow/workflow-loader.d.ts +0 -0
  684. /package/{dist → packages/core/dist}/workflow/workflow-loader.d.ts.map +0 -0
  685. /package/{dist → packages/core/dist}/workflow/workflow-loader.js +0 -0
  686. /package/{dist → packages/core/dist}/workflow/workflow-loader.js.map +0 -0
  687. /package/{dist → packages/core/dist}/workflow/workflow-loader.test.d.ts +0 -0
  688. /package/{dist → packages/core/dist}/workflow/workflow-loader.test.d.ts.map +0 -0
  689. /package/{dist → packages/core/dist}/workflow/workflow-loader.test.js +0 -0
  690. /package/{dist → packages/core/dist}/workflow/workflow-loader.test.js.map +0 -0
  691. /package/{dist → packages/core/dist}/workflow/workflow-migration.test.d.ts +0 -0
  692. /package/{dist → packages/core/dist}/workflow/workflow-migration.test.d.ts.map +0 -0
  693. /package/{dist → packages/core/dist}/workflow/workflow-migration.test.js +0 -0
  694. /package/{dist → packages/core/dist}/workflow/workflow-migration.test.js.map +0 -0
  695. /package/{dist → packages/core/dist}/workflow/workflow-permissions.d.ts +0 -0
  696. /package/{dist → packages/core/dist}/workflow/workflow-permissions.d.ts.map +0 -0
  697. /package/{dist → packages/core/dist}/workflow/workflow-permissions.js +0 -0
  698. /package/{dist → packages/core/dist}/workflow/workflow-permissions.js.map +0 -0
  699. /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.d.ts +0 -0
  700. /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.d.ts.map +0 -0
  701. /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.js +0 -0
  702. /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.js.map +0 -0
  703. /package/{dist → packages/core/dist}/workflow/workflow-router.d.ts +0 -0
  704. /package/{dist → packages/core/dist}/workflow/workflow-router.d.ts.map +0 -0
  705. /package/{dist → packages/core/dist}/workflow/workflow-router.js +0 -0
  706. /package/{dist → packages/core/dist}/workflow/workflow-router.js.map +0 -0
  707. /package/{dist → packages/core/dist}/workflow/workflow-router.test.d.ts +0 -0
  708. /package/{dist → packages/core/dist}/workflow/workflow-router.test.d.ts.map +0 -0
  709. /package/{dist → packages/core/dist}/workflow/workflow-router.test.js +0 -0
  710. /package/{dist → packages/core/dist}/workflow/workflow-router.test.js.map +0 -0
  711. /package/{dist → packages/core/dist}/workflow/workflow-schema.d.ts +0 -0
  712. /package/{dist → packages/core/dist}/workflow/workflow-schema.d.ts.map +0 -0
  713. /package/{dist → packages/core/dist}/workflow/workflow-schema.js +0 -0
  714. /package/{dist → packages/core/dist}/workflow/workflow-schema.js.map +0 -0
  715. /package/{dist → packages/core/dist}/workflow/workflow-schema.test.d.ts +0 -0
  716. /package/{dist → packages/core/dist}/workflow/workflow-schema.test.d.ts.map +0 -0
  717. /package/{dist → packages/core/dist}/workflow/workflow-schema.test.js +0 -0
  718. /package/{dist → packages/core/dist}/workflow/workflow-schema.test.js.map +0 -0
  719. /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.d.ts +0 -0
  720. /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.d.ts.map +0 -0
  721. /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.js +0 -0
  722. /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.js.map +0 -0
@@ -0,0 +1,1863 @@
1
+ """
2
+ Sprint CLI - Click-based CLI for sprint operations.
3
+
4
+ Usage:
5
+ pf sprint [COMMAND] [ARGS]...
6
+
7
+ Commands:
8
+ status Show sprint status
9
+ backlog Show available stories
10
+ work Start work on a story
11
+ archive Archive a completed story
12
+ story Story subcommands (show, add, update, size, template, finish, claim)
13
+ epic Epic subcommands (show, add, promote, archive, cancel, import, remove)
14
+ initiative Initiative subcommands (show, cancel)
15
+ """
16
+
17
+ import click
18
+
19
+
20
+ @click.group()
21
+ def sprint():
22
+ """Sprint status and story operations.
23
+
24
+ \b
25
+ Commands:
26
+ status - Show sprint status
27
+ backlog - Show available stories
28
+ story - Story operations (show, add, update, size, template, finish, claim)
29
+ epic - Epic operations (show, add, promote, archive, cancel, import, remove)
30
+ initiative - Initiative operations (show, cancel)
31
+ work - Start work on a story
32
+ archive - Archive a completed story
33
+ """
34
+ pass
35
+
36
+
37
+ @sprint.command()
38
+ @click.argument("filter", required=False, type=click.Choice(
39
+ ["backlog", "todo", "in-progress", "review", "done"],
40
+ case_sensitive=False,
41
+ ))
42
+ def status(filter: str | None):
43
+ """Show sprint status.
44
+
45
+ \b
46
+ Arguments:
47
+ FILTER - Optional status filter (backlog, in-progress, done, etc.)
48
+ """
49
+ # Lazy import to maintain startup performance
50
+ from pennyfarthing_scripts.sprint.status import format_status, get_sprint_status
51
+
52
+ sprint_status = get_sprint_status(filter)
53
+ click.echo(format_status(sprint_status))
54
+
55
+
56
+ @sprint.command()
57
+ def backlog():
58
+ """Show available stories grouped by epic.
59
+
60
+ Shows stories with backlog, ready, or planning status.
61
+ Output is grouped by epic with a markdown table per epic.
62
+ """
63
+ from pennyfarthing_scripts.sprint.loader import load_sprint
64
+
65
+ data = load_sprint()
66
+ if not data or "epics" not in data:
67
+ click.echo("No sprint data available")
68
+ return
69
+
70
+ sprint_info = data.get("sprint", {})
71
+ click.echo(f"# Available Stories - {sprint_info.get('name', 'Unknown Sprint')}")
72
+ click.echo("")
73
+
74
+ available_statuses = {"backlog", "ready", "planning"}
75
+ total_count = 0
76
+ total_points = 0
77
+
78
+ for epic in data["epics"]:
79
+ if not isinstance(epic, dict):
80
+ continue
81
+
82
+ stories = [
83
+ s for s in epic.get("stories", [])
84
+ if s.get("status") in available_statuses
85
+ ]
86
+ if not stories:
87
+ continue
88
+
89
+ click.echo(f"### {epic.get('title', 'Unknown Epic')}")
90
+ if epic.get("description"):
91
+ desc = epic["description"].strip().split("\n")[0][:200]
92
+ click.echo(f"*{desc}*")
93
+ click.echo("")
94
+ click.echo("| ID | Title | Pts | Pri | Status | Workflow |")
95
+ click.echo("|----|-------|-----|-----|--------|----------|")
96
+
97
+ for s in stories:
98
+ title = s.get("title", "?")
99
+ if len(title) > 40:
100
+ title = title[:37] + "..."
101
+ sid = s.get("id", "?")
102
+ pts = s.get("points", "?")
103
+ pri = s.get("priority", "P2")
104
+ stat = s.get("status", "backlog")
105
+ wf = s.get("workflow", "tdd")
106
+ click.echo(f"| {sid} | {title} | {pts} | {pri} | {stat} | {wf} |")
107
+ total_count += 1
108
+ total_points += s.get("points", 0) or 0
109
+
110
+ click.echo("")
111
+
112
+ click.echo("---")
113
+ click.echo(f"**Total available:** {total_count} stories, {total_points} points")
114
+
115
+
116
+ @sprint.command()
117
+ @click.argument("story_id", required=False)
118
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
119
+ def work(story_id: str | None, dry_run: bool):
120
+ """Start work on a story.
121
+
122
+ \b
123
+ Arguments:
124
+ STORY_ID - Story ID to work on, or 'next' for highest priority
125
+ """
126
+ # Lazy import
127
+ from pennyfarthing_scripts.sprint.loader import get_stories_by_status
128
+ from pennyfarthing_scripts.sprint.work import check_story, get_next_story
129
+
130
+ if not story_id:
131
+ # Show backlog
132
+ stories = get_stories_by_status("backlog")
133
+ click.echo(f"Available stories: {len(stories)}")
134
+ for story in stories[:10]:
135
+ click.echo(f" {story.get('id')}: {story.get('title')} [{story.get('points', '?')}pts]")
136
+ return
137
+
138
+ if story_id == "next":
139
+ result = get_next_story()
140
+ else:
141
+ result = check_story(story_id)
142
+
143
+ if result.get("available"):
144
+ story = result.get("story", {})
145
+ click.echo(f"Story: {story.get('id')}")
146
+ click.echo(f"Title: {story.get('title')}")
147
+ click.echo(f"Points: {story.get('points')}")
148
+ click.echo(f"Status: Available")
149
+ else:
150
+ error_msg = result.get("error") or result.get("reason")
151
+ raise click.ClickException(f"Not available: {error_msg}")
152
+
153
+
154
+ @sprint.command()
155
+ @click.argument("story_id")
156
+ @click.argument("pr_number", required=False)
157
+ @click.option("--apply", is_flag=True, help="Also remove from current-sprint.yaml")
158
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
159
+ def archive(story_id: str, pr_number: str | None, apply: bool, dry_run: bool):
160
+ """Archive a completed story.
161
+
162
+ \b
163
+ Arguments:
164
+ STORY_ID - Story ID to archive
165
+ PR_NUMBER - Optional PR number if merged via PR
166
+ """
167
+ # Lazy import
168
+ from pennyfarthing_scripts.sprint.archive import archive_story
169
+
170
+ result = archive_story(
171
+ story_id,
172
+ pr_number=pr_number,
173
+ dry_run=dry_run,
174
+ apply=apply,
175
+ )
176
+
177
+ if result.get("success"):
178
+ if result.get("dry_run"):
179
+ click.echo(f"[DRY-RUN] {result.get('message')}")
180
+ else:
181
+ click.echo(result.get("message"))
182
+ else:
183
+ raise click.ClickException(f"Failed: {result.get('error')}")
184
+
185
+
186
+ # --- Story subgroup ---
187
+
188
+ @sprint.group()
189
+ def story():
190
+ """Story operations (show, add, update, size, template, finish, claim)."""
191
+ pass
192
+
193
+
194
+ @story.command("show")
195
+ @click.argument("story_id")
196
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
197
+ def story_show(story_id: str, output_json: bool):
198
+ """Show details for a specific story.
199
+
200
+ \b
201
+ Arguments:
202
+ STORY_ID - Story ID (e.g., MSSCI-12664 or 67-1)
203
+ """
204
+ # Lazy import
205
+ from pennyfarthing_scripts.sprint.loader import get_story_by_id
206
+
207
+ story_data = get_story_by_id(story_id)
208
+
209
+ if not story_data:
210
+ raise click.ClickException(f"Story not found: {story_id}")
211
+
212
+ if output_json:
213
+ import json
214
+
215
+ click.echo(json.dumps(story_data, indent=2))
216
+ else:
217
+ click.echo(f"Story: {story_data.get('id', story_id)}")
218
+ click.echo(f"Title: {story_data.get('title', 'N/A')}")
219
+ click.echo(f"Points: {story_data.get('points', 'N/A')}")
220
+ click.echo(f"Status: {story_data.get('status', 'N/A')}")
221
+ if story_data.get("priority"):
222
+ click.echo(f"Priority: {story_data.get('priority')}")
223
+ if story_data.get("workflow"):
224
+ click.echo(f"Workflow: {story_data.get('workflow')}")
225
+ if story_data.get("jira"):
226
+ click.echo(f"Jira: {story_data.get('jira')}")
227
+ if story_data.get("description"):
228
+ click.echo(f"Description: {story_data.get('description')}")
229
+
230
+
231
+ @story.command("size")
232
+ @click.argument("points", required=False, type=int)
233
+ def story_size(points: int | None):
234
+ """Display story sizing guidelines.
235
+
236
+ \b
237
+ Arguments:
238
+ POINTS - Optional specific point value to show guidance for
239
+ """
240
+ from pennyfarthing_scripts.story.size import format_size_info, get_sizing_guidelines
241
+
242
+ guidelines = get_sizing_guidelines(points)
243
+ click.echo(format_size_info(guidelines))
244
+
245
+
246
+ @story.command("template")
247
+ @click.argument("template_type", required=False)
248
+ def story_template(template_type: str | None):
249
+ """Display story templates by type.
250
+
251
+ \b
252
+ Arguments:
253
+ TYPE - Template type (feature, bug, refactor, chore)
254
+ """
255
+ from pennyfarthing_scripts.story.template import get_all_templates, get_template
256
+
257
+ if template_type:
258
+ template = get_template(template_type)
259
+ if template:
260
+ click.echo(f"Type: {template['type']}")
261
+ click.echo(f"Description: {template['description']}")
262
+ click.echo("")
263
+ click.echo("Template:")
264
+ click.echo(template["template"])
265
+ else:
266
+ raise click.ClickException(f"Unknown template type: {template_type}")
267
+ else:
268
+ click.echo("Available templates:")
269
+ for name, template in get_all_templates().items():
270
+ click.echo(f" {name}: {template['description']}")
271
+
272
+
273
+ @story.command("finish")
274
+ @click.argument("story_id")
275
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without executing")
276
+ def story_finish(story_id: str, dry_run: bool):
277
+ """Complete a story: archive session, merge PR, transition Jira, update sprint YAML.
278
+
279
+ \b
280
+ Arguments:
281
+ STORY_ID - Story ID (e.g., MSSCI-12052)
282
+ """
283
+ import subprocess as sp
284
+
285
+ from pennyfarthing_scripts.common.config import get_project_root
286
+
287
+ script = get_project_root() / ".pennyfarthing" / "scripts" / "workflow" / "finish-story.sh"
288
+ if not script.exists():
289
+ raise click.ClickException(f"Script not found: {script}")
290
+
291
+ cmd = [str(script), story_id]
292
+ if dry_run:
293
+ cmd.append("--dry-run")
294
+
295
+ result = sp.run(cmd, capture_output=True, text=True, cwd=str(get_project_root()))
296
+ if result.stdout:
297
+ click.echo(result.stdout.rstrip())
298
+ if result.returncode != 0:
299
+ error = result.stderr.strip() if result.stderr else "Unknown error"
300
+ raise click.ClickException(error)
301
+
302
+
303
+ @story.command("claim")
304
+ @click.argument("story_id")
305
+ @click.option("--claim/--unclaim", default=True, help="Claim or unclaim the story")
306
+ def story_claim(story_id: str, claim: bool):
307
+ """Claim or unclaim a story in Jira.
308
+
309
+ \b
310
+ Arguments:
311
+ STORY_ID - Story ID / Jira key to claim
312
+ """
313
+ from pennyfarthing_scripts.jira.claim import claim_issue, unclaim_issue
314
+
315
+ if claim:
316
+ result = claim_issue(story_id)
317
+ else:
318
+ result = unclaim_issue(story_id)
319
+
320
+ if result.get("success"):
321
+ click.echo(result.get("message", f"{'Claimed' if claim else 'Unclaimed'} {story_id}"))
322
+ else:
323
+ raise click.ClickException(result.get("error", "Unknown error"))
324
+
325
+
326
+ # Register story-add as story.add
327
+ from pennyfarthing_scripts.sprint.story_add import story_add_command
328
+
329
+ story.add_command(story_add_command, "add")
330
+
331
+ # Register story-update as story.update
332
+ from pennyfarthing_scripts.sprint.story_update import story_update_command
333
+
334
+ story.add_command(story_update_command, "update")
335
+
336
+
337
+ # --- Epic subgroup ---
338
+
339
+ @sprint.group()
340
+ def epic():
341
+ """Epic operations (show, add, promote, archive, cancel, import, remove)."""
342
+ pass
343
+
344
+
345
+ @epic.command("show")
346
+ @click.argument("epic_id")
347
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
348
+ def epic_show(epic_id: str, output_json: bool):
349
+ """Show details for a specific epic.
350
+
351
+ Searches both the current sprint and future initiative shards.
352
+
353
+ \b
354
+ Arguments:
355
+ EPIC_ID - Epic ID (e.g., epic-42 or MSSCI-14298)
356
+
357
+ \b
358
+ Examples:
359
+ pf sprint epic show MSSCI-14298
360
+ pf sprint epic show epic-42
361
+ pf sprint epic show epic-42 --json
362
+ """
363
+ import json as json_mod
364
+
365
+ from pennyfarthing_scripts.common.config import get_project_root
366
+ from pennyfarthing_scripts.sprint.loader import load_sprint
367
+
368
+ root = get_project_root()
369
+ epic_data = None
370
+ source = None
371
+
372
+ # 1. Search current sprint
373
+ sprint_data = load_sprint(root)
374
+ if sprint_data and "epics" in sprint_data:
375
+ for e in sprint_data["epics"]:
376
+ if isinstance(e, dict):
377
+ eid = str(e.get("id", ""))
378
+ ejira = str(e.get("jira", ""))
379
+ if epic_id in (eid, ejira, eid.replace("epic-", ""), f"epic-{epic_id}"):
380
+ epic_data = e
381
+ source = "current sprint"
382
+ break
383
+
384
+ # 2. Search future initiative shards
385
+ if not epic_data:
386
+ epic_data, source = _find_epic_in_initiatives(epic_id, root)
387
+
388
+ if not epic_data:
389
+ raise click.ClickException(f"Epic not found: {epic_id}")
390
+
391
+ if output_json:
392
+ # Convert to plain dict for JSON serialization
393
+ click.echo(json_mod.dumps(dict(epic_data), indent=2, default=str))
394
+ else:
395
+ click.echo(f"Epic: {epic_data.get('id', epic_id)}")
396
+ click.echo(f"Title: {epic_data.get('title', 'N/A')}")
397
+ click.echo(f"Status: {epic_data.get('status', 'N/A')}")
398
+ click.echo(f"Points: {epic_data.get('points', 'N/A')}")
399
+ click.echo(f"Source: {source}")
400
+ if epic_data.get("priority"):
401
+ click.echo(f"Priority: {epic_data.get('priority')}")
402
+ if epic_data.get("jira"):
403
+ click.echo(f"Jira: {epic_data.get('jira')}")
404
+ if epic_data.get("repos"):
405
+ click.echo(f"Repos: {epic_data.get('repos')}")
406
+ if epic_data.get("description"):
407
+ click.echo(f"Description: {epic_data.get('description').rstrip()}")
408
+
409
+ stories = epic_data.get("stories", [])
410
+ if stories:
411
+ click.echo(f"\nStories ({len(stories)}):")
412
+ for s in stories:
413
+ sid = s.get("id", "?")
414
+ stitle = s.get("title", "?")
415
+ spts = s.get("points", "?")
416
+ sstat = s.get("status", "?")
417
+ click.echo(f" {sid}: {stitle} [{spts}pts] ({sstat})")
418
+
419
+
420
+ def _epic_shard_path(sprint_dir, ref: str):
421
+ """Resolve an epic shard file path from a ref string.
422
+
423
+ Handles both 'epic-42' and 'MSSCI-12792' style refs.
424
+ The file naming convention is epic-{ref}.yaml, but refs that
425
+ already start with 'epic-' should not be double-prefixed.
426
+ """
427
+ if ref.startswith("epic-"):
428
+ return sprint_dir / f"{ref}.yaml"
429
+ return sprint_dir / f"epic-{ref}.yaml"
430
+
431
+
432
+ def _epic_ref_matches(ref: str, epic_id: str) -> bool:
433
+ """Check if an initiative epic ref matches the requested epic_id."""
434
+ # Normalize both to compare without prefix
435
+ ref_bare = ref.replace("epic-", "") if ref.startswith("epic-") else ref
436
+ id_bare = epic_id.replace("epic-", "") if epic_id.startswith("epic-") else epic_id
437
+ return ref_bare == id_bare or ref == epic_id
438
+
439
+
440
+ def _find_epic_in_initiatives(epic_id: str, root):
441
+ """Search initiative shard files for an epic by ID.
442
+
443
+ Returns (epic_dict, source_string) or (None, None).
444
+ """
445
+ import yaml
446
+
447
+ sprint_dir = root / "sprint"
448
+ for init_file in sorted(sprint_dir.glob("initiative-*.yaml")):
449
+ with open(init_file) as f:
450
+ init_data = yaml.safe_load(f.read())
451
+ if not init_data:
452
+ continue
453
+
454
+ init_name = init_data.get("name", init_file.stem)
455
+ epics = init_data.get("epics", [])
456
+ for e in epics:
457
+ if isinstance(e, str):
458
+ if _epic_ref_matches(e, epic_id):
459
+ shard = _epic_shard_path(sprint_dir, e)
460
+ if shard.exists():
461
+ with open(shard) as sf:
462
+ epic_data = yaml.safe_load(sf.read())
463
+ if epic_data:
464
+ return epic_data, f"initiative: {init_name}"
465
+ elif isinstance(e, dict):
466
+ eid = str(e.get("id", ""))
467
+ if _epic_ref_matches(eid, epic_id):
468
+ return e, f"initiative: {init_name}"
469
+
470
+ return None, None
471
+
472
+
473
+ @epic.command("cancel")
474
+ @click.argument("epic_id")
475
+ @click.option("--jira", is_flag=True, help="Also cancel the epic in Jira")
476
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
477
+ def epic_cancel(epic_id: str, jira: bool, dry_run: bool):
478
+ """Cancel an epic and all its stories.
479
+
480
+ Sets the epic status to 'canceled' and all stories to 'canceled'.
481
+ Searches both the current sprint and future initiative shards.
482
+
483
+ \b
484
+ Arguments:
485
+ EPIC_ID - Epic ID (e.g., epic-42 or MSSCI-14298)
486
+
487
+ \b
488
+ Examples:
489
+ pf sprint epic cancel epic-42 --dry-run
490
+ pf sprint epic cancel epic-42
491
+ pf sprint epic cancel epic-42 --jira
492
+ """
493
+ from pennyfarthing_scripts.common.config import get_project_root
494
+ from pennyfarthing_scripts.sprint.loader import load_sprint
495
+ from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
496
+
497
+ root = get_project_root()
498
+ sprint_dir = root / "sprint"
499
+ sprint_path = sprint_dir / "current-sprint.yaml"
500
+
501
+ # 1. Try current sprint
502
+ sprint_data = read_sprint(sprint_path) if sprint_path.exists() else None
503
+ found_in_sprint = False
504
+ if sprint_data and "epics" in sprint_data:
505
+ for e in sprint_data["epics"]:
506
+ if not isinstance(e, dict):
507
+ continue
508
+ eid = str(e.get("id", ""))
509
+ ejira = str(e.get("jira", ""))
510
+ if epic_id in (eid, ejira, eid.replace("epic-", ""), f"epic-{epic_id}"):
511
+ found_in_sprint = True
512
+ jira_key = e.get("jira")
513
+ stories = e.get("stories", [])
514
+ story_count = len(stories)
515
+
516
+ click.echo(f"Epic: {eid}")
517
+ click.echo(f"Title: {e.get('title', 'N/A')}")
518
+ click.echo(f"Stories: {story_count}")
519
+
520
+ if jira_key and not jira:
521
+ click.echo(f"\nWarning: Epic has Jira key {jira_key} -- pass --jira to also cancel in Jira")
522
+
523
+ if dry_run:
524
+ click.echo(f"\n[DRY-RUN] Would cancel {eid} and {story_count} stories")
525
+ return
526
+
527
+ e["status"] = "canceled"
528
+ for s in stories:
529
+ s["status"] = "canceled"
530
+
531
+ write_sprint(sprint_path, sprint_data)
532
+ click.echo(f"\nCanceled {eid} and {story_count} stories in current sprint")
533
+
534
+ if jira and jira_key:
535
+ _transition_jira(jira_key, "Cancelled")
536
+ click.echo(f"Transitioned Jira {jira_key} to Cancelled")
537
+ return
538
+
539
+ # 2. Try initiative shards
540
+ if not found_in_sprint:
541
+ _cancel_epic_in_initiatives(epic_id, root, jira=jira, dry_run=dry_run)
542
+
543
+
544
+ def _transition_jira(jira_key: str, status: str) -> bool:
545
+ """Transition a Jira issue to the given status."""
546
+ import subprocess
547
+
548
+ try:
549
+ result = subprocess.run(
550
+ ["jira", "issue", "move", jira_key, status],
551
+ capture_output=True,
552
+ text=True,
553
+ timeout=30,
554
+ )
555
+ return result.returncode == 0
556
+ except Exception:
557
+ return False
558
+
559
+
560
+ def _cancel_epic_in_initiatives(epic_id: str, root, *, jira: bool, dry_run: bool):
561
+ """Find and cancel an epic in initiative shard files."""
562
+ import yaml
563
+
564
+ sprint_dir = root / "sprint"
565
+
566
+ for init_file in sorted(sprint_dir.glob("initiative-*.yaml")):
567
+ with open(init_file) as f:
568
+ raw = f.read()
569
+ init_data = yaml.safe_load(raw)
570
+ if not init_data:
571
+ continue
572
+
573
+ init_name = init_data.get("name", init_file.stem)
574
+ epics = init_data.get("epics", [])
575
+
576
+ for i, e in enumerate(epics):
577
+ matched = False
578
+ epic_dict = None
579
+
580
+ if isinstance(e, str):
581
+ if _epic_ref_matches(e, epic_id):
582
+ shard = _epic_shard_path(sprint_dir, e)
583
+ if shard.exists():
584
+ with open(shard) as sf:
585
+ epic_dict = yaml.safe_load(sf.read())
586
+ matched = True
587
+ elif isinstance(e, dict):
588
+ eid = str(e.get("id", ""))
589
+ if _epic_ref_matches(eid, epic_id):
590
+ epic_dict = e
591
+ matched = True
592
+
593
+ if not matched or not epic_dict:
594
+ continue
595
+
596
+ jira_key = epic_dict.get("jira")
597
+ stories = epic_dict.get("stories", [])
598
+ story_count = len(stories)
599
+
600
+ click.echo(f"Epic: {epic_dict.get('id', epic_id)}")
601
+ click.echo(f"Title: {epic_dict.get('title', 'N/A')}")
602
+ click.echo(f"Initiative: {init_name}")
603
+ click.echo(f"Stories: {story_count}")
604
+
605
+ if jira_key and not jira:
606
+ click.echo(f"\nWarning: Epic has Jira key {jira_key} -- pass --jira to also cancel in Jira")
607
+
608
+ if dry_run:
609
+ click.echo(f"\n[DRY-RUN] Would cancel {epic_dict.get('id', epic_id)} and {story_count} stories")
610
+ return
611
+
612
+ epic_dict["status"] = "canceled"
613
+ for s in stories:
614
+ s["status"] = "canceled"
615
+
616
+ # Write back — either shard file or inline in initiative
617
+ if isinstance(e, str):
618
+ shard = _epic_shard_path(sprint_dir, e)
619
+ with open(shard, "w") as sf:
620
+ yaml.dump(dict(epic_dict), sf, default_flow_style=False, sort_keys=False)
621
+ else:
622
+ with open(init_file, "w") as f:
623
+ yaml.dump(init_data, f, default_flow_style=False, sort_keys=False)
624
+
625
+ click.echo(f"\nCanceled {epic_dict.get('id', epic_id)} and {story_count} stories")
626
+
627
+ if jira and jira_key:
628
+ _transition_jira(jira_key, "Cancelled")
629
+ click.echo(f"Transitioned Jira {jira_key} to Cancelled")
630
+ return
631
+
632
+ raise click.ClickException(f"Epic not found: {epic_id}")
633
+
634
+
635
+ @epic.command("archive")
636
+ @click.argument("epic_id", required=False)
637
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
638
+ @click.option("--jira", is_flag=True, help="Also update Jira epic status to Done")
639
+ def epic_archive(epic_id: str | None, dry_run: bool, jira: bool):
640
+ """Archive completed epics.
641
+
642
+ \b
643
+ Arguments:
644
+ EPIC_ID - Epic ID to archive (omit to scan all completed epics)
645
+
646
+ \b
647
+ Examples:
648
+ pf sprint epic archive # Scan and archive all completed
649
+ pf sprint epic archive --dry-run # Preview what would be archived
650
+ pf sprint epic archive epic-64 # Archive specific epic
651
+ pf sprint epic archive epic-64 --jira # Archive and update Jira
652
+ """
653
+ # Lazy import
654
+ from pennyfarthing_scripts.sprint.archive_epic import (
655
+ archive_all_completed,
656
+ archive_epic as do_archive_epic,
657
+ )
658
+
659
+ if epic_id:
660
+ result = do_archive_epic(epic_id, dry_run=dry_run, update_jira=jira)
661
+ else:
662
+ result = archive_all_completed(dry_run=dry_run, update_jira=jira)
663
+
664
+ if result.get("success"):
665
+ if dry_run:
666
+ click.echo(f"[DRY-RUN] {result.get('message')}")
667
+ if "archived" in result:
668
+ for r in result["archived"]:
669
+ e = r.get("epic", {})
670
+ eid = e.get("id") if e else r.get("epic_id")
671
+ stories = len(e.get("stories", [])) if e else r.get("stories_archived", 0)
672
+ click.echo(f" Would archive: {eid} ({stories} stories)")
673
+ else:
674
+ click.echo(result.get("message"))
675
+ if "archived" in result:
676
+ for r in result["archived"]:
677
+ click.echo(f" ✓ {r.get('epic_id')}: {r.get('stories_archived')} stories")
678
+ if result.get("stories_archived"):
679
+ click.echo(f" ✓ {result.get('epic_id')}: {result.get('stories_archived')} stories")
680
+ else:
681
+ error_msg = result.get("error", "Unknown error")
682
+ if result.get("incomplete_stories"):
683
+ error_msg += f"\n Incomplete: {', '.join(result['incomplete_stories'])}"
684
+ raise click.ClickException(error_msg)
685
+
686
+
687
+ @epic.command("import")
688
+ @click.argument("epics_file")
689
+ @click.argument("initiative_name", required=False)
690
+ @click.option("--marker", default="imported", help="Marker tag for stories (default: imported)")
691
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
692
+ def epic_import(epics_file: str, initiative_name: str | None, marker: str, dry_run: bool):
693
+ """Import BMAD epics-and-stories output to future.yaml.
694
+
695
+ \b
696
+ Arguments:
697
+ EPICS_FILE - Path to markdown file from epics-and-stories workflow
698
+ INITIATIVE_NAME - Name for the initiative (optional, extracted from file)
699
+
700
+ \b
701
+ Examples:
702
+ pf sprint epic import docs/planning/my-feature-epics.md
703
+ pf sprint epic import docs/planning/my-feature-epics.md "My Feature" --marker my-feature
704
+ pf sprint epic import docs/planning/my-feature-epics.md --dry-run
705
+ """
706
+ # Lazy import
707
+ from pennyfarthing_scripts.sprint.import_epic import import_epic as do_import
708
+
709
+ result = do_import(
710
+ epics_file,
711
+ initiative_name=initiative_name,
712
+ marker=marker,
713
+ dry_run=dry_run,
714
+ )
715
+
716
+ if result.get("success"):
717
+ if dry_run:
718
+ click.echo(f"[DRY-RUN] {result.get('message')}")
719
+ click.echo(f" Epics: {result.get('epics_count')}")
720
+ click.echo(f" Stories: {result.get('stories_count')}")
721
+ click.echo(f" Points: {result.get('total_points')}")
722
+ click.echo(f" Epic numbers: epic-{result.get('start_epic_num')} to epic-{result.get('next_epic_num') - 1}")
723
+ click.echo("")
724
+ click.echo("YAML Preview:")
725
+ click.echo("-" * 60)
726
+ click.echo(result.get("yaml_preview"))
727
+ click.echo("-" * 60)
728
+ else:
729
+ click.echo(f"✓ {result.get('message')}")
730
+ click.echo(f" Next available epic number: {result.get('next_epic_num')}")
731
+ else:
732
+ raise click.ClickException(result.get("error", "Unknown error"))
733
+
734
+
735
+ @epic.command("remove")
736
+ @click.argument("epic_id")
737
+ @click.option("--dry-run", is_flag=True, help="Show what would be removed without making changes")
738
+ def epic_remove(epic_id: str, dry_run: bool):
739
+ """Remove an epic from future.yaml (for cancelled pre-Jira epics).
740
+
741
+ \b
742
+ Arguments:
743
+ EPIC_ID - Epic ID to remove (e.g., epic-41)
744
+
745
+ \b
746
+ Examples:
747
+ pf sprint epic remove epic-41
748
+ pf sprint epic remove epic-41 --dry-run
749
+ """
750
+ from pathlib import Path
751
+
752
+ import yaml
753
+
754
+ from pennyfarthing_scripts.common.config import get_project_root
755
+
756
+ future_path = get_project_root() / "sprint" / "future.yaml"
757
+ if not future_path.exists():
758
+ raise click.ClickException(f"File not found: {future_path}")
759
+
760
+ with open(future_path) as f:
761
+ data = yaml.safe_load(f.read())
762
+
763
+ if not data or "future" not in data or "initiatives" not in data["future"]:
764
+ raise click.ClickException("Invalid future.yaml structure")
765
+
766
+ # Find the epic
767
+ found = False
768
+ for init in data["future"]["initiatives"]:
769
+ epics = init.get("epics", [])
770
+ for e in epics:
771
+ if e.get("id") == epic_id:
772
+ found = True
773
+ story_count = len(e.get("stories", []))
774
+ click.echo(f"Found epic in initiative '{init.get('name', 'unknown')}':")
775
+ click.echo(f" ID: {epic_id}")
776
+ click.echo(f" Title: {e.get('title', 'unknown')}")
777
+ click.echo(f" Points: {e.get('points', '?')}")
778
+ click.echo(f" Stories: {story_count}")
779
+
780
+ if dry_run:
781
+ click.echo(f"\n[DRY-RUN] Would remove {epic_id} from future.yaml")
782
+ return
783
+
784
+ # Remove using yq to preserve comments and formatting
785
+ import subprocess as sp
786
+
787
+ result = sp.run(
788
+ [
789
+ "yq", "eval", "-i",
790
+ f'del(.future.initiatives[].epics[] | select(.id == "{epic_id}"))',
791
+ str(future_path),
792
+ ],
793
+ capture_output=True,
794
+ text=True,
795
+ )
796
+ if result.returncode != 0:
797
+ raise click.ClickException(f"yq failed: {result.stderr}")
798
+
799
+ click.echo(f"\n✓ Removed {epic_id} from future.yaml")
800
+ return
801
+
802
+ if not found:
803
+ raise click.ClickException(
804
+ f"Epic {epic_id} not found in future.yaml"
805
+ )
806
+
807
+
808
+ @epic.command("promote")
809
+ @click.argument("epic_id")
810
+ def epic_promote(epic_id: str):
811
+ """Move an epic from future initiatives to current-sprint.yaml.
812
+
813
+ Detects ID collisions and assigns new IDs if needed.
814
+ Automatically removes the epic from its initiative shard after promotion.
815
+
816
+ \b
817
+ Arguments:
818
+ EPIC_ID - Epic ID (e.g., epic-41 or 41)
819
+
820
+ \b
821
+ Examples:
822
+ pf sprint epic promote epic-41
823
+ pf sprint epic promote 41
824
+ """
825
+ import copy
826
+
827
+ import yaml
828
+
829
+ from pennyfarthing_scripts.common.config import get_project_root
830
+
831
+ root = get_project_root()
832
+ sprint_dir = root / "sprint"
833
+ sprint_file = sprint_dir / "current-sprint.yaml"
834
+
835
+ if not sprint_file.exists():
836
+ raise click.ClickException(f"Sprint file not found: {sprint_file}")
837
+
838
+ # Find the epic in initiative shards
839
+ epic_data = None
840
+ source_init_file = None
841
+ source_ref = None
842
+
843
+ for init_file in sorted(sprint_dir.glob("initiative-*.yaml")):
844
+ with open(init_file) as f:
845
+ init_data = yaml.safe_load(f.read())
846
+ if not init_data:
847
+ continue
848
+ for e in init_data.get("epics", []):
849
+ edata = _resolve_epic_ref(e, sprint_dir)
850
+ if not edata:
851
+ continue
852
+ eid = str(edata.get("id", ""))
853
+ if _epic_ref_matches(eid, epic_id):
854
+ epic_data = copy.deepcopy(edata)
855
+ source_init_file = init_file
856
+ source_ref = e
857
+ break
858
+ if epic_data:
859
+ break
860
+
861
+ if not epic_data:
862
+ raise click.ClickException(f"Epic {epic_id} not found in future initiatives")
863
+
864
+ # Load current sprint
865
+ with open(sprint_file) as f:
866
+ sprint_data = yaml.safe_load(f.read())
867
+
868
+ if not sprint_data:
869
+ raise click.ClickException(f"Invalid sprint file: {sprint_file}")
870
+
871
+ if "epics" not in sprint_data:
872
+ sprint_data["epics"] = []
873
+
874
+ # Check for ID collision
875
+ original_id = str(epic_data.get("id", epic_id))
876
+ new_epic_id = original_id
877
+ existing_ids = {str(e.get("id", "")) for e in sprint_data["epics"] if isinstance(e, dict)}
878
+
879
+ if new_epic_id in existing_ids:
880
+ max_num = 0
881
+ for eid in existing_ids:
882
+ if eid.startswith("epic-"):
883
+ try:
884
+ max_num = max(max_num, int(eid.replace("epic-", "")))
885
+ except ValueError:
886
+ pass
887
+ new_epic_id = f"epic-{max_num + 1}"
888
+ click.echo(f"Warning: Epic ID {original_id} already exists. Assigning new ID: {new_epic_id}")
889
+
890
+ # Transform epic for current sprint
891
+ old_id_num = original_id.replace("epic-", "")
892
+ new_id_num = new_epic_id.replace("epic-", "")
893
+
894
+ epic_data["id"] = new_epic_id
895
+ epic_data["status"] = "backlog"
896
+ if not epic_data.get("title", "").startswith("Epic:"):
897
+ epic_data["title"] = f"Epic: {epic_data.get('title', 'Unknown')}"
898
+
899
+ for s in epic_data.get("stories", []):
900
+ sid = str(s.get("id", ""))
901
+ if sid.startswith(f"{old_id_num}-"):
902
+ s["id"] = sid.replace(f"{old_id_num}-", f"{new_id_num}-", 1)
903
+ s["status"] = "backlog"
904
+ s.setdefault("repos", "pennyfarthing")
905
+ s.setdefault("workflow", "tdd")
906
+ s.setdefault("priority", "P2")
907
+ s.setdefault("acceptance_criteria", [])
908
+
909
+ story_count = len(epic_data.get("stories", []))
910
+
911
+ click.echo("")
912
+ click.echo("Promoting epic to current sprint:")
913
+ click.echo(f" Original ID: {original_id}")
914
+ if new_epic_id != original_id:
915
+ click.echo(f" New ID: {new_epic_id}")
916
+ click.echo(f" Title: {epic_data.get('title')}")
917
+ click.echo(f" Points: {epic_data.get('points', 0)}")
918
+ click.echo(f" Stories: {story_count}")
919
+ click.echo("")
920
+
921
+ # Append to sprint
922
+ sprint_data["epics"].append(epic_data)
923
+
924
+ from pennyfarthing_scripts.sprint.yaml_io import write_sprint
925
+ write_sprint(sprint_file, sprint_data)
926
+ click.echo(f"Added epic to {sprint_file}")
927
+
928
+ # Remove from initiative shard
929
+ with open(source_init_file) as f:
930
+ init_data = yaml.safe_load(f.read())
931
+
932
+ if isinstance(source_ref, str):
933
+ # String ref — remove from list and delete shard file
934
+ init_data["epics"] = [e for e in init_data.get("epics", []) if e != source_ref]
935
+ shard = _epic_shard_path(sprint_dir, source_ref)
936
+ if shard.exists():
937
+ shard.unlink()
938
+ else:
939
+ # Inline dict — remove matching entry
940
+ init_data["epics"] = [
941
+ e for e in init_data.get("epics", [])
942
+ if not (isinstance(e, dict) and _epic_ref_matches(str(e.get("id", "")), epic_id))
943
+ ]
944
+
945
+ remaining_epics = init_data.get("epics", [])
946
+ if remaining_epics:
947
+ # Initiative still has epics — update shard in place
948
+ with open(source_init_file, "w") as f:
949
+ yaml.dump(init_data, f, default_flow_style=False, sort_keys=False)
950
+ click.echo(f"Removed {original_id} from {source_init_file.name}")
951
+ else:
952
+ # Initiative is empty — remove shard and future.yaml reference
953
+ init_name = init_data.get("name", "")
954
+ init_slug = source_init_file.stem.replace("initiative-", "")
955
+ source_init_file.unlink()
956
+ click.echo(f"Removed empty initiative shard: {source_init_file.name}")
957
+
958
+ # Remove from future.yaml
959
+ future_file = sprint_dir / "future.yaml"
960
+ if future_file.exists():
961
+ with open(future_file) as f:
962
+ future_data = yaml.safe_load(f.read()) or {}
963
+ future_inits = future_data.get("future", {}).get("initiatives", [])
964
+ if init_slug in future_inits:
965
+ future_inits.remove(init_slug)
966
+ with open(future_file, "w") as f:
967
+ yaml.dump(future_data, f, default_flow_style=False, sort_keys=False)
968
+ click.echo(f"Removed '{init_slug}' from future.yaml")
969
+
970
+ click.echo("")
971
+ click.echo("Promotion complete!")
972
+ click.echo("")
973
+ click.echo("Next steps:")
974
+ click.echo(f" 1. Review the epic: pf sprint epic show {new_epic_id}")
975
+ click.echo(f" 2. Create Jira epic: pf jira create epic {new_epic_id}")
976
+ click.echo(f" 3. Start work: /sprint work {new_id_num}-1")
977
+
978
+
979
+ # Register epic-add as epic.add
980
+ from pennyfarthing_scripts.sprint.epic_add import epic_add_command
981
+
982
+ epic.add_command(epic_add_command, "add")
983
+
984
+
985
+ # --- Initiative subgroup ---
986
+
987
+ @sprint.group()
988
+ def initiative():
989
+ """Initiative operations (show, cancel)."""
990
+ pass
991
+
992
+
993
+ @initiative.command("show")
994
+ @click.argument("name")
995
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
996
+ def initiative_show(name: str, output_json: bool):
997
+ """Show details for a specific initiative.
998
+
999
+ \b
1000
+ Arguments:
1001
+ NAME - Initiative slug (e.g., benchmark-reliability, technical-debt)
1002
+
1003
+ \b
1004
+ Examples:
1005
+ pf sprint initiative show benchmark-reliability
1006
+ pf sprint initiative show technical-debt --json
1007
+ """
1008
+ import json as json_mod
1009
+
1010
+ import yaml
1011
+
1012
+ from pennyfarthing_scripts.common.config import get_project_root
1013
+
1014
+ root = get_project_root()
1015
+ init_file = root / "sprint" / f"initiative-{name}.yaml"
1016
+
1017
+ if not init_file.exists():
1018
+ raise click.ClickException(f"Initiative not found: {name}\n Expected: {init_file}")
1019
+
1020
+ with open(init_file) as f:
1021
+ init_data = yaml.safe_load(f.read())
1022
+
1023
+ if not init_data:
1024
+ raise click.ClickException(f"Empty initiative file: {init_file}")
1025
+
1026
+ if output_json:
1027
+ click.echo(json_mod.dumps(init_data, indent=2, default=str))
1028
+ return
1029
+
1030
+ click.echo(f"Initiative: {init_data.get('name', name)}")
1031
+ click.echo(f"Status: {init_data.get('status', 'N/A')}")
1032
+ if init_data.get("total_points"):
1033
+ click.echo(f"Total Points: {init_data.get('total_points')}")
1034
+ if init_data.get("blocked_by"):
1035
+ click.echo(f"Blocked By: {init_data.get('blocked_by')}")
1036
+ if init_data.get("description"):
1037
+ click.echo(f"Description: {init_data.get('description').rstrip()}")
1038
+
1039
+ epics = init_data.get("epics", [])
1040
+ if epics:
1041
+ click.echo(f"\nEpics ({len(epics)}):")
1042
+ sprint_dir = root / "sprint"
1043
+ for e in epics:
1044
+ if isinstance(e, str):
1045
+ # String ref — try to load shard for details
1046
+ shard = _epic_shard_path(sprint_dir, e)
1047
+ if shard.exists():
1048
+ with open(shard) as sf:
1049
+ edata = yaml.safe_load(sf.read())
1050
+ if edata:
1051
+ etitle = edata.get("title", "?")
1052
+ epts = edata.get("points", "?")
1053
+ estat = edata.get("status", "?")
1054
+ click.echo(f" {edata.get('id', e)}: {etitle} [{epts}pts] ({estat})")
1055
+ continue
1056
+ click.echo(f" {e} (shard not found)")
1057
+ elif isinstance(e, dict):
1058
+ eid = e.get("id", "?")
1059
+ etitle = e.get("title", "?")
1060
+ epts = e.get("points", "?")
1061
+ estat = e.get("status", "?")
1062
+ click.echo(f" {eid}: {etitle} [{epts}pts] ({estat})")
1063
+
1064
+ standalone_stories = init_data.get("standalone_stories", [])
1065
+ if standalone_stories:
1066
+ click.echo(f"\nStandalone Stories ({len(standalone_stories)}):")
1067
+ for s in standalone_stories:
1068
+ sid = s.get("id", "?")
1069
+ stitle = s.get("title", "?")
1070
+ spts = s.get("points", "?")
1071
+ sstat = s.get("status", "?")
1072
+ click.echo(f" {sid}: {stitle} [{spts}pts] ({sstat})")
1073
+
1074
+
1075
+ @initiative.command("cancel")
1076
+ @click.argument("name")
1077
+ @click.option("--jira", is_flag=True, help="Also cancel epics in Jira")
1078
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
1079
+ def initiative_cancel(name: str, jira: bool, dry_run: bool):
1080
+ """Cancel an initiative and all its epics/stories.
1081
+
1082
+ Sets the initiative status to 'canceled' and cancels all epics and stories
1083
+ within it.
1084
+
1085
+ \b
1086
+ Arguments:
1087
+ NAME - Initiative slug (e.g., benchmark-reliability, technical-debt)
1088
+
1089
+ \b
1090
+ Examples:
1091
+ pf sprint initiative cancel technical-debt --dry-run
1092
+ pf sprint initiative cancel technical-debt
1093
+ pf sprint initiative cancel technical-debt --jira
1094
+ """
1095
+ import yaml
1096
+
1097
+ from pennyfarthing_scripts.common.config import get_project_root
1098
+
1099
+ root = get_project_root()
1100
+ sprint_dir = root / "sprint"
1101
+ init_file = sprint_dir / f"initiative-{name}.yaml"
1102
+
1103
+ if not init_file.exists():
1104
+ raise click.ClickException(f"Initiative not found: {name}\n Expected: {init_file}")
1105
+
1106
+ with open(init_file) as f:
1107
+ init_data = yaml.safe_load(f.read())
1108
+
1109
+ if not init_data:
1110
+ raise click.ClickException(f"Empty initiative file: {init_file}")
1111
+
1112
+ init_name = init_data.get("name", name)
1113
+ epics = init_data.get("epics", [])
1114
+ standalone_stories = init_data.get("standalone_stories", [])
1115
+
1116
+ # Collect Jira keys for warning
1117
+ jira_keys = []
1118
+ epic_count = 0
1119
+ story_count = 0
1120
+
1121
+ for e in epics:
1122
+ if isinstance(e, str):
1123
+ shard = _epic_shard_path(sprint_dir, e)
1124
+ if shard.exists():
1125
+ with open(shard) as sf:
1126
+ edata = yaml.safe_load(sf.read())
1127
+ if edata:
1128
+ epic_count += 1
1129
+ if edata.get("jira"):
1130
+ jira_keys.append(edata["jira"])
1131
+ story_count += len(edata.get("stories", []))
1132
+ elif isinstance(e, dict):
1133
+ epic_count += 1
1134
+ if e.get("jira"):
1135
+ jira_keys.append(e["jira"])
1136
+ story_count += len(e.get("stories", []))
1137
+
1138
+ story_count += len(standalone_stories)
1139
+
1140
+ click.echo(f"Initiative: {init_name}")
1141
+ click.echo(f"Epics: {epic_count}")
1142
+ click.echo(f"Stories: {story_count}")
1143
+
1144
+ if jira_keys and not jira:
1145
+ click.echo(f"\nWarning: {len(jira_keys)} epic(s) have Jira keys -- pass --jira to also cancel in Jira")
1146
+ for k in jira_keys:
1147
+ click.echo(f" {k}")
1148
+
1149
+ if dry_run:
1150
+ click.echo(f"\n[DRY-RUN] Would cancel initiative '{init_name}' ({epic_count} epics, {story_count} stories)")
1151
+ return
1152
+
1153
+ # Cancel all epics
1154
+ for i, e in enumerate(epics):
1155
+ if isinstance(e, str):
1156
+ shard = _epic_shard_path(sprint_dir, e)
1157
+ if shard.exists():
1158
+ with open(shard) as sf:
1159
+ edata = yaml.safe_load(sf.read())
1160
+ if edata:
1161
+ edata["status"] = "canceled"
1162
+ for s in edata.get("stories", []):
1163
+ s["status"] = "canceled"
1164
+ with open(shard, "w") as sf:
1165
+ yaml.dump(edata, sf, default_flow_style=False, sort_keys=False)
1166
+ if jira and edata.get("jira"):
1167
+ _transition_jira(edata["jira"], "Cancelled")
1168
+ elif isinstance(e, dict):
1169
+ e["status"] = "canceled"
1170
+ for s in e.get("stories", []):
1171
+ s["status"] = "canceled"
1172
+ if jira and e.get("jira"):
1173
+ _transition_jira(e["jira"], "Cancelled")
1174
+
1175
+ # Cancel standalone stories
1176
+ for s in standalone_stories:
1177
+ s["status"] = "canceled"
1178
+
1179
+ # Update initiative status
1180
+ init_data["status"] = "canceled"
1181
+
1182
+ with open(init_file, "w") as f:
1183
+ yaml.dump(init_data, f, default_flow_style=False, sort_keys=False)
1184
+
1185
+ click.echo(f"\nCanceled initiative '{init_name}' ({epic_count} epics, {story_count} stories)")
1186
+ if jira and jira_keys:
1187
+ click.echo(f"Transitioned {len(jira_keys)} Jira epic(s) to Cancelled")
1188
+
1189
+
1190
+ # --- Check command (replaces check-story.sh) ---
1191
+
1192
+ @sprint.command()
1193
+ @click.argument("id")
1194
+ def check(id: str):
1195
+ """Check story/epic availability. Returns JSON.
1196
+
1197
+ \b
1198
+ Arguments:
1199
+ ID - Story ID, epic ID, or 'next' for highest priority
1200
+
1201
+ \b
1202
+ Returns JSON with type, details, and availability:
1203
+ type: "story" | "epic" | "next" | "not_found"
1204
+ """
1205
+ import json
1206
+
1207
+ from pennyfarthing_scripts.sprint.loader import (
1208
+ find_epic,
1209
+ get_all_stories,
1210
+ load_sprint,
1211
+ )
1212
+ from pennyfarthing_scripts.sprint.work import check_story, get_next_story
1213
+
1214
+ data = load_sprint()
1215
+
1216
+ if id == "next":
1217
+ result = get_next_story()
1218
+ if result.get("available"):
1219
+ story = result["story"]
1220
+ # Find parent epic
1221
+ epic_id = _find_epic_for_story(data, story.get("id", ""))
1222
+ out = {
1223
+ "type": "next",
1224
+ "story": {
1225
+ "id": story.get("id"),
1226
+ "title": story.get("title"),
1227
+ "points": story.get("points", 0),
1228
+ "priority": story.get("priority", "P2"),
1229
+ "workflow": story.get("workflow", "tdd"),
1230
+ "repos": story.get("repos", "pennyfarthing"),
1231
+ "epic_id": epic_id,
1232
+ "acceptance_criteria": story.get("acceptance_criteria", []),
1233
+ },
1234
+ }
1235
+ else:
1236
+ out = {"type": "next", "story": None, "message": "No available stories in backlog"}
1237
+ click.echo(json.dumps(out, indent=2))
1238
+ return
1239
+
1240
+ # Check if it's an epic
1241
+ if data:
1242
+ epic = find_epic(data, id)
1243
+ if epic:
1244
+ available_statuses = {"backlog", "ready", "planning"}
1245
+ available = [
1246
+ s for s in epic.get("stories", [])
1247
+ if s.get("status") in available_statuses
1248
+ ]
1249
+ # Sort by priority
1250
+ priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
1251
+ available.sort(key=lambda s: priority_order.get(s.get("priority", "P2"), 2))
1252
+
1253
+ first = available[0] if available else None
1254
+ out = {
1255
+ "type": "epic",
1256
+ "id": str(epic.get("id", id)),
1257
+ "title": epic.get("title", "Unknown"),
1258
+ "available_stories": len(available),
1259
+ }
1260
+ if first:
1261
+ out["first_story"] = {
1262
+ "id": first.get("id"),
1263
+ "title": first.get("title"),
1264
+ "points": first.get("points", 0),
1265
+ "workflow": first.get("workflow", "tdd"),
1266
+ "repos": first.get("repos", "pennyfarthing"),
1267
+ "acceptance_criteria": first.get("acceptance_criteria", []),
1268
+ }
1269
+ else:
1270
+ out["first_story"] = None
1271
+ out["message"] = "No available stories in this epic"
1272
+ click.echo(json.dumps(out, indent=2))
1273
+ return
1274
+
1275
+ # Check if it's a story
1276
+ result = check_story(id)
1277
+ story = result.get("story")
1278
+ if story:
1279
+ epic_id = _find_epic_for_story(data, story.get("id", ""))
1280
+ out = {
1281
+ "type": "story",
1282
+ "id": story.get("id", id),
1283
+ "title": story.get("title", "Unknown"),
1284
+ "points": story.get("points", 0),
1285
+ "workflow": story.get("workflow", "tdd"),
1286
+ "status": story.get("status", "backlog"),
1287
+ "assigned_to": story.get("assigned_to", ""),
1288
+ "epic_id": epic_id,
1289
+ "repos": story.get("repos", "pennyfarthing"),
1290
+ "available": result.get("available", False),
1291
+ "acceptance_criteria": story.get("acceptance_criteria", []),
1292
+ }
1293
+ click.echo(json.dumps(out, indent=2))
1294
+ return
1295
+
1296
+ # Not found
1297
+ click.echo(json.dumps({
1298
+ "type": "not_found",
1299
+ "id": id,
1300
+ "message": "Story or epic not found in current sprint",
1301
+ }, indent=2))
1302
+
1303
+
1304
+ def _find_epic_for_story(data: dict | None, story_id: str) -> str:
1305
+ """Find the parent epic ID for a story."""
1306
+ if not data or "epics" not in data:
1307
+ return ""
1308
+ for epic in data["epics"]:
1309
+ if not isinstance(epic, dict):
1310
+ continue
1311
+ for s in epic.get("stories", []):
1312
+ if s.get("id") == story_id:
1313
+ return str(epic.get("id", ""))
1314
+ return ""
1315
+
1316
+
1317
+ # --- Info command (replaces sprint-info.sh) ---
1318
+
1319
+ @sprint.command()
1320
+ def info():
1321
+ """Output sprint info as JSON for Cyclist sidebar.
1322
+
1323
+ \b
1324
+ Returns: {"remaining": N, "inProgress": N, "endDate": "YYYY-MM-DD"}
1325
+ """
1326
+ import json
1327
+
1328
+ from pennyfarthing_scripts.sprint.loader import get_all_stories, get_sprint_info
1329
+
1330
+ sprint_data = get_sprint_info()
1331
+ stories = get_all_stories()
1332
+
1333
+ end_date = sprint_data.get("end_date")
1334
+
1335
+ remaining = sum(
1336
+ s.get("points", 0) or 0
1337
+ for s in stories
1338
+ if s.get("status") in ("backlog", "planning", "ready", None)
1339
+ )
1340
+ in_progress = sum(
1341
+ s.get("points", 0) or 0
1342
+ for s in stories
1343
+ if s.get("status") == "in_progress"
1344
+ )
1345
+
1346
+ click.echo(json.dumps({
1347
+ "remaining": remaining,
1348
+ "inProgress": in_progress,
1349
+ "endDate": str(end_date) if end_date else None,
1350
+ }))
1351
+
1352
+
1353
+ # --- Metrics command (replaces sprint-metrics.sh) ---
1354
+
1355
+ @sprint.command()
1356
+ @click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
1357
+ def metrics(output_json: bool):
1358
+ """Display sprint metrics and progress.
1359
+
1360
+ Shows points, stories, timeline, and velocity tracking.
1361
+ """
1362
+ import json
1363
+ from datetime import date, datetime
1364
+
1365
+ from pennyfarthing_scripts.sprint.loader import get_all_stories, get_sprint_info
1366
+
1367
+ sprint_data = get_sprint_info()
1368
+ stories = get_all_stories()
1369
+
1370
+ if not sprint_data:
1371
+ click.echo("No sprint data available")
1372
+ return
1373
+
1374
+ sprint_name = sprint_data.get("name", "Unknown")
1375
+ goal = sprint_data.get("goal", "")
1376
+ start_date_str = sprint_data.get("start_date", "")
1377
+ end_date_str = sprint_data.get("end_date", "")
1378
+
1379
+ # Count stories/points by status
1380
+ done_stories = [s for s in stories if s.get("status") in ("done", "completed")]
1381
+ wip_stories = [s for s in stories if s.get("status") == "in_progress"]
1382
+ backlog_stories = [s for s in stories if s.get("status") in ("backlog", "planning", "ready", None)]
1383
+
1384
+ done_pts = sum(s.get("points", 0) or 0 for s in done_stories)
1385
+ wip_pts = sum(s.get("points", 0) or 0 for s in wip_stories)
1386
+ backlog_pts = sum(s.get("points", 0) or 0 for s in backlog_stories)
1387
+ total_pts = done_pts + wip_pts + backlog_pts
1388
+
1389
+ # Date calculations
1390
+ today = date.today()
1391
+ try:
1392
+ start_date = datetime.strptime(str(start_date_str), "%Y-%m-%d").date()
1393
+ end_date = datetime.strptime(str(end_date_str), "%Y-%m-%d").date()
1394
+ except (ValueError, TypeError):
1395
+ start_date = today
1396
+ end_date = today
1397
+
1398
+ total_days = (end_date - start_date).days or 1
1399
+ days_elapsed = max(0, (today - start_date).days)
1400
+ days_remaining = max(0, (end_date - today).days)
1401
+
1402
+ pct_complete = (done_pts * 100 // total_pts) if total_pts > 0 else 0
1403
+ pct_time = (days_elapsed * 100 // total_days) if total_days > 0 else 0
1404
+
1405
+ velocity_target = sprint_data.get("velocity_target", total_pts)
1406
+ expected_pts = (velocity_target * days_elapsed // total_days) if total_days > 0 else 0
1407
+
1408
+ if output_json:
1409
+ click.echo(json.dumps({
1410
+ "sprint": sprint_name,
1411
+ "dates": {
1412
+ "start": str(start_date_str),
1413
+ "end": str(end_date_str),
1414
+ "today": str(today),
1415
+ },
1416
+ "points": {
1417
+ "total": total_pts,
1418
+ "completed": done_pts,
1419
+ "in_progress": wip_pts,
1420
+ "backlog": backlog_pts,
1421
+ "velocity_target": velocity_target,
1422
+ },
1423
+ "stories": {
1424
+ "total": len(stories),
1425
+ "done": len(done_stories),
1426
+ "in_progress": len(wip_stories),
1427
+ "backlog": len(backlog_stories),
1428
+ },
1429
+ "progress": {
1430
+ "percent_complete": pct_complete,
1431
+ "percent_time": pct_time,
1432
+ "days_elapsed": days_elapsed,
1433
+ "days_remaining": days_remaining,
1434
+ "total_days": total_days,
1435
+ },
1436
+ "velocity": {
1437
+ "expected_points": expected_pts,
1438
+ "actual_points": done_pts,
1439
+ "on_track": done_pts >= expected_pts,
1440
+ },
1441
+ }, indent=2))
1442
+ return
1443
+
1444
+ # Human-readable output
1445
+ click.echo("")
1446
+ click.echo(f" Sprint: {sprint_name}")
1447
+ click.echo(f" Goal: {goal}")
1448
+ click.echo("")
1449
+ click.echo(f" Timeline: {start_date_str} to {end_date_str} (Day {days_elapsed}/{total_days}, {days_remaining} remaining)")
1450
+ click.echo("")
1451
+ click.echo(f" Points: {done_pts} done / {wip_pts} WIP / {backlog_pts} backlog = {total_pts} total ({pct_complete}%)")
1452
+ click.echo(f" Stories: {len(done_stories)} done / {len(wip_stories)} WIP / {len(backlog_stories)} backlog = {len(stories)} total")
1453
+ click.echo("")
1454
+ click.echo(f" Velocity: {done_pts}/{expected_pts} expected ({velocity_target} target)")
1455
+ if done_pts >= expected_pts:
1456
+ click.echo(" Status: On track")
1457
+ else:
1458
+ click.echo(" Status: Behind schedule")
1459
+
1460
+
1461
+ # --- Story field command (replaces get-story-field.sh) ---
1462
+
1463
+ @story.command("field")
1464
+ @click.argument("story_id")
1465
+ @click.argument("field_name")
1466
+ def story_field(story_id: str, field_name: str):
1467
+ """Get a field value from a story.
1468
+
1469
+ \b
1470
+ Arguments:
1471
+ STORY_ID - Story ID (e.g., 79-1 or MSSCI-12345)
1472
+ FIELD_NAME - Field to extract (e.g., workflow, status, points)
1473
+
1474
+ Returns the field value or "null" if not found.
1475
+ """
1476
+ from pennyfarthing_scripts.sprint.loader import get_story_by_id, get_story_field, load_sprint
1477
+
1478
+ # Default values for common fields
1479
+ defaults = {
1480
+ "workflow": "tdd",
1481
+ "status": "backlog",
1482
+ "repos": "pennyfarthing",
1483
+ }
1484
+
1485
+ # Try get_story_field first (works with epic-story format like "79-1")
1486
+ data = load_sprint()
1487
+ if data:
1488
+ value = get_story_field(data, story_id, field_name)
1489
+ if value is not None:
1490
+ click.echo(str(value))
1491
+ return
1492
+
1493
+ # Fallback: try direct story lookup (works with Jira keys)
1494
+ story = get_story_by_id(story_id)
1495
+ if story:
1496
+ value = story.get(field_name)
1497
+ if value is not None:
1498
+ click.echo(str(value))
1499
+ return
1500
+
1501
+ # Return default or null
1502
+ click.echo(defaults.get(field_name, "null"))
1503
+
1504
+
1505
+ # --- Epic field command (replaces get-epic-field.sh) ---
1506
+
1507
+ @epic.command("field")
1508
+ @click.argument("epic_id")
1509
+ @click.argument("field_name")
1510
+ def epic_field(epic_id: str, field_name: str):
1511
+ """Get a field value from an epic.
1512
+
1513
+ \b
1514
+ Arguments:
1515
+ EPIC_ID - Epic ID (e.g., epic-79 or 79)
1516
+ FIELD_NAME - Field to extract (e.g., jira, title, status)
1517
+
1518
+ Returns the field value or "null" if not found.
1519
+ """
1520
+ from pennyfarthing_scripts.sprint.loader import find_epic, load_sprint
1521
+
1522
+ data = load_sprint()
1523
+ if not data:
1524
+ click.echo("null")
1525
+ return
1526
+
1527
+ epic = find_epic(data, epic_id)
1528
+ if not epic:
1529
+ click.echo("null")
1530
+ return
1531
+
1532
+ value = epic.get(field_name)
1533
+ if value is not None:
1534
+ click.echo(str(value).rstrip())
1535
+ else:
1536
+ click.echo("null")
1537
+
1538
+
1539
+ # --- Future command (replaces list-future.sh) ---
1540
+
1541
+ @sprint.command()
1542
+ @click.argument("epic_id", required=False)
1543
+ def future(epic_id: str | None):
1544
+ """Show future work initiatives and epics.
1545
+
1546
+ \b
1547
+ Arguments:
1548
+ EPIC_ID - Optional epic ID to show detailed stories (e.g., epic-55)
1549
+
1550
+ \b
1551
+ Examples:
1552
+ pf sprint future # Show all initiatives
1553
+ pf sprint future epic-55 # Show stories for specific epic
1554
+ """
1555
+ import yaml
1556
+
1557
+ from pennyfarthing_scripts.common.config import get_project_root
1558
+
1559
+ root = get_project_root()
1560
+ sprint_dir = root / "sprint"
1561
+
1562
+ init_files = sorted(sprint_dir.glob("initiative-*.yaml"))
1563
+ if not init_files:
1564
+ click.echo("No future initiatives found.")
1565
+ return
1566
+
1567
+ # If specific epic requested, show detailed view
1568
+ if epic_id:
1569
+ _show_future_epic_detail(epic_id, init_files, sprint_dir)
1570
+ return
1571
+
1572
+ # Default: show initiative summary
1573
+ click.echo("# Future Work - Available for Promotion")
1574
+ click.echo("")
1575
+
1576
+ total_epics = 0
1577
+ total_points = 0
1578
+
1579
+ for init_file in init_files:
1580
+ with open(init_file) as f:
1581
+ init_data = yaml.safe_load(f.read())
1582
+ if not init_data:
1583
+ continue
1584
+
1585
+ init_name = init_data.get("name", init_file.stem)
1586
+ init_status = init_data.get("status", "planning")
1587
+ blocked_by = init_data.get("blocked_by")
1588
+ init_points = init_data.get("total_points", 0)
1589
+
1590
+ if init_status == "ready":
1591
+ status_tag = "[READY]"
1592
+ elif blocked_by:
1593
+ status_tag = "[BLOCKED]"
1594
+ else:
1595
+ status_tag = f"[{init_status}]"
1596
+
1597
+ click.echo(f"## {init_name} {status_tag}")
1598
+ click.echo(f"**Total:** {init_points} points")
1599
+ if blocked_by:
1600
+ click.echo(f"**Blocked:** {blocked_by}")
1601
+ click.echo("")
1602
+
1603
+ click.echo("| Epic | Title | Pts | Pri | Status |")
1604
+ click.echo("|------|-------|-----|-----|--------|")
1605
+
1606
+ epics = init_data.get("epics", [])
1607
+ for e in epics:
1608
+ edata = _resolve_epic_ref(e, sprint_dir)
1609
+ if not edata:
1610
+ continue
1611
+ eid = edata.get("id", "?")
1612
+ etitle = edata.get("title", "?")
1613
+ if len(etitle) > 40:
1614
+ etitle = etitle[:37] + "..."
1615
+ epts = edata.get("points", "?")
1616
+ epri = edata.get("priority", "P2")
1617
+ estat = edata.get("status", "planning")
1618
+ click.echo(f"| {eid} | {etitle} | {epts} | {epri} | {estat} |")
1619
+ total_epics += 1
1620
+ total_points += edata.get("points", 0) or 0
1621
+
1622
+ click.echo("")
1623
+
1624
+ click.echo("---")
1625
+ click.echo(f"**Summary:** {total_epics} epics, {total_points} points total")
1626
+ click.echo("")
1627
+ click.echo("To see epic details: `pf sprint future epic-55`")
1628
+ click.echo("To promote an epic: `pf sprint epic promote epic-55`")
1629
+
1630
+
1631
+ def _resolve_epic_ref(ref, sprint_dir) -> dict | None:
1632
+ """Resolve an epic reference (string ref or inline dict) to a dict."""
1633
+ import yaml
1634
+
1635
+ if isinstance(ref, dict):
1636
+ return ref
1637
+ if isinstance(ref, str):
1638
+ shard = _epic_shard_path(sprint_dir, ref)
1639
+ if shard.exists():
1640
+ with open(shard) as f:
1641
+ return yaml.safe_load(f.read())
1642
+ return None
1643
+
1644
+
1645
+ def _show_future_epic_detail(epic_id: str, init_files, sprint_dir):
1646
+ """Show detailed view of a specific future epic."""
1647
+ import yaml
1648
+
1649
+ for init_file in init_files:
1650
+ with open(init_file) as f:
1651
+ init_data = yaml.safe_load(f.read())
1652
+ if not init_data:
1653
+ continue
1654
+
1655
+ for e in init_data.get("epics", []):
1656
+ edata = _resolve_epic_ref(e, sprint_dir)
1657
+ if not edata:
1658
+ continue
1659
+ eid = str(edata.get("id", ""))
1660
+ if epic_id not in (eid, eid.replace("epic-", ""), f"epic-{epic_id}"):
1661
+ continue
1662
+
1663
+ click.echo(f"# Epic Details: {eid}")
1664
+ click.echo("")
1665
+ click.echo(f"**Title:** {edata.get('title', '?')}")
1666
+ click.echo(f"**Points:** {edata.get('points', '?')} | **Priority:** {edata.get('priority', 'P2')} | **Status:** {edata.get('status', 'planning')}")
1667
+ click.echo("")
1668
+ desc = edata.get("description", "No description")
1669
+ if desc:
1670
+ click.echo("**Description:**")
1671
+ for line in str(desc).strip().split("\n")[:5]:
1672
+ click.echo(line)
1673
+ click.echo("")
1674
+
1675
+ stories = edata.get("stories", [])
1676
+ if stories:
1677
+ click.echo("## Stories")
1678
+ click.echo("")
1679
+ click.echo("| ID | Title | Pts | Pri | Status |")
1680
+ click.echo("|----|-------|-----|-----|--------|")
1681
+ for s in stories:
1682
+ stitle = s.get("title", "?")
1683
+ if len(stitle) > 45:
1684
+ stitle = stitle[:42] + "..."
1685
+ click.echo(f"| {s.get('id', '?')} | {stitle} | {s.get('points', '?')} | {s.get('priority', 'P1')} | {s.get('status', 'planning')} |")
1686
+ click.echo("")
1687
+
1688
+ click.echo("---")
1689
+ click.echo(f"To promote this epic: `pf sprint epic promote {eid}`")
1690
+ return
1691
+
1692
+ raise click.ClickException(f"Epic {epic_id} not found in future initiatives")
1693
+
1694
+
1695
+ # --- New sprint command (replaces new-sprint.sh) ---
1696
+
1697
+ @sprint.command("new")
1698
+ @click.argument("sprint_yyww")
1699
+ @click.argument("jira_id", type=int)
1700
+ @click.argument("start_date")
1701
+ @click.argument("end_date")
1702
+ @click.argument("goal")
1703
+ def new_sprint(sprint_yyww: str, jira_id: int, start_date: str, end_date: str, goal: str):
1704
+ """Initialize a new sprint.
1705
+
1706
+ \b
1707
+ Arguments:
1708
+ SPRINT_YYWW Sprint identifier in YYWW format (e.g., 2607)
1709
+ JIRA_ID Jira sprint ID number (e.g., 278)
1710
+ START_DATE Sprint start date YYYY-MM-DD
1711
+ END_DATE Sprint end date YYYY-MM-DD
1712
+ GOAL Sprint goal (quoted string)
1713
+
1714
+ \b
1715
+ Examples:
1716
+ pf sprint new 2607 278 2026-02-16 2026-03-01 "Performance and polish"
1717
+ """
1718
+ from pennyfarthing_scripts.common.config import get_project_root
1719
+
1720
+ root = get_project_root()
1721
+ sprint_file = root / "sprint" / "current-sprint.yaml"
1722
+ archive_file = root / "sprint" / "archive" / f"sprint-{sprint_yyww}-completed.yaml"
1723
+
1724
+ # Warn if current sprint is active
1725
+ if sprint_file.exists():
1726
+ import yaml
1727
+
1728
+ with open(sprint_file) as f:
1729
+ existing = yaml.safe_load(f.read())
1730
+ if existing and existing.get("sprint", {}).get("status") == "active":
1731
+ click.echo("Warning: Current sprint is still active!")
1732
+ click.echo("Current sprint file will be overwritten.")
1733
+ if not click.confirm("Continue?"):
1734
+ click.echo("Aborted.")
1735
+ return
1736
+
1737
+ # Create sprint file using write_sprint for consistency
1738
+ from pennyfarthing_scripts.sprint.yaml_io import write_sprint
1739
+
1740
+ sprint_data = {
1741
+ "sprint": {
1742
+ "name": f"TO Sprint {sprint_yyww}",
1743
+ "jira_sprint_id": jira_id,
1744
+ "jira_sprint_name": f"TO Sprint {sprint_yyww}",
1745
+ "goal": goal,
1746
+ "start_date": start_date,
1747
+ "end_date": end_date,
1748
+ "status": "active",
1749
+ },
1750
+ "epics": [],
1751
+ }
1752
+ write_sprint(sprint_file, sprint_data)
1753
+ click.echo(f"Created {sprint_file}")
1754
+
1755
+ # Create archive file
1756
+ from datetime import date
1757
+
1758
+ archive_content = f"""# Sprint TO Sprint {sprint_yyww} - Completed Stories
1759
+ # Jira Sprint ID: {jira_id}
1760
+ # Archived: {date.today()}
1761
+
1762
+ sprint:
1763
+ name: "TO Sprint {sprint_yyww}"
1764
+ jira_sprint_id: {jira_id}
1765
+ jira_sprint_name: "TO Sprint {sprint_yyww}"
1766
+ goal: {goal}
1767
+
1768
+ completed:
1769
+ # Completed stories will be appended here by pf sprint archive
1770
+ """
1771
+ archive_file.parent.mkdir(parents=True, exist_ok=True)
1772
+ archive_file.write_text(archive_content)
1773
+ click.echo(f"Created {archive_file}")
1774
+
1775
+ click.echo("")
1776
+ click.echo(f"New sprint initialized:")
1777
+ click.echo(f" Name: TO Sprint {sprint_yyww}")
1778
+ click.echo(f" Jira ID: {jira_id}")
1779
+ click.echo(f" Dates: {start_date} to {end_date}")
1780
+ click.echo(f" Goal: {goal}")
1781
+ click.echo("")
1782
+ click.echo("Next steps:")
1783
+ click.echo(" 1. Add epics: pf sprint epic promote <epic-id>")
1784
+ click.echo(" 2. Check status: pf sprint status")
1785
+
1786
+
1787
+ # --- Standalone command ---
1788
+
1789
+ @sprint.command()
1790
+ @click.argument("title", required=False)
1791
+ @click.argument("points", required=False, type=int)
1792
+ def standalone(title: str | None, points: int | None):
1793
+ """Wrap current changes into a standalone Jira story, branch, PR, and merge.
1794
+
1795
+ This is an agent-executed workflow. Use /standalone to run it interactively.
1796
+ """
1797
+ click.echo("The standalone command is an agent-executed workflow.")
1798
+ click.echo("Use /standalone to run it interactively with full agent support.")
1799
+
1800
+
1801
+ # --- Backwards compatibility aliases (hidden) ---
1802
+
1803
+ # Hidden alias: sprint story-add -> sprint story add
1804
+ sprint.add_command(story_add_command, "story-add")
1805
+ sprint.commands["story-add"].hidden = True
1806
+
1807
+ # Hidden alias: sprint story-update -> sprint story update
1808
+ sprint.add_command(story_update_command, "story-update")
1809
+ sprint.commands["story-update"].hidden = True
1810
+
1811
+ # Hidden alias: sprint archive-epic -> sprint epic archive
1812
+ @sprint.command("archive-epic", hidden=True)
1813
+ @click.argument("epic_id", required=False)
1814
+ @click.option("--dry-run", is_flag=True)
1815
+ @click.option("--jira", is_flag=True)
1816
+ def archive_epic_compat(epic_id, dry_run, jira):
1817
+ """(Deprecated) Use 'sprint epic archive' instead."""
1818
+ ctx = click.get_current_context()
1819
+ ctx.invoke(epic_archive, epic_id=epic_id, dry_run=dry_run, jira=jira)
1820
+
1821
+ # Hidden alias: sprint import-epic -> sprint epic import
1822
+ @sprint.command("import-epic", hidden=True)
1823
+ @click.argument("epics_file")
1824
+ @click.argument("initiative_name", required=False)
1825
+ @click.option("--marker", default="imported")
1826
+ @click.option("--dry-run", is_flag=True)
1827
+ def import_epic_compat(epics_file, initiative_name, marker, dry_run):
1828
+ """(Deprecated) Use 'sprint epic import' instead."""
1829
+ ctx = click.get_current_context()
1830
+ ctx.invoke(epic_import, epics_file=epics_file, initiative_name=initiative_name, marker=marker, dry_run=dry_run)
1831
+
1832
+ # Hidden alias: sprint remove-epic -> sprint epic remove
1833
+ @sprint.command("remove-epic", hidden=True)
1834
+ @click.argument("epic_id")
1835
+ @click.option("--dry-run", is_flag=True)
1836
+ def remove_epic_compat(epic_id, dry_run):
1837
+ """(Deprecated) Use 'sprint epic remove' instead."""
1838
+ ctx = click.get_current_context()
1839
+ ctx.invoke(epic_remove, epic_id=epic_id, dry_run=dry_run)
1840
+
1841
+ # Hidden alias: sprint epic-add -> sprint epic add
1842
+ sprint.add_command(epic_add_command, "epic-add")
1843
+ sprint.commands["epic-add"].hidden = True
1844
+
1845
+
1846
+ # Register validate command from validate_cmd module
1847
+ from pennyfarthing_scripts.sprint.validate_cmd import validate_command
1848
+
1849
+ sprint.add_command(validate_command)
1850
+
1851
+
1852
+ # For backwards compatibility when running as module
1853
+ def main(args: list[str] | None = None) -> int:
1854
+ """Entry point for backwards compatibility."""
1855
+ try:
1856
+ sprint(args)
1857
+ return 0
1858
+ except SystemExit as e:
1859
+ return e.code if isinstance(e.code, int) else 0
1860
+
1861
+
1862
+ if __name__ == "__main__":
1863
+ sprint()