@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
@@ -17,16 +17,20 @@ from typing import Any
17
17
  from textual.app import App, ComposeResult
18
18
  from textual.binding import Binding
19
19
  from textual.command import Hit, Hits, Provider
20
- from textual.containers import VerticalScroll
20
+ from textual.containers import Horizontal, VerticalScroll
21
+ from textual.message import Message
21
22
  from textual.reactive import reactive
22
- from textual.widgets import Footer, Header, Static
23
+ from textual.widgets import Footer, Header, Static, Tab, Tabs
23
24
 
24
25
  from pennyfarthing_scripts.bc.focus import get_last_panel, save_last_panel
26
+ from pennyfarthing_scripts.bikerack.audit_log_panel import AuditLogPanel
25
27
  from pennyfarthing_scripts.bikerack.background_panel import BackgroundPanel
26
28
  from pennyfarthing_scripts.bikerack.base_panel import get_panel_icon
27
29
  from pennyfarthing_scripts.bikerack.changed_panel import ChangedPanel
30
+ from pennyfarthing_scripts.bikerack.context_meter_footer import ContextMeterFooter
28
31
  from pennyfarthing_scripts.bikerack.debug_panel import DebugPanel
29
32
  from pennyfarthing_scripts.bikerack.diffs_panel import DiffsPanel
33
+ from pennyfarthing_scripts.bikerack.events import NavigateToFile
30
34
  from pennyfarthing_scripts.bikerack.git_panel import GitPanel
31
35
  from pennyfarthing_scripts.bikerack.progress_panel import ProgressPanel
32
36
  from pennyfarthing_scripts.bikerack.sprint_panel import SprintPanel
@@ -76,6 +80,7 @@ PANEL_REGISTRY: list[tuple[str, str]] = [
76
80
  ("diffs", "Diffs"),
77
81
  ("changed", "Changed"),
78
82
  ("background", "Background"),
83
+ ("audit-log", "Audit Log"),
79
84
  ("debug", "Debug"),
80
85
  ("progress", "Progress"),
81
86
  ]
@@ -101,38 +106,69 @@ PANEL_DISPLAY_NAMES: dict[str, str] = {
101
106
  _PANEL_KEYS = [key for key, _ in PANEL_REGISTRY]
102
107
 
103
108
 
104
- class PanelTabBar(Static):
105
- """Horizontal tab bar showing all available panels with active highlight."""
109
+ class BindingFooter(Footer):
110
+ """Footer subclass that exposes active binding text via render().
106
111
 
107
- active: reactive[str] = reactive("sprint")
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
+ """
108
116
 
109
- def watch_active(self, key: str) -> None:
110
- """Re-render tab bar when active panel changes."""
111
- parts: list[str] = []
112
- for panel_key, display_name in PANEL_REGISTRY:
113
- icon = get_panel_icon(panel_key)
114
- idx = _PANEL_KEYS.index(panel_key) + 1
115
- prefix = f"{idx}:"
116
- if panel_key == key:
117
- if icon:
118
- parts.append(f"[bold reverse] {prefix}{icon} {display_name} [/]")
119
- else:
120
- parts.append(f"[bold reverse] {prefix}{display_name} [/]")
121
- else:
122
- if icon:
123
- parts.append(f"[dim]{prefix}{icon} {display_name}[/]")
124
- else:
125
- parts.append(f"[dim]{prefix}{display_name}[/]")
126
- self.update(" ".join(parts))
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]"""
127
147
 
128
148
 
129
149
  class AgentHeader(Static):
130
- """Displays current agent persona from WheelHub /ws/persona channel."""
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
131
165
 
132
166
  def __init__(self, **kwargs: Any) -> None:
133
167
  super().__init__(**kwargs)
134
168
  self._is_streaming: bool = False
135
169
  self._persona_data: dict[str, Any] = {}
170
+ self._header_text: str = ""
171
+ self._current_portrait: Path | None = None
136
172
 
137
173
  def _apply_persona(self, data: dict[str, Any]) -> None:
138
174
  """Render persona data into the header."""
@@ -145,6 +181,21 @@ class AgentHeader(Static):
145
181
  self._is_streaming = bool(data.get("isStreaming", False))
146
182
  self._render_header()
147
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
+
148
199
  def _render_header(self) -> None:
149
200
  """Re-render the header from stored state."""
150
201
  data = self._persona_data
@@ -152,20 +203,20 @@ class AgentHeader(Static):
152
203
  role = data.get("role", "")
153
204
  role_desc = data.get("roleDescription", "")
154
205
  quote = data.get("quote", "")
155
- style = data.get("style", "")
156
206
  theme = data.get("theme", "")
157
207
 
158
208
  if not char:
159
209
  self.update("[dim]Waiting for agent...[/dim]")
210
+ self.post_message(self.PortraitLayoutUpdate(portrait_path=None))
160
211
  return
161
212
 
162
213
  parts: list[str] = []
163
214
 
164
- # Role badge
215
+ # Role badge — escape brackets so Rich doesn't eat them as tags
165
216
  if role:
166
217
  abbrev = AGENT_ABBREV.get(role, role.upper()[:3])
167
218
  color = AGENT_ROLE_COLORS.get(role, "bright_magenta")
168
- parts.append(f"[bold {color}][{abbrev}][/bold {color}]")
219
+ parts.append(f"[bold {color}]\\[{abbrev}][/bold {color}]")
169
220
 
170
221
  # Character name
171
222
  parts.append(f"[bold]{char}[/bold]")
@@ -173,6 +224,7 @@ class AgentHeader(Static):
173
224
  # Theme name
174
225
  if theme:
175
226
  from pennyfarthing_scripts.bikerack.base_panel import humanize_theme
227
+
176
228
  parts.append(f"[dim]{humanize_theme(theme)}[/dim]")
177
229
 
178
230
  # Streaming indicator
@@ -181,17 +233,87 @@ class AgentHeader(Static):
181
233
 
182
234
  line = " ".join(parts)
183
235
 
184
- # Role description / style subtitle
185
- if role_desc:
186
- line += f"\n[dim]{role_desc}[/dim]"
187
- elif style:
188
- line += f"\n[dim]{style}[/dim]"
189
-
190
- # Quote
236
+ # Catchphrase subtitle (quote is a random catchphrase from the theme)
191
237
  if quote:
192
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
243
+
244
+ # Check portrait and schedule layout update
245
+ portrait = self._resolve_portrait(data)
246
+ self.post_message(self.PortraitLayoutUpdate(portrait_path=portrait))
247
+
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.
193
252
 
194
- self.update(line)
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)
311
+ else:
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)
195
317
 
196
318
 
197
319
  class ConnectionStatus(Static):
@@ -227,20 +349,64 @@ class PanelCommands(Provider):
227
349
  class BikeRackApp(App):
228
350
  """BikeRack TUI application shell."""
229
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
+
230
373
  TITLE = "BikeRack"
231
374
 
232
375
  CSS = """
233
376
  #agent-header {
234
377
  height: auto;
235
- max-height: 3;
378
+ max-height: 7;
236
379
  padding: 0 1;
380
+ border-bottom: solid $accent;
237
381
  }
238
- #tab-bar {
239
- height: 1;
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;
240
403
  }
241
404
  #connection-status {
242
405
  height: 1;
243
406
  }
407
+ ContextMeterFooter {
408
+ height: 1;
409
+ }
244
410
  """
245
411
 
246
412
  COMMANDS = App.COMMANDS | {PanelCommands}
@@ -252,8 +418,9 @@ class BikeRackApp(App):
252
418
  Binding("3", "switch_panel('diffs')", "Diffs", show=False),
253
419
  Binding("4", "switch_panel('changed')", "Changed", show=False),
254
420
  Binding("5", "switch_panel('background')", "Background", show=False),
255
- Binding("6", "switch_panel('debug')", "Debug", show=False),
256
- Binding("7", "switch_panel('progress')", "Progress", 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),
257
424
  Binding("bracketright", "next_panel", "]Next"),
258
425
  Binding("bracketleft", "prev_panel", "[Prev"),
259
426
  Binding("tab", "next_panel", show=False),
@@ -265,16 +432,21 @@ class BikeRackApp(App):
265
432
  Binding("e", "toggle_epic", show=False),
266
433
  ]
267
434
 
435
+ def _get_dom_base(self):
436
+ """Query the active screen so app.query() finds pushed screen widgets."""
437
+ return self.screen
438
+
268
439
  def __init__(self, client=None, **kwargs):
269
440
  super().__init__(**kwargs)
270
441
  self._client = client
271
442
  self._focused_panel: str = "sprint"
272
443
  self._previous_panel: str | None = None
444
+ self._programmatic_tab_count: int = 0
273
445
 
274
446
  def compose(self) -> ComposeResult:
275
447
  yield Header()
276
448
  yield AgentHeader(id="agent-header")
277
- yield PanelTabBar(id="tab-bar")
449
+ yield Tabs(*_build_panel_tabs(), id="tab-bar")
278
450
  yield ConnectionStatus(
279
451
  STATE_DISPLAY[ConnectionState.DISCONNECTED],
280
452
  id="connection-status",
@@ -285,9 +457,11 @@ class BikeRackApp(App):
285
457
  yield DiffsPanel(client=self._client, id="panel-diffs")
286
458
  yield ChangedPanel(client=self._client, id="panel-changed")
287
459
  yield BackgroundPanel(client=self._client, id="panel-background")
460
+ yield AuditLogPanel(client=self._client, id="panel-audit-log")
288
461
  yield DebugPanel(client=self._client, id="panel-debug")
289
462
  yield ProgressPanel(client=self._client, id="panel-progress")
290
- yield Footer()
463
+ yield ContextMeterFooter(client=self._client)
464
+ yield BindingFooter()
291
465
 
292
466
  async def on_mount(self) -> None:
293
467
  # Restore last panel or default to sprint
@@ -309,8 +483,13 @@ class BikeRackApp(App):
309
483
  except Exception:
310
484
  pass
311
485
 
312
- # Set tab bar active state
486
+ # Set tab bar active state and focus initial panel
313
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
314
493
 
315
494
  if self._client is not None:
316
495
  self._client.on_state_change(self._on_ws_state_change)
@@ -318,6 +497,15 @@ class BikeRackApp(App):
318
497
  self._client.subscribe("persona", self._handle_persona_message)
319
498
  self.run_worker(self._client.connect(), exclusive=True, name="ws-client")
320
499
 
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
+
321
509
  def action_switch_panel(self, key: str) -> None:
322
510
  """Switch to a panel by key."""
323
511
  if key not in _PANEL_KEYS:
@@ -332,10 +520,11 @@ class BikeRackApp(App):
332
520
  except Exception:
333
521
  pass
334
522
 
335
- # Show target panel
523
+ # Show target panel and focus it
336
524
  try:
337
525
  target = self.query_one(f"#panel-{key}")
338
526
  target.display = True
527
+ target.focus()
339
528
  except Exception:
340
529
  pass
341
530
 
@@ -408,18 +597,40 @@ class BikeRackApp(App):
408
597
  pass
409
598
 
410
599
  def _update_tab_bar(self, panel_key: str) -> None:
411
- """Update the tab bar widget with the given panel key."""
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
+ """
412
605
  try:
413
- tab_bar = self.query_one("#tab-bar", PanelTabBar)
414
- tab_bar.active = panel_key
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
415
611
  except Exception:
416
612
  pass
417
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
+
418
628
  def _handle_focus_message(self, message: dict[str, Any] | None) -> None:
419
629
  """Handle incoming focus channel messages.
420
630
 
421
631
  Expected format: {type: 'init'|'update', focus: '<panel>'|null}
422
632
  Only 'update' messages trigger panel switches (matching React hook).
633
+ Routes through Textual message system via post_message for proper repaint.
423
634
  """
424
635
  if message is None or not isinstance(message, dict):
425
636
  return
@@ -427,31 +638,47 @@ class BikeRackApp(App):
427
638
  return
428
639
  if "focus" not in message:
429
640
  return
430
-
431
- focus = message["focus"]
432
- if focus is not None and focus in _PANEL_KEYS:
433
- self.action_switch_panel(focus)
434
- elif focus is not None:
435
- # Panel exists in display names but not implemented — just update state
436
- self._previous_panel = self._focused_panel
437
- self._focused_panel = focus
438
- save_last_panel(focus, project_dir=None)
641
+ self.post_message(self.FocusUpdate(message["focus"]))
439
642
 
440
643
  def _handle_persona_message(self, message: dict[str, Any] | None) -> None:
441
- """Handle incoming persona channel messages."""
644
+ """Handle incoming persona channel messages.
645
+
646
+ Routes through Textual message system via post_message for proper repaint.
647
+ """
442
648
  if message is None or not isinstance(message, dict):
443
649
  return
650
+ self.post_message(self.PersonaUpdate(message))
651
+
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."""
444
661
  try:
445
662
  header = self.query_one("#agent-header", AgentHeader)
446
- header._apply_persona(message)
663
+ header._apply_persona(event.data)
447
664
  except Exception:
448
665
  pass
449
666
 
450
- def _on_ws_state_change(self, state: ConnectionState) -> None:
451
- """Handle WheelHub connection state changes."""
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:
673
+ self._previous_panel = self._focused_panel
674
+ self._focused_panel = focus
675
+ save_last_panel(focus, project_dir=None)
676
+
677
+ def on_bike_rack_app_ws_state_update(self, event: WsStateUpdate) -> None:
678
+ """Apply connection state in Textual message context."""
452
679
  try:
453
680
  widget = self.query_one("#connection-status", ConnectionStatus)
454
- widget.connection_state = state
681
+ widget.connection_state = event.state
455
682
  except Exception:
456
683
  pass
457
684
 
@@ -469,6 +696,11 @@ def main(
469
696
  port: Explicit WheelHub port. If None, reads from .wheelhub-port file.
470
697
  project_dir: Project directory for port file discovery. Defaults to cwd.
471
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
+
472
704
  if port is None:
473
705
  if project_dir is not None:
474
706
  port_file = project_dir / ".wheelhub-port"
@@ -142,6 +142,11 @@ from pennyfarthing_scripts.consultation.cli import consultation # noqa: E402
142
142
 
143
143
  cli.add_command(consultation)
144
144
 
145
+ # Import and register hooks group
146
+ from pennyfarthing_scripts.hooks.cli import hooks # noqa: E402
147
+
148
+ cli.add_command(hooks)
149
+
145
150
 
146
151
  @cli.group()
147
152
  def agent():
@@ -0,0 +1,38 @@
1
+ """PR mode configuration reader.
2
+
3
+ Reads the pr_mode preference from .pennyfarthing/config.local.yaml.
4
+
5
+ Values: draft | ready | none (default: draft)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pennyfarthing_scripts.common.config import load_pennyfarthing_config
11
+
12
+ VALID_PR_MODES = {"draft", "ready", "none"}
13
+ DEFAULT_PR_MODE = "draft"
14
+
15
+
16
+ def get_pr_mode() -> str:
17
+ """Read pr_mode from pennyfarthing config.
18
+
19
+ Looks for workflow.pr_mode in .pennyfarthing/config.local.yaml.
20
+ Falls back to 'draft' if not set or invalid.
21
+
22
+ Returns:
23
+ One of: 'draft', 'ready', 'none'
24
+ """
25
+ config = load_pennyfarthing_config()
26
+ workflow = config.get("workflow", {})
27
+ if not isinstance(workflow, dict):
28
+ return DEFAULT_PR_MODE
29
+
30
+ mode = workflow.get("pr_mode", DEFAULT_PR_MODE)
31
+ if mode not in VALID_PR_MODES:
32
+ return DEFAULT_PR_MODE
33
+
34
+ return mode
35
+
36
+
37
+ if __name__ == "__main__":
38
+ print(get_pr_mode())