@pennyfarthing/core 11.0.0-alpha.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +84 -26
  2. package/package.json +14 -16
  3. package/packages/core/dist/cli/cyclist-migration.test.js +2 -1
  4. package/packages/core/dist/cli/cyclist-migration.test.js.map +1 -1
  5. package/packages/core/dist/cli/ocean-profiles.test.js +5 -4
  6. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  7. package/packages/core/dist/cli/theme-maker.test.js +5 -4
  8. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  9. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts +20 -0
  10. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts.map +1 -0
  11. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js +278 -0
  12. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js.map +1 -0
  13. package/packages/core/dist/cli/utils/constants.d.ts +7 -1
  14. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  15. package/packages/core/dist/cli/utils/constants.js +2 -0
  16. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  17. package/packages/core/dist/cli/utils/constants.test.d.ts +10 -0
  18. package/packages/core/dist/cli/utils/constants.test.d.ts.map +1 -0
  19. package/packages/core/dist/cli/utils/constants.test.js +38 -0
  20. package/packages/core/dist/cli/utils/constants.test.js.map +1 -0
  21. package/packages/core/dist/consultation/consultation-protocol.d.ts +139 -0
  22. package/packages/core/dist/consultation/consultation-protocol.d.ts.map +1 -0
  23. package/packages/core/dist/consultation/consultation-protocol.js +178 -0
  24. package/packages/core/dist/consultation/consultation-protocol.js.map +1 -0
  25. package/packages/core/dist/consultation/consultation-protocol.test.d.ts +20 -0
  26. package/packages/core/dist/consultation/consultation-protocol.test.d.ts.map +1 -0
  27. package/packages/core/dist/consultation/consultation-protocol.test.js +474 -0
  28. package/packages/core/dist/consultation/consultation-protocol.test.js.map +1 -0
  29. package/packages/core/dist/public/js/react/react.js +30 -30
  30. package/packages/core/dist/scripts/generate-report.test.js +2 -2
  31. package/packages/core/dist/scripts/generate-spider-report.test.js +2 -2
  32. package/packages/core/dist/scripts/generate-spider.test.js +2 -1
  33. package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
  34. package/packages/core/dist/server/api/file-browser.d.ts.map +1 -1
  35. package/packages/core/dist/server/api/file-browser.js +19 -1
  36. package/packages/core/dist/server/api/file-browser.js.map +1 -1
  37. package/packages/core/dist/server/api/git-fetch-cooldown.test.d.ts +10 -0
  38. package/packages/core/dist/server/api/git-fetch-cooldown.test.d.ts.map +1 -0
  39. package/packages/core/dist/server/api/git-fetch-cooldown.test.js +30 -0
  40. package/packages/core/dist/server/api/git-fetch-cooldown.test.js.map +1 -0
  41. package/packages/core/dist/server/api/git.d.ts +8 -0
  42. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  43. package/packages/core/dist/server/api/git.js +37 -10
  44. package/packages/core/dist/server/api/git.js.map +1 -1
  45. package/packages/core/dist/server/api/health-score.d.ts.map +1 -1
  46. package/packages/core/dist/server/api/health-score.js +25 -1
  47. package/packages/core/dist/server/api/health-score.js.map +1 -1
  48. package/packages/core/dist/server/api/index.d.ts +1 -1
  49. package/packages/core/dist/server/api/index.d.ts.map +1 -1
  50. package/packages/core/dist/server/api/index.js +1 -1
  51. package/packages/core/dist/server/api/index.js.map +1 -1
  52. package/packages/core/dist/server/api/settings.d.ts.map +1 -1
  53. package/packages/core/dist/server/api/settings.js +73 -2
  54. package/packages/core/dist/server/api/settings.js.map +1 -1
  55. package/packages/core/dist/server/api/theme-agents.d.ts.map +1 -1
  56. package/packages/core/dist/server/api/theme-agents.js +61 -0
  57. package/packages/core/dist/server/api/theme-agents.js.map +1 -1
  58. package/packages/core/dist/server/otlp-receiver.d.ts +35 -13
  59. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  60. package/packages/core/dist/server/otlp-receiver.js +76 -16
  61. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  62. package/packages/core/dist/server/paths.d.ts.map +1 -1
  63. package/packages/core/dist/server/paths.js +11 -1
  64. package/packages/core/dist/server/paths.js.map +1 -1
  65. package/packages/core/dist/server/server.d.ts +3 -1
  66. package/packages/core/dist/server/server.d.ts.map +1 -1
  67. package/packages/core/dist/server/server.js +23 -16
  68. package/packages/core/dist/server/server.js.map +1 -1
  69. package/packages/core/dist/server/server.test.js.map +1 -1
  70. package/packages/core/dist/workflow/gate-file-validation.d.ts +49 -0
  71. package/packages/core/dist/workflow/gate-file-validation.d.ts.map +1 -0
  72. package/packages/core/dist/workflow/gate-file-validation.js +157 -0
  73. package/packages/core/dist/workflow/gate-file-validation.js.map +1 -0
  74. package/packages/core/dist/workflow/gate-file-validation.test.d.ts +19 -0
  75. package/packages/core/dist/workflow/gate-file-validation.test.d.ts.map +1 -0
  76. package/packages/core/dist/workflow/gate-file-validation.test.js +536 -0
  77. package/packages/core/dist/workflow/gate-file-validation.test.js.map +1 -0
  78. package/packages/core/dist/workflow/gate-schema-validation.test.d.ts +14 -0
  79. package/packages/core/dist/workflow/gate-schema-validation.test.d.ts.map +1 -0
  80. package/packages/core/dist/workflow/gate-schema-validation.test.js +339 -0
  81. package/packages/core/dist/workflow/gate-schema-validation.test.js.map +1 -0
  82. package/packages/core/dist/workflow/handoff.js +2 -2
  83. package/packages/core/dist/workflow/handoff.js.map +1 -1
  84. package/packages/core/dist/workflow/handoff.test.js +16 -0
  85. package/packages/core/dist/workflow/handoff.test.js.map +1 -1
  86. package/packages/core/dist/workflow/variable-resolver.test.js +1 -1
  87. package/packages/core/dist/workflow/variable-resolver.test.js.map +1 -1
  88. package/packages/core/dist/workflow/workflow-migration.test.js +4 -3
  89. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  90. package/packages/core/dist/workflow/workflow-schema.d.ts +4 -2
  91. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  92. package/packages/core/dist/workflow/workflow-schema.js +43 -8
  93. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  94. package/pennyfarthing-dist/agents/README.md +6 -14
  95. package/pennyfarthing-dist/agents/architect.md +43 -30
  96. package/pennyfarthing-dist/agents/ba.md +30 -29
  97. package/pennyfarthing-dist/agents/dev.md +76 -41
  98. package/pennyfarthing-dist/agents/devops.md +57 -21
  99. package/pennyfarthing-dist/agents/orchestrator.md +3 -11
  100. package/pennyfarthing-dist/agents/pm.md +45 -31
  101. package/pennyfarthing-dist/agents/reviewer.md +20 -66
  102. package/pennyfarthing-dist/agents/sm-setup.md +2 -2
  103. package/pennyfarthing-dist/agents/sm.md +8 -30
  104. package/pennyfarthing-dist/agents/tea.md +25 -41
  105. package/pennyfarthing-dist/agents/tech-writer.md +33 -90
  106. package/pennyfarthing-dist/agents/ux-designer.md +39 -40
  107. package/pennyfarthing-dist/commands/benchmark-control.md +8 -64
  108. package/pennyfarthing-dist/commands/benchmark.md +8 -480
  109. package/pennyfarthing-dist/commands/job-fair.md +8 -97
  110. package/pennyfarthing-dist/commands/pf-benchmark-control.md +70 -0
  111. package/pennyfarthing-dist/commands/pf-benchmark.md +486 -0
  112. package/pennyfarthing-dist/commands/pf-chore.md +4 -4
  113. package/pennyfarthing-dist/commands/pf-ci.md +40 -0
  114. package/pennyfarthing-dist/commands/pf-close-epic.md +9 -27
  115. package/pennyfarthing-dist/commands/pf-continue-session.md +9 -213
  116. package/pennyfarthing-dist/commands/pf-create-branches-from-story.md +11 -353
  117. package/pennyfarthing-dist/commands/pf-docs.md +28 -0
  118. package/pennyfarthing-dist/commands/pf-epic.md +67 -0
  119. package/pennyfarthing-dist/commands/pf-git-cleanup.md +11 -52
  120. package/pennyfarthing-dist/commands/pf-git.md +75 -0
  121. package/pennyfarthing-dist/commands/pf-help.md +110 -128
  122. package/pennyfarthing-dist/commands/pf-job-fair.md +102 -0
  123. package/pennyfarthing-dist/commands/pf-new-work.md +9 -18
  124. package/pennyfarthing-dist/commands/pf-parallel-work.md +6 -66
  125. package/pennyfarthing-dist/commands/pf-release.md +11 -76
  126. package/pennyfarthing-dist/commands/pf-repo-status.md +11 -44
  127. package/pennyfarthing-dist/commands/pf-run-ci.md +8 -111
  128. package/pennyfarthing-dist/commands/pf-session.md +51 -0
  129. package/pennyfarthing-dist/commands/pf-solo.md +447 -0
  130. package/pennyfarthing-dist/commands/pf-sprint-planning.md +8 -104
  131. package/pennyfarthing-dist/commands/pf-standalone.md +1 -1
  132. package/pennyfarthing-dist/commands/pf-start-epic.md +9 -163
  133. package/pennyfarthing-dist/commands/pf-sync-epic-to-jira.md +8 -179
  134. package/pennyfarthing-dist/commands/pf-sync-work-with-sprint.md +8 -368
  135. package/pennyfarthing-dist/commands/pf-update-domain-docs.md +8 -78
  136. package/pennyfarthing-dist/commands/solo.md +8 -442
  137. package/pennyfarthing-dist/guides/agent-behavior.md +14 -14
  138. package/pennyfarthing-dist/guides/agent-coordination.md +7 -7
  139. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -6
  140. package/pennyfarthing-dist/guides/bikerack.md +128 -0
  141. package/pennyfarthing-dist/guides/brownfield-tools.md +133 -0
  142. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +2 -2
  143. package/pennyfarthing-dist/guides/gate-schema.md +227 -0
  144. package/pennyfarthing-dist/guides/gates.md +120 -0
  145. package/pennyfarthing-dist/guides/handoff-cli.md +116 -0
  146. package/pennyfarthing-dist/guides/hooks.md +86 -4
  147. package/pennyfarthing-dist/guides/output-styles.md +65 -0
  148. package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +5 -5
  149. package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -4
  150. package/pennyfarthing-dist/guides/prompt-patterns.md +5 -5
  151. package/pennyfarthing-dist/guides/reflector.md +4 -4
  152. package/pennyfarthing-dist/guides/session-artifacts.md +1 -1
  153. package/pennyfarthing-dist/guides/skill-schema.md +1 -1
  154. package/pennyfarthing-dist/guides/tandem-protocol.md +13 -1
  155. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  156. package/pennyfarthing-dist/guides/xml-tags.md +5 -4
  157. package/pennyfarthing-dist/personas/themes/hogans-heroes.yaml +11 -22
  158. package/pennyfarthing-dist/personas/themes/stephen-king.yaml +13 -24
  159. package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
  160. package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
  161. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  162. package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
  163. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  164. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
  165. package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
  166. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +0 -0
  167. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  168. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
  169. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  170. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +0 -0
  171. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  172. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  173. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  174. package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
  175. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +19 -14
  176. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
  177. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
  178. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  179. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
  180. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  181. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  182. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  183. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  184. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  185. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  186. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
  187. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  188. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  189. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  190. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  191. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  192. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  193. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  194. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  195. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  196. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  197. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  198. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  199. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  200. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  201. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  202. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  203. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  204. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  205. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  206. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  207. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  208. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  209. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
  210. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  211. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  212. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  213. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  214. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  215. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  216. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  217. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  218. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
  219. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +191 -57
  220. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +26 -10
  221. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  222. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  223. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  224. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  225. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  226. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  227. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
  228. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  229. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  230. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  231. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  232. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  233. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
  234. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  235. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  236. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  237. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  238. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  239. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
  240. package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
  241. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  242. package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
  243. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
  244. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
  245. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  246. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
  247. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
  248. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
  249. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
  250. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
  251. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
  252. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
  253. package/pennyfarthing-dist/skills/pf-changelog/SKILL.md +4 -4
  254. package/pennyfarthing-dist/skills/pf-sprint/skill.md +1 -1
  255. package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
  256. package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
  257. package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
  258. package/pennyfarthing-dist/skills/pf-systematic-debugging/SKILL.md +0 -1
  259. package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -0
  260. package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -0
  261. package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -0
  262. package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -0
  263. package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -0
  264. package/pennyfarthing-dist/skills/skill-registry.schema.json +4 -0
  265. package/pennyfarthing-dist/skills/skill-registry.yaml +8 -21
  266. package/pennyfarthing-dist/workflows/2party-tdd.yaml +11 -0
  267. package/pennyfarthing-dist/workflows/agent-docs.yaml +2 -0
  268. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +4 -0
  269. package/pennyfarthing-dist/workflows/bdd.yaml +4 -0
  270. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  271. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +3 -0
  272. package/pennyfarthing-dist/workflows/tdd.yaml +3 -0
  273. package/pennyfarthing-dist/workflows/trivial.yaml +2 -0
  274. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  283. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  289. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  291. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  292. package/pennyfarthing_scripts/bikerack/changed_panel.py +105 -0
  293. package/pennyfarthing_scripts/bikerack/debug_panel.py +218 -0
  294. package/pennyfarthing_scripts/bikerack/diffs_panel.py +203 -27
  295. package/pennyfarthing_scripts/cli.py +114 -0
  296. package/pennyfarthing_scripts/epic/__init__.py +0 -0
  297. package/pennyfarthing_scripts/epic/cli.py +64 -0
  298. package/pennyfarthing_scripts/gate/__init__.py +1 -0
  299. package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/gate/cli.py +56 -0
  303. package/pennyfarthing_scripts/gate/validate.py +266 -0
  304. package/pennyfarthing_scripts/git_group/__init__.py +0 -0
  305. package/pennyfarthing_scripts/git_group/cli.py +100 -0
  306. package/pennyfarthing_scripts/handoff/__init__.py +1 -0
  307. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  309. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/handoff/cli.py +120 -0
  315. package/pennyfarthing_scripts/handoff/complete_phase.py +155 -0
  316. package/pennyfarthing_scripts/handoff/gate_file.py +105 -0
  317. package/pennyfarthing_scripts/handoff/gate_runner.py +152 -0
  318. package/pennyfarthing_scripts/handoff/marker.py +109 -0
  319. package/pennyfarthing_scripts/handoff/resolve_gate.py +152 -0
  320. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  323. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  324. package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
  326. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  327. package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
  328. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  329. package/pennyfarthing_scripts/prime/workflow.py +39 -0
  330. package/pennyfarthing_scripts/session/__init__.py +0 -0
  331. package/pennyfarthing_scripts/session/cli.py +87 -0
  332. package/pennyfarthing_scripts/session_start_hook.py +4 -4
  333. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  334. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  335. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  336. package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  337. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  338. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  339. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  340. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/sprint/archive_epic.py +8 -0
  344. package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
  345. package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
  346. package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
  347. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  348. package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  353. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
  354. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
  355. package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
  356. package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
  357. package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
  358. package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
  359. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  360. package/pennyfarthing_scripts/tests/test_108_1_gate_migration.py +540 -0
  361. package/pennyfarthing_scripts/tests/test_108_2_remove_handoff_fallback.py +339 -0
  362. package/pennyfarthing_scripts/tests/test_archive_epic.py +1 -2
  363. package/pennyfarthing_scripts/tests/test_confidence_sm_evaluation.py +253 -0
  364. package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +315 -0
  365. package/pennyfarthing_scripts/tests/test_gate_file_resolution.py +341 -0
  366. package/pennyfarthing_scripts/tests/test_gate_runner.py +620 -0
  367. package/pennyfarthing_scripts/tests/test_handoff_cli.py +929 -0
  368. package/pennyfarthing_scripts/tests/test_handoff_e2e.py +454 -0
  369. package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +464 -0
  370. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  371. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  372. package/pennyfarthing_scripts/validate/adapters/skill_command.py +200 -0
  373. package/pennyfarthing_scripts/validate/adapters/workflow.py +64 -0
  374. package/pennyfarthing_scripts/validate/cli.py +15 -4
  375. package/packages/core/dist/benchmark/package-exports.test.d.ts.map +0 -1
  376. package/packages/core/dist/benchmark/package-exports.test.js.map +0 -1
  377. package/packages/core/dist/scripts/benchmark-integration.d.ts +0 -182
  378. package/packages/core/dist/scripts/benchmark-integration.d.ts.map +0 -1
  379. package/packages/core/dist/scripts/benchmark-integration.js +0 -691
  380. package/packages/core/dist/scripts/benchmark-integration.js.map +0 -1
  381. package/packages/core/dist/scripts/benchmark-integration.test.d.ts +0 -13
  382. package/packages/core/dist/scripts/benchmark-integration.test.d.ts.map +0 -1
  383. package/packages/core/dist/scripts/benchmark-integration.test.js +0 -680
  384. package/packages/core/dist/scripts/benchmark-integration.test.js.map +0 -1
  385. package/packages/core/dist/scripts/debugging-scenarios.test.d.ts +0 -18
  386. package/packages/core/dist/scripts/debugging-scenarios.test.d.ts.map +0 -1
  387. package/packages/core/dist/scripts/debugging-scenarios.test.js +0 -317
  388. package/packages/core/dist/scripts/debugging-scenarios.test.js.map +0 -1
  389. package/packages/core/dist/scripts/job-fair-aggregator.d.ts +0 -150
  390. package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +0 -1
  391. package/packages/core/dist/scripts/job-fair-aggregator.js +0 -547
  392. package/packages/core/dist/scripts/job-fair-aggregator.js.map +0 -1
  393. package/packages/core/dist/scripts/job-fair-aggregator.test.d.ts +0 -14
  394. package/packages/core/dist/scripts/job-fair-aggregator.test.d.ts.map +0 -1
  395. package/packages/core/dist/scripts/job-fair-aggregator.test.js +0 -616
  396. package/packages/core/dist/scripts/job-fair-aggregator.test.js.map +0 -1
  397. package/pennyfarthing-dist/agents/handoff.md +0 -250
  398. package/pennyfarthing-dist/agents/sm-handoff.md +0 -152
  399. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -112
  400. package/pennyfarthing-dist/skills/pf-dev-patterns/SKILL.md +0 -461
  401. package/scripts/README.md +0 -41
@@ -0,0 +1,218 @@
1
+ """DebugPanel — Context usage and token stats for BikeRack TUI.
2
+
3
+ Story 103-17: Port of the React DebugPanel. Subscribes to /ws/context
4
+ and /ws/token-stats, renders context usage (tokens, percent, tier) and
5
+ token consumption stats (input, output, cache, cost).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from rich.console import Group
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+
16
+ from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
17
+
18
+ # Tier → Rich style mapping
19
+ _TIER_STYLES: dict[str, str] = {
20
+ "FULL": "bold green",
21
+ "REFRESH": "bold yellow",
22
+ "HANDOFF": "bold cyan",
23
+ "MINIMAL": "bold red",
24
+ }
25
+
26
+
27
+ def _safe_int(value: Any) -> int | None:
28
+ """Safely convert a value to int, returning None on failure."""
29
+ if value is None:
30
+ return None
31
+ try:
32
+ return int(value)
33
+ except (ValueError, TypeError):
34
+ return None
35
+
36
+
37
+ def _safe_float(value: Any) -> float | None:
38
+ """Safely convert a value to float, returning None on failure."""
39
+ if value is None:
40
+ return None
41
+ try:
42
+ return float(value)
43
+ except (ValueError, TypeError):
44
+ return None
45
+
46
+
47
+ def _format_tokens(value: Any) -> str:
48
+ """Format a token count with comma separators."""
49
+ n = _safe_int(value)
50
+ if n is None:
51
+ return "—"
52
+ return f"{n:,}"
53
+
54
+
55
+ class DebugPanel(BasePanel):
56
+ """Context usage and token stats panel.
57
+
58
+ Subscribes to both ``context`` and ``token-stats`` WebSocket channels.
59
+ Renders context usage (tokens, percentage, tier) and token consumption
60
+ stats (input, output, cache read/write, cost).
61
+ """
62
+
63
+ channel: str = "context"
64
+ panel_name: str = "Debug"
65
+ icon: str = PANEL_ICONS["debug"][0]
66
+
67
+ def __init__(self, client: Any = None, **kwargs: Any) -> None:
68
+ super().__init__(client=client, **kwargs)
69
+ self._context_data: dict[str, Any] | None = None
70
+ self._token_stats: dict[str, Any] | None = None
71
+
72
+ def on_mount(self) -> None:
73
+ """Subscribe to both context and token-stats channels."""
74
+ self._mounted = True
75
+ if self._client is not None:
76
+ self._client.subscribe("context", self._handle_context_message)
77
+ self._client.subscribe("token-stats", self._handle_token_stats_message)
78
+
79
+ def _handle_context_message(self, message: dict[str, Any] | None) -> None:
80
+ """Handle incoming context channel message."""
81
+ if message is None:
82
+ return
83
+ ctx = message.get("context")
84
+ if isinstance(ctx, dict):
85
+ self._context_data = ctx
86
+ else:
87
+ self._context_data = {}
88
+ self._rerender()
89
+
90
+ def _handle_token_stats_message(self, message: dict[str, Any] | None) -> None:
91
+ """Handle incoming token-stats channel message."""
92
+ if message is None:
93
+ return
94
+ self._token_stats = message
95
+ self._rerender()
96
+
97
+ def _rerender(self) -> None:
98
+ """Re-render with the latest data from both channels."""
99
+ rendered = self.render_panel(self._context_data or {})
100
+ try:
101
+ self.update(rendered)
102
+ except Exception:
103
+ pass
104
+
105
+ def render_panel(self, payload: dict[str, Any]) -> Any:
106
+ """Render combined context usage and token stats."""
107
+ parts: list[Any] = []
108
+
109
+ # --- Context Usage Section ---
110
+ ctx = self._context_data
111
+ if ctx:
112
+ parts.append(_render_context(ctx))
113
+ elif not self._token_stats:
114
+ return Text("No context data", style="dim italic")
115
+
116
+ # --- Token Stats Section ---
117
+ if self._token_stats:
118
+ if parts:
119
+ parts.append(Text("")) # spacer
120
+ parts.append(_render_token_stats(self._token_stats))
121
+
122
+ if not parts:
123
+ return Text("No context data", style="dim italic")
124
+
125
+ return Group(*parts)
126
+
127
+
128
+ def _render_context(ctx: dict[str, Any]) -> Any:
129
+ """Render context usage section."""
130
+ parts: list[Any] = []
131
+
132
+ # Tier badge
133
+ tier = ctx.get("tier")
134
+ if tier and isinstance(tier, str):
135
+ style = _TIER_STYLES.get(tier, "bold")
136
+ tier_text = Text()
137
+ tier_text.append(tier, style=style)
138
+ savings = _tier_savings(tier)
139
+ if savings > 0:
140
+ tier_text.append(f" {savings}% savings", style="dim")
141
+ parts.append(tier_text)
142
+
143
+ # Token usage
144
+ tokens = _safe_int(ctx.get("tokens"))
145
+ percent = _safe_int(ctx.get("percent"))
146
+ baseline = _safe_int(ctx.get("baseline"))
147
+ available = _safe_int(ctx.get("available"))
148
+
149
+ if tokens is not None:
150
+ usage_text = Text()
151
+ usage_text.append(f"{tokens:,}", style="bold")
152
+ if baseline is not None and available is not None:
153
+ total = baseline + available
154
+ usage_text.append(f" / {total:,}")
155
+ usage_text.append(" tokens")
156
+ if percent is not None:
157
+ usage_text.append(f" ({percent}%)")
158
+ parts.append(usage_text)
159
+
160
+ # Breakdown: baseline / conversation / available
161
+ if baseline is not None:
162
+ breakdown = Table(show_header=False, show_edge=False, pad_edge=False, box=None)
163
+ breakdown.add_column("Label", style="dim")
164
+ breakdown.add_column("Value", justify="right")
165
+ breakdown.add_row("System Prompt", _format_tokens(baseline))
166
+ usable = _safe_int(ctx.get("usableTokens"))
167
+ if usable is not None:
168
+ breakdown.add_row("Conversation", _format_tokens(usable))
169
+ if available is not None:
170
+ breakdown.add_row("Available", _format_tokens(available))
171
+ parts.append(breakdown)
172
+
173
+ if not parts:
174
+ return Text("No context data", style="dim italic")
175
+
176
+ return Group(*parts)
177
+
178
+
179
+ def _render_token_stats(stats: dict[str, Any]) -> Any:
180
+ """Render token stats section."""
181
+ table = Table(show_header=False, show_edge=False, pad_edge=False, box=None)
182
+ table.add_column("Stat", style="dim")
183
+ table.add_column("Value", justify="right")
184
+
185
+ rows: list[tuple[str, str]] = []
186
+
187
+ input_t = _safe_int(stats.get("inputTokens"))
188
+ if input_t is not None:
189
+ rows.append(("Input", f"{input_t:,}"))
190
+
191
+ output_t = _safe_int(stats.get("outputTokens"))
192
+ if output_t is not None:
193
+ rows.append(("Output", f"{output_t:,}"))
194
+
195
+ cache_read = _safe_int(stats.get("cacheReadTokens"))
196
+ if cache_read is not None:
197
+ rows.append(("Cache Read", f"{cache_read:,}"))
198
+
199
+ cache_write = _safe_int(stats.get("cacheCreationTokens"))
200
+ if cache_write is not None:
201
+ rows.append(("Cache Write", f"{cache_write:,}"))
202
+
203
+ cost = _safe_float(stats.get("totalCostUsd"))
204
+ if cost is not None and cost > 0:
205
+ rows.append(("Cost", f"${cost:.4f}"))
206
+
207
+ if not rows:
208
+ return Text("No token stats", style="dim italic")
209
+
210
+ for label, value in rows:
211
+ table.add_row(label, value)
212
+
213
+ return table
214
+
215
+
216
+ def _tier_savings(tier: str) -> int:
217
+ """Calculate token savings percentage for a tier vs FULL."""
218
+ return {"FULL": 0, "REFRESH": 85, "HANDOFF": 82, "MINIMAL": 95}.get(tier, 0)
@@ -2,12 +2,16 @@
2
2
 
3
3
  Story 103-18: Subscribes to /ws/diffs, renders file diffs with syntax
4
4
  highlighting using Rich. File headers, added/removed line coloring, line numbers.
5
+
6
+ Story 103-19: Large diff handling — truncation at 1000 lines, pagination,
7
+ temp file storage for very large diffs (>5000 lines), non-blocking rendering.
5
8
  """
6
9
 
7
10
  from __future__ import annotations
8
11
 
9
12
  import os
10
13
  import re
14
+ import tempfile
11
15
  from typing import Any
12
16
 
13
17
  from rich.console import Group
@@ -16,6 +20,16 @@ from rich.text import Text
16
20
 
17
21
  from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
18
22
 
23
+ #: Default max content lines rendered per page before truncation kicks in.
24
+ DEFAULT_LINE_LIMIT = 1000
25
+
26
+ #: Diffs with more raw lines than this threshold are written to temp files.
27
+ TEMP_FILE_THRESHOLD = 5000
28
+
29
+ #: Skip syntax highlighting for diffs larger than this to maintain rendering performance.
30
+ #: At 5000+ lines, syntax highlighting cost becomes prohibitive for <100ms frame time.
31
+ HIGHLIGHT_THRESHOLD = 5000
32
+
19
33
 
20
34
  class DiffsPanel(BasePanel):
21
35
  """Diff rendering panel with syntax highlighting.
@@ -23,25 +37,99 @@ class DiffsPanel(BasePanel):
23
37
  Subscribes to the ``diffs`` WebSocket channel and renders
24
38
  file diffs with syntax highlighting, colored added/removed lines,
25
39
  file headers, and line numbers.
40
+
41
+ Large diffs (>1000 content lines) are truncated with a text indicator
42
+ and support pagination via ``next_page()`` / ``prev_page()``.
43
+ Very large diffs (>5000 lines) use temp file storage for memory efficiency.
26
44
  """
27
45
 
28
46
  channel: str = "diffs"
29
47
  panel_name: str = "Diffs"
30
48
  icon: str = PANEL_ICONS["diffs"][0]
31
49
 
50
+ def __init__(self, client=None, **kwargs):
51
+ super().__init__(client=client, **kwargs)
52
+ self._current_page: int = 0
53
+ self._max_page: int = 0
54
+ self._temp_files: list[str] = []
55
+
56
+ def next_page(self) -> None:
57
+ """Advance to the next page of truncated diff content."""
58
+ if self._current_page < self._max_page:
59
+ self._current_page += 1
60
+
61
+ def prev_page(self) -> None:
62
+ """Go back to the previous page of truncated diff content."""
63
+ if self._current_page > 0:
64
+ self._current_page -= 1
65
+
66
+ def handle_message(self, message: dict[str, Any] | None) -> None:
67
+ """Handle incoming WebSocket message with pagination reset and temp management."""
68
+ if not self._mounted or message is None:
69
+ return
70
+ self._current_page = 0
71
+ self._cleanup_temp_files()
72
+ self._store_large_diffs(message)
73
+ super().handle_message(message)
74
+
75
+ def on_unmount(self) -> None:
76
+ """Clean up temp files and mark panel as unmounted."""
77
+ self._cleanup_temp_files()
78
+ super().on_unmount()
79
+
32
80
  def render_panel(self, payload: dict[str, Any]) -> Any:
33
- """Render diff data from WebSocket payload."""
81
+ """Render diff data from WebSocket payload with truncation/pagination."""
34
82
  diffs = payload.get("diffs", [])
35
83
  if not diffs:
36
84
  return Text("No diffs yet", style="dim italic")
37
85
 
38
86
  parts: list[Any] = []
87
+ max_total = 0
39
88
  for diff_entry in diffs:
40
- parts.extend(_render_file_diff(diff_entry))
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
+
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)
41
100
  parts.append(Text("")) # separator between files
101
+ max_total = max(max_total, total_lines)
102
+
103
+ # Track max page for pagination bounds
104
+ if max_total > DEFAULT_LINE_LIMIT:
105
+ self._max_page = -(-max_total // DEFAULT_LINE_LIMIT) - 1
106
+ else:
107
+ self._max_page = 0
42
108
 
43
109
  return Group(*parts)
44
110
 
111
+ def _store_large_diffs(self, message: dict[str, Any]) -> None:
112
+ """Write very large diffs to temp files for memory-efficient access."""
113
+ diffs = message.get("diffs", [])
114
+ for diff_entry in diffs:
115
+ raw_diff = diff_entry.get("diff", "")
116
+ if raw_diff.count("\n") > TEMP_FILE_THRESHOLD:
117
+ fd, path = tempfile.mkstemp(
118
+ prefix="bikerack_diff_", suffix=".diff"
119
+ )
120
+ with os.fdopen(fd, "w") as f:
121
+ f.write(raw_diff)
122
+ self._temp_files.append(path)
123
+
124
+ def _cleanup_temp_files(self) -> None:
125
+ """Remove any temp files created for large diff storage."""
126
+ for path in self._temp_files:
127
+ try:
128
+ os.unlink(path)
129
+ except OSError:
130
+ pass
131
+ self._temp_files.clear()
132
+
45
133
 
46
134
  _LANG_MAP: dict[str, str] = {
47
135
  ".py": "python", ".ts": "typescript", ".tsx": "tsx",
@@ -74,8 +162,23 @@ def _highlight_code(code: str, language: str) -> Text:
74
162
  return Text(code)
75
163
 
76
164
 
77
- def _render_file_diff(diff_entry: dict[str, Any]) -> list[Any]:
78
- """Render a single file's diff as a list of Rich renderables."""
165
+ def _render_file_diff(
166
+ diff_entry: dict[str, Any],
167
+ page: int = 0,
168
+ page_size: int = DEFAULT_LINE_LIMIT,
169
+ skip_highlight: bool = False,
170
+ ) -> tuple[list[Any], int]:
171
+ """Render a single file's diff as a list of Rich renderables.
172
+
173
+ Args:
174
+ diff_entry: Diff data from WebSocket payload
175
+ page: Current page number for pagination
176
+ page_size: Lines per page for truncation
177
+ skip_highlight: Skip syntax highlighting for performance on large diffs
178
+
179
+ Returns:
180
+ Tuple of (renderable_parts, total_content_line_count).
181
+ """
79
182
  path = diff_entry.get("path", "unknown")
80
183
  status = diff_entry.get("status", "")
81
184
  additions = diff_entry.get("additions")
@@ -92,19 +195,70 @@ def _render_file_diff(diff_entry: dict[str, Any]) -> list[Any]:
92
195
 
93
196
  language = _detect_language(path)
94
197
  parts: list[Any] = [header]
95
- parts.extend(_parse_diff_lines(raw_diff, language))
96
- return parts
198
+ rendered_lines, total_lines = _parse_diff_lines(
199
+ raw_diff, language, page=page, page_size=page_size, skip_highlight=skip_highlight,
200
+ )
201
+ parts.extend(rendered_lines)
202
+
203
+ # Add truncation/pagination indicator if content was truncated
204
+ if total_lines > page_size:
205
+ total_pages = -(-total_lines // page_size) # ceil division
206
+ current_page_display = page + 1
207
+ visible_end = min((page + 1) * page_size, total_lines)
208
+
209
+ if page == 0:
210
+ indicator = (
211
+ f"Showing first {visible_end} of {total_lines} lines"
212
+ f" — Page {current_page_display} / {total_pages}"
213
+ )
214
+ else:
215
+ visible_start = page * page_size + 1
216
+ indicator = (
217
+ f"Showing lines {visible_start}-{visible_end}"
218
+ f" of {total_lines} lines"
219
+ f" — Page {current_page_display} / {total_pages}"
220
+ )
221
+ parts.append(Text(indicator, style="dim yellow"))
97
222
 
223
+ return parts, total_lines
98
224
 
99
- def _parse_diff_lines(raw_diff: str, language: str = "text") -> list[Any]:
100
- """Parse unified diff output into styled Rich Text lines with syntax highlighting."""
225
+
226
+ def _parse_diff_lines(
227
+ raw_diff: str,
228
+ language: str = "text",
229
+ page: int = 0,
230
+ page_size: int = DEFAULT_LINE_LIMIT,
231
+ skip_highlight: bool = False,
232
+ ) -> tuple[list[Any], int]:
233
+ """Parse unified diff output into styled Rich Text lines with syntax highlighting.
234
+
235
+ Optimized single-pass streaming:
236
+ - Only highlight lines within current page range (unless skip_highlight=True)
237
+ - Count total lines without building objects for them
238
+ - Scan entire diff to get accurate total for pagination
239
+
240
+ Args:
241
+ raw_diff: Raw unified diff string
242
+ language: Programming language for syntax highlighting
243
+ page: Current page number for pagination
244
+ page_size: Lines per page
245
+ skip_highlight: Skip syntax highlighting for performance on large diffs
246
+
247
+ Returns:
248
+ Tuple of (rendered_lines, total_content_line_count).
249
+ """
101
250
  if not raw_diff or not raw_diff.strip():
102
- return []
251
+ return [], 0
103
252
 
104
253
  result: list[Any] = []
105
254
  line_num = 0
255
+ content_idx = 0
256
+ start = page * page_size
257
+ end = start + page_size
258
+ current_hunk_header = None
106
259
 
107
260
  for line in raw_diff.split("\n"):
261
+ # Skip diff metadata lines (not counted as content)
108
262
  if line.startswith("diff --git") or line.startswith("index "):
109
263
  continue
110
264
  if line.startswith("---") or line.startswith("+++"):
@@ -113,34 +267,56 @@ def _parse_diff_lines(raw_diff: str, language: str = "text") -> list[Any]:
113
267
  continue
114
268
  if line.startswith("rename "):
115
269
  continue
270
+ if line == "":
271
+ continue
116
272
 
117
273
  if line.startswith("Binary files"):
118
- result.append(Text(line, style="dim italic"))
274
+ if start <= content_idx < end:
275
+ result.append(Text(line, style="dim italic"))
276
+ content_idx += 1
119
277
  elif line.startswith("@@"):
278
+ # @@ line is metadata, extract line number but don't count as content
279
+ current_hunk_header = line
120
280
  match = re.match(r"@@ -\d+(?:,\d+)? \+(\d+)", line)
121
281
  if match:
122
282
  line_num = int(match.group(1)) - 1
123
- result.append(Text(line, style="cyan"))
283
+ if start <= content_idx < end:
284
+ result.append(Text(current_hunk_header, style="cyan"))
124
285
  elif line.startswith("+"):
125
286
  line_num += 1
126
- t = Text()
127
- t.append(f"{line_num:4d} ", style="dim green")
128
- t.append_text(_highlight_code(line[1:], language))
129
- result.append(t)
287
+ if start <= content_idx < end:
288
+ t = Text()
289
+ t.append(f"{line_num:4d} ", style="dim green")
290
+ if not skip_highlight:
291
+ t.append_text(_highlight_code(line[1:], language))
292
+ else:
293
+ t.append(line[1:])
294
+ result.append(t)
295
+ content_idx += 1
130
296
  elif line.startswith("-"):
131
- t = Text()
132
- t.append(" - ", style="red")
133
- t.append_text(_highlight_code(line[1:], language))
134
- result.append(t)
297
+ if start <= content_idx < end:
298
+ t = Text()
299
+ t.append(" - ", style="red")
300
+ if not skip_highlight:
301
+ t.append_text(_highlight_code(line[1:], language))
302
+ else:
303
+ t.append(line[1:])
304
+ result.append(t)
305
+ content_idx += 1
135
306
  elif line.startswith(" "):
136
307
  line_num += 1
137
- t = Text()
138
- t.append(f"{line_num:4d} ", style="dim")
139
- t.append_text(_highlight_code(line[1:], language))
140
- result.append(t)
141
- elif line == "":
142
- continue
308
+ if start <= content_idx < end:
309
+ t = Text()
310
+ t.append(f"{line_num:4d} ", style="dim")
311
+ if not skip_highlight:
312
+ t.append_text(_highlight_code(line[1:], language))
313
+ else:
314
+ t.append(line[1:])
315
+ result.append(t)
316
+ content_idx += 1
143
317
  else:
144
- result.append(Text(line, style="dim"))
318
+ if start <= content_idx < end:
319
+ result.append(Text(line, style="dim"))
320
+ content_idx += 1
145
321
 
146
- return result
322
+ return result, content_idx
@@ -26,6 +26,9 @@ def cli():
26
26
  workflow - Workflow state and phase management
27
27
  agent - Agent session management
28
28
  sprint - Sprint status and story operations
29
+ git - Repository operations (status, cleanup, branches, release)
30
+ session - Session lifecycle (new, continue, parallel)
31
+ epic - Epic lifecycle (start, close)
29
32
  debug - Analysis tools (hotspots, deadcode, healthscore)
30
33
 
31
34
  \b
@@ -114,6 +117,26 @@ from pennyfarthing_scripts.bc.cli import bc # noqa: E402
114
117
 
115
118
  cli.add_command(bc)
116
119
 
120
+ # Import and register handoff group
121
+ from pennyfarthing_scripts.handoff.cli import handoff # noqa: E402
122
+
123
+ cli.add_command(handoff)
124
+
125
+ # Import and register git group
126
+ from pennyfarthing_scripts.git_group.cli import git # noqa: E402
127
+
128
+ cli.add_command(git)
129
+
130
+ # Import and register session group
131
+ from pennyfarthing_scripts.session.cli import session # noqa: E402
132
+
133
+ cli.add_command(session)
134
+
135
+ # Import and register epic group
136
+ from pennyfarthing_scripts.epic.cli import epic # noqa: E402
137
+
138
+ cli.add_command(epic)
139
+
117
140
 
118
141
  @cli.group()
119
142
  def agent():
@@ -244,6 +267,97 @@ def workflow_handoff(next_agent: str):
244
267
  click.echo("---")
245
268
 
246
269
 
270
+ @cli.command("help")
271
+ @click.argument("group", required=False)
272
+ def help_cmd(group: str | None):
273
+ """Context-aware help for Pennyfarthing commands.
274
+
275
+ \b
276
+ Arguments:
277
+ GROUP - Optional command group name (sprint, git, session, epic, jira, theme, workflow, etc.)
278
+ """
279
+
280
+ import yaml
281
+
282
+ from pennyfarthing_scripts.common.config import get_project_root
283
+
284
+ root = get_project_root()
285
+ registry_path = root / "pennyfarthing-dist" / "command-registry.yaml"
286
+
287
+ if not registry_path.is_file():
288
+ click.echo("Command registry not found. Run /pf-health-check.", err=True)
289
+ raise SystemExit(1)
290
+
291
+ registry = yaml.safe_load(registry_path.read_text())
292
+
293
+ if group is None:
294
+ # Show overview
295
+ click.echo("Pennyfarthing CLI — Command Reference")
296
+ click.echo("=" * 45)
297
+ click.echo("")
298
+ click.echo("Resource Groups:")
299
+ for name, grp in registry.get("groups", {}).items():
300
+ cli_cmd = grp.get("cli", "")
301
+ slash = grp.get("slash", "")
302
+ desc = grp.get("description", "")
303
+ parts = []
304
+ if slash:
305
+ parts.append(slash)
306
+ if cli_cmd:
307
+ parts.append(cli_cmd)
308
+ ref = ", ".join(parts)
309
+ click.echo(f" {name:<12} {desc:<45} ({ref})")
310
+ click.echo("")
311
+ click.echo("Standalone:")
312
+ for name, cmd in registry.get("standalone", {}).items():
313
+ slash = cmd.get("slash", "")
314
+ desc = cmd.get("description", "")
315
+ click.echo(f" {name:<12} {desc:<45} ({slash})")
316
+ click.echo("")
317
+ click.echo("Use 'pf help <group>' for detailed commands.")
318
+ return
319
+
320
+ # Show specific group
321
+ groups = registry.get("groups", {})
322
+ if group not in groups:
323
+ click.echo(f"Unknown group: {group}", err=True)
324
+ click.echo(f"Available groups: {', '.join(groups.keys())}", err=True)
325
+ raise SystemExit(1)
326
+
327
+ grp = groups[group]
328
+ click.echo(f"{group} — {grp.get('description', '')}")
329
+ click.echo("-" * 45)
330
+ if grp.get("cli"):
331
+ click.echo(f"CLI: {grp['cli']}")
332
+ if grp.get("slash"):
333
+ click.echo(f"Slash: {grp['slash']}")
334
+ if grp.get("skill"):
335
+ click.echo(f"Skill: {grp['skill']}")
336
+ click.echo("")
337
+
338
+ commands = grp.get("commands", {})
339
+ if commands:
340
+ click.echo("Commands:")
341
+ for cmd_name, cmd in commands.items():
342
+ args = cmd.get("args", "")
343
+ desc = cmd.get("description", "")
344
+ if args:
345
+ click.echo(f" {cmd_name} {args:<20} {desc}")
346
+ else:
347
+ click.echo(f" {cmd_name:<25} {desc}")
348
+
349
+ subgroups = grp.get("subgroups", {})
350
+ for sg_name, sg in subgroups.items():
351
+ click.echo(f"\n {sg_name} — {sg.get('description', '')}")
352
+ for cmd_name, cmd in sg.get("commands", {}).items():
353
+ args = cmd.get("args", "")
354
+ desc = cmd.get("description", "")
355
+ if args:
356
+ click.echo(f" {cmd_name} {args:<18} {desc}")
357
+ else:
358
+ click.echo(f" {cmd_name:<23} {desc}")
359
+
360
+
247
361
  def main():
248
362
  """Entry point for the CLI."""
249
363
  cli()
File without changes