@pennyfarthing/core 10.1.0 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/README.md +22 -24
  2. package/package.json +3 -1
  3. package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js +24 -0
  5. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +101 -15
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js +2 -2
  10. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js.map +1 -1
  11. package/packages/core/dist/cli/commands/e2e-upgrade.test.js +2 -2
  12. package/packages/core/dist/cli/commands/e2e-upgrade.test.js.map +1 -1
  13. package/packages/core/dist/cli/commands/hooks-consolidation.test.js +2 -2
  14. package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -1
  15. package/packages/core/dist/cli/commands/init-consolidation.test.js.map +1 -1
  16. package/packages/core/dist/cli/commands/theme.js +1 -1
  17. package/packages/core/dist/cli/commands/theme.js.map +1 -1
  18. package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
  19. package/packages/core/dist/cli/commands/uninstall.js +24 -13
  20. package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
  21. package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
  22. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  23. package/packages/core/dist/cli/commands/update.js.map +1 -1
  24. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  25. package/packages/core/dist/cli/theme-maker.test.js +64 -115
  26. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  27. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
  28. package/packages/core/dist/cli/utils/themes.js +3 -2
  29. package/packages/core/dist/cli/utils/themes.js.map +1 -1
  30. package/packages/core/dist/index.d.ts +1 -1
  31. package/packages/core/dist/index.d.ts.map +1 -1
  32. package/packages/core/dist/index.js +2 -2
  33. package/packages/core/dist/index.js.map +1 -1
  34. package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
  35. package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
  36. package/packages/core/dist/plugins/plugin-discovery.js +165 -0
  37. package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
  38. package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
  39. package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
  40. package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
  41. package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
  42. package/packages/core/dist/scripts/add-ocean-profiles.js +1 -1
  43. package/packages/core/dist/scripts/add-ocean-profiles.js.map +1 -1
  44. package/packages/core/dist/scripts/generate-all-spiders.js +2 -0
  45. package/packages/core/dist/scripts/generate-all-spiders.js.map +1 -1
  46. package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
  47. package/packages/core/dist/scripts/generate-report.js +2 -0
  48. package/packages/core/dist/scripts/generate-report.js.map +1 -1
  49. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  50. package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
  51. package/packages/core/dist/scripts/generate-spider.js +2 -0
  52. package/packages/core/dist/scripts/generate-spider.js.map +1 -1
  53. package/packages/core/dist/scripts/validate-ocean-profiles.js +1 -1
  54. package/packages/core/dist/scripts/validate-ocean-profiles.js.map +1 -1
  55. package/packages/core/dist/workflow/file-watch.d.ts +82 -0
  56. package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
  57. package/packages/core/dist/workflow/file-watch.js +198 -0
  58. package/packages/core/dist/workflow/file-watch.js.map +1 -0
  59. package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
  60. package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
  61. package/packages/core/dist/workflow/file-watch.test.js +469 -0
  62. package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
  63. package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
  64. package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
  65. package/packages/core/dist/workflow/observation-writer.js +97 -0
  66. package/packages/core/dist/workflow/observation-writer.js.map +1 -0
  67. package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
  68. package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
  69. package/packages/core/dist/workflow/observation-writer.test.js +424 -0
  70. package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
  71. package/packages/core/dist/workflow/output-path-normalizer.d.ts +47 -0
  72. package/packages/core/dist/workflow/output-path-normalizer.d.ts.map +1 -0
  73. package/packages/core/dist/workflow/output-path-normalizer.js +79 -0
  74. package/packages/core/dist/workflow/output-path-normalizer.js.map +1 -0
  75. package/packages/core/dist/workflow/output-path-normalizer.test.d.ts +16 -0
  76. package/packages/core/dist/workflow/output-path-normalizer.test.d.ts.map +1 -0
  77. package/packages/core/dist/workflow/output-path-normalizer.test.js +157 -0
  78. package/packages/core/dist/workflow/output-path-normalizer.test.js.map +1 -0
  79. package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
  80. package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
  81. package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
  82. package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
  83. package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
  84. package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
  85. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
  86. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
  87. package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
  88. package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
  89. package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
  90. package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
  91. package/packages/core/dist/workflow/tool-watch.js +166 -0
  92. package/packages/core/dist/workflow/tool-watch.js.map +1 -0
  93. package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
  94. package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
  95. package/packages/core/dist/workflow/tool-watch.test.js +717 -0
  96. package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
  97. package/packages/core/dist/workflow/variable-resolver.js +1 -1
  98. package/packages/core/dist/workflow/variable-resolver.js.map +1 -1
  99. package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
  100. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  101. package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
  102. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  103. package/packages/core/dist/workflow/workflow-schema.js +44 -0
  104. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  105. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  106. package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
  107. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  108. package/pennyfarthing-dist/agents/README.md +3 -1
  109. package/pennyfarthing-dist/agents/ba.md +165 -0
  110. package/pennyfarthing-dist/agents/handoff.md +18 -3
  111. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  112. package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
  113. package/pennyfarthing-dist/agents/sm.md +11 -5
  114. package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
  115. package/pennyfarthing-dist/commands/ba.md +17 -0
  116. package/pennyfarthing-dist/commands/setup.md +4 -0
  117. package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
  118. package/pennyfarthing-dist/guides/bikelane.md +3 -2
  119. package/pennyfarthing-dist/guides/scale-levels.md +4 -6
  120. package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
  121. package/pennyfarthing-dist/guides/workflow-schema.md +1 -1
  122. package/pennyfarthing-dist/personas/themes/a-team.yaml +30 -0
  123. package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +30 -0
  124. package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +30 -0
  125. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +30 -0
  126. package/pennyfarthing-dist/personas/themes/catch-22.yaml +30 -0
  127. package/pennyfarthing-dist/personas/themes/control.yaml +30 -0
  128. package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +31 -0
  129. package/pennyfarthing-dist/personas/themes/discworld.yaml +32 -1
  130. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +31 -0
  131. package/pennyfarthing-dist/personas/themes/dune.yaml +32 -0
  132. package/pennyfarthing-dist/personas/themes/fifth-element.yaml +327 -0
  133. package/pennyfarthing-dist/personas/themes/firefly.yaml +31 -0
  134. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +30 -0
  135. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +30 -0
  136. package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +30 -0
  137. package/pennyfarthing-dist/personas/themes/lord-of-the-rings.yaml +30 -0
  138. package/pennyfarthing-dist/personas/themes/mad-max.yaml +30 -0
  139. package/pennyfarthing-dist/personas/themes/mash.yaml +33 -0
  140. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +34 -0
  141. package/pennyfarthing-dist/personas/themes/sandman.yaml +33 -0
  142. package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +34 -0
  143. package/pennyfarthing-dist/personas/themes/star-wars.yaml +33 -0
  144. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +30 -0
  145. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +30 -0
  146. package/pennyfarthing-dist/personas/themes/watchmen.yaml +30 -0
  147. package/pennyfarthing-dist/personas/themes/west-wing.yaml +30 -0
  148. package/pennyfarthing-dist/personas/themes/x-files.yaml +30 -0
  149. package/pennyfarthing-dist/scripts/README.md +1 -1
  150. package/pennyfarthing-dist/scripts/core/agent-session.sh +1 -1
  151. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
  152. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +20 -10
  153. package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
  154. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +2 -2
  155. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +1 -0
  156. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  157. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -189
  158. package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
  159. package/pennyfarthing-dist/skills/skill-registry.yaml +1 -1
  160. package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
  161. package/pennyfarthing-dist/skills/theme/skill.md +1 -1
  162. package/pennyfarthing-dist/skills/workflow/skill.md +24 -1
  163. package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
  164. package/pennyfarthing-dist/workflows/architecture.yaml +2 -2
  165. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
  166. package/pennyfarthing-dist/workflows/epics-and-stories/workflow.yaml +2 -2
  167. package/pennyfarthing-dist/workflows/implementation-readiness/workflow.yaml +2 -2
  168. package/pennyfarthing-dist/workflows/prd/workflow.yaml +2 -2
  169. package/pennyfarthing-dist/workflows/product-brief/workflow.yaml +2 -2
  170. package/pennyfarthing-dist/workflows/project-context/workflow.yaml +2 -2
  171. package/pennyfarthing-dist/workflows/quick-dev/workflow.yaml +2 -2
  172. package/pennyfarthing-dist/workflows/research/workflow.yaml +2 -2
  173. package/pennyfarthing-dist/workflows/retrospective/workflow.yaml +1 -1
  174. package/pennyfarthing-dist/workflows/sprint-planning/workflow.yaml +3 -3
  175. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
  176. package/pennyfarthing-dist/workflows/ux-design/workflow.yaml +2 -2
  177. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/bellmode_hook.py +202 -47
  183. package/pennyfarthing_scripts/bikerack/__init__.py +36 -0
  184. package/pennyfarthing_scripts/bikerack/__main__.py +5 -0
  185. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/bikerack/cli.py +148 -0
  190. package/pennyfarthing_scripts/bikerack/launcher.py +181 -0
  191. package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
  192. package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
  193. package/pennyfarthing_scripts/brownfield/cli.py +0 -1
  194. package/pennyfarthing_scripts/brownfield/discover.py +1 -2
  195. package/pennyfarthing_scripts/cli.py +16 -6
  196. package/pennyfarthing_scripts/codemarkers/__init__.py +5 -1
  197. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/codemarkers/analyze.py +177 -2
  204. package/pennyfarthing_scripts/codemarkers/cli.py +50 -0
  205. package/pennyfarthing_scripts/codemarkers/formatters.py +0 -1
  206. package/pennyfarthing_scripts/codemarkers/models.py +15 -0
  207. package/pennyfarthing_scripts/common/__init__.py +8 -9
  208. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/common/config.py +1 -1
  211. package/pennyfarthing_scripts/complexity/__init__.py +1 -1
  212. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/complexity/analyze.py +1 -1
  219. package/pennyfarthing_scripts/complexity/cli.py +5 -1
  220. package/pennyfarthing_scripts/complexity/formatters.py +1 -1
  221. package/pennyfarthing_scripts/context.py +14 -15
  222. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/deadcode/analyze.py +3 -4
  229. package/pennyfarthing_scripts/deadcode/cli.py +2 -2
  230. package/pennyfarthing_scripts/dependencies/__init__.py +2 -2
  231. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/dependencies/analyze.py +1 -1
  238. package/pennyfarthing_scripts/dependencies/cli.py +8 -4
  239. package/pennyfarthing_scripts/dependencies/formatters.py +1 -1
  240. package/pennyfarthing_scripts/git/__init__.py +5 -5
  241. package/pennyfarthing_scripts/git/create_branches.py +3 -2
  242. package/pennyfarthing_scripts/git/status_all.py +1 -1
  243. package/pennyfarthing_scripts/healthscore/__init__.py +2 -2
  244. package/pennyfarthing_scripts/healthscore/__main__.py +8 -0
  245. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/healthscore/analyze.py +452 -21
  252. package/pennyfarthing_scripts/healthscore/cli.py +5 -1
  253. package/pennyfarthing_scripts/healthscore/models.py +0 -1
  254. package/pennyfarthing_scripts/hooks.py +8 -11
  255. package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
  256. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/hotspots/analyze.py +128 -14
  261. package/pennyfarthing_scripts/hotspots/cli.py +2 -2
  262. package/pennyfarthing_scripts/hotspots/models.py +0 -1
  263. package/pennyfarthing_scripts/jira/__init__.py +15 -17
  264. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/jira/bidirectional.py +2 -3
  275. package/pennyfarthing_scripts/jira/claim.py +21 -0
  276. package/pennyfarthing_scripts/jira/cli.py +2 -2
  277. package/pennyfarthing_scripts/jira/client.py +4 -4
  278. package/pennyfarthing_scripts/jira/create.py +45 -1
  279. package/pennyfarthing_scripts/jira/epic.py +3 -2
  280. package/pennyfarthing_scripts/jira/reconcile.py +0 -1
  281. package/pennyfarthing_scripts/jira/story.py +2 -0
  282. package/pennyfarthing_scripts/jira/sync.py +1 -1
  283. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/migration/skill.py +0 -1
  289. package/pennyfarthing_scripts/migration/step.py +0 -1
  290. package/pennyfarthing_scripts/migration/validate.py +8 -5
  291. package/pennyfarthing_scripts/patch_mode.py +2 -2
  292. package/pennyfarthing_scripts/preflight/__init__.py +1 -1
  293. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  294. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  295. package/pennyfarthing_scripts/preflight/finish.py +0 -1
  296. package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
  297. package/pennyfarthing_scripts/prime/__init__.py +2 -0
  298. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/prime/cli.py +18 -1
  304. package/pennyfarthing_scripts/prime/loader.py +72 -3
  305. package/pennyfarthing_scripts/prime/persona.py +4 -2
  306. package/pennyfarthing_scripts/prime/tiers.py +17 -4
  307. package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
  308. package/pennyfarthing_scripts/sprint/__init__.py +10 -12
  309. package/pennyfarthing_scripts/sprint/__main__.py +2 -2
  310. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  315. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  316. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  317. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  318. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  319. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  320. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  323. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  324. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/sprint/archive.py +0 -1
  326. package/pennyfarthing_scripts/sprint/archive_epic.py +1 -4
  327. package/pennyfarthing_scripts/sprint/cli.py +34 -28
  328. package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
  329. package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
  330. package/pennyfarthing_scripts/sprint/loader.py +6 -0
  331. package/pennyfarthing_scripts/sprint/status.py +1 -2
  332. package/pennyfarthing_scripts/sprint/story_add.py +2 -2
  333. package/pennyfarthing_scripts/sprint/story_finish.py +3 -5
  334. package/pennyfarthing_scripts/sprint/story_update.py +11 -3
  335. package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
  336. package/pennyfarthing_scripts/sprint/validator.py +120 -6
  337. package/pennyfarthing_scripts/sprint/work.py +1 -4
  338. package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
  339. package/pennyfarthing_scripts/story/__init__.py +14 -16
  340. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/story/size.py +0 -1
  344. package/pennyfarthing_scripts/story/template.py +0 -1
  345. package/pennyfarthing_scripts/swebench.py +1 -2
  346. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  347. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  348. package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/conftest.py +1 -2
  353. package/pennyfarthing_scripts/tests/test_bikerack.py +785 -0
  354. package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
  355. package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
  356. package/pennyfarthing_scripts/tests/test_codemarkers.py +13 -8
  357. package/pennyfarthing_scripts/tests/test_common.py +9 -4
  358. package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
  359. package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
  360. package/pennyfarthing_scripts/tests/test_healthscore.py +17 -25
  361. package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
  362. package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
  363. package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
  364. package/pennyfarthing_scripts/tests/test_prime.py +39 -21
  365. package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
  366. package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
  367. package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
  368. package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
  369. package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
  370. package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
  371. package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
  372. package/pennyfarthing_scripts/tests/test_topology_loader.py +620 -0
  373. package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
  374. package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
  375. package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
  376. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  377. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  378. package/pennyfarthing_scripts/theme/cli.py +3 -2
  379. package/pennyfarthing_scripts/validate/__init__.py +21 -0
  380. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  381. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  382. package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
  383. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  384. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  385. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  386. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  387. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  388. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  389. package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
  390. package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
  391. package/pennyfarthing_scripts/validate/adapters/skill_command.py +291 -0
  392. package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
  393. package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
  394. package/pennyfarthing_scripts/validate/cli.py +141 -0
  395. package/pennyfarthing_scripts/welcome_hook.py +2 -3
  396. package/pennyfarthing_scripts/workflow.py +3 -3
  397. package/scripts/README.md +3 -15
  398. package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
  399. package/pennyfarthing-dist/commands/benchmark.md +0 -485
  400. package/pennyfarthing-dist/commands/job-fair.md +0 -102
  401. package/pennyfarthing-dist/commands/solo.md +0 -447
  402. package/pennyfarthing-dist/guides/benchmarks.md +0 -62
  403. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
  404. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
  405. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
  406. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
  407. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
  408. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
  409. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
  410. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
  411. package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
  412. package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
  413. package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
  414. package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
  415. package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
  416. package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
  417. package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
  418. package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
  419. package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
  420. package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
  421. package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
  422. package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
@@ -0,0 +1,181 @@
1
+ """BikeRack launcher — core lifecycle functions.
2
+
3
+ Story 101-5: BikeRack launcher CLI (pf bikerack start/stop/status)
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import atexit
9
+ import os
10
+ import signal
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+ from typing import NoReturn
15
+
16
+
17
+ def is_process_alive(pid: int) -> bool:
18
+ """Check if a process with given PID is alive."""
19
+ try:
20
+ os.kill(pid, 0)
21
+ return True
22
+ except (ProcessLookupError, PermissionError, OSError):
23
+ return False
24
+
25
+
26
+ def cleanup_files(project_dir: Path) -> None:
27
+ """Clean up .bikerack-port and .bikerack-pid files."""
28
+ for name in (".bikerack-port", ".bikerack-pid"):
29
+ try:
30
+ (project_dir / name).unlink()
31
+ except FileNotFoundError:
32
+ pass
33
+
34
+
35
+ def read_port_file(project_dir: Path) -> int | None:
36
+ """Read port from .bikerack-port file. Returns None if not found."""
37
+ try:
38
+ return int((project_dir / ".bikerack-port").read_text().strip())
39
+ except (FileNotFoundError, ValueError):
40
+ return None
41
+
42
+
43
+ def read_pid_file(project_dir: Path) -> int | None:
44
+ """Read PID from .bikerack-pid file. Returns None if not found."""
45
+ try:
46
+ return int((project_dir / ".bikerack-pid").read_text().strip())
47
+ except (FileNotFoundError, ValueError):
48
+ return None
49
+
50
+
51
+ def write_pid_file(project_dir: Path, pid: int) -> None:
52
+ """Write .bikerack-pid file."""
53
+ (project_dir / ".bikerack-pid").write_text(str(pid))
54
+
55
+
56
+ def build_otel_env(port: int) -> dict[str, str]:
57
+ """Build the 5 OTEL environment variables from discovered port (Rule 5)."""
58
+ return {
59
+ "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
60
+ "OTEL_LOGS_EXPORTER": "otlp",
61
+ "OTEL_METRICS_EXPORTER": "otlp",
62
+ "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
63
+ "OTEL_EXPORTER_OTLP_ENDPOINT": f"http://localhost:{port}",
64
+ }
65
+
66
+
67
+ def _find_framework_dir() -> Path:
68
+ """Locate the pennyfarthing framework root from this package's location."""
69
+ # pennyfarthing_scripts/bikerack/launcher.py -> pennyfarthing/
70
+ return Path(__file__).resolve().parent.parent.parent
71
+
72
+
73
+ def start_wheelhub(project_dir: Path) -> subprocess.Popen:
74
+ """Start WheelHub server in background with IS_BIKERACK=1."""
75
+ framework_dir = _find_framework_dir()
76
+ bikerack_entry = framework_dir / "packages" / "cyclist" / "dist" / "bikerack.js"
77
+
78
+ env = os.environ.copy()
79
+ env["IS_BIKERACK"] = "1"
80
+ env["CYCLIST_PROJECT_DIR"] = str(project_dir)
81
+
82
+ return subprocess.Popen(
83
+ ["node", str(bikerack_entry)],
84
+ env=env,
85
+ cwd=str(project_dir),
86
+ stdout=subprocess.DEVNULL,
87
+ stderr=subprocess.DEVNULL,
88
+ )
89
+
90
+
91
+ def poll_for_port_file(
92
+ project_dir: Path, timeout: float = 5.0, interval: float = 0.1
93
+ ) -> int:
94
+ """Poll for .bikerack-port file, return port number."""
95
+ port_file = project_dir / ".bikerack-port"
96
+ deadline = time.monotonic() + timeout
97
+
98
+ while True:
99
+ if port_file.exists():
100
+ return int(port_file.read_text().strip())
101
+ if time.monotonic() >= deadline:
102
+ raise TimeoutError(
103
+ f"Timed out waiting for {port_file} after {timeout}s"
104
+ )
105
+ time.sleep(interval)
106
+
107
+
108
+ def register_cleanup(project_dir: Path, pid: int) -> None:
109
+ """Register atexit handler to kill WheelHub and clean up files (Rule 8)."""
110
+
111
+ def _cleanup(project_dir: Path, pid: int) -> None:
112
+ try:
113
+ os.kill(pid, signal.SIGTERM)
114
+ except (ProcessLookupError, OSError):
115
+ pass
116
+ cleanup_files(project_dir)
117
+
118
+ atexit.register(_cleanup, project_dir, pid)
119
+
120
+
121
+ def exec_claude(otel_env: dict[str, str], project_dir: Path | None = None) -> NoReturn:
122
+ """Replace current process with Claude CLI using os.execvpe (CE-4)."""
123
+ env = os.environ.copy()
124
+ env.update(otel_env)
125
+ if project_dir:
126
+ os.chdir(project_dir)
127
+ os.execvpe("claude", ["claude"], env)
128
+
129
+
130
+ def is_already_running(project_dir: Path) -> tuple[bool, int | None, int | None]:
131
+ """Check if BikeRack is already running.
132
+
133
+ Returns (is_running, pid_or_none, port_or_none).
134
+ Cleans up stale files if PID is dead.
135
+ """
136
+ pid = read_pid_file(project_dir)
137
+ port = read_port_file(project_dir)
138
+
139
+ if pid is None or port is None:
140
+ return (False, None, None)
141
+
142
+ if is_process_alive(pid):
143
+ return (True, pid, port)
144
+
145
+ cleanup_files(project_dir)
146
+ return (False, None, None)
147
+
148
+
149
+ def stop_bikerack(project_dir: Path) -> dict:
150
+ """Stop running BikeRack instance. Returns {success, pid, message}."""
151
+ pid = read_pid_file(project_dir)
152
+
153
+ if pid is None:
154
+ return {"success": False, "message": "BikeRack is not running"}
155
+
156
+ if not is_process_alive(pid):
157
+ cleanup_files(project_dir)
158
+ return {"success": False, "message": "BikeRack is not running (stale PID)"}
159
+
160
+ os.kill(pid, signal.SIGTERM)
161
+ cleanup_files(project_dir)
162
+ return {"success": True, "pid": pid, "message": f"Stopped BikeRack (PID {pid})"}
163
+
164
+
165
+ def get_status(project_dir: Path) -> dict:
166
+ """Get BikeRack running status. Returns {running, pid, port, dashboard}."""
167
+ pid = read_pid_file(project_dir)
168
+ port = read_port_file(project_dir)
169
+
170
+ if pid is None or port is None:
171
+ return {"running": False}
172
+
173
+ if not is_process_alive(pid):
174
+ return {"running": False}
175
+
176
+ return {
177
+ "running": True,
178
+ "pid": pid,
179
+ "port": port,
180
+ "dashboard": f"http://localhost:{port}/bikerack",
181
+ }
@@ -6,17 +6,17 @@ Analyzes existing codebases and generates AI-ready documentation.
6
6
 
7
7
  from pennyfarthing_scripts.brownfield.discover import (
8
8
  DepthLevel,
9
- ProjectType,
10
9
  DiscoveryResult,
10
+ ProjectType,
11
+ detect_architecture_patterns,
11
12
  detect_project_type,
12
13
  detect_tech_stack,
13
- scan_directory_structure,
14
- detect_architecture_patterns,
14
+ discover,
15
+ generate_ai_guidance_doc,
15
16
  generate_project_overview,
16
- generate_tech_stack_doc,
17
17
  generate_source_tree_doc,
18
- generate_ai_guidance_doc,
19
- discover,
18
+ generate_tech_stack_doc,
19
+ scan_directory_structure,
20
20
  )
21
21
 
22
22
  __all__ = [
@@ -1,6 +1,7 @@
1
1
  """Entry point for python -m pennyfarthing_scripts.brownfield."""
2
2
 
3
3
  import sys
4
+
4
5
  from pennyfarthing_scripts.brownfield.cli import cli
5
6
 
6
7
  if __name__ == "__main__":
@@ -12,7 +12,6 @@ import argparse
12
12
  import asyncio
13
13
  import sys
14
14
  from pathlib import Path
15
- from typing import Any
16
15
 
17
16
  from pennyfarthing_scripts.brownfield.discover import (
18
17
  DepthLevel,
@@ -11,7 +11,6 @@ import re
11
11
  from dataclasses import dataclass, field
12
12
  from enum import Enum
13
13
  from pathlib import Path
14
- from typing import Literal
15
14
 
16
15
  # Try to import tomllib (Python 3.11+) or fall back to tomli
17
16
  try:
@@ -642,7 +641,7 @@ def generate_ai_guidance_doc(result: DiscoveryResult) -> str:
642
641
 
643
642
  if result.tech_stack:
644
643
  # Get unique tech names
645
- tech_names = sorted(set(item.name.lower() for item in result.tech_stack))[:10]
644
+ tech_names = sorted({item.name.lower() for item in result.tech_stack})[:10]
646
645
  lines.extend([
647
646
  "## Key Technologies",
648
647
  "",
@@ -31,35 +31,45 @@ def cli():
31
31
 
32
32
 
33
33
  # Import and register sprint group (lazy registration preserves startup time)
34
- from pennyfarthing_scripts.sprint.cli import sprint
34
+ from pennyfarthing_scripts.sprint.cli import sprint # noqa: E402
35
35
 
36
36
  cli.add_command(sprint)
37
37
 
38
38
  # Import and register hotspots group
39
- from pennyfarthing_scripts.hotspots.cli import hotspots
39
+ from pennyfarthing_scripts.hotspots.cli import hotspots # noqa: E402
40
40
 
41
41
  cli.add_command(hotspots)
42
42
 
43
43
  # Import and register jira group
44
- from pennyfarthing_scripts.jira.cli import jira
44
+ from pennyfarthing_scripts.jira.cli import jira # noqa: E402
45
45
 
46
46
  cli.add_command(jira)
47
47
 
48
48
  # Import and register deadcode group
49
- from pennyfarthing_scripts.deadcode.cli import deadcode
49
+ from pennyfarthing_scripts.deadcode.cli import deadcode # noqa: E402
50
50
 
51
51
  cli.add_command(deadcode)
52
52
 
53
53
  # Import and register theme group
54
- from pennyfarthing_scripts.theme.cli import theme
54
+ from pennyfarthing_scripts.theme.cli import theme # noqa: E402
55
55
 
56
56
  cli.add_command(theme)
57
57
 
58
58
  # Import and register healthscore group
59
- from pennyfarthing_scripts.healthscore.cli import healthscore
59
+ from pennyfarthing_scripts.healthscore.cli import healthscore # noqa: E402
60
60
 
61
61
  cli.add_command(healthscore)
62
62
 
63
+ # Import and register validate group
64
+ from pennyfarthing_scripts.validate.cli import validate # noqa: E402
65
+
66
+ cli.add_command(validate)
67
+
68
+ # Import and register bikerack group
69
+ from pennyfarthing_scripts.bikerack.cli import bikerack # noqa: E402
70
+
71
+ cli.add_command(bikerack)
72
+
63
73
 
64
74
  @cli.group()
65
75
  def agent():
@@ -2,18 +2,22 @@
2
2
  Code marker analysis — TODO, FIXME, HACK, XXX detection with git blame.
3
3
 
4
4
  Story 80-1: Python codemarkers module.
5
+ Story 80-2: @deprecated detection and caller cross-reference.
5
6
  """
6
7
 
8
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_deprecations, analyze_repo
7
9
  from pennyfarthing_scripts.codemarkers.models import (
8
10
  CodeMarker,
9
11
  CodeMarkersResult,
12
+ DeprecationMarker,
10
13
  MarkerSummary,
11
14
  )
12
- from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
13
15
 
14
16
  __all__ = [
15
17
  "CodeMarker",
16
18
  "CodeMarkersResult",
19
+ "DeprecationMarker",
17
20
  "MarkerSummary",
21
+ "analyze_deprecations",
18
22
  "analyze_repo",
19
23
  ]
@@ -12,12 +12,13 @@ import fnmatch
12
12
  import re
13
13
  import time
14
14
  from collections import defaultdict
15
- from datetime import datetime, timezone
15
+ from datetime import UTC, datetime
16
16
  from pathlib import Path
17
17
 
18
18
  from pennyfarthing_scripts.codemarkers.models import (
19
19
  CodeMarker,
20
20
  CodeMarkersResult,
21
+ DeprecationMarker,
21
22
  MarkerSummary,
22
23
  )
23
24
 
@@ -286,7 +287,7 @@ async def analyze_repo(
286
287
  if author_time > 0:
287
288
  age_days = (now - author_time) / 86400
288
289
  date_str = datetime.fromtimestamp(
289
- author_time, tz=timezone.utc
290
+ author_time, tz=UTC
290
291
  ).isoformat()
291
292
  else:
292
293
  age_days = 0.0
@@ -324,3 +325,177 @@ async def analyze_repo(
324
325
  markers=markers,
325
326
  summary=summary,
326
327
  )
328
+
329
+
330
+ # =============================================================================
331
+ # @deprecated detection (Story 80-2)
332
+ # =============================================================================
333
+
334
+ # File extensions to scan for @deprecated JSDoc tags
335
+ _DEPRECATION_EXTENSIONS = frozenset({".ts", ".tsx", ".js"})
336
+
337
+ # Pattern to find @deprecated in JSDoc comments
338
+ _DEPRECATED_PATTERN = re.compile(r"@deprecated\b(.*)")
339
+
340
+ # Pattern to extract symbol name from export declarations
341
+ _SYMBOL_PATTERN = re.compile(
342
+ r"export\s+(?:default\s+)?(?:function|class|const|let|var|interface|type|enum)\s+(\w+)"
343
+ )
344
+
345
+
346
+ def _grep_deprecations(root: Path, excludes: list[str]) -> list[dict]:
347
+ """Scan TypeScript/JS files for @deprecated JSDoc tags.
348
+
349
+ For each @deprecated tag found, extracts the symbol name from the
350
+ next export declaration line following the JSDoc block.
351
+
352
+ Args:
353
+ root: Directory to scan recursively
354
+ excludes: Glob patterns for files/dirs to skip
355
+
356
+ Returns:
357
+ List of dicts: {path, line, symbol, text}
358
+ """
359
+ results: list[dict] = []
360
+
361
+ for file_path in sorted(root.rglob("*")):
362
+ if not file_path.is_file():
363
+ continue
364
+
365
+ if file_path.suffix.lower() not in _DEPRECATION_EXTENSIONS:
366
+ continue
367
+
368
+ rel_path = str(file_path.relative_to(root))
369
+
370
+ if _should_exclude(rel_path, excludes):
371
+ continue
372
+
373
+ try:
374
+ content = file_path.read_text(encoding="utf-8", errors="strict")
375
+ except (UnicodeDecodeError, OSError):
376
+ continue
377
+
378
+ lines = content.split("\n")
379
+ for line_num, line_text in enumerate(lines, start=1):
380
+ match = _DEPRECATED_PATTERN.search(line_text)
381
+ if not match:
382
+ continue
383
+
384
+ # Extract the @deprecated text
385
+ deprecated_text = line_text.strip()
386
+
387
+ # Look ahead for the symbol declaration
388
+ symbol = ""
389
+ for ahead in lines[line_num:]: # line_num is already 1-indexed, so lines[line_num:] starts after current
390
+ sym_match = _SYMBOL_PATTERN.search(ahead)
391
+ if sym_match:
392
+ symbol = sym_match.group(1)
393
+ break
394
+ # Stop looking if we hit another JSDoc or a blank line after the block closes
395
+ stripped = ahead.strip()
396
+ if stripped and not stripped.startswith("*") and not stripped.startswith("/") and not stripped == "":
397
+ break
398
+
399
+ if symbol:
400
+ results.append({
401
+ "path": rel_path,
402
+ "line": line_num,
403
+ "symbol": symbol,
404
+ "text": deprecated_text,
405
+ })
406
+
407
+ return results
408
+
409
+
410
+ def _count_callers(
411
+ symbol: str, root: Path, defining_file: str
412
+ ) -> tuple[int, list[str]]:
413
+ """Count files that import/reference a deprecated symbol.
414
+
415
+ Greps all TypeScript/JS files for the symbol name, excluding
416
+ the file that defines it.
417
+
418
+ Args:
419
+ symbol: The deprecated symbol name to search for
420
+ root: Directory to scan
421
+ defining_file: Relative path of the file defining the symbol (excluded)
422
+
423
+ Returns:
424
+ (caller_count, list_of_caller_paths)
425
+ """
426
+ callers: list[str] = []
427
+
428
+ for file_path in sorted(root.rglob("*")):
429
+ if not file_path.is_file():
430
+ continue
431
+
432
+ if file_path.suffix.lower() not in _DEPRECATION_EXTENSIONS:
433
+ continue
434
+
435
+ rel_path = str(file_path.relative_to(root))
436
+
437
+ if rel_path == defining_file:
438
+ continue
439
+
440
+ try:
441
+ content = file_path.read_text(encoding="utf-8", errors="strict")
442
+ except (UnicodeDecodeError, OSError):
443
+ continue
444
+
445
+ if symbol in content:
446
+ callers.append(rel_path)
447
+
448
+ return len(callers), callers
449
+
450
+
451
+ async def analyze_deprecations(
452
+ path: Path,
453
+ excludes: list[str] | None = None,
454
+ ) -> dict:
455
+ """Analyze a directory for @deprecated symbols and their callers.
456
+
457
+ Args:
458
+ path: Directory to scan
459
+ excludes: Additional file patterns to exclude
460
+
461
+ Returns:
462
+ Dict with success, deprecations (list of DeprecationMarker),
463
+ summary (total_deprecations, deprecations_with_callers), and
464
+ optionally error.
465
+ """
466
+ resolved = Path(path).resolve()
467
+
468
+ if not resolved.exists():
469
+ return {
470
+ "success": False,
471
+ "error": f"Path not found: {resolved}",
472
+ }
473
+
474
+ all_excludes = DEFAULT_EXCLUDES + (excludes or [])
475
+
476
+ raw = _grep_deprecations(resolved, all_excludes)
477
+
478
+ markers: list[DeprecationMarker] = []
479
+ for item in raw:
480
+ count, caller_list = _count_callers(
481
+ item["symbol"], resolved, item["path"]
482
+ )
483
+ markers.append(DeprecationMarker(
484
+ path=item["path"],
485
+ line=item["line"],
486
+ symbol=item["symbol"],
487
+ text=item["text"],
488
+ caller_count=count,
489
+ callers=caller_list,
490
+ ))
491
+
492
+ with_callers = sum(1 for m in markers if m.caller_count > 0)
493
+
494
+ return {
495
+ "success": True,
496
+ "deprecations": markers,
497
+ "summary": {
498
+ "total_deprecations": len(markers),
499
+ "deprecations_with_callers": with_callers,
500
+ },
501
+ }
@@ -127,3 +127,53 @@ def summary(repo, repo_path, days, top, fmt, output_file, exclude):
127
127
  else:
128
128
  from pennyfarthing_scripts.codemarkers.formatters import format_summary
129
129
  format_summary(result, file=click.get_text_stream("stdout"))
130
+
131
+
132
+ def _run_deprecation_analysis(repo_path, exclude):
133
+ """Run deprecation analysis and return result dict."""
134
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_deprecations
135
+
136
+ excludes = list(exclude) if exclude else None
137
+ if repo_path:
138
+ p = Path(repo_path).resolve()
139
+ else:
140
+ from pennyfarthing_scripts.common.config import get_project_root
141
+ p = get_project_root()
142
+
143
+ return asyncio.run(analyze_deprecations(p, excludes))
144
+
145
+
146
+ @codemarkers.command()
147
+ @_common_options
148
+ def deprecations(repo, repo_path, days, top, fmt, output_file, exclude):
149
+ """Scan for @deprecated symbols and cross-reference callers."""
150
+ result = _run_deprecation_analysis(repo_path, exclude)
151
+
152
+ if fmt == "json":
153
+ import json
154
+ from dataclasses import asdict
155
+
156
+ output = {
157
+ "success": result["success"],
158
+ "deprecations": [asdict(m) for m in result.get("deprecations", [])],
159
+ "summary": result.get("summary", {}),
160
+ }
161
+ text = json.dumps(output, indent=2)
162
+ else:
163
+ lines = []
164
+ deps = result.get("deprecations", [])
165
+ summary = result.get("summary", {})
166
+ lines.append(f"Total deprecations: {summary.get('total_deprecations', 0)}")
167
+ lines.append(f"With active callers: {summary.get('deprecations_with_callers', 0)}")
168
+ lines.append("")
169
+ for m in deps[:top]:
170
+ callers_str = f" ({m.caller_count} callers)" if m.caller_count else ""
171
+ lines.append(f" {m.symbol} @ {m.path}:{m.line}{callers_str}")
172
+ lines.append(f" {m.text}")
173
+ text = "\n".join(lines)
174
+
175
+ if output_file:
176
+ Path(output_file).write_text(text)
177
+ click.echo(f"Output written to {output_file}", err=True)
178
+ else:
179
+ click.echo(text)
@@ -16,7 +16,6 @@ from typing import TextIO
16
16
  from pennyfarthing_scripts.codemarkers.models import (
17
17
  CodeMarker,
18
18
  CodeMarkersResult,
19
- MarkerSummary,
20
19
  )
21
20
 
22
21
 
@@ -43,3 +43,18 @@ class CodeMarkersResult:
43
43
  markers: list[CodeMarker] = field(default_factory=list)
44
44
  summary: MarkerSummary | None = None
45
45
  error: str | None = None
46
+
47
+
48
+ @dataclass
49
+ class DeprecationMarker:
50
+ """A deprecated symbol with caller cross-reference data (Story 80-2).
51
+
52
+ Stub: Dev will implement full functionality.
53
+ """
54
+
55
+ path: str
56
+ line: int
57
+ symbol: str
58
+ text: str
59
+ caller_count: int = 0
60
+ callers: list[str] = field(default_factory=list)
@@ -5,8 +5,16 @@ This package provides shared utilities used across all CLI tools:
5
5
  - config: Project configuration loading
6
6
  """
7
7
 
8
+ from pennyfarthing_scripts.common.config import (
9
+ find_project_root,
10
+ get_project_root,
11
+ load_pennyfarthing_config,
12
+ load_yaml_config,
13
+ )
8
14
  from pennyfarthing_scripts.common.output import (
9
15
  Colors,
16
+ _colorize,
17
+ _supports_color,
10
18
  bold,
11
19
  debug,
12
20
  dim,
@@ -16,15 +24,6 @@ from pennyfarthing_scripts.common.output import (
16
24
  info,
17
25
  success,
18
26
  warn,
19
- _colorize,
20
- _supports_color,
21
- )
22
-
23
- from pennyfarthing_scripts.common.config import (
24
- find_project_root,
25
- get_project_root,
26
- load_pennyfarthing_config,
27
- load_yaml_config,
28
27
  )
29
28
 
30
29
  __all__ = [
@@ -78,7 +78,7 @@ def load_yaml_config(path: Path) -> dict[str, Any] | None:
78
78
  if not path.exists():
79
79
  return None
80
80
 
81
- with open(path, "r") as f:
81
+ with open(path) as f:
82
82
  return yaml.safe_load(f)
83
83
 
84
84
 
@@ -5,8 +5,8 @@ Wraps eslint with complexity rules to extract per-file metrics:
5
5
  cyclomatic complexity, function length, nesting depth, and line counts.
6
6
  """
7
7
 
8
- from pennyfarthing_scripts.complexity.models import FileComplexity, ComplexityResult
9
8
  from pennyfarthing_scripts.complexity.analyze import analyze_complexity
9
+ from pennyfarthing_scripts.complexity.models import ComplexityResult, FileComplexity
10
10
 
11
11
  __all__ = [
12
12
  "FileComplexity",