@pennyfarthing/core 11.2.1 → 11.3.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 (405) hide show
  1. package/README.md +102 -42
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/doctor-legacy.test.js +2 -2
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  5. package/packages/core/dist/cli/commands/doctor.d.ts +55 -0
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +324 -50
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/init.d.ts +12 -0
  10. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  11. package/packages/core/dist/cli/commands/init.js +45 -0
  12. package/packages/core/dist/cli/commands/init.js.map +1 -1
  13. package/packages/core/dist/cli/commands/pyproject-install.test.d.ts +19 -0
  14. package/packages/core/dist/cli/commands/pyproject-install.test.d.ts.map +1 -0
  15. package/packages/core/dist/cli/commands/pyproject-install.test.js +261 -0
  16. package/packages/core/dist/cli/commands/pyproject-install.test.js.map +1 -0
  17. package/packages/core/dist/cli/commands/stale-artifacts-cleanup.test.d.ts +17 -0
  18. package/packages/core/dist/cli/commands/stale-artifacts-cleanup.test.d.ts.map +1 -0
  19. package/packages/core/dist/cli/commands/stale-artifacts-cleanup.test.js +470 -0
  20. package/packages/core/dist/cli/commands/stale-artifacts-cleanup.test.js.map +1 -0
  21. package/packages/core/dist/cli/commands/update-consolidation.test.js +14 -6
  22. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  23. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  24. package/packages/core/dist/cli/commands/update.js +31 -2
  25. package/packages/core/dist/cli/commands/update.js.map +1 -1
  26. package/packages/core/dist/cli/index.js +2 -0
  27. package/packages/core/dist/cli/index.js.map +1 -1
  28. package/packages/core/dist/cli/utils/python.d.ts.map +1 -1
  29. package/packages/core/dist/cli/utils/python.js +11 -0
  30. package/packages/core/dist/cli/utils/python.js.map +1 -1
  31. package/packages/core/dist/cli/utils/settings-hook-migration.test.d.ts +17 -0
  32. package/packages/core/dist/cli/utils/settings-hook-migration.test.d.ts.map +1 -0
  33. package/packages/core/dist/cli/utils/settings-hook-migration.test.js +382 -0
  34. package/packages/core/dist/cli/utils/settings-hook-migration.test.js.map +1 -0
  35. package/packages/core/dist/cli/utils/settings.d.ts +0 -4
  36. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  37. package/packages/core/dist/cli/utils/settings.js +45 -27
  38. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  39. package/packages/core/dist/cli/utils/stale-artifacts.d.ts +59 -0
  40. package/packages/core/dist/cli/utils/stale-artifacts.d.ts.map +1 -0
  41. package/packages/core/dist/cli/utils/stale-artifacts.js +163 -0
  42. package/packages/core/dist/cli/utils/stale-artifacts.js.map +1 -0
  43. package/packages/core/dist/consultation/dialogue-manager.d.ts +1 -1
  44. package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -1
  45. package/packages/core/dist/consultation/dialogue-manager.js +1 -1
  46. package/packages/core/dist/consultation/dialogue-manager.js.map +1 -1
  47. package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -1
  48. package/packages/core/dist/consultation/tandem-metrics.test.js.map +1 -1
  49. package/packages/core/dist/public/css/react.css +1 -1
  50. package/packages/core/dist/public/js/react/react.js +9 -9
  51. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  52. package/packages/core/dist/server/api/git.js +0 -1
  53. package/packages/core/dist/server/api/git.js.map +1 -1
  54. package/packages/core/dist/server/api/index.d.ts +2 -0
  55. package/packages/core/dist/server/api/index.d.ts.map +1 -1
  56. package/packages/core/dist/server/api/index.js +2 -0
  57. package/packages/core/dist/server/api/index.js.map +1 -1
  58. package/packages/core/dist/server/api/project-info.d.ts +11 -0
  59. package/packages/core/dist/server/api/project-info.d.ts.map +1 -0
  60. package/packages/core/dist/server/api/project-info.js +18 -0
  61. package/packages/core/dist/server/api/project-info.js.map +1 -0
  62. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  63. package/packages/core/dist/server/otlp-receiver.js +18 -1
  64. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  65. package/packages/core/dist/server/otlp-receiver.test.js +1 -1
  66. package/packages/core/dist/server/otlp-receiver.test.js.map +1 -1
  67. package/packages/core/dist/server/server.d.ts.map +1 -1
  68. package/packages/core/dist/server/server.js +3 -2
  69. package/packages/core/dist/server/server.js.map +1 -1
  70. package/packages/core/dist/server/server.test.d.ts +1 -1
  71. package/packages/core/dist/server/server.test.js +8 -8
  72. package/packages/core/dist/server/settings.d.ts +1 -0
  73. package/packages/core/dist/server/settings.d.ts.map +1 -1
  74. package/packages/core/dist/server/settings.js +18 -0
  75. package/packages/core/dist/server/settings.js.map +1 -1
  76. package/packages/core/dist/workflow/tandem-workflow-templates.test.js +7 -5
  77. package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -1
  78. package/packages/core/dist/workflow/workflow-migration.test.js +6 -5
  79. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  80. package/packages/core/dist/workflow/workflow-team-templates.test.d.ts +17 -0
  81. package/packages/core/dist/workflow/workflow-team-templates.test.d.ts.map +1 -0
  82. package/packages/core/dist/workflow/workflow-team-templates.test.js +275 -0
  83. package/packages/core/dist/workflow/workflow-team-templates.test.js.map +1 -0
  84. package/pennyfarthing-dist/agents/dev.md +19 -4
  85. package/pennyfarthing-dist/agents/devops.md +2 -10
  86. package/pennyfarthing-dist/agents/reviewer-preflight.md +4 -5
  87. package/pennyfarthing-dist/agents/reviewer.md +17 -4
  88. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  89. package/pennyfarthing-dist/agents/sm-setup.md +7 -7
  90. package/pennyfarthing-dist/agents/sm.md +16 -29
  91. package/pennyfarthing-dist/agents/tea.md +2 -2
  92. package/pennyfarthing-dist/agents/testing-runner.md +1 -1
  93. package/pennyfarthing-dist/commands/pf-architect.md +1 -1
  94. package/pennyfarthing-dist/commands/pf-ba.md +1 -1
  95. package/pennyfarthing-dist/commands/pf-chore.md +2 -2
  96. package/pennyfarthing-dist/commands/pf-dev.md +1 -1
  97. package/pennyfarthing-dist/commands/pf-devops.md +1 -1
  98. package/pennyfarthing-dist/commands/pf-epic.md +6 -6
  99. package/pennyfarthing-dist/commands/pf-git.md +10 -10
  100. package/pennyfarthing-dist/commands/pf-health-check.md +31 -12
  101. package/pennyfarthing-dist/commands/pf-help.md +12 -12
  102. package/pennyfarthing-dist/commands/pf-orchestrator.md +1 -1
  103. package/pennyfarthing-dist/commands/pf-pm.md +1 -1
  104. package/pennyfarthing-dist/commands/pf-prime.md +8 -8
  105. package/pennyfarthing-dist/commands/pf-reviewer.md +1 -1
  106. package/pennyfarthing-dist/commands/pf-session.md +7 -7
  107. package/pennyfarthing-dist/commands/pf-sm.md +1 -1
  108. package/pennyfarthing-dist/commands/pf-sprint.md +7 -7
  109. package/pennyfarthing-dist/commands/pf-tea.md +1 -1
  110. package/pennyfarthing-dist/commands/pf-tech-writer.md +1 -1
  111. package/pennyfarthing-dist/commands/pf-theme.md +9 -9
  112. package/pennyfarthing-dist/commands/pf-ux-designer.md +1 -1
  113. package/pennyfarthing-dist/commands/pf-work.md +1 -1
  114. package/pennyfarthing-dist/gates/{confidence-sm.md → confidence.md} +16 -17
  115. package/pennyfarthing-dist/gates/dev-exit.md +75 -0
  116. package/pennyfarthing-dist/gates/merge-ready.md +49 -0
  117. package/pennyfarthing-dist/gates/release-ready.md +95 -0
  118. package/pennyfarthing-dist/gates/reviewer-preflight-check.md +90 -0
  119. package/pennyfarthing-dist/gates/sm-setup-exit.md +82 -0
  120. package/pennyfarthing-dist/guides/agent-behavior.md +129 -20
  121. package/pennyfarthing-dist/guides/agent-coordination.md +10 -10
  122. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -6
  123. package/pennyfarthing-dist/guides/agent-template-tactical.md +1 -1
  124. package/pennyfarthing-dist/guides/bell-mode.md +1 -1
  125. package/pennyfarthing-dist/guides/bikerack.md +10 -10
  126. package/pennyfarthing-dist/guides/brownfield-tools.md +24 -24
  127. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +1 -1
  128. package/pennyfarthing-dist/guides/gate-schema.md +2 -2
  129. package/pennyfarthing-dist/guides/gates.md +10 -5
  130. package/pennyfarthing-dist/guides/handoff-cli.md +8 -8
  131. package/pennyfarthing-dist/guides/hooks.md +27 -27
  132. package/pennyfarthing-dist/guides/prime.md +2 -2
  133. package/pennyfarthing-dist/guides/reflector.md +1 -1
  134. package/pennyfarthing-dist/guides/skill-schema.md +6 -6
  135. package/pennyfarthing-dist/guides/tandem-protocol.md +3 -3
  136. package/pennyfarthing-dist/guides/workflow-schema.md +1 -1
  137. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  138. package/pennyfarthing-dist/guides/xml-tags.md +8 -8
  139. package/pennyfarthing-dist/scripts/README.md +4 -4
  140. package/pennyfarthing-dist/scripts/core/agent-session.sh +2 -5
  141. package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
  142. package/pennyfarthing-dist/scripts/core/pf.sh +5 -0
  143. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +2 -5
  144. package/pennyfarthing-dist/scripts/core/prime.sh +2 -25
  145. package/pennyfarthing-dist/scripts/git/README.md +14 -14
  146. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +2 -3
  147. package/pennyfarthing-dist/scripts/git/git-status-all.sh +2 -3
  148. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -3
  149. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +2 -4
  150. package/pennyfarthing-dist/scripts/hooks/README.md +6 -6
  151. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +3 -3
  152. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +3 -3
  153. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +3 -3
  154. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +3 -3
  155. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +5 -4
  156. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +2 -1
  157. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +3 -3
  158. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +3 -3
  159. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +3 -3
  160. package/pennyfarthing-dist/scripts/hooks/session-start.sh +3 -3
  161. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +3 -3
  162. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +3 -3
  163. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +3 -4
  164. package/pennyfarthing-dist/scripts/lib/env.sh +34 -0
  165. package/pennyfarthing-dist/scripts/lib/find-root.sh +5 -0
  166. package/pennyfarthing-dist/scripts/lib/run-pf.sh +43 -0
  167. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  168. package/pennyfarthing-dist/scripts/misc/statusline.sh +3 -3
  169. package/pennyfarthing-dist/scripts/sprint/README.md +21 -21
  170. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  171. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +2 -16
  172. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +3 -3
  173. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +3 -3
  174. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +3 -3
  175. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +3 -3
  176. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +3 -3
  177. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +3 -3
  178. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +3 -3
  179. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +3 -3
  180. package/pennyfarthing-dist/skills/pf-bc/examples.md +23 -23
  181. package/pennyfarthing-dist/skills/pf-bc/skill.md +17 -17
  182. package/pennyfarthing-dist/skills/pf-bc/usage.md +8 -8
  183. package/pennyfarthing-dist/skills/pf-jira/SKILL.md +15 -15
  184. package/pennyfarthing-dist/skills/pf-jira/examples.md +48 -48
  185. package/pennyfarthing-dist/skills/pf-jira/usage.md +15 -15
  186. package/pennyfarthing-dist/skills/pf-settings/skill.md +42 -0
  187. package/pennyfarthing-dist/skills/pf-sprint/examples.md +80 -80
  188. package/pennyfarthing-dist/skills/pf-sprint/skill.md +35 -35
  189. package/pennyfarthing-dist/skills/pf-sprint/usage.md +30 -30
  190. package/pennyfarthing-dist/skills/pf-theme/examples.md +15 -15
  191. package/pennyfarthing-dist/skills/pf-theme/skill.md +6 -6
  192. package/pennyfarthing-dist/skills/pf-theme/usage.md +5 -5
  193. package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -27
  194. package/pennyfarthing-dist/skills/pf-workflow/skill.md +11 -11
  195. package/pennyfarthing-dist/skills/pf-workflow/usage.md +11 -11
  196. package/pennyfarthing-dist/skills/skill-registry.yaml +34 -19
  197. package/pennyfarthing-dist/templates/pyproject.toml +27 -0
  198. package/pennyfarthing-dist/templates/settings.local.json.template +11 -11
  199. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +7 -3
  200. package/pennyfarthing-dist/workflows/bdd-team.yaml +89 -0
  201. package/pennyfarthing-dist/workflows/bdd.yaml +7 -3
  202. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +1 -1
  203. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
  204. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
  205. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  206. package/pennyfarthing-dist/workflows/installation-check/steps/step-01-foundation.md +77 -0
  207. package/pennyfarthing-dist/workflows/installation-check/steps/step-02-commands.md +82 -0
  208. package/pennyfarthing-dist/workflows/installation-check/steps/step-03-hooks.md +121 -0
  209. package/pennyfarthing-dist/workflows/installation-check/steps/step-04-scripts.md +83 -0
  210. package/pennyfarthing-dist/workflows/installation-check/steps/step-05-layout.md +81 -0
  211. package/pennyfarthing-dist/workflows/installation-check/steps/step-06-legacy.md +94 -0
  212. package/pennyfarthing-dist/workflows/installation-check/steps/step-07-tools.md +80 -0
  213. package/pennyfarthing-dist/workflows/installation-check/steps/step-08-summary.md +99 -0
  214. package/pennyfarthing-dist/workflows/installation-check/workflow.yaml +47 -0
  215. package/pennyfarthing-dist/workflows/project-setup/steps/step-01-discover.md +47 -0
  216. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +7 -3
  217. package/pennyfarthing-dist/workflows/tdd-team.yaml +80 -0
  218. package/pennyfarthing-dist/workflows/tdd.yaml +7 -3
  219. package/pennyfarthing-dist/workflows/trivial.yaml +7 -3
  220. package/pennyfarthing_scripts/__init__.py +1 -1
  221. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  222. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/__pycache__/cli.cpython-311.pyc +0 -0
  224. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/__pycache__/context.cpython-311.pyc +0 -0
  226. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-311.pyc +0 -0
  228. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-311.pyc +0 -0
  229. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-311.pyc +0 -0
  231. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/bc/__pycache__/split.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/bc/cli.py +23 -2
  234. package/pennyfarthing_scripts/bc/focus.py +1 -0
  235. package/pennyfarthing_scripts/bc/split.py +52 -0
  236. package/pennyfarthing_scripts/bellmode_hook.py +2 -5
  237. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-311.pyc +0 -0
  238. package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
  239. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  240. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  241. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  242. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-311.pyc +0 -0
  243. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  244. package/pennyfarthing_scripts/bikerack/__pycache__/context_meter_footer.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/bikerack/__pycache__/events.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-311.pyc +0 -0
  250. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/bikerack/audit_log_panel.py +48 -6
  259. package/pennyfarthing_scripts/bikerack/context_meter_footer.py +53 -3
  260. package/pennyfarthing_scripts/bikerack/launcher.py +6 -6
  261. package/pennyfarthing_scripts/bikerack/progress_panel.py +0 -1
  262. package/pennyfarthing_scripts/bikerack/sprint_panel.py +1 -1
  263. package/pennyfarthing_scripts/bikerack/story_detail_data.py +4 -1
  264. package/pennyfarthing_scripts/bikerack/story_detail_screen.py +2 -1
  265. package/pennyfarthing_scripts/bikerack/tui.py +214 -10
  266. package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
  267. package/pennyfarthing_scripts/cli.py +5 -0
  268. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-311.pyc +0 -0
  269. package/pennyfarthing_scripts/common/__pycache__/config.cpython-311.pyc +0 -0
  270. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/common/__pycache__/output.cpython-311.pyc +0 -0
  272. package/pennyfarthing_scripts/common/__pycache__/pr_config.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/common/config.py +29 -2
  274. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-311.pyc +0 -0
  275. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-311.pyc +0 -0
  277. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/consultation/cli.py +3 -3
  279. package/pennyfarthing_scripts/context.py +3 -3
  280. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-311.pyc +0 -0
  281. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-311.pyc +0 -0
  282. package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-311.pyc +0 -0
  283. package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-311.pyc +0 -0
  284. package/pennyfarthing_scripts/git/hooks_installer.py +2 -3
  285. package/pennyfarthing_scripts/git/status_all.py +1 -1
  286. package/pennyfarthing_scripts/git/worktree.py +2 -2
  287. package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-311.pyc +0 -0
  288. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-311.pyc +0 -0
  289. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-311.pyc +0 -0
  291. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-311.pyc +0 -0
  292. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  293. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  294. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  295. package/pennyfarthing_scripts/handoff/__pycache__/phase_check.cpython-314.pyc +0 -0
  296. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  297. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-311.pyc +0 -0
  298. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-311.pyc +0 -0
  299. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-311.pyc +0 -0
  300. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-311.pyc +0 -0
  301. package/pennyfarthing_scripts/hooks/__init__.py +8 -3
  302. package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  303. package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  304. package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-311.pyc +0 -0
  305. package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  306. package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-311.pyc +0 -0
  307. package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-311.pyc +0 -0
  309. package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-311.pyc +0 -0
  311. package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-311.pyc +0 -0
  313. package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-311.pyc +0 -0
  315. package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  316. package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-311.pyc +0 -0
  317. package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  318. package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-311.pyc +0 -0
  319. package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  320. package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
  321. package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  323. package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-311.pyc +0 -0
  324. package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-311.pyc +0 -0
  326. package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  327. package/pennyfarthing_scripts/hooks/bell_mode.py +0 -1
  328. package/pennyfarthing_scripts/hooks/pre_edit_check.py +0 -1
  329. package/pennyfarthing_scripts/hooks/reflector_check.py +1 -2
  330. package/pennyfarthing_scripts/hooks/schema_validation.py +0 -1
  331. package/pennyfarthing_scripts/hooks/session_start.py +6 -8
  332. package/pennyfarthing_scripts/hooks/statusline.py +10 -1
  333. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-311.pyc +0 -0
  334. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-311.pyc +0 -0
  335. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-311.pyc +0 -0
  336. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-311.pyc +0 -0
  337. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-311.pyc +0 -0
  338. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-311.pyc +0 -0
  339. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-311.pyc +0 -0
  340. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-311.pyc +0 -0
  341. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-311.pyc +0 -0
  342. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-311.pyc +0 -0
  343. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-311.pyc +0 -0
  344. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-311.pyc +0 -0
  345. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-311.pyc +0 -0
  346. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-311.pyc +0 -0
  347. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-311.pyc +0 -0
  348. package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-311.pyc +0 -0
  349. package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-311.pyc +0 -0
  350. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  351. package/pennyfarthing_scripts/prime/heatmap.py +3 -15
  352. package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-311.pyc +0 -0
  353. package/pennyfarthing_scripts/session/__pycache__/cli.cpython-311.pyc +0 -0
  354. package/pennyfarthing_scripts/settings/__init__.py +0 -0
  355. package/pennyfarthing_scripts/settings/__pycache__/__init__.cpython-314.pyc +0 -0
  356. package/pennyfarthing_scripts/settings/__pycache__/cli.cpython-314.pyc +0 -0
  357. package/pennyfarthing_scripts/settings/__pycache__/settings.cpython-314.pyc +0 -0
  358. package/pennyfarthing_scripts/settings/cli.py +55 -0
  359. package/pennyfarthing_scripts/settings/settings.py +98 -0
  360. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-311.pyc +0 -0
  361. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-311.pyc +0 -0
  362. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-311.pyc +0 -0
  363. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  364. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-311.pyc +0 -0
  365. package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-311.pyc +0 -0
  366. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-311.pyc +0 -0
  367. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  368. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-311.pyc +0 -0
  369. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-311.pyc +0 -0
  370. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  371. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-311.pyc +0 -0
  372. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  373. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-311.pyc +0 -0
  374. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-311.pyc +0 -0
  375. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-311.pyc +0 -0
  376. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-311.pyc +0 -0
  377. package/pennyfarthing_scripts/sprint/cli.py +121 -0
  378. package/pennyfarthing_scripts/sprint/loader.py +154 -3
  379. package/pennyfarthing_scripts/sprint/story_update.py +7 -0
  380. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  381. package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
  382. package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
  383. package/pennyfarthing_scripts/tests/test_bikerack.py +26 -26
  384. package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +17 -16
  385. package/pennyfarthing_scripts/tests/test_dialogue_manager.py +0 -1
  386. package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +45 -47
  387. package/pennyfarthing_scripts/tests/test_workflow_list_team.py +143 -0
  388. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-311.pyc +0 -0
  389. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-311.pyc +0 -0
  390. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-311.pyc +0 -0
  391. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-311.pyc +0 -0
  392. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  393. package/pennyfarthing_scripts/validate/adapters/team_mode.py +323 -0
  394. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-311.pyc +0 -0
  395. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  396. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-311.pyc +0 -0
  397. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  398. package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  399. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-311.pyc +0 -0
  400. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  401. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-311.pyc +0 -0
  402. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
  403. package/pennyfarthing_scripts/workflow/cli.py +15 -14
  404. package/pennyfarthing_scripts/workflow/state.py +0 -1
  405. package/pennyfarthing_scripts/workflow/team_lifecycle.py +3 -4
@@ -10,6 +10,7 @@ Panel navigation: Mount all panels, tab bar, keyboard switching, command palette
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import os
13
14
  from functools import partial
14
15
  from pathlib import Path
15
16
  from typing import Any
@@ -105,6 +106,14 @@ PANEL_DISPLAY_NAMES: dict[str, str] = {
105
106
  # Keys from PANEL_REGISTRY for fast lookup
106
107
  _PANEL_KEYS = [key for key, _ in PANEL_REGISTRY]
107
108
 
109
+ # Split-pane layout presets: name → (left_panel, right_panel)
110
+ # Story 110-4: Named presets for common side-by-side views.
111
+ SPLIT_PRESETS: dict[str, tuple[str, str]] = {
112
+ "sprint+diffs": ("sprint", "diffs"),
113
+ "changed+diffs": ("changed", "diffs"),
114
+ "progress+debug": ("progress", "debug"),
115
+ }
116
+
108
117
 
109
118
  class BindingFooter(Footer):
110
119
  """Footer subclass that exposes active binding text via render().
@@ -359,9 +368,10 @@ class BikeRackApp(App):
359
368
  class FocusUpdate(Message, bubble=False):
360
369
  """Focus change from WS — routed through Textual message system."""
361
370
 
362
- def __init__(self, focus: str | None) -> None:
371
+ def __init__(self, focus: str | None, split_config: dict | None = None) -> None:
363
372
  super().__init__()
364
373
  self.focus = focus
374
+ self.split_config = split_config
365
375
 
366
376
  class WsStateUpdate(Message, bubble=False):
367
377
  """WS connection state change — routed through Textual message system."""
@@ -392,6 +402,11 @@ class BikeRackApp(App):
392
402
  height: auto;
393
403
  width: 1fr;
394
404
  }
405
+ #project-dir {
406
+ height: 1;
407
+ padding: 0 1;
408
+ color: $text-muted;
409
+ }
395
410
  Tabs {
396
411
  dock: top;
397
412
  }
@@ -407,12 +422,23 @@ class BikeRackApp(App):
407
422
  ContextMeterFooter {
408
423
  height: 1;
409
424
  }
425
+ #split-container {
426
+ display: none;
427
+ height: 1fr;
428
+ }
429
+ #split-left {
430
+ width: 1fr;
431
+ }
432
+ #split-right {
433
+ width: 1fr;
434
+ }
410
435
  """
411
436
 
412
437
  COMMANDS = App.COMMANDS | {PanelCommands}
413
438
 
414
439
  BINDINGS = [
415
440
  Binding("q", "quit", "Quit"),
441
+ Binding("shift+s", "toggle_split", "Split"),
416
442
  Binding("1", "switch_panel('sprint')", "Sprint", show=False),
417
443
  Binding("2", "switch_panel('git')", "Git", show=False),
418
444
  Binding("3", "switch_panel('diffs')", "Diffs", show=False),
@@ -423,8 +449,8 @@ class BikeRackApp(App):
423
449
  Binding("8", "switch_panel('progress')", "Progress", show=False),
424
450
  Binding("bracketright", "next_panel", "]Next"),
425
451
  Binding("bracketleft", "prev_panel", "[Prev"),
426
- Binding("tab", "next_panel", show=False),
427
- Binding("shift+tab", "prev_panel", show=False),
452
+ Binding("tab", "next_panel", show=False, priority=True),
453
+ Binding("shift+tab", "prev_panel", show=False, priority=True),
428
454
  Binding("n", "next_diff_file", "Next file", show=False),
429
455
  Binding("p", "prev_diff_file", "Prev file", show=False),
430
456
  Binding("j", "next_epic", show=False),
@@ -442,10 +468,20 @@ class BikeRackApp(App):
442
468
  self._focused_panel: str = "sprint"
443
469
  self._previous_panel: str | None = None
444
470
  self._programmatic_tab_count: int = 0
471
+ self._context_meter: ContextMeterFooter | None = None
472
+ # Split-pane state (Story 110-4)
473
+ self._split_mode: bool = False
474
+ self._active_split_pane: str = "left"
475
+ self._split_left_key: str = "sprint"
476
+ self._split_right_key: str = "diffs"
445
477
 
446
478
  def compose(self) -> ComposeResult:
479
+ project_dir_name = Path(
480
+ os.environ.get("CYCLIST_PROJECT_DIR", os.getcwd())
481
+ ).name
447
482
  yield Header()
448
483
  yield AgentHeader(id="agent-header")
484
+ yield Static(f"[dim]{project_dir_name}[/dim]", id="project-dir")
449
485
  yield Tabs(*_build_panel_tabs(), id="tab-bar")
450
486
  yield ConnectionStatus(
451
487
  STATE_DISPLAY[ConnectionState.DISCONNECTED],
@@ -460,7 +496,11 @@ class BikeRackApp(App):
460
496
  yield AuditLogPanel(client=self._client, id="panel-audit-log")
461
497
  yield DebugPanel(client=self._client, id="panel-debug")
462
498
  yield ProgressPanel(client=self._client, id="panel-progress")
463
- yield ContextMeterFooter(client=self._client)
499
+ with Horizontal(id="split-container"):
500
+ yield VerticalScroll(id="split-left")
501
+ yield VerticalScroll(id="split-right")
502
+ self._context_meter = ContextMeterFooter(client=self._client)
503
+ yield self._context_meter
464
504
  yield BindingFooter()
465
505
 
466
506
  async def on_mount(self) -> None:
@@ -533,8 +573,27 @@ class BikeRackApp(App):
533
573
  save_last_panel(key, project_dir=None)
534
574
  self._update_tab_bar(key)
535
575
 
576
+ # Refresh context meter on panel switch (110-12)
577
+ if self._context_meter is not None:
578
+ self._context_meter.request_refresh()
579
+
536
580
  def action_next_panel(self) -> None:
537
- """Cycle to the next panel."""
581
+ """Cycle to the next panel, or toggle pane focus in split mode."""
582
+ if self._split_mode:
583
+ # In split mode, Tab toggles between left and right pane
584
+ if self._active_split_pane == "left":
585
+ self._active_split_pane = "right"
586
+ try:
587
+ self.query_one("#split-right").focus()
588
+ except Exception:
589
+ pass
590
+ else:
591
+ self._active_split_pane = "left"
592
+ try:
593
+ self.query_one("#split-left").focus()
594
+ except Exception:
595
+ pass
596
+ return
538
597
  try:
539
598
  idx = _PANEL_KEYS.index(self._focused_panel)
540
599
  except ValueError:
@@ -596,6 +655,131 @@ class BikeRackApp(App):
596
655
  except Exception:
597
656
  pass
598
657
 
658
+ # ------------------------------------------------------------------
659
+ # Split-pane layout (Story 110-4)
660
+ # ------------------------------------------------------------------
661
+
662
+ def _sync_reparent(self, widget: Any, new_parent: Any) -> None:
663
+ """Move widget from current parent to new parent synchronously.
664
+
665
+ Uses internal Textual DOM API so the move is visible immediately
666
+ without awaiting an async mount/remove cycle.
667
+ """
668
+ old_parent = widget._parent
669
+ if old_parent is not None:
670
+ try:
671
+ old_parent._nodes._remove(widget)
672
+ except (ValueError, AttributeError):
673
+ # Fallback: remove from internal list directly
674
+ try:
675
+ old_parent._nodes._nodes.remove(widget)
676
+ except Exception:
677
+ pass
678
+ try:
679
+ new_parent._nodes._append(widget)
680
+ except AttributeError:
681
+ new_parent._nodes._nodes.append(widget)
682
+ widget._parent = new_parent
683
+
684
+ def _enter_split(self, left_key: str, right_key: str) -> None:
685
+ """Activate split mode with specified panels (synchronous)."""
686
+ if left_key not in _PANEL_KEYS or right_key not in _PANEL_KEYS:
687
+ return
688
+ if left_key == right_key:
689
+ return
690
+
691
+ self._split_mode = True
692
+ self._split_left_key = left_key
693
+ self._split_right_key = right_key
694
+ self._active_split_pane = "left"
695
+
696
+ try:
697
+ main_content = self.query_one("#main-content")
698
+ split_container = self.query_one("#split-container")
699
+ split_left = self.query_one("#split-left")
700
+ split_right = self.query_one("#split-right")
701
+ except Exception:
702
+ return
703
+
704
+ # Move left panel to split-left pane
705
+ try:
706
+ left_panel = self.query_one(f"#panel-{left_key}")
707
+ left_panel.display = True
708
+ self._sync_reparent(left_panel, split_left)
709
+ except Exception:
710
+ pass
711
+
712
+ # Move right panel to split-right pane
713
+ try:
714
+ right_panel = self.query_one(f"#panel-{right_key}")
715
+ right_panel.display = True
716
+ self._sync_reparent(right_panel, split_right)
717
+ except Exception:
718
+ pass
719
+
720
+ # Hide all other panels remaining in main-content
721
+ for panel_key in _PANEL_KEYS:
722
+ if panel_key not in (left_key, right_key):
723
+ try:
724
+ p = self.query_one(f"#panel-{panel_key}")
725
+ p.display = False
726
+ except Exception:
727
+ pass
728
+
729
+ main_content.display = False
730
+ split_container.display = True
731
+
732
+ def _exit_split(self) -> None:
733
+ """Deactivate split mode, return panels to main-content."""
734
+ self._split_mode = False
735
+
736
+ try:
737
+ main_content = self.query_one("#main-content")
738
+ split_container = self.query_one("#split-container")
739
+ split_left = self.query_one("#split-left")
740
+ split_right = self.query_one("#split-right")
741
+ except Exception:
742
+ return
743
+
744
+ # Move panels back from split panes to main-content
745
+ for pane in (split_left, split_right):
746
+ for child in list(pane.children):
747
+ self._sync_reparent(child, main_content)
748
+
749
+ split_container.display = False
750
+ main_content.display = True
751
+
752
+ # Restore single-panel visibility
753
+ for panel_key in _PANEL_KEYS:
754
+ try:
755
+ p = self.query_one(f"#panel-{panel_key}")
756
+ p.display = (panel_key == self._focused_panel)
757
+ except Exception:
758
+ pass
759
+
760
+ def action_toggle_split(self) -> None:
761
+ """Toggle split mode on/off (Shift+S keybinding)."""
762
+ if self._split_mode:
763
+ self._exit_split()
764
+ else:
765
+ # Default: current panel left, next panel right
766
+ left_key = self._focused_panel
767
+ try:
768
+ idx = _PANEL_KEYS.index(left_key)
769
+ except ValueError:
770
+ idx = 0
771
+ right_key = _PANEL_KEYS[(idx + 1) % len(_PANEL_KEYS)]
772
+ self._enter_split(left_key, right_key)
773
+
774
+ def action_apply_split_preset(self, name: str) -> None:
775
+ """Apply a named split preset (synchronous)."""
776
+ if name not in SPLIT_PRESETS:
777
+ return
778
+ left_key, right_key = SPLIT_PRESETS[name]
779
+ if self._split_mode:
780
+ self._exit_split()
781
+ self._enter_split(left_key, right_key)
782
+
599
783
  def _update_tab_bar(self, panel_key: str) -> None:
600
784
  """Update the tab bar widget with the given panel key.
601
785
 
@@ -628,7 +812,10 @@ class BikeRackApp(App):
628
812
  def _handle_focus_message(self, message: dict[str, Any] | None) -> None:
629
813
  """Handle incoming focus channel messages.
630
814
 
631
- Expected format: {type: 'init'|'update', focus: '<panel>'|null}
815
+ Expected format:
816
+ Single panel: {type: 'update', focus: '<panel>'}
817
+ Split layout: {type: 'update', focus: 'split', split: {left: ..., right: ...}}
818
+ Split preset: {type: 'update', focus: 'split:<preset-name>'}
632
819
  Only 'update' messages trigger panel switches (matching React hook).
633
820
  Routes through Textual message system via post_message for proper repaint.
634
821
  """
@@ -638,7 +825,8 @@ class BikeRackApp(App):
638
825
  return
639
826
  if "focus" not in message:
640
827
  return
641
- self.post_message(self.FocusUpdate(message["focus"]))
828
+ split_config = message.get("split")
829
+ self.post_message(self.FocusUpdate(message["focus"], split_config=split_config))
642
830
 
643
831
  def _handle_persona_message(self, message: dict[str, Any] | None) -> None:
644
832
  """Handle incoming persona channel messages.
@@ -667,7 +855,23 @@ class BikeRackApp(App):
667
855
  def on_bike_rack_app_focus_update(self, event: FocusUpdate) -> None:
668
856
  """Apply focus change in Textual message context."""
669
857
  focus = event.focus
670
- if focus is not None and focus in _PANEL_KEYS:
858
+ split_config = event.split_config
859
+
860
+ if focus == "split" and split_config:
861
+ # Explicit split layout: {focus: "split", split: {left: ..., right: ...}}
862
+ left = split_config.get("left", "sprint")
863
+ right = split_config.get("right", "diffs")
864
+ if self._split_mode:
865
+ self._exit_split()
866
+ self._enter_split(left, right)
867
+ elif focus is not None and focus.startswith("split:"):
868
+ # Preset reference: {focus: "split:progress+debug"}
869
+ preset_name = focus[len("split:"):]
870
+ self.action_apply_split_preset(preset_name)
871
+ elif focus is not None and focus in _PANEL_KEYS:
872
+ # Single panel focus — exit split if active
873
+ if self._split_mode:
874
+ self._exit_split()
671
875
  self.action_switch_panel(focus)
672
876
  elif focus is not None:
673
877
  self._previous_panel = self._focused_panel
@@ -693,7 +897,7 @@ def main(
693
897
  """Launch BikeRack TUI as a standalone application.
694
898
 
695
899
  Args:
696
- port: Explicit WheelHub port. If None, reads from .wheelhub-port file.
900
+ port: Explicit WheelHub port. If None, reads from .bikerack-port file.
697
901
  project_dir: Project directory for port file discovery. Defaults to cwd.
698
902
  """
699
903
  # Detect terminal image protocol BEFORE App.run() claims the terminal
@@ -703,7 +907,7 @@ def main(
703
907
 
704
908
  if port is None:
705
909
  if project_dir is not None:
706
- port_file = project_dir / ".wheelhub-port"
910
+ port_file = project_dir / ".bikerack-port"
707
911
  if port_file.exists():
708
912
  try:
709
913
  port = int(port_file.read_text().strip())
@@ -75,14 +75,14 @@ class WheelHubClient:
75
75
  cb(new_state)
76
76
 
77
77
  def discover_port(self) -> int:
78
- """Read port from .wheelhub-port file, fallback to DEFAULT_PORT.
78
+ """Read port from .bikerack-port file, fallback to DEFAULT_PORT.
79
79
 
80
80
  Priority: explicit port > port file > DEFAULT_PORT.
81
81
  """
82
82
  if self._port is not None:
83
83
  return self._port
84
84
  if self._project_dir is not None:
85
- port_file = self._project_dir / ".wheelhub-port"
85
+ port_file = self._project_dir / ".bikerack-port"
86
86
  if port_file.exists():
87
87
  try:
88
88
  return int(port_file.read_text().strip())
@@ -147,6 +147,11 @@ from pennyfarthing_scripts.hooks.cli import hooks # noqa: E402
147
147
 
148
148
  cli.add_command(hooks)
149
149
 
150
+ # Import and register settings group
151
+ from pennyfarthing_scripts.settings.cli import settings # noqa: E402
152
+
153
+ cli.add_command(settings)
154
+
150
155
 
151
156
  @cli.group()
152
157
  def agent():
@@ -82,11 +82,38 @@ def load_yaml_config(path: Path) -> dict[str, Any] | None:
82
82
  return yaml.safe_load(f)
83
83
 
84
84
 
85
- def load_pennyfarthing_config() -> dict[str, Any]:
85
+ def load_pennyfarthing_config(project_root: Path | None = None) -> dict[str, Any]:
86
86
  """Load .pennyfarthing/config.local.yaml.
87
87
 
88
+ Args:
89
+ project_root: Project root path (defaults to auto-detect)
90
+
88
91
  Returns:
89
92
  Config dict, or empty dict if not found
90
93
  """
91
- config_path = get_project_root() / ".pennyfarthing" / "config.local.yaml"
94
+ root = project_root or get_project_root()
95
+ config_path = root / ".pennyfarthing" / "config.local.yaml"
92
96
  return load_yaml_config(config_path) or {}
97
+
98
+
99
+ def save_pennyfarthing_config_key(
100
+ key: str, value: Any, project_root: Path | None = None
101
+ ) -> None:
102
+ """Set a top-level key in .pennyfarthing/config.local.yaml.
103
+
104
+ Creates the file if it doesn't exist. Preserves existing keys.
105
+
106
+ Args:
107
+ key: Top-level key (e.g., "sprint")
108
+ value: Value to set (dict, str, etc.)
109
+ project_root: Project root path (defaults to auto-detect)
110
+ """
111
+ root = project_root or get_project_root()
112
+ config_path = root / ".pennyfarthing" / "config.local.yaml"
113
+
114
+ config = load_yaml_config(config_path) or {}
115
+ config[key] = value
116
+
117
+ config_path.parent.mkdir(parents=True, exist_ok=True)
118
+ with open(config_path, "w") as f:
119
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
@@ -6,7 +6,7 @@ the bash wrapper dialogue-manager.sh interface.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from datetime import datetime, timezone
9
+ from datetime import UTC, datetime
10
10
  from pathlib import Path
11
11
 
12
12
  import click
@@ -44,7 +44,7 @@ def init(story_id: str, workflow: str, leader: str, partner: str) -> None:
44
44
  workflow=workflow,
45
45
  leader=leader,
46
46
  partner=partner,
47
- started_at=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
47
+ started_at=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
48
48
  )
49
49
 
50
50
  content = create_dialogue_content(header)
@@ -80,7 +80,7 @@ def append(story_id: str, question: str, recommendation: str, confidence: str) -
80
80
 
81
81
  exchange = DialogueExchange(
82
82
  number=next_num,
83
- timestamp=datetime.now(timezone.utc).strftime("%H:%M"),
83
+ timestamp=datetime.now(UTC).strftime("%H:%M"),
84
84
  leader="",
85
85
  partner="",
86
86
  question=question,
@@ -269,7 +269,7 @@ def detect_cyclist(project_dir: str | None = None) -> bool:
269
269
 
270
270
  Checks:
271
271
  1. CYCLIST env var set to '1' (Electron mode - definitive)
272
- 2. .wheelhub-port file exists AND port is responding (Web mode)
272
+ 2. .bikerack-port file exists AND port is responding (Web mode)
273
273
  """
274
274
  # Env var is definitive - set by Cyclist when it spawns Claude
275
275
  if os.environ.get("CYCLIST") == "1":
@@ -284,8 +284,8 @@ def detect_cyclist(project_dir: str | None = None) -> bool:
284
284
  )
285
285
 
286
286
  port_files = [
287
- Path(project_dir) / "packages" / "cyclist" / ".wheelhub-port",
288
- Path(os.getcwd()) / ".wheelhub-port",
287
+ Path(project_dir) / "packages" / "cyclist" / ".bikerack-port",
288
+ Path(os.getcwd()) / ".bikerack-port",
289
289
  ]
290
290
 
291
291
  for port_file in port_files:
@@ -10,7 +10,6 @@ Usage via CLI:
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- import re
14
13
  from pathlib import Path
15
14
 
16
15
  from pennyfarthing_scripts.common.config import get_project_root
@@ -70,8 +69,8 @@ def install_git_hooks(project_root: Path | None = None) -> int:
70
69
  return 1
71
70
 
72
71
  print("Installing git hooks with .d/ dispatcher pattern...")
73
- print(f" Source: pennyfarthing-dist/scripts/hooks/")
74
- print(f" Dest: .git/hooks/")
72
+ print(" Source: pennyfarthing-dist/scripts/hooks/")
73
+ print(" Dest: .git/hooks/")
75
74
  print()
76
75
 
77
76
  for source_file, dest_name in HOOKS:
@@ -291,7 +291,7 @@ async def main(brief: bool = False) -> int:
291
291
  Returns:
292
292
  0 if all repos clean, 1 if any have changes/unpushed
293
293
  """
294
- from pennyfarthing_scripts.git.repos import load_repos_config, get_repo_paths
294
+ from pennyfarthing_scripts.git.repos import get_repo_paths, load_repos_config
295
295
 
296
296
  repos_with_upstream: list[tuple[str, Path, str]] = []
297
297
  repo_paths = get_repo_paths()
@@ -199,7 +199,7 @@ def remove_worktree(name: str) -> int:
199
199
  shutil.rmtree(wt_path)
200
200
 
201
201
  # Prune worktree references
202
- for repo_name, cfg in repos.items():
202
+ for _repo_name, cfg in repos.items():
203
203
  full_path = (project_root / cfg.path).resolve()
204
204
  if full_path.exists():
205
205
  _git(["worktree", "prune"], full_path)
@@ -277,7 +277,7 @@ def show_worktree_status() -> int:
277
277
  if repo_wt.exists():
278
278
  branch, _ = _git(["branch", "--show-current"], repo_wt)
279
279
  status_out, _ = _git(["status", "--short"], repo_wt)
280
- count = len([l for l in status_out.split("\n") if l.strip()]) if status_out else 0
280
+ count = len([line for line in status_out.split("\n") if line.strip()]) if status_out else 0
281
281
  print(f" {repo_name} ({cfg.repo_type}): {branch} ({count} uncommitted)")
282
282
 
283
283
  # Check for session files referencing this worktree
@@ -30,7 +30,7 @@ import yaml
30
30
 
31
31
  # WheelHub port file - central coordination server for all communication
32
32
  # Per ADR-0004: "the hub where all communication converges"
33
- CYCLIST_PORT_FILE = ".wheelhub-port"
33
+ CYCLIST_PORT_FILE = ".bikerack-port"
34
34
 
35
35
  # Default port if file not found
36
36
  DEFAULT_CYCLIST_PORT = 7431
@@ -48,7 +48,7 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
48
48
  """Find the project root by looking for marker files.
49
49
 
50
50
  Searches for (in order):
51
- 1. .wheelhub-port (WheelHub is running)
51
+ 1. .bikerack-port (WheelHub is running)
52
52
  2. .pennyfarthing directory
53
53
  3. .claude directory
54
54
 
@@ -84,7 +84,7 @@ def read_port_file(file_name: str, project_root: Path | None = None) -> int | No
84
84
  """Read a port number from a Cyclist port file.
85
85
 
86
86
  Args:
87
- file_name: Name of the port file (e.g. .wheelhub-port)
87
+ file_name: Name of the port file (e.g. .bikerack-port)
88
88
  project_root: Project root directory (auto-detected if not provided)
89
89
 
90
90
  Returns:
@@ -140,6 +140,7 @@ class CyclistSettings:
140
140
  permission_mode: str = "manual" # plan, manual, accept
141
141
  relay_mode: bool = False
142
142
  bell_mode: bool = False
143
+ git_monitor: bool = False
143
144
  theme: str | None = None
144
145
 
145
146
 
@@ -206,6 +207,10 @@ def load_settings(project_root: Path | None = None) -> CyclistSettings:
206
207
  if "bell_mode" in workflow and isinstance(workflow["bell_mode"], bool):
207
208
  settings.bell_mode = workflow["bell_mode"]
208
209
 
210
+ # Handle git_monitor
211
+ if "git_monitor" in workflow and isinstance(workflow["git_monitor"], bool):
212
+ settings.git_monitor = workflow["git_monitor"]
213
+
209
214
  return settings
210
215
 
211
216