@pennyfarthing/core 10.0.3 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/README.md +9 -7
  2. package/package.json +7 -1
  3. package/packages/core/dist/cli/commands/cyclist.d.ts +5 -1
  4. package/packages/core/dist/cli/commands/cyclist.d.ts.map +1 -1
  5. package/packages/core/dist/cli/commands/cyclist.js +4 -4
  6. package/packages/core/dist/cli/commands/cyclist.js.map +1 -1
  7. package/packages/core/dist/cli/commands/cyclist.test.js +2 -2
  8. package/packages/core/dist/cli/commands/cyclist.test.js.map +1 -1
  9. package/packages/core/dist/cli/commands/doctor-legacy.test.js +17 -16
  10. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  11. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  12. package/packages/core/dist/cli/commands/doctor.js +251 -4
  13. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  14. package/packages/core/dist/cli/commands/init.d.ts +7 -0
  15. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  16. package/packages/core/dist/cli/commands/init.js +43 -7
  17. package/packages/core/dist/cli/commands/init.js.map +1 -1
  18. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  19. package/packages/core/dist/cli/commands/update.js +26 -0
  20. package/packages/core/dist/cli/commands/update.js.map +1 -1
  21. package/packages/core/dist/cli/index.js +1 -1
  22. package/packages/core/dist/cli/index.js.map +1 -1
  23. package/packages/core/dist/cli/ocean-profiles.test.js +1 -1
  24. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  25. package/packages/core/dist/cli/utils/files.d.ts +10 -0
  26. package/packages/core/dist/cli/utils/files.d.ts.map +1 -1
  27. package/packages/core/dist/cli/utils/files.js +35 -0
  28. package/packages/core/dist/cli/utils/files.js.map +1 -1
  29. package/packages/core/dist/cli/utils/python.d.ts +22 -0
  30. package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
  31. package/packages/core/dist/cli/utils/python.js +102 -0
  32. package/packages/core/dist/cli/utils/python.js.map +1 -0
  33. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  34. package/packages/core/dist/cli/utils/settings.js +10 -0
  35. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  36. package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
  37. package/packages/core/dist/scripts/generate-report.js +11 -7
  38. package/packages/core/dist/scripts/generate-report.js.map +1 -1
  39. package/packages/core/dist/scripts/generate-spider-report.d.ts.map +1 -1
  40. package/packages/core/dist/scripts/generate-spider-report.js +12 -8
  41. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  42. package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
  43. package/packages/core/dist/scripts/generate-spider.js +6 -4
  44. package/packages/core/dist/scripts/generate-spider.js.map +1 -1
  45. package/packages/core/dist/scripts/generate-spider.test.js +2 -2
  46. package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
  47. package/pennyfarthing-dist/agents/README.md +1 -3
  48. package/pennyfarthing-dist/agents/architect.md +0 -6
  49. package/pennyfarthing-dist/agents/devops.md +0 -6
  50. package/pennyfarthing-dist/agents/orchestrator.md +0 -6
  51. package/pennyfarthing-dist/agents/pm.md +1 -7
  52. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  53. package/pennyfarthing-dist/agents/sm-setup.md +2 -2
  54. package/pennyfarthing-dist/agents/sm.md +4 -11
  55. package/pennyfarthing-dist/commands/architect.md +11 -3
  56. package/pennyfarthing-dist/commands/close-epic.md +24 -131
  57. package/pennyfarthing-dist/commands/create-theme.md +14 -24
  58. package/pennyfarthing-dist/commands/dev.md +11 -3
  59. package/pennyfarthing-dist/commands/devops.md +11 -3
  60. package/pennyfarthing-dist/commands/health-check.md +1 -3
  61. package/pennyfarthing-dist/commands/help.md +8 -12
  62. package/pennyfarthing-dist/commands/list-themes.md +14 -16
  63. package/pennyfarthing-dist/commands/orchestrator.md +11 -3
  64. package/pennyfarthing-dist/commands/parallel-work.md +1 -3
  65. package/pennyfarthing-dist/commands/pm.md +11 -3
  66. package/pennyfarthing-dist/commands/prime.md +6 -6
  67. package/pennyfarthing-dist/commands/repo-status.md +2 -2
  68. package/pennyfarthing-dist/commands/reviewer.md +11 -3
  69. package/pennyfarthing-dist/commands/run-ci.md +1 -1
  70. package/pennyfarthing-dist/commands/set-theme.md +14 -51
  71. package/pennyfarthing-dist/commands/setup.md +1 -1
  72. package/pennyfarthing-dist/commands/show-theme.md +14 -16
  73. package/pennyfarthing-dist/commands/sm.md +11 -3
  74. package/pennyfarthing-dist/commands/sprint.md +8 -8
  75. package/pennyfarthing-dist/commands/tea.md +11 -3
  76. package/pennyfarthing-dist/commands/tech-writer.md +11 -3
  77. package/pennyfarthing-dist/commands/theme-maker.md +14 -671
  78. package/pennyfarthing-dist/commands/theme.md +95 -0
  79. package/pennyfarthing-dist/commands/ux-designer.md +11 -3
  80. package/pennyfarthing-dist/commands/work.md +3 -5
  81. package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
  82. package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
  83. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
  84. package/pennyfarthing-dist/guides/hooks.md +5 -5
  85. package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
  86. package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
  87. package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
  88. package/pennyfarthing-dist/guides/prime.md +2 -2
  89. package/pennyfarthing-dist/guides/skill-schema.md +25 -26
  90. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  91. package/pennyfarthing-dist/scripts/README.md +2 -2
  92. package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
  93. package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
  94. package/pennyfarthing-dist/scripts/git/git-status-all.sh +1 -1
  95. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
  96. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +3 -3
  97. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +14 -12
  98. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
  99. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
  100. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +1 -1
  101. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  102. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +3 -3
  103. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
  104. package/pennyfarthing-dist/scripts/sprint/README.md +32 -17
  105. package/pennyfarthing-dist/scripts/story/README.md +1 -1
  106. package/pennyfarthing-dist/scripts/test/test-setup.sh +1 -1
  107. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
  108. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
  109. package/pennyfarthing-dist/scripts/theme/README.md +1 -1
  110. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
  111. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +62 -17
  112. package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
  113. package/pennyfarthing-dist/skills/skill-registry.yaml +41 -28
  114. package/pennyfarthing-dist/skills/sprint/skill.md +386 -68
  115. package/pennyfarthing-dist/skills/story/skill.md +14 -206
  116. package/pennyfarthing-dist/skills/theme/skill.md +290 -75
  117. package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
  118. package/pennyfarthing-dist/skills/workflow/skill.md +4 -4
  119. package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
  120. package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
  121. package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
  122. package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
  123. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +42 -25
  124. package/pennyfarthing-dist/workflows/git-cleanup.yaml +1 -1
  125. package/pennyfarthing-dist/workflows/project-setup/steps/step-10-complete.md +1 -1
  126. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/cli.py +15 -0
  131. package/pennyfarthing_scripts/codemarkers/__init__.py +19 -0
  132. package/pennyfarthing_scripts/codemarkers/__main__.py +6 -0
  133. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  135. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  136. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  137. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  138. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/codemarkers/analyze.py +326 -0
  140. package/pennyfarthing_scripts/codemarkers/cli.py +129 -0
  141. package/pennyfarthing_scripts/codemarkers/formatters.py +89 -0
  142. package/pennyfarthing_scripts/codemarkers/models.py +45 -0
  143. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  145. package/pennyfarthing_scripts/common/config.py +2 -1
  146. package/pennyfarthing_scripts/complexity/__init__.py +15 -0
  147. package/pennyfarthing_scripts/complexity/__main__.py +6 -0
  148. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  149. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  150. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  151. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  152. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  153. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  154. package/pennyfarthing_scripts/complexity/analyze.py +207 -0
  155. package/pennyfarthing_scripts/complexity/cli.py +78 -0
  156. package/pennyfarthing_scripts/complexity/formatters.py +64 -0
  157. package/pennyfarthing_scripts/complexity/models.py +32 -0
  158. package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
  159. package/pennyfarthing_scripts/deadcode/__main__.py +6 -0
  160. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  162. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  163. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  164. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  165. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/deadcode/analyze.py +323 -0
  167. package/pennyfarthing_scripts/deadcode/cli.py +163 -0
  168. package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
  169. package/pennyfarthing_scripts/deadcode/models.py +54 -0
  170. package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
  171. package/pennyfarthing_scripts/dependencies/__main__.py +5 -0
  172. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/dependencies/analyze.py +155 -0
  179. package/pennyfarthing_scripts/dependencies/cli.py +72 -0
  180. package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
  181. package/pennyfarthing_scripts/dependencies/models.py +39 -0
  182. package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
  183. package/pennyfarthing_scripts/healthscore/__main__.py +6 -0
  184. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  185. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  190. package/pennyfarthing_scripts/healthscore/analyze.py +161 -0
  191. package/pennyfarthing_scripts/healthscore/cli.py +76 -0
  192. package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
  193. package/pennyfarthing_scripts/healthscore/models.py +44 -0
  194. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/hotspots/analyze.py +28 -1
  201. package/pennyfarthing_scripts/hotspots/cli.py +11 -9
  202. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/jira/bidirectional.py +42 -15
  210. package/pennyfarthing_scripts/jira/cli.py +78 -1
  211. package/pennyfarthing_scripts/jira/client.py +28 -0
  212. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/prime/workflow.py +5 -3
  218. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  220. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  222. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/sprint/archive.py +63 -6
  231. package/pennyfarthing_scripts/sprint/archive_epic.py +198 -85
  232. package/pennyfarthing_scripts/sprint/cli.py +1565 -65
  233. package/pennyfarthing_scripts/sprint/epic_add.py +173 -0
  234. package/pennyfarthing_scripts/sprint/loader.py +46 -2
  235. package/pennyfarthing_scripts/sprint/story_add.py +202 -27
  236. package/pennyfarthing_scripts/sprint/story_finish.py +211 -0
  237. package/pennyfarthing_scripts/sprint/validate_cmd.py +44 -5
  238. package/pennyfarthing_scripts/sprint/validator.py +13 -3
  239. package/pennyfarthing_scripts/sprint/work.py +43 -3
  240. package/pennyfarthing_scripts/sprint/yaml_io.py +124 -15
  241. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  242. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  243. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  244. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  245. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  246. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  247. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  248. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  249. package/pennyfarthing_scripts/tests/test_codemarkers.py +682 -0
  250. package/pennyfarthing_scripts/tests/test_healthscore.py +524 -0
  251. package/pennyfarthing_scripts/tests/test_sprint_package.py +166 -0
  252. package/pennyfarthing_scripts/tests/test_yaml_io.py +117 -0
  253. package/pennyfarthing_scripts/theme/__init__.py +5 -0
  254. package/pennyfarthing_scripts/theme/__main__.py +6 -0
  255. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/theme/cli.py +286 -0
  258. package/scripts/README.md +53 -0
  259. package/scripts/postinstall.cjs +34 -0
  260. package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
  261. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +0 -133
  262. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +0 -91
  263. package/pennyfarthing-dist/scripts/sprint/check-story.sh +0 -158
  264. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +0 -52
  265. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +0 -63
  266. package/pennyfarthing-dist/scripts/sprint/list-future.sh +0 -145
  267. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +0 -110
  268. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +0 -148
  269. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +0 -415
  270. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +0 -33
  271. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +0 -230
  272. package/pennyfarthing-dist/scripts/sprint/sprint-status.sh +0 -134
  273. package/pennyfarthing-dist/scripts/sprint/validate-sprint-yaml.sh +0 -139
  274. package/pennyfarthing-dist/skills/sprint/scripts/archive-story.sh +0 -101
  275. package/pennyfarthing-dist/skills/sprint/scripts/available-stories.sh +0 -97
  276. package/pennyfarthing-dist/skills/sprint/scripts/check-story.sh +0 -164
  277. package/pennyfarthing-dist/skills/sprint/scripts/create-jira-epic.sh +0 -23
  278. package/pennyfarthing-dist/skills/sprint/scripts/new-sprint.sh +0 -116
  279. package/pennyfarthing-dist/skills/sprint/scripts/promote-epic.sh +0 -164
  280. package/pennyfarthing-dist/skills/sprint/scripts/sprint-info.sh +0 -39
  281. package/pennyfarthing-dist/skills/sprint/scripts/sprint-status.sh +0 -147
  282. package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +0 -23
@@ -1,25 +1,30 @@
1
1
  """
2
2
  Sprint epic archiving.
3
3
 
4
- Provides functions for archiving completed epics to sprint archive files.
5
- Handles archive file creation, epic completion detection, and YAML updates.
4
+ Archives completed epics by moving their shard files to sprint/archive/.
5
+ The sprint completed file references archived epics by ID (not inlined).
6
6
  """
7
7
 
8
+ import shutil
8
9
  from datetime import date
9
10
  from pathlib import Path
10
11
  from typing import Any
11
12
 
12
- import yaml
13
-
14
- from pennyfarthing_scripts.common.config import get_project_root, load_yaml_config
13
+ from pennyfarthing_scripts.common.config import get_project_root
15
14
  from pennyfarthing_scripts.sprint.loader import load_sprint
15
+ from pennyfarthing_scripts.sprint.yaml_io import (
16
+ _get_epic_ref,
17
+ _make_yaml,
18
+ _read_yaml_file,
19
+ _write_yaml_file,
20
+ read_sprint,
21
+ write_sprint,
22
+ )
16
23
 
17
24
 
18
25
  def get_archive_path(project_root: Path | None = None) -> Path:
19
26
  """Get the archive file path for the current sprint.
20
27
 
21
- Creates the archive file with a template if it doesn't exist.
22
-
23
28
  Args:
24
29
  project_root: Project root path (defaults to auto-detect)
25
30
 
@@ -34,9 +39,7 @@ def get_archive_path(project_root: Path | None = None) -> Path:
34
39
 
35
40
  sprint_info = sprint_data["sprint"]
36
41
 
37
- # Extract sprint identifier (YYWW format from jira_sprint_name)
38
42
  sprint_name = sprint_info.get("jira_sprint_name", "")
39
- # Extract "2604" from "TO Sprint 2604"
40
43
  sprint_id = sprint_name.split()[-1] if sprint_name else str(sprint_info.get("number", "unknown"))
41
44
 
42
45
  archive_path = root / "sprint" / "archive" / f"sprint-{sprint_id}-completed.yaml"
@@ -58,7 +61,6 @@ def ensure_archive_file(project_root: Path | None = None) -> Path:
58
61
  if archive_path.exists():
59
62
  return archive_path
60
63
 
61
- # Create archive file with template
62
64
  sprint_data = load_sprint(root)
63
65
  sprint_info = sprint_data.get("sprint", {})
64
66
 
@@ -66,7 +68,7 @@ def ensure_archive_file(project_root: Path | None = None) -> Path:
66
68
  sprint_id = sprint_info.get("jira_sprint_id", "")
67
69
  goal = sprint_info.get("goal", "")
68
70
 
69
- template = f"""# Sprint {sprint_name} - Completed Stories
71
+ template = f"""# Sprint {sprint_name} - Completed Work
70
72
  # Jira Sprint ID: {sprint_id}
71
73
  # Archived: {date.today().isoformat()}
72
74
 
@@ -76,7 +78,11 @@ sprint:
76
78
  jira_sprint_name: "{sprint_name}"
77
79
  goal: {goal}
78
80
 
79
- completed:
81
+ completed_epics:
82
+ # Epic shard files live in sprint/archive/epic-{{ref}}.yaml
83
+
84
+ completed_stories:
85
+ # Orphan stories not belonging to an epic
80
86
  """
81
87
 
82
88
  archive_path.parent.mkdir(parents=True, exist_ok=True)
@@ -85,25 +91,92 @@ completed:
85
91
  return archive_path
86
92
 
87
93
 
94
+ def _load_archive_file(archive_path: Path) -> dict[str, Any]:
95
+ """Load the sprint archive file, handling both old and new formats.
96
+
97
+ Args:
98
+ archive_path: Path to the archive YAML file
99
+
100
+ Returns:
101
+ Archive data dict with completed_epics and completed_stories
102
+ """
103
+ yml = _make_yaml()
104
+ with open(archive_path) as f:
105
+ data = yml.load(f)
106
+
107
+ if data is None:
108
+ data = {}
109
+
110
+ # Ensure new-format keys exist
111
+ if "completed_epics" not in data:
112
+ data["completed_epics"] = []
113
+ if "completed_stories" not in data:
114
+ data["completed_stories"] = []
115
+
116
+ # Normalize: ensure lists are not None
117
+ if data["completed_epics"] is None:
118
+ data["completed_epics"] = []
119
+ if data["completed_stories"] is None:
120
+ data["completed_stories"] = []
121
+
122
+ return data
123
+
124
+
125
+ def _write_archive_file(archive_path: Path, data: dict[str, Any]) -> None:
126
+ """Write the sprint archive file.
127
+
128
+ Args:
129
+ archive_path: Path to the archive YAML file
130
+ data: Archive data dict
131
+ """
132
+ import io
133
+
134
+ from ruamel.yaml.comments import CommentedMap, CommentedSeq
135
+
136
+ yml = _make_yaml()
137
+
138
+ # Build output preserving comments at the top
139
+ cm = CommentedMap()
140
+ if "sprint" in data:
141
+ cm["sprint"] = data["sprint"]
142
+
143
+ # completed_epics as string refs
144
+ epic_refs = CommentedSeq()
145
+ for ref in data.get("completed_epics", []):
146
+ epic_refs.append(ref)
147
+ cm["completed_epics"] = epic_refs
148
+
149
+ # completed_stories for orphans
150
+ stories = CommentedSeq()
151
+ for story in data.get("completed_stories", []):
152
+ stories.append(story)
153
+ cm["completed_stories"] = stories
154
+
155
+ stream = io.StringIO()
156
+ yml.dump(cm, stream)
157
+ output = stream.getvalue()
158
+
159
+ # Clean trailing whitespace
160
+ lines = output.split("\n")
161
+ cleaned = [line.rstrip() for line in lines]
162
+ result = "\n".join(cleaned).rstrip("\n") + "\n"
163
+
164
+ archive_path.write_text(result)
165
+
166
+
88
167
  def is_epic_complete(epic: dict[str, Any]) -> tuple[bool, list[str]]:
89
168
  """Check if an epic is complete by examining story statuses.
90
169
 
91
- An epic is complete if:
92
- - It has status 'done' or 'completed', OR
93
- - All of its stories have a terminal status ('done', 'completed', or 'cancelled')
94
-
95
170
  Args:
96
171
  epic: Epic dict from sprint YAML
97
172
 
98
173
  Returns:
99
174
  Tuple of (is_complete, list of incomplete story IDs)
100
175
  """
101
- # Check if epic itself is marked done
102
176
  epic_status = epic.get("status", "backlog")
103
177
  if epic_status in ("done", "completed"):
104
178
  return True, []
105
179
 
106
- # Check all stories
107
180
  stories = epic.get("stories", [])
108
181
  if not stories:
109
182
  return False, []
@@ -153,6 +226,10 @@ def archive_epic(
153
226
  ) -> dict[str, Any]:
154
227
  """Archive a completed epic.
155
228
 
229
+ Moves the epic shard file to sprint/archive/, adds the epic ref to the
230
+ sprint completed file, and removes it from the current sprint index.
231
+ Context files are also moved to archive.
232
+
156
233
  Args:
157
234
  epic_id: Epic ID to archive (e.g., "epic-64" or "MSSCI-12465")
158
235
  project_root: Project root path (defaults to auto-detect)
@@ -163,16 +240,23 @@ def archive_epic(
163
240
  Dict with success status and details
164
241
  """
165
242
  root = project_root or get_project_root()
166
- sprint_data = load_sprint(root)
243
+ sprint_dir = root / "sprint"
244
+ archive_dir = sprint_dir / "archive"
245
+ archive_dir.mkdir(parents=True, exist_ok=True)
246
+
247
+ sprint_path = sprint_dir / "current-sprint.yaml"
248
+ sprint_data = read_sprint(sprint_path)
167
249
 
168
250
  if not sprint_data or "epics" not in sprint_data:
169
251
  return {"success": False, "error": "Could not load sprint data"}
170
252
 
171
- # Find the epic
253
+ # Find the epic in merged data
172
254
  epic = None
173
255
  epic_index = None
174
256
  for i, e in enumerate(sprint_data["epics"]):
175
- if e.get("id") == epic_id or e.get("jira") == epic_id:
257
+ eid = str(e.get("id", ""))
258
+ ejira = str(e.get("jira", ""))
259
+ if eid == epic_id or ejira == epic_id:
176
260
  epic = e
177
261
  epic_index = i
178
262
  break
@@ -189,73 +273,104 @@ def archive_epic(
189
273
  "incomplete_stories": incomplete,
190
274
  }
191
275
 
276
+ # Determine the shard ref (filename stem)
277
+ epic_ref = _get_epic_ref(epic)
278
+ shard_file = sprint_dir / f"epic-{epic_ref}.yaml"
279
+ archive_shard = archive_dir / f"epic-{epic_ref}.yaml"
280
+ story_count = len(epic.get("stories", []))
281
+ total_points = sum(s.get("points", 0) for s in epic.get("stories", []))
282
+
192
283
  if dry_run:
284
+ msg_parts = [f"Would archive {epic_id} ({story_count} stories, {total_points} pts)"]
285
+ if shard_file.exists():
286
+ msg_parts.append(f" Move: {shard_file.name} → archive/")
287
+ # Check for context file
288
+ for ctx_name in [f"context-epic-{epic_ref}.md", f"context-epic-{epic.get('id', '')}.md"]:
289
+ ctx_file = sprint_dir / "context" / ctx_name
290
+ if ctx_file.exists():
291
+ msg_parts.append(f" Move: context/{ctx_name} → archive/")
292
+ break
193
293
  return {
194
294
  "success": True,
195
295
  "dry_run": True,
196
296
  "epic": epic,
197
- "message": f"Would archive {epic_id} ({len(epic.get('stories', []))} stories)",
297
+ "stories_archived": story_count,
298
+ "total_points": total_points,
299
+ "message": "\n".join(msg_parts),
198
300
  }
199
301
 
200
- # Ensure archive file exists
302
+ # 1. Update epic status in the shard before moving
303
+ if shard_file.exists():
304
+ shard_data = _read_yaml_file(shard_file)
305
+ shard_data["status"] = "done"
306
+ if "completed" not in shard_data:
307
+ shard_data["completed"] = date.today().isoformat()
308
+ _write_yaml_file(shard_file, shard_data)
309
+ # Move shard to archive
310
+ shutil.move(str(shard_file), str(archive_shard))
311
+ else:
312
+ # No shard file on disk — write epic data directly to archive
313
+ epic["status"] = "done"
314
+ if "completed" not in epic:
315
+ epic["completed"] = date.today().isoformat()
316
+ _write_yaml_file(archive_shard, epic)
317
+
318
+ # 2. Move context file if it exists
319
+ context_moved = None
320
+ for ctx_name in [f"context-epic-{epic_ref}.md", f"context-epic-{epic.get('id', '')}.md"]:
321
+ ctx_file = sprint_dir / "context" / ctx_name
322
+ if ctx_file.exists():
323
+ shutil.move(str(ctx_file), str(archive_dir / ctx_name))
324
+ context_moved = ctx_name
325
+ break
326
+
327
+ # 3. Add epic ref to sprint completed file
201
328
  archive_path = ensure_archive_file(root)
329
+ archive_data = _load_archive_file(archive_path)
330
+
331
+ # Add ref if not already present
332
+ if epic_ref not in archive_data["completed_epics"]:
333
+ archive_data["completed_epics"].append(epic_ref)
334
+ _write_archive_file(archive_path, archive_data)
335
+
336
+ # 4. Remove epic from current-sprint.yaml index
337
+ # Re-read the raw index (not merged) to update refs
338
+ yml = _make_yaml()
339
+ with open(sprint_path) as f:
340
+ index_data = yml.load(f)
341
+
342
+ epics_list = index_data.get("epics", [])
343
+ # Remove the matching ref (string) or dict
344
+ new_epics = []
345
+ for item in epics_list:
346
+ if isinstance(item, str):
347
+ if item != epic_ref:
348
+ new_epics.append(item)
349
+ else:
350
+ item_id = str(item.get("id", ""))
351
+ item_jira = str(item.get("jira", ""))
352
+ if item_id != epic_id and item_jira != epic_id:
353
+ new_epics.append(item)
202
354
 
203
- # Build archive entries for all stories
204
- archive_entries = []
205
- epic_jira = epic.get("jira", epic_id)
206
- for story in epic.get("stories", []):
207
- entry = {
208
- "id": story.get("id") or story.get("jira"),
209
- "epic": epic_jira,
210
- "title": story.get("title", ""),
211
- "points": story.get("points", 0),
212
- "completed": story.get("completed", date.today().isoformat()),
213
- }
214
- if story.get("pr"):
215
- entry["pr"] = story["pr"]
216
- archive_entries.append(entry)
217
-
218
- # Append to archive file
219
- with open(archive_path, "a") as f:
220
- # Add epic header comment
221
- epic_title = epic.get("title", epic_id)
222
- f.write(f"\n # {epic_id}: {epic_title} - COMPLETE\n")
223
-
224
- for entry in archive_entries:
225
- f.write(f" - id: {entry['id']}\n")
226
- f.write(f" epic: {entry['epic']}\n")
227
- # Escape quotes in title
228
- title = entry['title'].replace('"', '\\"')
229
- f.write(f' title: "{title}"\n')
230
- f.write(f" points: {entry['points']}\n")
231
- f.write(f" completed: {entry['completed']}\n")
232
- if entry.get("pr"):
233
- f.write(f" pr: {entry['pr']}\n")
234
-
235
- # Remove epic from current-sprint.yaml
236
- sprint_path = root / "sprint" / "current-sprint.yaml"
237
- del sprint_data["epics"][epic_index]
238
-
239
- # Note: We do NOT update completed_points here because stories were already
240
- # marked done - their points are already counted. Archiving just moves them
241
- # to the archive file without changing the accounting.
242
-
243
- # Write updated sprint file
244
- with open(sprint_path, "w") as f:
245
- yaml.dump(sprint_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
355
+ from ruamel.yaml.comments import CommentedSeq
356
+ index_data["epics"] = CommentedSeq(new_epics)
357
+ _write_yaml_file(sprint_path, index_data)
246
358
 
247
359
  result = {
248
360
  "success": True,
249
361
  "epic_id": epic_id,
250
- "jira": epic_jira,
251
- "stories_archived": len(archive_entries),
252
- "archive_path": str(archive_path),
253
- "message": f"Archived {epic_id} with {len(archive_entries)} stories",
362
+ "epic_ref": epic_ref,
363
+ "stories_archived": story_count,
364
+ "total_points": total_points,
365
+ "archive_shard": str(archive_shard),
366
+ "context_moved": context_moved,
367
+ "message": f"Archived {epic_id} ({story_count} stories, {total_points} pts) → archive/epic-{epic_ref}.yaml",
254
368
  }
255
369
 
256
370
  # Update Jira if requested
371
+ epic_jira = epic.get("jira", "")
257
372
  if update_jira and epic_jira:
258
- result["jira_updated"] = _update_jira_epic(epic_jira)
373
+ result["jira_updated"] = _update_jira_epic(str(epic_jira))
259
374
 
260
375
  return result
261
376
 
@@ -312,7 +427,7 @@ def archive_all_completed(
312
427
  results = []
313
428
  for item in completed:
314
429
  epic = item["epic"]
315
- epic_id = epic.get("id")
430
+ epic_id = str(epic.get("id", ""))
316
431
  result = archive_epic(
317
432
  epic_id,
318
433
  project_root=root,
@@ -321,22 +436,18 @@ def archive_all_completed(
321
436
  )
322
437
  results.append(result)
323
438
 
439
+ total_stories = sum(r.get("stories_archived", 0) for r in results if r.get("success"))
440
+ total_points = sum(r.get("total_points", 0) for r in results if r.get("success"))
441
+
324
442
  return {
325
443
  "success": all(r.get("success") for r in results),
326
444
  "archived": results,
327
- "message": f"Processed {len(results)} epics",
445
+ "message": f"Archived {len(results)} epics ({total_stories} stories, {total_points} pts)",
328
446
  }
329
447
 
330
448
 
331
449
  def main(args: list[str] | None = None) -> int:
332
- """CLI entry point for epic archiving.
333
-
334
- Args:
335
- args: Command line arguments
336
-
337
- Returns:
338
- Exit code
339
- """
450
+ """CLI entry point for epic archiving."""
340
451
  import argparse
341
452
  import sys
342
453
 
@@ -378,14 +489,16 @@ def main(args: list[str] | None = None) -> int:
378
489
  print("[DRY-RUN]", result.get("message"))
379
490
  if "archived" in result:
380
491
  for r in result["archived"]:
381
- epic_id = r.get("epic", {}).get("id") if "epic" in r else r.get("epic_id")
382
- stories = len(r.get("epic", {}).get("stories", [])) if "epic" in r else r.get("stories_archived", 0)
383
- print(f" Would archive: {epic_id} ({stories} stories)")
492
+ print(f" {r.get('message')}")
384
493
  else:
385
494
  print(result.get("message"))
386
495
  if "archived" in result:
387
496
  for r in result["archived"]:
388
- print(f" {r.get('epic_id')}: {r.get('stories_archived')} stories")
497
+ print(f" \u2713 {r.get('message')}")
498
+ elif result.get("archive_shard"):
499
+ print(f" Shard: {result.get('archive_shard')}")
500
+ if result.get("context_moved"):
501
+ print(f" Context: {result.get('context_moved')}")
389
502
  return 0
390
503
  else:
391
504
  print(f"Failed: {result.get('error')}", file=sys.stderr)