@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,687 @@
1
+ """Tests for codemarkers module.
2
+
3
+ Story 80-1: Python codemarkers module — grep + git blame.
4
+ MSSCI-14454
5
+
6
+ TDD RED phase — all tests written before implementation.
7
+ Tests cover: models, analyze engine, CLI, formatters.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from dataclasses import asdict
14
+ from pathlib import Path
15
+ from unittest.mock import AsyncMock, patch
16
+
17
+ import pytest
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Models
21
+ # ---------------------------------------------------------------------------
22
+
23
+
24
+ class TestCodeMarkerModel:
25
+ """CodeMarker dataclass fields and defaults."""
26
+
27
+ def test_required_fields(self) -> None:
28
+ """CodeMarker requires path, line, marker_type, text."""
29
+ from pennyfarthing_scripts.codemarkers.models import CodeMarker
30
+
31
+ m = CodeMarker(path="src/app.ts", line=42, marker_type="TODO", text="TODO: refactor")
32
+ assert m.path == "src/app.ts"
33
+ assert m.line == 42
34
+ assert m.marker_type == "TODO"
35
+ assert m.text == "TODO: refactor"
36
+
37
+ def test_default_blame_fields(self) -> None:
38
+ """Blame fields default to empty/zero."""
39
+ from pennyfarthing_scripts.codemarkers.models import CodeMarker
40
+
41
+ m = CodeMarker(path="a.py", line=1, marker_type="FIXME", text="FIXME: broken")
42
+ assert m.author == ""
43
+ assert m.date == ""
44
+ assert m.age_days == 0.0
45
+ assert m.is_stale is False
46
+
47
+ def test_stale_marker(self) -> None:
48
+ """is_stale can be set to True."""
49
+ from pennyfarthing_scripts.codemarkers.models import CodeMarker
50
+
51
+ m = CodeMarker(
52
+ path="old.py", line=10, marker_type="TODO", text="TODO: old",
53
+ author="dev", date="2025-01-01T00:00:00", age_days=120.0, is_stale=True,
54
+ )
55
+ assert m.is_stale is True
56
+ assert m.age_days == 120.0
57
+
58
+ def test_serializable(self) -> None:
59
+ """CodeMarker should be serializable via dataclasses.asdict."""
60
+ from pennyfarthing_scripts.codemarkers.models import CodeMarker
61
+
62
+ m = CodeMarker(path="x.py", line=5, marker_type="HACK", text="HACK: workaround")
63
+ d = asdict(m)
64
+ assert d["path"] == "x.py"
65
+ assert d["marker_type"] == "HACK"
66
+ assert isinstance(d, dict)
67
+
68
+
69
+ class TestMarkerSummaryModel:
70
+ """MarkerSummary aggregates counts by type."""
71
+
72
+ def test_defaults(self) -> None:
73
+ """MarkerSummary fields default to zero/empty."""
74
+ from pennyfarthing_scripts.codemarkers.models import MarkerSummary
75
+
76
+ s = MarkerSummary()
77
+ assert s.total_markers == 0
78
+ assert s.stale_markers == 0
79
+ assert s.by_type == {}
80
+
81
+ def test_populated(self) -> None:
82
+ """MarkerSummary with values."""
83
+ from pennyfarthing_scripts.codemarkers.models import MarkerSummary
84
+
85
+ s = MarkerSummary(
86
+ total_markers=10, stale_markers=3,
87
+ by_type={"TODO": 6, "FIXME": 4},
88
+ )
89
+ assert s.total_markers == 10
90
+ assert s.by_type["FIXME"] == 4
91
+
92
+
93
+ class TestCodeMarkersResultModel:
94
+ """CodeMarkersResult follows ADR-0008 pattern."""
95
+
96
+ def test_success_result(self) -> None:
97
+ """Successful result has success=True and markers list."""
98
+ from pennyfarthing_scripts.codemarkers.models import CodeMarker, CodeMarkersResult
99
+
100
+ r = CodeMarkersResult(
101
+ success=True, repo_name="test", repo_path="/tmp/test",
102
+ stale_threshold_days=90,
103
+ markers=[CodeMarker(path="a.py", line=1, marker_type="TODO", text="TODO: x")],
104
+ )
105
+ assert r.success is True
106
+ assert len(r.markers) == 1
107
+ assert r.error is None
108
+
109
+ def test_error_result(self) -> None:
110
+ """Error result has success=False and error message."""
111
+ from pennyfarthing_scripts.codemarkers.models import CodeMarkersResult
112
+
113
+ r = CodeMarkersResult(
114
+ success=False, repo_name="bad", repo_path="/nonexistent",
115
+ stale_threshold_days=90, error="Path not found",
116
+ )
117
+ assert r.success is False
118
+ assert "not found" in r.error
119
+
120
+ def test_json_serializable(self) -> None:
121
+ """Full result serializes to JSON via asdict."""
122
+ from pennyfarthing_scripts.codemarkers.models import (
123
+ CodeMarker,
124
+ CodeMarkersResult,
125
+ MarkerSummary,
126
+ )
127
+
128
+ r = CodeMarkersResult(
129
+ success=True, repo_name="repo", repo_path="/tmp",
130
+ stale_threshold_days=90,
131
+ markers=[CodeMarker(path="b.py", line=2, marker_type="XXX", text="XXX: bad")],
132
+ summary=MarkerSummary(total_markers=1, by_type={"XXX": 1}),
133
+ )
134
+ text = json.dumps(asdict(r), default=str)
135
+ parsed = json.loads(text)
136
+ assert parsed["success"] is True
137
+ assert parsed["markers"][0]["marker_type"] == "XXX"
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Analyze — grep and blame
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ class TestGrepMarkers:
146
+ """_grep_markers finds TODO/FIXME/HACK/XXX in source files."""
147
+
148
+ def test_finds_todo(self, tmp_path: Path) -> None:
149
+ """Detects TODO comments."""
150
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
151
+
152
+ f = tmp_path / "app.py"
153
+ f.write_text("x = 1\n# TODO: fix this\ny = 2\n")
154
+ markers = _grep_markers(tmp_path, excludes=[])
155
+ assert len(markers) == 1
156
+ assert markers[0]["marker_type"] == "TODO"
157
+ assert markers[0]["line"] == 2
158
+
159
+ def test_finds_fixme(self, tmp_path: Path) -> None:
160
+ """Detects FIXME comments."""
161
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
162
+
163
+ f = tmp_path / "bug.ts"
164
+ f.write_text("// FIXME: null check needed\n")
165
+ markers = _grep_markers(tmp_path, excludes=[])
166
+ assert len(markers) == 1
167
+ assert markers[0]["marker_type"] == "FIXME"
168
+
169
+ def test_finds_hack(self, tmp_path: Path) -> None:
170
+ """Detects HACK comments."""
171
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
172
+
173
+ f = tmp_path / "util.js"
174
+ f.write_text("/* HACK: temporary workaround */\n")
175
+ markers = _grep_markers(tmp_path, excludes=[])
176
+ assert len(markers) == 1
177
+ assert markers[0]["marker_type"] == "HACK"
178
+
179
+ def test_finds_xxx(self, tmp_path: Path) -> None:
180
+ """Detects XXX comments."""
181
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
182
+
183
+ f = tmp_path / "danger.py"
184
+ f.write_text("# XXX: this is terrible\n")
185
+ markers = _grep_markers(tmp_path, excludes=[])
186
+ assert len(markers) == 1
187
+ assert markers[0]["marker_type"] == "XXX"
188
+
189
+ def test_multiple_markers_same_file(self, tmp_path: Path) -> None:
190
+ """Finds multiple markers in a single file."""
191
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
192
+
193
+ f = tmp_path / "multi.py"
194
+ f.write_text("# TODO: first\nx = 1\n# FIXME: second\n# HACK: third\n")
195
+ markers = _grep_markers(tmp_path, excludes=[])
196
+ assert len(markers) == 3
197
+ types = {m["marker_type"] for m in markers}
198
+ assert types == {"TODO", "FIXME", "HACK"}
199
+
200
+ def test_excludes_node_modules(self, tmp_path: Path) -> None:
201
+ """Files matching exclude patterns are skipped."""
202
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
203
+
204
+ nm = tmp_path / "node_modules" / "pkg"
205
+ nm.mkdir(parents=True)
206
+ (nm / "index.js").write_text("// TODO: vendor code\n")
207
+ (tmp_path / "src.py").write_text("# TODO: real code\n")
208
+
209
+ markers = _grep_markers(tmp_path, excludes=["node_modules/*"])
210
+ paths = [m["path"] for m in markers]
211
+ assert not any("node_modules" in p for p in paths)
212
+ assert len(markers) == 1
213
+
214
+ def test_case_sensitive(self, tmp_path: Path) -> None:
215
+ """Markers must be uppercase to match."""
216
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
217
+
218
+ f = tmp_path / "lower.py"
219
+ f.write_text("# todo: lowercase should not match\n# TODO: uppercase matches\n")
220
+ markers = _grep_markers(tmp_path, excludes=[])
221
+ assert len(markers) == 1
222
+ assert markers[0]["marker_type"] == "TODO"
223
+
224
+ def test_nested_directories(self, tmp_path: Path) -> None:
225
+ """Scans recursively into subdirectories."""
226
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
227
+
228
+ sub = tmp_path / "src" / "lib"
229
+ sub.mkdir(parents=True)
230
+ (sub / "deep.ts").write_text("// FIXME: deep bug\n")
231
+ markers = _grep_markers(tmp_path, excludes=[])
232
+ assert len(markers) == 1
233
+
234
+ def test_empty_directory(self, tmp_path: Path) -> None:
235
+ """Returns empty list for directory with no markers."""
236
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
237
+
238
+ (tmp_path / "clean.py").write_text("x = 1\ny = 2\n")
239
+ markers = _grep_markers(tmp_path, excludes=[])
240
+ assert markers == []
241
+
242
+ def test_binary_files_skipped(self, tmp_path: Path) -> None:
243
+ """Binary files should not cause crashes."""
244
+ from pennyfarthing_scripts.codemarkers.analyze import _grep_markers
245
+
246
+ (tmp_path / "image.png").write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
247
+ (tmp_path / "code.py").write_text("# TODO: real marker\n")
248
+ markers = _grep_markers(tmp_path, excludes=[])
249
+ assert len(markers) == 1
250
+
251
+
252
+ class TestParseBlameOutput:
253
+ """_parse_blame_porcelain extracts author and date from git blame --porcelain."""
254
+
255
+ def test_parses_author_and_time(self) -> None:
256
+ """Extracts author name and author-time from porcelain output."""
257
+ from pennyfarthing_scripts.codemarkers.analyze import _parse_blame_porcelain
258
+
259
+ porcelain = (
260
+ "abc123 1 1 1\n"
261
+ "author John Doe\n"
262
+ "author-mail <john@example.com>\n"
263
+ "author-time 1700000000\n"
264
+ "author-tz +0000\n"
265
+ "committer John Doe\n"
266
+ "committer-mail <john@example.com>\n"
267
+ "committer-time 1700000000\n"
268
+ "committer-tz +0000\n"
269
+ "summary Some commit\n"
270
+ "filename src/app.py\n"
271
+ "\t# TODO: fix this\n"
272
+ )
273
+ result = _parse_blame_porcelain(porcelain, line=1)
274
+ assert result["author"] == "John Doe"
275
+ assert result["author_time"] == 1700000000
276
+
277
+ def test_empty_output(self) -> None:
278
+ """Returns empty dict for empty output."""
279
+ from pennyfarthing_scripts.codemarkers.analyze import _parse_blame_porcelain
280
+
281
+ result = _parse_blame_porcelain("", line=1)
282
+ assert result == {} or result.get("author") == ""
283
+
284
+
285
+ class TestBatchBlame:
286
+ """_batch_blame_file calls git blame once per file, extracts multiple lines."""
287
+
288
+ @pytest.mark.asyncio
289
+ async def test_blames_entire_file(self) -> None:
290
+ """Blames the whole file and returns data for requested lines."""
291
+ from pennyfarthing_scripts.codemarkers.analyze import _batch_blame_file
292
+
293
+ blame_output = (
294
+ "abc123 1 1 1\n"
295
+ "author Alice\n"
296
+ "author-time 1700000000\n"
297
+ "author-tz +0000\n"
298
+ "committer Alice\n"
299
+ "committer-time 1700000000\n"
300
+ "committer-tz +0000\n"
301
+ "summary init\n"
302
+ "filename test.py\n"
303
+ "\tline 1\n"
304
+ "def456 2 2 1\n"
305
+ "author Bob\n"
306
+ "author-time 1700100000\n"
307
+ "author-tz +0000\n"
308
+ "committer Bob\n"
309
+ "committer-time 1700100000\n"
310
+ "committer-tz +0000\n"
311
+ "summary fix\n"
312
+ "filename test.py\n"
313
+ "\tline 2\n"
314
+ )
315
+
316
+ with patch(
317
+ "pennyfarthing_scripts.codemarkers.analyze._run_git_command",
318
+ new_callable=AsyncMock,
319
+ return_value=(blame_output, "", 0),
320
+ ):
321
+ results = await _batch_blame_file(
322
+ Path("/repo"), "test.py", [1, 2]
323
+ )
324
+ assert 1 in results
325
+ assert 2 in results
326
+ assert results[1]["author"] == "Alice"
327
+ assert results[2]["author"] == "Bob"
328
+
329
+ @pytest.mark.asyncio
330
+ async def test_git_blame_failure_returns_empty(self) -> None:
331
+ """When git blame fails, returns empty dict for all lines."""
332
+ from pennyfarthing_scripts.codemarkers.analyze import _batch_blame_file
333
+
334
+ with patch(
335
+ "pennyfarthing_scripts.codemarkers.analyze._run_git_command",
336
+ new_callable=AsyncMock,
337
+ return_value=("", "fatal: not a git repo", 128),
338
+ ):
339
+ results = await _batch_blame_file(Path("/bad"), "x.py", [1])
340
+ assert results == {} or results.get(1, {}).get("author", "") == ""
341
+
342
+
343
+ class TestAnalyzeRepo:
344
+ """analyze_repo is the main entry point — grep + blame + staleness."""
345
+
346
+ @pytest.mark.asyncio
347
+ async def test_nonexistent_path(self) -> None:
348
+ """Returns error result for nonexistent path."""
349
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
350
+
351
+ result = await analyze_repo("bad", Path("/nonexistent/repo"), days=90)
352
+ assert result.success is False
353
+ assert "not found" in result.error.lower() or "not exist" in result.error.lower()
354
+
355
+ @pytest.mark.asyncio
356
+ async def test_empty_repo(self, tmp_path: Path) -> None:
357
+ """Returns success with empty markers for repo with no markers."""
358
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
359
+
360
+ (tmp_path / ".git").mkdir()
361
+ (tmp_path / "clean.py").write_text("x = 1\n")
362
+
363
+ with patch(
364
+ "pennyfarthing_scripts.codemarkers.analyze._batch_blame_file",
365
+ new_callable=AsyncMock,
366
+ return_value={},
367
+ ):
368
+ result = await analyze_repo("test", tmp_path, days=90)
369
+ assert result.success is True
370
+ assert len(result.markers) == 0
371
+
372
+ @pytest.mark.asyncio
373
+ async def test_markers_enriched_with_blame(self, tmp_path: Path) -> None:
374
+ """Markers get author, date, age_days from git blame."""
375
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
376
+
377
+ (tmp_path / ".git").mkdir()
378
+ (tmp_path / "app.py").write_text("x = 1\n# TODO: fix this\ny = 2\n")
379
+
380
+ # Mock blame: author Alice, time = 90 days ago
381
+ import time
382
+ ninety_days_ago = int(time.time()) - (90 * 86400)
383
+
384
+ async def mock_blame(repo_path, file_path, lines):
385
+ return {
386
+ 2: {"author": "Alice", "author_time": ninety_days_ago},
387
+ }
388
+
389
+ with patch(
390
+ "pennyfarthing_scripts.codemarkers.analyze._batch_blame_file",
391
+ side_effect=mock_blame,
392
+ ):
393
+ result = await analyze_repo("test", tmp_path, days=90)
394
+ assert result.success is True
395
+ assert len(result.markers) == 1
396
+ assert result.markers[0].author == "Alice"
397
+ assert result.markers[0].age_days >= 89 # approximately 90
398
+
399
+ @pytest.mark.asyncio
400
+ async def test_stale_threshold(self, tmp_path: Path) -> None:
401
+ """Markers older than threshold are flagged is_stale=True."""
402
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
403
+
404
+ (tmp_path / ".git").mkdir()
405
+ (tmp_path / "old.py").write_text("# TODO: ancient code\n")
406
+
407
+ import time
408
+ old_time = int(time.time()) - (200 * 86400) # 200 days ago
409
+
410
+ async def mock_blame(repo_path, file_path, lines):
411
+ return {1: {"author": "OldDev", "author_time": old_time}}
412
+
413
+ with patch(
414
+ "pennyfarthing_scripts.codemarkers.analyze._batch_blame_file",
415
+ side_effect=mock_blame,
416
+ ):
417
+ result = await analyze_repo("test", tmp_path, days=90)
418
+ assert result.markers[0].is_stale is True
419
+ assert result.markers[0].age_days >= 199
420
+
421
+ @pytest.mark.asyncio
422
+ async def test_summary_computed(self, tmp_path: Path) -> None:
423
+ """Result includes summary with counts by type."""
424
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
425
+
426
+ (tmp_path / ".git").mkdir()
427
+ (tmp_path / "mix.py").write_text("# TODO: one\n# FIXME: two\n# TODO: three\n")
428
+
429
+ import time
430
+ recent = int(time.time()) - (10 * 86400)
431
+
432
+ async def mock_blame(repo_path, file_path, lines):
433
+ return {ln: {"author": "Dev", "author_time": recent} for ln in lines}
434
+
435
+ with patch(
436
+ "pennyfarthing_scripts.codemarkers.analyze._batch_blame_file",
437
+ side_effect=mock_blame,
438
+ ):
439
+ result = await analyze_repo("test", tmp_path, days=90)
440
+ assert result.summary is not None
441
+ assert result.summary.total_markers == 3
442
+ assert result.summary.by_type["TODO"] == 2
443
+ assert result.summary.by_type["FIXME"] == 1
444
+ assert result.summary.stale_markers == 0
445
+
446
+ @pytest.mark.asyncio
447
+ async def test_default_excludes_applied(self, tmp_path: Path) -> None:
448
+ """DEFAULT_EXCLUDES filters out node_modules, dist, etc."""
449
+ from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
450
+
451
+ (tmp_path / ".git").mkdir()
452
+ nm = tmp_path / "node_modules" / "pkg"
453
+ nm.mkdir(parents=True)
454
+ (nm / "lib.js").write_text("// TODO: vendor code\n")
455
+ (tmp_path / "src.py").write_text("# TODO: real code\n")
456
+
457
+ import time
458
+ recent = int(time.time()) - 86400
459
+
460
+ async def mock_blame(repo_path, file_path, lines):
461
+ return {ln: {"author": "Dev", "author_time": recent} for ln in lines}
462
+
463
+ with patch(
464
+ "pennyfarthing_scripts.codemarkers.analyze._batch_blame_file",
465
+ side_effect=mock_blame,
466
+ ):
467
+ result = await analyze_repo("test", tmp_path, days=90)
468
+ paths = [m.path for m in result.markers]
469
+ assert not any("node_modules" in p for p in paths)
470
+
471
+
472
+ class TestShouldExclude:
473
+ """_should_exclude matches file paths against glob patterns."""
474
+
475
+ def test_matches_node_modules(self) -> None:
476
+ from pennyfarthing_scripts.codemarkers.analyze import _should_exclude
477
+
478
+ assert _should_exclude("node_modules/pkg/index.js", ["node_modules/*"]) is True
479
+
480
+ def test_matches_lock_file(self) -> None:
481
+ from pennyfarthing_scripts.codemarkers.analyze import _should_exclude
482
+
483
+ assert _should_exclude("package-lock.json", ["package-lock.json"]) is True
484
+
485
+ def test_matches_glob_extension(self) -> None:
486
+ from pennyfarthing_scripts.codemarkers.analyze import _should_exclude
487
+
488
+ assert _should_exclude("bundle.min.js", ["*.min.js"]) is True
489
+
490
+ def test_no_match(self) -> None:
491
+ from pennyfarthing_scripts.codemarkers.analyze import _should_exclude
492
+
493
+ assert _should_exclude("src/app.py", ["node_modules/*", "dist/*"]) is False
494
+
495
+
496
+ # ---------------------------------------------------------------------------
497
+ # Formatters
498
+ # ---------------------------------------------------------------------------
499
+
500
+
501
+ class TestTableFormatter:
502
+ """format_marker_table produces column-aligned output."""
503
+
504
+ def test_empty_list(self) -> None:
505
+ """Empty marker list produces 'no markers' message."""
506
+ from pennyfarthing_scripts.codemarkers.formatters import format_marker_table
507
+
508
+ out = format_marker_table([])
509
+ assert "no" in out.lower() or "No" in out
510
+
511
+ def test_table_has_headers(self) -> None:
512
+ """Table output includes column headers."""
513
+ from pennyfarthing_scripts.codemarkers.formatters import format_marker_table
514
+ from pennyfarthing_scripts.codemarkers.models import CodeMarker
515
+
516
+ markers = [
517
+ CodeMarker(path="a.py", line=1, marker_type="TODO", text="TODO: test",
518
+ author="Dev", age_days=10.0),
519
+ ]
520
+ out = format_marker_table(markers)
521
+ assert "Type" in out
522
+ assert "File" in out or "Path" in out
523
+ assert "Age" in out
524
+
525
+ def test_top_n_limits_output(self) -> None:
526
+ """Table respects top_n limit."""
527
+ from pennyfarthing_scripts.codemarkers.formatters import format_marker_table
528
+ from pennyfarthing_scripts.codemarkers.models import CodeMarker
529
+
530
+ markers = [
531
+ CodeMarker(path=f"f{i}.py", line=i, marker_type="TODO", text=f"TODO: {i}")
532
+ for i in range(50)
533
+ ]
534
+ out = format_marker_table(markers, top_n=5)
535
+ # Header + separator + 5 data rows = 7 lines
536
+ lines = [line for line in out.strip().split("\n") if line.strip()]
537
+ assert len(lines) <= 7
538
+
539
+
540
+ class TestJsonExport:
541
+ """export_json serializes full result to JSON."""
542
+
543
+ def test_valid_json(self) -> None:
544
+ from pennyfarthing_scripts.codemarkers.formatters import export_json
545
+ from pennyfarthing_scripts.codemarkers.models import (
546
+ CodeMarker,
547
+ CodeMarkersResult,
548
+ MarkerSummary,
549
+ )
550
+
551
+ r = CodeMarkersResult(
552
+ success=True, repo_name="test", repo_path="/tmp",
553
+ stale_threshold_days=90,
554
+ markers=[CodeMarker(path="x.py", line=1, marker_type="TODO", text="TODO: x")],
555
+ summary=MarkerSummary(total_markers=1, by_type={"TODO": 1}),
556
+ )
557
+ text = export_json(r)
558
+ parsed = json.loads(text)
559
+ assert parsed["success"] is True
560
+ assert len(parsed["markers"]) == 1
561
+
562
+
563
+ class TestCsvExport:
564
+ """export_csv outputs CSV with headers."""
565
+
566
+ def test_csv_header_row(self) -> None:
567
+ from pennyfarthing_scripts.codemarkers.formatters import export_csv
568
+ from pennyfarthing_scripts.codemarkers.models import CodeMarker
569
+
570
+ markers = [
571
+ CodeMarker(path="a.py", line=1, marker_type="TODO", text="TODO: test"),
572
+ ]
573
+ text = export_csv(markers)
574
+ lines = text.strip().split("\n")
575
+ assert "path" in lines[0]
576
+ assert "marker_type" in lines[0]
577
+ assert len(lines) == 2 # header + 1 data row
578
+
579
+
580
+ # ---------------------------------------------------------------------------
581
+ # CLI
582
+ # ---------------------------------------------------------------------------
583
+
584
+
585
+ class TestCLI:
586
+ """Click CLI commands."""
587
+
588
+ def test_cli_help(self) -> None:
589
+ """codemarkers group shows help."""
590
+ from click.testing import CliRunner
591
+
592
+ from pennyfarthing_scripts.codemarkers.cli import codemarkers
593
+
594
+ runner = CliRunner()
595
+ result = runner.invoke(codemarkers, ["--help"])
596
+ assert result.exit_code == 0
597
+ assert "analyze" in result.output.lower()
598
+
599
+ def test_analyze_command_exists(self) -> None:
600
+ """analyze subcommand is registered."""
601
+ from click.testing import CliRunner
602
+
603
+ from pennyfarthing_scripts.codemarkers.cli import codemarkers
604
+
605
+ runner = CliRunner()
606
+ result = runner.invoke(codemarkers, ["analyze", "--help"])
607
+ assert result.exit_code == 0
608
+ assert "--days" in result.output
609
+ assert "--format" in result.output
610
+
611
+ def test_stale_command_exists(self) -> None:
612
+ """stale subcommand is registered."""
613
+ from click.testing import CliRunner
614
+
615
+ from pennyfarthing_scripts.codemarkers.cli import codemarkers
616
+
617
+ runner = CliRunner()
618
+ result = runner.invoke(codemarkers, ["stale", "--help"])
619
+ assert result.exit_code == 0
620
+
621
+ def test_summary_command_exists(self) -> None:
622
+ """summary subcommand is registered."""
623
+ from click.testing import CliRunner
624
+
625
+ from pennyfarthing_scripts.codemarkers.cli import codemarkers
626
+
627
+ runner = CliRunner()
628
+ result = runner.invoke(codemarkers, ["summary", "--help"])
629
+ assert result.exit_code == 0
630
+
631
+ def test_json_format_option(self) -> None:
632
+ """--format json produces JSON output."""
633
+ from click.testing import CliRunner
634
+
635
+ from pennyfarthing_scripts.codemarkers.cli import codemarkers
636
+
637
+ runner = CliRunner()
638
+ with patch(
639
+ "pennyfarthing_scripts.codemarkers.cli._run_analysis"
640
+ ) as mock_run:
641
+ from pennyfarthing_scripts.codemarkers.models import CodeMarkersResult, MarkerSummary
642
+ mock_run.return_value = CodeMarkersResult(
643
+ success=True, repo_name="test", repo_path="/tmp",
644
+ stale_threshold_days=90, markers=[],
645
+ summary=MarkerSummary(),
646
+ )
647
+ result = runner.invoke(codemarkers, ["analyze", "--path", "/tmp", "--format", "json"])
648
+ assert result.exit_code == 0
649
+ parsed = json.loads(result.output)
650
+ assert parsed["success"] is True
651
+
652
+
653
+ class TestMainModule:
654
+ """python -m pennyfarthing_scripts.codemarkers works."""
655
+
656
+ def test_module_runnable(self) -> None:
657
+ """Module can be invoked with --help."""
658
+ import subprocess
659
+ import sys
660
+
661
+ result = subprocess.run(
662
+ [sys.executable, "-m", "pennyfarthing_scripts.codemarkers", "--help"],
663
+ capture_output=True, text=True, timeout=30,
664
+ )
665
+ assert result.returncode == 0
666
+ assert "analyze" in result.stdout.lower()
667
+
668
+
669
+ # ---------------------------------------------------------------------------
670
+ # __init__ re-exports
671
+ # ---------------------------------------------------------------------------
672
+
673
+
674
+ class TestModuleExports:
675
+ """__init__.py re-exports key symbols."""
676
+
677
+ def test_exports_models(self) -> None:
678
+ from pennyfarthing_scripts.codemarkers import (
679
+ CodeMarker,
680
+ CodeMarkersResult,
681
+ )
682
+ assert CodeMarker is not None
683
+ assert CodeMarkersResult is not None
684
+
685
+ def test_exports_analyze(self) -> None:
686
+ from pennyfarthing_scripts.codemarkers import analyze_repo
687
+ assert callable(analyze_repo)