@pennyfarthing/core 10.0.5 → 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 (303) hide show
  1. package/README.md +9 -7
  2. package/package.json +15 -11
  3. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor.js +251 -4
  5. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  6. package/packages/core/dist/cli/commands/init.d.ts +7 -0
  7. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  8. package/packages/core/dist/cli/commands/init.js +41 -8
  9. package/packages/core/dist/cli/commands/init.js.map +1 -1
  10. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  11. package/packages/core/dist/cli/commands/update.js +26 -0
  12. package/packages/core/dist/cli/commands/update.js.map +1 -1
  13. package/packages/core/dist/cli/index.js +1 -1
  14. package/packages/core/dist/cli/index.js.map +1 -1
  15. package/packages/core/dist/cli/utils/python.d.ts +22 -0
  16. package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
  17. package/packages/core/dist/cli/utils/python.js +102 -0
  18. package/packages/core/dist/cli/utils/python.js.map +1 -0
  19. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  20. package/packages/core/dist/cli/utils/settings.js +10 -0
  21. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  22. package/pennyfarthing-dist/agents/README.md +1 -3
  23. package/pennyfarthing-dist/agents/architect.md +0 -6
  24. package/pennyfarthing-dist/agents/devops.md +0 -6
  25. package/pennyfarthing-dist/agents/orchestrator.md +0 -6
  26. package/pennyfarthing-dist/agents/pm.md +0 -6
  27. package/pennyfarthing-dist/agents/sm.md +0 -6
  28. package/pennyfarthing-dist/commands/architect.md +11 -3
  29. package/pennyfarthing-dist/commands/close-epic.md +24 -131
  30. package/pennyfarthing-dist/commands/create-theme.md +14 -24
  31. package/pennyfarthing-dist/commands/dev.md +11 -3
  32. package/pennyfarthing-dist/commands/devops.md +11 -3
  33. package/pennyfarthing-dist/commands/health-check.md +1 -3
  34. package/pennyfarthing-dist/commands/help.md +8 -12
  35. package/pennyfarthing-dist/commands/list-themes.md +14 -16
  36. package/pennyfarthing-dist/commands/orchestrator.md +11 -3
  37. package/pennyfarthing-dist/commands/parallel-work.md +1 -3
  38. package/pennyfarthing-dist/commands/pm.md +11 -3
  39. package/pennyfarthing-dist/commands/prime.md +6 -6
  40. package/pennyfarthing-dist/commands/reviewer.md +11 -3
  41. package/pennyfarthing-dist/commands/run-ci.md +1 -1
  42. package/pennyfarthing-dist/commands/set-theme.md +14 -51
  43. package/pennyfarthing-dist/commands/setup.md +1 -1
  44. package/pennyfarthing-dist/commands/show-theme.md +14 -16
  45. package/pennyfarthing-dist/commands/sm.md +11 -3
  46. package/pennyfarthing-dist/commands/tea.md +11 -3
  47. package/pennyfarthing-dist/commands/tech-writer.md +11 -3
  48. package/pennyfarthing-dist/commands/theme-maker.md +14 -671
  49. package/pennyfarthing-dist/commands/theme.md +95 -0
  50. package/pennyfarthing-dist/commands/ux-designer.md +11 -3
  51. package/pennyfarthing-dist/commands/work.md +3 -5
  52. package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
  53. package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
  54. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
  55. package/pennyfarthing-dist/guides/hooks.md +5 -5
  56. package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
  57. package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
  58. package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
  59. package/pennyfarthing-dist/guides/prime.md +2 -2
  60. package/pennyfarthing-dist/guides/skill-schema.md +4 -4
  61. package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
  62. package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
  63. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -0
  64. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
  65. package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
  66. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  67. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
  68. package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
  69. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
  70. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  71. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
  72. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  73. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +0 -0
  74. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  75. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  76. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  77. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +0 -0
  78. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +12 -5
  79. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
  80. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  81. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
  82. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  83. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  84. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  85. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  86. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  87. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  88. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
  89. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  90. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  91. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  92. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  93. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  94. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  95. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  96. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  97. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  98. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  99. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  100. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  101. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  102. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  103. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  104. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  105. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  106. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  107. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  108. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  109. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  110. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  111. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  112. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
  113. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  114. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  115. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  116. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  117. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  118. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  119. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  120. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  121. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
  122. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
  123. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  124. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  125. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  126. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -0
  127. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -0
  128. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -0
  129. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -0
  130. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  131. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  132. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  133. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
  134. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  135. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  136. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  137. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  138. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  139. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
  140. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  141. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  142. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  143. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  144. package/pennyfarthing-dist/scripts/theme/README.md +1 -1
  145. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -0
  146. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -0
  147. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  148. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -0
  149. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
  150. package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
  151. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  152. package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
  153. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +62 -17
  154. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
  155. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  156. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
  157. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
  158. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
  159. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
  160. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
  161. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
  162. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
  163. package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
  164. package/pennyfarthing-dist/skills/skill-registry.yaml +20 -16
  165. package/pennyfarthing-dist/skills/story/scripts/create-story.sh +0 -0
  166. package/pennyfarthing-dist/skills/story/scripts/size-story.sh +0 -0
  167. package/pennyfarthing-dist/skills/story/scripts/story-template.sh +0 -0
  168. package/pennyfarthing-dist/skills/theme/skill.md +290 -75
  169. package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
  170. package/pennyfarthing-dist/skills/workflow/scripts/list-workflows.sh +0 -0
  171. package/pennyfarthing-dist/skills/workflow/scripts/resume-workflow.sh +0 -0
  172. package/pennyfarthing-dist/skills/workflow/scripts/show-workflow.sh +0 -0
  173. package/pennyfarthing-dist/skills/workflow/scripts/start-workflow.sh +0 -0
  174. package/pennyfarthing-dist/skills/workflow/scripts/workflow-status.sh +0 -0
  175. package/pennyfarthing-dist/skills/workflow/skill.md +4 -4
  176. package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
  177. package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
  178. package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
  179. package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
  180. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +41 -24
  181. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  183. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  184. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  185. package/pennyfarthing_scripts/cli.py +15 -0
  186. package/pennyfarthing_scripts/codemarkers/__init__.py +19 -0
  187. package/pennyfarthing_scripts/codemarkers/__main__.py +6 -0
  188. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  190. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  191. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  192. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/codemarkers/analyze.py +326 -0
  195. package/pennyfarthing_scripts/codemarkers/cli.py +129 -0
  196. package/pennyfarthing_scripts/codemarkers/formatters.py +89 -0
  197. package/pennyfarthing_scripts/codemarkers/models.py +45 -0
  198. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/complexity/__init__.py +15 -0
  201. package/pennyfarthing_scripts/complexity/__main__.py +6 -0
  202. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/complexity/analyze.py +207 -0
  209. package/pennyfarthing_scripts/complexity/cli.py +78 -0
  210. package/pennyfarthing_scripts/complexity/formatters.py +64 -0
  211. package/pennyfarthing_scripts/complexity/models.py +32 -0
  212. package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
  213. package/pennyfarthing_scripts/deadcode/__main__.py +6 -0
  214. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  220. package/pennyfarthing_scripts/deadcode/analyze.py +323 -0
  221. package/pennyfarthing_scripts/deadcode/cli.py +163 -0
  222. package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
  223. package/pennyfarthing_scripts/deadcode/models.py +54 -0
  224. package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
  225. package/pennyfarthing_scripts/dependencies/__main__.py +5 -0
  226. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/dependencies/analyze.py +155 -0
  233. package/pennyfarthing_scripts/dependencies/cli.py +72 -0
  234. package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
  235. package/pennyfarthing_scripts/dependencies/models.py +39 -0
  236. package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
  237. package/pennyfarthing_scripts/healthscore/__main__.py +6 -0
  238. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  239. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  240. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  241. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  242. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  243. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  244. package/pennyfarthing_scripts/healthscore/analyze.py +161 -0
  245. package/pennyfarthing_scripts/healthscore/cli.py +76 -0
  246. package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
  247. package/pennyfarthing_scripts/healthscore/models.py +44 -0
  248. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  249. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/hotspots/analyze.py +27 -0
  256. package/pennyfarthing_scripts/hotspots/cli.py +10 -8
  257. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  261. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  262. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  263. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  264. package/pennyfarthing_scripts/jira/bidirectional.py +42 -15
  265. package/pennyfarthing_scripts/jira/cli.py +4 -1
  266. package/pennyfarthing_scripts/jira/client.py +28 -0
  267. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  283. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/sprint/archive_epic.py +198 -94
  285. package/pennyfarthing_scripts/sprint/cli.py +29 -19
  286. package/pennyfarthing_scripts/sprint/story_add.py +202 -27
  287. package/pennyfarthing_scripts/sprint/story_finish.py +211 -0
  288. package/pennyfarthing_scripts/sprint/work.py +27 -3
  289. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  290. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  291. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  292. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  293. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  294. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  295. package/pennyfarthing_scripts/tests/test_codemarkers.py +682 -0
  296. package/pennyfarthing_scripts/tests/test_healthscore.py +524 -0
  297. package/pennyfarthing_scripts/theme/__init__.py +5 -0
  298. package/pennyfarthing_scripts/theme/__main__.py +6 -0
  299. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/theme/cli.py +286 -0
  302. package/scripts/README.md +53 -0
  303. package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
@@ -5,6 +5,7 @@ Story: MSSCI-14256 - Sprint story add command
5
5
  This module provides:
6
6
  - generate_story_id(sprint_data, epic) -> str
7
7
  - add_story(sprint_path, epic_id, title, points, ...) -> dict
8
+ - add_initiative_story(initiative_slug, title, points, ...) -> dict
8
9
  - story_add_command (Click command for CLI registration)
9
10
  """
10
11
 
@@ -144,44 +145,218 @@ def add_story(
144
145
  }
145
146
 
146
147
 
148
+ def _generate_initiative_story_id(init_data: dict[str, Any], slug: str) -> str:
149
+ """Generate the next standalone story ID for an initiative.
150
+
151
+ Uses the pattern {slug-prefix}-{N} where slug-prefix is derived from
152
+ the initiative slug (e.g., "technical-debt" -> "td", "quality-scale" -> "qs").
153
+ Only counts existing stories that share the same prefix.
154
+
155
+ Args:
156
+ init_data: Initiative YAML data
157
+ slug: Initiative slug (e.g., "technical-debt")
158
+
159
+ Returns:
160
+ Next story ID string (e.g., "td-2")
161
+ """
162
+ # Build prefix from initiative slug initials
163
+ parts = slug.split("-")
164
+ prefix = "".join(p[0] for p in parts if p)
165
+
166
+ stories = init_data.get("standalone_stories", [])
167
+ max_seq = 0
168
+ for story in stories:
169
+ story_id = str(story.get("id", ""))
170
+ # Only count stories with matching prefix
171
+ if not story_id.startswith(f"{prefix}-"):
172
+ continue
173
+ suffix = story_id[len(prefix) + 1:]
174
+ try:
175
+ seq = int(suffix)
176
+ if seq > max_seq:
177
+ max_seq = seq
178
+ except ValueError:
179
+ pass
180
+
181
+ return f"{prefix}-{max_seq + 1}"
182
+
183
+
184
+ def add_initiative_story(
185
+ initiative_slug: str,
186
+ title: str,
187
+ points: int,
188
+ *,
189
+ story_type: str | None = None,
190
+ priority: str = "P1",
191
+ workflow: str = "tdd",
192
+ jira: str | None = None,
193
+ repos: str = "pennyfarthing",
194
+ ) -> dict[str, Any]:
195
+ """Add a standalone story to an initiative YAML file.
196
+
197
+ Args:
198
+ initiative_slug: Initiative slug (e.g., "technical-debt")
199
+ title: Story title
200
+ points: Story points
201
+ story_type: Optional story type (feature, bug, chore, refactor)
202
+ priority: Priority (default: P1)
203
+ workflow: Workflow (default: tdd)
204
+ jira: Optional Jira key
205
+ repos: Repos (default: pennyfarthing)
206
+
207
+ Returns:
208
+ Dict with success status and story_id or error
209
+ """
210
+ from pennyfarthing_scripts.common.config import get_project_root
211
+
212
+ root = get_project_root()
213
+ init_path = root / "sprint" / f"initiative-{initiative_slug}.yaml"
214
+
215
+ if not init_path.exists():
216
+ available = [
217
+ f.stem.replace("initiative-", "")
218
+ for f in (root / "sprint").glob("initiative-*.yaml")
219
+ ]
220
+ return {
221
+ "success": False,
222
+ "error": f"Initiative '{initiative_slug}' not found. Available: {', '.join(sorted(available))}",
223
+ }
224
+
225
+ # Use ruamel.yaml to preserve block scalars and formatting
226
+ from ruamel.yaml import YAML as RuamelYAML
227
+
228
+ ryml = RuamelYAML()
229
+ ryml.preserve_quotes = True
230
+ ryml.default_flow_style = False
231
+ ryml.indent(mapping=2, sequence=4, offset=2)
232
+ ryml.width = 4096
233
+
234
+ with open(init_path) as f:
235
+ init_data = ryml.load(f)
236
+
237
+ if not init_data:
238
+ return {"success": False, "error": f"Empty initiative file: {init_path}"}
239
+
240
+ story_id = _generate_initiative_story_id(init_data, initiative_slug)
241
+
242
+ story: dict[str, Any] = {
243
+ "id": story_id,
244
+ "title": title,
245
+ "points": points,
246
+ "priority": priority,
247
+ "status": "backlog",
248
+ "repos": repos,
249
+ "workflow": workflow,
250
+ }
251
+ if jira is not None:
252
+ story["jira"] = jira
253
+ if story_type is not None:
254
+ story["type"] = story_type
255
+
256
+ if "standalone_stories" not in init_data:
257
+ init_data["standalone_stories"] = []
258
+ init_data["standalone_stories"].append(story)
259
+
260
+ # Update total_points
261
+ current_total = init_data.get("total_points", 0) or 0
262
+ init_data["total_points"] = current_total + points
263
+
264
+ with open(init_path, "w") as f:
265
+ ryml.dump(init_data, f)
266
+
267
+ return {
268
+ "success": True,
269
+ "story_id": story_id,
270
+ }
271
+
272
+
147
273
  @click.command("add")
148
- @click.argument("epic_id", type=str)
149
- @click.argument("title", type=str)
150
- @click.argument("points", type=int)
274
+ @click.argument("epic_id", type=str, required=False)
275
+ @click.argument("title", type=str, required=False)
276
+ @click.argument("points", type=int, required=False)
151
277
  @click.option("--type", "story_type", type=click.Choice(["feature", "bug", "chore", "refactor"]), default="feature")
152
278
  @click.option("--priority", type=click.Choice(["P0", "P1", "P2", "P3"]), default="P1")
153
279
  @click.option("--workflow", type=click.Choice(["tdd", "trivial", "bdd"]), default="tdd")
154
280
  @click.option("--jira", "jira_id", type=str, default=None)
155
281
  @click.option("--sprint-file", type=click.Path(), default=None, help="Path to sprint YAML file")
282
+ @click.option("--initiative", type=str, default=None, help="Add as standalone story to initiative (e.g., technical-debt)")
283
+ @click.option("--repos", type=str, default="pennyfarthing", help="Repos (default: pennyfarthing)")
156
284
  def story_add_command(
157
- epic_id: str,
158
- title: str,
159
- points: int,
285
+ epic_id: str | None,
286
+ title: str | None,
287
+ points: int | None,
160
288
  story_type: str,
161
289
  priority: str,
162
290
  workflow: str,
163
291
  jira_id: str | None,
164
292
  sprint_file: str | None,
293
+ initiative: str | None,
294
+ repos: str,
165
295
  ) -> None:
166
- """Add a new story to an epic."""
167
- if sprint_file is None:
168
- from pennyfarthing_scripts.common.config import get_project_root
169
- path = get_project_root() / "sprint" / "current-sprint.yaml"
170
- else:
171
- path = Path(sprint_file)
172
-
173
- result = add_story(
174
- sprint_path=path,
175
- epic_id=epic_id,
176
- title=title,
177
- points=points,
178
- story_type=story_type if story_type != "feature" else None,
179
- priority=priority,
180
- workflow=workflow,
181
- jira=jira_id,
182
- )
183
-
184
- if result["success"]:
185
- click.echo(f"Added story {result['story_id']}: {title} [{points}pts]")
296
+ """Add a new story to an epic or initiative.
297
+
298
+ \b
299
+ Epic mode (default):
300
+ pf sprint story add <EPIC_ID> <TITLE> <POINTS>
301
+
302
+ Initiative mode (--initiative):
303
+ pf sprint story add --initiative <SLUG> <TITLE> <POINTS>
304
+ """
305
+ if initiative:
306
+ # Initiative mode: first positional arg is title, second is points
307
+ # epic_id absorbs the title, title absorbs points (as str)
308
+ if epic_id is None:
309
+ raise click.ClickException("TITLE is required")
310
+ init_title = epic_id
311
+ if title is None:
312
+ raise click.ClickException("POINTS is required")
313
+ try:
314
+ init_points = int(title)
315
+ except ValueError:
316
+ raise click.ClickException(f"POINTS must be an integer, got '{title}'")
317
+
318
+ result = add_initiative_story(
319
+ initiative_slug=initiative,
320
+ title=init_title,
321
+ points=init_points,
322
+ story_type=story_type if story_type != "feature" else None,
323
+ priority=priority,
324
+ workflow=workflow,
325
+ jira=jira_id,
326
+ repos=repos,
327
+ )
328
+
329
+ if result["success"]:
330
+ click.echo(f"Added story {result['story_id']}: {init_title} [{init_points}pts] to initiative {initiative}")
331
+ else:
332
+ raise click.ClickException(result["error"])
186
333
  else:
187
- raise click.ClickException(result["error"])
334
+ # Epic mode: all three positional args required
335
+ if epic_id is None:
336
+ raise click.ClickException("EPIC_ID is required")
337
+ if title is None:
338
+ raise click.ClickException("TITLE is required")
339
+ if points is None:
340
+ raise click.ClickException("POINTS is required")
341
+
342
+ if sprint_file is None:
343
+ from pennyfarthing_scripts.common.config import get_project_root
344
+ path = get_project_root() / "sprint" / "current-sprint.yaml"
345
+ else:
346
+ path = Path(sprint_file)
347
+
348
+ result = add_story(
349
+ sprint_path=path,
350
+ epic_id=epic_id,
351
+ title=title,
352
+ points=points,
353
+ story_type=story_type if story_type != "feature" else None,
354
+ priority=priority,
355
+ workflow=workflow,
356
+ jira=jira_id,
357
+ )
358
+
359
+ if result["success"]:
360
+ click.echo(f"Added story {result['story_id']}: {title} [{points}pts]")
361
+ else:
362
+ raise click.ClickException(result["error"])
@@ -0,0 +1,211 @@
1
+ """Finish a completed story: archive, merge PR, update Jira, update YAML.
2
+
3
+ Replaces finish-story.sh with native Python that correctly handles
4
+ sharded epic YAML files via read_sprint/write_sprint.
5
+
6
+ Steps:
7
+ 1. Archive session file to sprint/archive/{jira-key}-session.md
8
+ 2. Squash merge PR via gh (handle already-merged)
9
+ 3. Transition Jira to Done
10
+ 4. Update sprint YAML (status: done, completed date, remove assigned_to)
11
+ 5. Archive completed epics
12
+ 6. Git cleanup (checkout develop, pull, delete local branch)
13
+ 7. Remove session file
14
+ """
15
+
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ from datetime import date
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from pennyfarthing_scripts.sprint.loader import find_epic, find_story
24
+ from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
25
+
26
+
27
+ SESSION_FIELD_RE = re.compile(r"\*\*(\w[\w\s]*):\*\*\s*(.*)")
28
+
29
+
30
+ def _parse_session(session_path: Path) -> dict[str, str]:
31
+ """Extract metadata fields from a session markdown file.
32
+
33
+ Parses lines like ``**Jira:** MSSCI-14467`` and
34
+ ``**PR:** #748 - title`` into a dict.
35
+ """
36
+ fields: dict[str, str] = {}
37
+ if not session_path.exists():
38
+ return fields
39
+ for line in session_path.read_text().splitlines():
40
+ m = SESSION_FIELD_RE.search(line)
41
+ if m:
42
+ key = m.group(1).strip().lower()
43
+ value = m.group(2).strip()
44
+ fields[key] = value
45
+ return fields
46
+
47
+
48
+ def _extract_jira_key(fields: dict[str, str]) -> str | None:
49
+ """Get Jira key from session fields, handling markdown link format."""
50
+ raw = fields.get("jira", "")
51
+ # Strip markdown link: [MSSCI-14467](https://...)
52
+ raw = re.sub(r"\[([^\]]+)\].*", r"\1", raw).strip()
53
+ if re.match(r"^MSSCI-\d+$", raw):
54
+ return raw
55
+ return None
56
+
57
+
58
+ def _extract_pr_number(fields: dict[str, str]) -> str | None:
59
+ """Get PR number from session fields like ``#748 - title``."""
60
+ raw = fields.get("pr", "")
61
+ m = re.search(r"#(\d+)", raw)
62
+ return m.group(1) if m else None
63
+
64
+
65
+ def _extract_branch(fields: dict[str, str]) -> str | None:
66
+ """Get branch name, stripping trailing annotations like ``(pushed)``."""
67
+ raw = fields.get("branch", "")
68
+ return re.sub(r"\s*\(.*\)\s*$", "", raw).strip() or None
69
+
70
+
71
+ def _run(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
72
+ """Run a subprocess with sane defaults."""
73
+ return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
74
+
75
+
76
+ def finish_story(
77
+ project_root: Path,
78
+ story_id: str,
79
+ *,
80
+ dry_run: bool = False,
81
+ ) -> dict[str, Any]:
82
+ """Finish a story: archive, merge, update Jira, update YAML, clean up.
83
+
84
+ Args:
85
+ project_root: Project root directory.
86
+ story_id: Story ID (e.g., "83-2").
87
+ dry_run: If True, report what would happen without side-effects.
88
+
89
+ Returns:
90
+ Result dict ``{success, data?, error?, steps?}``.
91
+ """
92
+ session_path = project_root / ".session" / f"{story_id}-session.md"
93
+ sprint_path = project_root / "sprint" / "current-sprint.yaml"
94
+ archive_dir = project_root / "sprint" / "archive"
95
+ archive_dir.mkdir(parents=True, exist_ok=True)
96
+
97
+ # --- Validate session ---
98
+ if not session_path.exists():
99
+ return {"success": False, "error": f"Session file not found: {session_path}"}
100
+
101
+ fields = _parse_session(session_path)
102
+ jira_key = _extract_jira_key(fields)
103
+ branch = _extract_branch(fields)
104
+ pr_number = _extract_pr_number(fields)
105
+
106
+ # Fallback: resolve Jira key from sprint YAML
107
+ if not jira_key:
108
+ try:
109
+ data = read_sprint(sprint_path)
110
+ parts = story_id.split("-")
111
+ if len(parts) >= 2:
112
+ epic = find_epic(data, parts[0])
113
+ story = find_story(epic, story_id) if epic else None
114
+ if story:
115
+ jira_key = story.get("jira")
116
+ except Exception:
117
+ pass
118
+
119
+ if not jira_key:
120
+ return {"success": False, "error": f"Could not determine Jira key for {story_id}"}
121
+
122
+ # Fallback: resolve PR from GitHub if not in session
123
+ if not pr_number and branch:
124
+ result = _run(["gh", "pr", "list", "--head", branch, "--json", "number", "--jq", ".[0].number"])
125
+ if result.returncode == 0 and result.stdout.strip():
126
+ pr_number = result.stdout.strip()
127
+
128
+ today = date.today().isoformat()
129
+ steps: list[dict[str, Any]] = []
130
+
131
+ if dry_run:
132
+ steps.append({"step": 1, "action": f"Archive session → {archive_dir / f'{jira_key}-session.md'}"})
133
+ if pr_number:
134
+ steps.append({"step": 2, "action": f"Merge PR #{pr_number} (squash, delete branch)"})
135
+ else:
136
+ steps.append({"step": 2, "action": "No PR to merge"})
137
+ steps.append({"step": 3, "action": f"Transition {jira_key} to Done"})
138
+ steps.append({"step": 4, "action": f"Update sprint YAML (status: done, completed: {today})"})
139
+ steps.append({"step": 5, "action": "Archive completed epics"})
140
+ steps.append({"step": 6, "action": f"Delete local branch: {branch}"})
141
+ steps.append({"step": 7, "action": "Remove session file"})
142
+ return {"success": True, "dry_run": True, "jira_key": jira_key, "steps": steps}
143
+
144
+ # --- Step 1: Archive session ---
145
+ archive_dest = archive_dir / f"{jira_key}-session.md"
146
+ shutil.copy2(session_path, archive_dest)
147
+ steps.append({"step": 1, "action": "archive_session", "dest": str(archive_dest)})
148
+
149
+ # --- Step 2: Merge PR ---
150
+ if pr_number:
151
+ result = _run(["gh", "pr", "merge", pr_number, "--squash", "--delete-branch"])
152
+ if result.returncode == 0:
153
+ steps.append({"step": 2, "action": "merge_pr", "pr": pr_number})
154
+ else:
155
+ steps.append({"step": 2, "action": "merge_pr", "pr": pr_number, "warning": "Already merged or failed"})
156
+ else:
157
+ steps.append({"step": 2, "action": "merge_pr", "skipped": True})
158
+
159
+ # --- Step 3: Transition Jira ---
160
+ result = _run(["jira", "issue", "move", jira_key, "Done"])
161
+ if result.returncode == 0:
162
+ steps.append({"step": 3, "action": "jira_done", "key": jira_key})
163
+ else:
164
+ steps.append({"step": 3, "action": "jira_done", "key": jira_key, "warning": "Already Done or failed"})
165
+
166
+ # --- Step 4: Update sprint YAML ---
167
+ try:
168
+ data = read_sprint(sprint_path)
169
+ parts = story_id.split("-")
170
+ if len(parts) < 2:
171
+ steps.append({"step": 4, "action": "yaml_update", "error": f"Invalid story ID: {story_id}"})
172
+ else:
173
+ epic = find_epic(data, parts[0])
174
+ story = find_story(epic, story_id) if epic else None
175
+ if story:
176
+ story["status"] = "done"
177
+ story["completed"] = today
178
+ if "assigned_to" in story:
179
+ del story["assigned_to"]
180
+ write_sprint(sprint_path, data)
181
+ steps.append({"step": 4, "action": "yaml_update", "status": "done", "completed": today})
182
+ else:
183
+ steps.append({"step": 4, "action": "yaml_update", "warning": f"Story {story_id} not found in YAML"})
184
+ except Exception as exc:
185
+ steps.append({"step": 4, "action": "yaml_update", "error": str(exc)})
186
+
187
+ # --- Step 5: Archive completed epics ---
188
+ result = _run(
189
+ ["python", "-m", "pennyfarthing_scripts.cli", "sprint", "epic", "archive"],
190
+ cwd=str(project_root),
191
+ )
192
+ steps.append({"step": 5, "action": "archive_epics", "ran": True})
193
+
194
+ # --- Step 6: Git cleanup ---
195
+ _run(["git", "checkout", "develop"], cwd=str(project_root))
196
+ _run(["git", "pull", "origin", "develop"], cwd=str(project_root))
197
+ if branch:
198
+ _run(["git", "branch", "-d", branch], cwd=str(project_root))
199
+ steps.append({"step": 6, "action": "git_cleanup", "branch": branch})
200
+
201
+ # --- Step 7: Remove session file ---
202
+ if session_path.exists():
203
+ session_path.unlink()
204
+ steps.append({"step": 7, "action": "remove_session"})
205
+
206
+ return {
207
+ "success": True,
208
+ "story_id": story_id,
209
+ "jira_key": jira_key,
210
+ "steps": steps,
211
+ }
@@ -35,6 +35,20 @@ def check_story(story_id: str) -> dict[str, Any]:
35
35
  status = story.get("status", "backlog")
36
36
  assigned = story.get("assigned_to")
37
37
 
38
+ # Check if assigned to someone else
39
+ if assigned:
40
+ from pennyfarthing_scripts.jira.client import get_current_user_email
41
+
42
+ current_user = get_current_user_email()
43
+ if assigned != current_user:
44
+ return {
45
+ "available": False,
46
+ "type": "story",
47
+ "story": story,
48
+ "reason": f"Assigned to {assigned}",
49
+ "assigned_to": assigned,
50
+ }
51
+
38
52
  # Check if already in progress
39
53
  if status == "in_progress":
40
54
  return {
@@ -78,15 +92,22 @@ def get_next_story() -> dict[str, Any]:
78
92
  """Get the highest priority available story.
79
93
 
80
94
  Considers stories with backlog, ready, or planning status.
95
+ Excludes stories assigned to other users.
81
96
 
82
97
  Returns:
83
98
  Dict with next story details or error
84
99
  """
100
+ from pennyfarthing_scripts.jira.client import get_current_user_email
85
101
  from pennyfarthing_scripts.sprint.loader import get_all_stories
86
102
 
103
+ current_user = get_current_user_email()
87
104
  all_stories = get_all_stories()
88
105
  available_statuses = {"backlog", "ready", "planning"}
89
- backlog = [s for s in all_stories if s.get("status") in available_statuses]
106
+ backlog = [
107
+ s for s in all_stories
108
+ if s.get("status") in available_statuses
109
+ and (not s.get("assigned_to") or s.get("assigned_to") == current_user)
110
+ ]
90
111
 
91
112
  if not backlog:
92
113
  return {
@@ -94,11 +115,14 @@ def get_next_story() -> dict[str, Any]:
94
115
  "error": "No stories in backlog",
95
116
  }
96
117
 
97
- # Sort by priority (P0 > P1 > P2 > P3)
118
+ # Sort by priority (P0 > P1 > P2 > P3), preferring own assignments
98
119
  priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
99
120
  sorted_stories = sorted(
100
121
  backlog,
101
- key=lambda s: priority_order.get(s.get("priority", "P2"), 2),
122
+ key=lambda s: (
123
+ 0 if s.get("assigned_to") == current_user else 1,
124
+ priority_order.get(s.get("priority", "P2"), 2),
125
+ ),
102
126
  )
103
127
 
104
128
  next_story = sorted_stories[0]