@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,266 @@
1
+ """Gate file validation — schema, depth, cycles, and mandatory elements.
2
+
3
+ Validates a gate file for:
4
+ 1. Schema: <gate name="..."> with <purpose>, <pass>, <fail>
5
+ 2. Depth: max nesting depth of 3
6
+ 3. Cycles: no duplicate gate names
7
+ 4. Completeness: all required elements non-empty
8
+
9
+ Reports ALL errors at once (not fail-fast).
10
+
11
+ Story: 107-3 (Gate authoring guide and validation command)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+
20
+ MAX_DEPTH = 3
21
+
22
+
23
+ @dataclass
24
+ class GateInfo:
25
+ """Parsed gate metadata."""
26
+
27
+ name: str
28
+ model: str = "haiku"
29
+ depth: int = 0
30
+ children: list[str] = field(default_factory=list)
31
+
32
+
33
+ @dataclass
34
+ class ValidationResult:
35
+ """Result of gate file validation."""
36
+
37
+ valid: bool
38
+ gate_name: str | None = None
39
+ model: str | None = None
40
+ depth: int = 0
41
+ child_count: int = 0
42
+ errors: list[str] = field(default_factory=list)
43
+
44
+
45
+ def validate_gate_file(path: str | Path) -> ValidationResult:
46
+ """Validate a gate file for schema compliance, depth, and cycles.
47
+
48
+ Args:
49
+ path: Path to the gate file.
50
+
51
+ Returns:
52
+ ValidationResult with all errors collected.
53
+ """
54
+ path = Path(path)
55
+ errors: list[str] = []
56
+
57
+ if not path.exists():
58
+ return ValidationResult(valid=False, errors=[f"File not found: {path}"])
59
+
60
+ content = path.read_text()
61
+ if not content.strip():
62
+ return ValidationResult(valid=False, errors=["File is empty"])
63
+
64
+ # Tokenize all gate-related tags
65
+ tokens = _tokenize(content)
66
+
67
+ if not tokens:
68
+ return ValidationResult(
69
+ valid=False,
70
+ errors=["No <gate> tag found in file"],
71
+ )
72
+
73
+ # Parse gate structure and collect errors
74
+ gate_names: list[str] = []
75
+ root_name: str | None = None
76
+ root_model: str | None = None
77
+ max_depth = 0
78
+ depth = 0
79
+
80
+ for token in tokens:
81
+ if token["type"] == "gate_open":
82
+ name = token.get("name")
83
+ model = token.get("model", "haiku")
84
+
85
+ if depth == 0:
86
+ root_name = name
87
+ root_model = model
88
+
89
+ if not name:
90
+ errors.append(
91
+ f"Gate element at depth {depth} missing required 'name' attribute"
92
+ )
93
+ else:
94
+ # Check for duplicate names (cycle indicator)
95
+ if name in gate_names:
96
+ errors.append(
97
+ f"Duplicate gate name '{name}' — names must be unique within a file"
98
+ )
99
+ gate_names.append(name)
100
+
101
+ depth += 1
102
+ if depth - 1 > max_depth:
103
+ max_depth = depth - 1
104
+
105
+ # Depth limit check (depth is 0-indexed, so depth-1 after increment)
106
+ if depth - 1 > MAX_DEPTH:
107
+ gate_label = name or "(unnamed)"
108
+ errors.append(
109
+ f"Gate depth limit exceeded: '{gate_label}' at depth {depth - 1} (max {MAX_DEPTH})"
110
+ )
111
+
112
+ elif token["type"] == "gate_close":
113
+ depth -= 1
114
+
115
+ # Validate required child elements for each gate
116
+ _validate_required_elements(content, gate_names, errors)
117
+
118
+ child_count = len(gate_names) - 1 if len(gate_names) > 1 else 0
119
+
120
+ return ValidationResult(
121
+ valid=len(errors) == 0,
122
+ gate_name=root_name,
123
+ model=root_model,
124
+ depth=max_depth,
125
+ child_count=child_count,
126
+ errors=errors,
127
+ )
128
+
129
+
130
+ def _tokenize(content: str) -> list[dict]:
131
+ """Tokenize gate-related XML tags from content."""
132
+ tokens: list[dict] = []
133
+
134
+ # Match opening gate tags with attributes
135
+ open_pattern = re.compile(r"<gate\b([^>]*)>", re.IGNORECASE)
136
+ close_pattern = re.compile(r"</gate\s*>", re.IGNORECASE)
137
+
138
+ # Build a list of all tags with their positions
139
+ events: list[tuple[int, dict]] = []
140
+
141
+ for m in open_pattern.finditer(content):
142
+ attrs = m.group(1)
143
+ name_match = re.search(r'name="([^"]*)"', attrs)
144
+ model_match = re.search(r'model="([^"]*)"', attrs)
145
+ events.append((m.start(), {
146
+ "type": "gate_open",
147
+ "name": name_match.group(1) if name_match else None,
148
+ "model": model_match.group(1) if model_match else "haiku",
149
+ }))
150
+
151
+ for m in close_pattern.finditer(content):
152
+ events.append((m.start(), {"type": "gate_close"}))
153
+
154
+ # Sort by position
155
+ events.sort(key=lambda e: e[0])
156
+ tokens = [e[1] for e in events]
157
+
158
+ return tokens
159
+
160
+
161
+ def _validate_required_elements(
162
+ content: str,
163
+ gate_names: list[str],
164
+ errors: list[str],
165
+ ) -> None:
166
+ """Validate that each gate has <purpose>, <pass>, and <fail> elements.
167
+
168
+ For each gate, checks that required child elements exist and are non-empty
169
+ within the gate's scope.
170
+ """
171
+ # For simple files with one gate, check globally
172
+ # For nested files, check per-gate scope
173
+ if len(gate_names) <= 1:
174
+ name = gate_names[0] if gate_names else "(unnamed)"
175
+ _check_required_for_gate(content, name, errors)
176
+ else:
177
+ # Split content by gate scopes and validate each
178
+ # Use a stack-based approach to extract each gate's direct content
179
+ _validate_nested_gates(content, errors)
180
+
181
+
182
+ def _check_required_for_gate(
183
+ scope_content: str,
184
+ gate_name: str,
185
+ errors: list[str],
186
+ ) -> None:
187
+ """Check that a gate scope contains non-empty <purpose>, <pass>, <fail>."""
188
+ for element in ("purpose", "pass", "fail"):
189
+ pattern = re.compile(
190
+ rf"<{element}>(.*?)</{element}>",
191
+ re.DOTALL | re.IGNORECASE,
192
+ )
193
+ match = pattern.search(scope_content)
194
+ if not match:
195
+ errors.append(f"Gate '{gate_name}' missing required <{element}> block")
196
+ elif not match.group(1).strip():
197
+ errors.append(f"Gate '{gate_name}' has empty <{element}> block")
198
+
199
+
200
+ def _validate_nested_gates(content: str, errors: list[str]) -> None:
201
+ """Validate required elements for each gate in a nested structure."""
202
+ # Extract each gate's scope using a stack-based parser
203
+ open_pattern = re.compile(r"<gate\b([^>]*)>", re.IGNORECASE)
204
+ close_pattern = re.compile(r"</gate\s*>", re.IGNORECASE)
205
+
206
+ events: list[tuple[int, str, str | None]] = []
207
+
208
+ for m in open_pattern.finditer(content):
209
+ name_match = re.search(r'name="([^"]*)"', m.group(1))
210
+ name = name_match.group(1) if name_match else None
211
+ events.append((m.start(), "open", name))
212
+
213
+ for m in close_pattern.finditer(content):
214
+ events.append((m.start(), "close", None))
215
+
216
+ events.sort(key=lambda e: e[0])
217
+
218
+ # Stack-based scope extraction
219
+ stack: list[tuple[str | None, int]] = [] # (name, start_pos)
220
+
221
+ for pos, event_type, name in events:
222
+ if event_type == "open":
223
+ stack.append((name, pos))
224
+ elif event_type == "close" and stack:
225
+ gate_name, start_pos = stack.pop()
226
+ # Extract this gate's direct content (between open and close)
227
+ gate_scope = content[start_pos:pos]
228
+ # Remove nested gate content to check only direct children
229
+ direct_content = _remove_nested_gates(gate_scope)
230
+ label = gate_name or "(unnamed)"
231
+ _check_required_for_gate(direct_content, label, errors)
232
+
233
+
234
+ def _remove_nested_gates(scope: str) -> str:
235
+ """Remove nested <gate>...</gate> blocks from scope content.
236
+
237
+ Keeps the outermost gate's direct content only.
238
+ """
239
+ depth = 0
240
+ result: list[str] = []
241
+ i = 0
242
+
243
+ open_pattern = re.compile(r"<gate\b[^>]*>", re.IGNORECASE)
244
+ close_pattern = re.compile(r"</gate\s*>", re.IGNORECASE)
245
+
246
+ while i < len(scope):
247
+ open_match = open_pattern.match(scope, i)
248
+ close_match = close_pattern.match(scope, i)
249
+
250
+ if open_match:
251
+ depth += 1
252
+ if depth <= 1:
253
+ # Keep the outermost gate open tag
254
+ result.append(open_match.group())
255
+ i = open_match.end()
256
+ elif close_match:
257
+ if depth <= 1:
258
+ result.append(close_match.group())
259
+ depth -= 1
260
+ i = close_match.end()
261
+ else:
262
+ if depth <= 1:
263
+ result.append(scope[i])
264
+ i += 1
265
+
266
+ return "".join(result)
File without changes
@@ -0,0 +1,100 @@
1
+ """
2
+ Git CLI - Click-based CLI for git repository operations.
3
+
4
+ Usage:
5
+ pf git [COMMAND] [ARGS]...
6
+
7
+ Commands:
8
+ status Check git status of all project repos
9
+ cleanup Organize changes into proper commits/branches
10
+ branches Create feature branches from a story
11
+ release Interactive release with verification gates
12
+ """
13
+
14
+ import click
15
+
16
+ from pennyfarthing_scripts.common.config import get_project_root
17
+
18
+
19
+ @click.group()
20
+ def git():
21
+ """Repository operations across all configured repos.
22
+
23
+ \b
24
+ Commands:
25
+ status - Check git status of all repos
26
+ cleanup - Organize changes into commits/branches
27
+ branches - Create feature branches from a story
28
+ release - Interactive release with verification gates
29
+ """
30
+ pass
31
+
32
+
33
+ @git.command()
34
+ @click.option("--brief", is_flag=True, help="One-line-per-repo summary")
35
+ def status(brief: bool):
36
+ """Check git status of all project repos.
37
+
38
+ Shows branch, uncommitted changes, and ahead/behind status for each repo.
39
+ """
40
+ import subprocess
41
+
42
+ root = get_project_root()
43
+ script = root / ".pennyfarthing" / "scripts" / "git" / "git-status-all.sh"
44
+
45
+ if not script.is_file():
46
+ click.echo("Error: git-status-all.sh not found", err=True)
47
+ raise SystemExit(1)
48
+
49
+ cmd = [str(script)]
50
+ if brief:
51
+ cmd.append("--brief")
52
+
53
+ result = subprocess.run(cmd, cwd=str(root))
54
+ raise SystemExit(result.returncode)
55
+
56
+
57
+ @git.command()
58
+ def cleanup():
59
+ """Organize changes into proper commits and branches.
60
+
61
+ Starts the git-cleanup stepped workflow via BikeLane.
62
+ Equivalent to: /pf-workflow start git-cleanup
63
+ """
64
+ click.echo("Starting git-cleanup workflow...")
65
+ click.echo("Run: /pf-workflow start git-cleanup")
66
+ click.echo("Or: pf workflow start git-cleanup")
67
+
68
+
69
+ @git.command()
70
+ @click.argument("story_id")
71
+ def branches(story_id: str):
72
+ """Create feature branches in both repos from a story.
73
+
74
+ \b
75
+ Arguments:
76
+ STORY_ID - The story ID to create branches for (e.g., 86-3)
77
+ """
78
+ import subprocess
79
+
80
+ root = get_project_root()
81
+ script = root / ".pennyfarthing" / "scripts" / "git" / "create-branches.sh"
82
+
83
+ if not script.is_file():
84
+ click.echo("Error: create-branches.sh not found", err=True)
85
+ raise SystemExit(1)
86
+
87
+ result = subprocess.run([str(script), story_id], cwd=str(root))
88
+ raise SystemExit(result.returncode)
89
+
90
+
91
+ @git.command()
92
+ def release():
93
+ """Interactive release with verification gates.
94
+
95
+ Starts the release stepped workflow via BikeLane.
96
+ Equivalent to: /pf-workflow start release
97
+ """
98
+ click.echo("Starting release workflow...")
99
+ click.echo("Run: /pf-workflow start release")
100
+ click.echo("Or: pf workflow start release")
@@ -0,0 +1 @@
1
+ """Handoff CLI — Phase gate resolution and atomic session transitions."""
@@ -0,0 +1,120 @@
1
+ """Handoff CLI — Phase gate resolution, session transitions, and marker generation.
2
+
3
+ Usage:
4
+ pf handoff resolve-gate STORY_ID WORKFLOW PHASE
5
+ pf handoff complete-phase STORY_ID WORKFLOW FROM_PHASE TO_PHASE GATE_TYPE
6
+ pf handoff marker NEXT_AGENT [--error MESSAGE]
7
+
8
+ Stories: 105-1, 105-4 (Script-First Handoff)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import click
14
+
15
+
16
+ @click.group()
17
+ def handoff():
18
+ """Phase gate resolution and session transitions.
19
+
20
+ \b
21
+ Commands:
22
+ resolve-gate - Resolve gate for current phase
23
+ complete-phase - Complete phase transition atomically
24
+ marker - Generate AGENT_COMMAND handoff marker
25
+ """
26
+ pass
27
+
28
+
29
+ @handoff.command("resolve-gate")
30
+ @click.argument("story_id")
31
+ @click.argument("workflow")
32
+ @click.argument("phase")
33
+ def resolve_gate_cmd(story_id: str, workflow: str, phase: str):
34
+ """Resolve the gate for the current workflow phase.
35
+
36
+ Reads workflow YAML, checks assessment, returns RESOLVE_RESULT.
37
+
38
+ \b
39
+ Arguments:
40
+ STORY_ID - Story identifier (e.g., 105-1)
41
+ WORKFLOW - Workflow name (e.g., tdd, trivial, patch)
42
+ PHASE - Current phase name (e.g., green, implement, fix)
43
+ """
44
+ from pennyfarthing_scripts.handoff.resolve_gate import resolve_gate
45
+
46
+ result = resolve_gate(story_id, workflow, phase)
47
+
48
+ import yaml
49
+
50
+ click.echo(yaml.dump({"RESOLVE_RESULT": result}, default_flow_style=False).rstrip())
51
+
52
+ if result.get("status") == "blocked":
53
+ raise SystemExit(1)
54
+
55
+
56
+ @handoff.command("complete-phase")
57
+ @click.argument("story_id")
58
+ @click.argument("workflow")
59
+ @click.argument("from_phase")
60
+ @click.argument("to_phase")
61
+ @click.argument("gate_type")
62
+ def complete_phase_cmd(
63
+ story_id: str,
64
+ workflow: str,
65
+ from_phase: str,
66
+ to_phase: str,
67
+ gate_type: str,
68
+ ):
69
+ """Complete a phase transition with atomic session update.
70
+
71
+ Updates session file: phase line, timestamps, history tables.
72
+
73
+ \b
74
+ Arguments:
75
+ STORY_ID - Story identifier (e.g., 105-1)
76
+ WORKFLOW - Workflow name (e.g., tdd, trivial)
77
+ FROM_PHASE - Phase being completed (e.g., green)
78
+ TO_PHASE - Phase being entered (e.g., review)
79
+ GATE_TYPE - Gate type that was passed (e.g., tests_pass)
80
+ """
81
+ from pennyfarthing_scripts.handoff.complete_phase import complete_phase
82
+
83
+ result = complete_phase(story_id, workflow, from_phase, to_phase, gate_type)
84
+
85
+ import yaml
86
+
87
+ click.echo(yaml.dump({"COMPLETE_RESULT": result}, default_flow_style=False).rstrip())
88
+
89
+ if result.get("status") == "error":
90
+ raise SystemExit(1)
91
+
92
+
93
+ @handoff.command("marker")
94
+ @click.argument("next_agent", required=False, default=None)
95
+ @click.option("--error", "error_msg", default=None, help="Generate error marker")
96
+ def marker_cmd(next_agent: str | None, error_msg: str | None):
97
+ """Generate AGENT_COMMAND handoff marker block.
98
+
99
+ Environment-aware marker generation. Detects Cyclist, relay mode,
100
+ and context usage to choose the appropriate marker type.
101
+
102
+ \b
103
+ Arguments:
104
+ NEXT_AGENT - Agent to hand off to (e.g., dev, tea, reviewer)
105
+
106
+ \b
107
+ Options:
108
+ --error MSG - Generate an error marker instead of a handoff
109
+ """
110
+ from pennyfarthing_scripts.handoff.marker import generate_marker
111
+
112
+ if not next_agent and not error_msg:
113
+ raise click.UsageError(
114
+ "Provide NEXT_AGENT or --error MESSAGE.\n\n"
115
+ "Examples:\n"
116
+ " pf handoff marker dev\n"
117
+ " pf handoff marker --error 'Tests failing'"
118
+ )
119
+
120
+ click.echo(generate_marker(next_agent, error=error_msg))
@@ -0,0 +1,155 @@
1
+ """Complete phase transition with atomic session update.
2
+
3
+ Atomically updates the session file (temp + mv) with phase transition,
4
+ timestamps, and history table entries.
5
+
6
+ Story: 105-1 (Script-First Handoff)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import re
13
+ import tempfile
14
+ from datetime import UTC, datetime
15
+ from pathlib import Path
16
+
17
+ import yaml
18
+
19
+
20
+ def complete_phase(
21
+ story_id: str,
22
+ workflow: str,
23
+ from_phase: str,
24
+ to_phase: str,
25
+ gate_type: str,
26
+ project_root: Path | None = None,
27
+ ) -> dict:
28
+ """Complete a phase transition with atomic session file update.
29
+
30
+ Args:
31
+ story_id: Story identifier (e.g., "105-1")
32
+ workflow: Workflow name (e.g., "tdd", "trivial")
33
+ from_phase: Phase being completed (e.g., "green")
34
+ to_phase: Phase being entered (e.g., "review")
35
+ gate_type: Gate type that was passed (e.g., "tests_pass")
36
+ project_root: Project root path. Auto-detected if None.
37
+
38
+ Returns:
39
+ COMPLETE_RESULT dict with keys:
40
+ status: "success" | "error"
41
+ session_file: str (path to session file)
42
+ error: str | None
43
+ """
44
+ if project_root is None:
45
+ project_root = _find_project_root()
46
+
47
+ session_path = project_root / ".session" / f"{story_id}-session.md"
48
+ if not session_path.exists():
49
+ return {
50
+ "status": "error",
51
+ "session_file": None,
52
+ "error": "Session file not found",
53
+ }
54
+
55
+ content = session_path.read_text()
56
+ now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
57
+
58
+ from_agent = _get_phase_agent(project_root, workflow, from_phase)
59
+ to_agent = _get_phase_agent(project_root, workflow, to_phase)
60
+
61
+ # Update all **Phase:** lines to new phase
62
+ content = re.sub(r"(\*\*Phase:\*\*) \S+", rf"\1 {to_phase}", content)
63
+
64
+ # Update all **Phase Started:** lines to now
65
+ content = re.sub(r"(\*\*Phase Started:\*\*) \S+", rf"\1 {now}", content)
66
+
67
+ # Update Phase History: fill Ended/Duration for from_phase, add new row
68
+ lines = content.splitlines()
69
+ result_lines = []
70
+ for line in lines:
71
+ if line.strip().startswith(f"| {from_phase}"):
72
+ cols = [c.strip() for c in line.split("|") if c.strip()]
73
+ if len(cols) >= 4 and cols[2] == "-":
74
+ started_str = cols[1]
75
+ duration = _calc_duration(started_str, now)
76
+ result_lines.append(
77
+ f"| {from_phase} | {started_str} | {now} | {duration} |"
78
+ )
79
+ result_lines.append(f"| {to_phase} | {now} | - | - |")
80
+ continue
81
+ result_lines.append(line)
82
+ content = "\n".join(result_lines)
83
+
84
+ # Add Handoff History row at end of table
85
+ handoff_row = (
86
+ f"| {from_phase} ({from_agent}) | {to_phase} ({to_agent}) "
87
+ f"| {gate_type} | PASSED | {now} |"
88
+ )
89
+ lines = content.splitlines()
90
+ insert_after = None
91
+ in_handoff = False
92
+ for i, line in enumerate(lines):
93
+ if "### Handoff History" in line:
94
+ in_handoff = True
95
+ if in_handoff and line.strip().startswith("|"):
96
+ insert_after = i
97
+ if insert_after is not None:
98
+ lines.insert(insert_after + 1, handoff_row)
99
+ content = "\n".join(lines)
100
+
101
+ # Atomic write: temp file in same directory + rename
102
+ temp_fd, temp_path_str = tempfile.mkstemp(
103
+ dir=str(session_path.parent), suffix=".tmp"
104
+ )
105
+ os.close(temp_fd)
106
+ temp_path = Path(temp_path_str)
107
+ try:
108
+ temp_path.write_text(content)
109
+ temp_path.rename(session_path)
110
+ except Exception:
111
+ temp_path.unlink(missing_ok=True)
112
+ raise
113
+
114
+ return {
115
+ "status": "success",
116
+ "session_file": f".session/{story_id}-session.md",
117
+ "error": None,
118
+ }
119
+
120
+
121
+ def _calc_duration(started_str: str, ended_str: str) -> str:
122
+ started = datetime.fromisoformat(started_str.replace("Z", "+00:00"))
123
+ ended = datetime.fromisoformat(ended_str.replace("Z", "+00:00"))
124
+ total_seconds = int((ended - started).total_seconds())
125
+ if total_seconds < 60:
126
+ return f"{total_seconds}s"
127
+ minutes = total_seconds // 60
128
+ seconds = total_seconds % 60
129
+ if total_seconds < 3600:
130
+ return f"{minutes}m {seconds}s" if seconds else f"{minutes}m"
131
+ hours = total_seconds // 3600
132
+ rem_minutes = (total_seconds % 3600) // 60
133
+ return f"{hours}h {rem_minutes}m" if rem_minutes else f"{hours}h"
134
+
135
+
136
+ def _get_phase_agent(project_root: Path, workflow: str, phase: str) -> str:
137
+ for name in [f"{workflow}.yaml", f"{workflow}/workflow.yaml"]:
138
+ path = project_root / ".pennyfarthing" / "workflows" / name
139
+ if path.exists():
140
+ try:
141
+ data = yaml.safe_load(path.read_text())
142
+ for p in data["workflow"]["phases"]:
143
+ if p["name"] == phase:
144
+ return p.get("agent", phase)
145
+ except Exception:
146
+ pass
147
+ return phase
148
+
149
+
150
+ def _find_project_root() -> Path:
151
+ cwd = Path.cwd()
152
+ for parent in [cwd, *cwd.parents]:
153
+ if (parent / ".pennyfarthing").is_dir():
154
+ return parent
155
+ return cwd