@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,620 @@
1
+ """Tests for gate subagent runner — Story 106-2.
2
+
3
+ Epic: 106 (Gate Files & First Migration)
4
+ Story: 106-2 — Gate subagent runner with GATE_RESULT contract
5
+
6
+ Tests parse_gate_file() and extract_gate_result() — the parsing layer
7
+ around gate file evaluation. The actual subagent spawning is handled by
8
+ the Claude agent; these functions parse inputs and outputs.
9
+
10
+ Acceptance Criteria:
11
+ - [AC1] Gate runner function accepts a gate file path and spawns it as a haiku Task subagent
12
+ - [AC2] Returns structured GATE_RESULT: {status: pass|fail, message, checks}
13
+ - [AC3] Supports model attribute (default: haiku, overridable per gate)
14
+ - [AC4] Default-deny: missing GATE_RESULT = fail
15
+ - [AC5] GATE_RESULT extraction uses regex/grep, not full YAML parser
16
+ - [AC6] Gate files are read-only at runtime (never written to)
17
+ - [AC7] Handles subagent timeouts and crashes gracefully (default to fail)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ from pathlib import Path
24
+
25
+ import pytest
26
+
27
+ from pennyfarthing_scripts.handoff.gate_runner import (
28
+ extract_gate_result,
29
+ parse_gate_file,
30
+ )
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Fixtures: Gate file content
34
+ # ---------------------------------------------------------------------------
35
+
36
+ GATE_WITH_MODEL = """\
37
+ <gate name="tests-pass" model="haiku">
38
+
39
+ <purpose>
40
+ Verify that all tests pass and working tree is clean.
41
+ </purpose>
42
+
43
+ <pass>
44
+ Run tests and report results.
45
+
46
+ ```yaml
47
+ GATE_RESULT:
48
+ status: pass
49
+ message: "All tests passing"
50
+ ```
51
+ </pass>
52
+
53
+ <fail>
54
+ Report failures.
55
+
56
+ ```yaml
57
+ GATE_RESULT:
58
+ status: fail
59
+ message: "Tests failing"
60
+ ```
61
+ </fail>
62
+
63
+ </gate>
64
+ """
65
+
66
+ GATE_WITH_CUSTOM_MODEL = """\
67
+ <gate name="deep-review" model="sonnet">
68
+
69
+ <purpose>
70
+ Deep code review requiring more capable model.
71
+ </purpose>
72
+
73
+ <pass>
74
+ Review passes.
75
+ </pass>
76
+
77
+ <fail>
78
+ Review fails.
79
+ </fail>
80
+
81
+ </gate>
82
+ """
83
+
84
+ GATE_WITHOUT_MODEL = """\
85
+ <gate name="simple-check">
86
+
87
+ <purpose>
88
+ A gate with no model attribute — should default to haiku.
89
+ </purpose>
90
+
91
+ <pass>
92
+ Check passes.
93
+ </pass>
94
+
95
+ <fail>
96
+ Check fails.
97
+ </fail>
98
+
99
+ </gate>
100
+ """
101
+
102
+ GATE_MINIMAL = """\
103
+ <gate name="minimal" model="haiku">
104
+ <pass>Pass</pass>
105
+ <fail>Fail</fail>
106
+ </gate>
107
+ """
108
+
109
+
110
+ @pytest.fixture
111
+ def gate_file(tmp_path: Path) -> Path:
112
+ """Create a standard gate file with model="haiku"."""
113
+ p = tmp_path / "tests-pass.md"
114
+ p.write_text(GATE_WITH_MODEL)
115
+ return p
116
+
117
+
118
+ @pytest.fixture
119
+ def gate_file_custom_model(tmp_path: Path) -> Path:
120
+ """Create a gate file with model="sonnet"."""
121
+ p = tmp_path / "deep-review.md"
122
+ p.write_text(GATE_WITH_CUSTOM_MODEL)
123
+ return p
124
+
125
+
126
+ @pytest.fixture
127
+ def gate_file_no_model(tmp_path: Path) -> Path:
128
+ """Create a gate file with no model attribute."""
129
+ p = tmp_path / "simple-check.md"
130
+ p.write_text(GATE_WITHOUT_MODEL)
131
+ return p
132
+
133
+
134
+ @pytest.fixture
135
+ def gate_file_minimal(tmp_path: Path) -> Path:
136
+ """Create a minimal gate file."""
137
+ p = tmp_path / "minimal.md"
138
+ p.write_text(GATE_MINIMAL)
139
+ return p
140
+
141
+
142
+ # ===========================================================================
143
+ # AC1: Gate runner accepts a gate file path and parses it
144
+ # ===========================================================================
145
+
146
+
147
+ class TestParseGateFileBasic:
148
+ """AC1: parse_gate_file reads gate file and returns structured metadata."""
149
+
150
+ def test_returns_ok_status(self, gate_file: Path) -> None:
151
+ """AC1: Successful parse returns status 'ok'."""
152
+ result = parse_gate_file(gate_file)
153
+ assert result["status"] == "ok"
154
+
155
+ def test_returns_gate_name(self, gate_file: Path) -> None:
156
+ """AC1: Extracts gate name from <gate name="...">."""
157
+ result = parse_gate_file(gate_file)
158
+ assert result["name"] == "tests-pass"
159
+
160
+ def test_returns_gate_content(self, gate_file: Path) -> None:
161
+ """AC1: Returns full gate file content for subagent prompt."""
162
+ result = parse_gate_file(gate_file)
163
+ assert result["content"] is not None
164
+ assert len(result["content"]) > 0
165
+ assert "<gate" in result["content"]
166
+
167
+ def test_content_matches_file(self, gate_file: Path) -> None:
168
+ """AC1: Content matches what was written to file."""
169
+ result = parse_gate_file(gate_file)
170
+ assert result["content"] == GATE_WITH_MODEL
171
+
172
+ def test_returns_model(self, gate_file: Path) -> None:
173
+ """AC1: Extracts model from <gate model="...">."""
174
+ result = parse_gate_file(gate_file)
175
+ assert result["model"] == "haiku"
176
+
177
+ def test_error_is_none_on_success(self, gate_file: Path) -> None:
178
+ """AC1: Error field is None when parse succeeds."""
179
+ result = parse_gate_file(gate_file)
180
+ assert result["error"] is None
181
+
182
+ def test_result_has_required_keys(self, gate_file: Path) -> None:
183
+ """AC1: Result dict has all required keys."""
184
+ result = parse_gate_file(gate_file)
185
+ assert "status" in result
186
+ assert "name" in result
187
+ assert "model" in result
188
+ assert "content" in result
189
+ assert "error" in result
190
+
191
+ def test_accepts_string_path(self, gate_file: Path) -> None:
192
+ """AC1: Accepts both str and Path objects."""
193
+ result = parse_gate_file(str(gate_file))
194
+ assert result["status"] == "ok"
195
+
196
+ def test_nonexistent_file_returns_error(self, tmp_path: Path) -> None:
197
+ """AC1: Missing file returns error status."""
198
+ result = parse_gate_file(tmp_path / "nonexistent.md")
199
+ assert result["status"] == "error"
200
+ assert result["error"] is not None
201
+
202
+ def test_empty_file_returns_error(self, tmp_path: Path) -> None:
203
+ """AC1: Empty file with no <gate> tag returns error."""
204
+ p = tmp_path / "empty.md"
205
+ p.write_text("")
206
+ result = parse_gate_file(p)
207
+ assert result["status"] == "error"
208
+
209
+ def test_file_without_gate_tag_returns_error(self, tmp_path: Path) -> None:
210
+ """AC1: File without <gate> tag returns error."""
211
+ p = tmp_path / "bad.md"
212
+ p.write_text("# Just a regular markdown file\nNo gate here.\n")
213
+ result = parse_gate_file(p)
214
+ assert result["status"] == "error"
215
+
216
+
217
+ # ===========================================================================
218
+ # AC3: Model attribute — default haiku, overridable per gate
219
+ # ===========================================================================
220
+
221
+
222
+ class TestParseGateFileModel:
223
+ """AC3: Model attribute extraction with default fallback."""
224
+
225
+ def test_extracts_haiku_model(self, gate_file: Path) -> None:
226
+ """AC3: model="haiku" extracted correctly."""
227
+ result = parse_gate_file(gate_file)
228
+ assert result["model"] == "haiku"
229
+
230
+ def test_extracts_custom_model(self, gate_file_custom_model: Path) -> None:
231
+ """AC3: model="sonnet" extracted correctly."""
232
+ result = parse_gate_file(gate_file_custom_model)
233
+ assert result["model"] == "sonnet"
234
+
235
+ def test_defaults_to_haiku_when_no_model(
236
+ self, gate_file_no_model: Path
237
+ ) -> None:
238
+ """AC3: Missing model attribute defaults to 'haiku'."""
239
+ result = parse_gate_file(gate_file_no_model)
240
+ assert result["model"] == "haiku"
241
+
242
+ def test_extracts_name_when_no_model(
243
+ self, gate_file_no_model: Path
244
+ ) -> None:
245
+ """AC3: Gate name still extracted when model is missing."""
246
+ result = parse_gate_file(gate_file_no_model)
247
+ assert result["name"] == "simple-check"
248
+
249
+
250
+ # ===========================================================================
251
+ # AC6: Gate files are read-only at runtime
252
+ # ===========================================================================
253
+
254
+
255
+ class TestParseGateFileReadOnly:
256
+ """AC6: Gate files must not be modified during parsing."""
257
+
258
+ def test_file_not_modified(self, gate_file: Path) -> None:
259
+ """AC6: File mtime unchanged after parsing."""
260
+ mtime_before = os.path.getmtime(gate_file)
261
+ content_before = gate_file.read_text()
262
+ parse_gate_file(gate_file)
263
+ mtime_after = os.path.getmtime(gate_file)
264
+ content_after = gate_file.read_text()
265
+ assert mtime_before == mtime_after
266
+ assert content_before == content_after
267
+
268
+ def test_file_size_unchanged(self, gate_file: Path) -> None:
269
+ """AC6: File size unchanged after parsing."""
270
+ size_before = gate_file.stat().st_size
271
+ parse_gate_file(gate_file)
272
+ size_after = gate_file.stat().st_size
273
+ assert size_before == size_after
274
+
275
+
276
+ # ===========================================================================
277
+ # AC2: Returns structured GATE_RESULT: {status, message, checks}
278
+ # ===========================================================================
279
+
280
+
281
+ # Subagent output samples for extract_gate_result tests
282
+
283
+ PASSING_OUTPUT = """\
284
+ I've run all the checks. Here are the results:
285
+
286
+ GATE_RESULT:
287
+ status: pass
288
+ gate: tests-pass
289
+ message: "All 47 tests passing. Working tree clean."
290
+ checks:
291
+ - name: test-suite
292
+ status: pass
293
+ detail: "47/47 tests passing (0 skipped)"
294
+ - name: working-tree
295
+ status: pass
296
+ detail: "No uncommitted changes"
297
+ - name: branch-status
298
+ status: pass
299
+ detail: "On branch feature/106-2, HEAD at abc1234"
300
+ """
301
+
302
+ FAILING_OUTPUT = """\
303
+ Some checks failed:
304
+
305
+ GATE_RESULT:
306
+ status: fail
307
+ gate: tests-pass
308
+ message: "3 tests failing, dirty working tree"
309
+ checks:
310
+ - name: test-suite
311
+ status: fail
312
+ detail: "44/47 tests passing, 3 failing"
313
+ - name: working-tree
314
+ status: fail
315
+ detail: "2 uncommitted files"
316
+ - name: branch-status
317
+ status: pass
318
+ detail: "On branch feature/106-2, HEAD at abc1234"
319
+ """
320
+
321
+ MINIMAL_PASS_OUTPUT = """\
322
+ GATE_RESULT:
323
+ status: pass
324
+ message: "All good"
325
+ checks: []
326
+ """
327
+
328
+ MINIMAL_FAIL_OUTPUT = """\
329
+ GATE_RESULT:
330
+ status: fail
331
+ message: "Something wrong"
332
+ checks: []
333
+ """
334
+
335
+
336
+ class TestExtractGateResultPass:
337
+ """AC2: Correctly extracts passing GATE_RESULT."""
338
+
339
+ def test_status_is_pass(self) -> None:
340
+ """AC2: Passing output yields status 'pass'."""
341
+ result = extract_gate_result(PASSING_OUTPUT)
342
+ assert result["status"] == "pass"
343
+
344
+ def test_message_extracted(self) -> None:
345
+ """AC2: Message field extracted from output."""
346
+ result = extract_gate_result(PASSING_OUTPUT)
347
+ assert "47 tests passing" in result["message"]
348
+
349
+ def test_checks_is_list(self) -> None:
350
+ """AC2: Checks field is a list."""
351
+ result = extract_gate_result(PASSING_OUTPUT)
352
+ assert isinstance(result["checks"], list)
353
+
354
+ def test_checks_have_required_fields(self) -> None:
355
+ """AC2: Each check has name, status, detail."""
356
+ result = extract_gate_result(PASSING_OUTPUT)
357
+ for check in result["checks"]:
358
+ assert "name" in check
359
+ assert "status" in check
360
+ assert "detail" in check
361
+
362
+ def test_check_count(self) -> None:
363
+ """AC2: Correct number of checks extracted."""
364
+ result = extract_gate_result(PASSING_OUTPUT)
365
+ assert len(result["checks"]) == 3
366
+
367
+ def test_check_names(self) -> None:
368
+ """AC2: Check names match expected values."""
369
+ result = extract_gate_result(PASSING_OUTPUT)
370
+ names = [c["name"] for c in result["checks"]]
371
+ assert "test-suite" in names
372
+ assert "working-tree" in names
373
+ assert "branch-status" in names
374
+
375
+ def test_all_checks_pass(self) -> None:
376
+ """AC2: All checks have status 'pass' in passing output."""
377
+ result = extract_gate_result(PASSING_OUTPUT)
378
+ for check in result["checks"]:
379
+ assert check["status"] == "pass"
380
+
381
+
382
+ class TestExtractGateResultFail:
383
+ """AC2: Correctly extracts failing GATE_RESULT."""
384
+
385
+ def test_status_is_fail(self) -> None:
386
+ """AC2: Failing output yields status 'fail'."""
387
+ result = extract_gate_result(FAILING_OUTPUT)
388
+ assert result["status"] == "fail"
389
+
390
+ def test_message_extracted(self) -> None:
391
+ """AC2: Failure message extracted."""
392
+ result = extract_gate_result(FAILING_OUTPUT)
393
+ assert "failing" in result["message"].lower()
394
+
395
+ def test_mixed_check_statuses(self) -> None:
396
+ """AC2: Individual checks can have different statuses."""
397
+ result = extract_gate_result(FAILING_OUTPUT)
398
+ statuses = {c["name"]: c["status"] for c in result["checks"]}
399
+ assert statuses["test-suite"] == "fail"
400
+ assert statuses["working-tree"] == "fail"
401
+ assert statuses["branch-status"] == "pass"
402
+
403
+
404
+ class TestExtractGateResultMinimal:
405
+ """AC2: Handles minimal GATE_RESULT (no checks)."""
406
+
407
+ def test_minimal_pass(self) -> None:
408
+ """AC2: Minimal pass output with empty checks."""
409
+ result = extract_gate_result(MINIMAL_PASS_OUTPUT)
410
+ assert result["status"] == "pass"
411
+ assert isinstance(result["checks"], list)
412
+
413
+ def test_minimal_fail(self) -> None:
414
+ """AC2: Minimal fail output with empty checks."""
415
+ result = extract_gate_result(MINIMAL_FAIL_OUTPUT)
416
+ assert result["status"] == "fail"
417
+
418
+ def test_result_has_required_keys(self) -> None:
419
+ """AC2: Result always has status, message, checks."""
420
+ result = extract_gate_result(MINIMAL_PASS_OUTPUT)
421
+ assert "status" in result
422
+ assert "message" in result
423
+ assert "checks" in result
424
+
425
+
426
+ # ===========================================================================
427
+ # AC4: Default-deny — missing GATE_RESULT = fail
428
+ # ===========================================================================
429
+
430
+
431
+ class TestExtractGateResultDefaultDeny:
432
+ """AC4: Missing or unparseable GATE_RESULT always returns fail."""
433
+
434
+ def test_none_output_returns_fail(self) -> None:
435
+ """AC4: None input (crash/timeout) → fail."""
436
+ result = extract_gate_result(None)
437
+ assert result["status"] == "fail"
438
+
439
+ def test_empty_string_returns_fail(self) -> None:
440
+ """AC4: Empty string output → fail."""
441
+ result = extract_gate_result("")
442
+ assert result["status"] == "fail"
443
+
444
+ def test_no_gate_result_block_returns_fail(self) -> None:
445
+ """AC4: Output without GATE_RESULT → fail."""
446
+ result = extract_gate_result("I ran the tests and everything looks good!")
447
+ assert result["status"] == "fail"
448
+
449
+ def test_partial_gate_result_no_status_returns_fail(self) -> None:
450
+ """AC4: GATE_RESULT without status field → fail."""
451
+ output = "GATE_RESULT:\n message: 'partial result'\n checks: []\n"
452
+ result = extract_gate_result(output)
453
+ assert result["status"] == "fail"
454
+
455
+ def test_malformed_yaml_returns_fail(self) -> None:
456
+ """AC4: Malformed GATE_RESULT block → fail."""
457
+ output = "GATE_RESULT:\n status: [[[invalid yaml\n"
458
+ result = extract_gate_result(output)
459
+ assert result["status"] == "fail"
460
+
461
+ def test_default_deny_has_message(self) -> None:
462
+ """AC4: Default-deny result includes explanatory message."""
463
+ result = extract_gate_result(None)
464
+ assert result["message"] is not None
465
+ assert len(result["message"]) > 0
466
+
467
+ def test_default_deny_has_empty_checks(self) -> None:
468
+ """AC4: Default-deny result has empty checks list."""
469
+ result = extract_gate_result(None)
470
+ assert result["checks"] == []
471
+
472
+ def test_status_typo_returns_fail(self) -> None:
473
+ """AC4: status: 'passed' (not 'pass') → fail (strict enum)."""
474
+ output = "GATE_RESULT:\n status: passed\n message: 'oops'\n checks: []\n"
475
+ result = extract_gate_result(output)
476
+ assert result["status"] == "fail"
477
+
478
+ def test_status_uppercase_returns_fail(self) -> None:
479
+ """AC4: status: PASS (uppercase) → fail (strict matching)."""
480
+ output = "GATE_RESULT:\n status: PASS\n message: 'oops'\n checks: []\n"
481
+ result = extract_gate_result(output)
482
+ assert result["status"] == "fail"
483
+
484
+
485
+ # ===========================================================================
486
+ # AC5: GATE_RESULT extraction uses regex, not full YAML parser
487
+ # ===========================================================================
488
+
489
+
490
+ class TestExtractGateResultRegex:
491
+ """AC5: Extraction uses regex patterns, handles various formats."""
492
+
493
+ def test_gate_result_embedded_in_prose(self) -> None:
494
+ """AC5: GATE_RESULT found even when surrounded by other text."""
495
+ output = (
496
+ "Here is my analysis of the codebase.\n"
497
+ "I found several issues.\n\n"
498
+ "GATE_RESULT:\n"
499
+ " status: fail\n"
500
+ ' message: "Found issues"\n'
501
+ " checks:\n"
502
+ " - name: lint\n"
503
+ " status: fail\n"
504
+ ' detail: "3 lint errors"\n'
505
+ "\nLet me know if you need more details."
506
+ )
507
+ result = extract_gate_result(output)
508
+ assert result["status"] == "fail"
509
+
510
+ def test_gate_result_in_code_block(self) -> None:
511
+ """AC5: GATE_RESULT inside a code block still extracted."""
512
+ output = (
513
+ "```yaml\n"
514
+ "GATE_RESULT:\n"
515
+ " status: pass\n"
516
+ ' message: "All clear"\n'
517
+ " checks: []\n"
518
+ "```\n"
519
+ )
520
+ result = extract_gate_result(output)
521
+ assert result["status"] == "pass"
522
+
523
+ def test_handles_extra_whitespace(self) -> None:
524
+ """AC5: Extra whitespace in GATE_RESULT still parses."""
525
+ output = (
526
+ "GATE_RESULT:\n"
527
+ " status: pass\n"
528
+ ' message: "Tests OK"\n'
529
+ " checks: []\n"
530
+ )
531
+ result = extract_gate_result(output)
532
+ assert result["status"] == "pass"
533
+
534
+ def test_handles_unquoted_message(self) -> None:
535
+ """AC5: Message without quotes still extracted."""
536
+ output = (
537
+ "GATE_RESULT:\n"
538
+ " status: pass\n"
539
+ " message: All tests passing\n"
540
+ " checks: []\n"
541
+ )
542
+ result = extract_gate_result(output)
543
+ assert result["status"] == "pass"
544
+ assert len(result["message"]) > 0
545
+
546
+ def test_handles_single_quoted_message(self) -> None:
547
+ """AC5: Single-quoted message extracted."""
548
+ output = (
549
+ "GATE_RESULT:\n"
550
+ " status: pass\n"
551
+ " message: 'All tests passing'\n"
552
+ " checks: []\n"
553
+ )
554
+ result = extract_gate_result(output)
555
+ assert result["status"] == "pass"
556
+ assert "All tests passing" in result["message"]
557
+
558
+
559
+ # ===========================================================================
560
+ # AC7: Handles subagent timeouts and crashes gracefully
561
+ # ===========================================================================
562
+
563
+
564
+ class TestExtractGateResultEdgeCases:
565
+ """AC7: Graceful handling of timeouts, crashes, and edge cases."""
566
+
567
+ def test_timeout_marker_returns_fail(self) -> None:
568
+ """AC7: Output indicating timeout → fail."""
569
+ output = "Error: Task timed out after 120 seconds"
570
+ result = extract_gate_result(output)
571
+ assert result["status"] == "fail"
572
+
573
+ def test_very_long_output_still_extracts(self) -> None:
574
+ """AC7: Large output doesn't break extraction."""
575
+ padding = "x" * 10000
576
+ output = (
577
+ f"{padding}\n"
578
+ "GATE_RESULT:\n"
579
+ " status: pass\n"
580
+ ' message: "Found after long output"\n'
581
+ " checks: []\n"
582
+ f"\n{padding}"
583
+ )
584
+ result = extract_gate_result(output)
585
+ assert result["status"] == "pass"
586
+
587
+ def test_multiple_gate_results_takes_last(self) -> None:
588
+ """AC7: If multiple GATE_RESULT blocks, take the last one."""
589
+ output = (
590
+ "GATE_RESULT:\n"
591
+ " status: fail\n"
592
+ ' message: "First attempt failed"\n'
593
+ " checks: []\n"
594
+ "\nRetrying...\n\n"
595
+ "GATE_RESULT:\n"
596
+ " status: pass\n"
597
+ ' message: "Second attempt passed"\n'
598
+ " checks: []\n"
599
+ )
600
+ result = extract_gate_result(output)
601
+ assert result["status"] == "pass"
602
+ assert "Second" in result["message"] or "passed" in result["message"]
603
+
604
+ def test_only_whitespace_returns_fail(self) -> None:
605
+ """AC7: Whitespace-only output → fail."""
606
+ result = extract_gate_result(" \n\n\t \n")
607
+ assert result["status"] == "fail"
608
+
609
+ def test_gate_result_with_extra_fields_ignored(self) -> None:
610
+ """AC7: Extra fields in GATE_RESULT don't break extraction."""
611
+ output = (
612
+ "GATE_RESULT:\n"
613
+ " status: pass\n"
614
+ " gate: tests-pass\n"
615
+ ' message: "All good"\n'
616
+ " extra_field: ignored\n"
617
+ " checks: []\n"
618
+ )
619
+ result = extract_gate_result(output)
620
+ assert result["status"] == "pass"