@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,270 @@
1
+ """
2
+ CYCLIST reflector marker enforcement hook (Stop hook).
3
+
4
+ Ensures every turn end has a CYCLIST reflector marker so the Cyclist UI
5
+ can render appropriate buttons/actions.
6
+
7
+ Delegates to the existing question_reflector_check module which contains
8
+ all detection logic and patterns.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ # =============================================================================
21
+ # Import detection logic — inline to avoid import path issues
22
+ # The logic lives in question_reflector_check.py in pennyfarthing-dist/scripts/hooks/
23
+ # We consolidate the essential parts here.
24
+ # =============================================================================
25
+
26
+ # Marker patterns
27
+ QUESTION_MARKER_PATTERN = re.compile(r'<!--\s*CYCLIST:QUESTION:(yesno|open)\s*-->', re.IGNORECASE)
28
+ CHOICES_MARKER_PATTERN = re.compile(r'<!--\s*CYCLIST:CHOICES:[^>]+\s*-->', re.IGNORECASE)
29
+ HANDOFF_MARKER_PATTERN = re.compile(r'<!--\s*CYCLIST:HANDOFF:/\w+\s*-->', re.IGNORECASE)
30
+ CONTEXT_CLEAR_MARKER_PATTERN = re.compile(r'<!--\s*CYCLIST:CONTEXT_CLEAR:/\w+\s*-->', re.IGNORECASE)
31
+ CONTINUE_MARKER_PATTERN = re.compile(r'<!--\s*CYCLIST:CONTINUE\s*-->', re.IGNORECASE)
32
+
33
+ DIRECT_QUESTION_PATTERN = re.compile(r'\?(\s*$|\s+[A-Z]|\s*\n)')
34
+ RHETORICAL_PATTERNS = re.compile(r'\b(the question (was|is)|asked whether|wondering if)\b', re.IGNORECASE)
35
+
36
+ IMPLICIT_PATTERNS = [
37
+ re.compile(r'\bwould you like\b', re.IGNORECASE),
38
+ re.compile(r'\bshould I\b', re.IGNORECASE),
39
+ re.compile(r'\bdo you want\b', re.IGNORECASE),
40
+ re.compile(r'\blet me know if\b', re.IGNORECASE),
41
+ re.compile(r'\bwhat do you (think|prefer)\b', re.IGNORECASE),
42
+ re.compile(r'\byour (preference|thoughts)\b', re.IGNORECASE),
43
+ re.compile(r'\bcould you (clarify|confirm|specify)\b', re.IGNORECASE),
44
+ re.compile(r'\bwhich (option|approach)\b', re.IGNORECASE),
45
+ re.compile(r'\bready to proceed\b', re.IGNORECASE),
46
+ ]
47
+
48
+ CHOICE_PATTERNS = [
49
+ re.compile(r'\boption [A-D]\b', re.IGNORECASE),
50
+ re.compile(r'\bchoice [0-9]\b', re.IGNORECASE),
51
+ re.compile(r'\bwe could (either|do)\b', re.IGNORECASE),
52
+ re.compile(r'\balternatively\b', re.IGNORECASE),
53
+ re.compile(r'\bor would you prefer\b', re.IGNORECASE),
54
+ re.compile(r'\bpick one\b', re.IGNORECASE),
55
+ re.compile(r'\bchoose between\b', re.IGNORECASE),
56
+ ]
57
+
58
+ HANDOFF_PHRASE_PATTERNS = [
59
+ re.compile(r'\bhanding (off )?to\b', re.IGNORECASE),
60
+ re.compile(r'\bpassing to\b', re.IGNORECASE),
61
+ re.compile(r'\bhand(ing)? this (off )?to\b', re.IGNORECASE),
62
+ re.compile(r'\b(Naomi|Amos|Avasarala|Holden|Alex|Drummer) (will|can)\b', re.IGNORECASE),
63
+ re.compile(r'\bfor (the )?(GREEN|RED|REVIEW) phase\b', re.IGNORECASE),
64
+ re.compile(r'\bspawning .* agent\b', re.IGNORECASE),
65
+ ]
66
+
67
+
68
+ def _strip_code_blocks(text: str) -> str:
69
+ result = re.sub(r'```[\s\S]*?```', '', text)
70
+ return re.sub(r'`[^`]+`', '', result)
71
+
72
+
73
+ def _has_reflector_marker(message: str) -> bool:
74
+ return bool(
75
+ QUESTION_MARKER_PATTERN.search(message) or
76
+ CHOICES_MARKER_PATTERN.search(message) or
77
+ HANDOFF_MARKER_PATTERN.search(message) or
78
+ CONTEXT_CLEAR_MARKER_PATTERN.search(message) or
79
+ CONTINUE_MARKER_PATTERN.search(message)
80
+ )
81
+
82
+
83
+ def _detect_question(message: str) -> dict[str, Any]:
84
+ clean = _strip_code_blocks(message)
85
+ if RHETORICAL_PATTERNS.search(clean):
86
+ return {'detected': False, 'type': ''}
87
+ if DIRECT_QUESTION_PATTERN.search(clean):
88
+ return {'detected': True, 'type': 'direct'}
89
+ for p in IMPLICIT_PATTERNS:
90
+ if p.search(clean):
91
+ return {'detected': True, 'type': 'implicit'}
92
+ for p in CHOICE_PATTERNS:
93
+ if p.search(clean):
94
+ return {'detected': True, 'type': 'choices'}
95
+ return {'detected': False, 'type': ''}
96
+
97
+
98
+ def _detect_handoff_phrase(message: str) -> bool:
99
+ clean = _strip_code_blocks(message)
100
+ return any(p.search(clean) for p in HANDOFF_PHRASE_PATTERNS)
101
+
102
+
103
+ def _has_task_tool_in_turn(transcript: list[dict[str, Any]]) -> bool:
104
+ current_turn_start = -1
105
+ for i, entry in enumerate(transcript):
106
+ msg = entry.get('message', entry)
107
+ if msg.get('role') == 'user':
108
+ current_turn_start = i
109
+ if current_turn_start < 0:
110
+ return False
111
+ for entry in transcript[current_turn_start:]:
112
+ msg = entry.get('message', entry)
113
+ if msg.get('role') == 'assistant':
114
+ content = msg.get('content', [])
115
+ if isinstance(content, list):
116
+ for block in content:
117
+ if block.get('type') == 'tool_use' and block.get('name') == 'Task':
118
+ return True
119
+ return False
120
+
121
+
122
+ def _extract_last_assistant_message(transcript: list[dict[str, Any]]) -> str:
123
+ for entry in reversed(transcript):
124
+ msg = entry.get('message', entry)
125
+ if msg.get('role') == 'assistant':
126
+ content = msg.get('content', '')
127
+ if isinstance(content, str):
128
+ return content
129
+ if isinstance(content, list):
130
+ return ''.join(
131
+ block.get('text', '')
132
+ for block in content
133
+ if block.get('type') == 'text'
134
+ )
135
+ return ''
136
+ return ''
137
+
138
+
139
+ def _build_block_reason(question_type: str, handoff_without_task: bool = False) -> str:
140
+ if handoff_without_task:
141
+ return (
142
+ 'HANDOFF COMPLIANCE VIOLATION: You said you would hand off but did NOT use the Task tool.\n\n'
143
+ 'SM Protocol: Task tool FIRST, narration SECOND.\n\n'
144
+ 'Either:\n'
145
+ '1. Actually spawn the agent now using the Task tool, OR\n'
146
+ '2. If you completed the work yourself, remove handoff language and add <!-- CYCLIST:CONTINUE -->'
147
+ )
148
+ reason = 'Missing CYCLIST marker. Your response content is fine - just APPEND the marker.\n\n'
149
+ if question_type == 'direct':
150
+ reason += 'Detected: direct question (?)\nAPPEND THIS: <!-- CYCLIST:QUESTION:open -->\n(Use yesno if it\'s a yes/no question)'
151
+ elif question_type == 'implicit':
152
+ reason += 'Detected: implicit question (would you like, should I, etc.)\nAPPEND THIS: <!-- CYCLIST:QUESTION:yesno -->'
153
+ elif question_type == 'choices':
154
+ reason += 'Detected: choice offering\nAPPEND THIS: <!-- CYCLIST:CHOICES:option1,option2 -->\n(Replace option1,option2 with actual choices)'
155
+ else:
156
+ reason += ('No question detected - this looks like a status update.\n'
157
+ 'APPEND THIS: <!-- CYCLIST:CONTINUE -->\n\n'
158
+ 'Other markers if needed:\n'
159
+ ' <!-- CYCLIST:HANDOFF:/agent --> - workflow handoff\n'
160
+ ' <!-- CYCLIST:QUESTION:yesno --> - yes/no question\n'
161
+ ' <!-- CYCLIST:QUESTION:open --> - open question')
162
+ return reason
163
+
164
+
165
+ def _load_config(project_dir: str) -> dict[str, Any]:
166
+ try:
167
+ config_path = Path(project_dir) / '.pennyfarthing' / 'config.local.yaml'
168
+ content = config_path.read_text()
169
+ config: dict[str, Any] = {'workflow': {}}
170
+ mode_match = re.search(r'permission_mode:\s*(\w+)', content)
171
+ if mode_match:
172
+ config['workflow']['permission_mode'] = mode_match.group(1)
173
+ relay_match = re.search(r'relay_mode:\s*(true|false)', content)
174
+ if relay_match:
175
+ config['workflow']['relay_mode'] = relay_match.group(1) == 'true'
176
+ return config
177
+ except Exception:
178
+ return {'workflow': {'permission_mode': 'manual'}}
179
+
180
+
181
+ def _should_skip_enforcement() -> bool:
182
+ return os.environ.get('CYCLIST') != '1'
183
+
184
+
185
+ def _read_transcript(transcript_path: str) -> tuple[str, list[dict[str, Any]]]:
186
+ try:
187
+ content = Path(transcript_path).read_text()
188
+ lines = [line for line in content.strip().split('\n') if line]
189
+ transcript: list[dict[str, Any]] = []
190
+ for line in lines:
191
+ try:
192
+ transcript.append(json.loads(line))
193
+ except json.JSONDecodeError:
194
+ pass
195
+ return _extract_last_assistant_message(transcript), transcript
196
+ except Exception:
197
+ return '', []
198
+
199
+
200
+ # =============================================================================
201
+ # Entry Point
202
+ # =============================================================================
203
+
204
+
205
+ def main() -> None:
206
+ """Main CLI entry point for reflector check hook."""
207
+ input_data_str = sys.stdin.read()
208
+
209
+ try:
210
+ input_data = json.loads(input_data_str)
211
+ except json.JSONDecodeError:
212
+ print(json.dumps({'ok': True}))
213
+ sys.exit(0)
214
+
215
+ project_dir = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
216
+ _load_config(project_dir)
217
+
218
+ # Prevent infinite loops
219
+ if input_data.get('stop_hook_active'):
220
+ print(json.dumps({'ok': True}))
221
+ sys.exit(0)
222
+
223
+ # Skip in CLI mode
224
+ if _should_skip_enforcement():
225
+ print(json.dumps({'ok': True}))
226
+ sys.exit(0)
227
+
228
+ # AskUserQuestion PreToolUse check
229
+ if input_data.get('tool_name') == 'AskUserQuestion':
230
+ print(json.dumps({'ok': True}))
231
+ sys.exit(0)
232
+
233
+ # Stop hook — validate reflector markers
234
+ transcript_path = input_data.get('transcript_path', '')
235
+ last_message, transcript = _read_transcript(transcript_path) if transcript_path else ('', [])
236
+
237
+ if not last_message:
238
+ print(json.dumps({'ok': True}))
239
+ sys.exit(0)
240
+
241
+ if _has_reflector_marker(last_message):
242
+ print(json.dumps({'ok': True}))
243
+ sys.exit(0)
244
+
245
+ # Handoff compliance check
246
+ if _detect_handoff_phrase(last_message):
247
+ has_task = transcript and _has_task_tool_in_turn(transcript)
248
+ if not has_task:
249
+ print(json.dumps({
250
+ 'decision': 'block',
251
+ 'reason': _build_block_reason('', handoff_without_task=True),
252
+ }))
253
+ sys.exit(0)
254
+
255
+ # No marker — block with guidance
256
+ detection = _detect_question(last_message)
257
+ q_type = detection['type'] if detection['detected'] else ''
258
+ print(json.dumps({
259
+ 'decision': 'block',
260
+ 'reason': _build_block_reason(q_type),
261
+ }))
262
+ sys.exit(0)
263
+
264
+
265
+ if __name__ == '__main__':
266
+ try:
267
+ main()
268
+ except Exception:
269
+ print(json.dumps({'ok': True}))
270
+ sys.exit(0)
@@ -0,0 +1,202 @@
1
+ """
2
+ Schema validation hook (PreToolUse) — validate XML schema on Write operations.
3
+
4
+ Validates session, skill, and workflow step files to ensure they have
5
+ required tags and attributes. Blocks the Write if validation fails.
6
+
7
+ Consolidates schema_validation_hook.py into the hooks subpackage.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ import sys
14
+
15
+ from pennyfarthing_scripts.hooks import (
16
+ HookResponse,
17
+ output_hook_response,
18
+ read_stdin_json,
19
+ )
20
+
21
+ # =============================================================================
22
+ # File Type Detection
23
+ # =============================================================================
24
+
25
+ SKILL_REQUIRED_TAGS = ["run", "output"]
26
+ STEP_REQUIRED_TAGS = ["purpose", "instructions", "output"]
27
+ STEP_META_FIELDS = ["step", "workflow", "agent", "next"]
28
+
29
+
30
+ def _is_session_file(file_path: str) -> bool:
31
+ return file_path.endswith("-session.md") and ".session/" in file_path
32
+
33
+
34
+ def _is_skill_file(file_path: str) -> bool:
35
+ return file_path.endswith("/SKILL.md") and "/skills/" in file_path
36
+
37
+
38
+ def _is_step_file(file_path: str) -> bool:
39
+ from pathlib import Path
40
+ path = Path(file_path)
41
+ return (
42
+ path.name.startswith("step-")
43
+ and path.name.endswith(".md")
44
+ and "/workflows/" in file_path
45
+ and "/steps/" in file_path
46
+ )
47
+
48
+
49
+ def _get_file_type(file_path: str) -> str | None:
50
+ if _is_session_file(file_path):
51
+ return "session"
52
+ elif _is_skill_file(file_path):
53
+ return "skill"
54
+ elif _is_step_file(file_path):
55
+ return "step"
56
+ return None
57
+
58
+
59
+ def _has_tag(content: str, tag: str) -> bool:
60
+ return f"<{tag}>" in content or f"<{tag} " in content
61
+
62
+
63
+ def _validate_session(content: str) -> list[str]:
64
+ errors = []
65
+ if "<session" not in content:
66
+ return [] # Old markdown format — warn but don't block
67
+ if not re.search(r'<session\s+story="[^"]+"', content):
68
+ errors.append("Missing story attribute on <session>")
69
+ if not re.search(r'<session[^>]+workflow="[^"]+"', content):
70
+ errors.append("Missing workflow attribute on <session>")
71
+ if not _has_tag(content, "meta"):
72
+ errors.append("Missing <meta> section")
73
+ else:
74
+ if "<jira>" not in content:
75
+ errors.append("Missing <jira> in <meta>")
76
+ if "<started>" not in content:
77
+ errors.append("Missing <started> in <meta>")
78
+ if not _has_tag(content, "status"):
79
+ errors.append("Missing <status> element")
80
+ elif 'phase="' not in content:
81
+ errors.append("Missing phase attribute on <status>")
82
+ return errors
83
+
84
+
85
+ def _validate_skill(content: str) -> list[str]:
86
+ errors = []
87
+ if not content.startswith("---\n"):
88
+ errors.append("Missing YAML frontmatter")
89
+ else:
90
+ parts = content.split("---", 2)
91
+ if len(parts) >= 2:
92
+ frontmatter = parts[1]
93
+ if "name:" not in frontmatter:
94
+ errors.append("Missing 'name' in frontmatter")
95
+ if "description:" not in frontmatter:
96
+ errors.append("Missing 'description' in frontmatter")
97
+ for tag in SKILL_REQUIRED_TAGS:
98
+ if not _has_tag(content, tag):
99
+ errors.append(f"Missing <{tag}> tag (required)")
100
+ return errors
101
+
102
+
103
+ def _validate_step(content: str) -> list[str]:
104
+ errors = []
105
+ for tag in STEP_REQUIRED_TAGS:
106
+ if not _has_tag(content, tag):
107
+ errors.append(f"Missing <{tag}> tag")
108
+ if _has_tag(content, "step-meta"):
109
+ meta_match = re.search(r"<step-meta>(.+?)</step-meta>", content, re.DOTALL)
110
+ if meta_match:
111
+ meta_content = meta_match.group(1)
112
+ for field_name in STEP_META_FIELDS:
113
+ if f"{field_name}:" not in meta_content:
114
+ errors.append(f"Missing '{field_name}' in step-meta")
115
+ return errors
116
+
117
+
118
+ def _validate_content(file_path: str, content: str) -> list[str]:
119
+ file_type = _get_file_type(file_path)
120
+ if file_type == "session":
121
+ return _validate_session(content)
122
+ elif file_type == "skill":
123
+ return _validate_skill(content)
124
+ elif file_type == "step":
125
+ return _validate_step(content)
126
+ return []
127
+
128
+
129
+ # =============================================================================
130
+ # Entry Point
131
+ # =============================================================================
132
+
133
+
134
+ def main() -> None:
135
+ """Main entry point for schema validation hook."""
136
+ try:
137
+ tool_data = read_stdin_json()
138
+ tool_name = tool_data.get("tool_name", "")
139
+ tool_input = tool_data.get("tool_input", {})
140
+
141
+ if tool_name not in ("Write", "Edit"):
142
+ output_hook_response(HookResponse(
143
+ event_name="PreToolUse",
144
+ decision="allow",
145
+ reason="Not a Write/Edit operation",
146
+ ))
147
+ sys.exit(0)
148
+
149
+ file_path = tool_input.get("file_path", "")
150
+ if not file_path:
151
+ output_hook_response(HookResponse(
152
+ event_name="PreToolUse",
153
+ decision="allow",
154
+ reason="No file path",
155
+ ))
156
+ sys.exit(0)
157
+
158
+ file_type = _get_file_type(file_path)
159
+ if not file_type:
160
+ output_hook_response(HookResponse(
161
+ event_name="PreToolUse",
162
+ decision="allow",
163
+ reason="Not a session/skill/step file",
164
+ ))
165
+ sys.exit(0)
166
+
167
+ if tool_name == "Write":
168
+ content = tool_input.get("content", "")
169
+ else:
170
+ output_hook_response(HookResponse(
171
+ event_name="PreToolUse",
172
+ decision="allow",
173
+ reason="Edit operations validated post-hoc",
174
+ ))
175
+ sys.exit(0)
176
+
177
+ errors = _validate_content(file_path, content)
178
+ if errors:
179
+ error_msg = f"Schema validation failed for {file_type} file:\n"
180
+ error_msg += "\n".join(f" - {e}" for e in errors)
181
+ error_msg += f"\n\nFile: {file_path}"
182
+ output_hook_response(HookResponse(
183
+ event_name="PreToolUse",
184
+ decision="deny",
185
+ reason=error_msg,
186
+ ))
187
+ sys.exit(0)
188
+
189
+ output_hook_response(HookResponse(
190
+ event_name="PreToolUse",
191
+ decision="allow",
192
+ reason=f"Schema validation passed for {file_type} file",
193
+ ))
194
+ sys.exit(0)
195
+
196
+ except Exception as e:
197
+ print(f"[schema-validation-hook] Error: {e}", file=sys.stderr)
198
+ sys.exit(0)
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()