@pennyfarthing/core 10.1.0 → 10.3.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 (422) hide show
  1. package/README.md +22 -24
  2. package/package.json +3 -1
  3. package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js +24 -0
  5. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +101 -15
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js +2 -2
  10. package/packages/core/dist/cli/commands/e2e-fresh-install.test.js.map +1 -1
  11. package/packages/core/dist/cli/commands/e2e-upgrade.test.js +2 -2
  12. package/packages/core/dist/cli/commands/e2e-upgrade.test.js.map +1 -1
  13. package/packages/core/dist/cli/commands/hooks-consolidation.test.js +2 -2
  14. package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -1
  15. package/packages/core/dist/cli/commands/init-consolidation.test.js.map +1 -1
  16. package/packages/core/dist/cli/commands/theme.js +1 -1
  17. package/packages/core/dist/cli/commands/theme.js.map +1 -1
  18. package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
  19. package/packages/core/dist/cli/commands/uninstall.js +24 -13
  20. package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
  21. package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
  22. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  23. package/packages/core/dist/cli/commands/update.js.map +1 -1
  24. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  25. package/packages/core/dist/cli/theme-maker.test.js +64 -115
  26. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  27. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
  28. package/packages/core/dist/cli/utils/themes.js +3 -2
  29. package/packages/core/dist/cli/utils/themes.js.map +1 -1
  30. package/packages/core/dist/index.d.ts +1 -1
  31. package/packages/core/dist/index.d.ts.map +1 -1
  32. package/packages/core/dist/index.js +2 -2
  33. package/packages/core/dist/index.js.map +1 -1
  34. package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
  35. package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
  36. package/packages/core/dist/plugins/plugin-discovery.js +165 -0
  37. package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
  38. package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
  39. package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
  40. package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
  41. package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
  42. package/packages/core/dist/scripts/add-ocean-profiles.js +1 -1
  43. package/packages/core/dist/scripts/add-ocean-profiles.js.map +1 -1
  44. package/packages/core/dist/scripts/generate-all-spiders.js +2 -0
  45. package/packages/core/dist/scripts/generate-all-spiders.js.map +1 -1
  46. package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
  47. package/packages/core/dist/scripts/generate-report.js +2 -0
  48. package/packages/core/dist/scripts/generate-report.js.map +1 -1
  49. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
  50. package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
  51. package/packages/core/dist/scripts/generate-spider.js +2 -0
  52. package/packages/core/dist/scripts/generate-spider.js.map +1 -1
  53. package/packages/core/dist/scripts/validate-ocean-profiles.js +1 -1
  54. package/packages/core/dist/scripts/validate-ocean-profiles.js.map +1 -1
  55. package/packages/core/dist/workflow/file-watch.d.ts +82 -0
  56. package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
  57. package/packages/core/dist/workflow/file-watch.js +198 -0
  58. package/packages/core/dist/workflow/file-watch.js.map +1 -0
  59. package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
  60. package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
  61. package/packages/core/dist/workflow/file-watch.test.js +469 -0
  62. package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
  63. package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
  64. package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
  65. package/packages/core/dist/workflow/observation-writer.js +97 -0
  66. package/packages/core/dist/workflow/observation-writer.js.map +1 -0
  67. package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
  68. package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
  69. package/packages/core/dist/workflow/observation-writer.test.js +424 -0
  70. package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
  71. package/packages/core/dist/workflow/output-path-normalizer.d.ts +47 -0
  72. package/packages/core/dist/workflow/output-path-normalizer.d.ts.map +1 -0
  73. package/packages/core/dist/workflow/output-path-normalizer.js +79 -0
  74. package/packages/core/dist/workflow/output-path-normalizer.js.map +1 -0
  75. package/packages/core/dist/workflow/output-path-normalizer.test.d.ts +16 -0
  76. package/packages/core/dist/workflow/output-path-normalizer.test.d.ts.map +1 -0
  77. package/packages/core/dist/workflow/output-path-normalizer.test.js +157 -0
  78. package/packages/core/dist/workflow/output-path-normalizer.test.js.map +1 -0
  79. package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
  80. package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
  81. package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
  82. package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
  83. package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
  84. package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
  85. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
  86. package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
  87. package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
  88. package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
  89. package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
  90. package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
  91. package/packages/core/dist/workflow/tool-watch.js +166 -0
  92. package/packages/core/dist/workflow/tool-watch.js.map +1 -0
  93. package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
  94. package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
  95. package/packages/core/dist/workflow/tool-watch.test.js +717 -0
  96. package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
  97. package/packages/core/dist/workflow/variable-resolver.js +1 -1
  98. package/packages/core/dist/workflow/variable-resolver.js.map +1 -1
  99. package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
  100. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  101. package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
  102. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  103. package/packages/core/dist/workflow/workflow-schema.js +44 -0
  104. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  105. package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
  106. package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
  107. package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
  108. package/pennyfarthing-dist/agents/README.md +3 -1
  109. package/pennyfarthing-dist/agents/ba.md +165 -0
  110. package/pennyfarthing-dist/agents/handoff.md +18 -3
  111. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  112. package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
  113. package/pennyfarthing-dist/agents/sm.md +11 -5
  114. package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
  115. package/pennyfarthing-dist/commands/ba.md +17 -0
  116. package/pennyfarthing-dist/commands/setup.md +4 -0
  117. package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
  118. package/pennyfarthing-dist/guides/bikelane.md +3 -2
  119. package/pennyfarthing-dist/guides/scale-levels.md +4 -6
  120. package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
  121. package/pennyfarthing-dist/guides/workflow-schema.md +1 -1
  122. package/pennyfarthing-dist/personas/themes/a-team.yaml +30 -0
  123. package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +30 -0
  124. package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +30 -0
  125. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +30 -0
  126. package/pennyfarthing-dist/personas/themes/catch-22.yaml +30 -0
  127. package/pennyfarthing-dist/personas/themes/control.yaml +30 -0
  128. package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +31 -0
  129. package/pennyfarthing-dist/personas/themes/discworld.yaml +32 -1
  130. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +31 -0
  131. package/pennyfarthing-dist/personas/themes/dune.yaml +32 -0
  132. package/pennyfarthing-dist/personas/themes/fifth-element.yaml +327 -0
  133. package/pennyfarthing-dist/personas/themes/firefly.yaml +31 -0
  134. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +30 -0
  135. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +30 -0
  136. package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +30 -0
  137. package/pennyfarthing-dist/personas/themes/lord-of-the-rings.yaml +30 -0
  138. package/pennyfarthing-dist/personas/themes/mad-max.yaml +30 -0
  139. package/pennyfarthing-dist/personas/themes/mash.yaml +33 -0
  140. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +34 -0
  141. package/pennyfarthing-dist/personas/themes/sandman.yaml +33 -0
  142. package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +34 -0
  143. package/pennyfarthing-dist/personas/themes/star-wars.yaml +33 -0
  144. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +30 -0
  145. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +30 -0
  146. package/pennyfarthing-dist/personas/themes/watchmen.yaml +30 -0
  147. package/pennyfarthing-dist/personas/themes/west-wing.yaml +30 -0
  148. package/pennyfarthing-dist/personas/themes/x-files.yaml +30 -0
  149. package/pennyfarthing-dist/scripts/README.md +1 -1
  150. package/pennyfarthing-dist/scripts/core/agent-session.sh +1 -1
  151. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
  152. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +20 -10
  153. package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
  154. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +2 -2
  155. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +1 -0
  156. package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
  157. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -189
  158. package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
  159. package/pennyfarthing-dist/skills/skill-registry.yaml +1 -1
  160. package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
  161. package/pennyfarthing-dist/skills/theme/skill.md +1 -1
  162. package/pennyfarthing-dist/skills/workflow/skill.md +24 -1
  163. package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
  164. package/pennyfarthing-dist/workflows/architecture.yaml +2 -2
  165. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
  166. package/pennyfarthing-dist/workflows/epics-and-stories/workflow.yaml +2 -2
  167. package/pennyfarthing-dist/workflows/implementation-readiness/workflow.yaml +2 -2
  168. package/pennyfarthing-dist/workflows/prd/workflow.yaml +2 -2
  169. package/pennyfarthing-dist/workflows/product-brief/workflow.yaml +2 -2
  170. package/pennyfarthing-dist/workflows/project-context/workflow.yaml +2 -2
  171. package/pennyfarthing-dist/workflows/quick-dev/workflow.yaml +2 -2
  172. package/pennyfarthing-dist/workflows/research/workflow.yaml +2 -2
  173. package/pennyfarthing-dist/workflows/retrospective/workflow.yaml +1 -1
  174. package/pennyfarthing-dist/workflows/sprint-planning/workflow.yaml +3 -3
  175. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
  176. package/pennyfarthing-dist/workflows/ux-design/workflow.yaml +2 -2
  177. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/bellmode_hook.py +202 -47
  183. package/pennyfarthing_scripts/bikerack/__init__.py +36 -0
  184. package/pennyfarthing_scripts/bikerack/__main__.py +5 -0
  185. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  186. package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
  187. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  188. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/bikerack/cli.py +148 -0
  190. package/pennyfarthing_scripts/bikerack/launcher.py +181 -0
  191. package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
  192. package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
  193. package/pennyfarthing_scripts/brownfield/cli.py +0 -1
  194. package/pennyfarthing_scripts/brownfield/discover.py +1 -2
  195. package/pennyfarthing_scripts/cli.py +16 -6
  196. package/pennyfarthing_scripts/codemarkers/__init__.py +5 -1
  197. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/codemarkers/analyze.py +177 -2
  204. package/pennyfarthing_scripts/codemarkers/cli.py +50 -0
  205. package/pennyfarthing_scripts/codemarkers/formatters.py +0 -1
  206. package/pennyfarthing_scripts/codemarkers/models.py +15 -0
  207. package/pennyfarthing_scripts/common/__init__.py +8 -9
  208. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/common/config.py +1 -1
  211. package/pennyfarthing_scripts/complexity/__init__.py +1 -1
  212. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/complexity/analyze.py +1 -1
  219. package/pennyfarthing_scripts/complexity/cli.py +5 -1
  220. package/pennyfarthing_scripts/complexity/formatters.py +1 -1
  221. package/pennyfarthing_scripts/context.py +14 -15
  222. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/deadcode/analyze.py +3 -4
  229. package/pennyfarthing_scripts/deadcode/cli.py +2 -2
  230. package/pennyfarthing_scripts/dependencies/__init__.py +2 -2
  231. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  235. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/dependencies/analyze.py +1 -1
  238. package/pennyfarthing_scripts/dependencies/cli.py +8 -4
  239. package/pennyfarthing_scripts/dependencies/formatters.py +1 -1
  240. package/pennyfarthing_scripts/git/__init__.py +5 -5
  241. package/pennyfarthing_scripts/git/create_branches.py +3 -2
  242. package/pennyfarthing_scripts/git/status_all.py +1 -1
  243. package/pennyfarthing_scripts/healthscore/__init__.py +2 -2
  244. package/pennyfarthing_scripts/healthscore/__main__.py +8 -0
  245. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/healthscore/analyze.py +452 -21
  252. package/pennyfarthing_scripts/healthscore/cli.py +5 -1
  253. package/pennyfarthing_scripts/healthscore/models.py +0 -1
  254. package/pennyfarthing_scripts/hooks.py +8 -11
  255. package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
  256. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/hotspots/analyze.py +128 -14
  261. package/pennyfarthing_scripts/hotspots/cli.py +2 -2
  262. package/pennyfarthing_scripts/hotspots/models.py +0 -1
  263. package/pennyfarthing_scripts/jira/__init__.py +15 -17
  264. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/jira/bidirectional.py +2 -3
  275. package/pennyfarthing_scripts/jira/claim.py +21 -0
  276. package/pennyfarthing_scripts/jira/cli.py +2 -2
  277. package/pennyfarthing_scripts/jira/client.py +4 -4
  278. package/pennyfarthing_scripts/jira/create.py +45 -1
  279. package/pennyfarthing_scripts/jira/epic.py +3 -2
  280. package/pennyfarthing_scripts/jira/reconcile.py +0 -1
  281. package/pennyfarthing_scripts/jira/story.py +2 -0
  282. package/pennyfarthing_scripts/jira/sync.py +1 -1
  283. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/migration/skill.py +0 -1
  289. package/pennyfarthing_scripts/migration/step.py +0 -1
  290. package/pennyfarthing_scripts/migration/validate.py +8 -5
  291. package/pennyfarthing_scripts/patch_mode.py +2 -2
  292. package/pennyfarthing_scripts/preflight/__init__.py +1 -1
  293. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  294. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  295. package/pennyfarthing_scripts/preflight/finish.py +0 -1
  296. package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
  297. package/pennyfarthing_scripts/prime/__init__.py +2 -0
  298. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/prime/cli.py +18 -1
  304. package/pennyfarthing_scripts/prime/loader.py +72 -3
  305. package/pennyfarthing_scripts/prime/persona.py +4 -2
  306. package/pennyfarthing_scripts/prime/tiers.py +17 -4
  307. package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
  308. package/pennyfarthing_scripts/sprint/__init__.py +10 -12
  309. package/pennyfarthing_scripts/sprint/__main__.py +2 -2
  310. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  315. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  316. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  317. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  318. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  319. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  320. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  323. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  324. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/sprint/archive.py +0 -1
  326. package/pennyfarthing_scripts/sprint/archive_epic.py +1 -4
  327. package/pennyfarthing_scripts/sprint/cli.py +34 -28
  328. package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
  329. package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
  330. package/pennyfarthing_scripts/sprint/loader.py +6 -0
  331. package/pennyfarthing_scripts/sprint/status.py +1 -2
  332. package/pennyfarthing_scripts/sprint/story_add.py +2 -2
  333. package/pennyfarthing_scripts/sprint/story_finish.py +3 -5
  334. package/pennyfarthing_scripts/sprint/story_update.py +11 -3
  335. package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
  336. package/pennyfarthing_scripts/sprint/validator.py +120 -6
  337. package/pennyfarthing_scripts/sprint/work.py +1 -4
  338. package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
  339. package/pennyfarthing_scripts/story/__init__.py +14 -16
  340. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/story/size.py +0 -1
  344. package/pennyfarthing_scripts/story/template.py +0 -1
  345. package/pennyfarthing_scripts/swebench.py +1 -2
  346. package/pennyfarthing_scripts/tests/__pycache__/conftest.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_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/conftest.py +1 -2
  353. package/pennyfarthing_scripts/tests/test_bikerack.py +785 -0
  354. package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
  355. package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
  356. package/pennyfarthing_scripts/tests/test_codemarkers.py +13 -8
  357. package/pennyfarthing_scripts/tests/test_common.py +9 -4
  358. package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
  359. package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
  360. package/pennyfarthing_scripts/tests/test_healthscore.py +17 -25
  361. package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
  362. package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
  363. package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
  364. package/pennyfarthing_scripts/tests/test_prime.py +39 -21
  365. package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
  366. package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
  367. package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
  368. package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
  369. package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
  370. package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
  371. package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
  372. package/pennyfarthing_scripts/tests/test_topology_loader.py +620 -0
  373. package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
  374. package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
  375. package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
  376. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  377. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  378. package/pennyfarthing_scripts/theme/cli.py +3 -2
  379. package/pennyfarthing_scripts/validate/__init__.py +21 -0
  380. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  381. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  382. package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
  383. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  384. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  385. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  386. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  387. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  388. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  389. package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
  390. package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
  391. package/pennyfarthing_scripts/validate/adapters/skill_command.py +291 -0
  392. package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
  393. package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
  394. package/pennyfarthing_scripts/validate/cli.py +141 -0
  395. package/pennyfarthing_scripts/welcome_hook.py +2 -3
  396. package/pennyfarthing_scripts/workflow.py +3 -3
  397. package/scripts/README.md +3 -15
  398. package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
  399. package/pennyfarthing-dist/commands/benchmark.md +0 -485
  400. package/pennyfarthing-dist/commands/job-fair.md +0 -102
  401. package/pennyfarthing-dist/commands/solo.md +0 -447
  402. package/pennyfarthing-dist/guides/benchmarks.md +0 -62
  403. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
  404. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
  405. package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
  406. package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
  407. package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
  408. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
  409. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
  410. package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
  411. package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
  412. package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
  413. package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
  414. package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
  415. package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
  416. package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
  417. package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
  418. package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
  419. package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
  420. package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
  421. package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
  422. package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
@@ -15,8 +15,8 @@ workflow:
15
15
  # Variables available in step files
16
16
  variables:
17
17
  project_root: .
18
- planning_artifacts: ./artifacts
19
- output_file: artifacts/generate-project-context.md
18
+ planning_artifacts: sprint/planning/
19
+ output_file: sprint/planning/generate-project-context.md
20
20
 
21
21
  # Agent assignment (customize as needed)
22
22
  agent: architect
@@ -15,8 +15,8 @@ workflow:
15
15
  # Variables available in step files
16
16
  variables:
17
17
  project_root: .
18
- planning_artifacts: ./artifacts
19
- output_file: artifacts/quick-dev.md
18
+ planning_artifacts: sprint/planning/
19
+ output_file: sprint/planning/quick-dev.md
20
20
 
21
21
  # Agent assignment (customize as needed)
22
22
  agent: architect
@@ -23,8 +23,8 @@ workflow:
23
23
  # Variables available in step files
24
24
  variables:
25
25
  project_root: .
26
- planning_artifacts: ./artifacts
27
- output_file: artifacts/research.md
26
+ planning_artifacts: sprint/planning/
27
+ output_file: sprint/planning/research.md
28
28
  research_type: ""
29
29
  research_topic: ""
30
30
  research_goals: ""
@@ -38,7 +38,7 @@ workflow:
38
38
  variables:
39
39
  project_root: .
40
40
  config_source: "./_bmad/bmm/config.yaml"
41
- planning_artifacts: ./artifacts/planning
41
+ planning_artifacts: sprint/planning/
42
42
  implementation_artifacts: ./artifacts
43
43
  sprint_status_file: ./artifacts/sprint-status.yaml
44
44
  story_directory: ./artifacts/stories
@@ -15,12 +15,12 @@ workflow:
15
15
  # Variables available in step files
16
16
  variables:
17
17
  project_root: .
18
- planning_artifacts: ./artifacts
18
+ planning_artifacts: sprint/planning/
19
19
  implementation_artifacts: ./artifacts
20
20
  epics_location: ./artifacts
21
21
  epics_pattern: "epic*.md"
22
- status_file: artifacts/sprint-status.yaml
23
- output_file: artifacts/sprint-status.yaml
22
+ status_file: sprint/planning/sprint-status.yaml
23
+ output_file: sprint/planning/sprint-status.yaml
24
24
 
25
25
  # Agent assignment - SM handles sprint planning
26
26
  agent: sm
@@ -0,0 +1,61 @@
1
+ # TDD Tandem Workflow - TDD with Full Tandem Chain
2
+ # Extends standard TDD with tandem observers on every phase:
3
+ # - Architect backseats TEA during red phase
4
+ # - TEA backseats Dev during green phase
5
+ # - PM backseats Reviewer during review phase
6
+ #
7
+ # Flow: SM → TEA (+Architect) → Dev (+TEA) → Reviewer (+PM) → SM
8
+
9
+ workflow:
10
+ name: tdd-tandem
11
+ description: TDD with full tandem chain across all phases
12
+ version: "2.0.0"
13
+
14
+ phases:
15
+ - name: setup
16
+ agent: sm
17
+ output: [session_file, branches, story_context]
18
+
19
+ - name: red
20
+ agent: tea
21
+ input: [session_file, story_context]
22
+ output: [failing_tests]
23
+ gate:
24
+ type: tests_fail
25
+ condition: All acceptance criteria have test coverage
26
+ tandem:
27
+ partner: architect
28
+ scope: file-watch
29
+
30
+ - name: green
31
+ agent: dev
32
+ input: [failing_tests, story_context]
33
+ output: [implementation, passing_tests]
34
+ gate:
35
+ type: tests_pass
36
+ condition: All tests passing, no skipped tests
37
+ tandem:
38
+ partner: tea
39
+ scope: file-watch
40
+
41
+ - name: review
42
+ agent: reviewer
43
+ input: [implementation, passing_tests]
44
+ output: [approval]
45
+ gate:
46
+ type: approval
47
+ condition: Code review approved, no blocking issues
48
+ tandem:
49
+ partner: pm
50
+ scope: file-watch
51
+
52
+ - name: finish
53
+ agent: sm
54
+ input: [approval]
55
+ output: [archived_session, story_summary]
56
+
57
+ triggers:
58
+ tags: [tandem]
59
+ points:
60
+ min: 3
61
+ default: false
@@ -29,8 +29,8 @@ workflow:
29
29
  # Variables available in step files
30
30
  variables:
31
31
  project_root: .
32
- planning_artifacts: ./artifacts
33
- output_file: artifacts/ux-design-specification.md
32
+ planning_artifacts: sprint/planning/
33
+ output_file: sprint/planning/ux-design-specification.md
34
34
 
35
35
  # Agent assignment - UX Designer for visual design workflow
36
36
  agent: ux-designer
@@ -1,30 +1,23 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Bell Mode PostToolUse Hook (Python)
3
+ PostToolUse Hook — Bell Mode + Tandem Injection (Python)
4
4
 
5
- This hook is called by Claude Code after each tool execution.
6
- When bell mode is enabled and there are queued messages, it returns
7
- the first queued message as additionalContext to be injected into
8
- Claude's next API call.
5
+ Called by Claude Code after each tool execution. Handles two independent
6
+ injection systems:
9
7
 
10
- Configuration files:
11
- .pennyfarthing/config.local.yaml - workflow.bell_mode: true/false
12
- .pennyfarthing/bell-queue.json - [{"text": "...", "images": [...]}, ...]
8
+ 1. Bell queue (Cyclist only) — injects queued user messages when Cyclist
9
+ is running and bell_mode is enabled. In CLI sessions this is a no-op.
10
+ 2. Tandem observations (always active) injects backseat agent observations
11
+ when tandem observation files exist. No configuration required.
13
12
 
14
- Output format (when injecting):
15
- {
16
- "hookSpecificOutput": {
17
- "hookEventName": "PostToolUse",
18
- "additionalContext": "User feedback: <message>"
19
- }
20
- }
21
-
22
- Output when disabled or queue empty: (nothing - exit 0)
13
+ Bell queue takes precedence: if a queued message exists, tandem is
14
+ deferred to the next hook invocation.
23
15
 
24
16
  Story: MSSCI-12409 - Hook consistency and WheelHub consolidation
25
17
  """
26
18
 
27
19
  import json
20
+ import re
28
21
  import sys
29
22
  from pathlib import Path
30
23
 
@@ -32,12 +25,13 @@ from pathlib import Path
32
25
  sys.path.insert(0, str(Path(__file__).parent))
33
26
 
34
27
  from hooks import (
35
- find_project_root,
36
- get_cyclist_port,
37
- send_to_cyclist,
38
- output_hook_response,
28
+ CYCLIST_PORT_FILE,
39
29
  HookResponse,
30
+ find_project_root,
40
31
  is_bell_mode_enabled,
32
+ output_hook_response,
33
+ read_port_file,
34
+ send_to_cyclist,
41
35
  )
42
36
 
43
37
 
@@ -106,8 +100,170 @@ def notify_cyclist(project_root: Path, message_text: str) -> None:
106
100
  pass
107
101
 
108
102
 
103
+ # =============================================================================
104
+ # Tandem Observation Injection (Story 95-7 / MSSCI-14672)
105
+ # =============================================================================
106
+ # Stubs for tandem observation injection. Dev will implement.
107
+
108
+
109
+ def read_tandem_observations(project_root: Path) -> list[Path]:
110
+ """Find tandem observation files in .session/ directory.
111
+
112
+ Looks for files matching .session/*-tandem-*.md pattern.
113
+
114
+ Args:
115
+ project_root: Project root directory
116
+
117
+ Returns:
118
+ List of Path objects for tandem observation files
119
+ """
120
+ session_dir = project_root / ".session"
121
+ if not session_dir.is_dir():
122
+ return []
123
+ return sorted(session_dir.glob("*-tandem-*.md"))
124
+
125
+
126
+ def get_latest_observation(file_content: str) -> dict | None:
127
+ """Extract the latest observation entry from a tandem observation file.
128
+
129
+ Parses the markdown format to extract the most recent ## [HH:MM] Observation
130
+ block, including the observer persona name from the file header.
131
+
132
+ Args:
133
+ file_content: Full text content of the tandem observation file
134
+
135
+ Returns:
136
+ Dict with 'persona' and 'text' keys, or None if no observations found
137
+ """
138
+ # Extract persona from header: **Observer:** agent (Persona Name)
139
+ persona_match = re.search(r"\*\*Observer:\*\*\s*\w+\s*\(([^)]+)\)", file_content)
140
+ persona = persona_match.group(1) if persona_match else "Unknown"
141
+
142
+ # Split on observation headers: ## [HH:MM] Observation
143
+ entries = re.split(r"## \[\d{1,2}:\d{2}\] Observation\n", file_content)
144
+ if len(entries) < 2:
145
+ return None
146
+
147
+ # Last entry is the most recent observation
148
+ last_entry = entries[-1].strip()
149
+ # Remove trailing --- separator
150
+ last_entry = re.sub(r"\n---\s*$", "", last_entry).strip()
151
+ # Remove the **Trigger:** line
152
+ lines = last_entry.split("\n")
153
+ text_lines = [line for line in lines if not line.startswith("**Trigger:**")]
154
+ text = "\n".join(text_lines).strip()
155
+
156
+ if not text:
157
+ return None
158
+
159
+ return {"persona": persona, "text": text}
160
+
161
+
162
+ def format_tandem_message(persona_name: str, observation_text: str) -> str:
163
+ """Format a tandem observation as a bell mode injection message.
164
+
165
+ Args:
166
+ persona_name: The backseat agent's persona name
167
+ observation_text: The observation summary text
168
+
169
+ Returns:
170
+ Formatted string: [Tandem] {persona_name}: {observation_text}
171
+ """
172
+ return f"[Tandem] {persona_name}: {observation_text}"
173
+
174
+
175
+ def get_tandem_mtime(project_root: Path, agent: str) -> float:
176
+ """Read the last-checked mtime for a tandem agent's observation file.
177
+
178
+ Args:
179
+ project_root: Project root directory
180
+ agent: Agent name (e.g. 'reviewer', 'tea')
181
+
182
+ Returns:
183
+ Last-checked mtime as float, or 0.0 if no sidecar exists
184
+ """
185
+ sidecar = project_root / ".session" / f".tandem-mtime-{agent}"
186
+ if not sidecar.exists():
187
+ return 0.0
188
+ try:
189
+ return float(sidecar.read_text().strip())
190
+ except (ValueError, OSError):
191
+ return 0.0
192
+
193
+
194
+ def save_tandem_mtime(project_root: Path, agent: str, mtime: float) -> None:
195
+ """Save the mtime for a tandem agent's observation file.
196
+
197
+ Writes to .session/.tandem-mtime-{agent} sidecar file.
198
+
199
+ Args:
200
+ project_root: Project root directory
201
+ agent: Agent name (e.g. 'reviewer', 'tea')
202
+ mtime: The mtime value to save
203
+ """
204
+ sidecar = project_root / ".session" / f".tandem-mtime-{agent}"
205
+ try:
206
+ sidecar.write_text(str(mtime))
207
+ except OSError:
208
+ pass
209
+
210
+
211
+ def check_tandem_files(project_root: Path) -> list[dict]:
212
+ """Check for new tandem observations and return injection messages.
213
+
214
+ Main entry point for tandem injection in the PostToolUse hook.
215
+ Always active — no configuration required. The presence of tandem
216
+ observation files in .session/ is the only signal needed.
217
+
218
+ Args:
219
+ project_root: Project root directory
220
+
221
+ Returns:
222
+ List of dicts with 'agent', 'message' keys for each new observation
223
+ """
224
+ tandem_files = read_tandem_observations(project_root)
225
+ if not tandem_files:
226
+ return []
227
+
228
+ results = []
229
+ for obs_file in tandem_files:
230
+ # Extract agent name from filename: *-tandem-{agent}.md
231
+ agent_match = re.search(r"-tandem-(\w+)\.md$", obs_file.name)
232
+ if not agent_match:
233
+ continue
234
+ agent = agent_match.group(1)
235
+
236
+ # Compare mtime
237
+ try:
238
+ file_mtime = obs_file.stat().st_mtime
239
+ except OSError:
240
+ continue
241
+ saved_mtime = get_tandem_mtime(project_root, agent)
242
+ if file_mtime == saved_mtime:
243
+ continue
244
+
245
+ # Read and parse
246
+ try:
247
+ content = obs_file.read_text()
248
+ except OSError:
249
+ continue
250
+ obs = get_latest_observation(content)
251
+ if not obs:
252
+ # Update mtime even for unparseable files to avoid re-checking
253
+ save_tandem_mtime(project_root, agent, file_mtime)
254
+ continue
255
+
256
+ message = format_tandem_message(obs["persona"], obs["text"])
257
+ results.append({"agent": agent, "message": message})
258
+
259
+ # Update mtime sidecar
260
+ save_tandem_mtime(project_root, agent, file_mtime)
261
+
262
+ return results
263
+
264
+
109
265
  def main() -> None:
110
- """Main entry point for PostToolUse bell mode hook."""
266
+ """Main entry point for PostToolUse hook."""
111
267
  try:
112
268
  # Read and discard stdin (required by hook protocol)
113
269
  sys.stdin.read()
@@ -117,35 +273,34 @@ def main() -> None:
117
273
  if not project_root:
118
274
  sys.exit(0)
119
275
 
120
- # Check if bell mode is enabled
121
- if not is_bell_mode_enabled(project_root):
122
- sys.exit(0)
123
-
124
- # Read queue
125
- queue = read_bell_queue(project_root)
126
- if not queue:
127
- sys.exit(0)
128
-
129
- # Get first message
130
- first_message = queue[0]
131
- message_text = first_message.get("text", "")
132
- if not message_text:
133
- sys.exit(0)
134
-
135
- # Output hook response with additionalContext
136
- output_hook_response(HookResponse(
137
- event_name="PostToolUse",
138
- additional_context=f"User feedback: {message_text}",
139
- ))
140
-
141
- # Dequeue message and notify Cyclist (in background-ish - after output)
142
- dequeue_message(project_root)
143
- notify_cyclist(project_root, message_text)
276
+ # --- Bell queue (Cyclist only, requires bell_mode: true) ---
277
+ # Takes precedence over tandem when active.
278
+ is_cyclist = read_port_file(CYCLIST_PORT_FILE, project_root) is not None
279
+ if is_cyclist and is_bell_mode_enabled(project_root):
280
+ queue = read_bell_queue(project_root)
281
+ if queue:
282
+ first_message = queue[0]
283
+ message_text = first_message.get("text", "")
284
+ if message_text:
285
+ output_hook_response(HookResponse(
286
+ event_name="PostToolUse",
287
+ additional_context=f"User feedback: {message_text}",
288
+ ))
289
+ dequeue_message(project_root)
290
+ notify_cyclist(project_root, message_text)
291
+ sys.exit(0)
292
+
293
+ # --- Tandem observations (always active, no config required) ---
294
+ tandem_results = check_tandem_files(project_root)
295
+ if tandem_results:
296
+ output_hook_response(HookResponse(
297
+ event_name="PostToolUse",
298
+ additional_context=tandem_results[0]["message"],
299
+ ))
144
300
 
145
301
  sys.exit(0)
146
302
 
147
303
  except Exception as e:
148
- # On error, exit silently
149
304
  print(f"[bellmode-hook] Error: {e}", file=sys.stderr)
150
305
  sys.exit(0)
151
306
 
@@ -0,0 +1,36 @@
1
+ """BikeRack Mode — Decoupled WheelHub launcher for CLI-first developers.
2
+
3
+ Story 101-5: BikeRack launcher CLI (pf bikerack start/stop/status)
4
+ """
5
+
6
+ from pennyfarthing_scripts.bikerack.launcher import (
7
+ build_otel_env,
8
+ cleanup_files,
9
+ exec_claude,
10
+ get_status,
11
+ is_already_running,
12
+ is_process_alive,
13
+ poll_for_port_file,
14
+ read_pid_file,
15
+ read_port_file,
16
+ register_cleanup,
17
+ start_wheelhub,
18
+ stop_bikerack,
19
+ write_pid_file,
20
+ )
21
+
22
+ __all__ = [
23
+ "build_otel_env",
24
+ "cleanup_files",
25
+ "exec_claude",
26
+ "get_status",
27
+ "is_already_running",
28
+ "is_process_alive",
29
+ "poll_for_port_file",
30
+ "read_pid_file",
31
+ "read_port_file",
32
+ "register_cleanup",
33
+ "start_wheelhub",
34
+ "stop_bikerack",
35
+ "write_pid_file",
36
+ ]
@@ -0,0 +1,5 @@
1
+ """Entry point for python -m pennyfarthing_scripts.bikerack."""
2
+
3
+ from pennyfarthing_scripts.bikerack.cli import bikerack
4
+
5
+ bikerack()
@@ -0,0 +1,148 @@
1
+ """BikeRack CLI — Click-based CLI for bikerack operations.
2
+
3
+ Usage:
4
+ pf bikerack [COMMAND]
5
+
6
+ Commands:
7
+ start Start BikeRack mode (default)
8
+ stop Stop running BikeRack instance
9
+ status Show running state
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import click
17
+
18
+
19
+ @click.group(invoke_without_command=True)
20
+ @click.pass_context
21
+ def bikerack(ctx):
22
+ """BikeRack Mode — Decoupled WheelHub dashboard launcher.
23
+
24
+ \b
25
+ Commands:
26
+ start - Start WheelHub + Claude CLI (default)
27
+ stop - Stop running BikeRack instance
28
+ status - Show running state (PID, port, uptime)
29
+ """
30
+ if ctx.invoked_subcommand is None:
31
+ ctx.invoke(start)
32
+
33
+
34
+ @bikerack.command()
35
+ @click.option(
36
+ "--project-dir",
37
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
38
+ default=None,
39
+ help="Project directory (where .pennyfarthing/ lives). Falls back to CYCLIST_PROJECT_DIR env var, then cwd.",
40
+ )
41
+ def start(project_dir):
42
+ """Start BikeRack mode.
43
+
44
+ Starts WheelHub in background, waits for readiness,
45
+ sets OTEL env vars, and execs Claude CLI.
46
+ """
47
+ from pennyfarthing_scripts.bikerack.launcher import (
48
+ build_otel_env,
49
+ exec_claude,
50
+ is_already_running,
51
+ poll_for_port_file,
52
+ register_cleanup,
53
+ start_wheelhub,
54
+ write_pid_file,
55
+ )
56
+
57
+ if project_dir:
58
+ project_dir = Path(project_dir)
59
+ elif os.environ.get("CYCLIST_PROJECT_DIR"):
60
+ project_dir = Path(os.environ["CYCLIST_PROJECT_DIR"])
61
+ else:
62
+ project_dir = Path.cwd()
63
+
64
+ running, pid, port = is_already_running(project_dir)
65
+ if running:
66
+ click.echo(
67
+ f"Error: BikeRack is already running (PID {pid}, port {port})",
68
+ err=True,
69
+ )
70
+ click.echo("Use 'pf bikerack stop' to stop it.", err=True)
71
+ sys.exit(2)
72
+
73
+ click.echo("Starting BikeRack mode...")
74
+ try:
75
+ proc = start_wheelhub(project_dir)
76
+ write_pid_file(project_dir, proc.pid)
77
+
78
+ port = poll_for_port_file(project_dir)
79
+ click.echo(f"WheelHub listening on http://localhost:{port}")
80
+
81
+ otel_env = build_otel_env(port)
82
+ click.echo("Setting OTEL environment variables...")
83
+
84
+ register_cleanup(project_dir, proc.pid)
85
+
86
+ click.echo(f"Dashboard: http://localhost:{port}/bikerack")
87
+ click.echo("Starting Claude CLI...")
88
+ exec_claude(otel_env, project_dir)
89
+ except TimeoutError as e:
90
+ click.echo(f"Error: {e}", err=True)
91
+ sys.exit(1)
92
+ except Exception as e:
93
+ click.echo(f"Error: {e}", err=True)
94
+ sys.exit(1)
95
+
96
+
97
+ @bikerack.command()
98
+ @click.option(
99
+ "--project-dir",
100
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
101
+ default=None,
102
+ help="Project directory. Falls back to CYCLIST_PROJECT_DIR env var, then cwd.",
103
+ )
104
+ def stop(project_dir):
105
+ """Stop running BikeRack instance."""
106
+ from pennyfarthing_scripts.bikerack.launcher import stop_bikerack
107
+
108
+ if project_dir:
109
+ project_dir = Path(project_dir)
110
+ elif os.environ.get("CYCLIST_PROJECT_DIR"):
111
+ project_dir = Path(os.environ["CYCLIST_PROJECT_DIR"])
112
+ else:
113
+ project_dir = Path.cwd()
114
+ result = stop_bikerack(project_dir)
115
+
116
+ if result["success"]:
117
+ click.echo(result["message"])
118
+ else:
119
+ click.echo(result["message"], err=True)
120
+ sys.exit(1)
121
+
122
+
123
+ @bikerack.command()
124
+ @click.option(
125
+ "--project-dir",
126
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
127
+ default=None,
128
+ help="Project directory. Falls back to CYCLIST_PROJECT_DIR env var, then cwd.",
129
+ )
130
+ def status(project_dir):
131
+ """Show BikeRack running state."""
132
+ from pennyfarthing_scripts.bikerack.launcher import get_status
133
+
134
+ if project_dir:
135
+ project_dir = Path(project_dir)
136
+ elif os.environ.get("CYCLIST_PROJECT_DIR"):
137
+ project_dir = Path(os.environ["CYCLIST_PROJECT_DIR"])
138
+ else:
139
+ project_dir = Path.cwd()
140
+ result = get_status(project_dir)
141
+
142
+ if result["running"]:
143
+ click.echo("BikeRack is running")
144
+ click.echo(f" PID: {result['pid']}")
145
+ click.echo(f" Port: {result['port']}")
146
+ click.echo(f" Dashboard: {result['dashboard']}")
147
+ else:
148
+ click.echo("BikeRack is not running")