@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
@@ -0,0 +1,699 @@
1
+ """Tests for epic shard write-time validation and reference integrity.
2
+
3
+ Story: MSSCI-14734 / 91-24 - Sprint shard write-time validation
4
+ ADR: ADR-0022 - Sprint Shard Validation and Reference Integrity
5
+
6
+ Tests cover all six acceptance criteria:
7
+ AC1: validate_epic_shard() rejects epics missing id, title, status, or stories
8
+ AC2: _get_epic_ref() strips epic- prefix from IDs to prevent double-prefix filenames
9
+ AC3: epic_add, epic_promote, jira_create_epic, import_epic all call validator before write
10
+ AC4: _merge_epic_shards() emits warning for unresolvable refs (not silent skip)
11
+ AC5: Jira create epic checks for existing epic with same title before creating
12
+ AC6: Existing validator tests pass; new tests cover all five validation rules
13
+ """
14
+
15
+ import warnings
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import pytest
20
+ import yaml
21
+
22
+ from pennyfarthing_scripts.sprint.validator import (
23
+ REQUIRED_EPIC_SHARD_FIELDS,
24
+ REQUIRED_SHARD_STORY_FIELDS,
25
+ ValidationResult,
26
+ validate_epic_shard,
27
+ validate_sprint_file,
28
+ )
29
+ from pennyfarthing_scripts.sprint.yaml_io import _get_epic_ref
30
+
31
+ # =============================================================================
32
+ # Fixtures
33
+ # =============================================================================
34
+
35
+
36
+ @pytest.fixture
37
+ def valid_epic_shard() -> dict[str, Any]:
38
+ """A minimal valid epic shard dict."""
39
+ return {
40
+ "id": "94",
41
+ "title": "Epic: Cross-File Validation",
42
+ "status": "backlog",
43
+ "stories": [
44
+ {
45
+ "id": "94-1",
46
+ "title": "First story",
47
+ "points": 3,
48
+ "status": "backlog",
49
+ },
50
+ ],
51
+ }
52
+
53
+
54
+ @pytest.fixture
55
+ def valid_epic_shard_with_jira() -> dict[str, Any]:
56
+ """A valid epic shard with a Jira key."""
57
+ return {
58
+ "id": "94",
59
+ "title": "Epic: Validation Pipeline",
60
+ "status": "in_progress",
61
+ "jira": "MSSCI-14659",
62
+ "stories": [
63
+ {
64
+ "id": "94-1",
65
+ "title": "Wire up validation",
66
+ "points": 5,
67
+ "status": "done",
68
+ },
69
+ ],
70
+ }
71
+
72
+
73
+ # =============================================================================
74
+ # AC1: validate_epic_shard() rejects epics missing required fields
75
+ # =============================================================================
76
+
77
+
78
+ class TestValidateEpicShardRequiredFields:
79
+ """validate_epic_shard enforces id, title, status, stories are required."""
80
+
81
+ def test_valid_shard_passes(self, valid_epic_shard: dict[str, Any]) -> None:
82
+ """A complete, well-formed shard should pass validation."""
83
+ result = validate_epic_shard(valid_epic_shard)
84
+
85
+ assert result.valid is True
86
+ assert len(result.errors) == 0
87
+
88
+ def test_constants_defined(self) -> None:
89
+ """Required field constants should exist and match ADR-0022."""
90
+ assert REQUIRED_EPIC_SHARD_FIELDS == {"id", "title", "status", "stories"}
91
+ assert REQUIRED_SHARD_STORY_FIELDS == {"id", "title", "points", "status"}
92
+
93
+ def test_missing_id_fails(self, valid_epic_shard: dict[str, Any]) -> None:
94
+ """Shard without 'id' should fail validation."""
95
+ del valid_epic_shard["id"]
96
+
97
+ result = validate_epic_shard(valid_epic_shard)
98
+
99
+ assert result.valid is False
100
+ assert any("id" in e.message.lower() for e in result.errors)
101
+
102
+ def test_missing_title_fails(self, valid_epic_shard: dict[str, Any]) -> None:
103
+ """Shard without 'title' should fail validation."""
104
+ del valid_epic_shard["title"]
105
+
106
+ result = validate_epic_shard(valid_epic_shard)
107
+
108
+ assert result.valid is False
109
+ assert any("title" in e.message.lower() for e in result.errors)
110
+
111
+ def test_missing_status_fails(self, valid_epic_shard: dict[str, Any]) -> None:
112
+ """Shard without 'status' should fail validation."""
113
+ del valid_epic_shard["status"]
114
+
115
+ result = validate_epic_shard(valid_epic_shard)
116
+
117
+ assert result.valid is False
118
+ assert any("status" in e.message.lower() for e in result.errors)
119
+
120
+ def test_missing_stories_fails(self, valid_epic_shard: dict[str, Any]) -> None:
121
+ """Shard without 'stories' should fail validation."""
122
+ del valid_epic_shard["stories"]
123
+
124
+ result = validate_epic_shard(valid_epic_shard)
125
+
126
+ assert result.valid is False
127
+ assert any("stories" in e.message.lower() for e in result.errors)
128
+
129
+ def test_empty_dict_fails_all_fields(self) -> None:
130
+ """Empty dict should report all four missing required fields."""
131
+ result = validate_epic_shard({})
132
+
133
+ assert result.valid is False
134
+ assert len(result.errors) >= 4
135
+
136
+ def test_stories_must_be_list(self, valid_epic_shard: dict[str, Any]) -> None:
137
+ """'stories' field must be a list, not a string or dict."""
138
+ valid_epic_shard["stories"] = "not a list"
139
+
140
+ result = validate_epic_shard(valid_epic_shard)
141
+
142
+ assert result.valid is False
143
+ assert any("list" in e.message.lower() for e in result.errors)
144
+
145
+ def test_story_missing_required_fields(
146
+ self, valid_epic_shard: dict[str, Any]
147
+ ) -> None:
148
+ """Stories within shard must have id, title, points, status."""
149
+ valid_epic_shard["stories"] = [{"id": "94-1"}] # Missing title, points, status
150
+
151
+ result = validate_epic_shard(valid_epic_shard)
152
+
153
+ assert result.valid is False
154
+ error_msgs = " ".join(e.message for e in result.errors)
155
+ assert "title" in error_msgs.lower()
156
+ assert "points" in error_msgs.lower()
157
+ assert "status" in error_msgs.lower()
158
+
159
+ def test_duplicate_story_ids_within_shard_fails(
160
+ self, valid_epic_shard: dict[str, Any]
161
+ ) -> None:
162
+ """No duplicate story IDs within a single shard."""
163
+ valid_epic_shard["stories"] = [
164
+ {"id": "94-1", "title": "Story A", "points": 3, "status": "backlog"},
165
+ {"id": "94-1", "title": "Story B", "points": 2, "status": "done"},
166
+ ]
167
+
168
+ result = validate_epic_shard(valid_epic_shard)
169
+
170
+ assert result.valid is False
171
+ assert any("duplicate" in e.message.lower() for e in result.errors)
172
+
173
+ def test_valid_jira_key_passes(
174
+ self, valid_epic_shard_with_jira: dict[str, Any]
175
+ ) -> None:
176
+ """Valid MSSCI-NNNNN Jira key should pass."""
177
+ result = validate_epic_shard(valid_epic_shard_with_jira)
178
+
179
+ assert result.valid is True
180
+
181
+ def test_invalid_jira_key_fails(
182
+ self, valid_epic_shard: dict[str, Any]
183
+ ) -> None:
184
+ """Invalid Jira key format should fail."""
185
+ valid_epic_shard["jira"] = "INVALID-KEY"
186
+
187
+ result = validate_epic_shard(valid_epic_shard)
188
+
189
+ assert result.valid is False
190
+ assert any("jira" in e.message.lower() for e in result.errors)
191
+
192
+ def test_jira_key_wrong_project_fails(
193
+ self, valid_epic_shard: dict[str, Any]
194
+ ) -> None:
195
+ """Jira key from wrong project should fail."""
196
+ valid_epic_shard["jira"] = "PROJ-12345"
197
+
198
+ result = validate_epic_shard(valid_epic_shard)
199
+
200
+ assert result.valid is False
201
+
202
+ def test_empty_stories_list_passes(
203
+ self, valid_epic_shard: dict[str, Any]
204
+ ) -> None:
205
+ """Empty stories list is valid (epic exists but has no stories yet)."""
206
+ valid_epic_shard["stories"] = []
207
+
208
+ result = validate_epic_shard(valid_epic_shard)
209
+
210
+ assert result.valid is True
211
+
212
+
213
+ # =============================================================================
214
+ # AC1 continued: epic- prefix rejection (ADR-0022 root cause fix)
215
+ # =============================================================================
216
+
217
+
218
+ class TestEpicPrefixRejection:
219
+ """validate_epic_shard rejects IDs starting with 'epic-' prefix."""
220
+
221
+ def test_epic_prefix_in_id_rejected(self) -> None:
222
+ """ID 'epic-94' should fail — reference prefix baked into value."""
223
+ shard = {
224
+ "id": "epic-94",
225
+ "title": "Test",
226
+ "status": "backlog",
227
+ "stories": [],
228
+ }
229
+
230
+ result = validate_epic_shard(shard)
231
+
232
+ assert result.valid is False
233
+ assert any("epic-" in e.message for e in result.errors)
234
+
235
+ def test_numeric_id_accepted(self) -> None:
236
+ """Numeric ID '94' should pass."""
237
+ shard = {
238
+ "id": "94",
239
+ "title": "Test",
240
+ "status": "backlog",
241
+ "stories": [],
242
+ }
243
+
244
+ result = validate_epic_shard(shard)
245
+
246
+ assert result.valid is True
247
+
248
+ def test_jira_key_as_id_accepted(self) -> None:
249
+ """Jira key as ID should pass (e.g., 'MSSCI-14510')."""
250
+ shard = {
251
+ "id": "MSSCI-14510",
252
+ "title": "Test",
253
+ "status": "backlog",
254
+ "stories": [],
255
+ }
256
+
257
+ result = validate_epic_shard(shard)
258
+
259
+ assert result.valid is True
260
+
261
+ def test_double_prefix_caught(self) -> None:
262
+ """Double prefix 'epic-epic-94' should definitely fail."""
263
+ shard = {
264
+ "id": "epic-epic-94",
265
+ "title": "Test",
266
+ "status": "backlog",
267
+ "stories": [],
268
+ }
269
+
270
+ result = validate_epic_shard(shard)
271
+
272
+ assert result.valid is False
273
+
274
+
275
+ # =============================================================================
276
+ # AC2: _get_epic_ref() strips epic- prefix, prevents double-prefix filenames
277
+ # =============================================================================
278
+
279
+
280
+ class TestGetEpicRefNormalization:
281
+ """_get_epic_ref returns canonical references for shard filenames."""
282
+
283
+ def test_jira_key_preferred_over_id(self) -> None:
284
+ """When both jira and id are present, Jira key wins."""
285
+ epic = {"id": "94", "jira": "MSSCI-14659"}
286
+
287
+ ref = _get_epic_ref(epic)
288
+
289
+ assert ref == "MSSCI-14659"
290
+
291
+ def test_strips_epic_prefix_from_id(self) -> None:
292
+ """ID 'epic-94' should return '94' (prevents epic-epic-94.yaml)."""
293
+ epic = {"id": "epic-94"}
294
+
295
+ ref = _get_epic_ref(epic)
296
+
297
+ assert ref == "94"
298
+
299
+ def test_numeric_id_unchanged(self) -> None:
300
+ """Plain numeric ID '94' should return '94'."""
301
+ epic = {"id": "94"}
302
+
303
+ ref = _get_epic_ref(epic)
304
+
305
+ assert ref == "94"
306
+
307
+ def test_jira_key_as_id_returned_directly(self) -> None:
308
+ """ID that IS a Jira key should be returned directly."""
309
+ epic = {"id": "MSSCI-14510"}
310
+
311
+ ref = _get_epic_ref(epic)
312
+
313
+ assert ref == "MSSCI-14510"
314
+
315
+ def test_invalid_jira_key_falls_through(self) -> None:
316
+ """Invalid Jira key in jira field should fall through to ID."""
317
+ epic = {"id": "94", "jira": "INVALID"}
318
+
319
+ ref = _get_epic_ref(epic)
320
+
321
+ # Invalid jira ignored, falls through to id
322
+ assert ref == "94"
323
+
324
+ def test_double_prefix_stripped(self) -> None:
325
+ """'epic-epic-94' should strip all epic- prefixes to '94'."""
326
+ epic = {"id": "epic-epic-94"}
327
+
328
+ ref = _get_epic_ref(epic)
329
+
330
+ assert ref == "94"
331
+
332
+ def test_resulting_filename_is_correct(self) -> None:
333
+ """Reference should produce correct filename: epic-{ref}.yaml."""
334
+ epic = {"id": "epic-94"}
335
+ ref = _get_epic_ref(epic)
336
+
337
+ filename = f"epic-{ref}.yaml"
338
+
339
+ assert filename == "epic-94.yaml"
340
+ assert "epic-epic-" not in filename
341
+
342
+ def test_jira_key_filename_correct(self) -> None:
343
+ """Jira key ref should produce epic-MSSCI-14659.yaml."""
344
+ epic = {"id": "94", "jira": "MSSCI-14659"}
345
+ ref = _get_epic_ref(epic)
346
+
347
+ filename = f"epic-{ref}.yaml"
348
+
349
+ assert filename == "epic-MSSCI-14659.yaml"
350
+
351
+
352
+ # =============================================================================
353
+ # AC3: All four write paths call validator before write
354
+ # =============================================================================
355
+
356
+
357
+ class TestWritePathValidatorIntegration:
358
+ """All four epic creation paths call validate_epic_shard before writing."""
359
+
360
+ def test_epic_add_validates_before_write(self, tmp_path: Path) -> None:
361
+ """epic_add.add_epic() should reject invalid epics before writing."""
362
+ from pennyfarthing_scripts.sprint.epic_add import add_epic
363
+
364
+ # Create a minimal sprint index
365
+ index = tmp_path / "current-sprint.yaml"
366
+ index.write_text("sprint:\n name: Test\nepics: []\n")
367
+
368
+ # Try to add an epic with prefix in ID (validator should catch)
369
+ result = add_epic(
370
+ sprint_path=index,
371
+ epic_id="epic-99",
372
+ title="Bad Epic",
373
+ )
374
+
375
+ assert result["success"] is False
376
+ assert "validation" in result["error"].lower() or "epic-" in result["error"]
377
+
378
+ def test_epic_add_valid_epic_succeeds(self, tmp_path: Path) -> None:
379
+ """epic_add.add_epic() should accept valid epics."""
380
+ from pennyfarthing_scripts.sprint.epic_add import add_epic
381
+
382
+ index = tmp_path / "current-sprint.yaml"
383
+ index.write_text("sprint:\n name: Test\nepics: []\n")
384
+
385
+ result = add_epic(
386
+ sprint_path=index,
387
+ epic_id="99",
388
+ title="Good Epic",
389
+ )
390
+
391
+ assert result["success"] is True
392
+
393
+ def test_import_epic_validates_before_write(self, tmp_path: Path) -> None:
394
+ """import_epic() validates generated YAML before writing to future.yaml."""
395
+ from pennyfarthing_scripts.sprint.import_epic import import_epic
396
+
397
+ # Create a markdown file with an epic
398
+ md_file = tmp_path / "epics.md"
399
+ md_file.write_text(
400
+ "# Test Initiative - Epics and Stories\n\n"
401
+ "## Overview\n\nTest description\n\n"
402
+ "## Epic 1: First Epic\n\n"
403
+ "**Points:** 5\n\n"
404
+ "### Story 1.1: First Story\n\n"
405
+ "**Points:** 5\n\n"
406
+ )
407
+
408
+ # Create future.yaml
409
+ future = tmp_path / "sprint" / "future.yaml"
410
+ future.parent.mkdir(parents=True)
411
+ future.write_text(
412
+ "# Next Available Epic Number: 100\n"
413
+ "future:\n"
414
+ " initiatives: []\n"
415
+ )
416
+
417
+ result = import_epic(
418
+ md_file,
419
+ initiative_name="Test Initiative",
420
+ project_root=tmp_path,
421
+ dry_run=True,
422
+ )
423
+
424
+ # If validation passes, dry_run should succeed
425
+ assert result["success"] is True
426
+
427
+ def test_epic_promote_validates_shard(self) -> None:
428
+ """epic_promote calls validate_epic_shard before writing.
429
+
430
+ We verify by checking the source code imports and calls the validator.
431
+ Full integration test requires complex future.yaml setup.
432
+ """
433
+ import inspect
434
+
435
+ from pennyfarthing_scripts.sprint import cli
436
+
437
+ # epic_promote is a Click Command; inspect the underlying callback
438
+ source = inspect.getsource(cli.epic_promote.callback)
439
+ assert "validate_epic_shard" in source
440
+
441
+ def test_jira_create_epic_validates_before_api_call(self) -> None:
442
+ """create_epic_in_jira calls validate_epic_shard before Jira API.
443
+
444
+ Verified via source inspection since Jira API is external.
445
+ """
446
+ import inspect
447
+
448
+ from pennyfarthing_scripts.jira.create import create_epic_in_jira
449
+
450
+ source = inspect.getsource(create_epic_in_jira)
451
+ assert "validate_epic_shard" in source
452
+ # Validator call should come before client.create_issue_sync
453
+ validator_pos = source.index("validate_epic_shard")
454
+ # The function should call validator before creating the issue
455
+ assert validator_pos > 0
456
+
457
+
458
+ # =============================================================================
459
+ # AC4: _merge_epic_shards() emits warning for unresolvable refs
460
+ # =============================================================================
461
+
462
+
463
+ class TestLoaderWarnings:
464
+ """_merge_epic_shards emits warnings instead of silently skipping."""
465
+
466
+ def test_missing_shard_emits_warning(self, tmp_path: Path) -> None:
467
+ """Unresolvable shard ref should emit a warning, not silently skip."""
468
+ from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
469
+
470
+ data = {
471
+ "epics": ["MSSCI-99999"], # Doesn't exist
472
+ }
473
+
474
+ with warnings.catch_warnings(record=True) as caught:
475
+ warnings.simplefilter("always")
476
+ _merge_epic_shards(data, tmp_path)
477
+
478
+ assert len(caught) == 1
479
+ assert "MSSCI-99999" in str(caught[0].message)
480
+
481
+ def test_missing_shard_excluded_from_result(self, tmp_path: Path) -> None:
482
+ """Missing shard refs should not appear in the merged epics list."""
483
+ from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
484
+
485
+ data = {"epics": ["MSSCI-99999"]}
486
+
487
+ with warnings.catch_warnings(record=True):
488
+ warnings.simplefilter("always")
489
+ result = _merge_epic_shards(data, tmp_path)
490
+
491
+ assert len(result["epics"]) == 0
492
+
493
+ def test_valid_shard_loaded_missing_shard_warned(self, tmp_path: Path) -> None:
494
+ """Mix of valid and missing shards: load valid, warn on missing."""
495
+ from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
496
+
497
+ # Create one valid shard
498
+ shard = tmp_path / "epic-MSSCI-14298.yaml"
499
+ shard.write_text(
500
+ "id: MSSCI-14298\ntitle: Valid Epic\nstatus: active\nstories: []\n"
501
+ )
502
+
503
+ data = {"epics": ["MSSCI-14298", "MISSING-REF"]}
504
+
505
+ with warnings.catch_warnings(record=True) as caught:
506
+ warnings.simplefilter("always")
507
+ result = _merge_epic_shards(data, tmp_path)
508
+
509
+ # One epic loaded, one warning
510
+ assert len(result["epics"]) == 1
511
+ assert result["epics"][0]["id"] == "MSSCI-14298"
512
+ assert len(caught) == 1
513
+ assert "MISSING-REF" in str(caught[0].message)
514
+
515
+ def test_non_sharded_data_unchanged(self, tmp_path: Path) -> None:
516
+ """Non-sharded data (epics are dicts) should pass through unchanged."""
517
+ from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
518
+
519
+ data = {
520
+ "epics": [
521
+ {"id": "94", "title": "Inline", "status": "backlog", "stories": []},
522
+ ]
523
+ }
524
+
525
+ with warnings.catch_warnings(record=True) as caught:
526
+ warnings.simplefilter("always")
527
+ result = _merge_epic_shards(data, tmp_path)
528
+
529
+ assert len(caught) == 0
530
+ assert result["epics"][0]["id"] == "94"
531
+
532
+ def test_strict_mode_promotes_warnings_to_errors(self, tmp_path: Path) -> None:
533
+ """validate_sprint_file(strict=True) should treat missing refs as errors."""
534
+ sprint_file = tmp_path / "current-sprint.yaml"
535
+ sprint_file.write_text(
536
+ "sprint:\n"
537
+ " number: 12\n"
538
+ " jira_sprint_id: 276\n"
539
+ " goal: Test\n"
540
+ " start_date: 2026-01-20\n"
541
+ " end_date: 2026-02-02\n"
542
+ " status: active\n"
543
+ "epics:\n"
544
+ " - MISSING-REF\n"
545
+ )
546
+
547
+ result = validate_sprint_file(sprint_file, strict=True)
548
+
549
+ assert result.valid is False
550
+ assert any("MISSING-REF" in e.message for e in result.errors)
551
+
552
+ def test_non_strict_mode_tolerates_missing_refs(self, tmp_path: Path) -> None:
553
+ """validate_sprint_file(strict=False) should not fail on missing refs."""
554
+ sprint_file = tmp_path / "current-sprint.yaml"
555
+ sprint_file.write_text(
556
+ "sprint:\n"
557
+ " number: 12\n"
558
+ " jira_sprint_id: 276\n"
559
+ " goal: Test\n"
560
+ " start_date: 2026-01-20\n"
561
+ " end_date: 2026-02-02\n"
562
+ " status: active\n"
563
+ "epics:\n"
564
+ " - MISSING-REF\n"
565
+ )
566
+
567
+ result = validate_sprint_file(sprint_file, strict=False)
568
+
569
+ # Should be valid (warnings, not errors)
570
+ assert result.valid is True
571
+
572
+
573
+ # =============================================================================
574
+ # AC5: Jira create epic checks for existing epic with same title
575
+ # =============================================================================
576
+
577
+
578
+ class TestJiraIdempotencyGuard:
579
+ """create_epic_in_jira checks for duplicate titles before creating."""
580
+
581
+ def test_idempotency_check_in_source(self) -> None:
582
+ """Source code should contain duplicate title search logic."""
583
+ import inspect
584
+
585
+ from pennyfarthing_scripts.jira.create import create_epic_in_jira
586
+
587
+ source = inspect.getsource(create_epic_in_jira)
588
+ # Should search for existing epic with same title
589
+ assert "search_issues_sync" in source or "duplicate" in source.lower()
590
+
591
+ def test_force_flag_parameter_exists(self) -> None:
592
+ """create_epic_in_jira should accept a force parameter."""
593
+ import inspect
594
+
595
+ from pennyfarthing_scripts.jira.create import create_epic_in_jira
596
+
597
+ sig = inspect.signature(create_epic_in_jira)
598
+ assert "force" in sig.parameters
599
+
600
+ def test_force_default_is_false(self) -> None:
601
+ """force parameter should default to False (safe by default)."""
602
+ import inspect
603
+
604
+ from pennyfarthing_scripts.jira.create import create_epic_in_jira
605
+
606
+ sig = inspect.signature(create_epic_in_jira)
607
+ assert sig.parameters["force"].default is False
608
+
609
+
610
+ # =============================================================================
611
+ # AC6: Integration — existing validator tests still pass, new rules covered
612
+ # =============================================================================
613
+
614
+
615
+ class TestValidationIntegration:
616
+ """Integration tests ensuring all validation rules work together."""
617
+
618
+ def test_full_valid_shard_roundtrip(
619
+ self, tmp_path: Path, valid_epic_shard_with_jira: dict[str, Any]
620
+ ) -> None:
621
+ """Valid shard passes validation, writes correctly, reads back."""
622
+ from pennyfarthing_scripts.sprint.yaml_io import _get_epic_ref
623
+
624
+ # Validate
625
+ result = validate_epic_shard(valid_epic_shard_with_jira)
626
+ assert result.valid is True
627
+
628
+ # Get ref
629
+ ref = _get_epic_ref(valid_epic_shard_with_jira)
630
+ assert ref == "MSSCI-14659"
631
+
632
+ # Write shard file
633
+ shard_path = tmp_path / f"epic-{ref}.yaml"
634
+ yaml_content = yaml.dump(
635
+ valid_epic_shard_with_jira, default_flow_style=False
636
+ )
637
+ shard_path.write_text(yaml_content)
638
+
639
+ # Verify file name is correct
640
+ assert shard_path.name == "epic-MSSCI-14659.yaml"
641
+ assert "epic-epic-" not in shard_path.name
642
+
643
+ def test_bad_shard_blocked_at_all_gates(self) -> None:
644
+ """An epic with epic- prefix should be caught by validator."""
645
+ bad_shard = {
646
+ "id": "epic-94",
647
+ "title": "Bad Epic",
648
+ "status": "backlog",
649
+ "stories": [],
650
+ }
651
+
652
+ result = validate_epic_shard(bad_shard)
653
+ assert result.valid is False
654
+
655
+ def test_validation_result_accumulates_multiple_errors(self) -> None:
656
+ """Multiple validation failures should all be reported."""
657
+ terrible_shard = {
658
+ "id": "epic-bad",
659
+ "jira": "INVALID",
660
+ "stories": "not a list",
661
+ }
662
+
663
+ result = validate_epic_shard(terrible_shard)
664
+
665
+ assert result.valid is False
666
+ # Should report: missing title, missing status, epic- prefix,
667
+ # invalid jira, stories not a list
668
+ assert len(result.errors) >= 3
669
+
670
+ def test_real_sprint_validates(self) -> None:
671
+ """The actual current-sprint.yaml should pass validation."""
672
+ project_root = Path(__file__).parent.parent.parent
673
+ sprint_file = project_root / "sprint" / "current-sprint.yaml"
674
+
675
+ if not sprint_file.exists():
676
+ pytest.skip("No current-sprint.yaml available")
677
+
678
+ result = validate_sprint_file(sprint_file)
679
+
680
+ assert isinstance(result, ValidationResult)
681
+ if not result.valid:
682
+ for error in result.errors:
683
+ print(f" {error.path}: {error.message}")
684
+
685
+ def test_real_sprint_strict_mode(self) -> None:
686
+ """The actual current-sprint.yaml should pass strict validation."""
687
+ project_root = Path(__file__).parent.parent.parent
688
+ sprint_file = project_root / "sprint" / "current-sprint.yaml"
689
+
690
+ if not sprint_file.exists():
691
+ pytest.skip("No current-sprint.yaml available")
692
+
693
+ result = validate_sprint_file(sprint_file, strict=True)
694
+
695
+ assert isinstance(result, ValidationResult)
696
+ # In strict mode, all shard refs should be resolvable
697
+ if not result.valid:
698
+ for error in result.errors:
699
+ print(f" {error.path}: {error.message}")