@pennyfarthing/core 10.1.0 → 10.3.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 (422) hide show
  1. package/README.md +22 -24
  2. package/package.json +3 -1
  3. package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js +24 -0
  5. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +101 -15
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js +2 -2
  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 +2 -2
  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/theme.js +1 -1
  17. package/packages/core/dist/cli/commands/theme.js.map +1 -1
  18. package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
  19. package/packages/core/dist/cli/commands/uninstall.js +24 -13
  20. package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
  21. package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
  22. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  23. package/packages/core/dist/cli/commands/update.js.map +1 -1
  24. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  25. package/packages/core/dist/cli/theme-maker.test.js +64 -115
  26. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  27. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
  28. package/packages/core/dist/cli/utils/themes.js +3 -2
  29. package/packages/core/dist/cli/utils/themes.js.map +1 -1
  30. package/packages/core/dist/index.d.ts +1 -1
  31. package/packages/core/dist/index.d.ts.map +1 -1
  32. package/packages/core/dist/index.js +2 -2
  33. package/packages/core/dist/index.js.map +1 -1
  34. package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
  35. package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
  36. package/packages/core/dist/plugins/plugin-discovery.js +165 -0
  37. package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
  38. package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
  39. package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
  40. package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
  41. package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
  42. package/packages/core/dist/scripts/add-ocean-profiles.js +1 -1
  43. package/packages/core/dist/scripts/add-ocean-profiles.js.map +1 -1
  44. package/packages/core/dist/scripts/generate-all-spiders.js +2 -0
  45. package/packages/core/dist/scripts/generate-all-spiders.js.map +1 -1
  46. package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
  47. package/packages/core/dist/scripts/generate-report.js +2 -0
  48. package/packages/core/dist/scripts/generate-report.js.map +1 -1
  49. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  50. package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
  51. package/packages/core/dist/scripts/generate-spider.js +2 -0
  52. package/packages/core/dist/scripts/generate-spider.js.map +1 -1
  53. package/packages/core/dist/scripts/validate-ocean-profiles.js +1 -1
  54. package/packages/core/dist/scripts/validate-ocean-profiles.js.map +1 -1
  55. package/packages/core/dist/workflow/file-watch.d.ts +82 -0
  56. package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
  57. package/packages/core/dist/workflow/file-watch.js +198 -0
  58. package/packages/core/dist/workflow/file-watch.js.map +1 -0
  59. package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
  60. package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
  61. package/packages/core/dist/workflow/file-watch.test.js +469 -0
  62. package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
  63. package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
  64. package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
  65. package/packages/core/dist/workflow/observation-writer.js +97 -0
  66. package/packages/core/dist/workflow/observation-writer.js.map +1 -0
  67. package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
  68. package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
  69. package/packages/core/dist/workflow/observation-writer.test.js +424 -0
  70. package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
  71. package/packages/core/dist/workflow/output-path-normalizer.d.ts +47 -0
  72. package/packages/core/dist/workflow/output-path-normalizer.d.ts.map +1 -0
  73. package/packages/core/dist/workflow/output-path-normalizer.js +79 -0
  74. package/packages/core/dist/workflow/output-path-normalizer.js.map +1 -0
  75. package/packages/core/dist/workflow/output-path-normalizer.test.d.ts +16 -0
  76. package/packages/core/dist/workflow/output-path-normalizer.test.d.ts.map +1 -0
  77. package/packages/core/dist/workflow/output-path-normalizer.test.js +157 -0
  78. package/packages/core/dist/workflow/output-path-normalizer.test.js.map +1 -0
  79. package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
  80. package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
  81. package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
  82. package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
  83. package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
  84. package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
  85. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
  86. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
  87. package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
  88. package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
  89. package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
  90. package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
  91. package/packages/core/dist/workflow/tool-watch.js +166 -0
  92. package/packages/core/dist/workflow/tool-watch.js.map +1 -0
  93. package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
  94. package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
  95. package/packages/core/dist/workflow/tool-watch.test.js +717 -0
  96. package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
  97. package/packages/core/dist/workflow/variable-resolver.js +1 -1
  98. package/packages/core/dist/workflow/variable-resolver.js.map +1 -1
  99. package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
  100. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  101. package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
  102. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  103. package/packages/core/dist/workflow/workflow-schema.js +44 -0
  104. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  105. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  106. package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
  107. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  108. package/pennyfarthing-dist/agents/README.md +3 -1
  109. package/pennyfarthing-dist/agents/ba.md +165 -0
  110. package/pennyfarthing-dist/agents/handoff.md +18 -3
  111. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  112. package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
  113. package/pennyfarthing-dist/agents/sm.md +11 -5
  114. package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
  115. package/pennyfarthing-dist/commands/ba.md +17 -0
  116. package/pennyfarthing-dist/commands/setup.md +4 -0
  117. package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
  118. package/pennyfarthing-dist/guides/bikelane.md +3 -2
  119. package/pennyfarthing-dist/guides/scale-levels.md +4 -6
  120. package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
  121. package/pennyfarthing-dist/guides/workflow-schema.md +1 -1
  122. package/pennyfarthing-dist/personas/themes/a-team.yaml +30 -0
  123. package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +30 -0
  124. package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +30 -0
  125. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +30 -0
  126. package/pennyfarthing-dist/personas/themes/catch-22.yaml +30 -0
  127. package/pennyfarthing-dist/personas/themes/control.yaml +30 -0
  128. package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +31 -0
  129. package/pennyfarthing-dist/personas/themes/discworld.yaml +32 -1
  130. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +31 -0
  131. package/pennyfarthing-dist/personas/themes/dune.yaml +32 -0
  132. package/pennyfarthing-dist/personas/themes/fifth-element.yaml +327 -0
  133. package/pennyfarthing-dist/personas/themes/firefly.yaml +31 -0
  134. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +30 -0
  135. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +30 -0
  136. package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +30 -0
  137. package/pennyfarthing-dist/personas/themes/lord-of-the-rings.yaml +30 -0
  138. package/pennyfarthing-dist/personas/themes/mad-max.yaml +30 -0
  139. package/pennyfarthing-dist/personas/themes/mash.yaml +33 -0
  140. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +34 -0
  141. package/pennyfarthing-dist/personas/themes/sandman.yaml +33 -0
  142. package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +34 -0
  143. package/pennyfarthing-dist/personas/themes/star-wars.yaml +33 -0
  144. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +30 -0
  145. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +30 -0
  146. package/pennyfarthing-dist/personas/themes/watchmen.yaml +30 -0
  147. package/pennyfarthing-dist/personas/themes/west-wing.yaml +30 -0
  148. package/pennyfarthing-dist/personas/themes/x-files.yaml +30 -0
  149. package/pennyfarthing-dist/scripts/README.md +1 -1
  150. package/pennyfarthing-dist/scripts/core/agent-session.sh +1 -1
  151. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
  152. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +20 -10
  153. package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
  154. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +2 -2
  155. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +1 -0
  156. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  157. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -189
  158. package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
  159. package/pennyfarthing-dist/skills/skill-registry.yaml +1 -1
  160. package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
  161. package/pennyfarthing-dist/skills/theme/skill.md +1 -1
  162. package/pennyfarthing-dist/skills/workflow/skill.md +24 -1
  163. package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
  164. package/pennyfarthing-dist/workflows/architecture.yaml +2 -2
  165. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
  166. package/pennyfarthing-dist/workflows/epics-and-stories/workflow.yaml +2 -2
  167. package/pennyfarthing-dist/workflows/implementation-readiness/workflow.yaml +2 -2
  168. package/pennyfarthing-dist/workflows/prd/workflow.yaml +2 -2
  169. package/pennyfarthing-dist/workflows/product-brief/workflow.yaml +2 -2
  170. package/pennyfarthing-dist/workflows/project-context/workflow.yaml +2 -2
  171. package/pennyfarthing-dist/workflows/quick-dev/workflow.yaml +2 -2
  172. package/pennyfarthing-dist/workflows/research/workflow.yaml +2 -2
  173. package/pennyfarthing-dist/workflows/retrospective/workflow.yaml +1 -1
  174. package/pennyfarthing-dist/workflows/sprint-planning/workflow.yaml +3 -3
  175. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
  176. package/pennyfarthing-dist/workflows/ux-design/workflow.yaml +2 -2
  177. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/bellmode_hook.py +202 -47
  183. package/pennyfarthing_scripts/bikerack/__init__.py +36 -0
  184. package/pennyfarthing_scripts/bikerack/__main__.py +5 -0
  185. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/bikerack/cli.py +148 -0
  190. package/pennyfarthing_scripts/bikerack/launcher.py +181 -0
  191. package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
  192. package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
  193. package/pennyfarthing_scripts/brownfield/cli.py +0 -1
  194. package/pennyfarthing_scripts/brownfield/discover.py +1 -2
  195. package/pennyfarthing_scripts/cli.py +16 -6
  196. package/pennyfarthing_scripts/codemarkers/__init__.py +5 -1
  197. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/codemarkers/analyze.py +177 -2
  204. package/pennyfarthing_scripts/codemarkers/cli.py +50 -0
  205. package/pennyfarthing_scripts/codemarkers/formatters.py +0 -1
  206. package/pennyfarthing_scripts/codemarkers/models.py +15 -0
  207. package/pennyfarthing_scripts/common/__init__.py +8 -9
  208. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/common/config.py +1 -1
  211. package/pennyfarthing_scripts/complexity/__init__.py +1 -1
  212. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/complexity/analyze.py +1 -1
  219. package/pennyfarthing_scripts/complexity/cli.py +5 -1
  220. package/pennyfarthing_scripts/complexity/formatters.py +1 -1
  221. package/pennyfarthing_scripts/context.py +14 -15
  222. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/deadcode/analyze.py +3 -4
  229. package/pennyfarthing_scripts/deadcode/cli.py +2 -2
  230. package/pennyfarthing_scripts/dependencies/__init__.py +2 -2
  231. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/dependencies/analyze.py +1 -1
  238. package/pennyfarthing_scripts/dependencies/cli.py +8 -4
  239. package/pennyfarthing_scripts/dependencies/formatters.py +1 -1
  240. package/pennyfarthing_scripts/git/__init__.py +5 -5
  241. package/pennyfarthing_scripts/git/create_branches.py +3 -2
  242. package/pennyfarthing_scripts/git/status_all.py +1 -1
  243. package/pennyfarthing_scripts/healthscore/__init__.py +2 -2
  244. package/pennyfarthing_scripts/healthscore/__main__.py +8 -0
  245. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/healthscore/analyze.py +452 -21
  252. package/pennyfarthing_scripts/healthscore/cli.py +5 -1
  253. package/pennyfarthing_scripts/healthscore/models.py +0 -1
  254. package/pennyfarthing_scripts/hooks.py +8 -11
  255. package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
  256. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/hotspots/analyze.py +128 -14
  261. package/pennyfarthing_scripts/hotspots/cli.py +2 -2
  262. package/pennyfarthing_scripts/hotspots/models.py +0 -1
  263. package/pennyfarthing_scripts/jira/__init__.py +15 -17
  264. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/jira/bidirectional.py +2 -3
  275. package/pennyfarthing_scripts/jira/claim.py +21 -0
  276. package/pennyfarthing_scripts/jira/cli.py +2 -2
  277. package/pennyfarthing_scripts/jira/client.py +4 -4
  278. package/pennyfarthing_scripts/jira/create.py +45 -1
  279. package/pennyfarthing_scripts/jira/epic.py +3 -2
  280. package/pennyfarthing_scripts/jira/reconcile.py +0 -1
  281. package/pennyfarthing_scripts/jira/story.py +2 -0
  282. package/pennyfarthing_scripts/jira/sync.py +1 -1
  283. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/migration/skill.py +0 -1
  289. package/pennyfarthing_scripts/migration/step.py +0 -1
  290. package/pennyfarthing_scripts/migration/validate.py +8 -5
  291. package/pennyfarthing_scripts/patch_mode.py +2 -2
  292. package/pennyfarthing_scripts/preflight/__init__.py +1 -1
  293. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  294. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  295. package/pennyfarthing_scripts/preflight/finish.py +0 -1
  296. package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
  297. package/pennyfarthing_scripts/prime/__init__.py +2 -0
  298. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/prime/cli.py +18 -1
  304. package/pennyfarthing_scripts/prime/loader.py +72 -3
  305. package/pennyfarthing_scripts/prime/persona.py +4 -2
  306. package/pennyfarthing_scripts/prime/tiers.py +17 -4
  307. package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
  308. package/pennyfarthing_scripts/sprint/__init__.py +10 -12
  309. package/pennyfarthing_scripts/sprint/__main__.py +2 -2
  310. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  315. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  316. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  317. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  318. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  319. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  320. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  323. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  324. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/sprint/archive.py +0 -1
  326. package/pennyfarthing_scripts/sprint/archive_epic.py +1 -4
  327. package/pennyfarthing_scripts/sprint/cli.py +34 -28
  328. package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
  329. package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
  330. package/pennyfarthing_scripts/sprint/loader.py +6 -0
  331. package/pennyfarthing_scripts/sprint/status.py +1 -2
  332. package/pennyfarthing_scripts/sprint/story_add.py +2 -2
  333. package/pennyfarthing_scripts/sprint/story_finish.py +3 -5
  334. package/pennyfarthing_scripts/sprint/story_update.py +11 -3
  335. package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
  336. package/pennyfarthing_scripts/sprint/validator.py +120 -6
  337. package/pennyfarthing_scripts/sprint/work.py +1 -4
  338. package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
  339. package/pennyfarthing_scripts/story/__init__.py +14 -16
  340. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/story/size.py +0 -1
  344. package/pennyfarthing_scripts/story/template.py +0 -1
  345. package/pennyfarthing_scripts/swebench.py +1 -2
  346. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  347. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  348. package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/conftest.py +1 -2
  353. package/pennyfarthing_scripts/tests/test_bikerack.py +785 -0
  354. package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
  355. package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
  356. package/pennyfarthing_scripts/tests/test_codemarkers.py +13 -8
  357. package/pennyfarthing_scripts/tests/test_common.py +9 -4
  358. package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
  359. package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
  360. package/pennyfarthing_scripts/tests/test_healthscore.py +17 -25
  361. package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
  362. package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
  363. package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
  364. package/pennyfarthing_scripts/tests/test_prime.py +39 -21
  365. package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
  366. package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
  367. package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
  368. package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
  369. package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
  370. package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
  371. package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
  372. package/pennyfarthing_scripts/tests/test_topology_loader.py +620 -0
  373. package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
  374. package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
  375. package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
  376. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  377. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  378. package/pennyfarthing_scripts/theme/cli.py +3 -2
  379. package/pennyfarthing_scripts/validate/__init__.py +21 -0
  380. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  381. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  382. package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
  383. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  384. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  385. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  386. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  387. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  388. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  389. package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
  390. package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
  391. package/pennyfarthing_scripts/validate/adapters/skill_command.py +291 -0
  392. package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
  393. package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
  394. package/pennyfarthing_scripts/validate/cli.py +141 -0
  395. package/pennyfarthing_scripts/welcome_hook.py +2 -3
  396. package/pennyfarthing_scripts/workflow.py +3 -3
  397. package/scripts/README.md +3 -15
  398. package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
  399. package/pennyfarthing-dist/commands/benchmark.md +0 -485
  400. package/pennyfarthing-dist/commands/job-fair.md +0 -102
  401. package/pennyfarthing-dist/commands/solo.md +0 -447
  402. package/pennyfarthing-dist/guides/benchmarks.md +0 -62
  403. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
  404. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
  405. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
  406. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
  407. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
  408. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
  409. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
  410. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
  411. package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
  412. package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
  413. package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
  414. package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
  415. package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
  416. package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
  417. package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
  418. package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
  419. package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
  420. package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
  421. package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
  422. package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
@@ -10,9 +10,7 @@ from datetime import date
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
- import yaml
14
-
15
- from pennyfarthing_scripts.common.config import get_project_root, load_yaml_config
13
+ from pennyfarthing_scripts.common.config import get_project_root
16
14
 
17
15
 
18
16
  def parse_epics_markdown(content: str) -> dict[str, Any]:
@@ -195,13 +193,13 @@ def generate_initiative_yaml(
195
193
  today = date.today().isoformat()
196
194
 
197
195
  lines = [
198
- f" # ==========================================================================",
196
+ " # ==========================================================================",
199
197
  f" # {initiative_name.upper()}",
200
198
  f" # Imported from: {source_file}",
201
199
  f" # Date: {today}",
202
- f" # ==========================================================================",
200
+ " # ==========================================================================",
203
201
  f' - name: "{initiative_name}"',
204
- f" description: |",
202
+ " description: |",
205
203
  ]
206
204
 
207
205
  # Add description lines with proper indentation
@@ -210,10 +208,10 @@ def generate_initiative_yaml(
210
208
 
211
209
  lines.extend(
212
210
  [
213
- f" status: ready",
214
- f" blocked_by: null",
211
+ " status: ready",
212
+ " blocked_by: null",
215
213
  f" total_points: {parsed['total_points']}",
216
- f" epics:",
214
+ " epics:",
217
215
  ]
218
216
  )
219
217
 
@@ -228,14 +226,14 @@ def generate_initiative_yaml(
228
226
  [
229
227
  f" - id: epic-{current_epic_num}",
230
228
  f' title: "{epic["title"]}"',
231
- f" description: |",
229
+ " description: |",
232
230
  f" {epic.get('description', epic['title'])}",
233
231
  f" points: {epic_points}",
234
- f" priority: P1",
232
+ " priority: P1",
235
233
  f' marker: "{marker}"',
236
- f" repos: pennyfarthing",
237
- f" status: planning",
238
- f" stories:",
234
+ " repos: pennyfarthing",
235
+ " status: planning",
236
+ " stories:",
239
237
  ]
240
238
  )
241
239
 
@@ -247,12 +245,12 @@ def generate_initiative_yaml(
247
245
  [
248
246
  f' - id: "{story_id}"',
249
247
  f' title: "{title}"',
250
- f" description: |",
248
+ " description: |",
251
249
  f" {story.get('description', title)}",
252
250
  f" points: {story.get('points', 1)}",
253
- f" priority: P0",
254
- f" status: planning",
255
- f" repos: pennyfarthing",
251
+ " priority: P0",
252
+ " status: planning",
253
+ " repos: pennyfarthing",
256
254
  ]
257
255
  )
258
256
 
@@ -304,6 +302,32 @@ def import_epic(
304
302
  # Get next epic number
305
303
  start_epic_num = get_next_epic_number(root)
306
304
 
305
+ # Validate each parsed epic before writing (ADR-0022)
306
+ from pennyfarthing_scripts.sprint.validator import validate_epic_shard
307
+
308
+ epic_num = start_epic_num
309
+ for epic in parsed["epics"]:
310
+ # Build a shard-like dict for validation
311
+ shard_dict = {
312
+ "id": str(epic_num),
313
+ "title": epic.get("title", ""),
314
+ "status": "planning",
315
+ "stories": [
316
+ {
317
+ "id": f"{epic_num}-{s['num']}",
318
+ "title": s.get("title", ""),
319
+ "points": s.get("points", 1),
320
+ "status": "planning",
321
+ }
322
+ for s in epic.get("stories", [])
323
+ ],
324
+ }
325
+ validation = validate_epic_shard(shard_dict)
326
+ if not validation.valid:
327
+ error_msgs = "; ".join(e.message for e in validation.errors)
328
+ return {"success": False, "error": f"Epic {epic_num} validation failed: {error_msgs}"}
329
+ epic_num += 1
330
+
307
331
  # Generate YAML
308
332
  relative_path = str(epics_path.relative_to(root)) if epics_path.is_relative_to(root) else str(epics_path)
309
333
  new_yaml, next_epic_num = generate_initiative_yaml(
@@ -5,6 +5,7 @@ Provides access to sprint/current-sprint.yaml data.
5
5
  Supports sharded per-epic format (epic-{ref}.yaml shard files).
6
6
  """
7
7
 
8
+ import warnings
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
@@ -40,6 +41,11 @@ def _merge_epic_shards(data: dict[str, Any], sprint_dir: Path) -> dict[str, Any]
40
41
  epic_data = load_yaml_config(epic_file)
41
42
  if epic_data is not None:
42
43
  merged_epics.append(epic_data)
44
+ else:
45
+ warnings.warn(
46
+ f"Sprint epic ref '{ref}' not found: {epic_file}",
47
+ stacklevel=2,
48
+ )
43
49
 
44
50
  data["epics"] = merged_epics
45
51
  return data
@@ -6,7 +6,7 @@ Provides functions for getting and displaying sprint status.
6
6
 
7
7
  from typing import Any
8
8
 
9
- from pennyfarthing_scripts.sprint.loader import get_all_stories, get_sprint_info, load_sprint
9
+ from pennyfarthing_scripts.sprint.loader import get_all_stories, get_sprint_info
10
10
 
11
11
 
12
12
  def get_sprint_status(filter_status: str | None = None) -> dict[str, Any]:
@@ -99,7 +99,6 @@ def main(args: list[str] | None = None) -> int:
99
99
  Exit code
100
100
  """
101
101
  import argparse
102
- import sys
103
102
 
104
103
  parser = argparse.ArgumentParser(description="Show sprint status")
105
104
  parser.add_argument(
@@ -312,8 +312,8 @@ def story_add_command(
312
312
  raise click.ClickException("POINTS is required")
313
313
  try:
314
314
  init_points = int(title)
315
- except ValueError:
316
- raise click.ClickException(f"POINTS must be an integer, got '{title}'")
315
+ except ValueError as err:
316
+ raise click.ClickException(f"POINTS must be an integer, got '{title}'") from err
317
317
 
318
318
  result = add_initiative_story(
319
319
  initiative_slug=initiative,
@@ -7,7 +7,7 @@ Steps:
7
7
  1. Archive session file to sprint/archive/{jira-key}-session.md
8
8
  2. Squash merge PR via gh (handle already-merged)
9
9
  3. Transition Jira to Done
10
- 4. Update sprint YAML (status: done, completed date, remove assigned_to)
10
+ 4. Update sprint YAML (status: done, completed date)
11
11
  5. Archive completed epics
12
12
  6. Git cleanup (checkout develop, pull, delete local branch)
13
13
  7. Remove session file
@@ -16,6 +16,7 @@ Steps:
16
16
  import re
17
17
  import shutil
18
18
  import subprocess
19
+ import sys
19
20
  from datetime import date
20
21
  from pathlib import Path
21
22
  from typing import Any
@@ -23,7 +24,6 @@ from typing import Any
23
24
  from pennyfarthing_scripts.sprint.loader import find_epic, find_story
24
25
  from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
25
26
 
26
-
27
27
  SESSION_FIELD_RE = re.compile(r"\*\*(\w[\w\s]*):\*\*\s*(.*)")
28
28
 
29
29
 
@@ -175,8 +175,6 @@ def finish_story(
175
175
  if story:
176
176
  story["status"] = "done"
177
177
  story["completed"] = today
178
- if "assigned_to" in story:
179
- del story["assigned_to"]
180
178
  write_sprint(sprint_path, data)
181
179
  steps.append({"step": 4, "action": "yaml_update", "status": "done", "completed": today})
182
180
  else:
@@ -186,7 +184,7 @@ def finish_story(
186
184
 
187
185
  # --- Step 5: Archive completed epics ---
188
186
  result = _run(
189
- ["python", "-m", "pennyfarthing_scripts.cli", "sprint", "epic", "archive"],
187
+ [sys.executable, "-m", "pennyfarthing_scripts.cli", "sprint", "epic", "archive"],
190
188
  cwd=str(project_root),
191
189
  )
192
190
  steps.append({"step": 5, "action": "archive_epics", "ran": True})
@@ -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:
@@ -7,11 +7,8 @@ Provides functions for starting and managing work on stories.
7
7
  from typing import Any
8
8
 
9
9
  from pennyfarthing_scripts.sprint.loader import (
10
- find_epic,
11
- find_story,
12
10
  get_stories_by_status,
13
11
  get_story_by_id,
14
- load_sprint,
15
12
  )
16
13
 
17
14
 
@@ -220,7 +217,7 @@ def main(args: list[str] | None = None) -> int:
220
217
  print(f"Story: {story.get('id')}")
221
218
  print(f"Title: {story.get('title')}")
222
219
  print(f"Points: {story.get('points')}")
223
- print(f"Status: Available")
220
+ print("Status: Available")
224
221
  return 0
225
222
  else:
226
223
  print(f"Not available: {result.get('error') or result.get('reason')}", file=sys.stderr)
@@ -272,7 +272,9 @@ def canonical_dump(data: Any) -> str:
272
272
  def _get_epic_ref(epic: Mapping) -> str:
273
273
  """Get the canonical reference ID for an epic shard file.
274
274
 
275
- Mirrors the logic in migrate-to-shards.py: prefer Jira key, fall back to ID.
275
+ Priority: Jira key > numeric ID extracted from epic-N > raw ID.
276
+ Strips 'epic-' prefix from IDs to prevent double-prefix filenames
277
+ (e.g., epic-epic-94.yaml). See ADR-0022.
276
278
  """
277
279
  jira = epic.get("jira")
278
280
  epic_id = str(epic.get("id", ""))
@@ -281,7 +283,13 @@ def _get_epic_ref(epic: Mapping) -> str:
281
283
  return str(jira)
282
284
  if JIRA_PATTERN.match(epic_id):
283
285
  return epic_id
284
- return epic_id
286
+
287
+ # Strip epic- prefix to prevent double-prefix filenames
288
+ # e.g., "epic-94" -> "94" so file becomes "epic-94.yaml" not "epic-epic-94.yaml"
289
+ stripped = epic_id
290
+ while stripped.startswith("epic-"):
291
+ stripped = stripped[5:]
292
+ return stripped or epic_id
285
293
 
286
294
 
287
295
  def _write_yaml_file(path: Path, data: Any) -> None:
@@ -15,6 +15,20 @@ Usage:
15
15
  """
16
16
 
17
17
  # Re-export common functions
18
+ # Import submodules
19
+ # CLI entry point - import module, not function, so "from story import cli" gets the module
20
+ from pennyfarthing_scripts.story import (
21
+ cli,
22
+ create,
23
+ size,
24
+ template,
25
+ )
26
+ from pennyfarthing_scripts.story.cli import main
27
+ from pennyfarthing_scripts.story.create import (
28
+ create_story,
29
+ generate_story_yaml,
30
+ validate_points,
31
+ )
18
32
  from pennyfarthing_scripts.story.size import (
19
33
  SIZING_GUIDELINES,
20
34
  format_size_info,
@@ -26,22 +40,6 @@ from pennyfarthing_scripts.story.template import (
26
40
  get_all_templates,
27
41
  get_template,
28
42
  )
29
- from pennyfarthing_scripts.story.create import (
30
- create_story,
31
- generate_story_yaml,
32
- validate_points,
33
- )
34
-
35
- # Import submodules
36
- from pennyfarthing_scripts.story import (
37
- create,
38
- size,
39
- template,
40
- )
41
-
42
- # CLI entry point - import module, not function, so "from story import cli" gets the module
43
- from pennyfarthing_scripts.story import cli
44
- from pennyfarthing_scripts.story.cli import main
45
43
 
46
44
  __all__ = [
47
45
  # Size
@@ -6,7 +6,6 @@ Provides guidelines and helpers for sizing stories.
6
6
 
7
7
  from typing import Any
8
8
 
9
-
10
9
  # Sizing guidelines
11
10
  SIZING_GUIDELINES = {
12
11
  1: {
@@ -6,7 +6,6 @@ Provides templates for different story types.
6
6
 
7
7
  from typing import Any
8
8
 
9
-
10
9
  # Story templates
11
10
  TEMPLATES = {
12
11
  "feature": {
@@ -19,7 +19,6 @@ from dataclasses import dataclass, field
19
19
  from pathlib import Path
20
20
  from typing import Any
21
21
 
22
-
23
22
  # Default cache location for SWE-bench data
24
23
  DEFAULT_CACHE_PATH = "/tmp/swebench_all.json"
25
24
 
@@ -108,7 +107,7 @@ def load_swebench_data(cache_path: str | Path = DEFAULT_CACHE_PATH) -> list[dict
108
107
  FileNotFoundError: If cache file doesn't exist
109
108
  json.JSONDecodeError: If cache file is invalid JSON
110
109
  """
111
- with open(cache_path, "r") as f:
110
+ with open(cache_path) as f:
112
111
  return json.load(f)
113
112
 
114
113
 
@@ -3,10 +3,9 @@
3
3
  Story 63-9: Reorganize pennyfarthing_scripts into fan-out CLI pattern.
4
4
  """
5
5
 
6
- import os
7
6
  import sys
7
+ from collections.abc import Generator
8
8
  from pathlib import Path
9
- from typing import Generator
10
9
 
11
10
  import pytest
12
11