@pennyfarthing/core 11.3.8 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -1
  3. package/packages/core/dist/public/css/react.css +1 -1
  4. package/packages/core/dist/public/js/react/react.js +24 -24
  5. package/packages/core/src/public/App.tsx +356 -0
  6. package/packages/core/src/public/components/AgentLoadDialog.tsx +202 -0
  7. package/packages/core/src/public/components/AgentPopup.tsx +308 -0
  8. package/packages/core/src/public/components/ApprovalModal/ApprovalModal.css +35 -0
  9. package/packages/core/src/public/components/ApprovalModal/index.tsx +632 -0
  10. package/packages/core/src/public/components/BikeRackIndex.tsx +53 -0
  11. package/packages/core/src/public/components/BikeRackWorkspace.tsx +217 -0
  12. package/packages/core/src/public/components/CommandPalette.tsx +554 -0
  13. package/packages/core/src/public/components/ConfirmDialog.tsx +168 -0
  14. package/packages/core/src/public/components/ContextIndicator/ContextIndicator.css +85 -0
  15. package/packages/core/src/public/components/ContextIndicator/index.tsx +330 -0
  16. package/packages/core/src/public/components/ContextSparkline.tsx +56 -0
  17. package/packages/core/src/public/components/ControlBar.tsx +636 -0
  18. package/packages/core/src/public/components/DeadCodeDialog.tsx +169 -0
  19. package/packages/core/src/public/components/DiffViewer.tsx +585 -0
  20. package/packages/core/src/public/components/DockviewWorkspace.tsx +749 -0
  21. package/packages/core/src/public/components/Editor.tsx +630 -0
  22. package/packages/core/src/public/components/ErrorBoundary.tsx +67 -0
  23. package/packages/core/src/public/components/FileTree.tsx +379 -0
  24. package/packages/core/src/public/components/FontPicker/FontPicker.css +276 -0
  25. package/packages/core/src/public/components/FontPicker/index.tsx +430 -0
  26. package/packages/core/src/public/components/FullFileTree.tsx +237 -0
  27. package/packages/core/src/public/components/HealthGauge.tsx +181 -0
  28. package/packages/core/src/public/components/Message.tsx +225 -0
  29. package/packages/core/src/public/components/MessageList.tsx +98 -0
  30. package/packages/core/src/public/components/MessageView.tsx +400 -0
  31. package/packages/core/src/public/components/ModeSwitch/ModeSwitch.css +165 -0
  32. package/packages/core/src/public/components/ModeSwitch/index.tsx +372 -0
  33. package/packages/core/src/public/components/PersonaHeader.tsx +242 -0
  34. package/packages/core/src/public/components/ProjectInfoBar.tsx +45 -0
  35. package/packages/core/src/public/components/QuickActions.tsx +267 -0
  36. package/packages/core/src/public/components/SpanTimeline.tsx +352 -0
  37. package/packages/core/src/public/components/StandalonePanel.tsx +82 -0
  38. package/packages/core/src/public/components/StatsStrip.tsx +162 -0
  39. package/packages/core/src/public/components/StreamingContent.tsx +77 -0
  40. package/packages/core/src/public/components/SubagentSpan.tsx +180 -0
  41. package/packages/core/src/public/components/TandemPortrait.tsx +72 -0
  42. package/packages/core/src/public/components/ThemePalette/ThemePalette.css +179 -0
  43. package/packages/core/src/public/components/ThemePalette/index.tsx +326 -0
  44. package/packages/core/src/public/components/ToolCallBlock.tsx +252 -0
  45. package/packages/core/src/public/components/ToolStack.tsx +209 -0
  46. package/packages/core/src/public/components/ToolStatus.tsx +57 -0
  47. package/packages/core/src/public/components/dialogs/CodeMarkersDialog.tsx +169 -0
  48. package/packages/core/src/public/components/dialogs/ComplexityDialog.tsx +163 -0
  49. package/packages/core/src/public/components/dialogs/DependenciesDialog.tsx +120 -0
  50. package/packages/core/src/public/components/dialogs/HotspotsDialog.tsx +451 -0
  51. package/packages/core/src/public/components/dialogs/ToolDialog.tsx +43 -0
  52. package/packages/core/src/public/components/panel-registry.ts +13 -0
  53. package/packages/core/src/public/components/panels/ACPanel.tsx +93 -0
  54. package/packages/core/src/public/components/panels/AcceptanceCriteriaPanel.tsx +104 -0
  55. package/packages/core/src/public/components/panels/AuditLogPanel.tsx +489 -0
  56. package/packages/core/src/public/components/panels/BackgroundPanel.tsx +115 -0
  57. package/packages/core/src/public/components/panels/BikeLanePanel.tsx +214 -0
  58. package/packages/core/src/public/components/panels/DebugPanel.tsx +344 -0
  59. package/packages/core/src/public/components/panels/DiffView.tsx +109 -0
  60. package/packages/core/src/public/components/panels/DiffsPanel.tsx +56 -0
  61. package/packages/core/src/public/components/panels/GitPanel.tsx +260 -0
  62. package/packages/core/src/public/components/panels/HotspotsPanel.tsx +365 -0
  63. package/packages/core/src/public/components/panels/MessageFeed.tsx +39 -0
  64. package/packages/core/src/public/components/panels/MessagePanel.tsx +497 -0
  65. package/packages/core/src/public/components/panels/ProgressPanel.tsx +189 -0
  66. package/packages/core/src/public/components/panels/SettingsPanel.tsx +361 -0
  67. package/packages/core/src/public/components/panels/SprintPanel.tsx +723 -0
  68. package/packages/core/src/public/components/panels/TandemPanel.tsx +104 -0
  69. package/packages/core/src/public/components/panels/TaskTracker.tsx +48 -0
  70. package/packages/core/src/public/components/panels/TeamPanel.tsx +64 -0
  71. package/packages/core/src/public/components/panels/TeamRoster.tsx +67 -0
  72. package/packages/core/src/public/components/panels/TodoPanel.tsx +142 -0
  73. package/packages/core/src/public/components/panels/WorkflowPanel.tsx +224 -0
  74. package/packages/core/src/public/components/panels/index.ts +24 -0
  75. package/packages/core/src/public/components/ui/alert-dialog.tsx +139 -0
  76. package/packages/core/src/public/components/ui/badge.tsx +36 -0
  77. package/packages/core/src/public/components/ui/button.tsx +57 -0
  78. package/packages/core/src/public/components/ui/checkbox.tsx +28 -0
  79. package/packages/core/src/public/components/ui/collapsible.tsx +9 -0
  80. package/packages/core/src/public/components/ui/command.tsx +151 -0
  81. package/packages/core/src/public/components/ui/dialog.tsx +120 -0
  82. package/packages/core/src/public/components/ui/popover.tsx +31 -0
  83. package/packages/core/src/public/components/ui/progress.tsx +28 -0
  84. package/packages/core/src/public/components/ui/scroll-area.tsx +46 -0
  85. package/packages/core/src/public/components/ui/select.tsx +157 -0
  86. package/packages/core/src/public/components/ui/separator.tsx +29 -0
  87. package/packages/core/src/public/components/ui/skeleton.tsx +15 -0
  88. package/packages/core/src/public/components/ui/switch.tsx +27 -0
  89. package/packages/core/src/public/components/ui/toggle-group.tsx +59 -0
  90. package/packages/core/src/public/components/ui/toggle.tsx +43 -0
  91. package/packages/core/src/public/components/ui/tooltip.tsx +30 -0
  92. package/packages/core/src/public/contexts/ClaudeContext.tsx +311 -0
  93. package/packages/core/src/public/contexts/MessageQueueContext.tsx +143 -0
  94. package/packages/core/src/public/css/theme-browser.css +550 -0
  95. package/packages/core/src/public/css/theme-system.css +630 -0
  96. package/packages/core/src/public/hooks/index.ts +49 -0
  97. package/packages/core/src/public/hooks/useAgentLoad.ts +105 -0
  98. package/packages/core/src/public/hooks/useBackgroundTasks.ts +131 -0
  99. package/packages/core/src/public/hooks/useClaude.ts +234 -0
  100. package/packages/core/src/public/hooks/useCodeMarkers.ts +101 -0
  101. package/packages/core/src/public/hooks/useColorScheme.ts +42 -0
  102. package/packages/core/src/public/hooks/useCommandHistory.ts +99 -0
  103. package/packages/core/src/public/hooks/useComplexity.ts +80 -0
  104. package/packages/core/src/public/hooks/useDeadCode.ts +99 -0
  105. package/packages/core/src/public/hooks/useDependencies.ts +82 -0
  106. package/packages/core/src/public/hooks/useDiffs.ts +143 -0
  107. package/packages/core/src/public/hooks/useFileBrowser.ts +73 -0
  108. package/packages/core/src/public/hooks/useFocusPanel.ts +137 -0
  109. package/packages/core/src/public/hooks/useGitStatus.ts +233 -0
  110. package/packages/core/src/public/hooks/useHealthScore.ts +71 -0
  111. package/packages/core/src/public/hooks/useHotspots.ts +123 -0
  112. package/packages/core/src/public/hooks/useLayoutPersistence.ts +141 -0
  113. package/packages/core/src/public/hooks/useMarkdownParser.ts +36 -0
  114. package/packages/core/src/public/hooks/useMarkerActions.ts +234 -0
  115. package/packages/core/src/public/hooks/useMessageQueue.ts +380 -0
  116. package/packages/core/src/public/hooks/useMessageStream.ts +131 -0
  117. package/packages/core/src/public/hooks/usePersona.ts +112 -0
  118. package/packages/core/src/public/hooks/usePlanModeExit.ts +105 -0
  119. package/packages/core/src/public/hooks/useResponsiveLayout.ts +173 -0
  120. package/packages/core/src/public/hooks/useSprint.ts +157 -0
  121. package/packages/core/src/public/hooks/useStatsStrip.ts +204 -0
  122. package/packages/core/src/public/hooks/useStory.ts +135 -0
  123. package/packages/core/src/public/hooks/useSubagentHelper.ts +64 -0
  124. package/packages/core/src/public/hooks/useSyntaxHighlighter.ts +52 -0
  125. package/packages/core/src/public/hooks/useTabCompletion.ts +124 -0
  126. package/packages/core/src/public/hooks/useTandemObservations.ts +165 -0
  127. package/packages/core/src/public/hooks/useTeamMembers.ts +273 -0
  128. package/packages/core/src/public/hooks/useTodos.ts +93 -0
  129. package/packages/core/src/public/hooks/useUserAvatar.ts +54 -0
  130. package/packages/core/src/public/images/cyclist-dark.png +0 -0
  131. package/packages/core/src/public/images/cyclist-light.png +0 -0
  132. package/packages/core/src/public/index.html +14 -0
  133. package/packages/core/src/public/index.tsx +10 -0
  134. package/packages/core/src/public/lib/utils.ts +6 -0
  135. package/packages/core/src/public/styles/dockview-theme.css +376 -0
  136. package/packages/core/src/public/styles/tailwind.css +4454 -0
  137. package/packages/core/src/public/types/message.ts +51 -0
  138. package/packages/core/src/public/utils/avatar-service.ts +73 -0
  139. package/packages/core/src/public/utils/color-presets.ts +940 -0
  140. package/packages/core/src/public/utils/font-presets.ts +362 -0
  141. package/packages/core/src/public/utils/formatDuration.ts +14 -0
  142. package/packages/core/src/public/utils/markdown.ts +249 -0
  143. package/packages/core/src/public/utils/messageFilters.ts +128 -0
  144. package/packages/core/src/public/utils/slash-commands.ts +341 -0
  145. package/packages/core/src/public/utils/subagent-display.ts +146 -0
  146. package/packages/core/src/public/utils/syntax.ts +219 -0
  147. package/packages/core/src/public/utils/toolIntentSummarizer.ts +199 -0
  148. package/packages/core/src/public/utils/toolStackGrouper.ts +106 -0
  149. package/packages/core/src/public/utils/toolTypeColors.ts +45 -0
  150. package/pennyfarthing-dist/pf/__pycache__/__init__.cpython-314.pyc +0 -0
  151. package/pennyfarthing-dist/pf/__pycache__/cli.cpython-314.pyc +0 -0
  152. package/pennyfarthing-dist/pf/__pycache__/context.cpython-314.pyc +0 -0
  153. package/pennyfarthing-dist/pf/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/pennyfarthing-dist/pf/bc/__pycache__/cli.cpython-314.pyc +0 -0
  155. package/pennyfarthing-dist/pf/bc/__pycache__/focus.cpython-314.pyc +0 -0
  156. package/pennyfarthing-dist/pf/bc/__pycache__/split.cpython-314.pyc +0 -0
  157. package/pennyfarthing-dist/pf/bc/cli.py +0 -1
  158. package/pennyfarthing-dist/pf/bc/focus.py +0 -1
  159. package/pennyfarthing-dist/pf/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  160. package/pennyfarthing-dist/pf/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  161. package/pennyfarthing-dist/pf/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  162. package/pennyfarthing-dist/pf/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  163. package/pennyfarthing-dist/pf/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  164. package/pennyfarthing-dist/pf/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  165. package/pennyfarthing-dist/pf/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  166. package/pennyfarthing-dist/pf/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
  167. package/pennyfarthing-dist/pf/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
  168. package/pennyfarthing-dist/pf/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  169. package/pennyfarthing-dist/pf/bikerack/base_panel.py +0 -1
  170. package/pennyfarthing-dist/pf/bikerack/events.py +1 -7
  171. package/pennyfarthing-dist/pf/bikerack/git_panel.py +273 -10
  172. package/pennyfarthing-dist/pf/bikerack/portrait_resolver.py +21 -0
  173. package/pennyfarthing-dist/pf/bikerack/sprint_panel.py +58 -1
  174. package/pennyfarthing-dist/pf/bikerack/tui.py +5 -20
  175. package/pennyfarthing-dist/pf/bmad/__pycache__/__init__.cpython-314.pyc +0 -0
  176. package/pennyfarthing-dist/pf/bmad/__pycache__/cli.cpython-314.pyc +0 -0
  177. package/pennyfarthing-dist/pf/bmad/__pycache__/parser.cpython-314.pyc +0 -0
  178. package/pennyfarthing-dist/pf/bmad/parser.py +15 -9
  179. package/pennyfarthing-dist/pf/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  180. package/pennyfarthing-dist/pf/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  181. package/pennyfarthing-dist/pf/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  182. package/pennyfarthing-dist/pf/common/__pycache__/__init__.cpython-314.pyc +0 -0
  183. package/pennyfarthing-dist/pf/common/__pycache__/config.cpython-314.pyc +0 -0
  184. package/pennyfarthing-dist/pf/common/__pycache__/output.cpython-314.pyc +0 -0
  185. package/pennyfarthing-dist/pf/common/__pycache__/pr_config.cpython-314.pyc +0 -0
  186. package/pennyfarthing-dist/pf/common/__pycache__/themes.cpython-314.pyc +0 -0
  187. package/pennyfarthing-dist/pf/common/pr_config.py +27 -2
  188. package/pennyfarthing-dist/pf/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  189. package/pennyfarthing-dist/pf/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  190. package/pennyfarthing-dist/pf/complexity/__pycache__/models.cpython-314.pyc +0 -0
  191. package/pennyfarthing-dist/pf/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/pennyfarthing-dist/pf/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  193. package/pennyfarthing-dist/pf/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  194. package/pennyfarthing-dist/pf/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  195. package/pennyfarthing-dist/pf/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  196. package/pennyfarthing-dist/pf/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  197. package/pennyfarthing-dist/pf/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/pennyfarthing-dist/pf/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  199. package/pennyfarthing-dist/pf/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  200. package/pennyfarthing-dist/pf/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  201. package/pennyfarthing-dist/pf/epic/__pycache__/cli.cpython-314.pyc +0 -0
  202. package/pennyfarthing-dist/pf/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing-dist/pf/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  204. package/pennyfarthing-dist/pf/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  205. package/pennyfarthing-dist/pf/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  206. package/pennyfarthing-dist/pf/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  207. package/pennyfarthing-dist/pf/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  208. package/pennyfarthing-dist/pf/handoff/__pycache__/phase_check.cpython-314.pyc +0 -0
  209. package/pennyfarthing-dist/pf/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  210. package/pennyfarthing-dist/pf/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  211. package/pennyfarthing-dist/pf/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  212. package/pennyfarthing-dist/pf/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  213. package/pennyfarthing-dist/pf/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  214. package/pennyfarthing-dist/pf/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  215. package/pennyfarthing-dist/pf/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  216. package/pennyfarthing-dist/pf/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  217. package/pennyfarthing-dist/pf/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  218. package/pennyfarthing-dist/pf/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  219. package/pennyfarthing-dist/pf/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  220. package/pennyfarthing-dist/pf/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  221. package/pennyfarthing-dist/pf/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  222. package/pennyfarthing-dist/pf/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  223. package/pennyfarthing-dist/pf/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  224. package/pennyfarthing-dist/pf/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  225. package/pennyfarthing-dist/pf/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  226. package/pennyfarthing-dist/pf/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  227. package/pennyfarthing-dist/pf/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  228. package/pennyfarthing-dist/pf/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  229. package/pennyfarthing-dist/pf/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  230. package/pennyfarthing-dist/pf/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  231. package/pennyfarthing-dist/pf/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  232. package/pennyfarthing-dist/pf/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  233. package/pennyfarthing-dist/pf/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  234. package/pennyfarthing-dist/pf/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  235. package/pennyfarthing-dist/pf/jira/__pycache__/claim.cpython-314.pyc +0 -0
  236. package/pennyfarthing-dist/pf/jira/__pycache__/cli.cpython-314.pyc +0 -0
  237. package/pennyfarthing-dist/pf/jira/__pycache__/client.cpython-314.pyc +0 -0
  238. package/pennyfarthing-dist/pf/jira/__pycache__/create.cpython-314.pyc +0 -0
  239. package/pennyfarthing-dist/pf/jira/__pycache__/epic.cpython-314.pyc +0 -0
  240. package/pennyfarthing-dist/pf/jira/__pycache__/operations.cpython-314.pyc +0 -0
  241. package/pennyfarthing-dist/pf/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  242. package/pennyfarthing-dist/pf/jira/__pycache__/story.cpython-314.pyc +0 -0
  243. package/pennyfarthing-dist/pf/jira/__pycache__/sync.cpython-314.pyc +0 -0
  244. package/pennyfarthing-dist/pf/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  245. package/pennyfarthing-dist/pf/launch/__pycache__/cli.cpython-314.pyc +0 -0
  246. package/pennyfarthing-dist/pf/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  247. package/pennyfarthing-dist/pf/prime/__pycache__/cli.cpython-314.pyc +0 -0
  248. package/pennyfarthing-dist/pf/prime/__pycache__/loader.cpython-314.pyc +0 -0
  249. package/pennyfarthing-dist/pf/prime/__pycache__/models.cpython-314.pyc +0 -0
  250. package/pennyfarthing-dist/pf/prime/__pycache__/persona.cpython-314.pyc +0 -0
  251. package/pennyfarthing-dist/pf/prime/__pycache__/session.cpython-314.pyc +0 -0
  252. package/pennyfarthing-dist/pf/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  253. package/pennyfarthing-dist/pf/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  254. package/pennyfarthing-dist/pf/session/__pycache__/__init__.cpython-314.pyc +0 -0
  255. package/pennyfarthing-dist/pf/session/__pycache__/cli.cpython-314.pyc +0 -0
  256. package/pennyfarthing-dist/pf/settings/__pycache__/__init__.cpython-314.pyc +0 -0
  257. package/pennyfarthing-dist/pf/settings/__pycache__/cli.cpython-314.pyc +0 -0
  258. package/pennyfarthing-dist/pf/settings/__pycache__/settings.cpython-314.pyc +0 -0
  259. package/pennyfarthing-dist/pf/settings/settings.py +44 -8
  260. package/pennyfarthing-dist/pf/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  261. package/pennyfarthing-dist/pf/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  262. package/pennyfarthing-dist/pf/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  263. package/pennyfarthing-dist/pf/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  264. package/pennyfarthing-dist/pf/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  265. package/pennyfarthing-dist/pf/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  266. package/pennyfarthing-dist/pf/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  267. package/pennyfarthing-dist/pf/sprint/__pycache__/status.cpython-314.pyc +0 -0
  268. package/pennyfarthing-dist/pf/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  269. package/pennyfarthing-dist/pf/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  270. package/pennyfarthing-dist/pf/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  271. package/pennyfarthing-dist/pf/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  272. package/pennyfarthing-dist/pf/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  273. package/pennyfarthing-dist/pf/sprint/__pycache__/work.cpython-314.pyc +0 -0
  274. package/pennyfarthing-dist/pf/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  275. package/pennyfarthing-dist/pf/sprint/story_finish.py +14 -2
  276. package/pennyfarthing-dist/pf/sprint/validator.py +7 -7
  277. package/pennyfarthing-dist/pf/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  278. package/pennyfarthing-dist/pf/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  279. package/pennyfarthing-dist/pf/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  280. package/pennyfarthing-dist/pf/tests/test_sprint_validator.py +44 -0
  281. package/pennyfarthing-dist/pf/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  282. package/pennyfarthing-dist/pf/theme/__pycache__/cli.cpython-314.pyc +0 -0
  283. package/pennyfarthing-dist/pf/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing-dist/pf/validate/__pycache__/cli.cpython-314.pyc +0 -0
  285. package/pennyfarthing-dist/pf/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  286. package/pennyfarthing-dist/pf/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  287. package/pennyfarthing-dist/pf/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  288. package/pennyfarthing-dist/pf/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  289. package/pennyfarthing-dist/pf/workflow/__pycache__/state.cpython-314.pyc +0 -0
  290. package/pennyfarthing-dist/scripts/lib/find-root.sh +1 -1
  291. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -1
  292. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +13 -13
  293. package/pennyfarthing-dist/scripts/workflow/check.py +4 -6
  294. package/pennyfarthing-dist/scripts/workflow/complete-step.py +2 -2
  295. package/pennyfarthing-dist/skills/skill-registry.yaml +19 -0
  296. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +15 -2
  297. package/packages/core/dist/workflow/__test_context_watch__/.session/.tandem-turn-counter +0 -1
  298. package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-session.md +0 -3
  299. package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-tandem-architect.md +0 -6
  300. package/packages/core/dist/workflow/__test_file_watch__/.session/95-4-tandem-architect.md +0 -6
  301. package/packages/core/dist/workflow/__test_file_watch__/workdir/trigger.ts +0 -1
  302. package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-architect.md +0 -6
  303. package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-toolcalls.jsonl +0 -1
  304. package/pennyfarthing-dist/pf/bikerack/changed_panel.py +0 -201
@@ -1,14 +1,22 @@
1
- """GitPanel — Multi-repo git status panel for BikeRack TUI.
1
+ """GitPanel — Multi-repo git status panel with diff drill-through for BikeRack TUI.
2
2
 
3
3
  Story 103-10: Subscribes to /ws/git, renders multi-repo git status
4
4
  as Rich table with Nerd Font glyphs for branch and status indicators.
5
+
6
+ Merge: Changed panel consolidated into Git panel — clickable file list
7
+ with Enter to view diff inline, Escape to return to repo overview.
5
8
  """
6
9
 
7
10
  from __future__ import annotations
8
11
 
12
+ import os
13
+ import re
9
14
  from typing import Any
10
15
 
16
+ from rich.console import Group as RichGroup
17
+ from rich.syntax import Syntax
11
18
  from rich.text import Text
19
+ from textual.binding import Binding
12
20
 
13
21
  from pf.bikerack.base_panel import PANEL_ICONS, BasePanel
14
22
 
@@ -63,27 +71,265 @@ def _parse_file_status(status: str) -> tuple[str, str, str]:
63
71
  return ("·", "Changed", "yellow")
64
72
 
65
73
 
74
+ # ---- Diff rendering helpers (from diffs_panel.py patterns) ----
75
+
76
+ _LANG_MAP: dict[str, str] = {
77
+ ".py": "python", ".ts": "typescript", ".tsx": "tsx",
78
+ ".js": "javascript", ".jsx": "jsx", ".go": "go",
79
+ ".rs": "rust", ".rb": "ruby", ".java": "java",
80
+ ".css": "css", ".html": "html", ".json": "json",
81
+ ".yaml": "yaml", ".yml": "yaml", ".md": "markdown",
82
+ ".sh": "bash", ".zsh": "bash", ".toml": "toml",
83
+ ".xml": "xml", ".sql": "sql", ".c": "c", ".cpp": "cpp",
84
+ ".h": "c", ".hpp": "cpp",
85
+ }
86
+
87
+
88
+ def _detect_language(path: str) -> str:
89
+ """Detect programming language from file path for syntax highlighting."""
90
+ ext = os.path.splitext(path)[1].lower()
91
+ return _LANG_MAP.get(ext, "text")
92
+
93
+
94
+ def _highlight_code(code: str, language: str) -> Text:
95
+ """Apply syntax highlighting to a single line."""
96
+ if not code or not code.strip():
97
+ return Text(code)
98
+ try:
99
+ syntax = Syntax(code, language, theme="monokai", background_color="default")
100
+ highlighted = syntax.highlight(code)
101
+ highlighted.rstrip()
102
+ return highlighted
103
+ except Exception:
104
+ return Text(code)
105
+
106
+
107
+ def _render_inline_diff(diff_entry: dict[str, Any]) -> list[Any]:
108
+ """Render a single file diff inline for drill-through view."""
109
+ path = diff_entry.get("path", "unknown")
110
+ status = diff_entry.get("status", "")
111
+ additions = diff_entry.get("additions")
112
+ deletions = diff_entry.get("deletions")
113
+ raw_diff = diff_entry.get("diff", "")
114
+
115
+ header = Text()
116
+ header.append(path, style="bold cyan")
117
+ if status:
118
+ header.append(f" {status}", style="dim")
119
+ if additions is not None and deletions is not None:
120
+ header.append(f" +{additions} -{deletions}", style="dim")
121
+
122
+ language = _detect_language(path)
123
+ parts: list[Any] = [header]
124
+
125
+ if not raw_diff or not raw_diff.strip():
126
+ parts.append(Text("(no diff content)", style="dim italic"))
127
+ return parts
128
+
129
+ line_num = 0
130
+ for line in raw_diff.split("\n"):
131
+ if line.startswith("diff --git") or line.startswith("index "):
132
+ continue
133
+ if line.startswith("---") or line.startswith("+++"):
134
+ continue
135
+ if line.startswith("new file") or line.startswith("deleted file"):
136
+ continue
137
+ if line.startswith("rename "):
138
+ continue
139
+ if line == "":
140
+ continue
141
+
142
+ if line.startswith("Binary files"):
143
+ parts.append(Text(line, style="dim italic"))
144
+ elif line.startswith("@@"):
145
+ match = re.match(r"@@ -\d+(?:,\d+)? \+(\d+)", line)
146
+ if match:
147
+ line_num = int(match.group(1)) - 1
148
+ parts.append(Text(line, style="cyan"))
149
+ elif line.startswith("+"):
150
+ line_num += 1
151
+ t = Text()
152
+ t.append(f"{line_num:4d} ", style="dim green")
153
+ t.append_text(_highlight_code(line[1:], language))
154
+ parts.append(t)
155
+ elif line.startswith("-"):
156
+ t = Text()
157
+ t.append(" - ", style="red")
158
+ t.append_text(_highlight_code(line[1:], language))
159
+ parts.append(t)
160
+ elif line.startswith(" "):
161
+ line_num += 1
162
+ t = Text()
163
+ t.append(f"{line_num:4d} ", style="dim")
164
+ t.append_text(_highlight_code(line[1:], language))
165
+ parts.append(t)
166
+ else:
167
+ parts.append(Text(line, style="dim"))
168
+
169
+ return parts
170
+
171
+
66
172
  class GitPanel(BasePanel):
67
- """Multi-repo git status panel.
173
+ """Multi-repo git status panel with diff drill-through.
68
174
 
69
- Subscribes to the ``git`` WebSocket channel and renders
70
- git status for all configured repos as a Rich table with
71
- columns: Repository, Branch, Commits, Changes, Status.
175
+ Subscribes to the ``git`` and ``diffs`` WebSocket channels.
176
+ Renders git status for all configured repos. Clicking a file
177
+ (Enter) shows its diff inline; Escape returns to the overview.
72
178
  """
73
179
 
74
180
  channel: str = "git"
75
181
  panel_name: str = "Git"
76
182
  icon: str = PANEL_ICONS["git"][0]
183
+ can_focus = True
184
+
185
+ BINDINGS = [
186
+ Binding("up", "select_prev_key", "Up"),
187
+ Binding("down", "select_next_key", "Down"),
188
+ Binding("enter", "drill_into_file", "View diff"),
189
+ Binding("escape", "back_to_overview", "Back"),
190
+ ]
191
+
192
+ def __init__(self, client=None, **kwargs):
193
+ super().__init__(client=client, **kwargs)
194
+ self._selected_index: int = 0
195
+ self._file_paths: list[str] = []
196
+ self._viewing_diff: bool = False
197
+ self._diff_file_path: str | None = None
198
+ self._diffs_payload: dict[str, Any] | None = None
199
+
200
+ # Subscribe to diffs channel for diff data
201
+ if client is not None:
202
+ client.subscribe("diffs", self._handle_diffs_message)
203
+
204
+ def _handle_diffs_message(self, message: dict[str, Any] | None) -> None:
205
+ """Store diffs payload for drill-through rendering."""
206
+ if message is not None:
207
+ self._diffs_payload = message
208
+ # If viewing a diff, re-render with updated diff data
209
+ if self._viewing_diff:
210
+ self._rerender()
211
+
212
+ def handle_message(self, message: dict[str, Any] | None) -> None:
213
+ """Handle incoming git message — build file path index then render."""
214
+ if message is not None:
215
+ self._build_file_paths(message)
216
+ super().handle_message(message)
217
+
218
+ def _build_file_paths(self, payload: dict[str, Any]) -> None:
219
+ """Extract flat list of file paths from repos payload."""
220
+ paths: list[str] = []
221
+ repos = payload.get("repos", [])
222
+ if isinstance(repos, list):
223
+ for repo in repos:
224
+ if not isinstance(repo, dict):
225
+ continue
226
+ dirty_files = repo.get("dirtyFiles", [])
227
+ if not isinstance(dirty_files, list):
228
+ continue
229
+ for f in dirty_files:
230
+ if isinstance(f, dict):
231
+ path = f.get("path", "")
232
+ if path:
233
+ paths.append(path)
234
+ self._file_paths = paths
235
+ if self._selected_index >= len(paths):
236
+ self._selected_index = max(0, len(paths) - 1)
237
+
238
+ def select_next(self) -> None:
239
+ """Move selection to the next file."""
240
+ if self._file_paths and self._selected_index < len(self._file_paths) - 1:
241
+ self._selected_index += 1
242
+ self._rerender()
243
+
244
+ def select_prev(self) -> None:
245
+ """Move selection to the previous file."""
246
+ if self._selected_index > 0:
247
+ self._selected_index -= 1
248
+ self._rerender()
249
+
250
+ def action_select_next_key(self) -> None:
251
+ """Binding action: move selection down."""
252
+ if not self._viewing_diff:
253
+ self.select_next()
254
+
255
+ def action_select_prev_key(self) -> None:
256
+ """Binding action: move selection up."""
257
+ if not self._viewing_diff:
258
+ self.select_prev()
259
+
260
+ def action_drill_into_file(self) -> None:
261
+ """Enter diff drill-through for the selected file."""
262
+ if self._viewing_diff:
263
+ return
264
+ if not self._file_paths:
265
+ return
266
+ if self._selected_index >= len(self._file_paths):
267
+ return
268
+ self._diff_file_path = self._file_paths[self._selected_index]
269
+ self._viewing_diff = True
270
+ self._rerender()
271
+
272
+ def action_back_to_overview(self) -> None:
273
+ """Return to the repo overview from diff view."""
274
+ if self._viewing_diff:
275
+ self._viewing_diff = False
276
+ self._diff_file_path = None
277
+ self._rerender()
278
+
279
+ def _rerender(self) -> None:
280
+ """Re-render panel with current payload."""
281
+ if self._last_payload:
282
+ try:
283
+ self.update(self.render_panel(self._last_payload))
284
+ except Exception:
285
+ pass
286
+
287
+ def _find_diff_for_path(self, path: str) -> dict[str, Any] | None:
288
+ """Find a diff entry matching the given file path."""
289
+ if self._diffs_payload is None:
290
+ return None
291
+ diffs = self._diffs_payload.get("diffs", [])
292
+ for d in diffs:
293
+ d_path = d.get("path", "")
294
+ if d_path == path or d_path.endswith(path) or path.endswith(d_path):
295
+ return d
296
+ return None
77
297
 
78
298
  def render_panel(self, payload: dict[str, Any]) -> Any:
79
- """Render git status as Rich Tree with expandable file lists."""
80
- from rich.console import Group as RichGroup
299
+ """Render git status or diff drill-through view."""
300
+ if self._viewing_diff and self._diff_file_path:
301
+ return self._render_diff_view()
302
+ return self._render_repo_overview(payload)
303
+
304
+ def _render_diff_view(self) -> Any:
305
+ """Render inline diff for the selected file."""
306
+ parts: list[Any] = []
81
307
 
308
+ # Back bar
309
+ back = Text()
310
+ back.append("← ", style="bold")
311
+ back.append("Esc", style="bold yellow")
312
+ back.append(" back ", style="dim")
313
+ back.append(self._diff_file_path or "", style="bold cyan")
314
+ parts.append(back)
315
+ parts.append(Text(""))
316
+
317
+ diff_entry = self._find_diff_for_path(self._diff_file_path or "")
318
+ if diff_entry:
319
+ parts.extend(_render_inline_diff(diff_entry))
320
+ else:
321
+ parts.append(Text(f"No diff available for {self._diff_file_path}", style="dim italic"))
322
+
323
+ return RichGroup(*parts)
324
+
325
+ def _render_repo_overview(self, payload: dict[str, Any]) -> Any:
326
+ """Render git status as repo list with selectable file lists."""
82
327
  repos = payload.get("repos", [])
83
328
  if not repos:
84
329
  return Text("No repository data", style="dim italic")
85
330
 
86
331
  parts: list[Any] = []
332
+ flat_idx = 0
87
333
  for repo in repos:
88
334
  branch = repo.get("branch", "")
89
335
  ahead = repo.get("ahead", 0)
@@ -120,7 +366,7 @@ class GitPanel(BasePanel):
120
366
 
121
367
  parts.append(header)
122
368
 
123
- # Expanded file list for dirty repos
369
+ # Expanded file list for dirty repos — with selection highlight
124
370
  if not clean and dirty_files:
125
371
  for f in dirty_files:
126
372
  if not isinstance(f, dict):
@@ -128,12 +374,29 @@ class GitPanel(BasePanel):
128
374
  status_code = f.get("status", " ")
129
375
  path = f.get("path", "")
130
376
  icon, label, style = _parse_file_status(status_code)
377
+ is_selected = flat_idx == self._selected_index
131
378
  file_line = Text()
132
- file_line.append(" ")
379
+ if is_selected:
380
+ file_line.append(" › ", style="bold reverse")
381
+ else:
382
+ file_line.append(" ")
133
383
  file_line.append(icon, style=f"bold {style}")
134
- file_line.append(f" {path}", style=style)
384
+ file_line.append(
385
+ f" {path}",
386
+ style="bold cyan reverse" if is_selected else style,
387
+ )
135
388
  parts.append(file_line)
389
+ flat_idx += 1
136
390
 
137
391
  parts.append(Text("")) # spacer
138
392
 
393
+ # Hint line
394
+ if self._file_paths:
395
+ hint = Text()
396
+ hint.append("↑↓", style="bold yellow")
397
+ hint.append(" select ", style="dim")
398
+ hint.append("Enter", style="bold yellow")
399
+ hint.append(" view diff", style="dim")
400
+ parts.append(hint)
401
+
139
402
  return RichGroup(*parts)
@@ -101,6 +101,27 @@ def resolve_portrait_path(
101
101
  if result:
102
102
  return result
103
103
 
104
+ # Fallback: search Cyclist package portrait directories
105
+ # Portraits are bundled in @pennyfarthing/cyclist, not alongside theme YAMLs
106
+ root = project_root or Path.cwd()
107
+ cyclist_portrait_dirs = [
108
+ root / "packages" / "cyclist" / "portraits" / theme, # monorepo dev
109
+ root / "node_modules" / "@pennyfarthing" / "cyclist" / "portraits" / theme, # npm
110
+ ]
111
+ # pnpm: resolve through .pennyfarthing symlink chain
112
+ pnpm_cyclist = root / "node_modules" / ".pnpm"
113
+ if pnpm_cyclist.is_dir():
114
+ for entry in pnpm_cyclist.iterdir():
115
+ if entry.name.startswith("@pennyfarthing+cyclist@"):
116
+ candidate = entry / "node_modules" / "@pennyfarthing" / "cyclist" / "portraits" / theme
117
+ cyclist_portrait_dirs.append(candidate)
118
+ break
119
+
120
+ for portraits_dir in cyclist_portrait_dirs:
121
+ result = _find_portrait(portraits_dir, slug)
122
+ if result:
123
+ return result
124
+
104
125
  return None
105
126
 
106
127
 
@@ -289,7 +289,7 @@ class SprintPanel(Widget):
289
289
  saved_expanded: dict[str, bool] = {}
290
290
  for node in tree.root.children:
291
291
  data = node.data
292
- if data and data.get("type") == "epic":
292
+ if data and data.get("type") in ("epic", "future_initiative"):
293
293
  saved_expanded[data["id"]] = node.is_expanded
294
294
 
295
295
  # Save cursor position by node identity
@@ -356,6 +356,63 @@ class SprintPanel(Widget):
356
356
  if cursor_node_key == f"epic:{epic_id}":
357
357
  cursor_target = epic_node
358
358
 
359
+ # Future Initiatives section (Story 118-1)
360
+ future_epics = payload.get("futureEpics", [])
361
+ if future_epics:
362
+ # Section separator
363
+ separator_label = Text("── Future Initiatives ──", style="bold dim")
364
+ tree.root.add_leaf(separator_label, data={"type": "separator"})
365
+
366
+ for initiative in future_epics:
367
+ init_id = initiative.get("id", "")
368
+ init_title = initiative.get("title", "")
369
+ init_pts = initiative.get("estimatedPoints", 0)
370
+ init_status = initiative.get("status", "planning")
371
+ children = initiative.get("children", [])
372
+
373
+ # Build initiative label
374
+ init_label = Text(no_wrap=True, overflow="ellipsis")
375
+ status_style = "green" if init_status == "ready" else "dim yellow"
376
+ init_label.append(f"[{init_status}]", style=status_style)
377
+ init_label.append(f" {init_title}", style="bold")
378
+ init_label.append(f" {init_pts} pts", style="dim")
379
+ if children:
380
+ init_label.append(f" ({len(children)} epics)", style="dim")
381
+
382
+ init_data: dict[str, Any] = {
383
+ "type": "future_initiative",
384
+ "id": init_id,
385
+ "title": init_title,
386
+ }
387
+ init_node = tree.root.add(init_label, data=init_data)
388
+
389
+ # Add child epics as leaves
390
+ for child in children:
391
+ child_label = Text(no_wrap=True, overflow="ellipsis")
392
+ child_status = child.get("status", "planning")
393
+ child_style = "green" if child_status == "ready" else "dim yellow"
394
+ child_label.append(f" [{child_status}]", style=child_style)
395
+ child_label.append(f" {child.get('title', '')}")
396
+ child_label.append(f" {child.get('estimatedPoints', 0)} pts", style="dim")
397
+ child_label.append(f" {child.get('storyCount', 0)} stories", style="dim")
398
+ child_data: dict[str, Any] = {
399
+ "type": "future_epic_child",
400
+ "id": child.get("id", ""),
401
+ }
402
+ init_node.add_leaf(child_label, data=child_data)
403
+
404
+ # Restore expand state or collapse by default
405
+ if init_id in saved_expanded:
406
+ if saved_expanded[init_id]:
407
+ init_node.expand()
408
+ else:
409
+ init_node.collapse()
410
+ else:
411
+ init_node.collapse()
412
+
413
+ if cursor_node_key == f"epic:{init_id}":
414
+ cursor_target = init_node
415
+
359
416
  # Restore cursor position
360
417
  if cursor_target is not None:
361
418
  try:
@@ -27,11 +27,9 @@ from pf.bc.focus import get_last_panel, save_last_panel
27
27
  from pf.bikerack.audit_log_panel import AuditLogPanel
28
28
  from pf.bikerack.background_panel import BackgroundPanel
29
29
  from pf.bikerack.base_panel import get_panel_icon
30
- from pf.bikerack.changed_panel import ChangedPanel
31
30
  from pf.bikerack.context_meter_footer import ContextMeterFooter
32
31
  from pf.bikerack.debug_panel import DebugPanel
33
32
  from pf.bikerack.diffs_panel import DiffsPanel
34
- from pf.bikerack.events import NavigateToFile
35
33
  from pf.bikerack.git_panel import GitPanel
36
34
  from pf.bikerack.progress_panel import ProgressPanel
37
35
  from pf.bikerack.sprint_panel import SprintPanel
@@ -79,7 +77,6 @@ PANEL_REGISTRY: list[tuple[str, str]] = [
79
77
  ("sprint", "Sprint"),
80
78
  ("git", "Git"),
81
79
  ("diffs", "Diffs"),
82
- ("changed", "Changed"),
83
80
  ("background", "Background"),
84
81
  ("audit-log", "Audit Log"),
85
82
  ("debug", "Debug"),
@@ -95,7 +92,6 @@ PANEL_DISPLAY_NAMES: dict[str, str] = {
95
92
  "workflow": "Workflow",
96
93
  "background": "Background",
97
94
  "audit-log": "Audit Log",
98
- "changed": "Changed",
99
95
  "ac": "Acceptance Criteria",
100
96
  "debug": "Debug",
101
97
  "progress": "Progress",
@@ -110,7 +106,7 @@ _PANEL_KEYS = [key for key, _ in PANEL_REGISTRY]
110
106
  # Story 110-4: Named presets for common side-by-side views.
111
107
  SPLIT_PRESETS: dict[str, tuple[str, str]] = {
112
108
  "sprint+diffs": ("sprint", "diffs"),
113
- "changed+diffs": ("changed", "diffs"),
109
+ "git+diffs": ("git", "diffs"),
114
110
  "progress+debug": ("progress", "debug"),
115
111
  }
116
112
 
@@ -442,11 +438,10 @@ class BikeRackApp(App):
442
438
  Binding("1", "switch_panel('sprint')", "Sprint", show=False),
443
439
  Binding("2", "switch_panel('git')", "Git", show=False),
444
440
  Binding("3", "switch_panel('diffs')", "Diffs", show=False),
445
- Binding("4", "switch_panel('changed')", "Changed", show=False),
446
- Binding("5", "switch_panel('background')", "Background", show=False),
447
- Binding("6", "switch_panel('audit-log')", "Audit Log", show=False),
448
- Binding("7", "switch_panel('debug')", "Debug", show=False),
449
- Binding("8", "switch_panel('progress')", "Progress", show=False),
441
+ Binding("4", "switch_panel('background')", "Background", show=False),
442
+ Binding("5", "switch_panel('audit-log')", "Audit Log", show=False),
443
+ Binding("6", "switch_panel('debug')", "Debug", show=False),
444
+ Binding("7", "switch_panel('progress')", "Progress", show=False),
450
445
  Binding("bracketright", "next_panel", "]Next"),
451
446
  Binding("bracketleft", "prev_panel", "[Prev"),
452
447
  Binding("tab", "next_panel", show=False, priority=True),
@@ -491,7 +486,6 @@ class BikeRackApp(App):
491
486
  yield SprintPanel(client=self._client, id="panel-sprint")
492
487
  yield GitPanel(client=self._client, id="panel-git")
493
488
  yield DiffsPanel(client=self._client, id="panel-diffs")
494
- yield ChangedPanel(client=self._client, id="panel-changed")
495
489
  yield BackgroundPanel(client=self._client, id="panel-background")
496
490
  yield AuditLogPanel(client=self._client, id="panel-audit-log")
497
491
  yield DebugPanel(client=self._client, id="panel-debug")
@@ -537,15 +531,6 @@ class BikeRackApp(App):
537
531
  self._client.subscribe("persona", self._handle_persona_message)
538
532
  self.run_worker(self._client.connect(), exclusive=True, name="ws-client")
539
533
 
540
- def on_navigate_to_file(self, event: NavigateToFile) -> None:
541
- """Handle NavigateToFile — switch to diffs and navigate to file."""
542
- self.action_switch_panel("diffs")
543
- try:
544
- diffs = self.query_one("#panel-diffs", DiffsPanel)
545
- diffs.navigate_to_file(event.path)
546
- except Exception:
547
- pass
548
-
549
534
  def action_switch_panel(self, key: str) -> None:
550
535
  """Switch to a panel by key."""
551
536
  if key not in _PANEL_KEYS:
@@ -61,7 +61,7 @@ _HEADER_PATTERNS: dict[str, re.Pattern[str]] = {
61
61
  "date": re.compile(r"^Date:\s*(.+)$", re.MULTILINE),
62
62
  }
63
63
 
64
- _TITLE_RE = re.compile(r"^#\s+Story\s+\d+\.\d+:\s*(.+)$", re.MULTILINE)
64
+ _TITLE_RE = re.compile(r"^#\s+Story\s+\d+\.\d+(?:\.\d+)?:\s*(.+)$", re.MULTILINE)
65
65
 
66
66
  # AC block: everything between ## Acceptance Criteria and the next ## heading
67
67
  _AC_RE = re.compile(
@@ -88,10 +88,17 @@ def parse_bmad_story(path: Path) -> dict[str, Any]:
88
88
  fields[name] = match.group(1).strip()
89
89
 
90
90
  story_key = fields.get("story_key", "")
91
- parts = story_key.split("-", 2) # e.g. "1-5-testing-framework" ["1","5","testing-framework"]
92
- epic_num = parts[0] if len(parts) >= 2 else "0"
93
- story_num = parts[1] if len(parts) >= 2 else "0"
94
- pf_id = f"{epic_num}-{story_num}"
91
+ # Consume all leading numeric segments as the ID
92
+ # e.g. "1-5-testing-framework" "1-5", "2-8-1-claroty-plugin" "2-8-1"
93
+ key_parts = story_key.split("-")
94
+ id_segments: list[str] = []
95
+ for seg in key_parts:
96
+ if seg.isdigit():
97
+ id_segments.append(seg)
98
+ else:
99
+ break
100
+ pf_id = "-".join(id_segments) if len(id_segments) >= 2 else "0-0"
101
+ epic_num = id_segments[0] if id_segments else "0"
95
102
 
96
103
  # Title from # heading
97
104
  title_match = _TITLE_RE.search(content)
@@ -194,10 +201,9 @@ def discover_bmad_stories(
194
201
  story = parse_bmad_story(md_file)
195
202
  stories.append(story)
196
203
 
197
- # Sort by epic number, then story number
198
- def sort_key(s: dict) -> tuple[int, int]:
199
- parts = s["id"].split("-")
200
- return (int(parts[0]), int(parts[1]))
204
+ # Sort by epic number, then story number (supports variable-length IDs like 2-8-1)
205
+ def sort_key(s: dict) -> tuple[int, ...]:
206
+ return tuple(int(p) for p in s["id"].split("-"))
201
207
 
202
208
  stories.sort(key=sort_key)
203
209
  return stories
@@ -1,8 +1,9 @@
1
1
  """PR mode configuration reader.
2
2
 
3
- Reads the pr_mode preference from .pennyfarthing/config.local.yaml.
3
+ Reads the pr_mode and pr_merge preferences from .pennyfarthing/config.local.yaml.
4
4
 
5
- Values: draft | ready | none (default: draft)
5
+ pr_mode values: draft | ready | none (default: draft)
6
+ pr_merge values: auto | human (default: auto)
6
7
  """
7
8
 
8
9
  from __future__ import annotations
@@ -12,6 +13,9 @@ from pf.common.config import load_pennyfarthing_config
12
13
  VALID_PR_MODES = {"draft", "ready", "none"}
13
14
  DEFAULT_PR_MODE = "draft"
14
15
 
16
+ VALID_PR_MERGE_MODES = {"auto", "human"}
17
+ DEFAULT_PR_MERGE_MODE = "auto"
18
+
15
19
 
16
20
  def get_pr_mode() -> str:
17
21
  """Read pr_mode from pennyfarthing config.
@@ -34,5 +38,26 @@ def get_pr_mode() -> str:
34
38
  return mode
35
39
 
36
40
 
41
+ def get_pr_merge_mode() -> str:
42
+ """Read pr_merge from pennyfarthing config.
43
+
44
+ Looks for workflow.pr_merge in .pennyfarthing/config.local.yaml.
45
+ Falls back to 'auto' if not set or invalid.
46
+
47
+ Returns:
48
+ One of: 'auto', 'human'
49
+ """
50
+ config = load_pennyfarthing_config()
51
+ workflow = config.get("workflow", {})
52
+ if not isinstance(workflow, dict):
53
+ return DEFAULT_PR_MERGE_MODE
54
+
55
+ mode = workflow.get("pr_merge", DEFAULT_PR_MERGE_MODE)
56
+ if mode not in VALID_PR_MERGE_MODES:
57
+ return DEFAULT_PR_MERGE_MODE
58
+
59
+ return mode
60
+
61
+
37
62
  if __name__ == "__main__":
38
63
  print(get_pr_mode())