@pennyfarthing/core 11.2.0 → 11.2.2

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 (367) hide show
  1. package/README.md +100 -40
  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 +474 -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 +7 -0
  16. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  17. package/packages/core/dist/cli/utils/settings.js +70 -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/dialogue-manager.d.ts +1 -1
  22. package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -1
  23. package/packages/core/dist/consultation/dialogue-manager.js +1 -1
  24. package/packages/core/dist/consultation/dialogue-manager.js.map +1 -1
  25. package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -1
  26. package/packages/core/dist/consultation/tandem-metrics.d.ts +91 -0
  27. package/packages/core/dist/consultation/tandem-metrics.d.ts.map +1 -0
  28. package/packages/core/dist/consultation/tandem-metrics.js +131 -0
  29. package/packages/core/dist/consultation/tandem-metrics.js.map +1 -0
  30. package/packages/core/dist/consultation/tandem-metrics.test.d.ts +18 -0
  31. package/packages/core/dist/consultation/tandem-metrics.test.d.ts.map +1 -0
  32. package/packages/core/dist/consultation/tandem-metrics.test.js +457 -0
  33. package/packages/core/dist/consultation/tandem-metrics.test.js.map +1 -0
  34. package/packages/core/dist/public/css/react.css +1 -1
  35. package/packages/core/dist/public/js/react/react.js +14 -14
  36. package/packages/core/dist/server/api/agent-load.js +1 -1
  37. package/packages/core/dist/server/api/agent-load.js.map +1 -1
  38. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  39. package/packages/core/dist/server/api/git.js +0 -1
  40. package/packages/core/dist/server/api/git.js.map +1 -1
  41. package/packages/core/dist/server/api/index.d.ts +2 -0
  42. package/packages/core/dist/server/api/index.d.ts.map +1 -1
  43. package/packages/core/dist/server/api/index.js +2 -0
  44. package/packages/core/dist/server/api/index.js.map +1 -1
  45. package/packages/core/dist/server/api/project-info.d.ts +11 -0
  46. package/packages/core/dist/server/api/project-info.d.ts.map +1 -0
  47. package/packages/core/dist/server/api/project-info.js +18 -0
  48. package/packages/core/dist/server/api/project-info.js.map +1 -0
  49. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  50. package/packages/core/dist/server/otlp-receiver.js +18 -1
  51. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  52. package/packages/core/dist/server/otlp-receiver.test.js +1 -1
  53. package/packages/core/dist/server/otlp-receiver.test.js.map +1 -1
  54. package/packages/core/dist/server/server.d.ts +0 -3
  55. package/packages/core/dist/server/server.d.ts.map +1 -1
  56. package/packages/core/dist/server/server.js +5 -38
  57. package/packages/core/dist/server/server.js.map +1 -1
  58. package/packages/core/dist/server/server.test.d.ts +1 -1
  59. package/packages/core/dist/server/server.test.js +12 -23
  60. package/packages/core/dist/server/server.test.js.map +1 -1
  61. package/packages/core/dist/server/settings.d.ts +1 -0
  62. package/packages/core/dist/server/settings.d.ts.map +1 -1
  63. package/packages/core/dist/server/settings.js +13 -0
  64. package/packages/core/dist/server/settings.js.map +1 -1
  65. package/packages/core/dist/shared/capabilities.d.ts +88 -0
  66. package/packages/core/dist/shared/capabilities.d.ts.map +1 -0
  67. package/packages/core/dist/shared/capabilities.js +133 -0
  68. package/packages/core/dist/shared/capabilities.js.map +1 -0
  69. package/packages/core/dist/shared/capabilities.test.d.ts +2 -0
  70. package/packages/core/dist/shared/capabilities.test.d.ts.map +1 -0
  71. package/packages/core/dist/shared/capabilities.test.js +217 -0
  72. package/packages/core/dist/shared/capabilities.test.js.map +1 -0
  73. package/packages/core/dist/shared/spawn-prompt.d.ts +47 -0
  74. package/packages/core/dist/shared/spawn-prompt.d.ts.map +1 -0
  75. package/packages/core/dist/shared/spawn-prompt.js +82 -0
  76. package/packages/core/dist/shared/spawn-prompt.js.map +1 -0
  77. package/packages/core/dist/shared/spawn-prompt.test.d.ts +2 -0
  78. package/packages/core/dist/shared/spawn-prompt.test.d.ts.map +1 -0
  79. package/packages/core/dist/shared/spawn-prompt.test.js +251 -0
  80. package/packages/core/dist/shared/spawn-prompt.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/team-lifecycle.d.ts +169 -0
  86. package/packages/core/dist/workflow/team-lifecycle.d.ts.map +1 -0
  87. package/packages/core/dist/workflow/team-lifecycle.js +217 -0
  88. package/packages/core/dist/workflow/team-lifecycle.js.map +1 -0
  89. package/packages/core/dist/workflow/team-lifecycle.test.d.ts +20 -0
  90. package/packages/core/dist/workflow/team-lifecycle.test.d.ts.map +1 -0
  91. package/packages/core/dist/workflow/team-lifecycle.test.js +966 -0
  92. package/packages/core/dist/workflow/team-lifecycle.test.js.map +1 -0
  93. package/packages/core/dist/workflow/workflow-schema.d.ts +32 -0
  94. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  95. package/packages/core/dist/workflow/workflow-schema.js +120 -0
  96. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  97. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  98. package/packages/core/dist/workflow/workflow-schema.test.js +570 -1
  99. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  100. package/packages/core/dist/workflow/workflow-team-templates.test.d.ts +17 -0
  101. package/packages/core/dist/workflow/workflow-team-templates.test.d.ts.map +1 -0
  102. package/packages/core/dist/workflow/workflow-team-templates.test.js +275 -0
  103. package/packages/core/dist/workflow/workflow-team-templates.test.js.map +1 -0
  104. package/pennyfarthing-dist/agents/dev.md +21 -12
  105. package/pennyfarthing-dist/agents/reviewer.md +23 -4
  106. package/pennyfarthing-dist/agents/sm-finish.md +19 -2
  107. package/pennyfarthing-dist/agents/sm-setup.md +7 -7
  108. package/pennyfarthing-dist/agents/sm.md +12 -12
  109. package/pennyfarthing-dist/agents/tea.md +2 -2
  110. package/pennyfarthing-dist/agents/testing-runner.md +1 -1
  111. package/pennyfarthing-dist/commands/pf-architect.md +1 -1
  112. package/pennyfarthing-dist/commands/pf-ba.md +1 -1
  113. package/pennyfarthing-dist/commands/pf-chore.md +2 -2
  114. package/pennyfarthing-dist/commands/pf-dev.md +1 -1
  115. package/pennyfarthing-dist/commands/pf-devops.md +1 -1
  116. package/pennyfarthing-dist/commands/pf-epic.md +6 -6
  117. package/pennyfarthing-dist/commands/pf-git.md +12 -10
  118. package/pennyfarthing-dist/commands/pf-health-check.md +1 -1
  119. package/pennyfarthing-dist/commands/pf-help.md +12 -12
  120. package/pennyfarthing-dist/commands/pf-orchestrator.md +1 -1
  121. package/pennyfarthing-dist/commands/pf-pm.md +1 -1
  122. package/pennyfarthing-dist/commands/pf-prime.md +8 -8
  123. package/pennyfarthing-dist/commands/pf-reviewer.md +1 -1
  124. package/pennyfarthing-dist/commands/pf-session.md +7 -7
  125. package/pennyfarthing-dist/commands/pf-sm.md +1 -1
  126. package/pennyfarthing-dist/commands/pf-sprint.md +7 -7
  127. package/pennyfarthing-dist/commands/pf-tea.md +1 -1
  128. package/pennyfarthing-dist/commands/pf-tech-writer.md +1 -1
  129. package/pennyfarthing-dist/commands/pf-theme.md +9 -9
  130. package/pennyfarthing-dist/commands/pf-ux-designer.md +1 -1
  131. package/pennyfarthing-dist/commands/pf-work.md +1 -1
  132. package/pennyfarthing-dist/gates/approval.md +63 -0
  133. package/pennyfarthing-dist/gates/confidence-sm.md +71 -0
  134. package/pennyfarthing-dist/gates/context-ok.md +56 -0
  135. package/pennyfarthing-dist/gates/evaluations/confidence-sm.md +54 -0
  136. package/pennyfarthing-dist/gates/quality-pass.md +67 -0
  137. package/pennyfarthing-dist/gates/tests-fail.md +84 -0
  138. package/pennyfarthing-dist/gates/tests-pass.md +79 -0
  139. package/pennyfarthing-dist/guides/agent-behavior.md +84 -29
  140. package/pennyfarthing-dist/guides/agent-coordination.md +10 -10
  141. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -6
  142. package/pennyfarthing-dist/guides/agent-template-tactical.md +1 -1
  143. package/pennyfarthing-dist/guides/bell-mode.md +1 -1
  144. package/pennyfarthing-dist/guides/bikerack.md +10 -10
  145. package/pennyfarthing-dist/guides/brownfield-tools.md +24 -24
  146. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +1 -1
  147. package/pennyfarthing-dist/guides/gate-schema.md +2 -2
  148. package/pennyfarthing-dist/guides/gates.md +3 -3
  149. package/pennyfarthing-dist/guides/handoff-cli.md +8 -8
  150. package/pennyfarthing-dist/guides/hooks.md +29 -29
  151. package/pennyfarthing-dist/guides/prime.md +2 -2
  152. package/pennyfarthing-dist/guides/reflector.md +1 -1
  153. package/pennyfarthing-dist/guides/skill-schema.md +6 -6
  154. package/pennyfarthing-dist/guides/tandem-protocol.md +3 -3
  155. package/pennyfarthing-dist/guides/workflow-schema.md +1 -1
  156. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  157. package/pennyfarthing-dist/guides/xml-tags.md +8 -8
  158. package/pennyfarthing-dist/scripts/README.md +4 -4
  159. package/pennyfarthing-dist/scripts/core/agent-session.sh +2 -5
  160. package/pennyfarthing-dist/scripts/core/check-context.sh +3 -1
  161. package/pennyfarthing-dist/scripts/core/pf.sh +5 -0
  162. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +4 -89
  163. package/pennyfarthing-dist/scripts/core/prime.sh +2 -25
  164. package/pennyfarthing-dist/scripts/git/README.md +14 -14
  165. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +2 -3
  166. package/pennyfarthing-dist/scripts/git/git-status-all.sh +2 -3
  167. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -3
  168. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +2 -4
  169. package/pennyfarthing-dist/scripts/hooks/README.md +6 -6
  170. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +4 -183
  171. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +4 -95
  172. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +4 -65
  173. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +3 -31
  174. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +5 -4
  175. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +29 -34
  176. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +4 -71
  177. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +3 -19
  178. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +4 -30
  179. package/pennyfarthing-dist/scripts/hooks/session-start.sh +3 -32
  180. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +4 -65
  181. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +4 -78
  182. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +3 -93
  183. package/pennyfarthing-dist/scripts/lib/env.sh +34 -0
  184. package/pennyfarthing-dist/scripts/lib/run-pf.sh +39 -0
  185. package/pennyfarthing-dist/scripts/misc/README.md +1 -1
  186. package/pennyfarthing-dist/scripts/misc/statusline.sh +4 -301
  187. package/pennyfarthing-dist/scripts/sprint/README.md +21 -21
  188. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  189. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +2 -16
  190. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +3 -3
  191. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +3 -3
  192. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +3 -3
  193. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +3 -3
  194. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +3 -3
  195. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +3 -3
  196. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +3 -3
  197. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +3 -3
  198. package/pennyfarthing-dist/skills/pf-bc/examples.md +23 -23
  199. package/pennyfarthing-dist/skills/pf-bc/skill.md +17 -17
  200. package/pennyfarthing-dist/skills/pf-bc/usage.md +8 -8
  201. package/pennyfarthing-dist/skills/pf-jira/SKILL.md +15 -15
  202. package/pennyfarthing-dist/skills/pf-jira/examples.md +48 -48
  203. package/pennyfarthing-dist/skills/pf-jira/usage.md +15 -15
  204. package/pennyfarthing-dist/skills/pf-sprint/examples.md +80 -80
  205. package/pennyfarthing-dist/skills/pf-sprint/skill.md +35 -35
  206. package/pennyfarthing-dist/skills/pf-sprint/usage.md +30 -30
  207. package/pennyfarthing-dist/skills/pf-theme/examples.md +15 -15
  208. package/pennyfarthing-dist/skills/pf-theme/skill.md +6 -6
  209. package/pennyfarthing-dist/skills/pf-theme/usage.md +5 -5
  210. package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -27
  211. package/pennyfarthing-dist/skills/pf-workflow/skill.md +11 -11
  212. package/pennyfarthing-dist/skills/pf-workflow/usage.md +11 -11
  213. package/pennyfarthing-dist/skills/skill-registry.yaml +19 -19
  214. package/pennyfarthing-dist/templates/settings.local.json.template +19 -10
  215. package/pennyfarthing-dist/workflows/bdd-team.yaml +89 -0
  216. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +1 -1
  217. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
  218. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
  219. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  220. package/pennyfarthing-dist/workflows/project-setup/steps/step-01-discover.md +47 -0
  221. package/pennyfarthing-dist/workflows/tdd-team.yaml +80 -0
  222. package/pennyfarthing-dist/workflows/tdd.yaml +11 -2
  223. package/pennyfarthing_scripts/CLAUDE.md +19 -10
  224. package/pennyfarthing_scripts/__init__.py +1 -1
  225. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/bc/__pycache__/split.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/bc/cli.py +2 -2
  237. package/pennyfarthing_scripts/bellmode_hook.py +9 -296
  238. package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
  239. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  240. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  241. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  242. package/pennyfarthing_scripts/bikerack/__pycache__/context_meter_footer.cpython-314.pyc +0 -0
  243. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  244. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/bikerack/__pycache__/events.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/bikerack/audit_log_panel.py +161 -0
  256. package/pennyfarthing_scripts/bikerack/base_panel.py +27 -4
  257. package/pennyfarthing_scripts/bikerack/changed_panel.py +96 -4
  258. package/pennyfarthing_scripts/bikerack/context_meter_footer.py +88 -0
  259. package/pennyfarthing_scripts/bikerack/debug_panel.py +1 -1
  260. package/pennyfarthing_scripts/bikerack/diffs_panel.py +30 -0
  261. package/pennyfarthing_scripts/bikerack/events.py +28 -0
  262. package/pennyfarthing_scripts/bikerack/launcher.py +6 -6
  263. package/pennyfarthing_scripts/bikerack/portrait_resolver.py +139 -0
  264. package/pennyfarthing_scripts/bikerack/progress_panel.py +0 -1
  265. package/pennyfarthing_scripts/bikerack/sprint_panel.py +373 -142
  266. package/pennyfarthing_scripts/bikerack/story_detail_data.py +247 -0
  267. package/pennyfarthing_scripts/bikerack/story_detail_screen.py +177 -0
  268. package/pennyfarthing_scripts/bikerack/tui.py +304 -62
  269. package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
  270. package/pennyfarthing_scripts/cli.py +5 -0
  271. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/common/config.py +29 -2
  273. package/pennyfarthing_scripts/common/pr_config.py +38 -0
  274. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/consultation/cli.py +3 -3
  277. package/pennyfarthing_scripts/context.py +3 -3
  278. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/git/hooks_installer.py +2 -3
  283. package/pennyfarthing_scripts/git/status_all.py +1 -1
  284. package/pennyfarthing_scripts/git/worktree.py +2 -2
  285. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  289. package/pennyfarthing_scripts/handoff/__pycache__/phase_check.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  291. package/pennyfarthing_scripts/handoff/cli.py +33 -1
  292. package/pennyfarthing_scripts/handoff/complete_phase.py +28 -0
  293. package/pennyfarthing_scripts/handoff/marker.py +15 -15
  294. package/pennyfarthing_scripts/handoff/phase_check.py +96 -0
  295. package/pennyfarthing_scripts/handoff/resolve_gate.py +13 -1
  296. package/pennyfarthing_scripts/hooks/__init__.py +442 -0
  297. package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  298. package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  304. package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  305. package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  306. package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  307. package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  309. package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/hooks/bell_mode.py +214 -0
  311. package/pennyfarthing_scripts/hooks/cli.py +96 -0
  312. package/pennyfarthing_scripts/hooks/context_breaker.py +104 -0
  313. package/pennyfarthing_scripts/hooks/context_warning.py +66 -0
  314. package/pennyfarthing_scripts/hooks/cyclist_pretooluse.py +129 -0
  315. package/pennyfarthing_scripts/hooks/pre_edit_check.py +77 -0
  316. package/pennyfarthing_scripts/hooks/reflector_check.py +270 -0
  317. package/pennyfarthing_scripts/hooks/schema_validation.py +202 -0
  318. package/pennyfarthing_scripts/hooks/session_start.py +294 -0
  319. package/pennyfarthing_scripts/hooks/session_stop.py +111 -0
  320. package/pennyfarthing_scripts/hooks/sprint_yaml_validation.py +97 -0
  321. package/pennyfarthing_scripts/hooks/statusline.py +429 -0
  322. package/pennyfarthing_scripts/hooks.py +27 -432
  323. package/pennyfarthing_scripts/pretooluse_hook.py +3 -185
  324. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/prime/heatmap.py +3 -15
  326. package/pennyfarthing_scripts/prime/workflow.py +2 -1
  327. package/pennyfarthing_scripts/schema_validation_hook.py +3 -298
  328. package/pennyfarthing_scripts/session_start_hook.py +4 -186
  329. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  330. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  331. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  332. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  333. package/pennyfarthing_scripts/sprint/cli.py +121 -0
  334. package/pennyfarthing_scripts/sprint/loader.py +154 -3
  335. package/pennyfarthing_scripts/sprint/story_update.py +26 -0
  336. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  337. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  338. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_list_team.cpython-314-pytest-9.0.2.pyc +0 -0
  339. package/pennyfarthing_scripts/tests/test_bikerack.py +26 -26
  340. package/pennyfarthing_scripts/tests/test_dialogue_manager.py +0 -1
  341. package/pennyfarthing_scripts/tests/test_sprint_panel.py +344 -265
  342. package/pennyfarthing_scripts/tests/test_workflow_list_team.py +147 -0
  343. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  344. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  345. package/pennyfarthing_scripts/validate/adapters/__pycache__/tandem_awareness.cpython-314.pyc +0 -0
  346. package/pennyfarthing_scripts/validate/adapters/__pycache__/team_mode.cpython-314.pyc +0 -0
  347. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  348. package/pennyfarthing_scripts/validate/adapters/team_mode.py +323 -0
  349. package/pennyfarthing_scripts/validate/adapters/workflow.py +19 -0
  350. package/pennyfarthing_scripts/welcome_hook.py +3 -149
  351. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  352. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  353. package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  354. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  355. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
  356. package/pennyfarthing_scripts/workflow/__pycache__/team_lifecycle.cpython-314.pyc +0 -0
  357. package/pennyfarthing_scripts/workflow/cli.py +22 -20
  358. package/pennyfarthing_scripts/workflow/state.py +0 -1
  359. package/pennyfarthing_scripts/workflow/team_lifecycle.py +256 -0
  360. package/packages/core/dist/cli/cyclist-migration.test.d.ts +0 -16
  361. package/packages/core/dist/cli/cyclist-migration.test.d.ts.map +0 -1
  362. package/packages/core/dist/cli/cyclist-migration.test.js +0 -229
  363. package/packages/core/dist/cli/cyclist-migration.test.js.map +0 -1
  364. package/packages/core/dist/scripts/theme-detail.test.d.ts +0 -10
  365. package/packages/core/dist/scripts/theme-detail.test.d.ts.map +0 -1
  366. package/packages/core/dist/scripts/theme-detail.test.js +0 -199
  367. package/packages/core/dist/scripts/theme-detail.test.js.map +0 -1
@@ -0,0 +1,442 @@
1
+ """
2
+ Shared utilities for Pennyfarthing Claude Code hooks.
3
+
4
+ Provides common functionality for all hooks:
5
+ - Project root detection
6
+ - Port file discovery
7
+ - Settings loading (relay_mode, permission_mode)
8
+ - Context state checking
9
+ - HTTP communication with Cyclist
10
+
11
+ All hooks should import from this module for consistency.
12
+
13
+ Story: MSSCI-12409 - Hook consistency and relay mode compatibility
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import sys
19
+ import urllib.error
20
+ import urllib.request
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import yaml
26
+
27
+ # =============================================================================
28
+ # Port File Constants
29
+ # =============================================================================
30
+
31
+ # WheelHub port file - central coordination server for all communication
32
+ # Per ADR-0004: "the hub where all communication converges"
33
+ CYCLIST_PORT_FILE = ".bikerack-port"
34
+
35
+ # Default port if file not found
36
+ DEFAULT_CYCLIST_PORT = 7431
37
+
38
+ # HTTP timeout for Cyclist communication
39
+ HTTP_TIMEOUT_SECONDS = 120
40
+
41
+
42
+ # =============================================================================
43
+ # Project Root Detection
44
+ # =============================================================================
45
+
46
+
47
+ def find_project_root(start_dir: Path | None = None) -> Path | None:
48
+ """Find the project root by looking for marker files.
49
+
50
+ Searches for (in order):
51
+ 1. .bikerack-port (WheelHub is running)
52
+ 2. .pennyfarthing directory
53
+ 3. .claude directory
54
+
55
+ Args:
56
+ start_dir: Directory to start search from (defaults to cwd)
57
+
58
+ Returns:
59
+ Path to project root, or None if not found
60
+ """
61
+ current = Path(start_dir) if start_dir else Path.cwd()
62
+ current = current.resolve()
63
+
64
+ while current != current.parent:
65
+ # Check for Cyclist port files first (indicates Cyclist is running)
66
+ if (current / CYCLIST_PORT_FILE).exists():
67
+ return current
68
+ # Fall back to directory markers
69
+ if (current / ".pennyfarthing").is_dir():
70
+ return current
71
+ if (current / ".claude").is_dir():
72
+ return current
73
+ current = current.parent
74
+
75
+ return None
76
+
77
+
78
+ # =============================================================================
79
+ # Port File Reading
80
+ # =============================================================================
81
+
82
+
83
+ def read_port_file(file_name: str, project_root: Path | None = None) -> int | None:
84
+ """Read a port number from a Cyclist port file.
85
+
86
+ Args:
87
+ file_name: Name of the port file (e.g. .bikerack-port)
88
+ project_root: Project root directory (auto-detected if not provided)
89
+
90
+ Returns:
91
+ Port number, or None if file not found or invalid
92
+ """
93
+ root = project_root or find_project_root()
94
+ if not root:
95
+ return None
96
+
97
+ port_file = root / file_name
98
+ if not port_file.exists():
99
+ return None
100
+
101
+ try:
102
+ content = port_file.read_text().strip()
103
+ port = int(content)
104
+ if 0 < port < 65536:
105
+ return port
106
+ except (ValueError, OSError):
107
+ pass
108
+
109
+ return None
110
+
111
+
112
+ def get_cyclist_port(project_root: Path | None = None) -> int:
113
+ """Get the WheelHub server port.
114
+
115
+ WheelHub is the central coordination server for all Cyclist communication,
116
+ including hook requests, OTEL, REST APIs, and WebSocket.
117
+
118
+ Args:
119
+ project_root: Project root directory (auto-detected if not provided)
120
+
121
+ Returns:
122
+ Port number (default if file not found)
123
+ """
124
+ port = read_port_file(CYCLIST_PORT_FILE, project_root)
125
+ if port:
126
+ return port
127
+
128
+ return DEFAULT_CYCLIST_PORT
129
+
130
+
131
+ # =============================================================================
132
+ # Settings Loading
133
+ # =============================================================================
134
+
135
+
136
+ @dataclass
137
+ class CyclistSettings:
138
+ """Cyclist workflow settings from config.local.yaml."""
139
+
140
+ permission_mode: str = "manual" # plan, manual, accept
141
+ relay_mode: bool = False
142
+ bell_mode: bool = False
143
+ git_monitor: bool = False
144
+ theme: str | None = None
145
+
146
+
147
+ def load_settings(project_root: Path | None = None) -> CyclistSettings:
148
+ """Load Cyclist settings from .pennyfarthing/config.local.yaml.
149
+
150
+ Handles legacy setting migrations:
151
+ - permission_mode: 'turbo' -> 'accept' + relay_mode: True
152
+ - handoff_mode: 'auto' -> relay_mode: True
153
+ - auto_handoff: True -> relay_mode: True
154
+
155
+ Args:
156
+ project_root: Project root directory (auto-detected if not provided)
157
+
158
+ Returns:
159
+ CyclistSettings with current configuration
160
+ """
161
+ settings = CyclistSettings()
162
+
163
+ root = project_root or find_project_root()
164
+ if not root:
165
+ return settings
166
+
167
+ config_path = root / ".pennyfarthing" / "config.local.yaml"
168
+ if not config_path.exists():
169
+ return settings
170
+
171
+ try:
172
+ with open(config_path) as f:
173
+ config = yaml.safe_load(f) or {}
174
+ except (OSError, yaml.YAMLError):
175
+ return settings
176
+
177
+ # Extract theme
178
+ settings.theme = config.get("theme")
179
+
180
+ # Extract workflow settings
181
+ workflow = config.get("workflow", {})
182
+ if not isinstance(workflow, dict):
183
+ return settings
184
+
185
+ # Handle permission_mode
186
+ mode = workflow.get("permission_mode", "manual")
187
+ if mode == "turbo":
188
+ # Migrate turbo -> accept + relay_mode
189
+ settings.permission_mode = "accept"
190
+ settings.relay_mode = True
191
+ elif mode in ("plan", "manual", "accept"):
192
+ settings.permission_mode = mode
193
+ else:
194
+ settings.permission_mode = "manual"
195
+
196
+ # Handle explicit relay_mode (overrides migration)
197
+ if "relay_mode" in workflow and isinstance(workflow["relay_mode"], bool):
198
+ settings.relay_mode = workflow["relay_mode"]
199
+ elif not settings.relay_mode:
200
+ # Check legacy settings
201
+ if workflow.get("handoff_mode") == "auto":
202
+ settings.relay_mode = True
203
+ elif workflow.get("auto_handoff") is True:
204
+ settings.relay_mode = True
205
+
206
+ # Handle bell_mode
207
+ if "bell_mode" in workflow and isinstance(workflow["bell_mode"], bool):
208
+ settings.bell_mode = workflow["bell_mode"]
209
+
210
+ # Handle git_monitor
211
+ if "git_monitor" in workflow and isinstance(workflow["git_monitor"], bool):
212
+ settings.git_monitor = workflow["git_monitor"]
213
+
214
+ return settings
215
+
216
+
217
+ def is_relay_mode_enabled(project_root: Path | None = None) -> bool:
218
+ """Check if relay mode (auto-handoff) is enabled.
219
+
220
+ Args:
221
+ project_root: Project root directory (auto-detected if not provided)
222
+
223
+ Returns:
224
+ True if relay mode is enabled
225
+ """
226
+ return load_settings(project_root).relay_mode
227
+
228
+
229
+ def is_bell_mode_enabled(project_root: Path | None = None) -> bool:
230
+ """Check if bell mode is enabled.
231
+
232
+ Args:
233
+ project_root: Project root directory (auto-detected if not provided)
234
+
235
+ Returns:
236
+ True if bell mode is enabled
237
+ """
238
+ return load_settings(project_root).bell_mode
239
+
240
+
241
+ # =============================================================================
242
+ # Context State
243
+ # =============================================================================
244
+
245
+
246
+ @dataclass
247
+ class ContextState:
248
+ """Current context usage state."""
249
+
250
+ used_tokens: int = 0
251
+ max_tokens: int = 200000
252
+ percentage: float = 0.0
253
+ is_high: bool = False # > 60%
254
+ is_critical: bool = False # > 80%
255
+
256
+
257
+ def get_context_state(project_root: Path | None = None) -> ContextState:
258
+ """Get current context usage from Cyclist API.
259
+
260
+ Calls Cyclist's /api/context endpoint which runs check-context.sh.
261
+
262
+ Args:
263
+ project_root: Project root directory (auto-detected if not provided)
264
+
265
+ Returns:
266
+ ContextState with current usage (defaults if Cyclist not running)
267
+ """
268
+ state = ContextState()
269
+
270
+ port = get_cyclist_port(project_root)
271
+ url = f"http://127.0.0.1:{port}/api/context"
272
+
273
+ try:
274
+ with urllib.request.urlopen(url, timeout=5) as response:
275
+ data = json.loads(response.read().decode())
276
+ state.used_tokens = data.get("used_tokens", 0)
277
+ state.max_tokens = data.get("max_tokens", 200000)
278
+ if state.max_tokens > 0:
279
+ state.percentage = (state.used_tokens / state.max_tokens) * 100
280
+ state.is_high = state.percentage > 60
281
+ state.is_critical = state.percentage > 80
282
+ except (urllib.error.URLError, json.JSONDecodeError, OSError):
283
+ # Cyclist not running or error - return defaults
284
+ pass
285
+
286
+ return state
287
+
288
+
289
+ # =============================================================================
290
+ # Cyclist HTTP Communication
291
+ # =============================================================================
292
+
293
+
294
+ def send_to_cyclist(
295
+ endpoint: str,
296
+ data: dict[str, Any],
297
+ port: int | None = None,
298
+ project_root: Path | None = None,
299
+ timeout: int = HTTP_TIMEOUT_SECONDS,
300
+ ) -> dict[str, Any] | None:
301
+ """Send a POST request to WheelHub (Cyclist's central coordination server).
302
+
303
+ All endpoints go through WheelHub per ADR-0004.
304
+
305
+ Args:
306
+ endpoint: API endpoint path (e.g., "/api/hook-request")
307
+ data: JSON data to send
308
+ port: Port to use (auto-detected if not provided)
309
+ project_root: Project root for port discovery
310
+ timeout: Request timeout in seconds
311
+
312
+ Returns:
313
+ Response JSON as dict, or None on error
314
+ """
315
+ if port is None:
316
+ port = get_cyclist_port(project_root)
317
+
318
+ url = f"http://127.0.0.1:{port}{endpoint}"
319
+ json_data = json.dumps(data).encode("utf-8")
320
+
321
+ request = urllib.request.Request(
322
+ url,
323
+ data=json_data,
324
+ headers={"Content-Type": "application/json"},
325
+ method="POST",
326
+ )
327
+
328
+ try:
329
+ with urllib.request.urlopen(request, timeout=timeout) as response:
330
+ return json.loads(response.read().decode())
331
+ except urllib.error.URLError as e:
332
+ # Connection refused means Cyclist isn't running
333
+ if "Connection refused" in str(e):
334
+ return None
335
+ raise
336
+ except (json.JSONDecodeError, OSError):
337
+ return None
338
+
339
+
340
+ # =============================================================================
341
+ # Hook Response Formatting
342
+ # =============================================================================
343
+
344
+
345
+ @dataclass
346
+ class HookResponse:
347
+ """Standard hook response for Claude Code."""
348
+
349
+ event_name: str
350
+ decision: str | None = None # allow, deny, ask (for PreToolUse)
351
+ reason: str | None = None
352
+ updated_input: dict[str, Any] | None = None
353
+ additional_context: str | None = None # For PostToolUse context injection
354
+
355
+ def to_json(self) -> str:
356
+ """Format as Claude Code hook JSON output."""
357
+ output: dict[str, Any] = {
358
+ "hookSpecificOutput": {
359
+ "hookEventName": self.event_name,
360
+ }
361
+ }
362
+
363
+ hook_output = output["hookSpecificOutput"]
364
+
365
+ if self.decision:
366
+ hook_output["permissionDecision"] = self.decision
367
+ if self.reason:
368
+ hook_output["permissionDecisionReason"] = self.reason
369
+ if self.updated_input:
370
+ hook_output["updatedInput"] = self.updated_input
371
+ if self.additional_context:
372
+ hook_output["additionalContext"] = self.additional_context
373
+
374
+ return json.dumps(output)
375
+
376
+
377
+ def output_hook_response(response: HookResponse) -> None:
378
+ """Output hook response to stdout for Claude Code."""
379
+ print(response.to_json())
380
+
381
+
382
+ def read_stdin_json() -> dict[str, Any]:
383
+ """Read JSON from stdin (hook input from Claude Code).
384
+
385
+ Returns:
386
+ Parsed JSON as dict
387
+
388
+ Raises:
389
+ ValueError: If input is not valid JSON
390
+ """
391
+ data = sys.stdin.read()
392
+ try:
393
+ return json.loads(data)
394
+ except json.JSONDecodeError as e:
395
+ raise ValueError(f"Invalid JSON input: {e}") from e
396
+
397
+
398
+ # =============================================================================
399
+ # Hook Execution Utilities
400
+ # =============================================================================
401
+
402
+
403
+ def is_cyclist_running(project_root: Path | None = None) -> bool:
404
+ """Check if Cyclist server is running.
405
+
406
+ Checks the CYCLIST environment variable set by ClaudeService when
407
+ spawning Claude inside Cyclist. No file I/O, no HTTP, no signals —
408
+ this runs on every tool invocation and must be instant.
409
+
410
+ The project_root parameter is kept for backward compatibility but
411
+ is no longer used.
412
+
413
+ Returns:
414
+ True if running inside a Cyclist-spawned Claude process
415
+ """
416
+ return os.environ.get("CYCLIST") == "1"
417
+
418
+
419
+ def should_auto_approve(settings: CyclistSettings) -> bool:
420
+ """Check if requests should be auto-approved based on settings.
421
+
422
+ Auto-approve when permission_mode is 'accept' (formerly turbo).
423
+
424
+ Args:
425
+ settings: Current Cyclist settings
426
+
427
+ Returns:
428
+ True if auto-approval is enabled
429
+ """
430
+ return settings.permission_mode == "accept"
431
+
432
+
433
+ def should_auto_handoff(settings: CyclistSettings) -> bool:
434
+ """Check if handoffs should be automatic based on settings.
435
+
436
+ Args:
437
+ settings: Current Cyclist settings
438
+
439
+ Returns:
440
+ True if relay_mode is enabled
441
+ """
442
+ return settings.relay_mode
@@ -0,0 +1,214 @@
1
+ """
2
+ PostToolUse Hook — Bell Mode + Tandem Injection.
3
+
4
+ Called by Claude Code after each tool execution. Handles two independent
5
+ injection systems:
6
+
7
+ 1. Bell queue (Cyclist only) — injects queued user messages when Cyclist
8
+ is running and bell_mode is enabled. In CLI sessions this is a no-op.
9
+ 2. Tandem observations (always active) — injects backseat agent observations
10
+ when tandem observation files exist. No configuration required.
11
+
12
+ Bell queue takes precedence: if a queued message exists, tandem is
13
+ deferred to the next hook invocation.
14
+
15
+ Consolidates bellmode_hook.py into the hooks subpackage.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import re
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ from pennyfarthing_scripts.hooks import (
26
+ CYCLIST_PORT_FILE,
27
+ HookResponse,
28
+ find_project_root,
29
+ is_bell_mode_enabled,
30
+ output_hook_response,
31
+ read_port_file,
32
+ send_to_cyclist,
33
+ )
34
+
35
+ # =============================================================================
36
+ # Bell Queue
37
+ # =============================================================================
38
+
39
+
40
+ def _read_bell_queue(project_root: Path) -> list[dict]:
41
+ queue_path = project_root / ".pennyfarthing" / "bell-queue.json"
42
+ if not queue_path.exists():
43
+ return []
44
+ try:
45
+ with open(queue_path) as f:
46
+ queue = json.load(f)
47
+ if isinstance(queue, list):
48
+ return queue
49
+ except (json.JSONDecodeError, OSError):
50
+ pass
51
+ return []
52
+
53
+
54
+ def _dequeue_message(project_root: Path) -> None:
55
+ queue_path = project_root / ".pennyfarthing" / "bell-queue.json"
56
+ if not queue_path.exists():
57
+ return
58
+ try:
59
+ with open(queue_path) as f:
60
+ queue = json.load(f)
61
+ if isinstance(queue, list) and len(queue) > 0:
62
+ queue = queue[1:]
63
+ with open(queue_path, "w") as f:
64
+ json.dump(queue, f)
65
+ except (json.JSONDecodeError, OSError):
66
+ pass
67
+
68
+
69
+ def _notify_cyclist(project_root: Path, message_text: str) -> None:
70
+ try:
71
+ send_to_cyclist(
72
+ endpoint="/api/bell-consumed",
73
+ data={"text": message_text},
74
+ project_root=project_root,
75
+ timeout=5,
76
+ )
77
+ except Exception:
78
+ pass
79
+
80
+
81
+ # =============================================================================
82
+ # Tandem Observations
83
+ # =============================================================================
84
+
85
+
86
+ def _read_tandem_observations(project_root: Path) -> list[Path]:
87
+ session_dir = project_root / ".session"
88
+ if not session_dir.is_dir():
89
+ return []
90
+ return sorted(session_dir.glob("*-tandem-*.md"))
91
+
92
+
93
+ def _get_latest_observation(file_content: str) -> dict | None:
94
+ persona_match = re.search(r"\*\*Observer:\*\*\s*\w+\s*\(([^)]+)\)", file_content)
95
+ persona = persona_match.group(1) if persona_match else "Unknown"
96
+
97
+ entries = re.split(r"## \[\d{1,2}:\d{2}\] Observation\n", file_content)
98
+ if len(entries) < 2:
99
+ return None
100
+
101
+ last_entry = entries[-1].strip()
102
+ last_entry = re.sub(r"\n---\s*$", "", last_entry).strip()
103
+ lines = last_entry.split("\n")
104
+ text_lines = [line for line in lines if not line.startswith("**Trigger:**")]
105
+ text = "\n".join(text_lines).strip()
106
+
107
+ if not text:
108
+ return None
109
+
110
+ return {"persona": persona, "text": text}
111
+
112
+
113
+ def _get_tandem_mtime(project_root: Path, agent: str) -> float:
114
+ sidecar = project_root / ".session" / f".tandem-mtime-{agent}"
115
+ if not sidecar.exists():
116
+ return 0.0
117
+ try:
118
+ return float(sidecar.read_text().strip())
119
+ except (ValueError, OSError):
120
+ return 0.0
121
+
122
+
123
+ def _save_tandem_mtime(project_root: Path, agent: str, mtime: float) -> None:
124
+ sidecar = project_root / ".session" / f".tandem-mtime-{agent}"
125
+ try:
126
+ sidecar.write_text(str(mtime))
127
+ except OSError:
128
+ pass
129
+
130
+
131
+ def _check_tandem_files(project_root: Path) -> list[dict]:
132
+ tandem_files = _read_tandem_observations(project_root)
133
+ if not tandem_files:
134
+ return []
135
+
136
+ results = []
137
+ for obs_file in tandem_files:
138
+ agent_match = re.search(r"-tandem-(\w+)\.md$", obs_file.name)
139
+ if not agent_match:
140
+ continue
141
+ agent = agent_match.group(1)
142
+
143
+ try:
144
+ file_mtime = obs_file.stat().st_mtime
145
+ except OSError:
146
+ continue
147
+ saved_mtime = _get_tandem_mtime(project_root, agent)
148
+ if file_mtime == saved_mtime:
149
+ continue
150
+
151
+ try:
152
+ content = obs_file.read_text()
153
+ except OSError:
154
+ continue
155
+ obs = _get_latest_observation(content)
156
+ if not obs:
157
+ _save_tandem_mtime(project_root, agent, file_mtime)
158
+ continue
159
+
160
+ message = f"[Tandem] {obs['persona']}: {obs['text']}"
161
+ results.append({"agent": agent, "message": message})
162
+ _save_tandem_mtime(project_root, agent, file_mtime)
163
+
164
+ return results
165
+
166
+
167
+ # =============================================================================
168
+ # Entry Point
169
+ # =============================================================================
170
+
171
+
172
+ def main() -> None:
173
+ """Main entry point for PostToolUse hook."""
174
+ try:
175
+ # Read and discard stdin (required by hook protocol)
176
+ sys.stdin.read()
177
+
178
+ project_root = find_project_root()
179
+ if not project_root:
180
+ sys.exit(0)
181
+
182
+ # --- Bell queue (Cyclist only, requires bell_mode: true) ---
183
+ is_cyclist = read_port_file(CYCLIST_PORT_FILE, project_root) is not None
184
+ if is_cyclist and is_bell_mode_enabled(project_root):
185
+ queue = _read_bell_queue(project_root)
186
+ if queue:
187
+ first_message = queue[0]
188
+ message_text = first_message.get("text", "")
189
+ if message_text:
190
+ output_hook_response(HookResponse(
191
+ event_name="PostToolUse",
192
+ additional_context=f"User feedback: {message_text}",
193
+ ))
194
+ _dequeue_message(project_root)
195
+ _notify_cyclist(project_root, message_text)
196
+ sys.exit(0)
197
+
198
+ # --- Tandem observations (always active) ---
199
+ tandem_results = _check_tandem_files(project_root)
200
+ if tandem_results:
201
+ output_hook_response(HookResponse(
202
+ event_name="PostToolUse",
203
+ additional_context=tandem_results[0]["message"],
204
+ ))
205
+
206
+ sys.exit(0)
207
+
208
+ except Exception as e:
209
+ print(f"[bellmode-hook] Error: {e}", file=sys.stderr)
210
+ sys.exit(0)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()