@pennyfarthing/core 11.0.0-alpha.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +84 -26
  2. package/package.json +14 -16
  3. package/packages/core/dist/cli/cyclist-migration.test.js +2 -1
  4. package/packages/core/dist/cli/cyclist-migration.test.js.map +1 -1
  5. package/packages/core/dist/cli/ocean-profiles.test.js +5 -4
  6. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  7. package/packages/core/dist/cli/theme-maker.test.js +5 -4
  8. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  9. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts +20 -0
  10. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts.map +1 -0
  11. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js +278 -0
  12. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js.map +1 -0
  13. package/packages/core/dist/cli/utils/constants.d.ts +7 -1
  14. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  15. package/packages/core/dist/cli/utils/constants.js +2 -0
  16. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  17. package/packages/core/dist/cli/utils/constants.test.d.ts +10 -0
  18. package/packages/core/dist/cli/utils/constants.test.d.ts.map +1 -0
  19. package/packages/core/dist/cli/utils/constants.test.js +38 -0
  20. package/packages/core/dist/cli/utils/constants.test.js.map +1 -0
  21. package/packages/core/dist/consultation/consultation-protocol.d.ts +139 -0
  22. package/packages/core/dist/consultation/consultation-protocol.d.ts.map +1 -0
  23. package/packages/core/dist/consultation/consultation-protocol.js +178 -0
  24. package/packages/core/dist/consultation/consultation-protocol.js.map +1 -0
  25. package/packages/core/dist/consultation/consultation-protocol.test.d.ts +20 -0
  26. package/packages/core/dist/consultation/consultation-protocol.test.d.ts.map +1 -0
  27. package/packages/core/dist/consultation/consultation-protocol.test.js +474 -0
  28. package/packages/core/dist/consultation/consultation-protocol.test.js.map +1 -0
  29. package/packages/core/dist/public/js/react/react.js +30 -30
  30. package/packages/core/dist/scripts/generate-report.test.js +2 -2
  31. package/packages/core/dist/scripts/generate-spider-report.test.js +2 -2
  32. package/packages/core/dist/scripts/generate-spider.test.js +2 -1
  33. package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
  34. package/packages/core/dist/server/api/file-browser.d.ts.map +1 -1
  35. package/packages/core/dist/server/api/file-browser.js +19 -1
  36. package/packages/core/dist/server/api/file-browser.js.map +1 -1
  37. package/packages/core/dist/server/api/git-fetch-cooldown.test.d.ts +10 -0
  38. package/packages/core/dist/server/api/git-fetch-cooldown.test.d.ts.map +1 -0
  39. package/packages/core/dist/server/api/git-fetch-cooldown.test.js +30 -0
  40. package/packages/core/dist/server/api/git-fetch-cooldown.test.js.map +1 -0
  41. package/packages/core/dist/server/api/git.d.ts +8 -0
  42. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  43. package/packages/core/dist/server/api/git.js +37 -10
  44. package/packages/core/dist/server/api/git.js.map +1 -1
  45. package/packages/core/dist/server/api/health-score.d.ts.map +1 -1
  46. package/packages/core/dist/server/api/health-score.js +25 -1
  47. package/packages/core/dist/server/api/health-score.js.map +1 -1
  48. package/packages/core/dist/server/api/index.d.ts +1 -1
  49. package/packages/core/dist/server/api/index.d.ts.map +1 -1
  50. package/packages/core/dist/server/api/index.js +1 -1
  51. package/packages/core/dist/server/api/index.js.map +1 -1
  52. package/packages/core/dist/server/api/settings.d.ts.map +1 -1
  53. package/packages/core/dist/server/api/settings.js +73 -2
  54. package/packages/core/dist/server/api/settings.js.map +1 -1
  55. package/packages/core/dist/server/api/theme-agents.d.ts.map +1 -1
  56. package/packages/core/dist/server/api/theme-agents.js +61 -0
  57. package/packages/core/dist/server/api/theme-agents.js.map +1 -1
  58. package/packages/core/dist/server/otlp-receiver.d.ts +35 -13
  59. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  60. package/packages/core/dist/server/otlp-receiver.js +76 -16
  61. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  62. package/packages/core/dist/server/paths.d.ts.map +1 -1
  63. package/packages/core/dist/server/paths.js +11 -1
  64. package/packages/core/dist/server/paths.js.map +1 -1
  65. package/packages/core/dist/server/server.d.ts +3 -1
  66. package/packages/core/dist/server/server.d.ts.map +1 -1
  67. package/packages/core/dist/server/server.js +23 -16
  68. package/packages/core/dist/server/server.js.map +1 -1
  69. package/packages/core/dist/server/server.test.js.map +1 -1
  70. package/packages/core/dist/workflow/gate-file-validation.d.ts +49 -0
  71. package/packages/core/dist/workflow/gate-file-validation.d.ts.map +1 -0
  72. package/packages/core/dist/workflow/gate-file-validation.js +157 -0
  73. package/packages/core/dist/workflow/gate-file-validation.js.map +1 -0
  74. package/packages/core/dist/workflow/gate-file-validation.test.d.ts +19 -0
  75. package/packages/core/dist/workflow/gate-file-validation.test.d.ts.map +1 -0
  76. package/packages/core/dist/workflow/gate-file-validation.test.js +536 -0
  77. package/packages/core/dist/workflow/gate-file-validation.test.js.map +1 -0
  78. package/packages/core/dist/workflow/gate-schema-validation.test.d.ts +14 -0
  79. package/packages/core/dist/workflow/gate-schema-validation.test.d.ts.map +1 -0
  80. package/packages/core/dist/workflow/gate-schema-validation.test.js +339 -0
  81. package/packages/core/dist/workflow/gate-schema-validation.test.js.map +1 -0
  82. package/packages/core/dist/workflow/handoff.js +2 -2
  83. package/packages/core/dist/workflow/handoff.js.map +1 -1
  84. package/packages/core/dist/workflow/handoff.test.js +16 -0
  85. package/packages/core/dist/workflow/handoff.test.js.map +1 -1
  86. package/packages/core/dist/workflow/variable-resolver.test.js +1 -1
  87. package/packages/core/dist/workflow/variable-resolver.test.js.map +1 -1
  88. package/packages/core/dist/workflow/workflow-migration.test.js +4 -3
  89. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  90. package/packages/core/dist/workflow/workflow-schema.d.ts +4 -2
  91. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  92. package/packages/core/dist/workflow/workflow-schema.js +43 -8
  93. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  94. package/pennyfarthing-dist/agents/README.md +6 -14
  95. package/pennyfarthing-dist/agents/architect.md +43 -30
  96. package/pennyfarthing-dist/agents/ba.md +30 -29
  97. package/pennyfarthing-dist/agents/dev.md +76 -41
  98. package/pennyfarthing-dist/agents/devops.md +57 -21
  99. package/pennyfarthing-dist/agents/orchestrator.md +3 -11
  100. package/pennyfarthing-dist/agents/pm.md +45 -31
  101. package/pennyfarthing-dist/agents/reviewer.md +20 -66
  102. package/pennyfarthing-dist/agents/sm-setup.md +2 -2
  103. package/pennyfarthing-dist/agents/sm.md +8 -30
  104. package/pennyfarthing-dist/agents/tea.md +25 -41
  105. package/pennyfarthing-dist/agents/tech-writer.md +33 -90
  106. package/pennyfarthing-dist/agents/ux-designer.md +39 -40
  107. package/pennyfarthing-dist/commands/benchmark-control.md +8 -64
  108. package/pennyfarthing-dist/commands/benchmark.md +8 -480
  109. package/pennyfarthing-dist/commands/job-fair.md +8 -97
  110. package/pennyfarthing-dist/commands/pf-benchmark-control.md +70 -0
  111. package/pennyfarthing-dist/commands/pf-benchmark.md +486 -0
  112. package/pennyfarthing-dist/commands/pf-chore.md +4 -4
  113. package/pennyfarthing-dist/commands/pf-ci.md +40 -0
  114. package/pennyfarthing-dist/commands/pf-close-epic.md +9 -27
  115. package/pennyfarthing-dist/commands/pf-continue-session.md +9 -213
  116. package/pennyfarthing-dist/commands/pf-create-branches-from-story.md +11 -353
  117. package/pennyfarthing-dist/commands/pf-docs.md +28 -0
  118. package/pennyfarthing-dist/commands/pf-epic.md +67 -0
  119. package/pennyfarthing-dist/commands/pf-git-cleanup.md +11 -52
  120. package/pennyfarthing-dist/commands/pf-git.md +75 -0
  121. package/pennyfarthing-dist/commands/pf-help.md +110 -128
  122. package/pennyfarthing-dist/commands/pf-job-fair.md +102 -0
  123. package/pennyfarthing-dist/commands/pf-new-work.md +9 -18
  124. package/pennyfarthing-dist/commands/pf-parallel-work.md +6 -66
  125. package/pennyfarthing-dist/commands/pf-release.md +11 -76
  126. package/pennyfarthing-dist/commands/pf-repo-status.md +11 -44
  127. package/pennyfarthing-dist/commands/pf-run-ci.md +8 -111
  128. package/pennyfarthing-dist/commands/pf-session.md +51 -0
  129. package/pennyfarthing-dist/commands/pf-solo.md +447 -0
  130. package/pennyfarthing-dist/commands/pf-sprint-planning.md +8 -104
  131. package/pennyfarthing-dist/commands/pf-standalone.md +1 -1
  132. package/pennyfarthing-dist/commands/pf-start-epic.md +9 -163
  133. package/pennyfarthing-dist/commands/pf-sync-epic-to-jira.md +8 -179
  134. package/pennyfarthing-dist/commands/pf-sync-work-with-sprint.md +8 -368
  135. package/pennyfarthing-dist/commands/pf-update-domain-docs.md +8 -78
  136. package/pennyfarthing-dist/commands/solo.md +8 -442
  137. package/pennyfarthing-dist/guides/agent-behavior.md +14 -14
  138. package/pennyfarthing-dist/guides/agent-coordination.md +7 -7
  139. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -6
  140. package/pennyfarthing-dist/guides/bikerack.md +128 -0
  141. package/pennyfarthing-dist/guides/brownfield-tools.md +133 -0
  142. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +2 -2
  143. package/pennyfarthing-dist/guides/gate-schema.md +227 -0
  144. package/pennyfarthing-dist/guides/gates.md +120 -0
  145. package/pennyfarthing-dist/guides/handoff-cli.md +116 -0
  146. package/pennyfarthing-dist/guides/hooks.md +86 -4
  147. package/pennyfarthing-dist/guides/output-styles.md +65 -0
  148. package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +5 -5
  149. package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -4
  150. package/pennyfarthing-dist/guides/prompt-patterns.md +5 -5
  151. package/pennyfarthing-dist/guides/reflector.md +4 -4
  152. package/pennyfarthing-dist/guides/session-artifacts.md +1 -1
  153. package/pennyfarthing-dist/guides/skill-schema.md +1 -1
  154. package/pennyfarthing-dist/guides/tandem-protocol.md +13 -1
  155. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  156. package/pennyfarthing-dist/guides/xml-tags.md +5 -4
  157. package/pennyfarthing-dist/personas/themes/hogans-heroes.yaml +11 -22
  158. package/pennyfarthing-dist/personas/themes/stephen-king.yaml +13 -24
  159. package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
  160. package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
  161. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  162. package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
  163. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  164. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
  165. package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
  166. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +0 -0
  167. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  168. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
  169. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  170. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +0 -0
  171. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  172. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  173. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  174. package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
  175. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +19 -14
  176. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
  177. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
  178. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  179. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
  180. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  181. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  182. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  183. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  184. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  185. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  186. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
  187. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  188. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  189. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  190. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  191. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  192. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  193. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  194. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  195. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  196. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  197. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  198. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  199. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  200. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  201. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  202. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  203. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  204. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  205. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  206. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  207. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  208. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  209. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
  210. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  211. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  212. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  213. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  214. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  215. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  216. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  217. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  218. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
  219. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +191 -57
  220. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +26 -10
  221. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  222. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  223. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  224. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  225. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  226. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  227. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
  228. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  229. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  230. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  231. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  232. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  233. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
  234. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  235. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  236. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  237. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  238. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  239. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
  240. package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
  241. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  242. package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
  243. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
  244. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
  245. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  246. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
  247. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
  248. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
  249. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
  250. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
  251. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
  252. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
  253. package/pennyfarthing-dist/skills/pf-changelog/SKILL.md +4 -4
  254. package/pennyfarthing-dist/skills/pf-sprint/skill.md +1 -1
  255. package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
  256. package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
  257. package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
  258. package/pennyfarthing-dist/skills/pf-systematic-debugging/SKILL.md +0 -1
  259. package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -0
  260. package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -0
  261. package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -0
  262. package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -0
  263. package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -0
  264. package/pennyfarthing-dist/skills/skill-registry.schema.json +4 -0
  265. package/pennyfarthing-dist/skills/skill-registry.yaml +8 -21
  266. package/pennyfarthing-dist/workflows/2party-tdd.yaml +11 -0
  267. package/pennyfarthing-dist/workflows/agent-docs.yaml +2 -0
  268. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +4 -0
  269. package/pennyfarthing-dist/workflows/bdd.yaml +4 -0
  270. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  271. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +3 -0
  272. package/pennyfarthing-dist/workflows/tdd.yaml +3 -0
  273. package/pennyfarthing-dist/workflows/trivial.yaml +2 -0
  274. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  283. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  289. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  291. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  292. package/pennyfarthing_scripts/bikerack/changed_panel.py +105 -0
  293. package/pennyfarthing_scripts/bikerack/debug_panel.py +218 -0
  294. package/pennyfarthing_scripts/bikerack/diffs_panel.py +203 -27
  295. package/pennyfarthing_scripts/cli.py +114 -0
  296. package/pennyfarthing_scripts/epic/__init__.py +0 -0
  297. package/pennyfarthing_scripts/epic/cli.py +64 -0
  298. package/pennyfarthing_scripts/gate/__init__.py +1 -0
  299. package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/gate/cli.py +56 -0
  303. package/pennyfarthing_scripts/gate/validate.py +266 -0
  304. package/pennyfarthing_scripts/git_group/__init__.py +0 -0
  305. package/pennyfarthing_scripts/git_group/cli.py +100 -0
  306. package/pennyfarthing_scripts/handoff/__init__.py +1 -0
  307. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  309. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/handoff/cli.py +120 -0
  315. package/pennyfarthing_scripts/handoff/complete_phase.py +155 -0
  316. package/pennyfarthing_scripts/handoff/gate_file.py +105 -0
  317. package/pennyfarthing_scripts/handoff/gate_runner.py +152 -0
  318. package/pennyfarthing_scripts/handoff/marker.py +109 -0
  319. package/pennyfarthing_scripts/handoff/resolve_gate.py +152 -0
  320. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  323. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  324. package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
  326. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  327. package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
  328. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  329. package/pennyfarthing_scripts/prime/workflow.py +39 -0
  330. package/pennyfarthing_scripts/session/__init__.py +0 -0
  331. package/pennyfarthing_scripts/session/cli.py +87 -0
  332. package/pennyfarthing_scripts/session_start_hook.py +4 -4
  333. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  334. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  335. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  336. package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  337. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  338. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  339. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  340. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/sprint/archive_epic.py +8 -0
  344. package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
  345. package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
  346. package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
  347. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  348. package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  353. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
  354. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
  355. package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
  356. package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
  357. package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
  358. package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
  359. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  360. package/pennyfarthing_scripts/tests/test_108_1_gate_migration.py +540 -0
  361. package/pennyfarthing_scripts/tests/test_108_2_remove_handoff_fallback.py +339 -0
  362. package/pennyfarthing_scripts/tests/test_archive_epic.py +1 -2
  363. package/pennyfarthing_scripts/tests/test_confidence_sm_evaluation.py +253 -0
  364. package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +315 -0
  365. package/pennyfarthing_scripts/tests/test_gate_file_resolution.py +341 -0
  366. package/pennyfarthing_scripts/tests/test_gate_runner.py +620 -0
  367. package/pennyfarthing_scripts/tests/test_handoff_cli.py +929 -0
  368. package/pennyfarthing_scripts/tests/test_handoff_e2e.py +454 -0
  369. package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +464 -0
  370. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  371. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  372. package/pennyfarthing_scripts/validate/adapters/skill_command.py +200 -0
  373. package/pennyfarthing_scripts/validate/adapters/workflow.py +64 -0
  374. package/pennyfarthing_scripts/validate/cli.py +15 -4
  375. package/packages/core/dist/benchmark/package-exports.test.d.ts.map +0 -1
  376. package/packages/core/dist/benchmark/package-exports.test.js.map +0 -1
  377. package/packages/core/dist/scripts/benchmark-integration.d.ts +0 -182
  378. package/packages/core/dist/scripts/benchmark-integration.d.ts.map +0 -1
  379. package/packages/core/dist/scripts/benchmark-integration.js +0 -691
  380. package/packages/core/dist/scripts/benchmark-integration.js.map +0 -1
  381. package/packages/core/dist/scripts/benchmark-integration.test.d.ts +0 -13
  382. package/packages/core/dist/scripts/benchmark-integration.test.d.ts.map +0 -1
  383. package/packages/core/dist/scripts/benchmark-integration.test.js +0 -680
  384. package/packages/core/dist/scripts/benchmark-integration.test.js.map +0 -1
  385. package/packages/core/dist/scripts/debugging-scenarios.test.d.ts +0 -18
  386. package/packages/core/dist/scripts/debugging-scenarios.test.d.ts.map +0 -1
  387. package/packages/core/dist/scripts/debugging-scenarios.test.js +0 -317
  388. package/packages/core/dist/scripts/debugging-scenarios.test.js.map +0 -1
  389. package/packages/core/dist/scripts/job-fair-aggregator.d.ts +0 -150
  390. package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +0 -1
  391. package/packages/core/dist/scripts/job-fair-aggregator.js +0 -547
  392. package/packages/core/dist/scripts/job-fair-aggregator.js.map +0 -1
  393. package/packages/core/dist/scripts/job-fair-aggregator.test.d.ts +0 -14
  394. package/packages/core/dist/scripts/job-fair-aggregator.test.d.ts.map +0 -1
  395. package/packages/core/dist/scripts/job-fair-aggregator.test.js +0 -616
  396. package/packages/core/dist/scripts/job-fair-aggregator.test.js.map +0 -1
  397. package/pennyfarthing-dist/agents/handoff.md +0 -250
  398. package/pennyfarthing-dist/agents/sm-handoff.md +0 -152
  399. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -112
  400. package/pennyfarthing-dist/skills/pf-dev-patterns/SKILL.md +0 -461
  401. package/scripts/README.md +0 -41
@@ -0,0 +1,464 @@
1
+ """Tests for gate.file field in resolve_gate — Story 106-3.
2
+
3
+ Epic: 106 (Gate Files & First Migration)
4
+ Story: 106-3 — Workflow YAML gate.file integration
5
+
6
+ Tests the gate.file field support in resolve_gate():
7
+ - Schema extension: gate.file extracted from workflow YAML
8
+ - Backward compatibility: gate.type-only workflows unchanged
9
+ - TDD workflow migration: green phase has file: gates/tests-pass
10
+
11
+ Acceptance Criteria:
12
+ - [AC1] resolve-gate.py reads gate.file field from workflow YAML phases
13
+ - [AC1] gate.file takes precedence over gate.type when both present
14
+ - [AC1] Returns gate_file: null when only gate.type exists
15
+ - [AC2] Workflows with only gate.type continue to work unchanged
16
+ - [AC2] Existing gate type logic remains functional
17
+ - [AC2] No breaking changes to resolve-gate API
18
+ - [AC3] Green phase in tdd.yaml has file: gates/tests-pass and type: tests_pass
19
+ - [AC3] Other phases remain unchanged (backward compat period)
20
+ - [AC3] File path is relative: gates/tests-pass (not absolute)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import textwrap
26
+ from pathlib import Path
27
+
28
+ import pytest
29
+ import yaml
30
+
31
+ from pennyfarthing_scripts.handoff.resolve_gate import resolve_gate
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Fixtures: Workflow YAML data with gate.file support
35
+ # ---------------------------------------------------------------------------
36
+
37
+ WORKFLOW_WITH_FILE_AND_TYPE = {
38
+ "workflow": {
39
+ "name": "tdd",
40
+ "phases": [
41
+ {"name": "setup", "agent": "sm"},
42
+ {"name": "red", "agent": "tea", "gate": {"type": "tests_fail"}},
43
+ {
44
+ "name": "green",
45
+ "agent": "dev",
46
+ "gate": {
47
+ "file": "gates/tests-pass",
48
+ "type": "tests_pass",
49
+ "condition": "All tests passing, no skipped tests",
50
+ },
51
+ },
52
+ {"name": "review", "agent": "reviewer", "gate": {"type": "approval"}},
53
+ {"name": "finish", "agent": "sm"},
54
+ ],
55
+ }
56
+ }
57
+
58
+ WORKFLOW_WITH_FILE_ONLY = {
59
+ "workflow": {
60
+ "name": "file-only",
61
+ "phases": [
62
+ {"name": "setup", "agent": "sm"},
63
+ {
64
+ "name": "green",
65
+ "agent": "dev",
66
+ "gate": {
67
+ "file": "gates/tests-pass",
68
+ "condition": "All tests passing",
69
+ },
70
+ },
71
+ {"name": "finish", "agent": "sm"},
72
+ ],
73
+ }
74
+ }
75
+
76
+ WORKFLOW_WITH_TYPE_ONLY = {
77
+ "workflow": {
78
+ "name": "legacy",
79
+ "phases": [
80
+ {"name": "setup", "agent": "sm"},
81
+ {
82
+ "name": "green",
83
+ "agent": "dev",
84
+ "gate": {"type": "tests_pass"},
85
+ },
86
+ {"name": "finish", "agent": "sm"},
87
+ ],
88
+ }
89
+ }
90
+
91
+ SESSION_WITH_ASSESSMENT = textwrap.dedent("""\
92
+ # Story 106-3: Workflow YAML gate.file integration
93
+
94
+ **Story ID:** 106-3
95
+ **Workflow:** tdd
96
+ **Phase:** green
97
+ **Phase Started:** 2026-02-15T10:00:00Z
98
+
99
+ ## TEA Assessment
100
+
101
+ **Tests Written:** 5 tests
102
+ **Status:** RED confirmed
103
+
104
+ ## Workflow Tracking
105
+
106
+ **Phase:** green
107
+ **Phase Started:** 2026-02-15T10:00:00Z
108
+
109
+ ### Phase History
110
+ | Phase | Started | Ended | Duration |
111
+ |-------|---------|-------|----------|
112
+ | green | 2026-02-15T10:00:00Z | - | - |
113
+
114
+ ### Handoff History
115
+ | From | To | Gate | Status | Timestamp |
116
+ |------|-----|------|--------|-----------|
117
+ """)
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Fixtures: Project structure
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ @pytest.fixture
126
+ def project(tmp_path: Path) -> Path:
127
+ """Create a minimal project structure with workflow YAMLs."""
128
+ workflows_dir = tmp_path / ".pennyfarthing" / "workflows"
129
+ workflows_dir.mkdir(parents=True)
130
+
131
+ for name, data in [
132
+ ("tdd", WORKFLOW_WITH_FILE_AND_TYPE),
133
+ ("file-only", WORKFLOW_WITH_FILE_ONLY),
134
+ ("legacy", WORKFLOW_WITH_TYPE_ONLY),
135
+ ]:
136
+ (workflows_dir / f"{name}.yaml").write_text(
137
+ yaml.dump(data, default_flow_style=False)
138
+ )
139
+
140
+ (tmp_path / ".session").mkdir()
141
+ session_file = tmp_path / ".session" / "106-3-session.md"
142
+ session_file.write_text(SESSION_WITH_ASSESSMENT)
143
+ return tmp_path
144
+
145
+
146
+ # ===========================================================================
147
+ # AC1: Schema Extension — gate.file extracted from workflow YAML
148
+ # ===========================================================================
149
+
150
+
151
+ class TestResolveGateFilePresent:
152
+ """AC1: resolve_gate returns gate_file when gate.file is in workflow YAML."""
153
+
154
+ def test_gate_file_populated_when_file_in_yaml(
155
+ self, project: Path
156
+ ) -> None:
157
+ """AC1: gate_file should be 'gates/tests-pass' when file field exists."""
158
+ result = resolve_gate("106-3", "tdd", "green", project_root=project)
159
+ assert result["gate_file"] == "gates/tests-pass"
160
+
161
+ def test_gate_type_still_populated_with_file(
162
+ self, project: Path
163
+ ) -> None:
164
+ """AC1: gate_type should still be 'tests_pass' when both file and type exist."""
165
+ result = resolve_gate("106-3", "tdd", "green", project_root=project)
166
+ assert result["gate_type"] == "tests_pass"
167
+
168
+ def test_both_fields_present_in_result(
169
+ self, project: Path
170
+ ) -> None:
171
+ """AC1: Both gate_file and gate_type should be non-None when both are in YAML."""
172
+ result = resolve_gate("106-3", "tdd", "green", project_root=project)
173
+ assert result["gate_file"] is not None
174
+ assert result["gate_type"] is not None
175
+
176
+ def test_status_still_ready_with_file(
177
+ self, project: Path
178
+ ) -> None:
179
+ """AC1: Status should still be 'ready' when assessment exists and gate.file is present."""
180
+ result = resolve_gate("106-3", "tdd", "green", project_root=project)
181
+ assert result["status"] == "ready"
182
+
183
+
184
+ class TestResolveGateFileOnly:
185
+ """AC1: resolve_gate handles gate with file but no type."""
186
+
187
+ def test_gate_file_populated_without_type(
188
+ self, project: Path
189
+ ) -> None:
190
+ """AC1: gate_file should be populated when only file exists (no type)."""
191
+ result = resolve_gate(
192
+ "106-3", "file-only", "green", project_root=project
193
+ )
194
+ assert result["gate_file"] == "gates/tests-pass"
195
+
196
+ def test_gate_type_null_when_only_file(
197
+ self, project: Path
198
+ ) -> None:
199
+ """AC1: gate_type should be None when only file is in gate."""
200
+ result = resolve_gate(
201
+ "106-3", "file-only", "green", project_root=project
202
+ )
203
+ assert result["gate_type"] is None
204
+
205
+ def test_status_skip_when_no_type_and_file_only(
206
+ self, project: Path
207
+ ) -> None:
208
+ """AC1: gate with file-only and no type → status depends on gate_type logic.
209
+
210
+ Current resolve_gate checks gate_type for skip logic:
211
+ - gate_type == 'manual' → skip
212
+ - gate_type is None → skip
213
+ So file-only gates (no type) currently get 'skip' status.
214
+ This is expected during migration — consumers check gate_file separately.
215
+ """
216
+ result = resolve_gate(
217
+ "106-3", "file-only", "green", project_root=project
218
+ )
219
+ assert result["status"] == "skip"
220
+
221
+
222
+ # ===========================================================================
223
+ # AC1: gate_file null when only gate.type exists
224
+ # ===========================================================================
225
+
226
+
227
+ class TestResolveGateTypeOnlyReturnsNullFile:
228
+ """AC1: gate_file is None when workflow only has gate.type."""
229
+
230
+ def test_gate_file_null_for_type_only(
231
+ self, project: Path
232
+ ) -> None:
233
+ """AC1: gate_file should be None when only gate.type exists."""
234
+ result = resolve_gate(
235
+ "106-3", "legacy", "green", project_root=project
236
+ )
237
+ assert result["gate_file"] is None
238
+
239
+ def test_gate_type_populated_for_type_only(
240
+ self, project: Path
241
+ ) -> None:
242
+ """AC1: gate_type should be 'tests_pass' for type-only gate."""
243
+ result = resolve_gate(
244
+ "106-3", "legacy", "green", project_root=project
245
+ )
246
+ assert result["gate_type"] == "tests_pass"
247
+
248
+
249
+ # ===========================================================================
250
+ # AC2: Backward Compatibility
251
+ # ===========================================================================
252
+
253
+
254
+ class TestResolveGateBackwardCompat:
255
+ """AC2: Workflows with only gate.type continue to work unchanged."""
256
+
257
+ def test_legacy_workflow_status_ready(
258
+ self, project: Path
259
+ ) -> None:
260
+ """AC2: Legacy type-only workflow should return 'ready' with assessment."""
261
+ result = resolve_gate(
262
+ "106-3", "legacy", "green", project_root=project
263
+ )
264
+ assert result["status"] == "ready"
265
+
266
+ def test_legacy_workflow_next_agent(
267
+ self, project: Path
268
+ ) -> None:
269
+ """AC2: Legacy workflow should still resolve next_agent correctly."""
270
+ result = resolve_gate(
271
+ "106-3", "legacy", "green", project_root=project
272
+ )
273
+ assert result["next_agent"] == "sm"
274
+
275
+ def test_legacy_workflow_next_phase(
276
+ self, project: Path
277
+ ) -> None:
278
+ """AC2: Legacy workflow should still resolve next_phase correctly."""
279
+ result = resolve_gate(
280
+ "106-3", "legacy", "green", project_root=project
281
+ )
282
+ assert result["next_phase"] == "finish"
283
+
284
+ def test_resolve_result_contract_unchanged(
285
+ self, project: Path
286
+ ) -> None:
287
+ """AC2: RESOLVE_RESULT still has all 7 required fields."""
288
+ result = resolve_gate(
289
+ "106-3", "legacy", "green", project_root=project
290
+ )
291
+ required_fields = [
292
+ "status",
293
+ "gate_type",
294
+ "gate_file",
295
+ "next_agent",
296
+ "next_phase",
297
+ "assessment_found",
298
+ "error",
299
+ ]
300
+ for field in required_fields:
301
+ assert field in result, f"Missing field: {field}"
302
+
303
+ def test_red_phase_type_only_unaffected(
304
+ self, project: Path
305
+ ) -> None:
306
+ """AC2: TDD red phase (type-only, no file) still works correctly."""
307
+ result = resolve_gate("106-3", "tdd", "red", project_root=project)
308
+ assert result["gate_type"] == "tests_fail"
309
+ assert result["gate_file"] is None
310
+
311
+
312
+ # ===========================================================================
313
+ # AC3: TDD Workflow Migration — verify actual tdd.yaml
314
+ # ===========================================================================
315
+
316
+
317
+ class TestTddWorkflowMigration:
318
+ """AC3: TDD workflow green phase has file: gates/tests-pass.
319
+
320
+ These tests read the ACTUAL tdd.yaml from the project to verify
321
+ the migration has been applied. They will FAIL until the Dev
322
+ updates tdd.yaml.
323
+ """
324
+
325
+ @pytest.fixture
326
+ def tdd_yaml(self) -> dict:
327
+ """Load the actual tdd.yaml from pennyfarthing-dist."""
328
+ tdd_path = (
329
+ Path(__file__).resolve().parents[2]
330
+ / "pennyfarthing-dist"
331
+ / "workflows"
332
+ / "tdd.yaml"
333
+ )
334
+ assert tdd_path.exists(), f"tdd.yaml not found at {tdd_path}"
335
+ return yaml.safe_load(tdd_path.read_text())
336
+
337
+ def _get_phase(self, tdd_yaml: dict, name: str) -> dict:
338
+ """Get a phase by name from the workflow."""
339
+ for phase in tdd_yaml["workflow"]["phases"]:
340
+ if phase["name"] == name:
341
+ return phase
342
+ raise ValueError(f"Phase '{name}' not found")
343
+
344
+ def test_green_phase_has_gate_file(self, tdd_yaml: dict) -> None:
345
+ """AC3: Green phase should have gate.file = 'gates/tests-pass'."""
346
+ green = self._get_phase(tdd_yaml, "green")
347
+ gate = green.get("gate", {})
348
+ assert gate.get("file") == "gates/tests-pass", (
349
+ f"Expected gate.file='gates/tests-pass', got gate={gate}"
350
+ )
351
+
352
+ def test_green_phase_keeps_legacy_type(self, tdd_yaml: dict) -> None:
353
+ """AC3: Green phase should keep gate.type = 'tests_pass' for backward compat."""
354
+ green = self._get_phase(tdd_yaml, "green")
355
+ gate = green.get("gate", {})
356
+ assert gate.get("type") == "tests_pass", (
357
+ f"Expected gate.type='tests_pass', got gate={gate}"
358
+ )
359
+
360
+ def test_green_phase_file_is_relative(self, tdd_yaml: dict) -> None:
361
+ """AC3: File path should be relative (gates/tests-pass), not absolute."""
362
+ green = self._get_phase(tdd_yaml, "green")
363
+ gate = green.get("gate", {})
364
+ file_path = gate.get("file", "")
365
+ assert not file_path.startswith("/"), (
366
+ f"gate.file should be relative, got: {file_path}"
367
+ )
368
+ assert file_path == "gates/tests-pass", (
369
+ f"Expected 'gates/tests-pass', got: {file_path}"
370
+ )
371
+
372
+ def test_red_phase_unchanged(self, tdd_yaml: dict) -> None:
373
+ """AC3: Red phase should NOT have gate.file (backward compat period)."""
374
+ red = self._get_phase(tdd_yaml, "red")
375
+ gate = red.get("gate", {})
376
+ assert "file" not in gate, (
377
+ f"Red phase should not have gate.file yet, got gate={gate}"
378
+ )
379
+
380
+ def test_review_phase_unchanged(self, tdd_yaml: dict) -> None:
381
+ """AC3: Review phase should NOT have gate.file (backward compat period)."""
382
+ review = self._get_phase(tdd_yaml, "review")
383
+ gate = review.get("gate", {})
384
+ assert "file" not in gate, (
385
+ f"Review phase should not have gate.file yet, got gate={gate}"
386
+ )
387
+
388
+ def test_setup_phase_unchanged(self, tdd_yaml: dict) -> None:
389
+ """AC3: Setup phase should remain gateless."""
390
+ setup = self._get_phase(tdd_yaml, "setup")
391
+ assert "gate" not in setup, (
392
+ f"Setup phase should not have a gate, got: {setup}"
393
+ )
394
+
395
+ def test_finish_phase_unchanged(self, tdd_yaml: dict) -> None:
396
+ """AC3: Finish phase should remain gateless."""
397
+ finish = self._get_phase(tdd_yaml, "finish")
398
+ assert "gate" not in finish, (
399
+ f"Finish phase should not have a gate, got: {finish}"
400
+ )
401
+
402
+
403
+ # ===========================================================================
404
+ # AC3: Integration — resolve_gate against actual tdd.yaml
405
+ # ===========================================================================
406
+
407
+
408
+ class TestResolveGateWithRealTddYaml:
409
+ """AC3: resolve_gate returns gate_file when run against actual tdd.yaml.
410
+
411
+ These integration tests use the real workflow file (via the project root)
412
+ to verify the end-to-end gate.file field extraction.
413
+ """
414
+
415
+ @pytest.fixture
416
+ def real_project(self, tmp_path: Path) -> Path:
417
+ """Create a project that uses the actual tdd.yaml from pennyfarthing-dist."""
418
+ tdd_source = (
419
+ Path(__file__).resolve().parents[2]
420
+ / "pennyfarthing-dist"
421
+ / "workflows"
422
+ / "tdd.yaml"
423
+ )
424
+ assert tdd_source.exists(), f"tdd.yaml not found at {tdd_source}"
425
+
426
+ workflows_dir = tmp_path / ".pennyfarthing" / "workflows"
427
+ workflows_dir.mkdir(parents=True)
428
+ (workflows_dir / "tdd.yaml").write_text(tdd_source.read_text())
429
+
430
+ (tmp_path / ".session").mkdir()
431
+ session_file = tmp_path / ".session" / "106-3-session.md"
432
+ session_file.write_text(SESSION_WITH_ASSESSMENT)
433
+ return tmp_path
434
+
435
+ def test_green_phase_returns_gate_file(
436
+ self, real_project: Path
437
+ ) -> None:
438
+ """AC3: resolve_gate for tdd/green should return gate_file='gates/tests-pass'."""
439
+ result = resolve_gate(
440
+ "106-3", "tdd", "green", project_root=real_project
441
+ )
442
+ assert result["gate_file"] == "gates/tests-pass", (
443
+ f"Expected gate_file='gates/tests-pass', got: {result}"
444
+ )
445
+
446
+ def test_green_phase_still_returns_gate_type(
447
+ self, real_project: Path
448
+ ) -> None:
449
+ """AC3: resolve_gate for tdd/green should still return gate_type='tests_pass'."""
450
+ result = resolve_gate(
451
+ "106-3", "tdd", "green", project_root=real_project
452
+ )
453
+ assert result["gate_type"] == "tests_pass", (
454
+ f"Expected gate_type='tests_pass', got: {result}"
455
+ )
456
+
457
+ def test_red_phase_gate_file_is_none(
458
+ self, real_project: Path
459
+ ) -> None:
460
+ """AC3: resolve_gate for tdd/red should have gate_file=None (not migrated yet)."""
461
+ result = resolve_gate(
462
+ "106-3", "tdd", "red", project_root=real_project
463
+ )
464
+ assert result["gate_file"] is None
@@ -211,6 +211,152 @@ def _get_body(content: str) -> str:
211
211
  return content[end + 4:].strip()
212
212
 
213
213
 
214
+ def _discover_registry(root: Path) -> dict | None:
215
+ """Load command-registry.yaml if it exists."""
216
+ path = root / "pennyfarthing-dist" / "command-registry.yaml"
217
+ if not path.is_file():
218
+ return None
219
+ try:
220
+ return yaml.safe_load(path.read_text())
221
+ except yaml.YAMLError:
222
+ return None
223
+
224
+
225
+ def _collect_registry_command_names(registry: dict) -> set[str]:
226
+ """Collect all command names expected from the registry."""
227
+ names: set[str] = set()
228
+
229
+ # Groups: each group has a slash field like "/pf-sprint"
230
+ for _group_name, group in registry.get("groups", {}).items():
231
+ slash = group.get("slash", "")
232
+ if slash:
233
+ # "/pf-sprint" -> "pf-sprint"
234
+ names.add(slash.lstrip("/"))
235
+
236
+ # Standalone
237
+ for _cmd_name, cmd in registry.get("standalone", {}).items():
238
+ slash = cmd.get("slash", "")
239
+ if slash:
240
+ names.add(slash.lstrip("/"))
241
+
242
+ # Agents
243
+ agents = registry.get("agents", {})
244
+ for _agent_name, agent in agents.get("commands", {}).items():
245
+ slash = agent.get("slash", "")
246
+ if slash:
247
+ names.add(slash.lstrip("/"))
248
+
249
+ # Benchmarking
250
+ benchmarking = registry.get("benchmarking", {})
251
+ for _cmd_name, cmd in benchmarking.get("commands", {}).items():
252
+ slash = cmd.get("slash", "")
253
+ if slash:
254
+ names.add(slash.lstrip("/"))
255
+
256
+ # Also add the new grouped commands (pf-git, pf-session, pf-epic, pf-ci, pf-docs)
257
+ for group_name in registry.get("groups", {}):
258
+ names.add(f"pf-{group_name}")
259
+ for cmd_name in registry.get("standalone", {}):
260
+ if cmd_name not in (
261
+ "help", "setup", "health-check", "prime", "check", "work",
262
+ "chore", "patch", "standalone", "party-mode", "brainstorming",
263
+ "retro", "permissions",
264
+ ):
265
+ names.add(f"pf-{cmd_name}")
266
+
267
+ return names
268
+
269
+
270
+ def validate_prefix(commands_dir: Path) -> tuple[list[str], list[str]]:
271
+ """Check all command files have pf- prefix."""
272
+ errors: list[str] = []
273
+ warnings: list[str] = []
274
+
275
+ for path in discover_command_files(commands_dir):
276
+ name = path.stem # filename without .md
277
+ if not name.startswith("pf-"):
278
+ # Check if it's a deprecated redirect stub
279
+ content = path.read_text()
280
+ fm = _parse_frontmatter(content)
281
+ if fm and fm.get("deprecated"):
282
+ continue # Redirect stubs are fine without prefix
283
+ warnings.append(f"{path.name}: missing 'pf-' prefix")
284
+
285
+ return errors, warnings
286
+
287
+
288
+ def validate_deprecated(commands_dir: Path) -> tuple[list[str], list[str]]:
289
+ """Check deprecated files have redirect field."""
290
+ errors: list[str] = []
291
+ warnings: list[str] = []
292
+
293
+ for path in discover_command_files(commands_dir):
294
+ content = path.read_text()
295
+ fm = _parse_frontmatter(content)
296
+ if fm and fm.get("deprecated") and not fm.get("redirect"):
297
+ errors.append(f"{path.name}: deprecated command missing 'redirect' field")
298
+
299
+ return errors, warnings
300
+
301
+
302
+ def validate_registry_crossref(
303
+ root: Path, commands_dir: Path
304
+ ) -> tuple[list[str], list[str]]:
305
+ """Cross-reference command files with command-registry.yaml."""
306
+ errors: list[str] = []
307
+ warnings: list[str] = []
308
+
309
+ registry = _discover_registry(root)
310
+ if registry is None:
311
+ warnings.append("command-registry.yaml not found — skipping cross-reference")
312
+ return errors, warnings
313
+
314
+ registry_names = _collect_registry_command_names(registry)
315
+
316
+ # Check command files against registry
317
+ for path in discover_command_files(commands_dir):
318
+ name = path.stem
319
+ content = path.read_text()
320
+ fm = _parse_frontmatter(content)
321
+ if fm and fm.get("deprecated"):
322
+ continue # Skip deprecated stubs
323
+ if name not in registry_names:
324
+ warnings.append(f"{path.name}: not found in command-registry.yaml")
325
+
326
+ return errors, warnings
327
+
328
+
329
+ def validate_skill_alignment(root: Path) -> tuple[list[str], list[str]]:
330
+ """Check skill command_group values match registry groups."""
331
+ errors: list[str] = []
332
+ warnings: list[str] = []
333
+
334
+ registry = _discover_registry(root)
335
+ if registry is None:
336
+ return errors, warnings
337
+
338
+ registry_path = discover_skill_registry(root)
339
+ if registry_path is None:
340
+ return errors, warnings
341
+
342
+ try:
343
+ skills_data = yaml.safe_load(registry_path.read_text())
344
+ except yaml.YAMLError:
345
+ return errors, warnings
346
+
347
+ registry_groups = set(registry.get("groups", {}).keys())
348
+
349
+ for skill_name, skill in skills_data.get("skills", {}).items():
350
+ command_group = skill.get("command_group")
351
+ if command_group and command_group not in registry_groups:
352
+ warnings.append(
353
+ f"skill '{skill_name}': command_group '{command_group}' "
354
+ f"not found in command-registry.yaml groups"
355
+ )
356
+
357
+ return errors, warnings
358
+
359
+
214
360
  def validate_command_file(path: Path) -> tuple[list[str], list[str]]:
215
361
  """Validate a command markdown file.
216
362
 
@@ -288,4 +434,58 @@ def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateRepor
288
434
  if not file_errors:
289
435
  report.passed += 1
290
436
 
437
+ # --- New validation checks ---
438
+
439
+ # Prefix check
440
+ prefix_errors, prefix_warnings = validate_prefix(commands_dir)
441
+ for e in prefix_errors:
442
+ report.errors += 1
443
+ report.details.append(f"[ERROR] prefix: {e}")
444
+ for w in prefix_warnings:
445
+ if strict:
446
+ report.errors += 1
447
+ report.details.append(f"[ERROR] prefix: {w}")
448
+ else:
449
+ report.warnings += 1
450
+ report.details.append(f"[WARN] prefix: {w}")
451
+
452
+ # Deprecated check
453
+ depr_errors, depr_warnings = validate_deprecated(commands_dir)
454
+ for e in depr_errors:
455
+ report.errors += 1
456
+ report.details.append(f"[ERROR] deprecated: {e}")
457
+ for w in depr_warnings:
458
+ if strict:
459
+ report.errors += 1
460
+ report.details.append(f"[ERROR] deprecated: {w}")
461
+ else:
462
+ report.warnings += 1
463
+ report.details.append(f"[WARN] deprecated: {w}")
464
+
465
+ # Registry cross-reference
466
+ xref_errors, xref_warnings = validate_registry_crossref(root, commands_dir)
467
+ for e in xref_errors:
468
+ report.errors += 1
469
+ report.details.append(f"[ERROR] registry: {e}")
470
+ for w in xref_warnings:
471
+ if strict:
472
+ report.errors += 1
473
+ report.details.append(f"[ERROR] registry: {w}")
474
+ else:
475
+ report.warnings += 1
476
+ report.details.append(f"[WARN] registry: {w}")
477
+
478
+ # Skill alignment
479
+ align_errors, align_warnings = validate_skill_alignment(root)
480
+ for e in align_errors:
481
+ report.errors += 1
482
+ report.details.append(f"[ERROR] skill-align: {e}")
483
+ for w in align_warnings:
484
+ if strict:
485
+ report.errors += 1
486
+ report.details.append(f"[ERROR] skill-align: {w}")
487
+ else:
488
+ report.warnings += 1
489
+ report.details.append(f"[WARN] skill-align: {w}")
490
+
291
491
  return report