@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,591 @@
1
+ """
2
+ Core health score analysis engine.
3
+
4
+ Aggregates lightweight dimension scores into a composite 0-100 score.
5
+ Supports caching with a configurable TTL (default 5 minutes).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import hashlib
12
+ import json
13
+ import logging
14
+ import time
15
+ from datetime import UTC
16
+ from pathlib import Path
17
+
18
+ from pennyfarthing_scripts.healthscore.models import (
19
+ DEFAULT_WEIGHTS,
20
+ DimensionScore,
21
+ HealthscoreResult,
22
+ )
23
+
24
+ logger = logging.getLogger("healthscore")
25
+
26
+
27
+ async def analyze_healthscore(
28
+ target_path: Path,
29
+ weights: dict[str, float] | None = None,
30
+ cache_ttl: int = 300,
31
+ ) -> HealthscoreResult:
32
+ """Analyze codebase health across all dimensions.
33
+
34
+ Args:
35
+ target_path: Directory to analyze.
36
+ weights: Custom dimension weights (must sum to 1.0). Uses defaults if None.
37
+ cache_ttl: Cache time-to-live in seconds (default 300 = 5 minutes).
38
+
39
+ Returns:
40
+ HealthscoreResult with composite score and per-dimension breakdown.
41
+ """
42
+ w = weights if weights is not None else DEFAULT_WEIGHTS
43
+ resolved = target_path.resolve()
44
+ logger.info("[healthscore] Starting analysis for %s", resolved)
45
+ logger.info("[healthscore] Dimensions: %s", list(w.keys()))
46
+
47
+ cache_dir = get_cache_path(resolved)
48
+ any_cached = False
49
+ raw_scores: dict[str, float | None] = {}
50
+ dimensions: list[DimensionScore] = []
51
+
52
+ # Separate cached vs uncached dimensions
53
+ uncached_dims: list[str] = []
54
+ for dim_name in w:
55
+ if cache_ttl > 0:
56
+ cached = read_cached_score(cache_dir, dim_name, cache_ttl)
57
+ if cached is not None:
58
+ logger.info("[healthscore] %s: cached score = %.1f", dim_name, cached)
59
+ raw_scores[dim_name] = cached
60
+ any_cached = True
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)
74
+ if score is not None and cache_ttl > 0:
75
+ cache_dir.mkdir(parents=True, exist_ok=True)
76
+ write_cached_score(cache_dir, dim_name, score)
77
+
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
82
+ dimensions.append(DimensionScore(
83
+ name=dim_name,
84
+ score=score,
85
+ weight=dim_weight,
86
+ error=error,
87
+ ))
88
+
89
+ composite = compute_composite_score(raw_scores, w)
90
+ logger.info("[healthscore] Composite score: %.1f", composite)
91
+
92
+ return HealthscoreResult(
93
+ success=True,
94
+ composite_score=composite,
95
+ target_path=str(resolved),
96
+ dimensions=dimensions,
97
+ cached=any_cached,
98
+ )
99
+
100
+
101
+ async def _probe_dimension(name: str, target_path: Path) -> float | None:
102
+ """Run a lightweight probe for a single dimension.
103
+
104
+ Returns a score 0-100 or None if the dimension cannot be assessed.
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.
135
+ """
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
526
+
527
+
528
+ def compute_composite_score(
529
+ dimension_scores: dict[str, float | None],
530
+ weights: dict[str, float],
531
+ ) -> float:
532
+ """Compute weighted average from dimension scores.
533
+
534
+ Dimensions with None scores are excluded and remaining weights
535
+ are renormalized.
536
+
537
+ Args:
538
+ dimension_scores: Map of dimension name to score (0-100 or None).
539
+ weights: Map of dimension name to weight.
540
+
541
+ Returns:
542
+ Composite score 0-100.
543
+ """
544
+ total_weight = 0.0
545
+ weighted_sum = 0.0
546
+
547
+ for name, score in dimension_scores.items():
548
+ if score is not None:
549
+ w = weights.get(name, 0.0)
550
+ weighted_sum += score * w
551
+ total_weight += w
552
+
553
+ if total_weight == 0.0:
554
+ return 0.0
555
+
556
+ return weighted_sum / total_weight
557
+
558
+
559
+ def get_cache_path(target_path: Path) -> Path:
560
+ """Return the cache directory for a given target path."""
561
+ path_hash = hashlib.md5(str(target_path).encode()).hexdigest()[:12]
562
+ return target_path / ".pennyfarthing" / ".cache" / "healthscore" / path_hash
563
+
564
+
565
+ def read_cached_score(cache_dir: Path, dimension: str, ttl: int) -> float | None:
566
+ """Read a cached dimension score if still valid.
567
+
568
+ Returns None if cache miss or expired.
569
+ """
570
+ cache_file = cache_dir / f"{dimension}.json"
571
+ if not cache_file.exists():
572
+ return None
573
+
574
+ try:
575
+ data = json.loads(cache_file.read_text())
576
+ except (json.JSONDecodeError, OSError):
577
+ return None
578
+
579
+ ts = data.get("timestamp", 0)
580
+ if ttl <= 0 or (time.time() - ts) > ttl:
581
+ return None
582
+
583
+ return data.get("score")
584
+
585
+
586
+ def write_cached_score(cache_dir: Path, dimension: str, score: float) -> None:
587
+ """Write a dimension score to cache."""
588
+ cache_dir.mkdir(parents=True, exist_ok=True)
589
+ cache_file = cache_dir / f"{dimension}.json"
590
+ data = {"score": score, "timestamp": time.time()}
591
+ cache_file.write_text(json.dumps(data))
@@ -0,0 +1,80 @@
1
+ """
2
+ CLI commands for health score analysis.
3
+
4
+ Usage:
5
+ pf healthscore analyze [OPTIONS]
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING
13
+
14
+ import click
15
+
16
+ if TYPE_CHECKING:
17
+ from pennyfarthing_scripts.healthscore.models import HealthscoreResult
18
+
19
+
20
+ @click.group()
21
+ def healthscore():
22
+ """Composite codebase health score.
23
+
24
+ \b
25
+ Commands:
26
+ analyze - Compute health score across all dimensions
27
+ """
28
+ pass
29
+
30
+
31
+ def _common_options(fn):
32
+ """Shared options for healthscore commands."""
33
+ fn = click.option("--path", "target_path", type=click.Path(exists=True),
34
+ help="Directory to analyze")(fn)
35
+ fn = click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]),
36
+ default="table", show_default=True)(fn)
37
+ fn = click.option("--output", "output_file", type=click.Path(),
38
+ help="Write output to file")(fn)
39
+ fn = click.option("--no-cache", is_flag=True,
40
+ help="Bypass cache, force fresh analysis")(fn)
41
+ return fn
42
+
43
+
44
+ def _run_analysis(target_path: str | None, no_cache: bool) -> HealthscoreResult:
45
+ """Run analysis and return result."""
46
+ from pennyfarthing_scripts.healthscore.analyze import analyze_healthscore
47
+
48
+ p = Path(target_path).resolve() if target_path else Path(".").resolve()
49
+ cache_ttl = 0 if no_cache else 300
50
+ return asyncio.run(analyze_healthscore(p, cache_ttl=cache_ttl))
51
+
52
+
53
+ def _output_result(result, fmt: str, output_file: str | None):
54
+ """Format and output the analysis result."""
55
+ from pennyfarthing_scripts.healthscore.formatters import (
56
+ export_csv,
57
+ export_json,
58
+ format_table,
59
+ )
60
+
61
+ if fmt == "json":
62
+ text = export_json(result)
63
+ elif fmt == "csv":
64
+ text = export_csv(result)
65
+ else:
66
+ text = format_table(result)
67
+
68
+ if output_file:
69
+ Path(output_file).write_text(text)
70
+ click.echo(f"Output written to {output_file}", err=True)
71
+ else:
72
+ click.echo(text)
73
+
74
+
75
+ @healthscore.command()
76
+ @_common_options
77
+ def analyze(target_path, fmt, output_file, no_cache):
78
+ """Compute composite health score."""
79
+ result = _run_analysis(target_path, no_cache)
80
+ _output_result(result, fmt, output_file)