@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,929 @@
1
+ """Tests for pf handoff — Script-First Handoff (Story 105-1).
2
+
3
+ Epic: 105 (Script-First Handoff)
4
+ Story: 105-1 — Create handoff CLI with resolve-gate and complete-phase
5
+
6
+ Tests the two subcommands:
7
+ - resolve-gate: reads workflow YAML, resolves gate, returns RESOLVE_RESULT
8
+ - complete-phase: atomically updates session file with phase transition
9
+
10
+ Acceptance Criteria:
11
+ - [AC1] resolve-gate reads workflow YAML, finds current phase gate,
12
+ checks assessment, returns structured RESOLVE_RESULT
13
+ - [AC2] complete-phase atomically updates session file (temp+mv) with
14
+ phase transition, timestamps, and history tables
15
+ - [AC3] Python module in pennyfarthing_scripts.handoff
16
+ - [AC4] stdout is the only communication channel — no side-channel files
17
+ - [AC5] Exit codes: 0 = ready/skip, 1 = blocked
18
+ - [AC6] YAML parsing via PyYAML
19
+ - [AC7] Session parsing via Python string/regex
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ import textwrap
26
+ from pathlib import Path
27
+ from unittest.mock import patch
28
+
29
+ import pytest
30
+ import yaml
31
+ from click.testing import CliRunner
32
+
33
+ from pennyfarthing_scripts.cli import cli
34
+ from pennyfarthing_scripts.handoff.complete_phase import complete_phase
35
+ from pennyfarthing_scripts.handoff.resolve_gate import resolve_gate
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Fixtures: Workflow YAML data
39
+ # ---------------------------------------------------------------------------
40
+
41
+ TDD_WORKFLOW = {
42
+ "workflow": {
43
+ "name": "tdd",
44
+ "phases": [
45
+ {"name": "setup", "agent": "sm"},
46
+ {"name": "red", "agent": "tea", "gate": {"type": "tests_fail"}},
47
+ {"name": "green", "agent": "dev", "gate": {"type": "tests_pass"}},
48
+ {"name": "review", "agent": "reviewer", "gate": {"type": "approval"}},
49
+ {"name": "finish", "agent": "sm"},
50
+ ],
51
+ }
52
+ }
53
+
54
+ TRIVIAL_WORKFLOW = {
55
+ "workflow": {
56
+ "name": "trivial",
57
+ "phases": [
58
+ {"name": "setup", "agent": "sm"},
59
+ {"name": "implement", "agent": "dev", "gate": {"type": "tests_pass"}},
60
+ {"name": "review", "agent": "reviewer", "gate": {"type": "approval"}},
61
+ {"name": "finish", "agent": "sm"},
62
+ ],
63
+ }
64
+ }
65
+
66
+ PATCH_WORKFLOW = {
67
+ "workflow": {
68
+ "name": "patch",
69
+ "phases": [
70
+ {"name": "fix", "agent": "dev", "gate": {"type": "manual"}},
71
+ ],
72
+ }
73
+ }
74
+
75
+ BDD_WORKFLOW = {
76
+ "workflow": {
77
+ "name": "bdd",
78
+ "phases": [
79
+ {"name": "setup", "agent": "sm"},
80
+ {"name": "design", "agent": "ux-designer", "gate": {"type": "design_review"}},
81
+ {"name": "red", "agent": "tea", "gate": {"type": "tests_fail"}},
82
+ {"name": "green", "agent": "dev", "gate": {"type": "tests_pass"}},
83
+ {"name": "review", "agent": "reviewer", "gate": {"type": "approval"}},
84
+ {"name": "finish", "agent": "sm"},
85
+ ],
86
+ }
87
+ }
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Fixtures: Session file content
92
+ # ---------------------------------------------------------------------------
93
+
94
+ SESSION_WITH_ASSESSMENT = textwrap.dedent("""\
95
+ # Story 105-1: Create handoff-cli with resolve-gate and complete-phase
96
+
97
+ **Story ID:** 105-1
98
+ **Workflow:** tdd
99
+ **Phase:** green
100
+ **Phase Started:** 2026-02-15T07:53:07Z
101
+
102
+ ## TEA Assessment
103
+
104
+ **Tests Written:** 5 tests
105
+ **Status:** RED confirmed
106
+
107
+ ## Workflow Tracking
108
+
109
+ **Phase:** green
110
+ **Phase Started:** 2026-02-15T07:53:07Z
111
+
112
+ ### Phase History
113
+ | Phase | Started | Ended | Duration |
114
+ |-------|---------|-------|----------|
115
+ | setup | 2026-02-15T07:50:00Z | 2026-02-15T07:52:00Z | 2m |
116
+ | red | 2026-02-15T07:52:00Z | 2026-02-15T07:53:07Z | 1m |
117
+ | green | 2026-02-15T07:53:07Z | - | - |
118
+
119
+ ### Handoff History
120
+ | From | To | Gate | Status | Timestamp |
121
+ |------|-----|------|--------|-----------|
122
+ | setup (sm) | red (tea) | - | PASSED | 2026-02-15T07:52:00Z |
123
+ | red (tea) | green (dev) | tests_fail | PASSED | 2026-02-15T07:53:07Z |
124
+ """)
125
+
126
+ SESSION_WITHOUT_ASSESSMENT = textwrap.dedent("""\
127
+ # Story 105-1: Create handoff-cli with resolve-gate and complete-phase
128
+
129
+ **Story ID:** 105-1
130
+ **Workflow:** tdd
131
+ **Phase:** green
132
+ **Phase Started:** 2026-02-15T07:53:07Z
133
+
134
+ ## Workflow Tracking
135
+
136
+ **Phase:** green
137
+ **Phase Started:** 2026-02-15T07:53:07Z
138
+
139
+ ### Phase History
140
+ | Phase | Started | Ended | Duration |
141
+ |-------|---------|-------|----------|
142
+ | green | 2026-02-15T07:53:07Z | - | - |
143
+
144
+ ### Handoff History
145
+ | From | To | Gate | Status | Timestamp |
146
+ |------|-----|------|--------|-----------|
147
+ """)
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Fixtures: Project structure
152
+ # ---------------------------------------------------------------------------
153
+
154
+
155
+ @pytest.fixture
156
+ def project(tmp_path: Path) -> Path:
157
+ """Create a minimal project structure with workflow YAMLs."""
158
+ workflows_dir = tmp_path / ".pennyfarthing" / "workflows"
159
+ workflows_dir.mkdir(parents=True)
160
+
161
+ for name, data in [
162
+ ("tdd", TDD_WORKFLOW),
163
+ ("trivial", TRIVIAL_WORKFLOW),
164
+ ("patch", PATCH_WORKFLOW),
165
+ ("bdd", BDD_WORKFLOW),
166
+ ]:
167
+ (workflows_dir / f"{name}.yaml").write_text(
168
+ yaml.dump(data, default_flow_style=False)
169
+ )
170
+
171
+ (tmp_path / ".session").mkdir()
172
+ return tmp_path
173
+
174
+
175
+ @pytest.fixture
176
+ def session_with_assessment(project: Path) -> Path:
177
+ """Create a session file that has an assessment section."""
178
+ session_file = project / ".session" / "105-1-session.md"
179
+ session_file.write_text(SESSION_WITH_ASSESSMENT)
180
+ return session_file
181
+
182
+
183
+ @pytest.fixture
184
+ def session_without_assessment(project: Path) -> Path:
185
+ """Create a session file without an assessment section."""
186
+ session_file = project / ".session" / "105-1-session.md"
187
+ session_file.write_text(SESSION_WITHOUT_ASSESSMENT)
188
+ return session_file
189
+
190
+
191
+ @pytest.fixture
192
+ def runner() -> CliRunner:
193
+ """Create a CLI test runner."""
194
+ return CliRunner()
195
+
196
+
197
+ # ===========================================================================
198
+ # AC1: resolve-gate returns structured RESOLVE_RESULT
199
+ # ===========================================================================
200
+
201
+
202
+ class TestResolveGateReady:
203
+ """AC1 + AC5: resolve-gate returns 'ready' with correct fields."""
204
+
205
+ def test_tdd_green_phase_returns_ready(
206
+ self, project: Path, session_with_assessment: Path
207
+ ) -> None:
208
+ """AC1: TDD green phase (gate=tests_pass) → status: ready."""
209
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
210
+ assert result["status"] == "ready"
211
+
212
+ def test_tdd_green_gate_type_is_tests_pass(
213
+ self, project: Path, session_with_assessment: Path
214
+ ) -> None:
215
+ """AC1: TDD green phase gate type should be tests_pass."""
216
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
217
+ assert result["gate_type"] == "tests_pass"
218
+
219
+ def test_tdd_green_next_agent_is_reviewer(
220
+ self, project: Path, session_with_assessment: Path
221
+ ) -> None:
222
+ """AC1: After TDD green, next agent should be reviewer."""
223
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
224
+ assert result["next_agent"] == "reviewer"
225
+
226
+ def test_tdd_green_next_phase_is_review(
227
+ self, project: Path, session_with_assessment: Path
228
+ ) -> None:
229
+ """AC1: After TDD green, next phase should be review."""
230
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
231
+ assert result["next_phase"] == "review"
232
+
233
+ def test_tdd_red_phase_returns_ready(
234
+ self, project: Path, session_with_assessment: Path
235
+ ) -> None:
236
+ """AC1: TDD red phase (gate=tests_fail) → status: ready."""
237
+ # Rewrite session to be in red phase with assessment
238
+ session_file = project / ".session" / "105-1-session.md"
239
+ content = session_file.read_text().replace(
240
+ "**Phase:** green", "**Phase:** red"
241
+ )
242
+ session_file.write_text(content)
243
+ result = resolve_gate("105-1", "tdd", "red", project_root=project)
244
+ assert result["status"] == "ready"
245
+ assert result["gate_type"] == "tests_fail"
246
+ assert result["next_agent"] == "dev"
247
+ assert result["next_phase"] == "green"
248
+
249
+ def test_trivial_implement_returns_ready(
250
+ self, project: Path, session_with_assessment: Path
251
+ ) -> None:
252
+ """AC1: Trivial implement (gate=tests_pass) → status: ready."""
253
+ result = resolve_gate("105-1", "trivial", "implement", project_root=project)
254
+ assert result["status"] == "ready"
255
+ assert result["gate_type"] == "tests_pass"
256
+ assert result["next_agent"] == "reviewer"
257
+
258
+ def test_bdd_design_returns_ready(
259
+ self, project: Path, session_with_assessment: Path
260
+ ) -> None:
261
+ """AC1: BDD design phase (gate=design_review) → ready."""
262
+ result = resolve_gate("105-1", "bdd", "design", project_root=project)
263
+ assert result["status"] == "ready"
264
+ assert result["gate_type"] == "design_review"
265
+ assert result["next_agent"] == "tea"
266
+ assert result["next_phase"] == "red"
267
+
268
+ def test_assessment_found_is_true(
269
+ self, project: Path, session_with_assessment: Path
270
+ ) -> None:
271
+ """AC1: assessment_found should be True when assessment exists."""
272
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
273
+ assert result["assessment_found"] is True
274
+
275
+ def test_exit_code_zero_when_ready(
276
+ self, project: Path, session_with_assessment: Path, runner: CliRunner
277
+ ) -> None:
278
+ """AC5: Exit code 0 when gate resolves to ready."""
279
+ with patch(
280
+ "pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
281
+ return_value={
282
+ "status": "ready",
283
+ "gate_type": "tests_pass",
284
+ "gate_file": None,
285
+ "next_agent": "reviewer",
286
+ "next_phase": "review",
287
+ "assessment_found": True,
288
+ "error": None,
289
+ },
290
+ ):
291
+ result = runner.invoke(
292
+ cli, ["handoff", "resolve-gate", "105-1", "tdd", "green"]
293
+ )
294
+ assert result.exit_code == 0
295
+
296
+
297
+ class TestResolveGateBlocked:
298
+ """AC1 + AC5: resolve-gate returns 'blocked' when no assessment."""
299
+
300
+ def test_missing_assessment_returns_blocked(
301
+ self, project: Path, session_without_assessment: Path
302
+ ) -> None:
303
+ """AC1: Missing assessment section → status: blocked."""
304
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
305
+ assert result["status"] == "blocked"
306
+
307
+ def test_blocked_has_assessment_found_false(
308
+ self, project: Path, session_without_assessment: Path
309
+ ) -> None:
310
+ """AC1: Blocked result should have assessment_found: False."""
311
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
312
+ assert result["assessment_found"] is False
313
+
314
+ def test_blocked_exit_code_one(self, runner: CliRunner) -> None:
315
+ """AC5: Exit code 1 when gate resolves to blocked."""
316
+ with patch(
317
+ "pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
318
+ return_value={
319
+ "status": "blocked",
320
+ "gate_type": "tests_pass",
321
+ "gate_file": None,
322
+ "next_agent": "reviewer",
323
+ "next_phase": "review",
324
+ "assessment_found": False,
325
+ "error": None,
326
+ },
327
+ ):
328
+ result = runner.invoke(
329
+ cli, ["handoff", "resolve-gate", "105-1", "tdd", "green"]
330
+ )
331
+ assert result.exit_code == 1
332
+
333
+
334
+ class TestResolveGateSkip:
335
+ """AC1: resolve-gate returns 'skip' for manual gates."""
336
+
337
+ def test_manual_gate_returns_skip(
338
+ self, project: Path, session_with_assessment: Path
339
+ ) -> None:
340
+ """AC1: Patch fix phase (gate=manual) → status: skip."""
341
+ result = resolve_gate("105-1", "patch", "fix", project_root=project)
342
+ assert result["status"] == "skip"
343
+
344
+ def test_manual_gate_type_is_manual(
345
+ self, project: Path, session_with_assessment: Path
346
+ ) -> None:
347
+ """AC1: Skip result should have gate_type: manual."""
348
+ result = resolve_gate("105-1", "patch", "fix", project_root=project)
349
+ assert result["gate_type"] == "manual"
350
+
351
+ def test_skip_exit_code_zero(self, runner: CliRunner) -> None:
352
+ """AC5: Exit code 0 for skip status."""
353
+ with patch(
354
+ "pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
355
+ return_value={
356
+ "status": "skip",
357
+ "gate_type": "manual",
358
+ "gate_file": None,
359
+ "next_agent": None,
360
+ "next_phase": None,
361
+ "assessment_found": True,
362
+ "error": None,
363
+ },
364
+ ):
365
+ result = runner.invoke(
366
+ cli, ["handoff", "resolve-gate", "105-1", "patch", "fix"]
367
+ )
368
+ assert result.exit_code == 0
369
+
370
+
371
+ class TestResolveGateErrors:
372
+ """AC1: resolve-gate returns error for invalid inputs."""
373
+
374
+ def test_invalid_workflow_returns_error(self, project: Path) -> None:
375
+ """AC1: Unknown workflow → error result."""
376
+ result = resolve_gate("105-1", "nonexistent", "green", project_root=project)
377
+ assert result["status"] == "error" or result.get("error") is not None
378
+
379
+ def test_invalid_phase_returns_error(
380
+ self, project: Path, session_with_assessment: Path
381
+ ) -> None:
382
+ """AC1: Unknown phase → error result."""
383
+ result = resolve_gate("105-1", "tdd", "nonexistent", project_root=project)
384
+ assert result["status"] == "error" or result.get("error") is not None
385
+
386
+ def test_missing_session_file_returns_blocked(self, project: Path) -> None:
387
+ """AC1: No session file → blocked (can't check assessment)."""
388
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
389
+ assert result["status"] in ("blocked", "error")
390
+
391
+
392
+ class TestResolveGateOutputContract:
393
+ """AC1: RESOLVE_RESULT must contain all specified fields."""
394
+
395
+ REQUIRED_FIELDS = [
396
+ "status",
397
+ "gate_type",
398
+ "gate_file",
399
+ "next_agent",
400
+ "next_phase",
401
+ "assessment_found",
402
+ "error",
403
+ ]
404
+
405
+ def test_ready_result_has_all_fields(
406
+ self, project: Path, session_with_assessment: Path
407
+ ) -> None:
408
+ """AC1: Ready result must contain all RESOLVE_RESULT fields."""
409
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
410
+ for field in self.REQUIRED_FIELDS:
411
+ assert field in result, f"Missing field: {field}"
412
+
413
+ def test_blocked_result_has_all_fields(
414
+ self, project: Path, session_without_assessment: Path
415
+ ) -> None:
416
+ """AC1: Blocked result must contain all RESOLVE_RESULT fields."""
417
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
418
+ for field in self.REQUIRED_FIELDS:
419
+ assert field in result, f"Missing field: {field}"
420
+
421
+ def test_skip_result_has_all_fields(
422
+ self, project: Path, session_with_assessment: Path
423
+ ) -> None:
424
+ """AC1: Skip result must contain all RESOLVE_RESULT fields."""
425
+ result = resolve_gate("105-1", "patch", "fix", project_root=project)
426
+ for field in self.REQUIRED_FIELDS:
427
+ assert field in result, f"Missing field: {field}"
428
+
429
+ def test_gate_file_is_null_for_mvp(
430
+ self, project: Path, session_with_assessment: Path
431
+ ) -> None:
432
+ """AC1: gate_file should be None for MVP (populated in epic 106)."""
433
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
434
+ assert result["gate_file"] is None
435
+
436
+ def test_error_is_null_on_success(
437
+ self, project: Path, session_with_assessment: Path
438
+ ) -> None:
439
+ """AC1: error should be None when resolve succeeds."""
440
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
441
+ assert result["error"] is None
442
+
443
+ def test_status_is_valid_value(
444
+ self, project: Path, session_with_assessment: Path
445
+ ) -> None:
446
+ """AC1: status must be one of: ready, blocked, skip."""
447
+ result = resolve_gate("105-1", "tdd", "green", project_root=project)
448
+ assert result["status"] in ("ready", "blocked", "skip")
449
+
450
+
451
+ class TestResolveGateCLIOutput:
452
+ """AC4 + AC6: CLI outputs valid YAML to stdout."""
453
+
454
+ def test_cli_output_is_valid_yaml(self, runner: CliRunner) -> None:
455
+ """AC6: CLI output should be parseable YAML."""
456
+ with patch(
457
+ "pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
458
+ return_value={
459
+ "status": "ready",
460
+ "gate_type": "tests_pass",
461
+ "gate_file": None,
462
+ "next_agent": "reviewer",
463
+ "next_phase": "review",
464
+ "assessment_found": True,
465
+ "error": None,
466
+ },
467
+ ):
468
+ result = runner.invoke(
469
+ cli, ["handoff", "resolve-gate", "105-1", "tdd", "green"]
470
+ )
471
+ parsed = yaml.safe_load(result.output)
472
+ assert "RESOLVE_RESULT" in parsed
473
+
474
+ def test_cli_output_wraps_in_resolve_result_key(self, runner: CliRunner) -> None:
475
+ """AC4: Output should be wrapped in RESOLVE_RESULT key."""
476
+ with patch(
477
+ "pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
478
+ return_value={
479
+ "status": "ready",
480
+ "gate_type": "tests_pass",
481
+ "gate_file": None,
482
+ "next_agent": "reviewer",
483
+ "next_phase": "review",
484
+ "assessment_found": True,
485
+ "error": None,
486
+ },
487
+ ):
488
+ result = runner.invoke(
489
+ cli, ["handoff", "resolve-gate", "105-1", "tdd", "green"]
490
+ )
491
+ parsed = yaml.safe_load(result.output)
492
+ assert parsed["RESOLVE_RESULT"]["status"] == "ready"
493
+
494
+
495
+ # ===========================================================================
496
+ # AC2: complete-phase atomically updates session file
497
+ # ===========================================================================
498
+
499
+
500
+ class TestCompletePhaseUpdatesSession:
501
+ """AC2: complete-phase updates session with phase transition."""
502
+
503
+ def test_updates_phase_line(
504
+ self, project: Path, session_with_assessment: Path
505
+ ) -> None:
506
+ """AC2: **Phase:** line should change to the new phase."""
507
+ complete_phase(
508
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
509
+ )
510
+ content = session_with_assessment.read_text()
511
+ # The Phase line in the Workflow Tracking section should be "review"
512
+ assert "**Phase:** review" in content
513
+
514
+ def test_phase_line_no_longer_has_old_value(
515
+ self, project: Path, session_with_assessment: Path
516
+ ) -> None:
517
+ """AC2: Old phase value should not appear in **Phase:** line."""
518
+ complete_phase(
519
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
520
+ )
521
+ content = session_with_assessment.read_text()
522
+ # Should not have the old phase value on a Phase line
523
+ phase_lines = [
524
+ line for line in content.splitlines() if line.startswith("**Phase:**")
525
+ ]
526
+ for line in phase_lines:
527
+ assert "green" not in line
528
+
529
+ def test_updates_phase_started_timestamp(
530
+ self, project: Path, session_with_assessment: Path
531
+ ) -> None:
532
+ """AC2: **Phase Started:** should have a new ISO timestamp."""
533
+ complete_phase(
534
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
535
+ )
536
+ content = session_with_assessment.read_text()
537
+ # Find Phase Started lines — at least one should NOT be the old timestamp
538
+ started_lines = [
539
+ line
540
+ for line in content.splitlines()
541
+ if line.startswith("**Phase Started:**")
542
+ ]
543
+ assert len(started_lines) > 0
544
+ # At least one should have a different timestamp than the original
545
+ assert any("2026-02-15T07:53:07Z" not in line for line in started_lines)
546
+
547
+ def test_fills_ended_in_phase_history(
548
+ self, project: Path, session_with_assessment: Path
549
+ ) -> None:
550
+ """AC2: Current phase row in Phase History should get Ended timestamp."""
551
+ complete_phase(
552
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
553
+ )
554
+ content = session_with_assessment.read_text()
555
+ # The green phase row should now have an Ended value (not just "- |")
556
+ # Find the row that starts with "| green"
557
+ green_rows = [
558
+ line
559
+ for line in content.splitlines()
560
+ if line.strip().startswith("| green")
561
+ ]
562
+ assert len(green_rows) > 0
563
+ # The Ended column (3rd) should not be "-"
564
+ green_row = green_rows[0]
565
+ cols = [c.strip() for c in green_row.split("|") if c.strip()]
566
+ assert len(cols) >= 4, f"Expected 4+ columns, got: {cols}"
567
+ ended = cols[2] # Phase | Started | Ended | Duration
568
+ assert ended != "-", f"Ended should be filled, got: {ended}"
569
+
570
+ def test_fills_duration_in_phase_history(
571
+ self, project: Path, session_with_assessment: Path
572
+ ) -> None:
573
+ """AC2: Current phase row should get Duration calculated."""
574
+ complete_phase(
575
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
576
+ )
577
+ content = session_with_assessment.read_text()
578
+ green_rows = [
579
+ line
580
+ for line in content.splitlines()
581
+ if line.strip().startswith("| green")
582
+ ]
583
+ assert len(green_rows) > 0
584
+ green_row = green_rows[0]
585
+ cols = [c.strip() for c in green_row.split("|") if c.strip()]
586
+ duration = cols[3] # Phase | Started | Ended | Duration
587
+ assert duration != "-", f"Duration should be filled, got: {duration}"
588
+
589
+ def test_adds_new_phase_row(
590
+ self, project: Path, session_with_assessment: Path
591
+ ) -> None:
592
+ """AC2: A new row for the next phase should appear in Phase History."""
593
+ complete_phase(
594
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
595
+ )
596
+ content = session_with_assessment.read_text()
597
+ review_rows = [
598
+ line
599
+ for line in content.splitlines()
600
+ if line.strip().startswith("| review")
601
+ ]
602
+ assert len(review_rows) > 0, "New 'review' phase row not found in Phase History"
603
+
604
+ def test_adds_handoff_history_row(
605
+ self, project: Path, session_with_assessment: Path
606
+ ) -> None:
607
+ """AC2: A new row should be added to Handoff History table."""
608
+ complete_phase(
609
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
610
+ )
611
+ content = session_with_assessment.read_text()
612
+ # Should have a row mentioning green → review with tests_pass
613
+ handoff_rows = [
614
+ line
615
+ for line in content.splitlines()
616
+ if "green" in line and "review" in line and "tests_pass" in line
617
+ ]
618
+ assert len(handoff_rows) > 0, "Handoff History row for green→review not found"
619
+
620
+ def test_handoff_history_includes_passed_status(
621
+ self, project: Path, session_with_assessment: Path
622
+ ) -> None:
623
+ """AC2: Handoff History row should include PASSED status."""
624
+ complete_phase(
625
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
626
+ )
627
+ content = session_with_assessment.read_text()
628
+ # Find the new handoff row (should be the last one with "review")
629
+ lines = content.splitlines()
630
+ handoff_section = False
631
+ handoff_rows = []
632
+ for line in lines:
633
+ if "### Handoff History" in line:
634
+ handoff_section = True
635
+ continue
636
+ if handoff_section and line.strip().startswith("|") and "---" not in line:
637
+ if "From" not in line: # Skip header
638
+ handoff_rows.append(line)
639
+ # Last row should have PASSED
640
+ assert len(handoff_rows) > 0
641
+ last_row = handoff_rows[-1]
642
+ assert "PASSED" in last_row
643
+
644
+ def test_handoff_history_includes_timestamp(
645
+ self, project: Path, session_with_assessment: Path
646
+ ) -> None:
647
+ """AC2: Handoff History row should include an ISO timestamp."""
648
+ complete_phase(
649
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
650
+ )
651
+ content = session_with_assessment.read_text()
652
+ lines = content.splitlines()
653
+ handoff_section = False
654
+ handoff_rows = []
655
+ for line in lines:
656
+ if "### Handoff History" in line:
657
+ handoff_section = True
658
+ continue
659
+ if handoff_section and line.strip().startswith("|") and "---" not in line:
660
+ if "From" not in line:
661
+ handoff_rows.append(line)
662
+ assert len(handoff_rows) > 0
663
+ last_row = handoff_rows[-1]
664
+ # Should contain ISO timestamp pattern
665
+ assert re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", last_row)
666
+
667
+
668
+ class TestCompletePhaseAtomicity:
669
+ """AC2: Session update must be atomic (temp + mv)."""
670
+
671
+ def test_session_file_exists_after_update(
672
+ self, project: Path, session_with_assessment: Path
673
+ ) -> None:
674
+ """AC2: Session file should still exist after update."""
675
+ complete_phase(
676
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
677
+ )
678
+ assert session_with_assessment.exists()
679
+
680
+ def test_no_temp_files_left_behind(
681
+ self, project: Path, session_with_assessment: Path
682
+ ) -> None:
683
+ """AC2: No .tmp or temporary files should remain in .session/."""
684
+ complete_phase(
685
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
686
+ )
687
+ session_dir = project / ".session"
688
+ temp_files = list(session_dir.glob("*.tmp")) + list(session_dir.glob("*.bak"))
689
+ assert len(temp_files) == 0, f"Temp files left behind: {temp_files}"
690
+
691
+ def test_session_content_is_valid_after_update(
692
+ self, project: Path, session_with_assessment: Path
693
+ ) -> None:
694
+ """AC2: Session file should still be valid markdown after update."""
695
+ complete_phase(
696
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
697
+ )
698
+ content = session_with_assessment.read_text()
699
+ # Should still have required sections
700
+ assert "# Story 105-1" in content
701
+ assert "## Workflow Tracking" in content
702
+ assert "### Phase History" in content
703
+ assert "### Handoff History" in content
704
+
705
+
706
+ class TestCompletePhaseOutputContract:
707
+ """AC2: COMPLETE_RESULT must contain all specified fields."""
708
+
709
+ def test_returns_success_status(
710
+ self, project: Path, session_with_assessment: Path
711
+ ) -> None:
712
+ """AC2: Successful update should return status: success."""
713
+ result = complete_phase(
714
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
715
+ )
716
+ assert result["status"] == "success"
717
+
718
+ def test_returns_session_file_path(
719
+ self, project: Path, session_with_assessment: Path
720
+ ) -> None:
721
+ """AC2: Result should include the session file path."""
722
+ result = complete_phase(
723
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
724
+ )
725
+ assert "session_file" in result
726
+ assert "105-1-session.md" in result["session_file"]
727
+
728
+ def test_returns_null_error_on_success(
729
+ self, project: Path, session_with_assessment: Path
730
+ ) -> None:
731
+ """AC2: error should be None on success."""
732
+ result = complete_phase(
733
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
734
+ )
735
+ assert result["error"] is None
736
+
737
+ def test_cli_output_is_valid_yaml(self, runner: CliRunner) -> None:
738
+ """AC6: CLI output should be parseable YAML."""
739
+ with patch(
740
+ "pennyfarthing_scripts.handoff.complete_phase.complete_phase",
741
+ return_value={
742
+ "status": "success",
743
+ "session_file": ".session/105-1-session.md",
744
+ "error": None,
745
+ },
746
+ ):
747
+ result = runner.invoke(
748
+ cli,
749
+ [
750
+ "handoff",
751
+ "complete-phase",
752
+ "105-1",
753
+ "tdd",
754
+ "green",
755
+ "review",
756
+ "tests_pass",
757
+ ],
758
+ )
759
+ assert result.exit_code == 0
760
+ parsed = yaml.safe_load(result.output)
761
+ assert "COMPLETE_RESULT" in parsed
762
+
763
+ def test_cli_exit_code_zero_on_success(self, runner: CliRunner) -> None:
764
+ """AC5: Exit code 0 on successful phase completion."""
765
+ with patch(
766
+ "pennyfarthing_scripts.handoff.complete_phase.complete_phase",
767
+ return_value={
768
+ "status": "success",
769
+ "session_file": ".session/105-1-session.md",
770
+ "error": None,
771
+ },
772
+ ):
773
+ result = runner.invoke(
774
+ cli,
775
+ [
776
+ "handoff",
777
+ "complete-phase",
778
+ "105-1",
779
+ "tdd",
780
+ "green",
781
+ "review",
782
+ "tests_pass",
783
+ ],
784
+ )
785
+ assert result.exit_code == 0
786
+
787
+
788
+ class TestCompletePhaseErrors:
789
+ """AC2: Error handling for complete-phase."""
790
+
791
+ def test_missing_session_file_returns_error(self, project: Path) -> None:
792
+ """AC2: No session file → error result."""
793
+ result = complete_phase(
794
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
795
+ )
796
+ assert result["status"] == "error"
797
+ assert result["error"] is not None
798
+
799
+ def test_error_exit_code_one(self, runner: CliRunner) -> None:
800
+ """AC5: Exit code 1 on error."""
801
+ with patch(
802
+ "pennyfarthing_scripts.handoff.complete_phase.complete_phase",
803
+ return_value={
804
+ "status": "error",
805
+ "session_file": None,
806
+ "error": "Session file not found",
807
+ },
808
+ ):
809
+ result = runner.invoke(
810
+ cli,
811
+ [
812
+ "handoff",
813
+ "complete-phase",
814
+ "105-1",
815
+ "tdd",
816
+ "green",
817
+ "review",
818
+ "tests_pass",
819
+ ],
820
+ )
821
+ assert result.exit_code == 1
822
+
823
+
824
+ # ===========================================================================
825
+ # AC4: stdout is the only communication channel
826
+ # ===========================================================================
827
+
828
+
829
+ class TestStdoutOnlyCommunication:
830
+ """AC4: No side-channel files created during execution."""
831
+
832
+ def test_resolve_gate_no_side_files(
833
+ self, project: Path, session_with_assessment: Path
834
+ ) -> None:
835
+ """AC4: resolve-gate should not create any files."""
836
+ files_before = set(project.rglob("*"))
837
+ resolve_gate("105-1", "tdd", "green", project_root=project)
838
+ files_after = set(project.rglob("*"))
839
+ new_files = files_after - files_before
840
+ assert len(new_files) == 0, f"Unexpected files created: {new_files}"
841
+
842
+ def test_complete_phase_only_modifies_session(
843
+ self, project: Path, session_with_assessment: Path
844
+ ) -> None:
845
+ """AC4: complete-phase should only modify the session file."""
846
+ # Record all files and their mtimes
847
+ files_before = {
848
+ p: p.stat().st_mtime for p in project.rglob("*") if p.is_file()
849
+ }
850
+ complete_phase(
851
+ "105-1", "tdd", "green", "review", "tests_pass", project_root=project
852
+ )
853
+ files_after = {
854
+ p: p.stat().st_mtime for p in project.rglob("*") if p.is_file()
855
+ }
856
+ # Only the session file should be modified
857
+ changed = {
858
+ p
859
+ for p, mtime in files_after.items()
860
+ if files_before.get(p) != mtime
861
+ }
862
+ new = set(files_after) - set(files_before)
863
+ all_changes = changed | new
864
+ allowed = {session_with_assessment}
865
+ unexpected = all_changes - allowed
866
+ assert len(unexpected) == 0, f"Unexpected file changes: {unexpected}"
867
+
868
+
869
+ # ===========================================================================
870
+ # AC3: CLI command group exists and is invokable
871
+ # ===========================================================================
872
+
873
+
874
+ class TestHandoffCommandExists:
875
+ """AC3: pf handoff command group with subcommands."""
876
+
877
+ def test_handoff_help(self, runner: CliRunner) -> None:
878
+ """AC3: pf handoff --help should work."""
879
+ result = runner.invoke(cli, ["handoff", "--help"])
880
+ assert result.exit_code == 0
881
+ assert "resolve-gate" in result.output
882
+ assert "complete-phase" in result.output
883
+
884
+ def test_resolve_gate_help(self, runner: CliRunner) -> None:
885
+ """AC3: pf handoff resolve-gate --help should work."""
886
+ result = runner.invoke(cli, ["handoff", "resolve-gate", "--help"])
887
+ assert result.exit_code == 0
888
+ assert "STORY_ID" in result.output
889
+
890
+ def test_complete_phase_help(self, runner: CliRunner) -> None:
891
+ """AC3: pf handoff complete-phase --help should work."""
892
+ result = runner.invoke(cli, ["handoff", "complete-phase", "--help"])
893
+ assert result.exit_code == 0
894
+ assert "STORY_ID" in result.output
895
+
896
+
897
+ # ===========================================================================
898
+ # Cross-workflow: resolve-gate works with multiple workflow types
899
+ # ===========================================================================
900
+
901
+
902
+ class TestResolveGateMultipleWorkflows:
903
+ """Verify resolve-gate handles the gate types across all workflows."""
904
+
905
+ def test_tdd_review_gate_is_approval(
906
+ self, project: Path, session_with_assessment: Path
907
+ ) -> None:
908
+ """TDD review phase gate should be 'approval'."""
909
+ result = resolve_gate("105-1", "tdd", "review", project_root=project)
910
+ assert result["gate_type"] == "approval"
911
+ assert result["next_agent"] == "sm"
912
+ assert result["next_phase"] == "finish"
913
+
914
+ def test_trivial_review_gate_is_approval(
915
+ self, project: Path, session_with_assessment: Path
916
+ ) -> None:
917
+ """Trivial review phase gate should be 'approval'."""
918
+ result = resolve_gate("105-1", "trivial", "review", project_root=project)
919
+ assert result["gate_type"] == "approval"
920
+ assert result["next_agent"] == "sm"
921
+ assert result["next_phase"] == "finish"
922
+
923
+ def test_last_phase_has_no_next(
924
+ self, project: Path, session_with_assessment: Path
925
+ ) -> None:
926
+ """Finish phase (last) should indicate no next phase/agent."""
927
+ result = resolve_gate("105-1", "tdd", "finish", project_root=project)
928
+ # Last phase has no gate and no next — should handle gracefully
929
+ assert result["next_phase"] is None or result.get("error") is not None