@pennyfarthing/core 11.2.0 → 11.2.1

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 (403) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor.js +381 -66
  5. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  6. package/packages/core/dist/cli/commands/init.js +4 -4
  7. package/packages/core/dist/cli/commands/init.js.map +1 -1
  8. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  9. package/packages/core/dist/cli/commands/update.js +4 -5
  10. package/packages/core/dist/cli/commands/update.js.map +1 -1
  11. package/packages/core/dist/cli/utils/constants.d.ts +3 -8
  12. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  13. package/packages/core/dist/cli/utils/constants.js +3 -4
  14. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  15. package/packages/core/dist/cli/utils/settings.d.ts +11 -0
  16. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  17. package/packages/core/dist/cli/utils/settings.js +65 -29
  18. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  19. package/packages/core/dist/cli/utils/symlinks.js +16 -16
  20. package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
  21. package/packages/core/dist/consultation/tandem-metrics.d.ts +91 -0
  22. package/packages/core/dist/consultation/tandem-metrics.d.ts.map +1 -0
  23. package/packages/core/dist/consultation/tandem-metrics.js +131 -0
  24. package/packages/core/dist/consultation/tandem-metrics.js.map +1 -0
  25. package/packages/core/dist/consultation/tandem-metrics.test.d.ts +18 -0
  26. package/packages/core/dist/consultation/tandem-metrics.test.d.ts.map +1 -0
  27. package/packages/core/dist/consultation/tandem-metrics.test.js +457 -0
  28. package/packages/core/dist/consultation/tandem-metrics.test.js.map +1 -0
  29. package/packages/core/dist/public/js/react/react.js +14 -14
  30. package/packages/core/dist/scripts/benchmark-integration.d.ts +182 -0
  31. package/packages/core/dist/scripts/benchmark-integration.d.ts.map +1 -0
  32. package/packages/core/dist/scripts/benchmark-integration.js +691 -0
  33. package/packages/core/dist/scripts/benchmark-integration.js.map +1 -0
  34. package/packages/core/dist/scripts/job-fair-aggregator.d.ts +150 -0
  35. package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +1 -0
  36. package/packages/core/dist/scripts/job-fair-aggregator.js +547 -0
  37. package/packages/core/dist/scripts/job-fair-aggregator.js.map +1 -0
  38. package/packages/core/dist/server/api/agent-load.js +1 -1
  39. package/packages/core/dist/server/api/agent-load.js.map +1 -1
  40. package/packages/core/dist/server/server.d.ts +0 -3
  41. package/packages/core/dist/server/server.d.ts.map +1 -1
  42. package/packages/core/dist/server/server.js +3 -37
  43. package/packages/core/dist/server/server.js.map +1 -1
  44. package/packages/core/dist/server/server.test.d.ts +1 -1
  45. package/packages/core/dist/server/server.test.js +12 -23
  46. package/packages/core/dist/server/server.test.js.map +1 -1
  47. package/packages/core/dist/shared/capabilities.d.ts +88 -0
  48. package/packages/core/dist/shared/capabilities.d.ts.map +1 -0
  49. package/packages/core/dist/shared/capabilities.js +133 -0
  50. package/packages/core/dist/shared/capabilities.js.map +1 -0
  51. package/packages/core/dist/shared/capabilities.test.d.ts +2 -0
  52. package/packages/core/dist/shared/capabilities.test.d.ts.map +1 -0
  53. package/packages/core/dist/shared/capabilities.test.js +217 -0
  54. package/packages/core/dist/shared/capabilities.test.js.map +1 -0
  55. package/packages/core/dist/shared/spawn-prompt.d.ts +47 -0
  56. package/packages/core/dist/shared/spawn-prompt.d.ts.map +1 -0
  57. package/packages/core/dist/shared/spawn-prompt.js +82 -0
  58. package/packages/core/dist/shared/spawn-prompt.js.map +1 -0
  59. package/packages/core/dist/shared/spawn-prompt.test.d.ts +2 -0
  60. package/packages/core/dist/shared/spawn-prompt.test.d.ts.map +1 -0
  61. package/packages/core/dist/shared/spawn-prompt.test.js +251 -0
  62. package/packages/core/dist/shared/spawn-prompt.test.js.map +1 -0
  63. package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts +18 -0
  64. package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts.map +1 -0
  65. package/packages/core/dist/workflow/tandem-workflow-templates.test.js +434 -0
  66. package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -0
  67. package/packages/core/dist/workflow/workflow-schema.d.ts +32 -0
  68. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  69. package/packages/core/dist/workflow/workflow-schema.js +120 -0
  70. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  71. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  72. package/packages/core/dist/workflow/workflow-schema.test.js +570 -1
  73. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  74. package/pennyfarthing-dist/agents/dev.md +6 -10
  75. package/pennyfarthing-dist/agents/reviewer.md +8 -2
  76. package/pennyfarthing-dist/agents/sm-finish.md +18 -1
  77. package/pennyfarthing-dist/commands/pf-git.md +4 -2
  78. package/pennyfarthing-dist/gates/approval.md +63 -0
  79. package/pennyfarthing-dist/gates/confidence-sm.md +71 -0
  80. package/pennyfarthing-dist/gates/context-ok.md +56 -0
  81. package/pennyfarthing-dist/gates/evaluations/confidence-sm.md +54 -0
  82. package/pennyfarthing-dist/gates/quality-pass.md +67 -0
  83. package/pennyfarthing-dist/gates/tests-fail.md +84 -0
  84. package/pennyfarthing-dist/gates/tests-pass.md +79 -0
  85. package/pennyfarthing-dist/guides/agent-behavior.md +23 -19
  86. package/pennyfarthing-dist/guides/bell-mode.md +1 -1
  87. package/pennyfarthing-dist/guides/hooks.md +28 -28
  88. package/pennyfarthing-dist/guides/reflector.md +1 -1
  89. package/pennyfarthing-dist/guides/tandem-protocol.md +3 -3
  90. package/pennyfarthing-dist/scripts/core/check-context.sh +2 -0
  91. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +5 -87
  92. package/pennyfarthing-dist/scripts/hooks/README.md +5 -5
  93. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  94. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +4 -183
  95. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +4 -95
  96. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +4 -65
  97. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +3 -31
  98. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +27 -33
  99. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +4 -71
  100. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +3 -19
  101. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +4 -30
  102. package/pennyfarthing-dist/scripts/hooks/session-start.sh +3 -32
  103. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +4 -65
  104. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +4 -78
  105. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +4 -93
  106. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  107. package/pennyfarthing-dist/scripts/misc/statusline.sh +4 -301
  108. package/pennyfarthing-dist/templates/settings.local.json.template +19 -10
  109. package/pennyfarthing-dist/workflows/tdd.yaml +11 -2
  110. package/pennyfarthing_scripts/CLAUDE.md +19 -10
  111. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  112. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  114. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  122. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  123. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  124. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  125. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  126. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  130. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/bellmode_hook.py +12 -296
  135. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  136. package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
  137. package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
  138. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/bikerack/__pycache__/context_meter_footer.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  145. package/pennyfarthing_scripts/bikerack/__pycache__/events.cpython-314.pyc +0 -0
  146. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  147. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  148. package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  149. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  150. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  151. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
  152. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
  153. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  154. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  155. package/pennyfarthing_scripts/bikerack/audit_log_panel.py +119 -0
  156. package/pennyfarthing_scripts/bikerack/base_panel.py +27 -4
  157. package/pennyfarthing_scripts/bikerack/changed_panel.py +96 -4
  158. package/pennyfarthing_scripts/bikerack/context_meter_footer.py +88 -0
  159. package/pennyfarthing_scripts/bikerack/debug_panel.py +1 -1
  160. package/pennyfarthing_scripts/bikerack/diffs_panel.py +30 -0
  161. package/pennyfarthing_scripts/bikerack/events.py +28 -0
  162. package/pennyfarthing_scripts/bikerack/portrait_resolver.py +139 -0
  163. package/pennyfarthing_scripts/bikerack/sprint_panel.py +373 -142
  164. package/pennyfarthing_scripts/bikerack/story_detail_data.py +244 -0
  165. package/pennyfarthing_scripts/bikerack/story_detail_screen.py +176 -0
  166. package/pennyfarthing_scripts/bikerack/tui.py +293 -61
  167. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/cli.py +5 -0
  172. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/common/pr_config.py +38 -0
  183. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  184. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  185. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  190. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  191. package/pennyfarthing_scripts/consultation/__pycache__/dialogue_manager.cpython-314.pyc +0 -0
  192. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  211. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  212. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/handoff/cli.py +33 -1
  219. package/pennyfarthing_scripts/handoff/complete_phase.py +28 -0
  220. package/pennyfarthing_scripts/handoff/marker.py +15 -15
  221. package/pennyfarthing_scripts/handoff/phase_check.py +96 -0
  222. package/pennyfarthing_scripts/handoff/resolve_gate.py +13 -1
  223. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/hooks/__init__.py +437 -0
  230. package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  238. package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  239. package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  240. package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  241. package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  242. package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  243. package/pennyfarthing_scripts/hooks/bell_mode.py +215 -0
  244. package/pennyfarthing_scripts/hooks/cli.py +96 -0
  245. package/pennyfarthing_scripts/hooks/context_breaker.py +104 -0
  246. package/pennyfarthing_scripts/hooks/context_warning.py +66 -0
  247. package/pennyfarthing_scripts/hooks/cyclist_pretooluse.py +129 -0
  248. package/pennyfarthing_scripts/hooks/pre_edit_check.py +78 -0
  249. package/pennyfarthing_scripts/hooks/reflector_check.py +271 -0
  250. package/pennyfarthing_scripts/hooks/schema_validation.py +203 -0
  251. package/pennyfarthing_scripts/hooks/session_start.py +296 -0
  252. package/pennyfarthing_scripts/hooks/session_stop.py +111 -0
  253. package/pennyfarthing_scripts/hooks/sprint_yaml_validation.py +97 -0
  254. package/pennyfarthing_scripts/hooks/statusline.py +420 -0
  255. package/pennyfarthing_scripts/hooks.py +27 -432
  256. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  261. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  262. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  263. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  264. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/jira/__pycache__/compat.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__/mappings.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  283. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  289. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/pretooluse_hook.py +3 -185
  291. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  292. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  293. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  294. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  295. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  296. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  297. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  298. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/prime/workflow.py +2 -1
  302. package/pennyfarthing_scripts/schema_validation_hook.py +3 -298
  303. package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
  304. package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
  305. package/pennyfarthing_scripts/session_start_hook.py +4 -186
  306. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  307. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  309. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  315. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  316. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  317. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  318. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  319. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  320. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  323. package/pennyfarthing_scripts/sprint/story_update.py +19 -0
  324. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  326. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  327. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  328. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  329. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  330. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  331. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  332. package/pennyfarthing_scripts/tests/__pycache__/test_108_1_gate_migration.cpython-314-pytest-9.0.2.pyc +0 -0
  333. package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
  334. package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
  335. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  336. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  337. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  338. package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
  339. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  340. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  341. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_evaluation.cpython-314-pytest-9.0.2.pyc +0 -0
  342. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_gate.cpython-314-pytest-9.0.2.pyc +0 -0
  343. package/pennyfarthing_scripts/tests/__pycache__/test_dialogue_manager.cpython-314-pytest-9.0.2.pyc +0 -0
  344. package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
  345. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  346. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  347. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  348. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  353. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  354. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
  355. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  356. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  357. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  358. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  359. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  360. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  361. package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
  362. package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
  363. package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
  364. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  365. package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
  366. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  367. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  368. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  369. package/pennyfarthing_scripts/tests/test_sprint_panel.py +344 -265
  370. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  371. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  372. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  373. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  374. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  375. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  376. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  377. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  378. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  379. package/pennyfarthing_scripts/validate/adapters/__pycache__/tandem_awareness.cpython-314.pyc +0 -0
  380. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  381. package/pennyfarthing_scripts/validate/adapters/workflow.py +19 -0
  382. package/pennyfarthing_scripts/welcome_hook.py +3 -149
  383. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  384. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  385. package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  386. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  387. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
  388. package/pennyfarthing_scripts/workflow/cli.py +7 -6
  389. package/pennyfarthing_scripts/workflow/team_lifecycle.py +257 -0
  390. package/packages/core/dist/scripts/theme-detail.test.d.ts +0 -10
  391. package/packages/core/dist/scripts/theme-detail.test.js +0 -199
  392. package/pennyfarthing_scripts/bikerack/__pycache__/portrait.cpython-314.pyc +0 -0
  393. package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
  394. package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
  395. package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
  396. package/pennyfarthing_scripts/git/__pycache__/hooks_installer.cpython-314.pyc +0 -0
  397. package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
  398. package/pennyfarthing_scripts/git/__pycache__/worktree.cpython-314.pyc +0 -0
  399. package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
  400. package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
  401. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  402. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  403. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -6,13 +6,13 @@ Epic: 103 — BikeRack TUI (MSSCI-14951)
6
6
  Acceptance Criteria:
7
7
  - [AC1] SprintPanel subscribes to /ws/sprint channel
8
8
  - [AC2] Receives and parses JSON payloads: {type, currentStory, nextStory, epics, ...}
9
- - [AC3] Renders sprint status as Rich table: story ID, title, status, points, Jira status
9
+ - [AC3] Renders sprint status with epic/story tree hierarchy
10
10
  - [AC4] Displays velocity and sprint metrics
11
11
  - [AC5] Default panel on TUI launch
12
12
  - [AC6] Updates in real-time when data changes on channel
13
13
  - [AC7] All tests GREEN (Dev phase)
14
14
 
15
- Tests should FAIL until sprint_panel.py is fully implemented.
15
+ Migrated to Textual Tree widget from Rich Table rendering.
16
16
  """
17
17
 
18
18
  from __future__ import annotations
@@ -21,11 +21,17 @@ from typing import Any
21
21
  from unittest.mock import MagicMock, patch
22
22
 
23
23
  import pytest
24
- from rich.table import Table
25
24
  from rich.text import Text
26
25
 
27
- from pennyfarthing_scripts.bikerack.base_panel import BasePanel
28
- from pennyfarthing_scripts.bikerack.sprint_panel import SprintPanel
26
+ from pennyfarthing_scripts.bikerack.sprint_panel import (
27
+ SprintPanel,
28
+ _build_epic_label,
29
+ _build_story_label,
30
+ _format_assignee,
31
+ _is_terminal,
32
+ _should_expand,
33
+ _status_badge,
34
+ )
29
35
 
30
36
  # ---------------------------------------------------------------------------
31
37
  # Fixtures
@@ -177,19 +183,20 @@ class TestSprintPanelChannel:
177
183
  panel = SprintPanel()
178
184
  assert panel.channel == "sprint"
179
185
 
180
- def test_inherits_base_panel(self) -> None:
181
- """SprintPanel should be a BasePanel subclass."""
182
- assert issubclass(SprintPanel, BasePanel)
186
+ def test_is_widget_subclass(self) -> None:
187
+ """SprintPanel should be a Widget subclass."""
188
+ from textual.widget import Widget
189
+
190
+ assert issubclass(SprintPanel, Widget)
183
191
 
184
192
  def test_subscribes_on_mount(self, panel: SprintPanel, mock_client: MagicMock) -> None:
185
193
  """SprintPanel should subscribe to 'sprint' channel on mount."""
186
194
  panel.on_mount()
187
- mock_client.subscribe.assert_called_once_with("sprint", panel.handle_message)
195
+ mock_client.subscribe.assert_called_once_with("sprint", panel._handle_ws_message)
188
196
 
189
197
  def test_no_subscribe_without_client(self) -> None:
190
198
  """SprintPanel should not crash on mount without client."""
191
199
  panel = SprintPanel(client=None)
192
- # Should not raise
193
200
  panel.on_mount()
194
201
 
195
202
 
@@ -201,117 +208,283 @@ class TestSprintPanelChannel:
201
208
  class TestSprintPanelParsing:
202
209
  """AC2: Receives and parses JSON payloads correctly."""
203
210
 
204
- def test_handles_init_message_type(self, panel: SprintPanel) -> None:
205
- """render_panel should handle 'init' type messages."""
206
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
207
- # Should return something meaningful, not empty/None
208
- assert result is not None
209
- assert result != ""
210
-
211
- def test_handles_update_message_type(self, panel: SprintPanel) -> None:
212
- """render_panel should handle 'update' type messages."""
213
- result = panel.render_panel(SAMPLE_UPDATE_PAYLOAD)
214
- assert result is not None
215
- assert result != ""
216
-
217
- def test_handles_empty_epics(self, panel: SprintPanel) -> None:
218
- """render_panel should handle payload with empty epics list."""
219
- payload = {**SAMPLE_INIT_PAYLOAD, "epics": []}
220
- result = panel.render_panel(payload)
221
- assert result is not None
222
- assert result != ""
223
-
224
- def test_handles_null_current_story(self, panel: SprintPanel) -> None:
225
- """render_panel should handle null currentStory."""
226
- payload = {**SAMPLE_INIT_PAYLOAD, "currentStory": None}
227
- result = panel.render_panel(payload)
228
- assert result is not None
229
- assert result != ""
230
-
231
- def test_handles_multi_epic_payload(self, panel: SprintPanel) -> None:
232
- """render_panel should handle payloads with multiple epics."""
233
- result = panel.render_panel(SAMPLE_MULTI_EPIC_PAYLOAD)
234
- assert result is not None
235
- assert result != ""
211
+ def test_stores_payload(self, panel: SprintPanel) -> None:
212
+ """_handle_ws_message should store the payload."""
213
+ panel._mounted = True
214
+ with patch.object(panel, "post_message"):
215
+ panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
216
+ assert panel._last_payload == SAMPLE_INIT_PAYLOAD
217
+
218
+ def test_ignores_none(self, panel: SprintPanel) -> None:
219
+ """_handle_ws_message should ignore None messages."""
220
+ panel._mounted = True
221
+ with patch.object(panel, "post_message") as mock_post:
222
+ panel._handle_ws_message(None)
223
+ mock_post.assert_not_called()
224
+
225
+ def test_ignores_after_unmount(self, panel: SprintPanel) -> None:
226
+ """Messages after unmount should be ignored."""
227
+ panel._mounted = True
228
+ panel.on_unmount()
229
+ with patch.object(panel, "post_message") as mock_post:
230
+ panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
231
+ mock_post.assert_not_called()
236
232
 
237
233
 
238
234
  # ---------------------------------------------------------------------------
239
- # AC3: Renders sprint status as Rich table with columns
235
+ # AC3: Renders sprint status with epic/story tree hierarchy
240
236
  # ---------------------------------------------------------------------------
241
237
 
242
238
 
243
- class TestSprintPanelRendering:
244
- """AC3: Renders sprint status as Rich table with correct columns."""
245
-
246
- def test_render_returns_rich_renderable(self, panel: SprintPanel) -> None:
247
- """render_panel should return a Rich renderable (Table or Group)."""
248
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
249
- # Must be a Rich renderable — at minimum, not a plain string
250
- assert not isinstance(result, str), "render_panel should return Rich renderable, not str"
251
-
252
- def test_render_contains_table(self, panel: SprintPanel) -> None:
253
- """render_panel output should contain a Rich Table."""
254
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
255
- # Result is either a Table directly or a Group containing a Table
256
- if isinstance(result, Table):
257
- table = result
258
- else:
259
- # Check if result contains a Table (for Group/renderables)
260
- tables = [r for r in getattr(result, "renderables", [result]) if isinstance(r, Table)]
261
- assert len(tables) > 0, "Output should contain at least one Rich Table"
262
- table = tables[0]
263
- assert isinstance(table, Table)
264
-
265
- def test_table_has_id_column(self, panel: SprintPanel) -> None:
266
- """Story table should have an ID column."""
267
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
268
- table = _extract_table(result)
269
- column_names = [col.header.plain if isinstance(col.header, Text) else str(col.header) for col in table.columns]
270
- assert any("id" in name.lower() for name in column_names), f"Expected 'ID' column, got: {column_names}"
271
-
272
- def test_table_has_title_column(self, panel: SprintPanel) -> None:
273
- """Story table should have a Title column."""
274
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
275
- table = _extract_table(result)
276
- column_names = [col.header.plain if isinstance(col.header, Text) else str(col.header) for col in table.columns]
277
- assert any("title" in name.lower() for name in column_names), f"Expected 'Title' column, got: {column_names}"
278
-
279
- def test_table_has_status_column(self, panel: SprintPanel) -> None:
280
- """Story table should have a Status column."""
281
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
282
- table = _extract_table(result)
283
- column_names = [col.header.plain if isinstance(col.header, Text) else str(col.header) for col in table.columns]
284
- assert any("status" in name.lower() for name in column_names), f"Expected 'Status' column, got: {column_names}"
285
-
286
- def test_table_has_points_column(self, panel: SprintPanel) -> None:
287
- """Story table should have a Points column."""
288
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
289
- table = _extract_table(result)
290
- column_names = [col.header.plain if isinstance(col.header, Text) else str(col.header) for col in table.columns]
291
- assert any("pts" in name.lower() or "points" in name.lower() for name in column_names), (
292
- f"Expected 'Points' column, got: {column_names}"
293
- )
239
+ class TestStatusBadge:
240
+ """Status badge helper produces symbol-only Rich Text (no text word)."""
241
+
242
+ def test_done_badge(self) -> None:
243
+ badge = _status_badge("done")
244
+ assert "\u2713" in badge.plain
245
+ assert "done" not in badge.plain
246
+
247
+ def test_in_progress_badge(self) -> None:
248
+ badge = _status_badge("in-progress")
249
+ assert "\u27f3" in badge.plain
250
+ assert "in-progress" not in badge.plain
251
+
252
+ def test_in_progress_underscore(self) -> None:
253
+ """Handle both 'in-progress' and 'in_progress' status strings."""
254
+ badge = _status_badge("in_progress")
255
+ assert "\u27f3" in badge.plain
256
+
257
+ def test_backlog_badge(self) -> None:
258
+ badge = _status_badge("backlog")
259
+ assert "\u25ef" in badge.plain
260
+ assert "backlog" not in badge.plain
261
+
262
+ def test_blocked_badge(self) -> None:
263
+ badge = _status_badge("blocked")
264
+ assert "!" in badge.plain
265
+ assert "blocked" not in badge.plain
266
+
267
+ def test_review_badge(self) -> None:
268
+ badge = _status_badge("review")
269
+ assert "\u25ce" in badge.plain
270
+ assert "review" not in badge.plain
271
+
272
+ def test_canceled_badge(self) -> None:
273
+ badge = _status_badge("canceled")
274
+ assert "\u2715" in badge.plain
275
+
276
+ def test_cancelled_british_spelling(self) -> None:
277
+ badge = _status_badge("cancelled")
278
+ assert "\u2715" in badge.plain
279
+
280
+ def test_unknown_status(self) -> None:
281
+ badge = _status_badge("unknown-status")
282
+ assert "\u2014" in badge.plain
283
+
284
+
285
+ class TestEpicLabel:
286
+ """Epic label builder produces correct Rich Text."""
287
+
288
+ def test_includes_epic_id_fallback(self) -> None:
289
+ label = _build_epic_label("103", "BikeRack TUI", 4, 6)
290
+ assert "103" in label.plain
291
+
292
+ def test_includes_jira_key_when_provided(self) -> None:
293
+ label = _build_epic_label("103", "BikeRack TUI", 4, 6, jira_key="MSSCI-14510")
294
+ assert "MSSCI-14510" in label.plain
295
+
296
+ def test_long_id_gets_ellipsed(self) -> None:
297
+ label = _build_epic_label("standalone", "Standalone Stories", 2, 7, jira_key="epic-standalone")
298
+ plain = label.plain
299
+ assert "\u2026" in plain, f"Long ID should be ellipsed, got: {plain}"
300
+ # Should not exceed 11 chars for the ID portion
301
+ id_part = plain.split(" ")[0]
302
+ assert len(id_part) <= 11
303
+
304
+ def test_includes_progress(self) -> None:
305
+ label = _build_epic_label("103", "BikeRack TUI", 4, 6)
306
+ assert "4/6 pts" in label.plain
307
+
308
+ def test_includes_title(self) -> None:
309
+ label = _build_epic_label("103", "BikeRack TUI", 4, 6)
310
+ assert "BikeRack TUI" in label.plain
311
+
312
+ def test_zero_points(self) -> None:
313
+ label = _build_epic_label("100", "Empty", 0, 0)
314
+ assert "0 pts" in label.plain
315
+
316
+
317
+ class TestStoryLabel:
318
+ """Story label builder produces correct Rich Text."""
319
+
320
+ def test_includes_jira_key(self) -> None:
321
+ story = {"id": "103-1", "title": "Scaffold", "points": 2, "status": "done", "jiraKey": "MSSCI-14952"}
322
+ label = _build_story_label(story, "")
323
+ assert "MSSCI-14952" in label.plain
324
+
325
+ def test_includes_points(self) -> None:
326
+ story = {"id": "103-1", "title": "Scaffold", "points": 2, "status": "done", "jiraKey": "MSSCI-14952"}
327
+ label = _build_story_label(story, "")
328
+ assert "2" in label.plain
329
+
330
+ def test_includes_title(self) -> None:
331
+ story = {"id": "103-1", "title": "Scaffold", "points": 2, "status": "done", "jiraKey": "MSSCI-14952"}
332
+ label = _build_story_label(story, "")
333
+ assert "Scaffold" in label.plain
334
+
335
+ def test_null_jira_key_shows_dash(self) -> None:
336
+ story = {"id": "103-1", "title": "Test", "points": 1, "status": "backlog", "jiraKey": None}
337
+ label = _build_story_label(story, "")
338
+ assert "\u2014" in label.plain
339
+
340
+ def test_current_story_bolded(self) -> None:
341
+ story = {"id": "103-6", "title": "Current", "points": 2, "status": "in-progress", "jiraKey": "X"}
342
+ label = _build_story_label(story, "103-6")
343
+ has_bold = any("bold" in str(span.style) for span in label._spans)
344
+ assert has_bold, "Current story should have bold styling"
345
+
346
+ def test_done_story_is_dim(self) -> None:
347
+ story = {"id": "103-1", "title": "Done one", "points": 2, "status": "done", "jiraKey": "MSSCI-14952"}
348
+ label = _build_story_label(story, "")
349
+ # Overall dim styling applied to done stories
350
+ has_dim = any("dim" in str(span.style) for span in label._spans)
351
+ assert has_dim, "Done story should have dim styling"
352
+
353
+
354
+ class TestFormatAssignee:
355
+ """Email to display name formatting."""
356
+
357
+ def test_standard_email(self) -> None:
358
+ assert _format_assignee("keith.avery@1898andco.io") == "K. Avery"
359
+
360
+ def test_underscore_email(self) -> None:
361
+ assert _format_assignee("john_doe@example.com") == "J. Doe"
362
+
363
+ def test_none_returns_empty(self) -> None:
364
+ assert _format_assignee(None) == ""
365
+
366
+ def test_empty_string_returns_empty(self) -> None:
367
+ assert _format_assignee("") == ""
368
+
369
+ def test_single_part_local(self) -> None:
370
+ result = _format_assignee("admin@example.com")
371
+ assert result == "Admin"
372
+
373
+
374
+ class TestStoryLabelOwner:
375
+ """Owner shown for in-progress stories, hidden for done/backlog."""
376
+
377
+ def test_in_progress_shows_owner(self) -> None:
378
+ story = {
379
+ "id": "110-2", "title": "Drill", "points": 5,
380
+ "status": "in-progress", "jiraKey": "MSSCI-15186",
381
+ "assignee": "keith.avery@1898andco.io",
382
+ }
383
+ label = _build_story_label(story, "")
384
+ assert "K. Avery" in label.plain
385
+
386
+ def test_done_hides_owner(self) -> None:
387
+ story = {
388
+ "id": "110-1", "title": "Done", "points": 3,
389
+ "status": "done", "jiraKey": "MSSCI-15185",
390
+ "assignee": "keith.avery@1898andco.io",
391
+ }
392
+ label = _build_story_label(story, "")
393
+ assert "K. Avery" not in label.plain
394
+
395
+ def test_backlog_hides_owner(self) -> None:
396
+ story = {
397
+ "id": "110-3", "title": "Backlog", "points": 3,
398
+ "status": "backlog", "jiraKey": "MSSCI-15187",
399
+ "assignee": "keith.avery@1898andco.io",
400
+ }
401
+ label = _build_story_label(story, "")
402
+ assert "K. Avery" not in label.plain
403
+
404
+ def test_in_progress_no_assignee(self) -> None:
405
+ story = {
406
+ "id": "110-2", "title": "Drill", "points": 5,
407
+ "status": "in-progress", "jiraKey": "MSSCI-15186",
408
+ }
409
+ label = _build_story_label(story, "")
410
+ assert "[" not in label.plain or "[]" not in label.plain
411
+
412
+ def test_canceled_story_is_dim(self) -> None:
413
+ story = {
414
+ "id": "110-4", "title": "Canceled one", "points": 2,
415
+ "status": "canceled", "jiraKey": "MSSCI-15999",
416
+ }
417
+ label = _build_story_label(story, "")
418
+ has_dim = any("dim" in str(span.style) for span in label._spans)
419
+ assert has_dim, "Canceled story should have dim styling"
420
+
421
+
422
+ class TestShouldExpand:
423
+ """Default expand logic for epics."""
424
+
425
+ def test_expands_with_incomplete_work(self) -> None:
426
+ epic = {"stories": [
427
+ {"points": 2, "status": "done"},
428
+ {"points": 3, "status": "in-progress"},
429
+ ]}
430
+ assert _should_expand(epic) is True
431
+
432
+ def test_collapses_when_all_done(self) -> None:
433
+ epic = {"stories": [
434
+ {"points": 2, "status": "done"},
435
+ {"points": 3, "status": "done"},
436
+ ]}
437
+ assert _should_expand(epic) is False
438
+
439
+ def test_expands_when_backlog_remains(self) -> None:
440
+ epic = {"stories": [
441
+ {"points": 2, "status": "done"},
442
+ {"points": 3, "status": "backlog"},
443
+ ]}
444
+ assert _should_expand(epic) is True
445
+
446
+ def test_expands_empty_epic(self) -> None:
447
+ epic = {"stories": []}
448
+ assert _should_expand(epic) is False
294
449
 
295
- def test_table_has_jira_column(self, panel: SprintPanel) -> None:
296
- """Story table should have a Jira column."""
297
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
298
- table = _extract_table(result)
299
- column_names = [col.header.plain if isinstance(col.header, Text) else str(col.header) for col in table.columns]
300
- assert any("jira" in name.lower() for name in column_names), f"Expected 'Jira' column, got: {column_names}"
450
+ def test_collapses_canceled_epic(self) -> None:
451
+ epic = {"status": "canceled", "stories": [
452
+ {"points": 2, "status": "backlog"},
453
+ ]}
454
+ assert _should_expand(epic) is False
301
455
 
302
- def test_table_contains_story_rows(self, panel: SprintPanel) -> None:
303
- """Table should contain rows for stories from epics."""
304
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
305
- table = _extract_table(result)
306
- # SAMPLE_INIT_PAYLOAD has 3 stories in 1 epic
307
- assert table.row_count >= 3, f"Expected at least 3 rows, got {table.row_count}"
456
+ def test_collapses_when_all_done_or_canceled(self) -> None:
457
+ epic = {"stories": [
458
+ {"points": 2, "status": "done"},
459
+ {"points": 3, "status": "canceled"},
460
+ ]}
461
+ assert _should_expand(epic) is False
308
462
 
309
- def test_table_contains_stories_from_all_epics(self, panel: SprintPanel) -> None:
310
- """Table should include stories from ALL epics, not just the first."""
311
- result = panel.render_panel(SAMPLE_MULTI_EPIC_PAYLOAD)
312
- table = _extract_table(result)
313
- # 1 story from epic 101 + 2 from epic 103 = 3 total
314
- assert table.row_count >= 3, f"Expected at least 3 rows from 2 epics, got {table.row_count}"
463
+ def test_expands_when_mix_of_canceled_and_backlog(self) -> None:
464
+ epic = {"stories": [
465
+ {"points": 2, "status": "canceled"},
466
+ {"points": 3, "status": "backlog"},
467
+ ]}
468
+ assert _should_expand(epic) is True
469
+
470
+
471
+ class TestIsTerminal:
472
+ """Terminal status detection."""
473
+
474
+ def test_done_is_terminal(self) -> None:
475
+ assert _is_terminal("done") is True
476
+
477
+ def test_canceled_is_terminal(self) -> None:
478
+ assert _is_terminal("canceled") is True
479
+
480
+ def test_cancelled_british_is_terminal(self) -> None:
481
+ assert _is_terminal("cancelled") is True
482
+
483
+ def test_in_progress_is_not_terminal(self) -> None:
484
+ assert _is_terminal("in-progress") is False
485
+
486
+ def test_backlog_is_not_terminal(self) -> None:
487
+ assert _is_terminal("backlog") is False
315
488
 
316
489
 
317
490
  # ---------------------------------------------------------------------------
@@ -320,38 +493,25 @@ class TestSprintPanelRendering:
320
493
 
321
494
 
322
495
  class TestSprintPanelMetrics:
323
- """AC4: Displays velocity and sprint metrics."""
324
-
325
- def test_output_contains_velocity(self, panel: SprintPanel) -> None:
326
- """Output should display velocity metric."""
327
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
328
- rendered_str = _render_to_string(result)
329
- assert "8" in rendered_str, "Velocity value (8) should appear in output"
330
-
331
- def test_output_contains_sprint_name(self, panel: SprintPanel) -> None:
332
- """Output should display sprint name."""
333
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
334
- rendered_str = _render_to_string(result)
335
- assert "2606" in rendered_str, "Sprint number (2606) should appear in output"
336
-
337
- def test_output_contains_done_count(self, panel: SprintPanel) -> None:
338
- """Output should display done points count."""
339
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
340
- rendered_str = _render_to_string(result)
341
- assert "71" in rendered_str, "Done count (71) should appear in output"
342
-
343
- def test_output_contains_remaining_count(self, panel: SprintPanel) -> None:
344
- """Output should display remaining points count."""
345
- result = panel.render_panel(SAMPLE_INIT_PAYLOAD)
346
- rendered_str = _render_to_string(result)
347
- assert "128" in rendered_str, "Remaining count (128) should appear in output"
348
-
349
- def test_metrics_update_with_new_data(self, panel: SprintPanel) -> None:
350
- """Metrics should reflect updated payload values."""
351
- result = panel.render_panel(SAMPLE_UPDATE_PAYLOAD)
352
- rendered_str = _render_to_string(result)
353
- assert "9" in rendered_str, "Updated velocity (9) should appear in output"
354
- assert "73" in rendered_str, "Updated done count (73) should appear in output"
496
+ """AC4: Metrics are included in sprint header text."""
497
+
498
+ def test_header_format(self) -> None:
499
+ """Header text builder includes key metrics."""
500
+ # Test by checking the header text that _rebuild_tree would produce
501
+ sprint = SAMPLE_INIT_PAYLOAD["sprint"]
502
+ metrics = SAMPLE_INIT_PAYLOAD["metrics"]
503
+ header = Text.from_markup(
504
+ f"Sprint {sprint.get('number', '')} "
505
+ f"[green]Done: {sprint.get('done', 0)}[/green] | "
506
+ f"Remaining: {sprint.get('remaining', 0)} | "
507
+ f"In Progress: {sprint.get('inProgress', 0)} | "
508
+ f"Velocity: {metrics.get('velocity', 0)}"
509
+ )
510
+ plain = header.plain
511
+ assert "2606" in plain
512
+ assert "71" in plain
513
+ assert "128" in plain
514
+ assert "8" in plain
355
515
 
356
516
 
357
517
  # ---------------------------------------------------------------------------
@@ -373,8 +533,6 @@ class TestDefaultPanel:
373
533
  async with app.run_test():
374
534
  panels = app.query(SprintPanel)
375
535
  assert len(panels) > 0, "SprintPanel should be mounted as default panel"
376
- panel = panels.first()
377
- assert panel.id == "sprint-panel"
378
536
 
379
537
 
380
538
  # ---------------------------------------------------------------------------
@@ -385,55 +543,30 @@ class TestDefaultPanel:
385
543
  class TestSprintPanelRealtime:
386
544
  """AC6: Updates in real-time when data changes on channel."""
387
545
 
388
- def test_handle_message_calls_render_panel(self, panel: SprintPanel) -> None:
389
- """handle_message should call render_panel with the payload."""
390
- panel._mounted = True
391
- with patch.object(panel, "render_panel", return_value="rendered") as mock_render:
392
- with patch.object(panel, "update"):
393
- panel.handle_message(SAMPLE_INIT_PAYLOAD)
394
- mock_render.assert_called_once_with(SAMPLE_INIT_PAYLOAD)
395
-
396
- def test_handle_message_updates_widget(self, panel: SprintPanel) -> None:
397
- """handle_message should call self.update() with rendered output."""
398
- panel._mounted = True
399
- with patch.object(panel, "render_panel", return_value="rendered"):
400
- with patch.object(panel, "update") as mock_update:
401
- panel.handle_message(SAMPLE_INIT_PAYLOAD)
402
- mock_update.assert_called_once_with("rendered")
403
-
404
- def test_sequential_updates_re_render(self, panel: SprintPanel) -> None:
405
- """Multiple messages should each trigger a re-render."""
406
- panel._mounted = True
407
- with patch.object(panel, "render_panel", return_value="rendered"):
408
- with patch.object(panel, "update") as mock_update:
409
- panel.handle_message(SAMPLE_INIT_PAYLOAD)
410
- panel.handle_message(SAMPLE_UPDATE_PAYLOAD)
411
- assert mock_update.call_count == 2
412
-
413
- def test_ignores_none_messages(self, panel: SprintPanel) -> None:
414
- """None messages should be silently ignored."""
546
+ def test_handle_message_posts_data_received(self, panel: SprintPanel) -> None:
547
+ """_handle_ws_message should post DataReceived message."""
415
548
  panel._mounted = True
416
- with patch.object(panel, "render_panel") as mock_render:
417
- with patch.object(panel, "update"):
418
- panel.handle_message(None)
419
- mock_render.assert_not_called()
420
-
421
- def test_ignores_messages_after_unmount(self, panel: SprintPanel) -> None:
422
- """Messages after unmount should be silently ignored."""
549
+ with patch.object(panel, "post_message") as mock_post:
550
+ panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
551
+ assert mock_post.call_count == 1
552
+ event = mock_post.call_args[0][0]
553
+ assert isinstance(event, SprintPanel.DataReceived)
554
+ assert event.payload == SAMPLE_INIT_PAYLOAD
555
+
556
+ def test_sequential_updates(self, panel: SprintPanel) -> None:
557
+ """Multiple messages should each trigger a post_message."""
423
558
  panel._mounted = True
424
- panel.on_unmount()
425
- with patch.object(panel, "render_panel") as mock_render:
426
- with patch.object(panel, "update"):
427
- panel.handle_message(SAMPLE_INIT_PAYLOAD)
428
- mock_render.assert_not_called()
559
+ with patch.object(panel, "post_message") as mock_post:
560
+ panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
561
+ panel._handle_ws_message(SAMPLE_UPDATE_PAYLOAD)
562
+ assert mock_post.call_count == 2
429
563
 
430
564
  def test_stores_last_payload(self, panel: SprintPanel) -> None:
431
- """handle_message should store the last payload."""
565
+ """_handle_ws_message should store the last payload."""
432
566
  panel._mounted = True
433
- with patch.object(panel, "render_panel", return_value="rendered"):
434
- with patch.object(panel, "update"):
435
- panel.handle_message(SAMPLE_INIT_PAYLOAD)
436
- assert panel._last_payload == SAMPLE_INIT_PAYLOAD
567
+ with patch.object(panel, "post_message"):
568
+ panel._handle_ws_message(SAMPLE_INIT_PAYLOAD)
569
+ assert panel._last_payload == SAMPLE_INIT_PAYLOAD
437
570
 
438
571
 
439
572
  # ---------------------------------------------------------------------------
@@ -444,46 +577,27 @@ class TestSprintPanelRealtime:
444
577
  class TestSprintPanelEdgeCases:
445
578
  """Edge cases and robustness tests."""
446
579
 
447
- def test_handles_missing_metrics(self, panel: SprintPanel) -> None:
448
- """render_panel should handle payload without metrics key."""
449
- payload = {k: v for k, v in SAMPLE_INIT_PAYLOAD.items() if k != "metrics"}
450
- # Should not raise
451
- result = panel.render_panel(payload)
452
- assert result is not None
453
-
454
- def test_handles_missing_sprint(self, panel: SprintPanel) -> None:
455
- """render_panel should handle payload without sprint key."""
456
- payload = {k: v for k, v in SAMPLE_INIT_PAYLOAD.items() if k != "sprint"}
457
- result = panel.render_panel(payload)
458
- assert result is not None
459
-
460
- def test_handles_empty_stories_in_epic(self, panel: SprintPanel) -> None:
461
- """render_panel should handle epic with empty stories list."""
462
- payload = {
463
- **SAMPLE_INIT_PAYLOAD,
464
- "epics": [{"id": "100", "title": "Empty Epic", "jiraKey": "MSSCI-10000", "stories": []}],
465
- }
466
- result = panel.render_panel(payload)
467
- assert result is not None
468
-
469
- def test_handles_story_with_null_jira_key(self, panel: SprintPanel) -> None:
470
- """render_panel should handle story with null jiraKey."""
471
- payload = {
472
- **SAMPLE_INIT_PAYLOAD,
473
- "epics": [
474
- {
475
- "id": "103",
476
- "title": "Test",
477
- "jiraKey": None,
478
- "stories": [
479
- {"id": "103-99", "title": "No Jira", "points": 1, "status": "backlog", "jiraKey": None},
480
- ],
481
- },
482
- ],
483
- }
484
- # Should not raise
485
- result = panel.render_panel(payload)
486
- assert result is not None
580
+ def test_handles_missing_metrics(self) -> None:
581
+ """Label builder handles payload without metrics key."""
582
+ sprint = {"number": "2606", "done": 0, "remaining": 0, "inProgress": 0}
583
+ header = Text.from_markup(
584
+ f"Sprint {sprint.get('number', '')} "
585
+ f"[green]Done: {sprint.get('done', 0)}[/green] | "
586
+ f"Remaining: {sprint.get('remaining', 0)} | "
587
+ f"Velocity: 0"
588
+ )
589
+ assert header.plain is not None
590
+
591
+ def test_status_badge_empty_string(self) -> None:
592
+ """Status badge handles empty string."""
593
+ badge = _status_badge("")
594
+ assert badge is not None
595
+
596
+ def test_story_label_missing_fields(self) -> None:
597
+ """Story label handles minimal story dict."""
598
+ story: dict[str, Any] = {"id": "X", "title": "", "points": 0, "status": "", "jiraKey": None}
599
+ label = _build_story_label(story, "")
600
+ assert label is not None
487
601
 
488
602
 
489
603
  # ---------------------------------------------------------------------------
@@ -491,41 +605,6 @@ class TestSprintPanelEdgeCases:
491
605
  # ---------------------------------------------------------------------------
492
606
 
493
607
 
494
- def _extract_table(result: Any) -> Table:
495
- """Extract a Rich Table from render_panel output.
496
-
497
- Handles both direct Table returns and Group/container returns.
498
- Raises AssertionError if no Table found.
499
- """
500
- if isinstance(result, Table):
501
- return result
502
-
503
- # Check renderables in Group
504
- renderables = getattr(result, "renderables", [])
505
- for r in renderables:
506
- if isinstance(r, Table):
507
- return r
508
-
509
- raise AssertionError(
510
- f"Expected Rich Table in output, got {type(result).__name__}: {result!r}"
511
- )
512
-
513
-
514
608
  async def _noop_coroutine() -> None:
515
609
  """No-op coroutine for mocking async client.connect()."""
516
610
  pass
517
-
518
-
519
- def _render_to_string(result: Any) -> str:
520
- """Render a Rich renderable to plain string for content assertions.
521
-
522
- Uses Rich Console with no color to get plain text output.
523
- """
524
- from io import StringIO
525
-
526
- from rich.console import Console
527
-
528
- buffer = StringIO()
529
- console = Console(file=buffer, no_color=True, width=120)
530
- console.print(result)
531
- return buffer.getvalue()