@pennyfarthing/core 10.1.0 → 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 (407) hide show
  1. package/README.md +13 -18
  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 +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/uninstall.d.ts.map +1 -1
  17. package/packages/core/dist/cli/commands/uninstall.js +24 -13
  18. package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
  19. package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
  20. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  21. package/packages/core/dist/cli/commands/update.js.map +1 -1
  22. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  23. package/packages/core/dist/cli/theme-maker.test.js +64 -115
  24. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  25. package/packages/core/dist/index.d.ts +1 -1
  26. package/packages/core/dist/index.d.ts.map +1 -1
  27. package/packages/core/dist/index.js +2 -2
  28. package/packages/core/dist/index.js.map +1 -1
  29. package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
  30. package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
  31. package/packages/core/dist/plugins/plugin-discovery.js +165 -0
  32. package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
  33. package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
  34. package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
  35. package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
  36. package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
  37. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  38. package/packages/core/dist/workflow/context-watch.d.ts +80 -0
  39. package/packages/core/dist/workflow/context-watch.d.ts.map +1 -0
  40. package/packages/core/dist/workflow/context-watch.js +235 -0
  41. package/packages/core/dist/workflow/context-watch.js.map +1 -0
  42. package/packages/core/dist/workflow/context-watch.test.d.ts +1 -0
  43. package/packages/core/dist/workflow/context-watch.test.d.ts.map +1 -0
  44. package/packages/core/dist/workflow/context-watch.test.js +746 -0
  45. package/packages/core/dist/workflow/context-watch.test.js.map +1 -0
  46. package/packages/core/dist/workflow/file-watch.d.ts +82 -0
  47. package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
  48. package/packages/core/dist/workflow/file-watch.js +198 -0
  49. package/packages/core/dist/workflow/file-watch.js.map +1 -0
  50. package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
  51. package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
  52. package/packages/core/dist/workflow/file-watch.test.js +469 -0
  53. package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
  54. package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
  55. package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
  56. package/packages/core/dist/workflow/observation-writer.js +97 -0
  57. package/packages/core/dist/workflow/observation-writer.js.map +1 -0
  58. package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
  59. package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
  60. package/packages/core/dist/workflow/observation-writer.test.js +424 -0
  61. package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
  62. package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
  63. package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
  64. package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
  65. package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
  66. package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
  67. package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
  68. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
  69. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
  70. package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
  71. package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
  72. package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
  73. package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
  74. package/packages/core/dist/workflow/tool-watch.js +166 -0
  75. package/packages/core/dist/workflow/tool-watch.js.map +1 -0
  76. package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
  77. package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
  78. package/packages/core/dist/workflow/tool-watch.test.js +718 -0
  79. package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
  80. package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
  81. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  82. package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
  83. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  84. package/packages/core/dist/workflow/workflow-schema.js +44 -0
  85. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  86. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  87. package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
  88. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  89. package/pennyfarthing-dist/agents/handoff.md +18 -3
  90. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  91. package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
  92. package/pennyfarthing-dist/agents/sm.md +11 -5
  93. package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
  94. package/pennyfarthing-dist/commands/setup.md +4 -0
  95. package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
  96. package/pennyfarthing-dist/guides/bikelane.md +3 -2
  97. package/pennyfarthing-dist/guides/scale-levels.md +4 -6
  98. package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
  99. package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
  100. package/pennyfarthing-dist/personas/themes/fifth-element.yaml +295 -0
  101. package/pennyfarthing-dist/scripts/README.md +1 -1
  102. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
  103. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +20 -10
  104. package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
  105. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  106. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -189
  107. package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
  108. package/pennyfarthing-dist/skills/skill-registry.yaml +1 -1
  109. package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
  110. package/pennyfarthing-dist/skills/workflow/skill.md +24 -1
  111. package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
  112. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
  113. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
  114. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  122. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  123. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  124. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  125. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  126. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/bellmode_hook.py +202 -47
  128. package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
  129. package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
  130. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/brownfield/cli.py +0 -1
  135. package/pennyfarthing_scripts/brownfield/discover.py +1 -2
  136. package/pennyfarthing_scripts/cli.py +11 -6
  137. package/pennyfarthing_scripts/codemarkers/__init__.py +5 -1
  138. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/codemarkers/analyze.py +177 -2
  145. package/pennyfarthing_scripts/codemarkers/cli.py +50 -0
  146. package/pennyfarthing_scripts/codemarkers/formatters.py +0 -1
  147. package/pennyfarthing_scripts/codemarkers/models.py +15 -0
  148. package/pennyfarthing_scripts/common/__init__.py +8 -9
  149. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  150. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  151. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  152. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  153. package/pennyfarthing_scripts/common/config.py +1 -1
  154. package/pennyfarthing_scripts/complexity/__init__.py +1 -1
  155. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  156. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  157. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  158. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  159. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  160. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/complexity/analyze.py +1 -1
  162. package/pennyfarthing_scripts/complexity/cli.py +5 -1
  163. package/pennyfarthing_scripts/complexity/formatters.py +1 -1
  164. package/pennyfarthing_scripts/context.py +14 -15
  165. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  167. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/deadcode/analyze.py +3 -4
  172. package/pennyfarthing_scripts/deadcode/cli.py +2 -2
  173. package/pennyfarthing_scripts/dependencies/__init__.py +2 -2
  174. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/dependencies/analyze.py +1 -1
  181. package/pennyfarthing_scripts/dependencies/cli.py +8 -4
  182. package/pennyfarthing_scripts/dependencies/formatters.py +1 -1
  183. package/pennyfarthing_scripts/git/__init__.py +5 -5
  184. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  185. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/git/create_branches.py +3 -2
  188. package/pennyfarthing_scripts/git/status_all.py +1 -1
  189. package/pennyfarthing_scripts/healthscore/__init__.py +2 -2
  190. package/pennyfarthing_scripts/healthscore/__main__.py +8 -0
  191. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/healthscore/analyze.py +451 -21
  198. package/pennyfarthing_scripts/healthscore/cli.py +5 -1
  199. package/pennyfarthing_scripts/healthscore/models.py +0 -1
  200. package/pennyfarthing_scripts/hooks.py +8 -11
  201. package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
  202. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/hotspots/analyze.py +128 -14
  209. package/pennyfarthing_scripts/hotspots/cli.py +2 -2
  210. package/pennyfarthing_scripts/hotspots/models.py +0 -1
  211. package/pennyfarthing_scripts/jira/__init__.py +15 -17
  212. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  220. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  222. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/jira/bidirectional.py +2 -3
  225. package/pennyfarthing_scripts/jira/claim.py +21 -0
  226. package/pennyfarthing_scripts/jira/cli.py +2 -2
  227. package/pennyfarthing_scripts/jira/client.py +4 -4
  228. package/pennyfarthing_scripts/jira/create.py +45 -1
  229. package/pennyfarthing_scripts/jira/epic.py +3 -2
  230. package/pennyfarthing_scripts/jira/reconcile.py +0 -1
  231. package/pennyfarthing_scripts/jira/story.py +2 -0
  232. package/pennyfarthing_scripts/jira/sync.py +1 -1
  233. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  238. package/pennyfarthing_scripts/migration/skill.py +0 -1
  239. package/pennyfarthing_scripts/migration/step.py +0 -1
  240. package/pennyfarthing_scripts/migration/validate.py +8 -5
  241. package/pennyfarthing_scripts/patch_mode.py +2 -2
  242. package/pennyfarthing_scripts/preflight/__init__.py +1 -1
  243. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  244. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/preflight/finish.py +0 -1
  248. package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
  249. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/prime/cli.py +5 -1
  258. package/pennyfarthing_scripts/prime/loader.py +2 -3
  259. package/pennyfarthing_scripts/prime/persona.py +2 -1
  260. package/pennyfarthing_scripts/prime/tiers.py +4 -4
  261. package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
  262. package/pennyfarthing_scripts/sprint/__init__.py +10 -12
  263. package/pennyfarthing_scripts/sprint/__main__.py +2 -2
  264. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/sprint/archive.py +0 -1
  281. package/pennyfarthing_scripts/sprint/archive_epic.py +1 -4
  282. package/pennyfarthing_scripts/sprint/cli.py +34 -28
  283. package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
  284. package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
  285. package/pennyfarthing_scripts/sprint/loader.py +6 -0
  286. package/pennyfarthing_scripts/sprint/status.py +1 -2
  287. package/pennyfarthing_scripts/sprint/story_add.py +2 -2
  288. package/pennyfarthing_scripts/sprint/story_finish.py +3 -5
  289. package/pennyfarthing_scripts/sprint/story_update.py +11 -3
  290. package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
  291. package/pennyfarthing_scripts/sprint/validator.py +120 -6
  292. package/pennyfarthing_scripts/sprint/work.py +1 -4
  293. package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
  294. package/pennyfarthing_scripts/story/__init__.py +14 -16
  295. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  296. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  297. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  298. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/story/size.py +0 -1
  302. package/pennyfarthing_scripts/story/template.py +0 -1
  303. package/pennyfarthing_scripts/swebench.py +1 -2
  304. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  305. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  306. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  307. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  308. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  309. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  310. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  311. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  312. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  313. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  314. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  315. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  316. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  317. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  318. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  319. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  320. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  321. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  322. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  323. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  324. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  325. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  326. package/pennyfarthing_scripts/tests/conftest.py +1 -2
  327. package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
  328. package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
  329. package/pennyfarthing_scripts/tests/test_codemarkers.py +13 -8
  330. package/pennyfarthing_scripts/tests/test_common.py +9 -4
  331. package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
  332. package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
  333. package/pennyfarthing_scripts/tests/test_healthscore.py +17 -25
  334. package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
  335. package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
  336. package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
  337. package/pennyfarthing_scripts/tests/test_prime.py +39 -21
  338. package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
  339. package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
  340. package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
  341. package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
  342. package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
  343. package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
  344. package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
  345. package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
  346. package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
  347. package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
  348. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  349. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  350. package/pennyfarthing_scripts/theme/cli.py +3 -2
  351. package/pennyfarthing_scripts/validate/__init__.py +21 -0
  352. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  353. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  354. package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
  355. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  356. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  357. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  358. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  359. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  360. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  361. package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
  362. package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
  363. package/pennyfarthing_scripts/validate/adapters/skill_command.py +292 -0
  364. package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
  365. package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
  366. package/pennyfarthing_scripts/validate/cli.py +141 -0
  367. package/pennyfarthing_scripts/welcome_hook.py +2 -3
  368. package/pennyfarthing_scripts/workflow.py +3 -3
  369. package/scripts/README.md +3 -15
  370. package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
  371. package/pennyfarthing-dist/commands/benchmark.md +0 -485
  372. package/pennyfarthing-dist/commands/job-fair.md +0 -102
  373. package/pennyfarthing-dist/commands/solo.md +0 -447
  374. package/pennyfarthing-dist/guides/benchmarks.md +0 -62
  375. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  376. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
  377. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
  378. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
  379. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
  380. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
  381. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
  382. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
  383. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
  384. package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
  385. package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
  386. package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
  387. package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
  388. package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
  389. package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
  390. package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
  391. package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
  392. package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
  393. package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
  394. package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
  395. package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
  396. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  397. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  398. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  399. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  400. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  401. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  402. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  403. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  404. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  405. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  406. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  407. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -7,9 +7,12 @@ Supports caching with a configurable TTL (default 5 minutes).
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import asyncio
10
11
  import hashlib
11
12
  import json
13
+ import logging
12
14
  import time
15
+ from datetime import UTC
13
16
  from pathlib import Path
14
17
 
15
18
  from pennyfarthing_scripts.healthscore.models import (
@@ -18,6 +21,8 @@ from pennyfarthing_scripts.healthscore.models import (
18
21
  HealthscoreResult,
19
22
  )
20
23
 
24
+ logger = logging.getLogger("healthscore")
25
+
21
26
 
22
27
  async def analyze_healthscore(
23
28
  target_path: Path,
@@ -36,35 +41,44 @@ async def analyze_healthscore(
36
41
  """
37
42
  w = weights if weights is not None else DEFAULT_WEIGHTS
38
43
  resolved = target_path.resolve()
44
+ logger.info("[healthscore] Starting analysis for %s", resolved)
45
+ logger.info("[healthscore] Dimensions: %s", list(w.keys()))
39
46
 
40
47
  cache_dir = get_cache_path(resolved)
41
48
  any_cached = False
42
49
  raw_scores: dict[str, float | None] = {}
43
50
  dimensions: list[DimensionScore] = []
44
51
 
45
- for dim_name, dim_weight in w.items():
46
- score: float | None = None
47
- error: str | None = None
48
-
49
- # Try cache if ttl > 0
52
+ # Separate cached vs uncached dimensions
53
+ uncached_dims: list[str] = []
54
+ for dim_name in w:
50
55
  if cache_ttl > 0:
51
56
  cached = read_cached_score(cache_dir, dim_name, cache_ttl)
52
57
  if cached is not None:
53
- score = cached
58
+ logger.info("[healthscore] %s: cached score = %.1f", dim_name, cached)
59
+ raw_scores[dim_name] = cached
54
60
  any_cached = True
55
-
56
- # If no cached value, run lightweight probe
57
- if score is None:
58
- score = _probe_dimension(dim_name, resolved)
59
- # Cache result if we got one and caching is enabled
61
+ continue
62
+ uncached_dims.append(dim_name)
63
+
64
+ logger.info("[healthscore] Uncached dimensions to probe: %s", uncached_dims)
65
+
66
+ # Run all uncached probes concurrently
67
+ if uncached_dims:
68
+ probe_results = await asyncio.gather(
69
+ *(_probe_dimension(name, resolved) for name in uncached_dims)
70
+ )
71
+ for dim_name, score in zip(uncached_dims, probe_results, strict=False):
72
+ raw_scores[dim_name] = score
73
+ logger.info("[healthscore] %s: probed score = %s", dim_name, score)
60
74
  if score is not None and cache_ttl > 0:
61
75
  cache_dir.mkdir(parents=True, exist_ok=True)
62
76
  write_cached_score(cache_dir, dim_name, score)
63
77
 
64
- if score is None:
65
- error = f"{dim_name} not available"
66
-
67
- raw_scores[dim_name] = score
78
+ # Build dimension list in original weight order
79
+ for dim_name, dim_weight in w.items():
80
+ score = raw_scores.get(dim_name)
81
+ error = f"{dim_name} not available" if score is None else None
68
82
  dimensions.append(DimensionScore(
69
83
  name=dim_name,
70
84
  score=score,
@@ -73,6 +87,7 @@ async def analyze_healthscore(
73
87
  ))
74
88
 
75
89
  composite = compute_composite_score(raw_scores, w)
90
+ logger.info("[healthscore] Composite score: %.1f", composite)
76
91
 
77
92
  return HealthscoreResult(
78
93
  success=True,
@@ -83,16 +98,431 @@ async def analyze_healthscore(
83
98
  )
84
99
 
85
100
 
86
- def _probe_dimension(name: str, target_path: Path) -> float | None:
101
+ async def _probe_dimension(name: str, target_path: Path) -> float | None:
87
102
  """Run a lightweight probe for a single dimension.
88
103
 
89
104
  Returns a score 0-100 or None if the dimension cannot be assessed.
90
- These are intentionally simple heuristics full analysis is deferred
91
- to each dimension's own module when available.
105
+ Wires into existing analyzer modules where available.
106
+ """
107
+ try:
108
+ probes = {
109
+ "churn": _probe_churn,
110
+ "todo_density": _probe_todo_density,
111
+ "complexity": _probe_complexity,
112
+ "dead_code": _probe_dead_code,
113
+ "dependency_freshness": _probe_dependency_freshness,
114
+ "deprecation_debt": _probe_deprecation_debt,
115
+ "test_gaps": _probe_test_gaps,
116
+ "agent_context_efficiency": _probe_agent_context_efficiency,
117
+ }
118
+ probe_fn = probes.get(name)
119
+ if probe_fn is None:
120
+ logger.warning("[healthscore] No probe registered for dimension: %s", name)
121
+ return None
122
+ logger.info("[healthscore] Running probe: %s", name)
123
+ result = await probe_fn(target_path)
124
+ logger.info("[healthscore] Probe %s returned: %s", name, result)
125
+ return result
126
+ except Exception as exc:
127
+ logger.error("[healthscore] Probe %s failed: %s", name, exc, exc_info=True)
128
+ return None
129
+
130
+
131
+ async def _probe_churn(target_path: Path) -> float | None:
132
+ """Score based on code churn — uses PyDriller for smart file filtering.
133
+
134
+ Falls back to existing hotspots analyzer if PyDriller is unavailable.
92
135
  """
93
- # For now, return None for all dimensions.
94
- # Each dimension will be wired to its respective analyzer in future stories.
95
- return None
136
+ try:
137
+ return await _probe_churn_pydriller(target_path)
138
+ except ImportError:
139
+ logger.info("[healthscore:churn] PyDriller not available, falling back to hotspots")
140
+ return await _probe_churn_fallback(target_path)
141
+
142
+
143
+ async def _probe_churn_pydriller(target_path: Path) -> float | None:
144
+ """PyDriller-based churn: counts changes per file with noise filtering."""
145
+ from datetime import datetime, timedelta
146
+
147
+ from pydriller import Repository
148
+
149
+ # Files that churn naturally but aren't code quality signals
150
+ noise_patterns = {
151
+ "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
152
+ "tsconfig.json", "pyproject.toml", ".gitignore",
153
+ }
154
+ noise_exts = {
155
+ ".md", ".yaml", ".yml", ".json", ".lock", ".toml",
156
+ ".png", ".jpg", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot",
157
+ ".d.ts", ".snap", ".map",
158
+ }
159
+ noise_dirs = {
160
+ "node_modules", "dist", "build", ".git", "sprint", ".session",
161
+ "docs", ".github", "coverage", "__pycache__",
162
+ }
163
+ code_exts = {".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".rb"}
164
+
165
+ since = datetime.now(UTC) - timedelta(days=90)
166
+ file_changes: dict[str, int] = {}
167
+
168
+ # PyDriller is sync — run in executor to avoid blocking
169
+ def _collect():
170
+ repo = Repository(str(target_path), since=since)
171
+ for commit in repo.traverse_commits():
172
+ for mod in commit.modified_files:
173
+ fpath = mod.new_path or mod.old_path
174
+ if not fpath:
175
+ continue
176
+ # Skip noise files
177
+ fname = fpath.split("/")[-1]
178
+ if fname in noise_patterns:
179
+ continue
180
+ ext = "." + fname.rsplit(".", 1)[-1] if "." in fname else ""
181
+ if ext.lower() in noise_exts:
182
+ continue
183
+ # Skip noise directories
184
+ if any(d in fpath.split("/") for d in noise_dirs):
185
+ continue
186
+ # Only count source code files
187
+ if ext.lower() not in code_exts:
188
+ continue
189
+
190
+ file_changes[fpath] = file_changes.get(fpath, 0) + 1
191
+ return file_changes
192
+
193
+ loop = asyncio.get_event_loop()
194
+ await loop.run_in_executor(None, _collect)
195
+
196
+ if not file_changes:
197
+ logger.info("[healthscore:churn] No code file changes in 90 days")
198
+ return 100.0 # No churn = perfect score
199
+
200
+ # Score based on top-20 most-churned files
201
+ sorted_files = sorted(file_changes.items(), key=lambda x: x[1], reverse=True)
202
+ top20 = sorted_files[:20]
203
+ max_changes = top20[0][1] if top20 else 1
204
+
205
+ # Normalize: files with many changes score higher (worse churn)
206
+ # Score each file 0-100 based on its change count relative to max
207
+ churn_scores = [(changes / max_changes) * 100.0 for _, changes in top20]
208
+ avg_churn = sum(churn_scores) / len(churn_scores)
209
+
210
+ # Invert: high churn = low health score
211
+ score = max(0.0, min(100.0, 100.0 - avg_churn))
212
+
213
+ logger.info("[healthscore:churn] PyDriller: %d code files changed, top=%s(%d), avg_churn=%.1f, score=%.1f",
214
+ len(file_changes), top20[0][0] if top20 else "?", max_changes, avg_churn, score)
215
+ for f, c in top20[:5]:
216
+ logger.info("[healthscore:churn] %s: %d changes", f, c)
217
+ return score
218
+
219
+
220
+ async def _probe_churn_fallback(target_path: Path) -> float | None:
221
+ """Fallback churn probe using existing hotspots analyzer."""
222
+ from pennyfarthing_scripts.hotspots.analyze import analyze_repo
223
+
224
+ result = await analyze_repo("project", target_path, days=90)
225
+ if not result.success or not result.file_hotspots:
226
+ logger.info("[healthscore:churn] No hotspot data (success=%s, count=%s)",
227
+ result.success, len(result.file_hotspots) if result.file_hotspots else 0)
228
+ return None
229
+ top = sorted(result.file_hotspots, key=lambda h: h.hotspot_score, reverse=True)[:20]
230
+ avg_hotspot = sum(h.hotspot_score for h in top) / len(top)
231
+ score = max(0.0, min(100.0, 100.0 - avg_hotspot))
232
+ logger.info("[healthscore:churn] fallback: top20 avg=%.1f, score=%.1f", avg_hotspot, score)
233
+ return score
234
+
235
+
236
+ async def _probe_todo_density(target_path: Path) -> float | None:
237
+ """Score based on TODO/FIXME marker count."""
238
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
239
+
240
+ result = await analyze_repo("project", target_path)
241
+ if not result.success or not result.summary:
242
+ logger.info("[healthscore:todo_density] No marker data (success=%s)", result.success)
243
+ return None
244
+ total = result.summary.total_markers
245
+ logger.info("[healthscore:todo_density] Found %d markers", total)
246
+ if total <= 10:
247
+ score = 95.0
248
+ elif total <= 50:
249
+ score = 90.0 - (total - 10) * (30.0 / 40.0)
250
+ elif total <= 200:
251
+ score = 60.0 - (total - 50) * (30.0 / 150.0)
252
+ elif total <= 1000:
253
+ score = 30.0 - (total - 200) * (25.0 / 800.0)
254
+ else:
255
+ score = max(0.0, 5.0 - (total - 1000) * 0.005)
256
+ logger.info("[healthscore:todo_density] total=%d, score=%.1f", total, score)
257
+ return score
258
+
259
+
260
+ async def _probe_complexity(target_path: Path) -> float | None:
261
+ """Score based on average cyclomatic complexity."""
262
+ from pennyfarthing_scripts.complexity.analyze import analyze_complexity
263
+
264
+ result = await analyze_complexity(target_path)
265
+ if not result.success or not result.files:
266
+ logger.info("[healthscore:complexity] No complexity data (success=%s)", result.success)
267
+ return None
268
+ files_with_fns = [f for f in result.files if f.function_count > 0]
269
+ if not files_with_fns:
270
+ logger.info("[healthscore:complexity] No files with functions found")
271
+ return None
272
+ avg = sum(f.avg_cyclomatic_complexity for f in files_with_fns) / len(files_with_fns)
273
+ if avg <= 2.0:
274
+ score = 95.0
275
+ elif avg <= 5.0:
276
+ score = 90.0 - (avg - 2.0) * (20.0 / 3.0)
277
+ elif avg <= 10.0:
278
+ score = 70.0 - (avg - 5.0) * (30.0 / 5.0)
279
+ else:
280
+ score = max(0.0, 40.0 - (avg - 10.0) * 4.0)
281
+ logger.info("[healthscore:complexity] avg=%.2f, files=%d, score=%.1f", avg, len(files_with_fns), score)
282
+ return score
283
+
284
+
285
+ async def _probe_dead_code(target_path: Path) -> float | None:
286
+ """Score based on unused export count."""
287
+ from pennyfarthing_scripts.deadcode.analyze import find_unused_exports
288
+
289
+ result = await find_unused_exports(target_path)
290
+ if not result.success:
291
+ logger.info("[healthscore:dead_code] Analysis failed")
292
+ return None
293
+ count = len(result.unused_exports)
294
+ score = max(0.0, 100.0 - count * 2.0)
295
+ logger.info("[healthscore:dead_code] unused_exports=%d, score=%.1f", count, score)
296
+ return score
297
+
298
+
299
+ async def _probe_dependency_freshness(target_path: Path) -> float | None:
300
+ """Score based on outdated dependency count."""
301
+ from pennyfarthing_scripts.dependencies.analyze import analyze_dependencies
302
+
303
+ result = await analyze_dependencies(target_path)
304
+ if not result.success:
305
+ logger.info("[healthscore:dependency_freshness] Analysis failed")
306
+ return None
307
+ outdated = len(result.outdated)
308
+ advisories = len(result.advisories)
309
+ score = max(0.0, 100.0 - outdated * 5.0 - advisories * 15.0)
310
+ logger.info("[healthscore:dependency_freshness] outdated=%d, advisories=%d, score=%.1f",
311
+ outdated, advisories, score)
312
+ return score
313
+
314
+
315
+ async def _probe_deprecation_debt(target_path: Path) -> float | None:
316
+ """Score based on @deprecated symbol count and active callers."""
317
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_deprecations
318
+
319
+ result = await analyze_deprecations(target_path)
320
+ if not result.get("success"):
321
+ logger.info("[healthscore:deprecation_debt] Analysis failed: %s", result.get("error"))
322
+ return None
323
+ summary = result.get("summary", {})
324
+ total = summary.get("total_deprecations", 0)
325
+ with_callers = summary.get("deprecations_with_callers", 0)
326
+ # Heuristic: each deprecated symbol deducts 5 points,
327
+ # each one still actively called deducts an extra 10
328
+ score = max(0.0, 100.0 - total * 5.0 - with_callers * 10.0)
329
+ logger.info("[healthscore:deprecation_debt] total=%d, with_callers=%d, score=%.1f",
330
+ total, with_callers, score)
331
+ return score
332
+
333
+
334
+ async def _probe_test_gaps(target_path: Path) -> float | None:
335
+ """Score based on ratio of testable source files with corresponding test files.
336
+
337
+ Uses directory-aware matching: for a source file like src/api/health-score.ts,
338
+ checks for tests/api/health-score.test.ts, src/api/__tests__/health-score.test.ts,
339
+ test_health_score.py, etc. Also filters out non-testable files (configs, types, index
340
+ re-exports) to avoid inflating the denominator.
341
+ """
342
+ logger.info("[healthscore:test_gaps] Scanning %s", target_path)
343
+
344
+ exclude_dirs = {"node_modules", "dist", "build", ".git", "__pycache__", ".cache",
345
+ ".pennyfarthing", "coverage", ".next", ".venv", "venv", ".session",
346
+ "sprint", "docs"}
347
+ source_exts = {".ts", ".tsx", ".js", ".jsx", ".py"}
348
+ # Files that don't need dedicated tests
349
+ non_testable_stems = {"index", "types", "constants", "config", "__init__",
350
+ "cli", "__main__", "main", "preload", "vite-env"}
351
+ non_testable_patterns = {".d.ts", ".config.ts", ".config.js", "vite.config",
352
+ "tailwind.config", "postcss.config", "jest.config",
353
+ "vitest.config", "tsconfig"}
354
+
355
+ # Collect source files as (stem_lower, rel_path) and test files as set of stem variants
356
+ source_files: list[tuple[str, str]] = []
357
+ # test_stems: set of lowered stems stripped of test prefixes/suffixes
358
+ test_stems: set[str] = set()
359
+ # test_relpaths: full relative paths of test files for directory matching
360
+ test_relpaths: set[str] = set()
361
+
362
+ for file_path in target_path.rglob("*"):
363
+ if not file_path.is_file():
364
+ continue
365
+ if file_path.suffix.lower() not in source_exts:
366
+ continue
367
+
368
+ parts = file_path.relative_to(target_path).parts
369
+ if any(p in exclude_dirs for p in parts):
370
+ continue
371
+
372
+ rel = str(file_path.relative_to(target_path))
373
+ fname = file_path.name.lower()
374
+ stem = file_path.stem.lower()
375
+ # Strip double extensions: foo.test.ts -> stem is "foo.test"
376
+ if "." in stem:
377
+ base_stem = stem.split(".")[0]
378
+ else:
379
+ base_stem = stem
380
+
381
+ is_test = (
382
+ fname.startswith("test_")
383
+ or ".test." in fname
384
+ or ".spec." in fname
385
+ or fname.endswith("_test.py")
386
+ or "__tests__" in rel
387
+ or "/tests/" in rel
388
+ or "/test/" in rel
389
+ or rel.startswith("tests/")
390
+ or rel.startswith("test/")
391
+ )
392
+
393
+ if is_test:
394
+ # Extract the tested module stem from test file name
395
+ # test_foo.py -> foo, foo.test.ts -> foo, foo.spec.tsx -> foo, foo_test.py -> foo
396
+ tested = base_stem
397
+ if tested.startswith("test_"):
398
+ tested = tested[5:]
399
+ elif tested.startswith("test"):
400
+ tested = tested[4:]
401
+ if tested.endswith("_test"):
402
+ tested = tested[:-5]
403
+ if tested:
404
+ test_stems.add(tested)
405
+ test_relpaths.add(rel.lower())
406
+ else:
407
+ # Filter out non-testable files
408
+ if base_stem in non_testable_stems:
409
+ continue
410
+ if any(p in fname for p in non_testable_patterns):
411
+ continue
412
+ source_files.append((base_stem, rel))
413
+
414
+ if not source_files:
415
+ logger.info("[healthscore:test_gaps] No testable source files found")
416
+ return None
417
+
418
+ covered = 0
419
+ uncovered_samples: list[str] = []
420
+ for src_stem, src_rel in source_files:
421
+ # Strategy 1: Direct stem match in test_stems set
422
+ if src_stem in test_stems:
423
+ covered += 1
424
+ continue
425
+
426
+ # Strategy 2: Hyphenated/underscored variants (health-score -> health_score)
427
+ normalized = src_stem.replace("-", "_")
428
+ if normalized in test_stems or normalized.replace("_", "-") in test_stems:
429
+ covered += 1
430
+ continue
431
+
432
+ # Strategy 3: Directory-aware — check if test file exists at parallel path
433
+ # src/api/health-score.ts -> tests/api/health-score.test.ts
434
+ src_lower = src_rel.lower()
435
+ src_dir = "/".join(src_lower.split("/")[:-1])
436
+ matched = False
437
+ for variant in [
438
+ f"{src_dir}/{src_stem}.test.",
439
+ f"{src_dir}/{src_stem}.spec.",
440
+ f"{src_dir}/__tests__/{src_stem}.",
441
+ ]:
442
+ if any(variant in tp for tp in test_relpaths):
443
+ matched = True
444
+ break
445
+ # Also check tests/ mirror: src/api/foo.ts -> tests/api/foo.test.ts
446
+ if not matched and src_dir:
447
+ for prefix in ["tests/", "test/"]:
448
+ for variant in [
449
+ f"{prefix}{src_dir}/{src_stem}.test.",
450
+ f"{prefix}{src_dir}/{src_stem}.spec.",
451
+ f"{prefix}{src_dir}/test_{src_stem}.",
452
+ ]:
453
+ if any(variant in tp for tp in test_relpaths):
454
+ matched = True
455
+ break
456
+ if matched:
457
+ break
458
+
459
+ if matched:
460
+ covered += 1
461
+ else:
462
+ if len(uncovered_samples) < 10:
463
+ uncovered_samples.append(src_rel)
464
+
465
+ ratio = covered / len(source_files)
466
+ if ratio >= 0.8:
467
+ score = 90.0 + (ratio - 0.8) * 50.0
468
+ elif ratio >= 0.5:
469
+ score = 60.0 + (ratio - 0.5) * 100.0
470
+ elif ratio >= 0.2:
471
+ score = 30.0 + (ratio - 0.2) * 100.0
472
+ else:
473
+ score = max(5.0, ratio * 150.0)
474
+
475
+ score = max(0.0, min(100.0, score))
476
+ logger.info("[healthscore:test_gaps] source=%d, covered=%d, ratio=%.2f, score=%.1f",
477
+ len(source_files), covered, ratio, score)
478
+ if uncovered_samples:
479
+ logger.info("[healthscore:test_gaps] Sample uncovered: %s", uncovered_samples[:5])
480
+ return score
481
+
482
+
483
+ async def _probe_agent_context_efficiency(target_path: Path) -> float | None:
484
+ """Score based on agent context token budgets.
485
+
486
+ Uses the Prime tier system to load FULL context for each agent
487
+ and scores based on how well agents stay within token budget.
488
+ Target: ~4000 tokens per agent for FULL tier.
489
+ """
490
+ from pennyfarthing_scripts.prime.tiers import ContextTier, load_tier_components
491
+
492
+ agents = ["sm", "tea", "dev", "reviewer", "architect",
493
+ "pm", "tech-writer", "ux-designer", "devops", "orchestrator"]
494
+
495
+ target_budget = 4000
496
+ scores: list[float] = []
497
+
498
+ for agent in agents:
499
+ try:
500
+ components = load_tier_components(ContextTier.FULL, agent, target_path)
501
+ total = components.get("total_tokens", 0)
502
+ if total <= 0:
503
+ logger.info("[healthscore:agent_context] %s: no tokens loaded", agent)
504
+ continue
505
+ # Score per agent: at or under budget = 100, over budget degrades linearly
506
+ # 2x budget = 0
507
+ ratio = total / target_budget
508
+ if ratio <= 1.0:
509
+ agent_score = 100.0
510
+ else:
511
+ agent_score = max(0.0, 100.0 - (ratio - 1.0) * 100.0)
512
+ logger.info("[healthscore:agent_context] %s: %d tokens (%.1f%% of budget), score=%.1f",
513
+ agent, total, ratio * 100, agent_score)
514
+ scores.append(agent_score)
515
+ except Exception as exc:
516
+ logger.warning("[healthscore:agent_context] %s failed: %s", agent, exc)
517
+ continue
518
+
519
+ if not scores:
520
+ logger.info("[healthscore:agent_context] No agent scores collected")
521
+ return None
522
+
523
+ avg = sum(scores) / len(scores)
524
+ logger.info("[healthscore:agent_context] %d agents scored, avg=%.1f", len(scores), avg)
525
+ return avg
96
526
 
97
527
 
98
528
  def compute_composite_score(
@@ -9,9 +9,13 @@ from __future__ import annotations
9
9
 
10
10
  import asyncio
11
11
  from pathlib import Path
12
+ from typing import TYPE_CHECKING
12
13
 
13
14
  import click
14
15
 
16
+ if TYPE_CHECKING:
17
+ from pennyfarthing_scripts.healthscore.models import HealthscoreResult
18
+
15
19
 
16
20
  @click.group()
17
21
  def healthscore():
@@ -37,7 +41,7 @@ def _common_options(fn):
37
41
  return fn
38
42
 
39
43
 
40
- def _run_analysis(target_path: str | None, no_cache: bool) -> "HealthscoreResult":
44
+ def _run_analysis(target_path: str | None, no_cache: bool) -> HealthscoreResult:
41
45
  """Run analysis and return result."""
42
46
  from pennyfarthing_scripts.healthscore.analyze import analyze_healthscore
43
47
 
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  from dataclasses import dataclass, field
10
10
 
11
-
12
11
  # Default weights for each dimension (must sum to 1.0)
13
12
  DEFAULT_WEIGHTS: dict[str, float] = {
14
13
  "churn": 0.15,
@@ -16,15 +16,14 @@ Story: MSSCI-12409 - Hook consistency and relay mode compatibility
16
16
  import json
17
17
  import os
18
18
  import sys
19
- import urllib.request
20
19
  import urllib.error
20
+ import urllib.request
21
21
  from dataclasses import dataclass
22
22
  from pathlib import Path
23
23
  from typing import Any
24
24
 
25
25
  import yaml
26
26
 
27
-
28
27
  # =============================================================================
29
28
  # Port File Constants
30
29
  # =============================================================================
@@ -413,19 +412,17 @@ def read_stdin_json() -> dict[str, Any]:
413
412
  def is_cyclist_running(project_root: Path | None = None) -> bool:
414
413
  """Check if Cyclist server is running.
415
414
 
416
- Checks for .cyclist-port file existence. No HTTP calls — this runs on
417
- every tool invocation and must be fast.
415
+ Checks the CYCLIST environment variable set by ClaudeService when
416
+ spawning Claude inside Cyclist. No file I/O, no HTTP, no signals —
417
+ this runs on every tool invocation and must be instant.
418
418
 
419
- Args:
420
- project_root: Project root directory (auto-detected if not provided)
419
+ The project_root parameter is kept for backward compatibility but
420
+ is no longer used.
421
421
 
422
422
  Returns:
423
- True if .cyclist-port file exists at project root
423
+ True if running inside a Cyclist-spawned Claude process
424
424
  """
425
- root = project_root or find_project_root()
426
- if not root:
427
- return False
428
- return (root / CYCLIST_PORT_FILE).exists()
425
+ return os.environ.get("CYCLIST") == "1"
429
426
 
430
427
 
431
428
  def should_auto_approve(settings: CyclistSettings) -> bool:
@@ -6,18 +6,18 @@ bug fix concentration, and multi-author churn — indicators of code hotspots th
6
6
  may benefit from refactoring attention.
7
7
  """
8
8
 
9
+ from pennyfarthing_scripts.hotspots.analyze import (
10
+ analyze_all_repos,
11
+ analyze_repo,
12
+ calculate_hotspot_score,
13
+ is_bug_fix_commit,
14
+ )
9
15
  from pennyfarthing_scripts.hotspots.models import (
10
16
  DirectoryHotspot,
11
17
  FileHotspot,
12
18
  HotspotResult,
13
19
  MultiRepoHotspotResult,
14
20
  )
15
- from pennyfarthing_scripts.hotspots.analyze import (
16
- analyze_repo,
17
- analyze_all_repos,
18
- calculate_hotspot_score,
19
- is_bug_fix_commit,
20
- )
21
21
 
22
22
  __all__ = [
23
23
  "DirectoryHotspot",