@pennyfarthing/core 11.0.0-alpha.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +84 -26
  2. package/package.json +14 -16
  3. package/packages/core/dist/cli/cyclist-migration.test.js +2 -1
  4. package/packages/core/dist/cli/cyclist-migration.test.js.map +1 -1
  5. package/packages/core/dist/cli/ocean-profiles.test.js +5 -4
  6. package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
  7. package/packages/core/dist/cli/theme-maker.test.js +5 -4
  8. package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
  9. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts +20 -0
  10. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts.map +1 -0
  11. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js +278 -0
  12. package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js.map +1 -0
  13. package/packages/core/dist/cli/utils/constants.d.ts +7 -1
  14. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  15. package/packages/core/dist/cli/utils/constants.js +2 -0
  16. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  17. package/packages/core/dist/cli/utils/constants.test.d.ts +10 -0
  18. package/packages/core/dist/cli/utils/constants.test.d.ts.map +1 -0
  19. package/packages/core/dist/cli/utils/constants.test.js +38 -0
  20. package/packages/core/dist/cli/utils/constants.test.js.map +1 -0
  21. package/packages/core/dist/consultation/consultation-protocol.d.ts +139 -0
  22. package/packages/core/dist/consultation/consultation-protocol.d.ts.map +1 -0
  23. package/packages/core/dist/consultation/consultation-protocol.js +178 -0
  24. package/packages/core/dist/consultation/consultation-protocol.js.map +1 -0
  25. package/packages/core/dist/consultation/consultation-protocol.test.d.ts +20 -0
  26. package/packages/core/dist/consultation/consultation-protocol.test.d.ts.map +1 -0
  27. package/packages/core/dist/consultation/consultation-protocol.test.js +474 -0
  28. package/packages/core/dist/consultation/consultation-protocol.test.js.map +1 -0
  29. package/packages/core/dist/public/js/react/react.js +30 -30
  30. package/packages/core/dist/scripts/generate-report.test.js +2 -2
  31. package/packages/core/dist/scripts/generate-spider-report.test.js +2 -2
  32. package/packages/core/dist/scripts/generate-spider.test.js +2 -1
  33. package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
  34. package/packages/core/dist/server/api/file-browser.d.ts.map +1 -1
  35. package/packages/core/dist/server/api/file-browser.js +19 -1
  36. package/packages/core/dist/server/api/file-browser.js.map +1 -1
  37. package/packages/core/dist/server/api/git-fetch-cooldown.test.d.ts +10 -0
  38. package/packages/core/dist/server/api/git-fetch-cooldown.test.d.ts.map +1 -0
  39. package/packages/core/dist/server/api/git-fetch-cooldown.test.js +30 -0
  40. package/packages/core/dist/server/api/git-fetch-cooldown.test.js.map +1 -0
  41. package/packages/core/dist/server/api/git.d.ts +8 -0
  42. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  43. package/packages/core/dist/server/api/git.js +37 -10
  44. package/packages/core/dist/server/api/git.js.map +1 -1
  45. package/packages/core/dist/server/api/health-score.d.ts.map +1 -1
  46. package/packages/core/dist/server/api/health-score.js +25 -1
  47. package/packages/core/dist/server/api/health-score.js.map +1 -1
  48. package/packages/core/dist/server/api/index.d.ts +1 -1
  49. package/packages/core/dist/server/api/index.d.ts.map +1 -1
  50. package/packages/core/dist/server/api/index.js +1 -1
  51. package/packages/core/dist/server/api/index.js.map +1 -1
  52. package/packages/core/dist/server/api/settings.d.ts.map +1 -1
  53. package/packages/core/dist/server/api/settings.js +73 -2
  54. package/packages/core/dist/server/api/settings.js.map +1 -1
  55. package/packages/core/dist/server/api/theme-agents.d.ts.map +1 -1
  56. package/packages/core/dist/server/api/theme-agents.js +61 -0
  57. package/packages/core/dist/server/api/theme-agents.js.map +1 -1
  58. package/packages/core/dist/server/otlp-receiver.d.ts +35 -13
  59. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  60. package/packages/core/dist/server/otlp-receiver.js +76 -16
  61. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  62. package/packages/core/dist/server/paths.d.ts.map +1 -1
  63. package/packages/core/dist/server/paths.js +11 -1
  64. package/packages/core/dist/server/paths.js.map +1 -1
  65. package/packages/core/dist/server/server.d.ts +3 -1
  66. package/packages/core/dist/server/server.d.ts.map +1 -1
  67. package/packages/core/dist/server/server.js +23 -16
  68. package/packages/core/dist/server/server.js.map +1 -1
  69. package/packages/core/dist/server/server.test.js.map +1 -1
  70. package/packages/core/dist/workflow/gate-file-validation.d.ts +49 -0
  71. package/packages/core/dist/workflow/gate-file-validation.d.ts.map +1 -0
  72. package/packages/core/dist/workflow/gate-file-validation.js +157 -0
  73. package/packages/core/dist/workflow/gate-file-validation.js.map +1 -0
  74. package/packages/core/dist/workflow/gate-file-validation.test.d.ts +19 -0
  75. package/packages/core/dist/workflow/gate-file-validation.test.d.ts.map +1 -0
  76. package/packages/core/dist/workflow/gate-file-validation.test.js +536 -0
  77. package/packages/core/dist/workflow/gate-file-validation.test.js.map +1 -0
  78. package/packages/core/dist/workflow/gate-schema-validation.test.d.ts +14 -0
  79. package/packages/core/dist/workflow/gate-schema-validation.test.d.ts.map +1 -0
  80. package/packages/core/dist/workflow/gate-schema-validation.test.js +339 -0
  81. package/packages/core/dist/workflow/gate-schema-validation.test.js.map +1 -0
  82. package/packages/core/dist/workflow/handoff.js +2 -2
  83. package/packages/core/dist/workflow/handoff.js.map +1 -1
  84. package/packages/core/dist/workflow/handoff.test.js +16 -0
  85. package/packages/core/dist/workflow/handoff.test.js.map +1 -1
  86. package/packages/core/dist/workflow/variable-resolver.test.js +1 -1
  87. package/packages/core/dist/workflow/variable-resolver.test.js.map +1 -1
  88. package/packages/core/dist/workflow/workflow-migration.test.js +4 -3
  89. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  90. package/packages/core/dist/workflow/workflow-schema.d.ts +4 -2
  91. package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
  92. package/packages/core/dist/workflow/workflow-schema.js +43 -8
  93. package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
  94. package/pennyfarthing-dist/agents/README.md +6 -14
  95. package/pennyfarthing-dist/agents/architect.md +43 -30
  96. package/pennyfarthing-dist/agents/ba.md +30 -29
  97. package/pennyfarthing-dist/agents/dev.md +76 -41
  98. package/pennyfarthing-dist/agents/devops.md +57 -21
  99. package/pennyfarthing-dist/agents/orchestrator.md +3 -11
  100. package/pennyfarthing-dist/agents/pm.md +45 -31
  101. package/pennyfarthing-dist/agents/reviewer.md +20 -66
  102. package/pennyfarthing-dist/agents/sm-setup.md +2 -2
  103. package/pennyfarthing-dist/agents/sm.md +8 -30
  104. package/pennyfarthing-dist/agents/tea.md +25 -41
  105. package/pennyfarthing-dist/agents/tech-writer.md +33 -90
  106. package/pennyfarthing-dist/agents/ux-designer.md +39 -40
  107. package/pennyfarthing-dist/commands/benchmark-control.md +8 -64
  108. package/pennyfarthing-dist/commands/benchmark.md +8 -480
  109. package/pennyfarthing-dist/commands/job-fair.md +8 -97
  110. package/pennyfarthing-dist/commands/pf-benchmark-control.md +70 -0
  111. package/pennyfarthing-dist/commands/pf-benchmark.md +486 -0
  112. package/pennyfarthing-dist/commands/pf-chore.md +4 -4
  113. package/pennyfarthing-dist/commands/pf-ci.md +40 -0
  114. package/pennyfarthing-dist/commands/pf-close-epic.md +9 -27
  115. package/pennyfarthing-dist/commands/pf-continue-session.md +9 -213
  116. package/pennyfarthing-dist/commands/pf-create-branches-from-story.md +11 -353
  117. package/pennyfarthing-dist/commands/pf-docs.md +28 -0
  118. package/pennyfarthing-dist/commands/pf-epic.md +67 -0
  119. package/pennyfarthing-dist/commands/pf-git-cleanup.md +11 -52
  120. package/pennyfarthing-dist/commands/pf-git.md +75 -0
  121. package/pennyfarthing-dist/commands/pf-help.md +110 -128
  122. package/pennyfarthing-dist/commands/pf-job-fair.md +102 -0
  123. package/pennyfarthing-dist/commands/pf-new-work.md +9 -18
  124. package/pennyfarthing-dist/commands/pf-parallel-work.md +6 -66
  125. package/pennyfarthing-dist/commands/pf-release.md +11 -76
  126. package/pennyfarthing-dist/commands/pf-repo-status.md +11 -44
  127. package/pennyfarthing-dist/commands/pf-run-ci.md +8 -111
  128. package/pennyfarthing-dist/commands/pf-session.md +51 -0
  129. package/pennyfarthing-dist/commands/pf-solo.md +447 -0
  130. package/pennyfarthing-dist/commands/pf-sprint-planning.md +8 -104
  131. package/pennyfarthing-dist/commands/pf-standalone.md +1 -1
  132. package/pennyfarthing-dist/commands/pf-start-epic.md +9 -163
  133. package/pennyfarthing-dist/commands/pf-sync-epic-to-jira.md +8 -179
  134. package/pennyfarthing-dist/commands/pf-sync-work-with-sprint.md +8 -368
  135. package/pennyfarthing-dist/commands/pf-update-domain-docs.md +8 -78
  136. package/pennyfarthing-dist/commands/solo.md +8 -442
  137. package/pennyfarthing-dist/guides/agent-behavior.md +14 -14
  138. package/pennyfarthing-dist/guides/agent-coordination.md +7 -7
  139. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -6
  140. package/pennyfarthing-dist/guides/bikerack.md +128 -0
  141. package/pennyfarthing-dist/guides/brownfield-tools.md +133 -0
  142. package/pennyfarthing-dist/guides/command-tag-taxonomy.md +2 -2
  143. package/pennyfarthing-dist/guides/gate-schema.md +227 -0
  144. package/pennyfarthing-dist/guides/gates.md +120 -0
  145. package/pennyfarthing-dist/guides/handoff-cli.md +116 -0
  146. package/pennyfarthing-dist/guides/hooks.md +86 -4
  147. package/pennyfarthing-dist/guides/output-styles.md +65 -0
  148. package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +5 -5
  149. package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -4
  150. package/pennyfarthing-dist/guides/prompt-patterns.md +5 -5
  151. package/pennyfarthing-dist/guides/reflector.md +4 -4
  152. package/pennyfarthing-dist/guides/session-artifacts.md +1 -1
  153. package/pennyfarthing-dist/guides/skill-schema.md +1 -1
  154. package/pennyfarthing-dist/guides/tandem-protocol.md +13 -1
  155. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  156. package/pennyfarthing-dist/guides/xml-tags.md +5 -4
  157. package/pennyfarthing-dist/personas/themes/hogans-heroes.yaml +11 -22
  158. package/pennyfarthing-dist/personas/themes/stephen-king.yaml +13 -24
  159. package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
  160. package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
  161. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  162. package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
  163. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  164. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
  165. package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
  166. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +0 -0
  167. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  168. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
  169. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  170. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +0 -0
  171. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  172. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  173. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  174. package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
  175. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +19 -14
  176. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
  177. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
  178. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  179. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
  180. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  181. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  182. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  183. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  184. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  185. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  186. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
  187. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  188. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  189. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  190. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  191. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  192. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  193. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  194. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  195. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  196. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  197. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  198. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  199. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  200. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  201. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  202. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  203. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  204. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  205. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  206. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  207. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  208. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  209. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
  210. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  211. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  212. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  213. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  214. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  215. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  216. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  217. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  218. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
  219. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +191 -57
  220. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +26 -10
  221. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  222. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  223. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  224. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  225. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  226. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  227. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
  228. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  229. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  230. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  231. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  232. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  233. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
  234. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  235. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  236. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  237. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  238. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  239. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
  240. package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
  241. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  242. package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
  243. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
  244. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
  245. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  246. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
  247. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
  248. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
  249. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
  250. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
  251. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
  252. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
  253. package/pennyfarthing-dist/skills/pf-changelog/SKILL.md +4 -4
  254. package/pennyfarthing-dist/skills/pf-sprint/skill.md +1 -1
  255. package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
  256. package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
  257. package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
  258. package/pennyfarthing-dist/skills/pf-systematic-debugging/SKILL.md +0 -1
  259. package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -0
  260. package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -0
  261. package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -0
  262. package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -0
  263. package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -0
  264. package/pennyfarthing-dist/skills/skill-registry.schema.json +4 -0
  265. package/pennyfarthing-dist/skills/skill-registry.yaml +8 -21
  266. package/pennyfarthing-dist/workflows/2party-tdd.yaml +11 -0
  267. package/pennyfarthing-dist/workflows/agent-docs.yaml +2 -0
  268. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +4 -0
  269. package/pennyfarthing-dist/workflows/bdd.yaml +4 -0
  270. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  271. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +3 -0
  272. package/pennyfarthing-dist/workflows/tdd.yaml +3 -0
  273. package/pennyfarthing-dist/workflows/trivial.yaml +2 -0
  274. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  278. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  279. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  280. package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  281. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  282. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  283. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  285. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  286. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  287. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  288. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  289. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  290. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  291. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  292. package/pennyfarthing_scripts/bikerack/changed_panel.py +105 -0
  293. package/pennyfarthing_scripts/bikerack/debug_panel.py +218 -0
  294. package/pennyfarthing_scripts/bikerack/diffs_panel.py +203 -27
  295. package/pennyfarthing_scripts/cli.py +114 -0
  296. package/pennyfarthing_scripts/epic/__init__.py +0 -0
  297. package/pennyfarthing_scripts/epic/cli.py +64 -0
  298. package/pennyfarthing_scripts/gate/__init__.py +1 -0
  299. package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
  300. package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
  301. package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
  302. package/pennyfarthing_scripts/gate/cli.py +56 -0
  303. package/pennyfarthing_scripts/gate/validate.py +266 -0
  304. package/pennyfarthing_scripts/git_group/__init__.py +0 -0
  305. package/pennyfarthing_scripts/git_group/cli.py +100 -0
  306. package/pennyfarthing_scripts/handoff/__init__.py +1 -0
  307. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  308. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  309. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  310. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  311. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  312. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  313. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  314. package/pennyfarthing_scripts/handoff/cli.py +120 -0
  315. package/pennyfarthing_scripts/handoff/complete_phase.py +155 -0
  316. package/pennyfarthing_scripts/handoff/gate_file.py +105 -0
  317. package/pennyfarthing_scripts/handoff/gate_runner.py +152 -0
  318. package/pennyfarthing_scripts/handoff/marker.py +109 -0
  319. package/pennyfarthing_scripts/handoff/resolve_gate.py +152 -0
  320. package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  321. package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  322. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  323. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  324. package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  325. package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
  326. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  327. package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
  328. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  329. package/pennyfarthing_scripts/prime/workflow.py +39 -0
  330. package/pennyfarthing_scripts/session/__init__.py +0 -0
  331. package/pennyfarthing_scripts/session/cli.py +87 -0
  332. package/pennyfarthing_scripts/session_start_hook.py +4 -4
  333. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  334. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  335. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  336. package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  337. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  338. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  339. package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  340. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  341. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  342. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  343. package/pennyfarthing_scripts/sprint/archive_epic.py +8 -0
  344. package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
  345. package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
  346. package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
  347. package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
  348. package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
  349. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  350. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  351. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  352. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  353. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
  354. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
  355. package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
  356. package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
  357. package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
  358. package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
  359. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  360. package/pennyfarthing_scripts/tests/test_108_1_gate_migration.py +540 -0
  361. package/pennyfarthing_scripts/tests/test_108_2_remove_handoff_fallback.py +339 -0
  362. package/pennyfarthing_scripts/tests/test_archive_epic.py +1 -2
  363. package/pennyfarthing_scripts/tests/test_confidence_sm_evaluation.py +253 -0
  364. package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +315 -0
  365. package/pennyfarthing_scripts/tests/test_gate_file_resolution.py +341 -0
  366. package/pennyfarthing_scripts/tests/test_gate_runner.py +620 -0
  367. package/pennyfarthing_scripts/tests/test_handoff_cli.py +929 -0
  368. package/pennyfarthing_scripts/tests/test_handoff_e2e.py +454 -0
  369. package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +464 -0
  370. package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
  371. package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
  372. package/pennyfarthing_scripts/validate/adapters/skill_command.py +200 -0
  373. package/pennyfarthing_scripts/validate/adapters/workflow.py +64 -0
  374. package/pennyfarthing_scripts/validate/cli.py +15 -4
  375. package/packages/core/dist/benchmark/package-exports.test.d.ts.map +0 -1
  376. package/packages/core/dist/benchmark/package-exports.test.js.map +0 -1
  377. package/packages/core/dist/scripts/benchmark-integration.d.ts +0 -182
  378. package/packages/core/dist/scripts/benchmark-integration.d.ts.map +0 -1
  379. package/packages/core/dist/scripts/benchmark-integration.js +0 -691
  380. package/packages/core/dist/scripts/benchmark-integration.js.map +0 -1
  381. package/packages/core/dist/scripts/benchmark-integration.test.d.ts +0 -13
  382. package/packages/core/dist/scripts/benchmark-integration.test.d.ts.map +0 -1
  383. package/packages/core/dist/scripts/benchmark-integration.test.js +0 -680
  384. package/packages/core/dist/scripts/benchmark-integration.test.js.map +0 -1
  385. package/packages/core/dist/scripts/debugging-scenarios.test.d.ts +0 -18
  386. package/packages/core/dist/scripts/debugging-scenarios.test.d.ts.map +0 -1
  387. package/packages/core/dist/scripts/debugging-scenarios.test.js +0 -317
  388. package/packages/core/dist/scripts/debugging-scenarios.test.js.map +0 -1
  389. package/packages/core/dist/scripts/job-fair-aggregator.d.ts +0 -150
  390. package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +0 -1
  391. package/packages/core/dist/scripts/job-fair-aggregator.js +0 -547
  392. package/packages/core/dist/scripts/job-fair-aggregator.js.map +0 -1
  393. package/packages/core/dist/scripts/job-fair-aggregator.test.d.ts +0 -14
  394. package/packages/core/dist/scripts/job-fair-aggregator.test.d.ts.map +0 -1
  395. package/packages/core/dist/scripts/job-fair-aggregator.test.js +0 -616
  396. package/packages/core/dist/scripts/job-fair-aggregator.test.js.map +0 -1
  397. package/pennyfarthing-dist/agents/handoff.md +0 -250
  398. package/pennyfarthing-dist/agents/sm-handoff.md +0 -152
  399. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -112
  400. package/pennyfarthing-dist/skills/pf-dev-patterns/SKILL.md +0 -461
  401. package/scripts/README.md +0 -41
@@ -0,0 +1,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
@@ -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