@pennyfarthing/core 11.1.1 → 11.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (487) hide show
  1. package/README.md +8 -8
  2. package/package.json +2 -1
  3. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor.js +381 -66
  5. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  6. package/packages/core/dist/cli/commands/init.js +4 -4
  7. package/packages/core/dist/cli/commands/init.js.map +1 -1
  8. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  9. package/packages/core/dist/cli/commands/update.js +4 -5
  10. package/packages/core/dist/cli/commands/update.js.map +1 -1
  11. package/packages/core/dist/cli/utils/constants.d.ts +3 -8
  12. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  13. package/packages/core/dist/cli/utils/constants.js +3 -4
  14. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  15. package/packages/core/dist/cli/utils/settings.d.ts +11 -0
  16. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  17. package/packages/core/dist/cli/utils/settings.js +65 -29
  18. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  19. package/packages/core/dist/cli/utils/symlinks.js +16 -16
  20. package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
  21. package/packages/core/dist/consultation/tandem-metrics.d.ts +91 -0
  22. package/packages/core/dist/consultation/tandem-metrics.d.ts.map +1 -0
  23. package/packages/core/dist/consultation/tandem-metrics.js +131 -0
  24. package/packages/core/dist/consultation/tandem-metrics.js.map +1 -0
  25. package/packages/core/dist/consultation/tandem-metrics.test.d.ts +18 -0
  26. package/packages/core/dist/consultation/tandem-metrics.test.d.ts.map +1 -0
  27. package/packages/core/dist/consultation/tandem-metrics.test.js +457 -0
  28. package/packages/core/dist/consultation/tandem-metrics.test.js.map +1 -0
  29. package/packages/core/dist/public/js/react/react.js +14 -14
  30. package/packages/core/dist/scripts/benchmark-integration.d.ts +182 -0
  31. package/packages/core/dist/scripts/benchmark-integration.d.ts.map +1 -0
  32. package/packages/core/dist/scripts/benchmark-integration.js +691 -0
  33. package/packages/core/dist/scripts/benchmark-integration.js.map +1 -0
  34. package/packages/core/dist/scripts/job-fair-aggregator.d.ts +150 -0
  35. package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +1 -0
  36. package/packages/core/dist/scripts/job-fair-aggregator.js +547 -0
  37. package/packages/core/dist/scripts/job-fair-aggregator.js.map +1 -0
  38. package/packages/core/dist/server/api/agent-load.js +1 -1
  39. package/packages/core/dist/server/api/agent-load.js.map +1 -1
  40. package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
  41. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  42. package/packages/core/dist/server/otlp-receiver.js +185 -24
  43. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  44. package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
  45. package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
  46. package/packages/core/dist/server/otlp-receiver.test.js +446 -0
  47. package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
  48. package/packages/core/dist/server/server.d.ts +0 -3
  49. package/packages/core/dist/server/server.d.ts.map +1 -1
  50. package/packages/core/dist/server/server.js +3 -37
  51. package/packages/core/dist/server/server.js.map +1 -1
  52. package/packages/core/dist/server/server.test.d.ts +1 -1
  53. package/packages/core/dist/server/server.test.js +12 -23
  54. package/packages/core/dist/server/server.test.js.map +1 -1
  55. package/packages/core/dist/shared/capabilities.d.ts +88 -0
  56. package/packages/core/dist/shared/capabilities.d.ts.map +1 -0
  57. package/packages/core/dist/shared/capabilities.js +133 -0
  58. package/packages/core/dist/shared/capabilities.js.map +1 -0
  59. package/packages/core/dist/shared/capabilities.test.d.ts +2 -0
  60. package/packages/core/dist/shared/capabilities.test.d.ts.map +1 -0
  61. package/packages/core/dist/shared/capabilities.test.js +217 -0
  62. package/packages/core/dist/shared/capabilities.test.js.map +1 -0
  63. package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
  64. package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
  65. package/packages/core/dist/shared/portrait-resolver.js +27 -0
  66. package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
  67. package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
  68. package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
  69. package/packages/core/dist/shared/spawn-prompt.d.ts +47 -0
  70. package/packages/core/dist/shared/spawn-prompt.d.ts.map +1 -0
  71. package/packages/core/dist/shared/spawn-prompt.js +82 -0
  72. package/packages/core/dist/shared/spawn-prompt.js.map +1 -0
  73. package/packages/core/dist/shared/spawn-prompt.test.d.ts +2 -0
  74. package/packages/core/dist/shared/spawn-prompt.test.d.ts.map +1 -0
  75. package/packages/core/dist/shared/spawn-prompt.test.js +251 -0
  76. package/packages/core/dist/shared/spawn-prompt.test.js.map +1 -0
  77. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
  78. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
  79. package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
  80. package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
  81. package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts +18 -0
  82. package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts.map +1 -0
  83. package/packages/core/dist/workflow/tandem-workflow-templates.test.js +434 -0
  84. package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -0
  85. package/packages/core/dist/workflow/workflow-schema.d.ts +32 -0
  86. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  87. package/packages/core/dist/workflow/workflow-schema.js +120 -0
  88. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  89. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  90. package/packages/core/dist/workflow/workflow-schema.test.js +570 -1
  91. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  92. package/pennyfarthing-dist/agents/dev.md +7 -11
  93. package/pennyfarthing-dist/agents/reviewer.md +9 -3
  94. package/pennyfarthing-dist/agents/sm-finish.md +18 -1
  95. package/pennyfarthing-dist/agents/sm-setup.md +1 -1
  96. package/pennyfarthing-dist/agents/sm.md +2 -2
  97. package/pennyfarthing-dist/agents/tea.md +1 -1
  98. package/pennyfarthing-dist/agents/testing-runner.md +2 -1
  99. package/pennyfarthing-dist/commands/pf-chore.md +2 -2
  100. package/pennyfarthing-dist/commands/pf-git.md +4 -2
  101. package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
  102. package/pennyfarthing-dist/gates/approval.md +63 -0
  103. package/pennyfarthing-dist/gates/confidence-sm.md +71 -0
  104. package/pennyfarthing-dist/gates/context-ok.md +56 -0
  105. package/pennyfarthing-dist/gates/evaluations/confidence-sm.md +54 -0
  106. package/pennyfarthing-dist/gates/quality-pass.md +67 -0
  107. package/pennyfarthing-dist/gates/tests-fail.md +84 -0
  108. package/pennyfarthing-dist/gates/tests-pass.md +79 -0
  109. package/pennyfarthing-dist/guides/agent-behavior.md +23 -19
  110. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
  111. package/pennyfarthing-dist/guides/bell-mode.md +1 -1
  112. package/pennyfarthing-dist/guides/bikerack.md +3 -3
  113. package/pennyfarthing-dist/guides/hooks.md +29 -29
  114. package/pennyfarthing-dist/guides/reflector.md +1 -1
  115. package/pennyfarthing-dist/guides/tandem-protocol.md +3 -3
  116. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  117. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  118. package/pennyfarthing-dist/scripts/README.md +1 -1
  119. package/pennyfarthing-dist/scripts/core/check-context.sh +3 -1
  120. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +5 -87
  121. package/pennyfarthing-dist/scripts/git/README.md +24 -14
  122. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
  123. package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
  124. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
  125. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
  126. package/pennyfarthing-dist/scripts/hooks/README.md +6 -6
  127. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  128. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +4 -183
  129. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +4 -95
  130. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +4 -65
  131. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +3 -31
  132. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
  133. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +27 -33
  134. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +4 -71
  135. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +3 -19
  136. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +4 -30
  137. package/pennyfarthing-dist/scripts/hooks/session-start.sh +3 -32
  138. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +4 -65
  139. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +4 -78
  140. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +4 -93
  141. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  142. package/pennyfarthing-dist/scripts/misc/statusline.sh +4 -301
  143. package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
  144. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
  145. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
  146. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
  147. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
  148. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
  149. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
  150. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
  151. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
  152. package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
  153. package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
  154. package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
  155. package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
  156. package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
  157. package/pennyfarthing-dist/templates/settings.local.json.template +19 -10
  158. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
  159. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
  160. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
  161. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  162. package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
  163. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
  164. package/pennyfarthing-dist/workflows/tdd.yaml +11 -2
  165. package/pennyfarthing_scripts/CLAUDE.md +45 -14
  166. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  167. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  172. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  183. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  184. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  185. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/bc/cli.py +3 -5
  190. package/pennyfarthing_scripts/bellmode_hook.py +12 -296
  191. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/bikerack/__pycache__/context_meter_footer.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/bikerack/__pycache__/events.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  211. package/pennyfarthing_scripts/bikerack/audit_log_panel.py +119 -0
  212. package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
  213. package/pennyfarthing_scripts/bikerack/base_panel.py +87 -2
  214. package/pennyfarthing_scripts/bikerack/changed_panel.py +125 -29
  215. package/pennyfarthing_scripts/bikerack/context_meter_footer.py +88 -0
  216. package/pennyfarthing_scripts/bikerack/debug_panel.py +32 -2
  217. package/pennyfarthing_scripts/bikerack/diffs_panel.py +104 -17
  218. package/pennyfarthing_scripts/bikerack/events.py +28 -0
  219. package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
  220. package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
  221. package/pennyfarthing_scripts/bikerack/portrait_resolver.py +139 -0
  222. package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
  223. package/pennyfarthing_scripts/bikerack/sprint_panel.py +395 -32
  224. package/pennyfarthing_scripts/bikerack/story_detail_data.py +244 -0
  225. package/pennyfarthing_scripts/bikerack/story_detail_screen.py +176 -0
  226. package/pennyfarthing_scripts/bikerack/tui.py +575 -37
  227. package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
  228. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/cli.py +42 -65
  233. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  238. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  239. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  240. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  241. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  242. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  243. package/pennyfarthing_scripts/common/pr_config.py +38 -0
  244. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/consultation/__init__.py +1 -0
  251. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/consultation/__pycache__/dialogue_manager.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/consultation/cli.py +149 -0
  255. package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
  256. package/pennyfarthing_scripts/context.py +3 -3
  257. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  261. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  262. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  263. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  264. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/git/__init__.py +12 -1
  272. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/git/create_branches.py +3 -4
  276. package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
  277. package/pennyfarthing_scripts/git/repos.py +196 -0
  278. package/pennyfarthing_scripts/git/status_all.py +27 -11
  279. package/pennyfarthing_scripts/git/worktree.py +302 -0
  280. package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/git_group/cli.py +143 -40
  283. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  289. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/handoff/cli.py +33 -1
  291. package/pennyfarthing_scripts/handoff/complete_phase.py +40 -0
  292. package/pennyfarthing_scripts/handoff/marker.py +15 -15
  293. package/pennyfarthing_scripts/handoff/phase_check.py +96 -0
  294. package/pennyfarthing_scripts/handoff/resolve_gate.py +18 -15
  295. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  296. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  297. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  298. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/hooks/__init__.py +437 -0
  302. package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  304. package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  305. package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  306. package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  307. package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  309. package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  315. package/pennyfarthing_scripts/hooks/bell_mode.py +215 -0
  316. package/pennyfarthing_scripts/hooks/cli.py +96 -0
  317. package/pennyfarthing_scripts/hooks/context_breaker.py +104 -0
  318. package/pennyfarthing_scripts/hooks/context_warning.py +66 -0
  319. package/pennyfarthing_scripts/hooks/cyclist_pretooluse.py +129 -0
  320. package/pennyfarthing_scripts/hooks/pre_edit_check.py +78 -0
  321. package/pennyfarthing_scripts/hooks/reflector_check.py +271 -0
  322. package/pennyfarthing_scripts/hooks/schema_validation.py +203 -0
  323. package/pennyfarthing_scripts/hooks/session_start.py +296 -0
  324. package/pennyfarthing_scripts/hooks/session_stop.py +111 -0
  325. package/pennyfarthing_scripts/hooks/sprint_yaml_validation.py +97 -0
  326. package/pennyfarthing_scripts/hooks/statusline.py +420 -0
  327. package/pennyfarthing_scripts/hooks.py +27 -446
  328. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  329. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  330. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  331. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  332. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  333. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  334. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  335. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  336. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  337. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  338. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  339. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  340. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  344. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  345. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  346. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  347. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  348. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  349. package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  350. package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
  351. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  352. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  353. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  354. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  355. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  356. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  357. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  358. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  359. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  360. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  361. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  362. package/pennyfarthing_scripts/pretooluse_hook.py +3 -185
  363. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  364. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  365. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  366. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  367. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  368. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  369. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  370. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  371. package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
  372. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  373. package/pennyfarthing_scripts/prime/heatmap.py +655 -0
  374. package/pennyfarthing_scripts/prime/workflow.py +2 -1
  375. package/pennyfarthing_scripts/schema_validation_hook.py +3 -298
  376. package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
  377. package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
  378. package/pennyfarthing_scripts/session_start_hook.py +4 -186
  379. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  380. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  381. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  382. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  383. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  384. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  385. package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  386. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  387. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  388. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  389. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  390. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  391. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  392. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  393. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  394. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  395. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  396. package/pennyfarthing_scripts/sprint/loader.py +15 -1
  397. package/pennyfarthing_scripts/sprint/story_update.py +19 -0
  398. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  399. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  400. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  401. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  402. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  403. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  404. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  405. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  406. package/pennyfarthing_scripts/tests/__pycache__/test_108_1_gate_migration.cpython-314-pytest-9.0.2.pyc +0 -0
  407. package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
  408. package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
  409. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  410. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  411. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  412. package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
  413. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  414. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  415. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_evaluation.cpython-314-pytest-9.0.2.pyc +0 -0
  416. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_gate.cpython-314-pytest-9.0.2.pyc +0 -0
  417. package/pennyfarthing_scripts/tests/__pycache__/test_dialogue_manager.cpython-314-pytest-9.0.2.pyc +0 -0
  418. package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
  419. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  420. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  421. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  422. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  423. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  424. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  425. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  426. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  427. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  428. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
  429. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  430. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  431. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  432. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  433. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  434. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  435. package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
  436. package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
  437. package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
  438. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  439. package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
  440. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  441. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  442. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  443. package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
  444. package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
  445. package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
  446. package/pennyfarthing_scripts/tests/test_sprint_panel.py +344 -265
  447. package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
  448. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  449. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  450. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  451. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  452. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  453. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  454. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  455. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  456. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  457. package/pennyfarthing_scripts/validate/adapters/__pycache__/tandem_awareness.cpython-314.pyc +0 -0
  458. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  459. package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
  460. package/pennyfarthing_scripts/validate/adapters/workflow.py +19 -0
  461. package/pennyfarthing_scripts/validate/cli.py +17 -5
  462. package/pennyfarthing_scripts/welcome_hook.py +3 -149
  463. package/pennyfarthing_scripts/workflow/__init__.py +40 -0
  464. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  465. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  466. package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  467. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  468. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
  469. package/pennyfarthing_scripts/workflow/cli.py +1100 -0
  470. package/pennyfarthing_scripts/workflow/helpers.py +241 -0
  471. package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
  472. package/pennyfarthing_scripts/workflow/state.py +112 -0
  473. package/pennyfarthing_scripts/workflow/team_lifecycle.py +257 -0
  474. package/packages/core/dist/scripts/theme-detail.test.d.ts +0 -10
  475. package/packages/core/dist/scripts/theme-detail.test.js +0 -199
  476. package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
  477. package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
  478. package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
  479. package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
  480. package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -167
  481. package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
  482. package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
  483. package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
  484. package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
  485. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  486. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  487. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -9,6 +9,8 @@ from __future__ import annotations
9
9
 
10
10
  from typing import Any
11
11
 
12
+ from rich.text import Text
13
+ from textual.message import Message
12
14
  from textual.widgets import Static
13
15
 
14
16
  # Nerd Font icon registry: panel_name → (nerd_font_icon, ascii_fallback)
@@ -25,6 +27,7 @@ PANEL_ICONS: dict[str, tuple[str, str]] = {
25
27
  "debug": ("\uf188", "d"), # nf-fa-bug
26
28
  "settings": ("\uf013", "S"), # nf-fa-gear
27
29
  "tty": ("\uf120", ">"), # nf-fa-terminal
30
+ "progress": ("\uf200", "P"), # nf-fa-pie_chart
28
31
  }
29
32
 
30
33
 
@@ -44,6 +47,74 @@ def get_panel_icon(panel_name: str, use_nerd_font: bool = True) -> str:
44
47
  return entry[0] if use_nerd_font else entry[1]
45
48
 
46
49
 
50
+ def render_progress_bar(
51
+ percent: int | float,
52
+ width: int = 20,
53
+ warn_high: bool = False,
54
+ fill_style: str | None = None,
55
+ ) -> Text:
56
+ """Render a Unicode progress bar with color based on percentage.
57
+
58
+ Args:
59
+ percent: Value 0-100.
60
+ width: Number of bar characters (default 20).
61
+ warn_high: If True, use red at high values (for resource usage).
62
+ If False (default), use blue at 100% (for completion).
63
+ fill_style: Override the computed fill color (e.g. ``"dim green"``).
64
+
65
+ Returns:
66
+ Rich Text like ``[████████░░░░░░░░░░░░] 22%``
67
+ """
68
+ percent = max(0, min(100, int(percent)))
69
+ filled = round(width * percent / 100)
70
+ empty = width - filled
71
+
72
+ if fill_style is not None:
73
+ style = fill_style
74
+ elif warn_high:
75
+ if percent < 50:
76
+ style = "green"
77
+ elif percent <= 80:
78
+ style = "yellow"
79
+ else:
80
+ style = "red"
81
+ else:
82
+ style = "blue"
83
+
84
+ bar = Text()
85
+ bar.append("[")
86
+ bar.append("█" * filled, style=style)
87
+ bar.append("░" * empty, style="dim")
88
+ bar.append(f"] {percent}%")
89
+ return bar
90
+
91
+
92
+ def format_duration(seconds: int | float) -> str:
93
+ """Format seconds into human-friendly duration string.
94
+
95
+ Returns:
96
+ ``47s``, ``2m 14s``, ``1h 5m``.
97
+ """
98
+ seconds = max(0, int(seconds))
99
+ if seconds < 60:
100
+ return f"{seconds}s"
101
+ minutes, secs = divmod(seconds, 60)
102
+ if minutes < 60:
103
+ return f"{minutes}m {secs}s"
104
+ hours, mins = divmod(minutes, 60)
105
+ return f"{hours}h {mins}m"
106
+
107
+
108
+ def humanize_theme(slug: str) -> str:
109
+ """Convert a theme slug to a display name.
110
+
111
+ ``princess-bride`` → ``Princess Bride``
112
+ """
113
+ if not slug:
114
+ return ""
115
+ return slug.replace("-", " ").replace("_", " ").title()
116
+
117
+
47
118
  class BasePanel(Static):
48
119
  """Base class for BikeRack TUI panels.
49
120
 
@@ -51,6 +122,13 @@ class BasePanel(Static):
51
122
  ``render_panel(payload)`` to return a Rich renderable.
52
123
  """
53
124
 
125
+ class DataReceived(Message, bubble=False):
126
+ """WebSocket data received — routed through Textual message system."""
127
+
128
+ def __init__(self, content: Any) -> None:
129
+ super().__init__()
130
+ self.content = content
131
+
54
132
  #: WebSocket channel this panel subscribes to (override in subclass)
55
133
  channel: str = ""
56
134
 
@@ -76,10 +154,17 @@ class BasePanel(Static):
76
154
  """Mark panel as unmounted — messages ignored after this."""
77
155
  self._mounted = False
78
156
 
157
+ def on_base_panel_data_received(self, event: DataReceived) -> None:
158
+ """Process DataReceived in Textual message context — triggers repaint."""
159
+ self.update(event.content)
160
+
79
161
  def handle_message(self, message: dict[str, Any] | None) -> None:
80
162
  """Handle incoming WebSocket message.
81
163
 
82
- Stores payload, calls render_panel, updates widget display.
164
+ Stores payload, calls render_panel, posts DataReceived message.
165
+ Uses post_message to route through Textual's message system,
166
+ ensuring proper repaint cycle (call_from_thread / direct update
167
+ from async WS tasks does not reliably trigger redraws).
83
168
  No-op after unmount or if message is None.
84
169
  """
85
170
  if not self._mounted or message is None:
@@ -87,7 +172,7 @@ class BasePanel(Static):
87
172
  self._last_payload = message
88
173
  rendered = self.render_panel(message)
89
174
  try:
90
- self.update(rendered)
175
+ self.post_message(self.DataReceived(rendered))
91
176
  except Exception:
92
177
  pass
93
178
 
@@ -2,14 +2,16 @@
2
2
 
3
3
  Story 103-14: Subscribes to /ws/git, extracts dirtyFiles from all repos,
4
4
  renders Rich table with file path, change type icon, and status.
5
+
6
+ Story 110-1: Selectable file list with arrow navigation and Enter to navigate.
5
7
  """
6
8
 
7
9
  from __future__ import annotations
8
10
 
9
11
  from typing import Any
10
12
 
11
- from rich.table import Table
12
13
  from rich.text import Text
14
+ from textual.binding import Binding
13
15
 
14
16
  from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
15
17
 
@@ -56,50 +58,144 @@ class ChangedPanel(BasePanel):
56
58
 
57
59
  Subscribes to the ``git`` WebSocket channel and renders
58
60
  dirty files from all repos as a Rich table with file path,
59
- change type icon, and status.
61
+ change type icon, and status. Supports arrow-key selection
62
+ and Enter to navigate to diffs.
60
63
  """
61
64
 
62
65
  channel: str = "git"
63
66
  panel_name: str = "Changed"
64
67
  icon: str = PANEL_ICONS["changed"][0]
68
+ can_focus = True
69
+
70
+ BINDINGS = [
71
+ Binding("up", "select_prev_key", "Up"),
72
+ Binding("down", "select_next_key", "Down"),
73
+ Binding("enter", "select_file", "Select file"),
74
+ ]
75
+
76
+ def __init__(self, client=None, **kwargs):
77
+ super().__init__(client=client, **kwargs)
78
+ self._selected_index: int = 0
79
+ self._file_paths: list[str] = []
80
+
81
+ def handle_message(self, message: dict[str, Any] | None) -> None:
82
+ """Handle incoming message — build file path index then render."""
83
+ if message is not None:
84
+ self._build_file_paths(message)
85
+ super().handle_message(message)
86
+
87
+ def _build_file_paths(self, payload: dict[str, Any]) -> None:
88
+ """Extract flat list of file paths from repos payload."""
89
+ paths: list[str] = []
90
+ repos = payload.get("repos", [])
91
+ if isinstance(repos, list):
92
+ for repo in repos:
93
+ if not isinstance(repo, dict):
94
+ continue
95
+ dirty_files = repo.get("dirtyFiles", [])
96
+ if not isinstance(dirty_files, list):
97
+ continue
98
+ for f in dirty_files:
99
+ if isinstance(f, dict):
100
+ path = f.get("path", "")
101
+ if path:
102
+ paths.append(path)
103
+ self._file_paths = paths
104
+ if self._selected_index >= len(paths):
105
+ self._selected_index = max(0, len(paths) - 1)
106
+
107
+ def select_next(self) -> None:
108
+ """Move selection to the next file."""
109
+ if self._file_paths and self._selected_index < len(self._file_paths) - 1:
110
+ self._selected_index += 1
111
+ self._rerender()
112
+
113
+ def select_prev(self) -> None:
114
+ """Move selection to the previous file."""
115
+ if self._selected_index > 0:
116
+ self._selected_index -= 1
117
+ self._rerender()
118
+
119
+ def action_select_next_key(self) -> None:
120
+ """Binding action: move selection down."""
121
+ self.select_next()
122
+
123
+ def action_select_prev_key(self) -> None:
124
+ """Binding action: move selection up."""
125
+ self.select_prev()
126
+
127
+ def _rerender(self) -> None:
128
+ """Re-render panel with current payload after selection change."""
129
+ if self._last_payload:
130
+ try:
131
+ self.update(self.render_panel(self._last_payload))
132
+ except Exception:
133
+ pass
134
+
135
+ def get_selected_path(self) -> str | None:
136
+ """Return the currently selected file path, or None if empty."""
137
+ if not self._file_paths:
138
+ return None
139
+ if self._selected_index >= len(self._file_paths):
140
+ return None
141
+ return self._file_paths[self._selected_index]
142
+
143
+ def action_select_file(self) -> None:
144
+ """Post NavigateToFile event for the selected file."""
145
+ path = self.get_selected_path()
146
+ if path is not None:
147
+ from pennyfarthing_scripts.bikerack.events import NavigateToFile
148
+
149
+ self.post_message(NavigateToFile(path=path))
65
150
 
66
151
  def render_panel(self, payload: dict[str, Any]) -> Any:
67
- """Render changed file data from WebSocket payload."""
152
+ """Render changed files grouped by repository with selection highlight."""
68
153
  repos = payload.get("repos", [])
69
154
  if not isinstance(repos, list):
70
155
  return Text("No changed files", style="dim italic")
71
156
 
72
- files: list[tuple[str, dict[str, Any]]] = []
157
+ # Group files by repo
158
+ repo_files: dict[str, list[dict[str, Any]]] = {}
73
159
  for repo in repos:
74
160
  if not isinstance(repo, dict):
75
161
  continue
76
- repo_name = repo.get("name", "")
162
+ repo_name = repo.get("name", "unknown")
77
163
  dirty_files = repo.get("dirtyFiles", [])
78
- if not isinstance(dirty_files, list):
164
+ if not isinstance(dirty_files, list) or not dirty_files:
79
165
  continue
80
- for f in dirty_files:
81
- if not isinstance(f, dict):
82
- continue
83
- files.append((repo_name, f))
166
+ repo_files[repo_name] = [f for f in dirty_files if isinstance(f, dict)]
84
167
 
85
- if not files:
168
+ if not repo_files:
86
169
  return Text("No changed files", style="dim italic")
87
170
 
88
- table = Table()
89
- table.add_column("", width=2)
90
- table.add_column("File", style="cyan")
91
- table.add_column("Status")
92
- table.add_column("Repo", style="dim")
93
-
94
- for repo_name, f in files:
95
- status_code = f.get("status", " ")
96
- path = f.get("path", "")
97
- icon, label, style = _parse_status(status_code)
98
- table.add_row(
99
- Text(icon, style=f"bold {style}"),
100
- path,
101
- Text(label, style=style),
102
- repo_name,
103
- )
104
-
105
- return table
171
+ from rich.console import Group as RichGroup
172
+
173
+ parts: list[Any] = []
174
+ flat_idx = 0
175
+ for repo_name, files in repo_files.items():
176
+ count = len(files)
177
+ label = "file" if count == 1 else "files"
178
+ header = Text()
179
+ header.append(repo_name, style="bold cyan")
180
+ header.append(f" ({count} {label})", style="dim")
181
+ parts.append(header)
182
+
183
+ for f in files:
184
+ status_code = f.get("status", " ")
185
+ path = f.get("path", "")
186
+ icon, label_text, style = _parse_status(status_code)
187
+ is_selected = flat_idx == self._selected_index
188
+ line = Text()
189
+ if is_selected:
190
+ line.append("› ", style="bold reverse")
191
+ else:
192
+ line.append(" ")
193
+ line.append(icon, style=f"bold {style}")
194
+ line.append(f" {path}", style="bold cyan reverse" if is_selected else "cyan")
195
+ line.append(f" {label_text}", style=style)
196
+ parts.append(line)
197
+ flat_idx += 1
198
+
199
+ parts.append(Text("")) # spacer between repos
200
+
201
+ return RichGroup(*parts)
@@ -0,0 +1,88 @@
1
+ """ContextMeterFooter — Persistent context usage footer bar for BikeRack TUI.
2
+
3
+ Story 110-5: Context meter footer bar. Displays context window usage
4
+ percentage with color-coded tier thresholds, always visible at the
5
+ bottom of the layout.
6
+
7
+ Subscribes to /ws/context WebSocket channel.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from rich.text import Text
15
+ from textual.message import Message
16
+ from textual.widgets import Static
17
+
18
+ from pennyfarthing_scripts.bikerack.base_panel import render_progress_bar
19
+
20
+
21
+ class ContextMeterFooter(Static):
22
+ """Persistent footer bar showing context window usage.
23
+
24
+ Not a Footer subclass — this is a Static widget mounted between
25
+ #main-content and BindingFooter in the app layout.
26
+ """
27
+
28
+ class MeterUpdate(Message, bubble=False):
29
+ """Context meter data received — routed through Textual message system."""
30
+
31
+ def __init__(self, content: Any) -> None:
32
+ super().__init__()
33
+ self.content = content
34
+
35
+ #: WebSocket channel this footer subscribes to
36
+ channel: str = "context"
37
+
38
+ def __init__(self, client: Any = None, **kwargs: Any) -> None:
39
+ super().__init__(**kwargs)
40
+ self._client = client
41
+ self._context_data: dict[str, Any] | None = None
42
+ self._mounted = False
43
+
44
+ def on_mount(self) -> None:
45
+ """Subscribe to context channel on mount."""
46
+ self._mounted = True
47
+ if self._client is not None:
48
+ self._client.subscribe("context", self.handle_context_message)
49
+
50
+ def on_unmount(self) -> None:
51
+ """Mark as unmounted so further messages are ignored."""
52
+ self._mounted = False
53
+
54
+ def on_context_meter_footer_meter_update(self, event: MeterUpdate) -> None:
55
+ """Process MeterUpdate in Textual message context — triggers repaint."""
56
+ self.update(event.content)
57
+
58
+ def handle_context_message(self, msg: dict[str, Any] | None) -> None:
59
+ """Process incoming /ws/context message."""
60
+ if not self._mounted or msg is None:
61
+ return
62
+ ctx = msg.get("context")
63
+ if ctx is None:
64
+ return
65
+ self._context_data = ctx
66
+ try:
67
+ rendered = self.render_meter(ctx)
68
+ self.post_message(self.MeterUpdate(rendered))
69
+ except Exception:
70
+ pass
71
+
72
+ def render_meter(self, ctx: dict[str, Any]) -> Text:
73
+ """Render a compact context usage bar with percentage and tier badge."""
74
+ percent = ctx.get("percent", 0)
75
+ tier = ctx.get("tier", "")
76
+
77
+ bar = render_progress_bar(percent, warn_high=True)
78
+
79
+ if tier:
80
+ if percent < 50:
81
+ tier_style = "green"
82
+ elif percent <= 80:
83
+ tier_style = "yellow"
84
+ else:
85
+ tier_style = "red"
86
+ bar.append(f" {tier}", style=f"bold {tier_style}")
87
+
88
+ return bar
@@ -7,13 +7,14 @@ token consumption stats (input, output, cache, cost).
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from collections import deque
10
11
  from typing import Any
11
12
 
12
13
  from rich.console import Group
13
14
  from rich.table import Table
14
15
  from rich.text import Text
15
16
 
16
- from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
17
+ from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel, render_progress_bar
17
18
 
18
19
  # Tier → Rich style mapping
19
20
  _TIER_STYLES: dict[str, str] = {
@@ -68,6 +69,7 @@ class DebugPanel(BasePanel):
68
69
  super().__init__(client=client, **kwargs)
69
70
  self._context_data: dict[str, Any] | None = None
70
71
  self._token_stats: dict[str, Any] | None = None
72
+ self._sparkline_history: deque[int] = deque(maxlen=20)
71
73
 
72
74
  def on_mount(self) -> None:
73
75
  """Subscribe to both context and token-stats channels."""
@@ -83,6 +85,9 @@ class DebugPanel(BasePanel):
83
85
  ctx = message.get("context")
84
86
  if isinstance(ctx, dict):
85
87
  self._context_data = ctx
88
+ pct = _safe_int(ctx.get("percent"))
89
+ if pct is not None:
90
+ self._sparkline_history.append(pct)
86
91
  else:
87
92
  self._context_data = {}
88
93
  self._rerender()
@@ -98,7 +103,7 @@ class DebugPanel(BasePanel):
98
103
  """Re-render with the latest data from both channels."""
99
104
  rendered = self.render_panel(self._context_data or {})
100
105
  try:
101
- self.update(rendered)
106
+ self._thread_safe_update(rendered)
102
107
  except Exception:
103
108
  pass
104
109
 
@@ -110,6 +115,8 @@ class DebugPanel(BasePanel):
110
115
  ctx = self._context_data
111
116
  if ctx:
112
117
  parts.append(_render_context(ctx))
118
+ if len(self._sparkline_history) >= 2:
119
+ parts.append(_render_sparkline(self._sparkline_history))
113
120
  elif not self._token_stats:
114
121
  return Text("No context data", style="dim italic")
115
122
 
@@ -157,6 +164,10 @@ def _render_context(ctx: dict[str, Any]) -> Any:
157
164
  usage_text.append(f" ({percent}%)")
158
165
  parts.append(usage_text)
159
166
 
167
+ # Context usage progress bar
168
+ if percent is not None:
169
+ parts.append(render_progress_bar(percent, warn_high=True))
170
+
160
171
  # Breakdown: baseline / conversation / available
161
172
  if baseline is not None:
162
173
  breakdown = Table(show_header=False, show_edge=False, pad_edge=False, box=None)
@@ -176,6 +187,25 @@ def _render_context(ctx: dict[str, Any]) -> Any:
176
187
  return Group(*parts)
177
188
 
178
189
 
190
+ _SPARKLINE_CHARS = "▁▂▃▄▅▆▇█"
191
+
192
+
193
+ def _render_sparkline(history: deque[int]) -> Text:
194
+ """Render a Unicode sparkline from context usage history."""
195
+ text = Text()
196
+ text.append("Context trend: ", style="dim")
197
+ for pct in history:
198
+ level = min(7, max(0, int(pct / 100 * 7.99)))
199
+ if pct < 50:
200
+ style = "green"
201
+ elif pct <= 80:
202
+ style = "yellow"
203
+ else:
204
+ style = "red"
205
+ text.append(_SPARKLINE_CHARS[level], style=style)
206
+ return text
207
+
208
+
179
209
  def _render_token_stats(stats: dict[str, Any]) -> Any:
180
210
  """Render token stats section."""
181
211
  table = Table(show_header=False, show_edge=False, pad_edge=False, box=None)
@@ -17,6 +17,7 @@ from typing import Any
17
17
  from rich.console import Group
18
18
  from rich.syntax import Syntax
19
19
  from rich.text import Text
20
+ from textual.binding import Binding
20
21
 
21
22
  from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
22
23
 
@@ -46,12 +47,20 @@ class DiffsPanel(BasePanel):
46
47
  channel: str = "diffs"
47
48
  panel_name: str = "Diffs"
48
49
  icon: str = PANEL_ICONS["diffs"][0]
50
+ can_focus = True
51
+
52
+ BINDINGS = [
53
+ Binding("n", "next_file_key", "Next file"),
54
+ Binding("p", "prev_file_key", "Prev file"),
55
+ ]
49
56
 
50
57
  def __init__(self, client=None, **kwargs):
51
58
  super().__init__(client=client, **kwargs)
52
59
  self._current_page: int = 0
53
60
  self._max_page: int = 0
54
61
  self._temp_files: list[str] = []
62
+ self._current_file_index: int = 0
63
+ self._total_files: int = 0
55
64
 
56
65
  def next_page(self) -> None:
57
66
  """Advance to the next page of truncated diff content."""
@@ -63,11 +72,57 @@ class DiffsPanel(BasePanel):
63
72
  if self._current_page > 0:
64
73
  self._current_page -= 1
65
74
 
75
+ def next_file(self) -> None:
76
+ """Advance to the next file."""
77
+ if self._current_file_index < self._total_files - 1:
78
+ self._current_file_index += 1
79
+ if self._last_payload:
80
+ rendered = self.render_panel(self._last_payload)
81
+ try:
82
+ self.update(rendered)
83
+ except Exception:
84
+ pass
85
+
86
+ def prev_file(self) -> None:
87
+ """Go back to the previous file."""
88
+ if self._current_file_index > 0:
89
+ self._current_file_index -= 1
90
+ if self._last_payload:
91
+ rendered = self.render_panel(self._last_payload)
92
+ try:
93
+ self.update(rendered)
94
+ except Exception:
95
+ pass
96
+
97
+ def navigate_to_file(self, path: str) -> None:
98
+ """Jump to a specific file by path. No-op if not found."""
99
+ if self._last_payload is None:
100
+ return
101
+ diffs = self._last_payload.get("diffs", [])
102
+ for i, d in enumerate(diffs):
103
+ if d.get("path") == path:
104
+ self._current_file_index = i
105
+ rendered = self.render_panel(self._last_payload)
106
+ try:
107
+ self.update(rendered)
108
+ except Exception:
109
+ pass
110
+ return
111
+
112
+ def action_next_file_key(self) -> None:
113
+ """Binding action: advance to next file."""
114
+ self.next_file()
115
+
116
+ def action_prev_file_key(self) -> None:
117
+ """Binding action: go to previous file."""
118
+ self.prev_file()
119
+
66
120
  def handle_message(self, message: dict[str, Any] | None) -> None:
67
121
  """Handle incoming WebSocket message with pagination reset and temp management."""
68
122
  if not self._mounted or message is None:
69
123
  return
70
124
  self._current_page = 0
125
+ self._current_file_index = 0
71
126
  self._cleanup_temp_files()
72
127
  self._store_large_diffs(message)
73
128
  super().handle_message(message)
@@ -78,31 +133,63 @@ class DiffsPanel(BasePanel):
78
133
  super().on_unmount()
79
134
 
80
135
  def render_panel(self, payload: dict[str, Any]) -> Any:
81
- """Render diff data from WebSocket payload with truncation/pagination."""
136
+ """Render diff data showing one file at a time with file selector header."""
82
137
  diffs = payload.get("diffs", [])
83
138
  if not diffs:
84
139
  return Text("No diffs yet", style="dim italic")
85
140
 
141
+ self._total_files = len(diffs)
142
+
143
+ # Clamp file index
144
+ if self._current_file_index >= len(diffs):
145
+ self._current_file_index = len(diffs) - 1
146
+
86
147
  parts: list[Any] = []
87
- max_total = 0
88
- for diff_entry in diffs:
89
- # Skip syntax highlighting for very large diffs (>2000 lines) for performance
90
- raw_diff = diff_entry.get("diff", "")
91
- skip_highlight = raw_diff.count("\n") > HIGHLIGHT_THRESHOLD
92
148
 
93
- file_parts, total_lines = _render_file_diff(
94
- diff_entry,
95
- page=self._current_page,
96
- page_size=DEFAULT_LINE_LIMIT,
97
- skip_highlight=skip_highlight,
98
- )
99
- parts.extend(file_parts)
100
- parts.append(Text("")) # separator between files
101
- max_total = max(max_total, total_lines)
149
+ # File selector header
150
+ selector = Text()
151
+ selector.append("Files: ", style="dim")
152
+ for i, d in enumerate(diffs):
153
+ path = d.get("path", "unknown")
154
+ additions = d.get("additions")
155
+ deletions = d.get("deletions")
156
+ stats = ""
157
+ if additions is not None and deletions is not None:
158
+ stats = f" +{additions} -{deletions}"
159
+
160
+ if i == self._current_file_index:
161
+ selector.append(f"[{i+1}/{len(diffs)}] ", style="bold")
162
+ selector.append(path, style="bold cyan")
163
+ if stats:
164
+ selector.append(stats, style="bold dim")
165
+ else:
166
+ selector.append(path, style="dim")
167
+ if stats:
168
+ selector.append(stats, style="dim")
169
+
170
+ if i < len(diffs) - 1:
171
+ selector.append(" | ", style="dim")
172
+
173
+ parts.append(selector)
174
+ parts.append(Text("n:next p:prev", style="dim"))
175
+ parts.append(Text(""))
176
+
177
+ # Render only current file's diff
178
+ diff_entry = diffs[self._current_file_index]
179
+ raw_diff = diff_entry.get("diff", "")
180
+ skip_highlight = raw_diff.count("\n") > HIGHLIGHT_THRESHOLD
181
+
182
+ file_parts, total_lines = _render_file_diff(
183
+ diff_entry,
184
+ page=self._current_page,
185
+ page_size=DEFAULT_LINE_LIMIT,
186
+ skip_highlight=skip_highlight,
187
+ )
188
+ parts.extend(file_parts)
102
189
 
103
190
  # Track max page for pagination bounds
104
- if max_total > DEFAULT_LINE_LIMIT:
105
- self._max_page = -(-max_total // DEFAULT_LINE_LIMIT) - 1
191
+ if total_lines > DEFAULT_LINE_LIMIT:
192
+ self._max_page = -(-total_lines // DEFAULT_LINE_LIMIT) - 1
106
193
  else:
107
194
  self._max_page = 0
108
195