@pennyfarthing/core 11.0.0 → 11.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +81 -23
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts +20 -0
  4. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts.map +1 -0
  5. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js +278 -0
  6. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js.map +1 -0
  7. package/packages/core/dist/cli/utils/constants.d.ts +8 -2
  8. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  9. package/packages/core/dist/cli/utils/constants.js +4 -1
  10. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  11. package/packages/core/dist/cli/utils/constants.test.d.ts +10 -0
  12. package/packages/core/dist/cli/utils/constants.test.d.ts.map +1 -0
  13. package/packages/core/dist/cli/utils/constants.test.js +38 -0
  14. package/packages/core/dist/cli/utils/constants.test.js.map +1 -0
  15. package/packages/core/dist/consultation/consultation-protocol.d.ts +139 -0
  16. package/packages/core/dist/consultation/consultation-protocol.d.ts.map +1 -0
  17. package/packages/core/dist/consultation/consultation-protocol.js +178 -0
  18. package/packages/core/dist/consultation/consultation-protocol.js.map +1 -0
  19. package/packages/core/dist/consultation/consultation-protocol.test.d.ts +20 -0
  20. package/packages/core/dist/consultation/consultation-protocol.test.d.ts.map +1 -0
  21. package/packages/core/dist/consultation/consultation-protocol.test.js +474 -0
  22. package/packages/core/dist/consultation/consultation-protocol.test.js.map +1 -0
  23. package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
  24. package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
  25. package/packages/core/dist/consultation/dialogue-manager.js +334 -0
  26. package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
  27. package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
  28. package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
  29. package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
  30. package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
  31. package/packages/core/dist/public/js/react/react.js +3 -3
  32. package/packages/core/dist/scripts/theme-detail.test.d.ts +10 -0
  33. package/packages/core/dist/scripts/theme-detail.test.js +199 -0
  34. package/packages/core/dist/server/api/git.d.ts +13 -1
  35. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  36. package/packages/core/dist/server/api/git.js +53 -34
  37. package/packages/core/dist/server/api/git.js.map +1 -1
  38. package/packages/core/dist/server/api/health-score.d.ts.map +1 -1
  39. package/packages/core/dist/server/api/health-score.js +25 -1
  40. package/packages/core/dist/server/api/health-score.js.map +1 -1
  41. package/packages/core/dist/server/api/settings.d.ts.map +1 -1
  42. package/packages/core/dist/server/api/settings.js +63 -1
  43. package/packages/core/dist/server/api/settings.js.map +1 -1
  44. package/packages/core/dist/server/api/theme-agents.d.ts.map +1 -1
  45. package/packages/core/dist/server/api/theme-agents.js +61 -0
  46. package/packages/core/dist/server/api/theme-agents.js.map +1 -1
  47. package/packages/core/dist/server/server.d.ts.map +1 -1
  48. package/packages/core/dist/server/server.js +17 -12
  49. package/packages/core/dist/server/server.js.map +1 -1
  50. package/packages/core/dist/shared/skill-search.test.js +2 -2
  51. package/packages/core/dist/workflow/gate-file-validation.d.ts +49 -0
  52. package/packages/core/dist/workflow/gate-file-validation.d.ts.map +1 -0
  53. package/packages/core/dist/workflow/gate-file-validation.js +157 -0
  54. package/packages/core/dist/workflow/gate-file-validation.js.map +1 -0
  55. package/packages/core/dist/workflow/gate-file-validation.test.d.ts +19 -0
  56. package/packages/core/dist/workflow/gate-file-validation.test.d.ts.map +1 -0
  57. package/packages/core/dist/workflow/gate-file-validation.test.js +536 -0
  58. package/packages/core/dist/workflow/gate-file-validation.test.js.map +1 -0
  59. package/packages/core/dist/workflow/gate-schema-validation.test.d.ts +14 -0
  60. package/packages/core/dist/workflow/gate-schema-validation.test.d.ts.map +1 -0
  61. package/packages/core/dist/workflow/gate-schema-validation.test.js +339 -0
  62. package/packages/core/dist/workflow/gate-schema-validation.test.js.map +1 -0
  63. package/packages/core/dist/workflow/handoff.js +2 -2
  64. package/packages/core/dist/workflow/handoff.js.map +1 -1
  65. package/packages/core/dist/workflow/handoff.test.js +16 -0
  66. package/packages/core/dist/workflow/handoff.test.js.map +1 -1
  67. package/packages/core/dist/workflow/workflow-schema.d.ts +4 -2
  68. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  69. package/packages/core/dist/workflow/workflow-schema.js +43 -8
  70. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  71. package/pennyfarthing-dist/agents/README.md +6 -14
  72. package/pennyfarthing-dist/agents/architect.md +43 -29
  73. package/pennyfarthing-dist/agents/ba.md +30 -29
  74. package/pennyfarthing-dist/agents/dev.md +32 -43
  75. package/pennyfarthing-dist/agents/devops.md +57 -21
  76. package/pennyfarthing-dist/agents/orchestrator.md +3 -10
  77. package/pennyfarthing-dist/agents/pm.md +45 -31
  78. package/pennyfarthing-dist/agents/reviewer.md +20 -66
  79. package/pennyfarthing-dist/agents/sm-setup.md +2 -2
  80. package/pennyfarthing-dist/agents/sm.md +8 -30
  81. package/pennyfarthing-dist/agents/tea.md +25 -41
  82. package/pennyfarthing-dist/agents/tech-writer.md +33 -90
  83. package/pennyfarthing-dist/agents/ux-designer.md +39 -39
  84. package/pennyfarthing-dist/commands/benchmark-control.md +8 -64
  85. package/pennyfarthing-dist/commands/benchmark.md +8 -480
  86. package/pennyfarthing-dist/commands/job-fair.md +8 -97
  87. package/pennyfarthing-dist/commands/pf-benchmark-control.md +70 -0
  88. package/pennyfarthing-dist/commands/pf-benchmark.md +486 -0
  89. package/pennyfarthing-dist/commands/pf-chore.md +4 -4
  90. package/pennyfarthing-dist/commands/pf-ci.md +40 -0
  91. package/pennyfarthing-dist/commands/pf-close-epic.md +9 -27
  92. package/pennyfarthing-dist/commands/pf-continue-session.md +9 -213
  93. package/pennyfarthing-dist/commands/pf-create-branches-from-story.md +11 -353
  94. package/pennyfarthing-dist/commands/pf-docs.md +28 -0
  95. package/pennyfarthing-dist/commands/pf-epic.md +67 -0
  96. package/pennyfarthing-dist/commands/pf-git-cleanup.md +11 -52
  97. package/pennyfarthing-dist/commands/pf-git.md +75 -0
  98. package/pennyfarthing-dist/commands/pf-help.md +110 -128
  99. package/pennyfarthing-dist/commands/pf-job-fair.md +102 -0
  100. package/pennyfarthing-dist/commands/pf-new-work.md +9 -18
  101. package/pennyfarthing-dist/commands/pf-parallel-work.md +6 -66
  102. package/pennyfarthing-dist/commands/pf-release.md +11 -76
  103. package/pennyfarthing-dist/commands/pf-repo-status.md +11 -44
  104. package/pennyfarthing-dist/commands/pf-run-ci.md +8 -111
  105. package/pennyfarthing-dist/commands/pf-session.md +51 -0
  106. package/pennyfarthing-dist/commands/pf-solo.md +447 -0
  107. package/pennyfarthing-dist/commands/pf-sprint-planning.md +8 -104
  108. package/pennyfarthing-dist/commands/pf-standalone.md +1 -1
  109. package/pennyfarthing-dist/commands/pf-start-epic.md +9 -163
  110. package/pennyfarthing-dist/commands/pf-sync-epic-to-jira.md +8 -179
  111. package/pennyfarthing-dist/commands/pf-sync-work-with-sprint.md +8 -368
  112. package/pennyfarthing-dist/commands/pf-update-domain-docs.md +8 -78
  113. package/pennyfarthing-dist/commands/solo.md +8 -442
  114. package/pennyfarthing-dist/guides/agent-behavior.md +13 -13
  115. package/pennyfarthing-dist/guides/agent-coordination.md +7 -7
  116. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -5
  117. package/pennyfarthing-dist/guides/bikerack.md +128 -0
  118. package/pennyfarthing-dist/guides/brownfield-tools.md +133 -0
  119. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +2 -2
  120. package/pennyfarthing-dist/guides/gate-schema.md +227 -0
  121. package/pennyfarthing-dist/guides/gates.md +120 -0
  122. package/pennyfarthing-dist/guides/handoff-cli.md +116 -0
  123. package/pennyfarthing-dist/guides/hooks.md +86 -4
  124. package/pennyfarthing-dist/guides/output-styles.md +65 -0
  125. package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +5 -5
  126. package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -4
  127. package/pennyfarthing-dist/guides/prompt-patterns.md +5 -5
  128. package/pennyfarthing-dist/guides/reflector.md +4 -4
  129. package/pennyfarthing-dist/guides/session-artifacts.md +1 -1
  130. package/pennyfarthing-dist/guides/skill-schema.md +1 -1
  131. package/pennyfarthing-dist/guides/tandem-protocol.md +13 -1
  132. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  133. package/pennyfarthing-dist/guides/xml-tags.md +5 -4
  134. package/pennyfarthing-dist/personas/themes/hogans-heroes.yaml +11 -22
  135. package/pennyfarthing-dist/personas/themes/stephen-king.yaml +13 -24
  136. package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
  137. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  138. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +19 -14
  139. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +191 -57
  140. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +26 -10
  141. package/pennyfarthing-dist/skills/pf-changelog/SKILL.md +4 -4
  142. package/pennyfarthing-dist/skills/pf-sprint/skill.md +1 -1
  143. package/pennyfarthing-dist/skills/skill-registry.schema.json +4 -0
  144. package/pennyfarthing-dist/skills/skill-registry.yaml +5 -0
  145. package/pennyfarthing-dist/workflows/2party-tdd.yaml +11 -0
  146. package/pennyfarthing-dist/workflows/agent-docs.yaml +2 -0
  147. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +4 -0
  148. package/pennyfarthing-dist/workflows/bdd.yaml +4 -0
  149. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  150. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +3 -0
  151. package/pennyfarthing-dist/workflows/tdd.yaml +3 -0
  152. package/pennyfarthing-dist/workflows/trivial.yaml +2 -0
  153. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
  155. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  156. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  157. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  158. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  159. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  160. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  162. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  163. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  164. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  165. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  167. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  172. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  183. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  184. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  185. package/pennyfarthing_scripts/bikerack/cli.py +10 -11
  186. package/pennyfarthing_scripts/bikerack/debug_panel.py +218 -0
  187. package/pennyfarthing_scripts/bikerack/diffs_panel.py +203 -27
  188. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  189. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  190. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  191. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  192. package/pennyfarthing_scripts/cli.py +114 -0
  193. package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
  195. package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  196. package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  206. package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
  211. package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  212. package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  213. package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
  214. package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
  220. package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/epic/__init__.py +0 -0
  222. package/pennyfarthing_scripts/epic/cli.py +64 -0
  223. package/pennyfarthing_scripts/gate/__init__.py +1 -0
  224. package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/gate/cli.py +56 -0
  228. package/pennyfarthing_scripts/gate/validate.py +266 -0
  229. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/git_group/__init__.py +0 -0
  233. package/pennyfarthing_scripts/git_group/cli.py +100 -0
  234. package/pennyfarthing_scripts/handoff/__init__.py +1 -0
  235. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  237. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  238. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  239. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  240. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  241. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  242. package/pennyfarthing_scripts/handoff/cli.py +120 -0
  243. package/pennyfarthing_scripts/handoff/complete_phase.py +155 -0
  244. package/pennyfarthing_scripts/handoff/gate_file.py +105 -0
  245. package/pennyfarthing_scripts/handoff/gate_runner.py +152 -0
  246. package/pennyfarthing_scripts/handoff/marker.py +109 -0
  247. package/pennyfarthing_scripts/handoff/resolve_gate.py +152 -0
  248. package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  257. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  258. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  259. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  260. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  261. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  262. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  263. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  264. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  265. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  266. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  267. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  268. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  269. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  270. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  271. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  283. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  289. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
  291. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  292. package/pennyfarthing_scripts/prime/workflow.py +39 -0
  293. package/pennyfarthing_scripts/session/__init__.py +0 -0
  294. package/pennyfarthing_scripts/session/cli.py +87 -0
  295. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  296. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  297. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  298. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  299. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
  303. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  304. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  305. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  306. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  307. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  309. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
  313. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  315. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  316. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  317. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  318. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  319. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  320. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  321. package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
  322. package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
  323. package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
  324. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  325. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  326. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  327. package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
  328. package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
  329. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  330. package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
  331. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  332. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  333. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  334. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  335. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  336. package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
  337. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  338. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  339. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  340. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  341. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
  342. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  343. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
  344. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  345. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  346. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  347. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  348. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
  353. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  354. package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
  355. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  356. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  357. package/pennyfarthing_scripts/tests/test_108_1_gate_migration.py +540 -0
  358. package/pennyfarthing_scripts/tests/test_108_2_remove_handoff_fallback.py +339 -0
  359. package/pennyfarthing_scripts/tests/test_confidence_sm_evaluation.py +253 -0
  360. package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +315 -0
  361. package/pennyfarthing_scripts/tests/test_gate_file_resolution.py +341 -0
  362. package/pennyfarthing_scripts/tests/test_gate_runner.py +620 -0
  363. package/pennyfarthing_scripts/tests/test_handoff_cli.py +929 -0
  364. package/pennyfarthing_scripts/tests/test_handoff_e2e.py +454 -0
  365. package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +464 -0
  366. package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  367. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  368. package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  369. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  370. package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  371. package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
  372. package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
  373. package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
  374. package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
  375. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  376. package/pennyfarthing_scripts/validate/adapters/skill_command.py +200 -0
  377. package/pennyfarthing_scripts/validate/adapters/workflow.py +64 -0
  378. package/pennyfarthing_scripts/validate/cli.py +15 -4
  379. package/packages/core/dist/scripts/benchmark-integration.d.ts +0 -182
  380. package/packages/core/dist/scripts/benchmark-integration.d.ts.map +0 -1
  381. package/packages/core/dist/scripts/benchmark-integration.js +0 -691
  382. package/packages/core/dist/scripts/benchmark-integration.js.map +0 -1
  383. package/packages/core/dist/scripts/job-fair-aggregator.d.ts +0 -150
  384. package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +0 -1
  385. package/packages/core/dist/scripts/job-fair-aggregator.js +0 -547
  386. package/packages/core/dist/scripts/job-fair-aggregator.js.map +0 -1
  387. package/pennyfarthing-dist/agents/handoff.md +0 -250
  388. package/pennyfarthing-dist/agents/sm-handoff.md +0 -152
  389. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -112
  390. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  391. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  392. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  393. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  394. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  395. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  396. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  397. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  398. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  399. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  400. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  401. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -0,0 +1,315 @@
1
+ """Tests for SM confidence gate file — Story 90-2.
2
+
3
+ Epic: 90 (Confidence Circuit Breaker via Gate)
4
+ Story: 90-2 — Implement SM confidence gate file
5
+
6
+ Tests the confidence-sm gate file that checks whether an instruction to the
7
+ SM agent is ambiguous. If ambiguous, <fail> returns clarifying options. If
8
+ unambiguous, <pass> lets the agent proceed.
9
+
10
+ Acceptance Criteria:
11
+ - [AC1] Gate file exists in pennyfarthing-dist/gates/ following Gate PRD schema
12
+ - [AC2] Gate has <gate>, <purpose>, <pass>, <fail> blocks
13
+ - [AC3] Gate checks whether SM instruction is ambiguous
14
+ - [AC4] <fail> block returns clarifying options when ambiguous
15
+ - [AC5] <pass> block lets the agent proceed when unambiguous
16
+ - [AC6] Gate uses model="haiku"
17
+ - [AC7] Gate validates against existing schema (same structure as tests-pass.md)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from pathlib import Path
23
+
24
+ import pytest
25
+
26
+ from pennyfarthing_scripts.handoff.gate_file import resolve_gate_file
27
+ from pennyfarthing_scripts.handoff.gate_runner import parse_gate_file
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Fixtures
31
+ # ---------------------------------------------------------------------------
32
+
33
+ GATE_NAME = "confidence-sm"
34
+
35
+ # The gate file lives in pennyfarthing-dist/gates/ relative to the framework root
36
+ # In the dogfooding context, the project root is the orchestrator, so we need
37
+ # to resolve paths relative to this test file.
38
+ _THIS_DIR = Path(__file__).resolve().parent
39
+ _SCRIPTS_DIR = _THIS_DIR.parent # pennyfarthing_scripts/
40
+ _FRAMEWORK_ROOT = _SCRIPTS_DIR.parent # pennyfarthing/
41
+ _GATE_FILE = _FRAMEWORK_ROOT / "pennyfarthing-dist" / "gates" / f"{GATE_NAME}.md"
42
+
43
+
44
+ @pytest.fixture
45
+ def gate_path() -> Path:
46
+ """Return the expected path to the confidence-sm gate file."""
47
+ return _GATE_FILE
48
+
49
+
50
+ @pytest.fixture
51
+ def gate_content(gate_path: Path) -> str:
52
+ """Read and return the gate file content. Fails if file missing."""
53
+ assert gate_path.is_file(), f"Gate file not found: {gate_path}"
54
+ return gate_path.read_text()
55
+
56
+
57
+ @pytest.fixture
58
+ def parsed_gate(gate_path: Path) -> dict:
59
+ """Parse the gate file using the gate runner's parse_gate_file."""
60
+ return parse_gate_file(gate_path)
61
+
62
+
63
+ # ===========================================================================
64
+ # AC1: Gate file exists in pennyfarthing-dist/gates/
65
+ # ===========================================================================
66
+
67
+
68
+ class TestGateFileExists:
69
+ """AC1: Gate file exists at the expected location."""
70
+
71
+ def test_gate_file_exists(self, gate_path: Path) -> None:
72
+ """AC1: confidence-sm.md exists in pennyfarthing-dist/gates/."""
73
+ assert gate_path.is_file(), f"Gate file not found: {gate_path}"
74
+
75
+ def test_gate_file_not_empty(self, gate_path: Path) -> None:
76
+ """AC1: Gate file is not empty."""
77
+ assert gate_path.is_file(), f"Gate file not found: {gate_path}"
78
+ content = gate_path.read_text()
79
+ assert len(content.strip()) > 0, "Gate file is empty"
80
+
81
+ def test_gate_discoverable(self, gate_path: Path) -> None:
82
+ """AC1: Gate is discoverable via resolve_gate_file()."""
83
+ # Use the framework root which has pennyfarthing-dist/gates/
84
+ result = resolve_gate_file(GATE_NAME, project_root=_FRAMEWORK_ROOT)
85
+ assert result["status"] == "found", (
86
+ f"Gate not discoverable: {result.get('error')}"
87
+ )
88
+
89
+ def test_gate_discoverable_with_prefix(self, gate_path: Path) -> None:
90
+ """AC1: Gate discoverable with gates/ prefix."""
91
+ result = resolve_gate_file(
92
+ f"gates/{GATE_NAME}", project_root=_FRAMEWORK_ROOT
93
+ )
94
+ assert result["status"] == "found"
95
+
96
+
97
+ # ===========================================================================
98
+ # AC2: Gate has <gate>, <purpose>, <pass>, <fail> blocks
99
+ # ===========================================================================
100
+
101
+
102
+ class TestGateSchemaStructure:
103
+ """AC2: Gate file has all required XML-tagged blocks."""
104
+
105
+ def test_has_gate_tag(self, gate_content: str) -> None:
106
+ """AC2: File contains a <gate> opening tag."""
107
+ assert "<gate " in gate_content or "<gate>" in gate_content
108
+
109
+ def test_has_closing_gate_tag(self, gate_content: str) -> None:
110
+ """AC2: File contains a </gate> closing tag."""
111
+ assert "</gate>" in gate_content
112
+
113
+ def test_has_purpose_block(self, gate_content: str) -> None:
114
+ """AC2: File contains <purpose> and </purpose> tags."""
115
+ assert "<purpose>" in gate_content
116
+ assert "</purpose>" in gate_content
117
+
118
+ def test_has_pass_block(self, gate_content: str) -> None:
119
+ """AC2: File contains <pass> and </pass> tags."""
120
+ assert "<pass>" in gate_content
121
+ assert "</pass>" in gate_content
122
+
123
+ def test_has_fail_block(self, gate_content: str) -> None:
124
+ """AC2: File contains <fail> and </fail> tags."""
125
+ assert "<fail>" in gate_content
126
+ assert "</fail>" in gate_content
127
+
128
+ def test_parses_without_error(self, parsed_gate: dict) -> None:
129
+ """AC2: parse_gate_file returns status 'ok'."""
130
+ assert parsed_gate["status"] == "ok", (
131
+ f"Parse error: {parsed_gate.get('error')}"
132
+ )
133
+
134
+
135
+ # ===========================================================================
136
+ # AC3: Gate checks whether SM instruction is ambiguous
137
+ # ===========================================================================
138
+
139
+
140
+ class TestGateAmbiguityDetection:
141
+ """AC3: Gate content describes checking for ambiguous SM instructions."""
142
+
143
+ def test_purpose_mentions_ambiguity(self, gate_content: str) -> None:
144
+ """AC3: Purpose section references ambiguity or unclear instructions."""
145
+ # Extract purpose content between tags
146
+ import re
147
+
148
+ purpose_match = re.search(
149
+ r"<purpose>(.*?)</purpose>", gate_content, re.DOTALL
150
+ )
151
+ assert purpose_match is not None, "No <purpose> block found"
152
+ purpose = purpose_match.group(1).lower()
153
+ assert any(
154
+ term in purpose
155
+ for term in ["ambig", "unclear", "vague", "confidence", "clarif"]
156
+ ), f"Purpose doesn't reference ambiguity: {purpose}"
157
+
158
+ def test_gate_name_is_confidence_sm(self, parsed_gate: dict) -> None:
159
+ """AC3: Gate name is 'confidence-sm'."""
160
+ assert parsed_gate["name"] == GATE_NAME
161
+
162
+ def test_content_references_sm_agent(self, gate_content: str) -> None:
163
+ """AC3: Gate content references the SM agent or scrum master role."""
164
+ content_lower = gate_content.lower()
165
+ assert any(
166
+ term in content_lower
167
+ for term in ["sm agent", "scrum master", "sm ", "story management"]
168
+ ), "Gate doesn't reference SM agent"
169
+
170
+
171
+ # ===========================================================================
172
+ # AC4: <fail> block returns clarifying options when ambiguous
173
+ # ===========================================================================
174
+
175
+
176
+ class TestGateFailBlock:
177
+ """AC4: Fail block provides clarifying options for ambiguous instructions."""
178
+
179
+ def test_fail_block_has_content(self, gate_content: str) -> None:
180
+ """AC4: Fail block is not empty."""
181
+ import re
182
+
183
+ fail_match = re.search(r"<fail>(.*?)</fail>", gate_content, re.DOTALL)
184
+ assert fail_match is not None, "No <fail> block found"
185
+ fail_content = fail_match.group(1).strip()
186
+ assert len(fail_content) > 0, "Fail block is empty"
187
+
188
+ def test_fail_block_mentions_clarification(self, gate_content: str) -> None:
189
+ """AC4: Fail block mentions clarifying or asking for more info."""
190
+ import re
191
+
192
+ fail_match = re.search(r"<fail>(.*?)</fail>", gate_content, re.DOTALL)
193
+ assert fail_match is not None
194
+ fail_content = fail_match.group(1).lower()
195
+ assert any(
196
+ term in fail_content
197
+ for term in ["clarif", "option", "which", "specify", "did you mean"]
198
+ ), f"Fail block doesn't offer clarifying options: {fail_content[:200]}"
199
+
200
+ def test_fail_block_has_gate_result(self, gate_content: str) -> None:
201
+ """AC4: Fail block includes GATE_RESULT template with status: fail."""
202
+ import re
203
+
204
+ fail_match = re.search(r"<fail>(.*?)</fail>", gate_content, re.DOTALL)
205
+ assert fail_match is not None
206
+ fail_content = fail_match.group(1)
207
+ assert "GATE_RESULT" in fail_content, "Fail block missing GATE_RESULT"
208
+ assert "status: fail" in fail_content, "Fail block missing status: fail"
209
+
210
+
211
+ # ===========================================================================
212
+ # AC5: <pass> block lets the agent proceed when unambiguous
213
+ # ===========================================================================
214
+
215
+
216
+ class TestGatePassBlock:
217
+ """AC5: Pass block describes proceeding when instruction is clear."""
218
+
219
+ def test_pass_block_has_content(self, gate_content: str) -> None:
220
+ """AC5: Pass block is not empty."""
221
+ import re
222
+
223
+ pass_match = re.search(r"<pass>(.*?)</pass>", gate_content, re.DOTALL)
224
+ assert pass_match is not None, "No <pass> block found"
225
+ pass_content = pass_match.group(1).strip()
226
+ assert len(pass_content) > 0, "Pass block is empty"
227
+
228
+ def test_pass_block_indicates_proceed(self, gate_content: str) -> None:
229
+ """AC5: Pass block indicates the agent should proceed."""
230
+ import re
231
+
232
+ pass_match = re.search(r"<pass>(.*?)</pass>", gate_content, re.DOTALL)
233
+ assert pass_match is not None
234
+ pass_content = pass_match.group(1).lower()
235
+ assert any(
236
+ term in pass_content
237
+ for term in ["proceed", "continue", "clear", "unambig", "confident"]
238
+ ), f"Pass block doesn't indicate proceeding: {pass_content[:200]}"
239
+
240
+ def test_pass_block_has_gate_result(self, gate_content: str) -> None:
241
+ """AC5: Pass block includes GATE_RESULT template with status: pass."""
242
+ import re
243
+
244
+ pass_match = re.search(r"<pass>(.*?)</pass>", gate_content, re.DOTALL)
245
+ assert pass_match is not None
246
+ pass_content = pass_match.group(1)
247
+ assert "GATE_RESULT" in pass_content, "Pass block missing GATE_RESULT"
248
+ assert "status: pass" in pass_content, "Pass block missing status: pass"
249
+
250
+
251
+ # ===========================================================================
252
+ # AC6: Gate uses model="haiku"
253
+ # ===========================================================================
254
+
255
+
256
+ class TestGateModel:
257
+ """AC6: Gate specifies model="haiku" per Gate PRD defaults."""
258
+
259
+ def test_model_is_haiku(self, parsed_gate: dict) -> None:
260
+ """AC6: parse_gate_file extracts model as 'haiku' from a valid gate."""
261
+ assert parsed_gate["status"] == "ok", (
262
+ f"Gate parse failed: {parsed_gate.get('error')}"
263
+ )
264
+ assert parsed_gate["model"] == "haiku", (
265
+ f"Expected model 'haiku', got '{parsed_gate['model']}'"
266
+ )
267
+
268
+ def test_gate_tag_has_model_attribute(self, gate_content: str) -> None:
269
+ """AC6: <gate> tag explicitly includes model="haiku"."""
270
+ import re
271
+
272
+ gate_match = re.search(r"<gate\b[^>]*>", gate_content)
273
+ assert gate_match is not None
274
+ gate_tag = gate_match.group(0)
275
+ assert 'model="haiku"' in gate_tag, (
276
+ f"<gate> tag missing model=\"haiku\": {gate_tag}"
277
+ )
278
+
279
+
280
+ # ===========================================================================
281
+ # AC7: Validates against existing schema (same structure as tests-pass.md)
282
+ # ===========================================================================
283
+
284
+
285
+ class TestGateSchemaConsistency:
286
+ """AC7: Gate follows the same structural patterns as tests-pass.md."""
287
+
288
+ def test_result_has_all_required_keys(self, parsed_gate: dict) -> None:
289
+ """AC7: parse_gate_file result has all expected keys and parse succeeds."""
290
+ assert parsed_gate["status"] == "ok", (
291
+ f"Gate parse failed: {parsed_gate.get('error')}"
292
+ )
293
+ for key in ("status", "name", "model", "content", "error"):
294
+ assert key in parsed_gate, f"Missing key: {key}"
295
+
296
+ def test_name_matches_filename(self, parsed_gate: dict) -> None:
297
+ """AC7: Gate name attribute matches the filename convention."""
298
+ assert parsed_gate["name"] == GATE_NAME
299
+
300
+ def test_content_is_full_file(self, gate_path: Path, parsed_gate: dict) -> None:
301
+ """AC7: Parsed content matches raw file read."""
302
+ assert parsed_gate["status"] == "ok"
303
+ raw = gate_path.read_text()
304
+ assert parsed_gate["content"] == raw
305
+
306
+ def test_gate_result_yaml_in_both_blocks(self, gate_content: str) -> None:
307
+ """AC7: Both pass and fail blocks include GATE_RESULT YAML blocks."""
308
+ import re
309
+
310
+ pass_match = re.search(r"<pass>(.*?)</pass>", gate_content, re.DOTALL)
311
+ fail_match = re.search(r"<fail>(.*?)</fail>", gate_content, re.DOTALL)
312
+ assert pass_match is not None
313
+ assert fail_match is not None
314
+ assert "GATE_RESULT:" in pass_match.group(1)
315
+ assert "GATE_RESULT:" in fail_match.group(1)
@@ -0,0 +1,341 @@
1
+ """Tests for gate file discovery and resolution — Story 106-4.
2
+
3
+ Epic: 106 (Gate Files & First Migration)
4
+ Story: 106-4 — Gate file discovery and resolution
5
+
6
+ Tests the resolve_gate_file() function that locates gate definition files
7
+ using a priority-based discovery algorithm.
8
+
9
+ Acceptance Criteria:
10
+ - [AC1] resolve_gate_file() resolves gate names to file paths
11
+ - [AC2] Resolution order: .pennyfarthing/gates/{name}.md → pennyfarthing-dist/gates/{name}.md
12
+ - [AC3] Non-existent gate file returns error/blocked status
13
+ - [AC5] Tests cover: found in local, found in built-in, not found, symlink resolution
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+
20
+ import pytest
21
+
22
+ from pennyfarthing_scripts.handoff.gate_file import resolve_gate_file
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Fixtures: Project structure
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ @pytest.fixture
30
+ def project(tmp_path: Path) -> Path:
31
+ """Create a minimal project structure with .pennyfarthing/ and gates."""
32
+ (tmp_path / ".pennyfarthing").mkdir()
33
+ (tmp_path / ".pennyfarthing" / "gates").mkdir()
34
+ (tmp_path / "pennyfarthing-dist" / "gates").mkdir(parents=True)
35
+ return tmp_path
36
+
37
+
38
+ @pytest.fixture
39
+ def project_with_builtin_gate(project: Path) -> Path:
40
+ """Project with a gate file in pennyfarthing-dist/gates/ only."""
41
+ gate = project / "pennyfarthing-dist" / "gates" / "tests-pass.md"
42
+ gate.write_text(
43
+ '<gate name="tests-pass" model="haiku">\n'
44
+ " <purpose>Verify tests pass</purpose>\n"
45
+ " <pass>Check tests</pass>\n"
46
+ " <fail>Report failures</fail>\n"
47
+ "</gate>\n"
48
+ )
49
+ return project
50
+
51
+
52
+ @pytest.fixture
53
+ def project_with_local_gate(project: Path) -> Path:
54
+ """Project with a gate file in .pennyfarthing/gates/ only."""
55
+ gate = project / ".pennyfarthing" / "gates" / "custom-gate.md"
56
+ gate.write_text(
57
+ '<gate name="custom-gate" model="haiku">\n'
58
+ " <purpose>Custom project gate</purpose>\n"
59
+ " <pass>Custom pass</pass>\n"
60
+ " <fail>Custom fail</fail>\n"
61
+ "</gate>\n"
62
+ )
63
+ return project
64
+
65
+
66
+ @pytest.fixture
67
+ def project_with_both_gates(project: Path) -> Path:
68
+ """Project with same gate in both locations (local should win)."""
69
+ # Built-in version
70
+ builtin = project / "pennyfarthing-dist" / "gates" / "tests-pass.md"
71
+ builtin.write_text(
72
+ '<gate name="tests-pass" model="haiku">\n'
73
+ " <purpose>Built-in version</purpose>\n"
74
+ " <pass>Built-in pass</pass>\n"
75
+ " <fail>Built-in fail</fail>\n"
76
+ "</gate>\n"
77
+ )
78
+ # Local override
79
+ local = project / ".pennyfarthing" / "gates" / "tests-pass.md"
80
+ local.write_text(
81
+ '<gate name="tests-pass" model="haiku">\n'
82
+ " <purpose>Local override version</purpose>\n"
83
+ " <pass>Local pass</pass>\n"
84
+ " <fail>Local fail</fail>\n"
85
+ "</gate>\n"
86
+ )
87
+ return project
88
+
89
+
90
+ @pytest.fixture
91
+ def project_with_symlinked_gates(project: Path) -> Path:
92
+ """Project where .pennyfarthing/gates is a symlink to pennyfarthing-dist/gates."""
93
+ import shutil
94
+
95
+ # Remove the real .pennyfarthing/gates dir
96
+ shutil.rmtree(project / ".pennyfarthing" / "gates")
97
+ # Create symlink (mimics real install)
98
+ (project / ".pennyfarthing" / "gates").symlink_to(
99
+ project / "pennyfarthing-dist" / "gates"
100
+ )
101
+ # Add a gate file to the source
102
+ gate = project / "pennyfarthing-dist" / "gates" / "tests-pass.md"
103
+ gate.write_text(
104
+ '<gate name="tests-pass" model="haiku">\n'
105
+ " <purpose>Symlinked gate</purpose>\n"
106
+ " <pass>Check tests</pass>\n"
107
+ " <fail>Report failures</fail>\n"
108
+ "</gate>\n"
109
+ )
110
+ return project
111
+
112
+
113
+ # ===========================================================================
114
+ # AC1: resolve_gate_file() resolves gate names to file paths
115
+ # ===========================================================================
116
+
117
+
118
+ class TestResolveGateFileFound:
119
+ """AC1: Function returns path when gate file exists."""
120
+
121
+ def test_returns_found_status(
122
+ self, project_with_builtin_gate: Path
123
+ ) -> None:
124
+ """AC1: Status should be 'found' when gate file exists."""
125
+ result = resolve_gate_file(
126
+ "tests-pass", project_root=project_with_builtin_gate
127
+ )
128
+ assert result["status"] == "found"
129
+
130
+ def test_returns_absolute_path(
131
+ self, project_with_builtin_gate: Path
132
+ ) -> None:
133
+ """AC1: Path should be an absolute path string."""
134
+ result = resolve_gate_file(
135
+ "tests-pass", project_root=project_with_builtin_gate
136
+ )
137
+ assert result["path"] is not None
138
+ assert Path(result["path"]).is_absolute()
139
+
140
+ def test_path_points_to_existing_file(
141
+ self, project_with_builtin_gate: Path
142
+ ) -> None:
143
+ """AC1: Returned path should point to an actual file."""
144
+ result = resolve_gate_file(
145
+ "tests-pass", project_root=project_with_builtin_gate
146
+ )
147
+ assert Path(result["path"]).is_file()
148
+
149
+ def test_error_is_none_when_found(
150
+ self, project_with_builtin_gate: Path
151
+ ) -> None:
152
+ """AC1: Error field should be None when gate is found."""
153
+ result = resolve_gate_file(
154
+ "tests-pass", project_root=project_with_builtin_gate
155
+ )
156
+ assert result["error"] is None
157
+
158
+ def test_strips_gates_prefix(
159
+ self, project_with_builtin_gate: Path
160
+ ) -> None:
161
+ """AC1: 'gates/tests-pass' and 'tests-pass' should resolve the same."""
162
+ result_bare = resolve_gate_file(
163
+ "tests-pass", project_root=project_with_builtin_gate
164
+ )
165
+ result_prefixed = resolve_gate_file(
166
+ "gates/tests-pass", project_root=project_with_builtin_gate
167
+ )
168
+ assert result_bare["path"] == result_prefixed["path"]
169
+
170
+ def test_result_has_required_fields(
171
+ self, project_with_builtin_gate: Path
172
+ ) -> None:
173
+ """AC1: Result dict must have status, path, error keys."""
174
+ result = resolve_gate_file(
175
+ "tests-pass", project_root=project_with_builtin_gate
176
+ )
177
+ assert "status" in result
178
+ assert "path" in result
179
+ assert "error" in result
180
+
181
+
182
+ # ===========================================================================
183
+ # AC2: Resolution order — local first, built-in fallback
184
+ # ===========================================================================
185
+
186
+
187
+ class TestResolveGateFileOrder:
188
+ """AC2: .pennyfarthing/gates/ takes priority over pennyfarthing-dist/gates/."""
189
+
190
+ def test_local_gate_found(self, project_with_local_gate: Path) -> None:
191
+ """AC2: Gate in .pennyfarthing/gates/ should be found."""
192
+ result = resolve_gate_file(
193
+ "custom-gate", project_root=project_with_local_gate
194
+ )
195
+ assert result["status"] == "found"
196
+ assert ".pennyfarthing/gates/custom-gate.md" in result["path"]
197
+
198
+ def test_builtin_gate_found_as_fallback(
199
+ self, project_with_builtin_gate: Path
200
+ ) -> None:
201
+ """AC2: Gate in pennyfarthing-dist/gates/ found when not in local."""
202
+ result = resolve_gate_file(
203
+ "tests-pass", project_root=project_with_builtin_gate
204
+ )
205
+ assert result["status"] == "found"
206
+ assert "pennyfarthing-dist/gates/tests-pass.md" in result["path"]
207
+
208
+ def test_local_overrides_builtin(
209
+ self, project_with_both_gates: Path
210
+ ) -> None:
211
+ """AC2: When same gate exists in both, local wins."""
212
+ result = resolve_gate_file(
213
+ "tests-pass", project_root=project_with_both_gates
214
+ )
215
+ assert result["status"] == "found"
216
+ # Path should be the .pennyfarthing/gates/ version, not pennyfarthing-dist/
217
+ assert ".pennyfarthing/gates/tests-pass.md" in result["path"]
218
+
219
+ def test_local_override_content_is_local_version(
220
+ self, project_with_both_gates: Path
221
+ ) -> None:
222
+ """AC2: Content at resolved path should be the local version."""
223
+ result = resolve_gate_file(
224
+ "tests-pass", project_root=project_with_both_gates
225
+ )
226
+ content = Path(result["path"]).read_text()
227
+ assert "Local override version" in content
228
+
229
+
230
+ # ===========================================================================
231
+ # AC3: Non-existent gate file returns error/blocked
232
+ # ===========================================================================
233
+
234
+
235
+ class TestResolveGateFileNotFound:
236
+ """AC3: Missing gate files return error status."""
237
+
238
+ def test_missing_gate_returns_not_found(self, project: Path) -> None:
239
+ """AC3: Non-existent gate → status: not_found."""
240
+ result = resolve_gate_file("nonexistent-gate", project_root=project)
241
+ assert result["status"] == "not_found"
242
+
243
+ def test_missing_gate_path_is_none(self, project: Path) -> None:
244
+ """AC3: Non-existent gate → path: None."""
245
+ result = resolve_gate_file("nonexistent-gate", project_root=project)
246
+ assert result["path"] is None
247
+
248
+ def test_missing_gate_has_error_message(self, project: Path) -> None:
249
+ """AC3: Non-existent gate → error message present."""
250
+ result = resolve_gate_file("nonexistent-gate", project_root=project)
251
+ assert result["error"] is not None
252
+ assert len(result["error"]) > 0
253
+
254
+ def test_empty_gate_name_returns_not_found(self, project: Path) -> None:
255
+ """AC3: Empty string gate name → not_found."""
256
+ result = resolve_gate_file("", project_root=project)
257
+ assert result["status"] == "not_found"
258
+
259
+ def test_missing_gates_directory_returns_not_found(
260
+ self, tmp_path: Path
261
+ ) -> None:
262
+ """AC3: No gates/ directories at all → not_found (no crash)."""
263
+ # Project with .pennyfarthing but no gates subdirectory
264
+ (tmp_path / ".pennyfarthing").mkdir()
265
+ result = resolve_gate_file("tests-pass", project_root=tmp_path)
266
+ assert result["status"] == "not_found"
267
+
268
+
269
+ # ===========================================================================
270
+ # AC5: Symlink resolution
271
+ # ===========================================================================
272
+
273
+
274
+ class TestResolveGateFileSymlink:
275
+ """AC5: Gate files found through symlinked .pennyfarthing/gates/."""
276
+
277
+ def test_symlinked_gate_found(
278
+ self, project_with_symlinked_gates: Path
279
+ ) -> None:
280
+ """AC5: Gate found through .pennyfarthing/gates/ symlink."""
281
+ result = resolve_gate_file(
282
+ "tests-pass", project_root=project_with_symlinked_gates
283
+ )
284
+ assert result["status"] == "found"
285
+
286
+ def test_symlinked_gate_path_is_valid(
287
+ self, project_with_symlinked_gates: Path
288
+ ) -> None:
289
+ """AC5: Path through symlink points to real file."""
290
+ result = resolve_gate_file(
291
+ "tests-pass", project_root=project_with_symlinked_gates
292
+ )
293
+ assert result["path"] is not None
294
+ assert Path(result["path"]).exists()
295
+
296
+ def test_symlinked_gate_content_readable(
297
+ self, project_with_symlinked_gates: Path
298
+ ) -> None:
299
+ """AC5: Content at symlinked path is the actual gate file."""
300
+ result = resolve_gate_file(
301
+ "tests-pass", project_root=project_with_symlinked_gates
302
+ )
303
+ content = Path(result["path"]).read_text()
304
+ assert "Symlinked gate" in content
305
+
306
+
307
+ # ===========================================================================
308
+ # Edge cases
309
+ # ===========================================================================
310
+
311
+
312
+ class TestResolveGateFileEdgeCases:
313
+ """Edge cases and boundary conditions."""
314
+
315
+ def test_gate_name_with_md_extension_still_works(
316
+ self, project_with_builtin_gate: Path
317
+ ) -> None:
318
+ """Edge: 'tests-pass.md' should resolve same as 'tests-pass'."""
319
+ result = resolve_gate_file(
320
+ "tests-pass.md", project_root=project_with_builtin_gate
321
+ )
322
+ assert result["status"] == "found"
323
+
324
+ def test_gate_name_with_nested_path_rejected(
325
+ self, project_with_builtin_gate: Path
326
+ ) -> None:
327
+ """Edge: '../escape/tests-pass' should not resolve (path traversal)."""
328
+ result = resolve_gate_file(
329
+ "../escape/tests-pass",
330
+ project_root=project_with_builtin_gate,
331
+ )
332
+ assert result["status"] == "not_found"
333
+
334
+ def test_result_status_is_valid_enum(
335
+ self, project_with_builtin_gate: Path
336
+ ) -> None:
337
+ """Edge: Status must be one of the expected values."""
338
+ result = resolve_gate_file(
339
+ "tests-pass", project_root=project_with_builtin_gate
340
+ )
341
+ assert result["status"] in ("found", "not_found")