@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,105 @@
1
+ """Gate file discovery and resolution.
2
+
3
+ Resolves gate file references (e.g., "gates/tests-pass") to actual file paths.
4
+ Resolution order:
5
+ 1. .pennyfarthing/gates/{name}.md (project-local override)
6
+ 2. pennyfarthing-dist/gates/{name}.md (built-in fallback)
7
+
8
+ Non-existent files return an error result with status "blocked".
9
+
10
+ Story: 106-4 (Gate File Discovery and Resolution)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+
18
+ def resolve_gate_file(
19
+ gate_ref: str,
20
+ project_root: Path | None = None,
21
+ ) -> dict:
22
+ """Resolve a gate file reference to an absolute file path.
23
+
24
+ Args:
25
+ gate_ref: Gate reference string (e.g., "gates/tests-pass" or "tests-pass")
26
+ project_root: Project root path. Auto-detected if None.
27
+
28
+ Returns:
29
+ dict with keys:
30
+ status: "found" | "not_found"
31
+ path: str | None (absolute path if found)
32
+ error: str | None (error message if not found)
33
+ """
34
+ if project_root is None:
35
+ project_root = _find_project_root()
36
+
37
+ name = _sanitize_gate_name(gate_ref)
38
+ if name is None:
39
+ return _result(
40
+ status="not_found",
41
+ error=f"Invalid gate reference: {gate_ref!r}",
42
+ )
43
+
44
+ # Resolution order: local first, built-in fallback
45
+ search_paths = [
46
+ project_root / ".pennyfarthing" / "gates" / f"{name}.md",
47
+ project_root / "pennyfarthing-dist" / "gates" / f"{name}.md",
48
+ ]
49
+
50
+ for candidate in search_paths:
51
+ if candidate.is_file():
52
+ return _result(status="found", path=str(candidate.resolve()))
53
+
54
+ return _result(
55
+ status="not_found",
56
+ error=f"Gate file not found: {name}",
57
+ )
58
+
59
+
60
+ def _sanitize_gate_name(gate_ref: str) -> str | None:
61
+ """Extract a clean gate name from a reference string.
62
+
63
+ Strips 'gates/' prefix and '.md' suffix. Rejects empty names
64
+ and path traversal attempts.
65
+ """
66
+ if not gate_ref:
67
+ return None
68
+
69
+ name = gate_ref
70
+ # Strip gates/ prefix
71
+ if name.startswith("gates/"):
72
+ name = name[len("gates/"):]
73
+ # Strip .md suffix
74
+ if name.endswith(".md"):
75
+ name = name[: -len(".md")]
76
+
77
+ if not name:
78
+ return None
79
+
80
+ # Reject path traversal
81
+ if ".." in name or "/" in name:
82
+ return None
83
+
84
+ return name
85
+
86
+
87
+ def _result(
88
+ status: str,
89
+ path: str | None = None,
90
+ error: str | None = None,
91
+ ) -> dict:
92
+ return {
93
+ "status": status,
94
+ "path": path,
95
+ "error": error,
96
+ }
97
+
98
+
99
+ def _find_project_root() -> Path:
100
+ """Walk up from cwd looking for .pennyfarthing/ directory."""
101
+ cwd = Path.cwd()
102
+ for parent in [cwd, *cwd.parents]:
103
+ if (parent / ".pennyfarthing").is_dir():
104
+ return parent
105
+ return cwd
@@ -0,0 +1,152 @@
1
+ """Gate subagent runner — parse gate files and extract GATE_RESULT.
2
+
3
+ Provides two core functions for the agent exit protocol (step 6):
4
+ 1. parse_gate_file(path) — read gate file, extract model/name/content
5
+ 2. extract_gate_result(raw_output) — regex-extract GATE_RESULT from subagent output
6
+
7
+ The actual subagent spawning is handled by the Claude agent via Task tool.
8
+ These functions provide the parsing layer around that interaction.
9
+
10
+ Default-deny: missing or unparseable GATE_RESULT always returns fail.
11
+ Gate files are read-only at runtime — never written to.
12
+
13
+ Story: 106-2 (Gate subagent runner with GATE_RESULT contract)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from pathlib import Path
20
+
21
+ _DEFAULT_FAIL: dict = {
22
+ "status": "fail",
23
+ "message": "Gate evaluation failed or did not return GATE_RESULT",
24
+ "checks": [],
25
+ }
26
+
27
+
28
+ def parse_gate_file(
29
+ gate_path: str | Path,
30
+ ) -> dict:
31
+ """Parse a gate file and extract its metadata and content.
32
+
33
+ Reads the gate file (read-only), extracts the model attribute and gate name
34
+ from the <gate> tag, and returns the full content for subagent prompting.
35
+
36
+ Args:
37
+ gate_path: Absolute path to the gate file.
38
+
39
+ Returns:
40
+ dict with keys:
41
+ status: "ok" | "error"
42
+ name: str | None (gate name from <gate name="...">)
43
+ model: str (model from <gate model="..."> or "haiku")
44
+ content: str | None (full gate file content)
45
+ error: str | None
46
+ """
47
+ path = Path(gate_path)
48
+
49
+ if not path.exists():
50
+ return {
51
+ "status": "error",
52
+ "name": None,
53
+ "model": "haiku",
54
+ "content": None,
55
+ "error": f"Gate file not found: {path}",
56
+ }
57
+
58
+ content = path.read_text()
59
+
60
+ gate_match = re.search(r"<gate\b[^>]*>", content)
61
+ if not gate_match:
62
+ return {
63
+ "status": "error",
64
+ "name": None,
65
+ "model": "haiku",
66
+ "content": None,
67
+ "error": "No <gate> tag found in file",
68
+ }
69
+
70
+ gate_tag = gate_match.group(0)
71
+
72
+ name_match = re.search(r'name="([^"]+)"', gate_tag)
73
+ name = name_match.group(1) if name_match else None
74
+
75
+ model_match = re.search(r'model="([^"]+)"', gate_tag)
76
+ model = model_match.group(1) if model_match else "haiku"
77
+
78
+ return {
79
+ "status": "ok",
80
+ "name": name,
81
+ "model": model,
82
+ "content": content,
83
+ "error": None,
84
+ }
85
+
86
+
87
+ def extract_gate_result(
88
+ raw_output: str | None,
89
+ ) -> dict:
90
+ """Extract GATE_RESULT from raw subagent output via regex.
91
+
92
+ Parses the subagent's text output to find a GATE_RESULT YAML block.
93
+ Uses regex/grep patterns — NOT a full YAML parser.
94
+
95
+ Default-deny: if GATE_RESULT cannot be extracted, returns fail.
96
+
97
+ Args:
98
+ raw_output: Raw text output from the gate subagent.
99
+
100
+ Returns:
101
+ dict with keys:
102
+ status: "pass" | "fail"
103
+ message: str
104
+ checks: list[dict] (each with name, status, detail)
105
+ """
106
+ if raw_output is None or not raw_output.strip():
107
+ return dict(_DEFAULT_FAIL)
108
+
109
+ # Split on GATE_RESULT: and take the last block (AC7: multiple → last wins)
110
+ blocks = raw_output.split("GATE_RESULT:")
111
+ if len(blocks) < 2:
112
+ return dict(_DEFAULT_FAIL)
113
+
114
+ block = blocks[-1]
115
+
116
+ # Extract status — strict enum: only "pass" or "fail"
117
+ status_match = re.search(r"^\s*status:\s*(pass|fail)\s*$", block, re.MULTILINE)
118
+ if not status_match:
119
+ return dict(_DEFAULT_FAIL)
120
+
121
+ status = status_match.group(1)
122
+
123
+ # Extract message — double-quoted, single-quoted, or unquoted
124
+ message_match = re.search(
125
+ r"""^\s*message:\s*(?:"([^"]*)"|'([^']*)'|(.+?))\s*$""",
126
+ block,
127
+ re.MULTILINE,
128
+ )
129
+ message = ""
130
+ if message_match:
131
+ message = message_match.group(1) or message_match.group(2) or message_match.group(3) or ""
132
+
133
+ # Extract checks list via regex on consecutive name/status/detail lines
134
+ checks: list[dict] = []
135
+ check_pattern = re.compile(
136
+ r"^\s*-\s*name:\s*(?:\"([^\"]*)\"|'([^']*)'|(\S+))\s*\n"
137
+ r"\s*status:\s*(pass|fail)\s*\n"
138
+ r"\s*detail:\s*(?:\"([^\"]*)\"|'([^']*)'|(.+?))\s*$",
139
+ re.MULTILINE,
140
+ )
141
+ for m in check_pattern.finditer(block):
142
+ checks.append({
143
+ "name": m.group(1) or m.group(2) or m.group(3),
144
+ "status": m.group(4),
145
+ "detail": m.group(5) or m.group(6) or m.group(7) or "",
146
+ })
147
+
148
+ return {
149
+ "status": status,
150
+ "message": message,
151
+ "checks": checks,
152
+ }
@@ -0,0 +1,109 @@
1
+ """Generate AGENT_COMMAND block for handoff markers.
2
+
3
+ Replaces handoff-marker.sh with a Python implementation that reuses
4
+ the existing context.py module for environment detection.
5
+
6
+ Story: 105-4 (Script-First Handoff)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pennyfarthing_scripts.context import check_context
12
+
13
+
14
+ def generate_marker(
15
+ next_agent: str | None = None,
16
+ *,
17
+ error: str | None = None,
18
+ ) -> str:
19
+ """Generate an AGENT_COMMAND block for agent handoff.
20
+
21
+ Args:
22
+ next_agent: Agent to hand off to (e.g., "dev", "tea", "reviewer").
23
+ error: If set, generates an error block instead of a handoff.
24
+
25
+ Returns:
26
+ YAML-formatted AGENT_COMMAND block string.
27
+ """
28
+ if error:
29
+ return _block(fallback=error, error=True)
30
+
31
+ if not next_agent:
32
+ return _block(fallback="No next agent specified", error=True)
33
+
34
+ ctx = check_context()
35
+
36
+ cmd = f"/pf-{next_agent}"
37
+ pct = ctx.usable_percent if not ctx.error else "unknown"
38
+
39
+ if pct != "unknown" and pct >= 60:
40
+ context_warning = f" (context: {pct}% - consider /clear before continuing)"
41
+ elif pct != "unknown":
42
+ context_warning = f" (context: {pct}%)"
43
+ else:
44
+ context_warning = ""
45
+
46
+ if not ctx.relay_mode:
47
+ # Relay off — ask for confirmation
48
+ if ctx.is_cyclist:
49
+ return _block(
50
+ marker="<!-- CYCLIST:QUESTION:yesno -->",
51
+ question=f"Ready to hand off to {cmd}?",
52
+ fallback=f"Run `{cmd}` to continue",
53
+ )
54
+ return _block(
55
+ fallback=f"Run `{cmd}` to continue{context_warning}",
56
+ relay_mode=False,
57
+ context_percent=pct,
58
+ )
59
+
60
+ # Relay on — auto-handoff
61
+ # Cyclist uses its feedback loop (QuickActions → slash command injection).
62
+ # Non-Cyclist: we're already in the session, invoke the agent directly.
63
+ marker = None
64
+ if ctx.use_tirepump:
65
+ marker = f"<!-- CYCLIST:CONTEXT_CLEAR:{cmd} -->" if ctx.is_cyclist else None
66
+ else:
67
+ marker = f"<!-- CYCLIST:HANDOFF:{cmd} -->" if ctx.is_cyclist else None
68
+
69
+ if ctx.is_cyclist:
70
+ return _block(
71
+ marker=marker,
72
+ fallback=f"Run `{cmd}` to continue",
73
+ )
74
+
75
+ # Non-Cyclist relay: invoke the next agent directly
76
+ import subprocess
77
+ try:
78
+ result = subprocess.run(
79
+ ["pf", "agent", "start", next_agent],
80
+ capture_output=True,
81
+ text=True,
82
+ timeout=30,
83
+ )
84
+ agent_output = result.stdout.strip()
85
+ except Exception as e:
86
+ agent_output = f"Failed to invoke agent: {e}"
87
+
88
+ return _block(
89
+ fallback=f"Run `{cmd}` to continue{context_warning}",
90
+ relay_mode=True,
91
+ context_percent=pct,
92
+ invoke=cmd,
93
+ ) + f"\n\n{agent_output}"
94
+
95
+
96
+ def _block(**fields: object) -> str:
97
+ """Format an AGENT_COMMAND YAML block."""
98
+ lines = ["---", "AGENT_COMMAND:"]
99
+ for key, value in fields.items():
100
+ if isinstance(value, bool):
101
+ lines.append(f" {key}: {str(value).lower()}")
102
+ elif isinstance(value, str) and value:
103
+ lines.append(f' {key}: "{value}"')
104
+ elif value == "":
105
+ lines.append(f' {key}: ""')
106
+ else:
107
+ lines.append(f" {key}: {value}")
108
+ lines.append("---")
109
+ return "\n".join(lines)
@@ -0,0 +1,152 @@
1
+ """Resolve gate for current workflow phase.
2
+
3
+ Reads workflow YAML, finds current phase gate, checks for assessment
4
+ section in session file, and returns a structured RESOLVE_RESULT.
5
+
6
+ Story: 105-1 (Script-First Handoff)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+
14
+ import yaml
15
+
16
+
17
+ def resolve_gate(
18
+ story_id: str,
19
+ workflow: str,
20
+ phase: str,
21
+ project_root: Path | None = None,
22
+ ) -> dict:
23
+ """Resolve the gate for the current workflow phase.
24
+
25
+ Args:
26
+ story_id: Story identifier (e.g., "105-1")
27
+ workflow: Workflow name (e.g., "tdd", "trivial", "patch")
28
+ phase: Current phase name (e.g., "green", "implement", "fix")
29
+ project_root: Project root path. Auto-detected if None.
30
+
31
+ Returns:
32
+ RESOLVE_RESULT dict with keys:
33
+ status: "ready" | "blocked" | "skip"
34
+ gate_type: str | None
35
+ gate_file: str | None
36
+ next_agent: str | None
37
+ next_phase: str | None
38
+ assessment_found: bool
39
+ error: str | None
40
+ """
41
+ if project_root is None:
42
+ project_root = _find_project_root()
43
+
44
+ workflow_path = _find_workflow_yaml(project_root, workflow)
45
+ if workflow_path is None:
46
+ return _result(status="error", error=f"Workflow '{workflow}' not found")
47
+
48
+ try:
49
+ data = yaml.safe_load(workflow_path.read_text())
50
+ phases = data["workflow"]["phases"]
51
+ except Exception as e:
52
+ return _result(status="error", error=f"Failed to parse workflow: {e}")
53
+
54
+ current_idx = None
55
+ current_phase = None
56
+ for i, p in enumerate(phases):
57
+ if p["name"] == phase:
58
+ current_idx = i
59
+ current_phase = p
60
+ break
61
+
62
+ if current_phase is None:
63
+ return _result(
64
+ status="error",
65
+ error=f"Phase '{phase}' not found in workflow '{workflow}'",
66
+ )
67
+
68
+ gate = current_phase.get("gate")
69
+
70
+ if current_idx + 1 < len(phases):
71
+ nxt = phases[current_idx + 1]
72
+ next_phase = nxt["name"]
73
+ next_agent = nxt["agent"]
74
+ else:
75
+ next_phase = None
76
+ next_agent = None
77
+
78
+ if not gate:
79
+ return _result(
80
+ status="skip",
81
+ next_agent=next_agent,
82
+ next_phase=next_phase,
83
+ assessment_found=True,
84
+ )
85
+
86
+ gate_type = gate.get("type")
87
+ gate_file = gate.get("file")
88
+
89
+ if gate_type == "manual":
90
+ return _result(
91
+ status="skip",
92
+ gate_type="manual",
93
+ next_agent=next_agent,
94
+ next_phase=next_phase,
95
+ assessment_found=True,
96
+ )
97
+
98
+ session_path = project_root / ".session" / f"{story_id}-session.md"
99
+ assessment_found = False
100
+ if session_path.exists():
101
+ content = session_path.read_text()
102
+ assessment_found = bool(
103
+ re.search(r"^##\s+.*Assessment", content, re.MULTILINE)
104
+ )
105
+
106
+ status = "ready" if assessment_found else "blocked"
107
+ return _result(
108
+ status=status,
109
+ gate_type=gate_type,
110
+ gate_file=gate_file,
111
+ next_agent=next_agent,
112
+ next_phase=next_phase,
113
+ assessment_found=assessment_found,
114
+ )
115
+
116
+
117
+ def _result(
118
+ status: str,
119
+ gate_type: str | None = None,
120
+ gate_file: str | None = None,
121
+ next_agent: str | None = None,
122
+ next_phase: str | None = None,
123
+ assessment_found: bool = False,
124
+ error: str | None = None,
125
+ ) -> dict:
126
+ return {
127
+ "status": status,
128
+ "gate_type": gate_type,
129
+ "gate_file": gate_file,
130
+ "next_agent": next_agent,
131
+ "next_phase": next_phase,
132
+ "assessment_found": assessment_found,
133
+ "error": error,
134
+ }
135
+
136
+
137
+ def _find_workflow_yaml(project_root: Path, workflow: str) -> Path | None:
138
+ flat = project_root / ".pennyfarthing" / "workflows" / f"{workflow}.yaml"
139
+ if flat.exists():
140
+ return flat
141
+ subdir = project_root / ".pennyfarthing" / "workflows" / workflow / "workflow.yaml"
142
+ if subdir.exists():
143
+ return subdir
144
+ return None
145
+
146
+
147
+ def _find_project_root() -> Path:
148
+ cwd = Path.cwd()
149
+ for parent in [cwd, *cwd.parents]:
150
+ if (parent / ".pennyfarthing").is_dir():
151
+ return parent
152
+ return cwd
@@ -275,3 +275,42 @@ def check_redirect(workflow_status: WorkflowStatus, agent_name: str) -> tuple[st
275
275
  )
276
276
 
277
277
  return None
278
+
279
+
280
+ def get_phase_tandem_config(
281
+ workflow_name: str, phase_name: str, project_root: Path | None = None
282
+ ) -> dict[str, Any] | None:
283
+ """Extract tandem configuration for a specific workflow phase.
284
+
285
+ Reads the workflow YAML and returns the tandem block for the given phase,
286
+ or None if the phase has no tandem configuration.
287
+
288
+ Args:
289
+ workflow_name: Workflow name (tdd-tandem, bdd-tandem, etc.)
290
+ phase_name: Phase name (red, green, review, etc.)
291
+ project_root: Project root path (auto-detected if not provided)
292
+
293
+ Returns:
294
+ Dict with tandem config (partner, mode, model, token_budget, triggers, scope)
295
+ or None if no tandem config on this phase.
296
+ """
297
+ root = project_root or get_project_root()
298
+ workflow_path = root / "pennyfarthing-dist" / "workflows" / f"{workflow_name}.yaml"
299
+
300
+ if not workflow_path.exists():
301
+ return None
302
+
303
+ try:
304
+ data = yaml.safe_load(workflow_path.read_text())
305
+ phases = data.get("workflow", {}).get("phases", [])
306
+
307
+ for phase in phases:
308
+ if isinstance(phase, dict) and phase.get("name") == phase_name:
309
+ tandem = phase.get("tandem")
310
+ if isinstance(tandem, dict):
311
+ return dict(tandem)
312
+ return None
313
+
314
+ return None
315
+ except Exception:
316
+ return None
File without changes