@pennyfarthing/core 10.0.5 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (580) hide show
  1. package/README.md +19 -22
  2. package/package.json +17 -11
  3. package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js +24 -0
  5. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +346 -13
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js +1 -1
  10. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js.map +1 -1
  11. package/packages/core/dist/cli/commands/e2e-upgrade.test.js +1 -1
  12. package/packages/core/dist/cli/commands/e2e-upgrade.test.js.map +1 -1
  13. package/packages/core/dist/cli/commands/hooks-consolidation.test.js +2 -2
  14. package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -1
  15. package/packages/core/dist/cli/commands/init-consolidation.test.js.map +1 -1
  16. package/packages/core/dist/cli/commands/init.d.ts +7 -0
  17. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  18. package/packages/core/dist/cli/commands/init.js +41 -8
  19. package/packages/core/dist/cli/commands/init.js.map +1 -1
  20. package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
  21. package/packages/core/dist/cli/commands/uninstall.js +24 -13
  22. package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
  23. package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
  24. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  25. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  26. package/packages/core/dist/cli/commands/update.js +26 -0
  27. package/packages/core/dist/cli/commands/update.js.map +1 -1
  28. package/packages/core/dist/cli/index.js +1 -1
  29. package/packages/core/dist/cli/index.js.map +1 -1
  30. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  31. package/packages/core/dist/cli/theme-maker.test.js +64 -115
  32. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  33. package/packages/core/dist/cli/utils/python.d.ts +22 -0
  34. package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
  35. package/packages/core/dist/cli/utils/python.js +102 -0
  36. package/packages/core/dist/cli/utils/python.js.map +1 -0
  37. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  38. package/packages/core/dist/cli/utils/settings.js +10 -0
  39. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  40. package/packages/core/dist/index.d.ts +1 -1
  41. package/packages/core/dist/index.d.ts.map +1 -1
  42. package/packages/core/dist/index.js +2 -2
  43. package/packages/core/dist/index.js.map +1 -1
  44. package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
  45. package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
  46. package/packages/core/dist/plugins/plugin-discovery.js +165 -0
  47. package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
  48. package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
  49. package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
  50. package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
  51. package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
  52. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  53. package/packages/core/dist/workflow/context-watch.d.ts +80 -0
  54. package/packages/core/dist/workflow/context-watch.d.ts.map +1 -0
  55. package/packages/core/dist/workflow/context-watch.js +235 -0
  56. package/packages/core/dist/workflow/context-watch.js.map +1 -0
  57. package/packages/core/dist/workflow/context-watch.test.d.ts +1 -0
  58. package/packages/core/dist/workflow/context-watch.test.d.ts.map +1 -0
  59. package/packages/core/dist/workflow/context-watch.test.js +746 -0
  60. package/packages/core/dist/workflow/context-watch.test.js.map +1 -0
  61. package/packages/core/dist/workflow/file-watch.d.ts +82 -0
  62. package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
  63. package/packages/core/dist/workflow/file-watch.js +198 -0
  64. package/packages/core/dist/workflow/file-watch.js.map +1 -0
  65. package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
  66. package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
  67. package/packages/core/dist/workflow/file-watch.test.js +469 -0
  68. package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
  69. package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
  70. package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
  71. package/packages/core/dist/workflow/observation-writer.js +97 -0
  72. package/packages/core/dist/workflow/observation-writer.js.map +1 -0
  73. package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
  74. package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
  75. package/packages/core/dist/workflow/observation-writer.test.js +424 -0
  76. package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
  77. package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
  78. package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
  79. package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
  80. package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
  81. package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
  82. package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
  83. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
  84. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
  85. package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
  86. package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
  87. package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
  88. package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
  89. package/packages/core/dist/workflow/tool-watch.js +166 -0
  90. package/packages/core/dist/workflow/tool-watch.js.map +1 -0
  91. package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
  92. package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
  93. package/packages/core/dist/workflow/tool-watch.test.js +718 -0
  94. package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
  95. package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
  96. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  97. package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
  98. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  99. package/packages/core/dist/workflow/workflow-schema.js +44 -0
  100. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  101. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  102. package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
  103. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  104. package/pennyfarthing-dist/agents/README.md +1 -3
  105. package/pennyfarthing-dist/agents/architect.md +0 -6
  106. package/pennyfarthing-dist/agents/devops.md +0 -6
  107. package/pennyfarthing-dist/agents/handoff.md +18 -3
  108. package/pennyfarthing-dist/agents/orchestrator.md +0 -6
  109. package/pennyfarthing-dist/agents/pm.md +0 -6
  110. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  111. package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
  112. package/pennyfarthing-dist/agents/sm.md +11 -11
  113. package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
  114. package/pennyfarthing-dist/commands/architect.md +11 -3
  115. package/pennyfarthing-dist/commands/close-epic.md +24 -131
  116. package/pennyfarthing-dist/commands/create-theme.md +14 -24
  117. package/pennyfarthing-dist/commands/dev.md +11 -3
  118. package/pennyfarthing-dist/commands/devops.md +11 -3
  119. package/pennyfarthing-dist/commands/health-check.md +1 -3
  120. package/pennyfarthing-dist/commands/help.md +8 -12
  121. package/pennyfarthing-dist/commands/list-themes.md +14 -16
  122. package/pennyfarthing-dist/commands/orchestrator.md +11 -3
  123. package/pennyfarthing-dist/commands/parallel-work.md +1 -3
  124. package/pennyfarthing-dist/commands/pm.md +11 -3
  125. package/pennyfarthing-dist/commands/prime.md +6 -6
  126. package/pennyfarthing-dist/commands/reviewer.md +11 -3
  127. package/pennyfarthing-dist/commands/run-ci.md +1 -1
  128. package/pennyfarthing-dist/commands/set-theme.md +14 -51
  129. package/pennyfarthing-dist/commands/setup.md +5 -1
  130. package/pennyfarthing-dist/commands/show-theme.md +14 -16
  131. package/pennyfarthing-dist/commands/sm.md +11 -3
  132. package/pennyfarthing-dist/commands/tea.md +11 -3
  133. package/pennyfarthing-dist/commands/tech-writer.md +11 -3
  134. package/pennyfarthing-dist/commands/theme-maker.md +14 -671
  135. package/pennyfarthing-dist/commands/theme.md +95 -0
  136. package/pennyfarthing-dist/commands/ux-designer.md +11 -3
  137. package/pennyfarthing-dist/commands/work.md +3 -5
  138. package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
  139. package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
  140. package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
  141. package/pennyfarthing-dist/guides/bikelane.md +3 -2
  142. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
  143. package/pennyfarthing-dist/guides/hooks.md +5 -5
  144. package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
  145. package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
  146. package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
  147. package/pennyfarthing-dist/guides/prime.md +2 -2
  148. package/pennyfarthing-dist/guides/scale-levels.md +4 -6
  149. package/pennyfarthing-dist/guides/skill-schema.md +4 -4
  150. package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
  151. package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
  152. package/pennyfarthing-dist/personas/themes/fifth-element.yaml +295 -0
  153. package/pennyfarthing-dist/scripts/README.md +1 -1
  154. package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
  155. package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
  156. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -0
  157. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
  158. package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
  159. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  160. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
  161. package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
  162. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
  163. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  164. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
  165. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  166. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
  167. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  168. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  169. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  170. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +0 -0
  171. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +32 -15
  172. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
  173. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  174. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
  175. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  176. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  177. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  178. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  179. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  180. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  181. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
  182. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  183. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  184. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  185. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  186. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  187. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  188. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  189. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  190. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  191. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  192. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  193. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  194. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  195. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  196. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  197. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  198. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  199. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  200. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  201. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  202. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  203. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  204. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  205. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
  206. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  207. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  208. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  209. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  210. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  211. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  212. package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
  213. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  214. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
  215. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
  216. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  217. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  218. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  219. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  220. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  221. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  222. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
  223. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  224. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  225. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  226. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  227. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  228. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
  229. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  230. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  231. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  232. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  233. package/pennyfarthing-dist/scripts/theme/README.md +1 -1
  234. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  235. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
  236. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  237. package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
  238. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  239. package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
  240. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -144
  241. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
  242. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  243. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
  244. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
  245. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
  246. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
  247. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
  248. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
  249. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
  250. package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
  251. package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
  252. package/pennyfarthing-dist/skills/skill-registry.yaml +21 -17
  253. package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
  254. package/pennyfarthing-dist/skills/story/scripts/create-story.sh +0 -0
  255. package/pennyfarthing-dist/skills/story/scripts/size-story.sh +0 -0
  256. package/pennyfarthing-dist/skills/story/scripts/story-template.sh +0 -0
  257. package/pennyfarthing-dist/skills/theme/skill.md +290 -75
  258. package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
  259. package/pennyfarthing-dist/skills/workflow/scripts/list-workflows.sh +0 -0
  260. package/pennyfarthing-dist/skills/workflow/scripts/resume-workflow.sh +0 -0
  261. package/pennyfarthing-dist/skills/workflow/scripts/show-workflow.sh +0 -0
  262. package/pennyfarthing-dist/skills/workflow/scripts/start-workflow.sh +0 -0
  263. package/pennyfarthing-dist/skills/workflow/scripts/workflow-status.sh +0 -0
  264. package/pennyfarthing-dist/skills/workflow/skill.md +27 -4
  265. package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
  266. package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
  267. package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
  268. package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
  269. package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
  270. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
  271. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +41 -24
  272. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
  273. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  283. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/bellmode_hook.py +202 -47
  287. package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
  288. package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
  289. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  291. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  292. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  293. package/pennyfarthing_scripts/brownfield/cli.py +0 -1
  294. package/pennyfarthing_scripts/brownfield/discover.py +1 -2
  295. package/pennyfarthing_scripts/cli.py +23 -3
  296. package/pennyfarthing_scripts/codemarkers/__init__.py +23 -0
  297. package/pennyfarthing_scripts/codemarkers/__main__.py +6 -0
  298. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  304. package/pennyfarthing_scripts/codemarkers/analyze.py +501 -0
  305. package/pennyfarthing_scripts/codemarkers/cli.py +179 -0
  306. package/pennyfarthing_scripts/codemarkers/formatters.py +88 -0
  307. package/pennyfarthing_scripts/codemarkers/models.py +60 -0
  308. package/pennyfarthing_scripts/common/__init__.py +8 -9
  309. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/common/config.py +1 -1
  314. package/pennyfarthing_scripts/complexity/__init__.py +15 -0
  315. package/pennyfarthing_scripts/complexity/__main__.py +6 -0
  316. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  317. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  318. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  319. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  320. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/complexity/analyze.py +207 -0
  323. package/pennyfarthing_scripts/complexity/cli.py +82 -0
  324. package/pennyfarthing_scripts/complexity/formatters.py +64 -0
  325. package/pennyfarthing_scripts/complexity/models.py +32 -0
  326. package/pennyfarthing_scripts/context.py +14 -15
  327. package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
  328. package/pennyfarthing_scripts/deadcode/__main__.py +6 -0
  329. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  330. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  331. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  332. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  333. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  334. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  335. package/pennyfarthing_scripts/deadcode/analyze.py +322 -0
  336. package/pennyfarthing_scripts/deadcode/cli.py +163 -0
  337. package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
  338. package/pennyfarthing_scripts/deadcode/models.py +54 -0
  339. package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
  340. package/pennyfarthing_scripts/dependencies/__main__.py +5 -0
  341. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  344. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  345. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  346. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  347. package/pennyfarthing_scripts/dependencies/analyze.py +155 -0
  348. package/pennyfarthing_scripts/dependencies/cli.py +76 -0
  349. package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
  350. package/pennyfarthing_scripts/dependencies/models.py +39 -0
  351. package/pennyfarthing_scripts/git/__init__.py +5 -5
  352. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  353. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  354. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  355. package/pennyfarthing_scripts/git/create_branches.py +3 -2
  356. package/pennyfarthing_scripts/git/status_all.py +1 -1
  357. package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
  358. package/pennyfarthing_scripts/healthscore/__main__.py +14 -0
  359. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  360. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  361. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  362. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  363. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  364. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  365. package/pennyfarthing_scripts/healthscore/analyze.py +591 -0
  366. package/pennyfarthing_scripts/healthscore/cli.py +80 -0
  367. package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
  368. package/pennyfarthing_scripts/healthscore/models.py +43 -0
  369. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  370. package/pennyfarthing_scripts/hooks.py +8 -11
  371. package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
  372. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  373. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  374. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  375. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  376. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  377. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  378. package/pennyfarthing_scripts/hotspots/analyze.py +155 -14
  379. package/pennyfarthing_scripts/hotspots/cli.py +12 -10
  380. package/pennyfarthing_scripts/hotspots/models.py +0 -1
  381. package/pennyfarthing_scripts/jira/__init__.py +15 -17
  382. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  383. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  384. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  385. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  386. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  387. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  388. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  389. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  390. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  391. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  392. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  393. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  394. package/pennyfarthing_scripts/jira/bidirectional.py +44 -18
  395. package/pennyfarthing_scripts/jira/claim.py +21 -0
  396. package/pennyfarthing_scripts/jira/cli.py +6 -3
  397. package/pennyfarthing_scripts/jira/client.py +32 -4
  398. package/pennyfarthing_scripts/jira/create.py +45 -1
  399. package/pennyfarthing_scripts/jira/epic.py +3 -2
  400. package/pennyfarthing_scripts/jira/reconcile.py +0 -1
  401. package/pennyfarthing_scripts/jira/story.py +2 -0
  402. package/pennyfarthing_scripts/jira/sync.py +1 -1
  403. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  404. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  405. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  406. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  407. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  408. package/pennyfarthing_scripts/migration/skill.py +0 -1
  409. package/pennyfarthing_scripts/migration/step.py +0 -1
  410. package/pennyfarthing_scripts/migration/validate.py +8 -5
  411. package/pennyfarthing_scripts/patch_mode.py +2 -2
  412. package/pennyfarthing_scripts/preflight/__init__.py +1 -1
  413. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  414. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  415. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  416. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  417. package/pennyfarthing_scripts/preflight/finish.py +0 -1
  418. package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
  419. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  420. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  421. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  422. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  423. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  424. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  425. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  426. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  427. package/pennyfarthing_scripts/prime/cli.py +5 -1
  428. package/pennyfarthing_scripts/prime/loader.py +2 -3
  429. package/pennyfarthing_scripts/prime/persona.py +2 -1
  430. package/pennyfarthing_scripts/prime/tiers.py +4 -4
  431. package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
  432. package/pennyfarthing_scripts/sprint/__init__.py +10 -12
  433. package/pennyfarthing_scripts/sprint/__main__.py +2 -2
  434. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  435. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  436. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  437. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  438. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  439. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  440. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  441. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  442. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  443. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  444. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  445. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  446. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  447. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  448. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  449. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  450. package/pennyfarthing_scripts/sprint/archive.py +0 -1
  451. package/pennyfarthing_scripts/sprint/archive_epic.py +198 -97
  452. package/pennyfarthing_scripts/sprint/cli.py +62 -46
  453. package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
  454. package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
  455. package/pennyfarthing_scripts/sprint/loader.py +6 -0
  456. package/pennyfarthing_scripts/sprint/status.py +1 -2
  457. package/pennyfarthing_scripts/sprint/story_add.py +202 -27
  458. package/pennyfarthing_scripts/sprint/story_finish.py +209 -0
  459. package/pennyfarthing_scripts/sprint/story_update.py +11 -3
  460. package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
  461. package/pennyfarthing_scripts/sprint/validator.py +120 -6
  462. package/pennyfarthing_scripts/sprint/work.py +28 -7
  463. package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
  464. package/pennyfarthing_scripts/story/__init__.py +14 -16
  465. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  466. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  467. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  468. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  469. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  470. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  471. package/pennyfarthing_scripts/story/size.py +0 -1
  472. package/pennyfarthing_scripts/story/template.py +0 -1
  473. package/pennyfarthing_scripts/swebench.py +1 -2
  474. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  475. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  476. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  477. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  478. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  479. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  480. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  481. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  482. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  483. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  484. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  485. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  486. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  487. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  488. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  489. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  490. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  491. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  492. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  493. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  494. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  495. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  496. package/pennyfarthing_scripts/tests/conftest.py +1 -2
  497. package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
  498. package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
  499. package/pennyfarthing_scripts/tests/test_codemarkers.py +687 -0
  500. package/pennyfarthing_scripts/tests/test_common.py +9 -4
  501. package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
  502. package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
  503. package/pennyfarthing_scripts/tests/test_healthscore.py +516 -0
  504. package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
  505. package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
  506. package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
  507. package/pennyfarthing_scripts/tests/test_prime.py +39 -21
  508. package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
  509. package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
  510. package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
  511. package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
  512. package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
  513. package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
  514. package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
  515. package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
  516. package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
  517. package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
  518. package/pennyfarthing_scripts/theme/__init__.py +5 -0
  519. package/pennyfarthing_scripts/theme/__main__.py +6 -0
  520. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  521. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  522. package/pennyfarthing_scripts/theme/cli.py +287 -0
  523. package/pennyfarthing_scripts/validate/__init__.py +21 -0
  524. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  525. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  526. package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
  527. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  528. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  529. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  530. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  531. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  532. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  533. package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
  534. package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
  535. package/pennyfarthing_scripts/validate/adapters/skill_command.py +292 -0
  536. package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
  537. package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
  538. package/pennyfarthing_scripts/validate/cli.py +141 -0
  539. package/pennyfarthing_scripts/welcome_hook.py +2 -3
  540. package/pennyfarthing_scripts/workflow.py +3 -3
  541. package/scripts/README.md +41 -0
  542. package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
  543. package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
  544. package/pennyfarthing-dist/commands/benchmark.md +0 -485
  545. package/pennyfarthing-dist/commands/job-fair.md +0 -102
  546. package/pennyfarthing-dist/commands/solo.md +0 -447
  547. package/pennyfarthing-dist/guides/benchmarks.md +0 -62
  548. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  549. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
  550. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
  551. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
  552. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
  553. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
  554. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
  555. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
  556. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
  557. package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
  558. package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
  559. package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
  560. package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
  561. package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
  562. package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
  563. package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
  564. package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
  565. package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
  566. package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
  567. package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
  568. package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
  569. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  570. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  571. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  572. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  573. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  574. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  575. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  576. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  577. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  578. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  579. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  580. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -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 as err:
316
+ raise click.ClickException(f"POINTS must be an integer, got '{title}'") from err
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,209 @@
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)
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
+ import sys
20
+ from datetime import date
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from pennyfarthing_scripts.sprint.loader import find_epic, find_story
25
+ from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
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
+ write_sprint(sprint_path, data)
179
+ steps.append({"step": 4, "action": "yaml_update", "status": "done", "completed": today})
180
+ else:
181
+ steps.append({"step": 4, "action": "yaml_update", "warning": f"Story {story_id} not found in YAML"})
182
+ except Exception as exc:
183
+ steps.append({"step": 4, "action": "yaml_update", "error": str(exc)})
184
+
185
+ # --- Step 5: Archive completed epics ---
186
+ result = _run(
187
+ [sys.executable, "-m", "pennyfarthing_scripts.cli", "sprint", "epic", "archive"],
188
+ cwd=str(project_root),
189
+ )
190
+ steps.append({"step": 5, "action": "archive_epics", "ran": True})
191
+
192
+ # --- Step 6: Git cleanup ---
193
+ _run(["git", "checkout", "develop"], cwd=str(project_root))
194
+ _run(["git", "pull", "origin", "develop"], cwd=str(project_root))
195
+ if branch:
196
+ _run(["git", "branch", "-d", branch], cwd=str(project_root))
197
+ steps.append({"step": 6, "action": "git_cleanup", "branch": branch})
198
+
199
+ # --- Step 7: Remove session file ---
200
+ if session_path.exists():
201
+ session_path.unlink()
202
+ steps.append({"step": 7, "action": "remove_session"})
203
+
204
+ return {
205
+ "success": True,
206
+ "story_id": story_id,
207
+ "jira_key": jira_key,
208
+ "steps": steps,
209
+ }
@@ -7,6 +7,7 @@ This module provides:
7
7
  - story_update_command (Click command for CLI registration)
8
8
  """
9
9
 
10
+ import subprocess
10
11
  from datetime import date
11
12
  from pathlib import Path
12
13
  from typing import Any
@@ -95,9 +96,6 @@ def update_story(
95
96
 
96
97
  # Auto-cleanup rules
97
98
  if status == "done":
98
- # Remove assigned_to
99
- if "assigned_to" in story:
100
- del story["assigned_to"]
101
99
  # Auto-set completed if not explicitly provided
102
100
  if completed_date is None and "completed" not in story:
103
101
  story["completed"] = date.today().isoformat()
@@ -108,6 +106,16 @@ def update_story(
108
106
  # Auto-set started if not already present
109
107
  if "started" not in story:
110
108
  story["started"] = date.today().isoformat()
109
+ # Auto-set assignee from current Jira user if not already assigned
110
+ if "assigned_to" not in story and assigned_to is None:
111
+ try:
112
+ result = subprocess.run(
113
+ ["jira", "me"], capture_output=True, text=True
114
+ )
115
+ if result.returncode == 0 and result.stdout.strip():
116
+ story["assigned_to"] = result.stdout.strip()
117
+ except Exception:
118
+ pass
111
119
 
112
120
  # Validate after mutation
113
121
  result = validate_full_sprint(data)
@@ -28,7 +28,6 @@ from pennyfarthing_scripts.sprint.yaml_io import (
28
28
  SPRINT_KEY_ORDER,
29
29
  STORY_KEY_ORDER,
30
30
  TOP_KEY_ORDER,
31
- canonical_dump,
32
31
  read_sprint,
33
32
  write_sprint,
34
33
  )
@@ -78,6 +78,10 @@ REQUIRED_STORY_FIELDS = {"id", "title", "status", "points"}
78
78
  # Required fields for epic
79
79
  REQUIRED_EPIC_FIELDS = {"id", "title"}
80
80
 
81
+ # Required fields for epic shard files (write-time validation, ADR-0022)
82
+ REQUIRED_EPIC_SHARD_FIELDS = {"id", "title", "status", "stories"}
83
+ REQUIRED_SHARD_STORY_FIELDS = {"id", "title", "points", "status"}
84
+
81
85
  # Required fields for future.yaml initiative
82
86
  REQUIRED_INITIATIVE_FIELDS = {"name", "status"}
83
87
 
@@ -235,6 +239,15 @@ def validate_epic(epic: dict[str, Any], all_story_ids: set[str], epic_index: int
235
239
  f"{base_path}.{field_name}",
236
240
  )
237
241
 
242
+ # Reject non-string IDs (YAML parses bare integers like `id: 87` as int,
243
+ # which crashes Cyclist's sprint-data.ts — epicId.match() fails on non-strings)
244
+ if "id" in epic and not isinstance(epic["id"], str):
245
+ result.add_error(
246
+ f"Epic ID must be a string, got {type(epic['id']).__name__} ({epic['id']!r}). "
247
+ "Quote it in YAML (e.g., id: \"87\" not id: 87)",
248
+ f"{base_path}.id",
249
+ )
250
+
238
251
  # Validate stories if present
239
252
  if "stories" in epic:
240
253
  seen_in_epic: set[str] = set()
@@ -262,6 +275,95 @@ def validate_epic(epic: dict[str, Any], all_story_ids: set[str], epic_index: int
262
275
  return result
263
276
 
264
277
 
278
+ def validate_epic_shard(epic: dict[str, Any]) -> ValidationResult:
279
+ """Validate an epic shard dict before writing to disk.
280
+
281
+ Enforces stricter requirements than validate_epic() since shards
282
+ are standalone files that must be self-contained.
283
+
284
+ Validates:
285
+ - Required fields present (id, title, status, stories)
286
+ - stories is a list
287
+ - Each story has required fields (id, title, points, status)
288
+ - No duplicate story IDs within the epic
289
+ - jira key follows MSSCI-NNNNN pattern if present
290
+
291
+ Args:
292
+ epic: Epic shard dict to validate
293
+
294
+ Returns:
295
+ ValidationResult with any errors found
296
+ """
297
+ result = ValidationResult(valid=True)
298
+
299
+ # Check required shard fields
300
+ for field_name in REQUIRED_EPIC_SHARD_FIELDS:
301
+ if field_name not in epic:
302
+ result.add_error(
303
+ f"Missing required field: {field_name}",
304
+ f"epic.{field_name}",
305
+ )
306
+
307
+ # Reject non-string IDs (YAML parses bare integers like `id: 87` as int,
308
+ # which crashes Cyclist's sprint-data.ts checkEpicContext — epicId.match() fails)
309
+ if "id" in epic and not isinstance(epic["id"], str):
310
+ result.add_error(
311
+ f"Epic ID must be a string, got {type(epic['id']).__name__} ({epic['id']!r}). "
312
+ "Quote it in YAML (e.g., id: \"87\" not id: 87)",
313
+ "epic.id",
314
+ )
315
+
316
+ # Reject epic- prefix in ID (ADR-0022: reference prefix should not be baked into value)
317
+ if "id" in epic:
318
+ epic_id_val = str(epic["id"])
319
+ if epic_id_val.startswith("epic-"):
320
+ result.add_error(
321
+ f"Epic ID '{epic_id_val}' starts with 'epic-' prefix. "
322
+ "Use the numeric ID (e.g., '94' not 'epic-94')",
323
+ "epic.id",
324
+ )
325
+
326
+ # Validate jira key format if present
327
+ if "jira" in epic:
328
+ jira_key = str(epic["jira"])
329
+ if not JIRA_KEY_PATTERN.match(jira_key):
330
+ result.add_error(
331
+ f"Invalid Jira key format '{jira_key}'. Expected MSSCI-NNNNN",
332
+ "epic.jira",
333
+ )
334
+
335
+ # Validate stories field
336
+ if "stories" in epic:
337
+ stories = epic["stories"]
338
+ if not isinstance(stories, list):
339
+ result.add_error(
340
+ "'stories' must be a list",
341
+ "epic.stories",
342
+ )
343
+ else:
344
+ seen_ids: set[str] = set()
345
+ epic_id = epic.get("id", "epic")
346
+ for idx, story in enumerate(stories):
347
+ story_id = story.get("id")
348
+ if story_id:
349
+ if story_id in seen_ids:
350
+ result.add_error(
351
+ f"Duplicate story ID '{story_id}' within epic",
352
+ f"epic.stories[{idx}].id",
353
+ )
354
+ seen_ids.add(story_id)
355
+
356
+ # Check required story fields
357
+ for field_name in REQUIRED_SHARD_STORY_FIELDS:
358
+ if field_name not in story:
359
+ result.add_error(
360
+ f"Missing required field: {field_name}",
361
+ f"{epic_id}.stories[{idx}].{field_name}",
362
+ )
363
+
364
+ return result
365
+
366
+
265
367
  def validate_full_sprint(data: dict[str, Any]) -> ValidationResult:
266
368
  """Validate complete sprint YAML including all epics and stories.
267
369
 
@@ -356,7 +458,7 @@ def validate_future(data: dict[str, Any]) -> ValidationResult:
356
458
  if isinstance(initiative, str):
357
459
  continue
358
460
  if not isinstance(initiative, dict):
359
- result.add_error(f"Initiative must be a mapping", f"future.initiatives[{i}]")
461
+ result.add_error("Initiative must be a mapping", f"future.initiatives[{i}]")
360
462
  continue
361
463
 
362
464
  base_path = f"future.initiatives[{i}]"
@@ -433,13 +535,15 @@ def validate_future(data: dict[str, Any]) -> ValidationResult:
433
535
  return result
434
536
 
435
537
 
436
- def validate_sprint_file(file_path: Path) -> ValidationResult:
538
+ def validate_sprint_file(file_path: Path, *, strict: bool = False) -> ValidationResult:
437
539
  """Validate a sprint YAML file from disk.
438
540
 
439
- Loads the file and validates its contents.
541
+ Loads the file and validates its contents. In strict mode, loader
542
+ warnings (e.g., unresolvable shard refs) are promoted to errors.
440
543
 
441
544
  Args:
442
545
  file_path: Path to sprint YAML file
546
+ strict: If True, treat loader warnings as validation errors
443
547
 
444
548
  Returns:
445
549
  ValidationResult with any errors (including load errors)
@@ -509,12 +613,22 @@ def validate_sprint_file(file_path: Path) -> ValidationResult:
509
613
  )
510
614
  return result
511
615
 
512
- # Merge sharded epic files if present
616
+ # Merge sharded epic files if present, capturing warnings in strict mode
513
617
  from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
514
- data = _merge_epic_shards(data, file_path.parent)
618
+ if strict:
619
+ import warnings as _warnings
620
+ with _warnings.catch_warnings(record=True) as caught:
621
+ _warnings.simplefilter("always")
622
+ data = _merge_epic_shards(data, file_path.parent)
623
+ for w in caught:
624
+ result.add_error(str(w.message), str(file_path))
625
+ else:
626
+ data = _merge_epic_shards(data, file_path.parent)
515
627
 
516
628
  # Validate loaded data
517
- return validate_full_sprint(data)
629
+ full_result = validate_full_sprint(data)
630
+ result.merge(full_result)
631
+ return result
518
632
 
519
633
 
520
634
  def format_validation_errors(result: ValidationResult) -> str: