@pennyfarthing/core 11.1.1 → 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 (487) hide show
  1. package/README.md +8 -8
  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/otlp-receiver.d.ts +16 -11
  41. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  42. package/packages/core/dist/server/otlp-receiver.js +185 -24
  43. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  44. package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
  45. package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
  46. package/packages/core/dist/server/otlp-receiver.test.js +446 -0
  47. package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
  48. package/packages/core/dist/server/server.d.ts +0 -3
  49. package/packages/core/dist/server/server.d.ts.map +1 -1
  50. package/packages/core/dist/server/server.js +3 -37
  51. package/packages/core/dist/server/server.js.map +1 -1
  52. package/packages/core/dist/server/server.test.d.ts +1 -1
  53. package/packages/core/dist/server/server.test.js +12 -23
  54. package/packages/core/dist/server/server.test.js.map +1 -1
  55. package/packages/core/dist/shared/capabilities.d.ts +88 -0
  56. package/packages/core/dist/shared/capabilities.d.ts.map +1 -0
  57. package/packages/core/dist/shared/capabilities.js +133 -0
  58. package/packages/core/dist/shared/capabilities.js.map +1 -0
  59. package/packages/core/dist/shared/capabilities.test.d.ts +2 -0
  60. package/packages/core/dist/shared/capabilities.test.d.ts.map +1 -0
  61. package/packages/core/dist/shared/capabilities.test.js +217 -0
  62. package/packages/core/dist/shared/capabilities.test.js.map +1 -0
  63. package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
  64. package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
  65. package/packages/core/dist/shared/portrait-resolver.js +27 -0
  66. package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
  67. package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
  68. package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
  69. package/packages/core/dist/shared/spawn-prompt.d.ts +47 -0
  70. package/packages/core/dist/shared/spawn-prompt.d.ts.map +1 -0
  71. package/packages/core/dist/shared/spawn-prompt.js +82 -0
  72. package/packages/core/dist/shared/spawn-prompt.js.map +1 -0
  73. package/packages/core/dist/shared/spawn-prompt.test.d.ts +2 -0
  74. package/packages/core/dist/shared/spawn-prompt.test.d.ts.map +1 -0
  75. package/packages/core/dist/shared/spawn-prompt.test.js +251 -0
  76. package/packages/core/dist/shared/spawn-prompt.test.js.map +1 -0
  77. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
  78. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
  79. package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
  80. package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
  81. package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts +18 -0
  82. package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts.map +1 -0
  83. package/packages/core/dist/workflow/tandem-workflow-templates.test.js +434 -0
  84. package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -0
  85. package/packages/core/dist/workflow/workflow-schema.d.ts +32 -0
  86. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  87. package/packages/core/dist/workflow/workflow-schema.js +120 -0
  88. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  89. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  90. package/packages/core/dist/workflow/workflow-schema.test.js +570 -1
  91. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  92. package/pennyfarthing-dist/agents/dev.md +7 -11
  93. package/pennyfarthing-dist/agents/reviewer.md +9 -3
  94. package/pennyfarthing-dist/agents/sm-finish.md +18 -1
  95. package/pennyfarthing-dist/agents/sm-setup.md +1 -1
  96. package/pennyfarthing-dist/agents/sm.md +2 -2
  97. package/pennyfarthing-dist/agents/tea.md +1 -1
  98. package/pennyfarthing-dist/agents/testing-runner.md +2 -1
  99. package/pennyfarthing-dist/commands/pf-chore.md +2 -2
  100. package/pennyfarthing-dist/commands/pf-git.md +4 -2
  101. package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
  102. package/pennyfarthing-dist/gates/approval.md +63 -0
  103. package/pennyfarthing-dist/gates/confidence-sm.md +71 -0
  104. package/pennyfarthing-dist/gates/context-ok.md +56 -0
  105. package/pennyfarthing-dist/gates/evaluations/confidence-sm.md +54 -0
  106. package/pennyfarthing-dist/gates/quality-pass.md +67 -0
  107. package/pennyfarthing-dist/gates/tests-fail.md +84 -0
  108. package/pennyfarthing-dist/gates/tests-pass.md +79 -0
  109. package/pennyfarthing-dist/guides/agent-behavior.md +23 -19
  110. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
  111. package/pennyfarthing-dist/guides/bell-mode.md +1 -1
  112. package/pennyfarthing-dist/guides/bikerack.md +3 -3
  113. package/pennyfarthing-dist/guides/hooks.md +29 -29
  114. package/pennyfarthing-dist/guides/reflector.md +1 -1
  115. package/pennyfarthing-dist/guides/tandem-protocol.md +3 -3
  116. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  117. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  118. package/pennyfarthing-dist/scripts/README.md +1 -1
  119. package/pennyfarthing-dist/scripts/core/check-context.sh +3 -1
  120. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +5 -87
  121. package/pennyfarthing-dist/scripts/git/README.md +24 -14
  122. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
  123. package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
  124. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
  125. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
  126. package/pennyfarthing-dist/scripts/hooks/README.md +6 -6
  127. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  128. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +4 -183
  129. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +4 -95
  130. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +4 -65
  131. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +3 -31
  132. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
  133. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +27 -33
  134. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +4 -71
  135. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +3 -19
  136. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +4 -30
  137. package/pennyfarthing-dist/scripts/hooks/session-start.sh +3 -32
  138. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +4 -65
  139. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +4 -78
  140. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +4 -93
  141. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  142. package/pennyfarthing-dist/scripts/misc/statusline.sh +4 -301
  143. package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
  144. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
  145. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
  146. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
  147. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
  148. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
  149. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
  150. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
  151. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
  152. package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
  153. package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
  154. package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
  155. package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
  156. package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
  157. package/pennyfarthing-dist/templates/settings.local.json.template +19 -10
  158. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
  159. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
  160. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
  161. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  162. package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
  163. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
  164. package/pennyfarthing-dist/workflows/tdd.yaml +11 -2
  165. package/pennyfarthing_scripts/CLAUDE.md +45 -14
  166. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  167. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  172. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  183. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  184. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  185. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/bc/cli.py +3 -5
  190. package/pennyfarthing_scripts/bellmode_hook.py +12 -296
  191. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/bikerack/__pycache__/context_meter_footer.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/bikerack/__pycache__/events.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  211. package/pennyfarthing_scripts/bikerack/audit_log_panel.py +119 -0
  212. package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
  213. package/pennyfarthing_scripts/bikerack/base_panel.py +87 -2
  214. package/pennyfarthing_scripts/bikerack/changed_panel.py +125 -29
  215. package/pennyfarthing_scripts/bikerack/context_meter_footer.py +88 -0
  216. package/pennyfarthing_scripts/bikerack/debug_panel.py +32 -2
  217. package/pennyfarthing_scripts/bikerack/diffs_panel.py +104 -17
  218. package/pennyfarthing_scripts/bikerack/events.py +28 -0
  219. package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
  220. package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
  221. package/pennyfarthing_scripts/bikerack/portrait_resolver.py +139 -0
  222. package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
  223. package/pennyfarthing_scripts/bikerack/sprint_panel.py +395 -32
  224. package/pennyfarthing_scripts/bikerack/story_detail_data.py +244 -0
  225. package/pennyfarthing_scripts/bikerack/story_detail_screen.py +176 -0
  226. package/pennyfarthing_scripts/bikerack/tui.py +575 -37
  227. package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
  228. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/cli.py +42 -65
  233. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  238. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  239. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  240. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  241. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  242. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  243. package/pennyfarthing_scripts/common/pr_config.py +38 -0
  244. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/consultation/__init__.py +1 -0
  251. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/consultation/__pycache__/dialogue_manager.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/consultation/cli.py +149 -0
  255. package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
  256. package/pennyfarthing_scripts/context.py +3 -3
  257. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  261. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  262. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  263. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  264. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/git/__init__.py +12 -1
  272. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/git/create_branches.py +3 -4
  276. package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
  277. package/pennyfarthing_scripts/git/repos.py +196 -0
  278. package/pennyfarthing_scripts/git/status_all.py +27 -11
  279. package/pennyfarthing_scripts/git/worktree.py +302 -0
  280. package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/git_group/cli.py +143 -40
  283. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  289. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/handoff/cli.py +33 -1
  291. package/pennyfarthing_scripts/handoff/complete_phase.py +40 -0
  292. package/pennyfarthing_scripts/handoff/marker.py +15 -15
  293. package/pennyfarthing_scripts/handoff/phase_check.py +96 -0
  294. package/pennyfarthing_scripts/handoff/resolve_gate.py +18 -15
  295. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  296. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  297. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  298. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/hooks/__init__.py +437 -0
  302. package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  304. package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  305. package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  306. package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  307. package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  309. package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  315. package/pennyfarthing_scripts/hooks/bell_mode.py +215 -0
  316. package/pennyfarthing_scripts/hooks/cli.py +96 -0
  317. package/pennyfarthing_scripts/hooks/context_breaker.py +104 -0
  318. package/pennyfarthing_scripts/hooks/context_warning.py +66 -0
  319. package/pennyfarthing_scripts/hooks/cyclist_pretooluse.py +129 -0
  320. package/pennyfarthing_scripts/hooks/pre_edit_check.py +78 -0
  321. package/pennyfarthing_scripts/hooks/reflector_check.py +271 -0
  322. package/pennyfarthing_scripts/hooks/schema_validation.py +203 -0
  323. package/pennyfarthing_scripts/hooks/session_start.py +296 -0
  324. package/pennyfarthing_scripts/hooks/session_stop.py +111 -0
  325. package/pennyfarthing_scripts/hooks/sprint_yaml_validation.py +97 -0
  326. package/pennyfarthing_scripts/hooks/statusline.py +420 -0
  327. package/pennyfarthing_scripts/hooks.py +27 -446
  328. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  329. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  330. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  331. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  332. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  333. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  334. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  335. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  336. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  337. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  338. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  339. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  340. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  344. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  345. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  346. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  347. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  348. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  349. package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  350. package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
  351. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  352. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  353. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  354. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  355. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  356. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  357. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  358. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  359. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  360. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  361. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  362. package/pennyfarthing_scripts/pretooluse_hook.py +3 -185
  363. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  364. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  365. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  366. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  367. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  368. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  369. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  370. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  371. package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
  372. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  373. package/pennyfarthing_scripts/prime/heatmap.py +655 -0
  374. package/pennyfarthing_scripts/prime/workflow.py +2 -1
  375. package/pennyfarthing_scripts/schema_validation_hook.py +3 -298
  376. package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
  377. package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
  378. package/pennyfarthing_scripts/session_start_hook.py +4 -186
  379. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  380. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  381. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  382. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  383. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  384. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  385. package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  386. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  387. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  388. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  389. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  390. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  391. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  392. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  393. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  394. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  395. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  396. package/pennyfarthing_scripts/sprint/loader.py +15 -1
  397. package/pennyfarthing_scripts/sprint/story_update.py +19 -0
  398. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  399. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  400. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  401. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  402. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  403. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  404. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  405. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  406. package/pennyfarthing_scripts/tests/__pycache__/test_108_1_gate_migration.cpython-314-pytest-9.0.2.pyc +0 -0
  407. package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
  408. package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
  409. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  410. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  411. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  412. package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
  413. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  414. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  415. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_evaluation.cpython-314-pytest-9.0.2.pyc +0 -0
  416. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_gate.cpython-314-pytest-9.0.2.pyc +0 -0
  417. package/pennyfarthing_scripts/tests/__pycache__/test_dialogue_manager.cpython-314-pytest-9.0.2.pyc +0 -0
  418. package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
  419. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  420. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  421. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  422. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  423. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  424. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  425. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  426. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  427. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  428. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
  429. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  430. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  431. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  432. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  433. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  434. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  435. package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
  436. package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
  437. package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
  438. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  439. package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
  440. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  441. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  442. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  443. package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
  444. package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
  445. package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
  446. package/pennyfarthing_scripts/tests/test_sprint_panel.py +344 -265
  447. package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
  448. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  449. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  450. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  451. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  452. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  453. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  454. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  455. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  456. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  457. package/pennyfarthing_scripts/validate/adapters/__pycache__/tandem_awareness.cpython-314.pyc +0 -0
  458. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  459. package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
  460. package/pennyfarthing_scripts/validate/adapters/workflow.py +19 -0
  461. package/pennyfarthing_scripts/validate/cli.py +17 -5
  462. package/pennyfarthing_scripts/welcome_hook.py +3 -149
  463. package/pennyfarthing_scripts/workflow/__init__.py +40 -0
  464. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  465. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  466. package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  467. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  468. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
  469. package/pennyfarthing_scripts/workflow/cli.py +1100 -0
  470. package/pennyfarthing_scripts/workflow/helpers.py +241 -0
  471. package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
  472. package/pennyfarthing_scripts/workflow/state.py +112 -0
  473. package/pennyfarthing_scripts/workflow/team_lifecycle.py +257 -0
  474. package/packages/core/dist/scripts/theme-detail.test.d.ts +0 -10
  475. package/packages/core/dist/scripts/theme-detail.test.js +0 -199
  476. package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
  477. package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
  478. package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
  479. package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
  480. package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -167
  481. package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
  482. package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
  483. package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
  484. package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
  485. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  486. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  487. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -5,21 +5,34 @@ Story 103-4: Connection status indicator in TUI header.
5
5
  Story 103-6: SprintPanel as default panel on launch.
6
6
  Story 103-7: /bc TUI panel focus — subscribe to /ws/focus, switch panels.
7
7
  Story 103-9: Panel header chrome — icon + name indicator for active panel.
8
+ Panel navigation: Mount all panels, tab bar, keyboard switching, command palette.
8
9
  """
9
10
 
10
11
  from __future__ import annotations
11
12
 
13
+ from functools import partial
12
14
  from pathlib import Path
13
15
  from typing import Any
14
16
 
15
17
  from textual.app import App, ComposeResult
16
18
  from textual.binding import Binding
17
- from textual.containers import VerticalScroll
19
+ from textual.command import Hit, Hits, Provider
20
+ from textual.containers import Horizontal, VerticalScroll
21
+ from textual.message import Message
18
22
  from textual.reactive import reactive
19
- from textual.widgets import Footer, Header, Static
23
+ from textual.widgets import Footer, Header, Static, Tab, Tabs
20
24
 
21
25
  from pennyfarthing_scripts.bc.focus import get_last_panel, save_last_panel
26
+ from pennyfarthing_scripts.bikerack.audit_log_panel import AuditLogPanel
27
+ from pennyfarthing_scripts.bikerack.background_panel import BackgroundPanel
22
28
  from pennyfarthing_scripts.bikerack.base_panel import get_panel_icon
29
+ from pennyfarthing_scripts.bikerack.changed_panel import ChangedPanel
30
+ from pennyfarthing_scripts.bikerack.context_meter_footer import ContextMeterFooter
31
+ from pennyfarthing_scripts.bikerack.debug_panel import DebugPanel
32
+ from pennyfarthing_scripts.bikerack.diffs_panel import DiffsPanel
33
+ from pennyfarthing_scripts.bikerack.events import NavigateToFile
34
+ from pennyfarthing_scripts.bikerack.git_panel import GitPanel
35
+ from pennyfarthing_scripts.bikerack.progress_panel import ProgressPanel
23
36
  from pennyfarthing_scripts.bikerack.sprint_panel import SprintPanel
24
37
  from pennyfarthing_scripts.bikerack.ws_client import ConnectionState, WheelHubClient
25
38
 
@@ -30,7 +43,49 @@ STATE_DISPLAY: dict[ConnectionState, str] = {
30
43
  ConnectionState.CONNECTING: "[yellow]● Connecting…[/yellow]",
31
44
  }
32
45
 
33
- # Human-readable display names for panels
46
+ # Agent role colors for Rich markup (mapped from React AGENT_COLORS)
47
+ AGENT_ROLE_COLORS: dict[str, str] = {
48
+ "pm": "purple",
49
+ "sm": "blue",
50
+ "dev": "green",
51
+ "tea": "cyan",
52
+ "reviewer": "red",
53
+ "architect": "dark_orange",
54
+ "devops": "bright_cyan",
55
+ "ux-designer": "magenta",
56
+ "tech-writer": "white",
57
+ "orchestrator": "bright_magenta",
58
+ "ba": "bright_green",
59
+ }
60
+
61
+ AGENT_ABBREV: dict[str, str] = {
62
+ "pm": "PM",
63
+ "sm": "SM",
64
+ "dev": "DEV",
65
+ "tea": "TEA",
66
+ "reviewer": "REV",
67
+ "architect": "ARC",
68
+ "devops": "OPS",
69
+ "ux-designer": "UX",
70
+ "tech-writer": "TW",
71
+ "orchestrator": "ORC",
72
+ "ba": "BA",
73
+ }
74
+
75
+ # Ordered panel registry: (key, display_name, widget_class)
76
+ # Only panels with implemented widget classes are included.
77
+ PANEL_REGISTRY: list[tuple[str, str]] = [
78
+ ("sprint", "Sprint"),
79
+ ("git", "Git"),
80
+ ("diffs", "Diffs"),
81
+ ("changed", "Changed"),
82
+ ("background", "Background"),
83
+ ("audit-log", "Audit Log"),
84
+ ("debug", "Debug"),
85
+ ("progress", "Progress"),
86
+ ]
87
+
88
+ # Human-readable display names for panels (full set for external focus messages)
34
89
  PANEL_DISPLAY_NAMES: dict[str, str] = {
35
90
  "sprint": "Sprint",
36
91
  "git": "Git",
@@ -42,24 +97,223 @@ PANEL_DISPLAY_NAMES: dict[str, str] = {
42
97
  "changed": "Changed",
43
98
  "ac": "Acceptance Criteria",
44
99
  "debug": "Debug",
100
+ "progress": "Progress",
45
101
  "settings": "Settings",
46
102
  "tty": "TTY",
47
103
  }
48
104
 
105
+ # Keys from PANEL_REGISTRY for fast lookup
106
+ _PANEL_KEYS = [key for key, _ in PANEL_REGISTRY]
107
+
108
+
109
+ class BindingFooter(Footer):
110
+ """Footer subclass that exposes active binding text via render().
111
+
112
+ Textual's Footer uses compose() for visual content, so render() returns
113
+ Blank. This override makes binding descriptions available through
114
+ str(footer.render()) for programmatic inspection.
115
+ """
116
+
117
+ def render(self) -> Any:
118
+ try:
119
+ bindings = self.screen.active_bindings
120
+ parts: list[str] = []
121
+ for _, binding, _enabled, _tooltip in bindings.values():
122
+ if binding.show:
123
+ parts.append(f"{binding.key}:{binding.description}")
124
+ if parts:
125
+ return " ".join(parts)
126
+ except Exception:
127
+ pass
128
+ return super().render()
129
+
130
+
131
+ def _build_panel_tabs() -> list[Tab]:
132
+ """Build Tab widgets for each panel in the registry."""
133
+ tabs: list[Tab] = []
134
+ for panel_key, display_name in PANEL_REGISTRY:
135
+ icon = get_panel_icon(panel_key)
136
+ label = f"{icon} {display_name}" if icon else display_name
137
+ tabs.append(Tab(label, id=f"tab-{panel_key}"))
138
+ return tabs
139
+
140
+
141
+ PORTRAIT_SKELETON = """\
142
+ [dim]┌────────┐
143
+ │░░░░░░░░│
144
+ │░░░▓▓░░░│
145
+ │░░░░░░░░│
146
+ └────────┘[/dim]"""
147
+
148
+
149
+ class AgentHeader(Static):
150
+ """Displays current agent persona from WheelHub /ws/persona channel.
151
+
152
+ When a portrait image is available (resolved locally or provided via
153
+ portraitPath in persona data), mounts a Horizontal layout container.
154
+ Shows a skeleton placeholder while the image loads.
155
+ Falls back to text-only when no portrait is found.
156
+ """
157
+
158
+ class PortraitLayoutUpdate(Message):
159
+ """Internal message to update portrait layout asynchronously."""
160
+
161
+ def __init__(self, portrait_path: Path | None) -> None:
162
+ super().__init__()
163
+ self.has_portrait = portrait_path is not None
164
+ self.portrait_path = portrait_path
165
+
166
+ def __init__(self, **kwargs: Any) -> None:
167
+ super().__init__(**kwargs)
168
+ self._is_streaming: bool = False
169
+ self._persona_data: dict[str, Any] = {}
170
+ self._header_text: str = ""
171
+ self._current_portrait: Path | None = None
172
+
173
+ def _apply_persona(self, data: dict[str, Any]) -> None:
174
+ """Render persona data into the header."""
175
+ if data.get("type") == "streaming":
176
+ self._is_streaming = bool(data.get("isStreaming", False))
177
+ self._render_header()
178
+ return
179
+
180
+ self._persona_data = data
181
+ self._is_streaming = bool(data.get("isStreaming", False))
182
+ self._render_header()
183
+
184
+ def _resolve_portrait(self, data: dict[str, Any]) -> Path | None:
185
+ """Get portrait path from persona data or resolve locally."""
186
+ portrait_path = data.get("portraitPath")
187
+ if portrait_path:
188
+ p = Path(portrait_path)
189
+ if p.exists():
190
+ return p
191
+ theme = data.get("theme", "")
192
+ role = data.get("role", "")
193
+ if theme and role:
194
+ from pennyfarthing_scripts.bikerack import portrait_resolver
195
+
196
+ return portrait_resolver.resolve_portrait_path(theme, role)
197
+ return None
198
+
199
+ def _render_header(self) -> None:
200
+ """Re-render the header from stored state."""
201
+ data = self._persona_data
202
+ char = data.get("character", "")
203
+ role = data.get("role", "")
204
+ role_desc = data.get("roleDescription", "")
205
+ quote = data.get("quote", "")
206
+ theme = data.get("theme", "")
207
+
208
+ if not char:
209
+ self.update("[dim]Waiting for agent...[/dim]")
210
+ self.post_message(self.PortraitLayoutUpdate(portrait_path=None))
211
+ return
212
+
213
+ parts: list[str] = []
214
+
215
+ # Role badge — escape brackets so Rich doesn't eat them as tags
216
+ if role:
217
+ abbrev = AGENT_ABBREV.get(role, role.upper()[:3])
218
+ color = AGENT_ROLE_COLORS.get(role, "bright_magenta")
219
+ parts.append(f"[bold {color}]\\[{abbrev}][/bold {color}]")
220
+
221
+ # Character name
222
+ parts.append(f"[bold]{char}[/bold]")
223
+
224
+ # Theme name
225
+ if theme:
226
+ from pennyfarthing_scripts.bikerack.base_panel import humanize_theme
227
+
228
+ parts.append(f"[dim]{humanize_theme(theme)}[/dim]")
229
+
230
+ # Streaming indicator
231
+ if self._is_streaming:
232
+ parts.append("[bold yellow]⚡[/bold yellow]")
233
+
234
+ line = " ".join(parts)
235
+
236
+ # Catchphrase subtitle (quote is a random catchphrase from the theme)
237
+ if quote:
238
+ line += f"\n[italic dim]\"{quote}\"[/italic dim]"
239
+ elif role_desc:
240
+ line += f"\n[dim]{role_desc}[/dim]"
241
+
242
+ self._header_text = line
49
243
 
50
- class PanelIndicator(Static):
51
- """Displays the active panel's Nerd Font icon and name."""
244
+ # Check portrait and schedule layout update
245
+ portrait = self._resolve_portrait(data)
246
+ self.post_message(self.PortraitLayoutUpdate(portrait_path=portrait))
52
247
 
53
- panel_key: reactive[str] = reactive("sprint")
248
+ async def on_agent_header_portrait_layout_update(
249
+ self, event: PortraitLayoutUpdate
250
+ ) -> None:
251
+ """Mount or remove Horizontal portrait layout with text beside image.
54
252
 
55
- def watch_panel_key(self, key: str) -> None:
56
- """Update display when the active panel changes."""
57
- icon = get_panel_icon(key)
58
- name = PANEL_DISPLAY_NAMES.get(key, key.title())
59
- if icon:
60
- self.update(f"[bold]{icon} {name}[/bold]")
253
+ Shows a skeleton placeholder immediately while the real image loads
254
+ to avoid a visible blank gap during image decode/render.
255
+ """
256
+ if event.has_portrait and event.portrait_path:
257
+ if self._current_portrait == event.portrait_path:
258
+ # Same portrait — just update text label if it exists
259
+ try:
260
+ text_widget = self.query_one("#agent-text", Static)
261
+ text_widget.update(self._header_text)
262
+ except Exception:
263
+ pass
264
+ return
265
+
266
+ # New portrait or first time — full layout rebuild
267
+ for child in list(self.query("Horizontal")):
268
+ await child.remove()
269
+
270
+ # Mount skeleton + text immediately so there's no blank gap
271
+ self.update("")
272
+ self._current_portrait = event.portrait_path
273
+ skeleton = Static(PORTRAIT_SKELETON, id="portrait-skeleton")
274
+ text = Static(self._header_text, id="agent-text")
275
+ row = Horizontal(skeleton, text, id="portrait-row")
276
+ await self.mount(row)
277
+
278
+ # Now try to load the real image and swap it in
279
+ try:
280
+ from pennyfarthing_scripts.bikerack.portrait_resolver import (
281
+ detect_image_protocol,
282
+ )
283
+
284
+ protocol = detect_image_protocol()
285
+ if protocol is None:
286
+ # No image protocol — remove skeleton, fall back to text-only
287
+ for child in list(self.query("Horizontal")):
288
+ await child.remove()
289
+ self.update(self._header_text)
290
+ return
291
+
292
+ if protocol == "kitty":
293
+ from textual_image.widget import TGPImage as ImageWidget
294
+ elif protocol == "sixel":
295
+ from textual_image.widget import SixelImage as ImageWidget
296
+ else:
297
+ from textual_image.widget import HalfcellImage as ImageWidget
298
+
299
+ img = ImageWidget(str(event.portrait_path), id="portrait-img")
300
+ try:
301
+ skel = self.query_one("#portrait-skeleton")
302
+ await skel.remove()
303
+ except Exception:
304
+ pass
305
+ await row.mount(img, before=0)
306
+ except (ImportError, Exception):
307
+ # textual-image not available — remove skeleton, text-only
308
+ for child in list(self.query("Horizontal")):
309
+ await child.remove()
310
+ self.update(self._header_text)
61
311
  else:
62
- self.update(f"[bold]{name}[/bold]")
312
+ # No portrait — text-only
313
+ for child in list(self.query("Horizontal")):
314
+ await child.remove()
315
+ self._current_portrait = None
316
+ self.update(self._header_text)
63
317
 
64
318
 
65
319
  class ConnectionStatus(Static):
@@ -74,58 +328,309 @@ class ConnectionStatus(Static):
74
328
  self.update(STATE_DISPLAY.get(state, "● Unknown"))
75
329
 
76
330
 
331
+ class PanelCommands(Provider):
332
+ """Command palette provider for panel switching."""
333
+
334
+ async def search(self, query: str) -> Hits:
335
+ matcher = self.matcher(query)
336
+ for panel_key, display_name in PANEL_REGISTRY:
337
+ icon = get_panel_icon(panel_key)
338
+ label = f"{icon} {display_name}" if icon else display_name
339
+ score = matcher.match(display_name)
340
+ if score > 0:
341
+ yield Hit(
342
+ score,
343
+ matcher.highlight(label),
344
+ partial(self.app.action_switch_panel, panel_key),
345
+ help=f"Switch to {display_name} panel",
346
+ )
347
+
348
+
77
349
  class BikeRackApp(App):
78
350
  """BikeRack TUI application shell."""
79
351
 
352
+ class PersonaUpdate(Message, bubble=False):
353
+ """Persona data from WS — routed through Textual message system."""
354
+
355
+ def __init__(self, data: dict[str, Any]) -> None:
356
+ super().__init__()
357
+ self.data = data
358
+
359
+ class FocusUpdate(Message, bubble=False):
360
+ """Focus change from WS — routed through Textual message system."""
361
+
362
+ def __init__(self, focus: str | None) -> None:
363
+ super().__init__()
364
+ self.focus = focus
365
+
366
+ class WsStateUpdate(Message, bubble=False):
367
+ """WS connection state change — routed through Textual message system."""
368
+
369
+ def __init__(self, state: ConnectionState) -> None:
370
+ super().__init__()
371
+ self.state = state
372
+
80
373
  TITLE = "BikeRack"
81
374
 
375
+ CSS = """
376
+ #agent-header {
377
+ height: auto;
378
+ max-height: 7;
379
+ padding: 0 1;
380
+ border-bottom: solid $accent;
381
+ }
382
+ #portrait-row {
383
+ height: 5;
384
+ width: 100%;
385
+ }
386
+ #portrait-img {
387
+ width: 10;
388
+ height: 5;
389
+ margin: 0 1 0 0;
390
+ }
391
+ #agent-text {
392
+ height: auto;
393
+ width: 1fr;
394
+ }
395
+ Tabs {
396
+ dock: top;
397
+ }
398
+ Tab.-active {
399
+ color: $text;
400
+ }
401
+ Tab {
402
+ color: $text-muted;
403
+ }
404
+ #connection-status {
405
+ height: 1;
406
+ }
407
+ ContextMeterFooter {
408
+ height: 1;
409
+ }
410
+ """
411
+
412
+ COMMANDS = App.COMMANDS | {PanelCommands}
413
+
82
414
  BINDINGS = [
83
415
  Binding("q", "quit", "Quit"),
416
+ Binding("1", "switch_panel('sprint')", "Sprint", show=False),
417
+ Binding("2", "switch_panel('git')", "Git", show=False),
418
+ Binding("3", "switch_panel('diffs')", "Diffs", show=False),
419
+ Binding("4", "switch_panel('changed')", "Changed", show=False),
420
+ Binding("5", "switch_panel('background')", "Background", show=False),
421
+ Binding("6", "switch_panel('audit-log')", "Audit Log", show=False),
422
+ Binding("7", "switch_panel('debug')", "Debug", show=False),
423
+ Binding("8", "switch_panel('progress')", "Progress", show=False),
424
+ Binding("bracketright", "next_panel", "]Next"),
425
+ Binding("bracketleft", "prev_panel", "[Prev"),
426
+ Binding("tab", "next_panel", show=False),
427
+ Binding("shift+tab", "prev_panel", show=False),
428
+ Binding("n", "next_diff_file", "Next file", show=False),
429
+ Binding("p", "prev_diff_file", "Prev file", show=False),
430
+ Binding("j", "next_epic", show=False),
431
+ Binding("k", "prev_epic", show=False),
432
+ Binding("e", "toggle_epic", show=False),
84
433
  ]
85
434
 
435
+ def _get_dom_base(self):
436
+ """Query the active screen so app.query() finds pushed screen widgets."""
437
+ return self.screen
438
+
86
439
  def __init__(self, client=None, **kwargs):
87
440
  super().__init__(**kwargs)
88
441
  self._client = client
89
- self._focused_panel: str | None = None
442
+ self._focused_panel: str = "sprint"
90
443
  self._previous_panel: str | None = None
444
+ self._programmatic_tab_count: int = 0
91
445
 
92
446
  def compose(self) -> ComposeResult:
93
447
  yield Header()
94
- yield PanelIndicator(id="panel-indicator")
448
+ yield AgentHeader(id="agent-header")
449
+ yield Tabs(*_build_panel_tabs(), id="tab-bar")
95
450
  yield ConnectionStatus(
96
451
  STATE_DISPLAY[ConnectionState.DISCONNECTED],
97
452
  id="connection-status",
98
453
  )
99
454
  with VerticalScroll(id="main-content"):
100
- yield SprintPanel(client=self._client, id="sprint-panel")
101
- yield Footer()
455
+ yield SprintPanel(client=self._client, id="panel-sprint")
456
+ yield GitPanel(client=self._client, id="panel-git")
457
+ yield DiffsPanel(client=self._client, id="panel-diffs")
458
+ yield ChangedPanel(client=self._client, id="panel-changed")
459
+ yield BackgroundPanel(client=self._client, id="panel-background")
460
+ yield AuditLogPanel(client=self._client, id="panel-audit-log")
461
+ yield DebugPanel(client=self._client, id="panel-debug")
462
+ yield ProgressPanel(client=self._client, id="panel-progress")
463
+ yield ContextMeterFooter(client=self._client)
464
+ yield BindingFooter()
102
465
 
103
466
  async def on_mount(self) -> None:
467
+ # Restore last panel or default to sprint
104
468
  result = get_last_panel()
469
+ initial = "sprint"
105
470
  if result.get("success") and result.get("last_panel"):
106
- self._focused_panel = result["last_panel"]
107
-
108
- # Set initial panel indicator
109
- self._update_panel_indicator(self._focused_panel or "sprint")
471
+ last = result["last_panel"]
472
+ if last in _PANEL_KEYS:
473
+ initial = last
474
+
475
+ self._focused_panel = initial
476
+
477
+ # Hide all panels except the active one
478
+ for panel_key in _PANEL_KEYS:
479
+ widget_id = f"panel-{panel_key}"
480
+ try:
481
+ widget = self.query_one(f"#{widget_id}")
482
+ widget.display = (panel_key == initial)
483
+ except Exception:
484
+ pass
485
+
486
+ # Set tab bar active state and focus initial panel
487
+ self._update_tab_bar(initial)
488
+ try:
489
+ initial_widget = self.query_one(f"#panel-{initial}")
490
+ initial_widget.focus()
491
+ except Exception:
492
+ pass
110
493
 
111
494
  if self._client is not None:
112
495
  self._client.on_state_change(self._on_ws_state_change)
113
496
  self._client.subscribe("focus", self._handle_focus_message)
497
+ self._client.subscribe("persona", self._handle_persona_message)
114
498
  self.run_worker(self._client.connect(), exclusive=True, name="ws-client")
115
499
 
116
- def _update_panel_indicator(self, panel_key: str) -> None:
117
- """Update the panel indicator widget with the given panel key."""
500
+ def on_navigate_to_file(self, event: NavigateToFile) -> None:
501
+ """Handle NavigateToFile switch to diffs and navigate to file."""
502
+ self.action_switch_panel("diffs")
503
+ try:
504
+ diffs = self.query_one("#panel-diffs", DiffsPanel)
505
+ diffs.navigate_to_file(event.path)
506
+ except Exception:
507
+ pass
508
+
509
+ def action_switch_panel(self, key: str) -> None:
510
+ """Switch to a panel by key."""
511
+ if key not in _PANEL_KEYS:
512
+ return
513
+ if key == self._focused_panel:
514
+ return
515
+
516
+ # Hide current panel
517
+ try:
518
+ current = self.query_one(f"#panel-{self._focused_panel}")
519
+ current.display = False
520
+ except Exception:
521
+ pass
522
+
523
+ # Show target panel and focus it
524
+ try:
525
+ target = self.query_one(f"#panel-{key}")
526
+ target.display = True
527
+ target.focus()
528
+ except Exception:
529
+ pass
530
+
531
+ self._previous_panel = self._focused_panel
532
+ self._focused_panel = key
533
+ save_last_panel(key, project_dir=None)
534
+ self._update_tab_bar(key)
535
+
536
+ def action_next_panel(self) -> None:
537
+ """Cycle to the next panel."""
118
538
  try:
119
- indicator = self.query_one("#panel-indicator", PanelIndicator)
120
- indicator.panel_key = panel_key
539
+ idx = _PANEL_KEYS.index(self._focused_panel)
540
+ except ValueError:
541
+ idx = 0
542
+ next_idx = (idx + 1) % len(_PANEL_KEYS)
543
+ self.action_switch_panel(_PANEL_KEYS[next_idx])
544
+
545
+ def action_prev_panel(self) -> None:
546
+ """Cycle to the previous panel."""
547
+ try:
548
+ idx = _PANEL_KEYS.index(self._focused_panel)
549
+ except ValueError:
550
+ idx = 0
551
+ prev_idx = (idx - 1) % len(_PANEL_KEYS)
552
+ self.action_switch_panel(_PANEL_KEYS[prev_idx])
553
+
554
+ def action_next_diff_file(self) -> None:
555
+ """Advance to next file in diffs panel."""
556
+ if self._focused_panel == "diffs":
557
+ try:
558
+ panel = self.query_one("#panel-diffs", DiffsPanel)
559
+ panel.next_file()
560
+ except Exception:
561
+ pass
562
+
563
+ def action_prev_diff_file(self) -> None:
564
+ """Go to previous file in diffs panel."""
565
+ if self._focused_panel == "diffs":
566
+ try:
567
+ panel = self.query_one("#panel-diffs", DiffsPanel)
568
+ panel.prev_file()
569
+ except Exception:
570
+ pass
571
+
572
+ def action_next_epic(self) -> None:
573
+ """Move to next epic in sprint panel."""
574
+ if self._focused_panel == "sprint":
575
+ try:
576
+ panel = self.query_one("#panel-sprint", SprintPanel)
577
+ panel.next_epic()
578
+ except Exception:
579
+ pass
580
+
581
+ def action_prev_epic(self) -> None:
582
+ """Move to previous epic in sprint panel."""
583
+ if self._focused_panel == "sprint":
584
+ try:
585
+ panel = self.query_one("#panel-sprint", SprintPanel)
586
+ panel.prev_epic()
587
+ except Exception:
588
+ pass
589
+
590
+ def action_toggle_epic(self) -> None:
591
+ """Toggle expand/collapse on selected epic in sprint panel."""
592
+ if self._focused_panel == "sprint":
593
+ try:
594
+ panel = self.query_one("#panel-sprint", SprintPanel)
595
+ panel.toggle_epic()
596
+ except Exception:
597
+ pass
598
+
599
+ def _update_tab_bar(self, panel_key: str) -> None:
600
+ """Update the tab bar widget with the given panel key.
601
+
602
+ Increments _programmatic_tab_count so the async TabActivated
603
+ handler knows to ignore the event (prevents infinite ping-pong).
604
+ """
605
+ try:
606
+ tab_bar = self.query_one("#tab-bar", Tabs)
607
+ tab_id = f"tab-{panel_key}"
608
+ if tab_bar.active != tab_id:
609
+ self._programmatic_tab_count += 1
610
+ tab_bar.active = tab_id
121
611
  except Exception:
122
612
  pass
123
613
 
614
+ def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
615
+ """Handle tab activation from the Tabs widget.
616
+
617
+ Programmatic tabs.active changes fire TabActivated asynchronously.
618
+ We use a counter to skip those and only react to genuine user clicks.
619
+ """
620
+ if self._programmatic_tab_count > 0:
621
+ self._programmatic_tab_count -= 1
622
+ return
623
+ tab_id = event.tab.id or ""
624
+ panel_key = tab_id.removeprefix("tab-")
625
+ if panel_key in _PANEL_KEYS and panel_key != self._focused_panel:
626
+ self.action_switch_panel(panel_key)
627
+
124
628
  def _handle_focus_message(self, message: dict[str, Any] | None) -> None:
125
629
  """Handle incoming focus channel messages.
126
630
 
127
631
  Expected format: {type: 'init'|'update', focus: '<panel>'|null}
128
632
  Only 'update' messages trigger panel switches (matching React hook).
633
+ Routes through Textual message system via post_message for proper repaint.
129
634
  """
130
635
  if message is None or not isinstance(message, dict):
131
636
  return
@@ -133,22 +638,47 @@ class BikeRackApp(App):
133
638
  return
134
639
  if "focus" not in message:
135
640
  return
641
+ self.post_message(self.FocusUpdate(message["focus"]))
642
+
643
+ def _handle_persona_message(self, message: dict[str, Any] | None) -> None:
644
+ """Handle incoming persona channel messages.
645
+
646
+ Routes through Textual message system via post_message for proper repaint.
647
+ """
648
+ if message is None or not isinstance(message, dict):
649
+ return
650
+ self.post_message(self.PersonaUpdate(message))
136
651
 
137
- focus = message["focus"]
138
- if focus is not None:
652
+ def _on_ws_state_change(self, state: ConnectionState) -> None:
653
+ """Handle WheelHub connection state changes.
654
+
655
+ Routes through Textual message system via post_message for proper repaint.
656
+ """
657
+ self.post_message(self.WsStateUpdate(state))
658
+
659
+ def on_bike_rack_app_persona_update(self, event: PersonaUpdate) -> None:
660
+ """Apply persona data in Textual message context."""
661
+ try:
662
+ header = self.query_one("#agent-header", AgentHeader)
663
+ header._apply_persona(event.data)
664
+ except Exception:
665
+ pass
666
+
667
+ def on_bike_rack_app_focus_update(self, event: FocusUpdate) -> None:
668
+ """Apply focus change in Textual message context."""
669
+ focus = event.focus
670
+ if focus is not None and focus in _PANEL_KEYS:
671
+ self.action_switch_panel(focus)
672
+ elif focus is not None:
139
673
  self._previous_panel = self._focused_panel
140
674
  self._focused_panel = focus
141
675
  save_last_panel(focus, project_dir=None)
142
- self._update_panel_indicator(focus)
143
- else:
144
- self._focused_panel = None
145
- self._previous_panel = None
146
676
 
147
- def _on_ws_state_change(self, state: ConnectionState) -> None:
148
- """Handle WheelHub connection state changes."""
677
+ def on_bike_rack_app_ws_state_update(self, event: WsStateUpdate) -> None:
678
+ """Apply connection state in Textual message context."""
149
679
  try:
150
680
  widget = self.query_one("#connection-status", ConnectionStatus)
151
- widget.connection_state = state
681
+ widget.connection_state = event.state
152
682
  except Exception:
153
683
  pass
154
684
 
@@ -156,16 +686,24 @@ class BikeRackApp(App):
156
686
  DEFAULT_PORT = 2898
157
687
 
158
688
 
159
- def main(port: int | None = None, project_dir: Path | None = None) -> None:
689
+ def main(
690
+ port: int | None = None,
691
+ project_dir: Path | None = None,
692
+ ) -> None:
160
693
  """Launch BikeRack TUI as a standalone application.
161
694
 
162
695
  Args:
163
- port: Explicit WheelHub port. If None, reads from .bikerack-port file.
696
+ port: Explicit WheelHub port. If None, reads from .wheelhub-port file.
164
697
  project_dir: Project directory for port file discovery. Defaults to cwd.
165
698
  """
699
+ # Detect terminal image protocol BEFORE App.run() claims the terminal
700
+ from pennyfarthing_scripts.bikerack import portrait_resolver
701
+
702
+ portrait_resolver.detect_image_protocol()
703
+
166
704
  if port is None:
167
705
  if project_dir is not None:
168
- port_file = project_dir / ".bikerack-port"
706
+ port_file = project_dir / ".wheelhub-port"
169
707
  if port_file.exists():
170
708
  try:
171
709
  port = int(port_file.read_text().strip())