@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
@@ -2,6 +2,8 @@
2
2
 
3
3
  Story 103-14: Subscribes to /ws/git, extracts dirtyFiles from all repos,
4
4
  renders Rich table with file path, change type icon, and status.
5
+
6
+ Story 110-1: Selectable file list with arrow navigation and Enter to navigate.
5
7
  """
6
8
 
7
9
  from __future__ import annotations
@@ -9,6 +11,7 @@ from __future__ import annotations
9
11
  from typing import Any
10
12
 
11
13
  from rich.text import Text
14
+ from textual.binding import Binding
12
15
 
13
16
  from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
14
17
 
@@ -55,15 +58,98 @@ class ChangedPanel(BasePanel):
55
58
 
56
59
  Subscribes to the ``git`` WebSocket channel and renders
57
60
  dirty files from all repos as a Rich table with file path,
58
- change type icon, and status.
61
+ change type icon, and status. Supports arrow-key selection
62
+ and Enter to navigate to diffs.
59
63
  """
60
64
 
61
65
  channel: str = "git"
62
66
  panel_name: str = "Changed"
63
67
  icon: str = PANEL_ICONS["changed"][0]
68
+ can_focus = True
69
+
70
+ BINDINGS = [
71
+ Binding("up", "select_prev_key", "Up"),
72
+ Binding("down", "select_next_key", "Down"),
73
+ Binding("enter", "select_file", "Select file"),
74
+ ]
75
+
76
+ def __init__(self, client=None, **kwargs):
77
+ super().__init__(client=client, **kwargs)
78
+ self._selected_index: int = 0
79
+ self._file_paths: list[str] = []
80
+
81
+ def handle_message(self, message: dict[str, Any] | None) -> None:
82
+ """Handle incoming message — build file path index then render."""
83
+ if message is not None:
84
+ self._build_file_paths(message)
85
+ super().handle_message(message)
86
+
87
+ def _build_file_paths(self, payload: dict[str, Any]) -> None:
88
+ """Extract flat list of file paths from repos payload."""
89
+ paths: list[str] = []
90
+ repos = payload.get("repos", [])
91
+ if isinstance(repos, list):
92
+ for repo in repos:
93
+ if not isinstance(repo, dict):
94
+ continue
95
+ dirty_files = repo.get("dirtyFiles", [])
96
+ if not isinstance(dirty_files, list):
97
+ continue
98
+ for f in dirty_files:
99
+ if isinstance(f, dict):
100
+ path = f.get("path", "")
101
+ if path:
102
+ paths.append(path)
103
+ self._file_paths = paths
104
+ if self._selected_index >= len(paths):
105
+ self._selected_index = max(0, len(paths) - 1)
106
+
107
+ def select_next(self) -> None:
108
+ """Move selection to the next file."""
109
+ if self._file_paths and self._selected_index < len(self._file_paths) - 1:
110
+ self._selected_index += 1
111
+ self._rerender()
112
+
113
+ def select_prev(self) -> None:
114
+ """Move selection to the previous file."""
115
+ if self._selected_index > 0:
116
+ self._selected_index -= 1
117
+ self._rerender()
118
+
119
+ def action_select_next_key(self) -> None:
120
+ """Binding action: move selection down."""
121
+ self.select_next()
122
+
123
+ def action_select_prev_key(self) -> None:
124
+ """Binding action: move selection up."""
125
+ self.select_prev()
126
+
127
+ def _rerender(self) -> None:
128
+ """Re-render panel with current payload after selection change."""
129
+ if self._last_payload:
130
+ try:
131
+ self.update(self.render_panel(self._last_payload))
132
+ except Exception:
133
+ pass
134
+
135
+ def get_selected_path(self) -> str | None:
136
+ """Return the currently selected file path, or None if empty."""
137
+ if not self._file_paths:
138
+ return None
139
+ if self._selected_index >= len(self._file_paths):
140
+ return None
141
+ return self._file_paths[self._selected_index]
142
+
143
+ def action_select_file(self) -> None:
144
+ """Post NavigateToFile event for the selected file."""
145
+ path = self.get_selected_path()
146
+ if path is not None:
147
+ from pennyfarthing_scripts.bikerack.events import NavigateToFile
148
+
149
+ self.post_message(NavigateToFile(path=path))
64
150
 
65
151
  def render_panel(self, payload: dict[str, Any]) -> Any:
66
- """Render changed files grouped by repository."""
152
+ """Render changed files grouped by repository with selection highlight."""
67
153
  repos = payload.get("repos", [])
68
154
  if not isinstance(repos, list):
69
155
  return Text("No changed files", style="dim italic")
@@ -85,6 +171,7 @@ class ChangedPanel(BasePanel):
85
171
  from rich.console import Group as RichGroup
86
172
 
87
173
  parts: list[Any] = []
174
+ flat_idx = 0
88
175
  for repo_name, files in repo_files.items():
89
176
  count = len(files)
90
177
  label = "file" if count == 1 else "files"
@@ -97,12 +184,17 @@ class ChangedPanel(BasePanel):
97
184
  status_code = f.get("status", " ")
98
185
  path = f.get("path", "")
99
186
  icon, label_text, style = _parse_status(status_code)
187
+ is_selected = flat_idx == self._selected_index
100
188
  line = Text()
101
- line.append(" ")
189
+ if is_selected:
190
+ line.append("› ", style="bold reverse")
191
+ else:
192
+ line.append(" ")
102
193
  line.append(icon, style=f"bold {style}")
103
- line.append(f" {path}", style="cyan")
194
+ line.append(f" {path}", style="bold cyan reverse" if is_selected else "cyan")
104
195
  line.append(f" {label_text}", style=style)
105
196
  parts.append(line)
197
+ flat_idx += 1
106
198
 
107
199
  parts.append(Text("")) # spacer between repos
108
200
 
@@ -0,0 +1,88 @@
1
+ """ContextMeterFooter — Persistent context usage footer bar for BikeRack TUI.
2
+
3
+ Story 110-5: Context meter footer bar. Displays context window usage
4
+ percentage with color-coded tier thresholds, always visible at the
5
+ bottom of the layout.
6
+
7
+ Subscribes to /ws/context WebSocket channel.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from rich.text import Text
15
+ from textual.message import Message
16
+ from textual.widgets import Static
17
+
18
+ from pennyfarthing_scripts.bikerack.base_panel import render_progress_bar
19
+
20
+
21
+ class ContextMeterFooter(Static):
22
+ """Persistent footer bar showing context window usage.
23
+
24
+ Not a Footer subclass — this is a Static widget mounted between
25
+ #main-content and BindingFooter in the app layout.
26
+ """
27
+
28
+ class MeterUpdate(Message, bubble=False):
29
+ """Context meter data received — routed through Textual message system."""
30
+
31
+ def __init__(self, content: Any) -> None:
32
+ super().__init__()
33
+ self.content = content
34
+
35
+ #: WebSocket channel this footer subscribes to
36
+ channel: str = "context"
37
+
38
+ def __init__(self, client: Any = None, **kwargs: Any) -> None:
39
+ super().__init__(**kwargs)
40
+ self._client = client
41
+ self._context_data: dict[str, Any] | None = None
42
+ self._mounted = False
43
+
44
+ def on_mount(self) -> None:
45
+ """Subscribe to context channel on mount."""
46
+ self._mounted = True
47
+ if self._client is not None:
48
+ self._client.subscribe("context", self.handle_context_message)
49
+
50
+ def on_unmount(self) -> None:
51
+ """Mark as unmounted so further messages are ignored."""
52
+ self._mounted = False
53
+
54
+ def on_context_meter_footer_meter_update(self, event: MeterUpdate) -> None:
55
+ """Process MeterUpdate in Textual message context — triggers repaint."""
56
+ self.update(event.content)
57
+
58
+ def handle_context_message(self, msg: dict[str, Any] | None) -> None:
59
+ """Process incoming /ws/context message."""
60
+ if not self._mounted or msg is None:
61
+ return
62
+ ctx = msg.get("context")
63
+ if ctx is None:
64
+ return
65
+ self._context_data = ctx
66
+ try:
67
+ rendered = self.render_meter(ctx)
68
+ self.post_message(self.MeterUpdate(rendered))
69
+ except Exception:
70
+ pass
71
+
72
+ def render_meter(self, ctx: dict[str, Any]) -> Text:
73
+ """Render a compact context usage bar with percentage and tier badge."""
74
+ percent = ctx.get("percent", 0)
75
+ tier = ctx.get("tier", "")
76
+
77
+ bar = render_progress_bar(percent, warn_high=True)
78
+
79
+ if tier:
80
+ if percent < 50:
81
+ tier_style = "green"
82
+ elif percent <= 80:
83
+ tier_style = "yellow"
84
+ else:
85
+ tier_style = "red"
86
+ bar.append(f" {tier}", style=f"bold {tier_style}")
87
+
88
+ return bar
@@ -103,7 +103,7 @@ class DebugPanel(BasePanel):
103
103
  """Re-render with the latest data from both channels."""
104
104
  rendered = self.render_panel(self._context_data or {})
105
105
  try:
106
- self.update(rendered)
106
+ self._thread_safe_update(rendered)
107
107
  except Exception:
108
108
  pass
109
109
 
@@ -17,6 +17,7 @@ from typing import Any
17
17
  from rich.console import Group
18
18
  from rich.syntax import Syntax
19
19
  from rich.text import Text
20
+ from textual.binding import Binding
20
21
 
21
22
  from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
22
23
 
@@ -46,6 +47,12 @@ class DiffsPanel(BasePanel):
46
47
  channel: str = "diffs"
47
48
  panel_name: str = "Diffs"
48
49
  icon: str = PANEL_ICONS["diffs"][0]
50
+ can_focus = True
51
+
52
+ BINDINGS = [
53
+ Binding("n", "next_file_key", "Next file"),
54
+ Binding("p", "prev_file_key", "Prev file"),
55
+ ]
49
56
 
50
57
  def __init__(self, client=None, **kwargs):
51
58
  super().__init__(client=client, **kwargs)
@@ -87,6 +94,29 @@ class DiffsPanel(BasePanel):
87
94
  except Exception:
88
95
  pass
89
96
 
97
+ def navigate_to_file(self, path: str) -> None:
98
+ """Jump to a specific file by path. No-op if not found."""
99
+ if self._last_payload is None:
100
+ return
101
+ diffs = self._last_payload.get("diffs", [])
102
+ for i, d in enumerate(diffs):
103
+ if d.get("path") == path:
104
+ self._current_file_index = i
105
+ rendered = self.render_panel(self._last_payload)
106
+ try:
107
+ self.update(rendered)
108
+ except Exception:
109
+ pass
110
+ return
111
+
112
+ def action_next_file_key(self) -> None:
113
+ """Binding action: advance to next file."""
114
+ self.next_file()
115
+
116
+ def action_prev_file_key(self) -> None:
117
+ """Binding action: go to previous file."""
118
+ self.prev_file()
119
+
90
120
  def handle_message(self, message: dict[str, Any] | None) -> None:
91
121
  """Handle incoming WebSocket message with pagination reset and temp management."""
92
122
  if not self._mounted or message is None:
@@ -0,0 +1,28 @@
1
+ """Cross-panel event bus for BikeRack TUI (Story 110-1).
2
+
3
+ Defines Textual Message subclasses for inter-panel communication.
4
+ First use case: NavigateToFile posted by ChangedPanel, handled by App
5
+ to switch to DiffsPanel filtered to that file.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from textual.message import Message
11
+
12
+
13
+ class PanelEvent(Message):
14
+ """Base message class for cross-panel communication."""
15
+
16
+ pass
17
+
18
+
19
+ class NavigateToFile(PanelEvent):
20
+ """Navigate to a specific file in the diffs panel.
21
+
22
+ Posted by ChangedPanel when user presses Enter on a selected file.
23
+ Handled by BikeRackApp to switch to DiffsPanel and navigate to that file.
24
+ """
25
+
26
+ def __init__(self, path: str) -> None:
27
+ super().__init__()
28
+ self.path = path
@@ -0,0 +1,139 @@
1
+ """Portrait path resolution for BikeRack TUI.
2
+
3
+ Resolves persona portrait image paths using the canonical theme discovery
4
+ from ``pennyfarthing_scripts.common.themes``. Each theme directory that
5
+ contains ``themes/{name}.yaml`` has a sibling ``portraits/{name}/`` with
6
+ size-bucketed portrait images.
7
+
8
+ Story 110-3: Portrait image header with textual-image.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from pathlib import Path
15
+
16
+ import yaml
17
+
18
+
19
+ def _to_slug(name: str) -> str:
20
+ """Convert a name to URL-safe slug (lowercase kebab-case)."""
21
+ return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
22
+
23
+
24
+ def _extract_agent_slug(theme_yaml: Path, agent: str) -> str | None:
25
+ """Extract portrait slug (shortName-OCEAN) from a theme YAML file."""
26
+ if not theme_yaml.exists():
27
+ return None
28
+ try:
29
+ data = yaml.safe_load(theme_yaml.read_text())
30
+ agent_data = (data or {}).get("agents", {}).get(agent)
31
+ if not agent_data:
32
+ return None
33
+ short_name = agent_data.get("shortName") or (
34
+ agent_data.get("character", "").split()[0] if agent_data.get("character") else None
35
+ )
36
+ ocean = agent_data.get("ocean", {})
37
+ if short_name and all(k in ocean for k in "OCEAN"):
38
+ return f"{_to_slug(short_name)}-{ocean['O']}{ocean['C']}{ocean['E']}{ocean['A']}{ocean['N']}"
39
+ except Exception:
40
+ pass
41
+ return None
42
+
43
+
44
+ def _find_portrait(portraits_theme_dir: Path, slug: str) -> Path | None:
45
+ """Find a portrait file matching the slug in a theme's portrait directory."""
46
+ if not portraits_theme_dir.is_dir():
47
+ return None
48
+ for size in ["medium", "large", "small", "original"]:
49
+ size_dir = portraits_theme_dir / size
50
+ if size_dir.is_dir():
51
+ for f in size_dir.iterdir():
52
+ if f.name.lower().startswith(slug.lower()) and f.suffix in (".png", ".jpg"):
53
+ return f
54
+ # Fallback to root of theme dir
55
+ for f in portraits_theme_dir.iterdir():
56
+ if f.is_file() and f.name.lower().startswith(slug.lower()) and f.suffix in (".png", ".jpg"):
57
+ return f
58
+ return None
59
+
60
+
61
+ def resolve_portrait_path(
62
+ theme: str, agent: str, project_root: Path | None = None
63
+ ) -> Path | None:
64
+ """Resolve the full path to a portrait image.
65
+
66
+ Uses ``discover_all_theme_dirs`` from ``common.themes`` to search core
67
+ themes, installed theme packages, monorepo workspace packages, and
68
+ custom themes — in canonical priority order.
69
+
70
+ For each theme directory the portrait sibling is derived:
71
+ - ``.pennyfarthing/personas/themes/`` → ``.pennyfarthing/personas/portraits/``
72
+ - ``themes-*/themes/`` → ``themes-*/portraits/``
73
+
74
+ Args:
75
+ theme: Theme name (e.g., 'hogans-heroes', 'monty-python')
76
+ agent: Agent role (e.g., 'sm', 'tea', 'dev')
77
+ project_root: Project root for path resolution. Defaults to cwd.
78
+
79
+ Returns:
80
+ Path to portrait file, or None if not found.
81
+ """
82
+ from pennyfarthing_scripts.common.themes import discover_all_theme_dirs
83
+
84
+ theme_dirs = discover_all_theme_dirs(project_root)
85
+
86
+ # Resolve slug from the first theme dir that has this theme's YAML
87
+ slug: str | None = None
88
+ for themes_dir in theme_dirs:
89
+ theme_yaml = themes_dir / f"{theme}.yaml"
90
+ slug = _extract_agent_slug(theme_yaml, agent)
91
+ if slug:
92
+ break
93
+
94
+ if not slug:
95
+ return None
96
+
97
+ # Search portrait directories (sibling of each themes dir)
98
+ for themes_dir in theme_dirs:
99
+ portraits_dir = themes_dir.parent / "portraits" / theme
100
+ result = _find_portrait(portraits_dir, slug)
101
+ if result:
102
+ return result
103
+
104
+ return None
105
+
106
+
107
+ def detect_image_protocol() -> str | None:
108
+ """Detect the best available terminal image protocol.
109
+
110
+ Uses environment variables for reliable detection since subprocess
111
+ stdout may not be a TTY (e.g., when launched via Claude Code).
112
+
113
+ Returns:
114
+ Protocol name ('kitty', 'sixel', 'halfcell', None for unsupported).
115
+ """
116
+ import os
117
+
118
+ # Kitty: TERM=xterm-kitty or KITTY_WINDOW_ID present
119
+ term = os.environ.get("TERM", "")
120
+ if "kitty" in term or os.environ.get("KITTY_WINDOW_ID"):
121
+ return "kitty"
122
+
123
+ # Sixel: some terminals advertise via TERM or COLORTERM
124
+ # WezTerm, foot, mlterm support sixel
125
+ term_program = os.environ.get("TERM_PROGRAM", "")
126
+ if term_program.lower() in ("wezterm", "foot", "mlterm"):
127
+ return "sixel"
128
+
129
+ # Fallback: try textual-image's cell size probe for halfcell baseline
130
+ try:
131
+ from textual_image._terminal import get_cell_size
132
+
133
+ cell_size = get_cell_size()
134
+ if cell_size and cell_size.width > 0:
135
+ return "halfcell"
136
+ except Exception:
137
+ pass
138
+
139
+ return None