@pennyfarthing/core 11.1.0 → 11.2.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 (263) hide show
  1. package/README.md +8 -8
  2. package/package.json +16 -14
  3. package/packages/core/dist/cli/utils/constants.d.ts +1 -1
  4. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  5. package/packages/core/dist/cli/utils/constants.js +2 -1
  6. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  7. package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
  8. package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
  9. package/packages/core/dist/consultation/dialogue-manager.js +334 -0
  10. package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
  11. package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
  12. package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
  13. package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
  14. package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
  15. package/packages/core/dist/server/api/git.d.ts +13 -1
  16. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  17. package/packages/core/dist/server/api/git.js +53 -34
  18. package/packages/core/dist/server/api/git.js.map +1 -1
  19. package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
  20. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  21. package/packages/core/dist/server/otlp-receiver.js +185 -24
  22. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  23. package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
  24. package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
  25. package/packages/core/dist/server/otlp-receiver.test.js +446 -0
  26. package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
  27. package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
  28. package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
  29. package/packages/core/dist/shared/portrait-resolver.js +27 -0
  30. package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
  31. package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
  32. package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
  33. package/packages/core/dist/shared/skill-search.test.js +2 -2
  34. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
  35. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
  36. package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
  37. package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
  38. package/pennyfarthing-dist/agents/dev.md +1 -1
  39. package/pennyfarthing-dist/agents/reviewer.md +1 -1
  40. package/pennyfarthing-dist/agents/sm-setup.md +1 -1
  41. package/pennyfarthing-dist/agents/sm.md +2 -2
  42. package/pennyfarthing-dist/agents/tea.md +1 -1
  43. package/pennyfarthing-dist/agents/testing-runner.md +2 -1
  44. package/pennyfarthing-dist/commands/pf-chore.md +2 -2
  45. package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
  46. package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
  47. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
  48. package/pennyfarthing-dist/guides/bikerack.md +3 -3
  49. package/pennyfarthing-dist/guides/hooks.md +1 -1
  50. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  51. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  52. package/pennyfarthing-dist/scripts/README.md +1 -1
  53. package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
  54. package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
  55. package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
  56. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
  57. package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
  58. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  59. package/pennyfarthing-dist/scripts/git/README.md +24 -14
  60. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
  61. package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
  62. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
  63. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  64. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
  65. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  66. package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
  67. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
  68. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  69. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  70. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  71. package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
  72. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
  73. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
  74. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
  75. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  76. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
  77. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  78. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  79. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  80. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  81. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  82. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  83. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
  84. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  85. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  86. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  87. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  88. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  89. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  90. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  91. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  92. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  93. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  94. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  95. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  96. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  97. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  98. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  99. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  100. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  101. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  102. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  103. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  104. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  105. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  106. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
  107. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  108. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  109. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  110. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  111. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  112. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  113. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  114. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  115. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
  116. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
  117. package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
  118. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  119. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  120. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  121. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  122. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  123. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  124. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
  125. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  126. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  127. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  128. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  129. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  130. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
  131. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  132. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  133. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  134. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  135. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  136. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
  137. package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
  138. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  139. package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
  140. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
  141. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
  142. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  143. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
  144. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
  145. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
  146. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
  147. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
  148. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
  149. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
  150. package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
  151. package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
  152. package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
  153. package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
  154. package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
  155. package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
  156. package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
  157. package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
  158. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
  159. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
  160. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
  161. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  162. package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
  163. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
  164. package/pennyfarthing_scripts/CLAUDE.md +26 -4
  165. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  167. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/bc/cli.py +3 -5
  171. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  172. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/bikerack/__pycache__/portrait.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  183. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  184. package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
  185. package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
  186. package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
  187. package/pennyfarthing_scripts/bikerack/cli.py +10 -11
  188. package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
  189. package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
  190. package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
  191. package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
  192. package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
  193. package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
  194. package/pennyfarthing_scripts/bikerack/tui.py +336 -30
  195. package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
  196. package/pennyfarthing_scripts/cli.py +37 -65
  197. package/pennyfarthing_scripts/consultation/__init__.py +1 -0
  198. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/consultation/cli.py +149 -0
  201. package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
  202. package/pennyfarthing_scripts/context.py +3 -3
  203. package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/git/__init__.py +12 -1
  206. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/git/__pycache__/hooks_installer.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  211. package/pennyfarthing_scripts/git/__pycache__/worktree.cpython-314.pyc +0 -0
  212. package/pennyfarthing_scripts/git/create_branches.py +3 -4
  213. package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
  214. package/pennyfarthing_scripts/git/repos.py +196 -0
  215. package/pennyfarthing_scripts/git/status_all.py +27 -11
  216. package/pennyfarthing_scripts/git/worktree.py +302 -0
  217. package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/git_group/cli.py +143 -40
  220. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  222. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
  225. package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
  226. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  227. package/pennyfarthing_scripts/hooks.py +3 -17
  228. package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
  229. package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/prime/heatmap.py +655 -0
  232. package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/session_start_hook.py +1 -1
  235. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/sprint/loader.py +15 -1
  237. package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
  238. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  239. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  240. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  241. package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
  242. package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
  243. package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
  244. package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
  245. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
  247. package/pennyfarthing_scripts/validate/cli.py +17 -5
  248. package/pennyfarthing_scripts/workflow/__init__.py +40 -0
  249. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/workflow/cli.py +1099 -0
  255. package/pennyfarthing_scripts/workflow/helpers.py +241 -0
  256. package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
  257. package/pennyfarthing_scripts/workflow/state.py +112 -0
  258. package/scripts/README.md +41 -0
  259. package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
  260. package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
  261. package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
  262. package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
  263. package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -167
@@ -0,0 +1,241 @@
1
+ """
2
+ Shared helpers for workflow commands.
3
+
4
+ Extracted from complete-step.py and bash scripts. Provides common
5
+ functions for session parsing, workflow file resolution, and step management.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from pathlib import Path
12
+
13
+ import yaml
14
+
15
+ from pennyfarthing_scripts.common.config import get_project_root
16
+
17
+
18
+ def get_workflows_dir(project_root: Path | None = None) -> Path:
19
+ """Get the workflows directory path."""
20
+ root = project_root or get_project_root()
21
+ return root / ".pennyfarthing" / "workflows"
22
+
23
+
24
+ def get_session_dir(project_root: Path | None = None) -> Path:
25
+ """Get the session directory path."""
26
+ root = project_root or get_project_root()
27
+ return root / ".session"
28
+
29
+
30
+ def find_workflow_file(workflows_dir: Path, workflow_name: str) -> Path | None:
31
+ """Find workflow YAML definition.
32
+
33
+ Supports both flat (name.yaml) and nested (name/workflow.yaml) layouts.
34
+
35
+ Returns:
36
+ Path to the workflow file, or None if not found.
37
+ """
38
+ flat = workflows_dir / f"{workflow_name}.yaml"
39
+ if flat.exists():
40
+ return flat
41
+
42
+ nested = workflows_dir / workflow_name / "workflow.yaml"
43
+ if nested.exists():
44
+ return nested
45
+
46
+ return None
47
+
48
+
49
+ def load_workflow_data(workflow_file: Path) -> dict:
50
+ """Load and parse workflow YAML file.
51
+
52
+ Returns:
53
+ Parsed YAML data dict.
54
+ """
55
+ with open(workflow_file) as f:
56
+ return yaml.safe_load(f) or {}
57
+
58
+
59
+ def get_workflow_type(workflow_data: dict) -> str:
60
+ """Get workflow type from parsed YAML data.
61
+
62
+ Returns:
63
+ 'phased', 'stepped', or 'procedural'
64
+ """
65
+ wf = workflow_data.get("workflow", {})
66
+ wf_type = wf.get("type", "phased")
67
+ has_steps = wf.get("steps") is not None
68
+ if has_steps or wf_type == "stepped":
69
+ return "stepped"
70
+ return wf_type
71
+
72
+
73
+ def resolve_steps_path(
74
+ workflow_data: dict,
75
+ workflow_dir: Path,
76
+ mode: str | None,
77
+ project_root: Path,
78
+ ) -> Path:
79
+ """Resolve the steps directory path from workflow data.
80
+
81
+ Handles mode-specific paths, relative paths, and absolute paths.
82
+
83
+ Args:
84
+ workflow_data: Parsed workflow YAML
85
+ workflow_dir: Directory containing the workflow.yaml file
86
+ mode: Active mode (create, validate, edit) or None
87
+ project_root: Project root directory
88
+
89
+ Returns:
90
+ Resolved absolute Path to steps directory
91
+ """
92
+ wf = workflow_data.get("workflow", {})
93
+
94
+ # Try mode-specific path first
95
+ if mode:
96
+ modes = wf.get("modes", {})
97
+ mode_path = modes.get(mode)
98
+ if mode_path and mode_path != "null":
99
+ return _resolve_path(mode_path, workflow_dir, project_root)
100
+
101
+ # Try default mode
102
+ default_mode = wf.get("modes", {}).get("default")
103
+ if default_mode:
104
+ modes = wf.get("modes", {})
105
+ mode_path = modes.get(default_mode)
106
+ if mode_path and mode_path != "null":
107
+ return _resolve_path(mode_path, workflow_dir, project_root)
108
+
109
+ # Fall back to steps.path
110
+ steps_path_str = wf.get("steps", {}).get("path", ".")
111
+ return _resolve_path(steps_path_str, workflow_dir, project_root)
112
+
113
+
114
+ def _resolve_path(path_str: str, workflow_dir: Path, project_root: Path) -> Path:
115
+ """Resolve a path string relative to workflow dir or project root."""
116
+ if path_str.startswith("./"):
117
+ return workflow_dir / path_str[2:]
118
+ elif not Path(path_str).is_absolute():
119
+ return project_root / path_str
120
+ else:
121
+ return Path(path_str)
122
+
123
+
124
+ def count_steps(steps_path: Path) -> int:
125
+ """Count step files in a directory."""
126
+ if not steps_path.is_dir():
127
+ return 0
128
+ return len([
129
+ f for f in steps_path.iterdir()
130
+ if f.is_file() and re.match(r"step-\d+", f.name) and f.suffix == ".md"
131
+ ])
132
+
133
+
134
+ def find_step_file(steps_path: Path, step_number: int) -> Path | None:
135
+ """Find step file for a given step number.
136
+
137
+ Handles naming variants: step-01.md, step-01-name.md, step-1-name.md
138
+ """
139
+ padded = f"{step_number:02d}"
140
+ matches = sorted([
141
+ f for f in steps_path.iterdir()
142
+ if f.is_file()
143
+ and (f.name.startswith(f"step-{padded}") or f.name.startswith(f"step-{step_number}-"))
144
+ and f.suffix == ".md"
145
+ ])
146
+ return matches[0] if matches else None
147
+
148
+
149
+ def strip_frontmatter(content: str) -> str:
150
+ """Remove YAML frontmatter from step file content."""
151
+ if not content.startswith("---"):
152
+ return content
153
+ end = content.find("---", 3)
154
+ if end == -1:
155
+ return content
156
+ return content[end + 3:].lstrip("\n")
157
+
158
+
159
+ def parse_session_field(content: str, field: str) -> str:
160
+ """Extract a field value from session markdown.
161
+
162
+ Matches lines like: - **Field:** value
163
+ """
164
+ pattern = rf"^- \*\*{re.escape(field)}:\*\*\s*(.+)$"
165
+ match = re.search(pattern, content, re.MULTILINE)
166
+ return match.group(1).strip() if match else ""
167
+
168
+
169
+ def find_workflow_session(
170
+ session_dir: Path, workflow_name: str | None
171
+ ) -> tuple[Path, str] | None:
172
+ """Find workflow session file and determine workflow name.
173
+
174
+ Args:
175
+ session_dir: Path to .session/ directory
176
+ workflow_name: Explicit workflow name, or None to auto-detect
177
+
178
+ Returns:
179
+ Tuple of (session_path, workflow_name), or None if not found
180
+ """
181
+ if workflow_name:
182
+ session_file = session_dir / f"{workflow_name}-workflow-session.md"
183
+ if session_file.exists():
184
+ return session_file, workflow_name
185
+ return None
186
+
187
+ # Auto-detect from session directory
188
+ sessions = sorted(session_dir.glob("*-workflow-session.md"))
189
+ if not sessions:
190
+ return None
191
+
192
+ session_file = sessions[0]
193
+ content = session_file.read_text()
194
+
195
+ # Try to extract workflow name from content
196
+ wf_match = re.search(r"^\*\*Workflow:\*\*\s*(.+)$", content, re.MULTILINE)
197
+ if wf_match:
198
+ name = wf_match.group(1).strip()
199
+ else:
200
+ name = session_file.stem.replace("-workflow-session", "")
201
+
202
+ return session_file, name
203
+
204
+
205
+ def parse_steps_completed(value: str) -> list[int]:
206
+ """Parse steps completed array from string like '[1, 2, 3]'."""
207
+ if not value or value == "[]":
208
+ return []
209
+ return [int(n) for n in re.findall(r"\d+", value)]
210
+
211
+
212
+ def format_steps_completed(steps: list[int]) -> str:
213
+ """Format steps list as bracket notation."""
214
+ if not steps:
215
+ return "[]"
216
+ return "[" + ", ".join(str(s) for s in steps) + "]"
217
+
218
+
219
+ def find_story_session(session_dir: Path, story_id: str) -> Path | None:
220
+ """Find a story session file by story ID.
221
+
222
+ Handles various naming patterns: 56-1-session.md, MSSCI-12190-session.md
223
+ Also searches file content for matching Jira/ID fields.
224
+ """
225
+ # Try direct filename match
226
+ story_id_lower = story_id.lower()
227
+ for pattern in [f"{story_id}-session.md", f"{story_id_lower}-session.md"]:
228
+ candidate = session_dir / pattern
229
+ if candidate.exists():
230
+ return candidate
231
+
232
+ # Search file contents for matching story ID
233
+ for session_file in session_dir.glob("*-session.md"):
234
+ try:
235
+ content = session_file.read_text()
236
+ if f"Jira:** {story_id}" in content or f"ID:** {story_id}" in content:
237
+ return session_file
238
+ except OSError:
239
+ continue
240
+
241
+ return None
@@ -181,107 +181,3 @@ def get_scale_level_info(level: int) -> dict[str, Any]:
181
181
  return {"level": level, "scope": "unknown", "stories_min": 0,
182
182
  "stories_max": 0, "workflow": "prd", "artifacts": []}
183
183
  return SCALE_LEVELS[level].copy()
184
-
185
-
186
- # Phase ownership mapping for TDD workflow
187
- # Canonical YAML names: setup, red, green, review, finish
188
- TDD_PHASE_OWNERS: dict[str, str] = {
189
- "setup": "sm",
190
- "red": "tea",
191
- "green": "dev",
192
- "review": "reviewer",
193
- "finish": "sm",
194
- }
195
-
196
- # Phase ownership mapping for trivial workflow (no TEA)
197
- # Canonical YAML names: setup, implement, review, finish
198
- TRIVIAL_PHASE_OWNERS: dict[str, str] = {
199
- "setup": "sm",
200
- "implement": "dev",
201
- "review": "reviewer",
202
- "finish": "sm",
203
- }
204
-
205
- # All workflow phase mappings
206
- WORKFLOW_PHASES: dict[str, dict[str, str]] = {
207
- "tdd": TDD_PHASE_OWNERS,
208
- "trivial": TRIVIAL_PHASE_OWNERS,
209
- "bdd": TDD_PHASE_OWNERS, # BDD uses same phases as TDD
210
- }
211
-
212
-
213
- def get_phase_owner(workflow: str, phase: str) -> str:
214
- """Get the agent that owns a workflow phase.
215
-
216
- Args:
217
- workflow: Workflow name (tdd, trivial, bdd)
218
- phase: Phase name (setup, red, implement, review, approved)
219
-
220
- Returns:
221
- Agent name (sm, tea, dev, reviewer)
222
- """
223
- phases = WORKFLOW_PHASES.get(workflow, TDD_PHASE_OWNERS)
224
- return phases.get(phase, "sm")
225
-
226
-
227
- def get_workflow_state() -> dict[str, Any]:
228
- """Get current workflow state from session files.
229
-
230
- Scans .session/ directory for active session files and extracts
231
- workflow state information.
232
-
233
- Returns:
234
- Dict with state, story_id, workflow, phase fields
235
- """
236
- from pathlib import Path
237
-
238
- # Look for session files in .session/
239
- session_dir = Path(".session")
240
- if not session_dir.exists():
241
- return {"state": "EMPTY_BACKLOG_STATE"}
242
-
243
- # Find session files (pattern: *-session.md)
244
- session_files = list(session_dir.glob("*-session.md"))
245
-
246
- # Filter out workflow session files and archived files
247
- story_sessions = [
248
- f for f in session_files
249
- if not f.name.startswith("prd-")
250
- and not f.name.startswith("architecture-")
251
- and not f.name.startswith("research-")
252
- and "workflow" not in f.name.lower()
253
- ]
254
-
255
- if not story_sessions:
256
- return {"state": "NEW_WORK_STATE"}
257
-
258
- # Read the most recent session file
259
- session_file = max(story_sessions, key=lambda f: f.stat().st_mtime)
260
- content = session_file.read_text()
261
-
262
- # Extract fields from markdown format
263
- # Session files use list format: "- **Field:** value"
264
- # Also handle direct format: "**Field:** value"
265
- result: dict[str, Any] = {"state": "IN_PROGRESS_STATE"}
266
-
267
- for line in content.split("\n"):
268
- # Strip leading "- " for list items
269
- stripped = line.lstrip("- ").strip()
270
-
271
- if stripped.startswith("**Story:**"):
272
- result["story_id"] = stripped.replace("**Story:**", "").strip()
273
- elif stripped.startswith("**Jira:**"):
274
- result["story_id"] = stripped.replace("**Jira:**", "").strip()
275
- elif stripped.startswith("**ID:**"):
276
- # Also check **ID:** field (used in Story Details section)
277
- if "story_id" not in result:
278
- result["story_id"] = stripped.replace("**ID:**", "").strip()
279
- elif stripped.startswith("**Type:**"):
280
- # Workflow section uses **Type:** not **Workflow:**
281
- result["workflow"] = stripped.replace("**Type:**", "").strip()
282
- elif stripped.startswith("**Workflow:**"):
283
- result["workflow"] = stripped.replace("**Workflow:**", "").strip()
284
- elif stripped.startswith("**Phase:**"):
285
- result["phase"] = stripped.replace("**Phase:**", "").strip()
286
-
287
- return result
@@ -0,0 +1,112 @@
1
+ """
2
+ Workflow phase ownership and state detection.
3
+
4
+ Maps workflow phases to their owning agents and provides session
5
+ state detection from .session/ files.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+
11
+ # Phase ownership mapping for TDD workflow
12
+ # Canonical YAML names: setup, red, green, review, finish
13
+ TDD_PHASE_OWNERS: dict[str, str] = {
14
+ "setup": "sm",
15
+ "red": "tea",
16
+ "green": "dev",
17
+ "review": "reviewer",
18
+ "finish": "sm",
19
+ }
20
+
21
+ # Phase ownership mapping for trivial workflow (no TEA)
22
+ # Canonical YAML names: setup, implement, review, finish
23
+ TRIVIAL_PHASE_OWNERS: dict[str, str] = {
24
+ "setup": "sm",
25
+ "implement": "dev",
26
+ "review": "reviewer",
27
+ "finish": "sm",
28
+ }
29
+
30
+ # All workflow phase mappings
31
+ WORKFLOW_PHASES: dict[str, dict[str, str]] = {
32
+ "tdd": TDD_PHASE_OWNERS,
33
+ "trivial": TRIVIAL_PHASE_OWNERS,
34
+ "bdd": TDD_PHASE_OWNERS, # BDD uses same phases as TDD
35
+ }
36
+
37
+
38
+ def get_phase_owner(workflow: str, phase: str) -> str:
39
+ """Get the agent that owns a workflow phase.
40
+
41
+ Args:
42
+ workflow: Workflow name (tdd, trivial, bdd)
43
+ phase: Phase name (setup, red, implement, review, approved)
44
+
45
+ Returns:
46
+ Agent name (sm, tea, dev, reviewer)
47
+ """
48
+ phases = WORKFLOW_PHASES.get(workflow, TDD_PHASE_OWNERS)
49
+ return phases.get(phase, "sm")
50
+
51
+
52
+ def get_workflow_state() -> dict[str, Any]:
53
+ """Get current workflow state from session files.
54
+
55
+ Scans .session/ directory for active session files and extracts
56
+ workflow state information.
57
+
58
+ Returns:
59
+ Dict with state, story_id, workflow, phase fields
60
+ """
61
+ from pathlib import Path
62
+
63
+ # Look for session files in .session/
64
+ session_dir = Path(".session")
65
+ if not session_dir.exists():
66
+ return {"state": "EMPTY_BACKLOG_STATE"}
67
+
68
+ # Find session files (pattern: *-session.md)
69
+ session_files = list(session_dir.glob("*-session.md"))
70
+
71
+ # Filter out workflow session files and archived files
72
+ story_sessions = [
73
+ f for f in session_files
74
+ if not f.name.startswith("prd-")
75
+ and not f.name.startswith("architecture-")
76
+ and not f.name.startswith("research-")
77
+ and "workflow" not in f.name.lower()
78
+ ]
79
+
80
+ if not story_sessions:
81
+ return {"state": "NEW_WORK_STATE"}
82
+
83
+ # Read the most recent session file
84
+ session_file = max(story_sessions, key=lambda f: f.stat().st_mtime)
85
+ content = session_file.read_text()
86
+
87
+ # Extract fields from markdown format
88
+ # Session files use list format: "- **Field:** value"
89
+ # Also handle direct format: "**Field:** value"
90
+ result: dict[str, Any] = {"state": "IN_PROGRESS_STATE"}
91
+
92
+ for line in content.split("\n"):
93
+ # Strip leading "- " for list items
94
+ stripped = line.lstrip("- ").strip()
95
+
96
+ if stripped.startswith("**Story:**"):
97
+ result["story_id"] = stripped.replace("**Story:**", "").strip()
98
+ elif stripped.startswith("**Jira:**"):
99
+ result["story_id"] = stripped.replace("**Jira:**", "").strip()
100
+ elif stripped.startswith("**ID:**"):
101
+ # Also check **ID:** field (used in Story Details section)
102
+ if "story_id" not in result:
103
+ result["story_id"] = stripped.replace("**ID:**", "").strip()
104
+ elif stripped.startswith("**Type:**"):
105
+ # Workflow section uses **Type:** not **Workflow:**
106
+ result["workflow"] = stripped.replace("**Type:**", "").strip()
107
+ elif stripped.startswith("**Workflow:**"):
108
+ result["workflow"] = stripped.replace("**Workflow:**", "").strip()
109
+ elif stripped.startswith("**Phase:**"):
110
+ result["phase"] = stripped.replace("**Phase:**", "").strip()
111
+
112
+ return result
@@ -0,0 +1,41 @@
1
+ # Meta Scripts
2
+
3
+ **These scripts are NOT distributed to users.** They are for Pennyfarthing framework development only.
4
+
5
+ ## Contents
6
+
7
+ | Script | Purpose |
8
+ |--------|---------|
9
+ | `deploy.sh` | Release Pennyfarthing (version bump, tag, push, GitHub release) |
10
+ | `cyclist-debug.mjs` | Debug Cyclist connection |
11
+ | `handoff-cli.{sh,js}` | Test handoff flow |
12
+ | `verify-visual-mapping.js` | Verify theme visual mappings |
13
+ | `migrate-assets-to-slug.sh` | One-time migration script |
14
+ | `resize-portraits.sh` | Resize portrait images |
15
+ | `resolve-portrait.mjs` | Portrait resolution logic |
16
+ | `validate-refs.js` | Validate internal references |
17
+
18
+ > Benchmark scripts have been moved to `packages/benchmark/`.
19
+
20
+ ## Usage
21
+
22
+ Run from pennyfarthing repo root:
23
+
24
+ ```bash
25
+ # Release a new version
26
+ ./scripts/deploy.sh --dry-run patch
27
+ ./scripts/deploy.sh patch
28
+ ```
29
+
30
+ ## Where Should My Script Go?
31
+
32
+ **Put it here if:**
33
+ - It's for framework development/CI only
34
+ - Users should NOT have access to it
35
+ - It uses GPU/heavy dependencies (keep in meta, not distributed)
36
+
37
+ **Put it in `pennyfarthing-dist/scripts/` if:**
38
+ - Users need it for their workflows
39
+ - It's part of the sprint/story/jira tooling
40
+
41
+ See `CLAUDE.md` for the full decision tree.
@@ -1,91 +0,0 @@
1
- #!/bin/bash
2
- # List all available workflows with type indicators
3
- # Usage: .pennyfarthing/scripts/workflow/list-workflows.sh
4
- # or: Invoked with PROJECT_ROOT already set
5
- #
6
- # MSSCI-12083: Added type, steps, and modes columns
7
-
8
- set -euo pipefail
9
-
10
- # PROJECT_ROOT should be set by find-root.sh, but find it if not
11
- if [[ -z "${PROJECT_ROOT:-}" ]]; then
12
- d="$PWD"
13
- while [[ ! -d "$d/.claude" ]] && [[ "$d" != "/" ]]; do
14
- d="$(dirname "$d")"
15
- done
16
- PROJECT_ROOT="$d"
17
- fi
18
-
19
- WORKFLOWS_DIR="$PROJECT_ROOT/.pennyfarthing/workflows"
20
-
21
- if [[ ! -d "$WORKFLOWS_DIR" ]]; then
22
- echo "Error: Workflows directory not found at $WORKFLOWS_DIR"
23
- exit 1
24
- fi
25
-
26
- if ! command -v yq &> /dev/null; then
27
- echo "Error: yq is required but not installed"
28
- echo "Install with: brew install yq"
29
- exit 1
30
- fi
31
-
32
- echo "# Available Workflows"
33
- echo ""
34
- echo "| Workflow | Type | Default | Steps/Phases | Modes | Description |"
35
- echo "|----------|------|---------|--------------|-------|-------------|"
36
-
37
- for f in "$WORKFLOWS_DIR"/*.yaml; do
38
- [[ -f "$f" ]] || continue
39
-
40
- name=$(yq eval '.workflow.name' "$f")
41
- desc=$(yq eval '.workflow.description' "$f" | head -1)
42
- is_default=$(yq eval '.workflow.triggers.default // false' "$f")
43
-
44
- # Detect workflow type (stepped vs phased)
45
- # Stepped workflows have .workflow.type == "stepped" or .workflow.steps
46
- workflow_type=$(yq eval '.workflow.type // "phased"' "$f")
47
- has_steps=$(yq eval '.workflow.steps != null' "$f")
48
-
49
- if [[ "$has_steps" == "true" ]] || [[ "$workflow_type" == "stepped" ]]; then
50
- type_col="stepped"
51
- # Count step files if steps.path is defined
52
- steps_path=$(yq eval '.workflow.steps.path // ""' "$f")
53
- steps_pattern=$(yq eval '.workflow.steps.pattern // "step-*.md"' "$f")
54
- if [[ -n "$steps_path" ]] && [[ -d "$PROJECT_ROOT/$steps_path" ]]; then
55
- step_count=$(find "$PROJECT_ROOT/$steps_path" -name "$steps_pattern" 2>/dev/null | wc -l | tr -d ' ')
56
- steps_col="${step_count} steps"
57
- else
58
- steps_col="-"
59
- fi
60
- else
61
- type_col="phased"
62
- # Count phases for phased workflows
63
- phase_count=$(yq eval '.workflow.phases | length' "$f")
64
- steps_col="${phase_count} phases"
65
- fi
66
-
67
- # Default column
68
- if [[ "$is_default" == "true" ]]; then
69
- default_col="yes"
70
- else
71
- default_col="no"
72
- fi
73
-
74
- # Modes column (for tri-modal workflows)
75
- modes=$(yq eval '.workflow.modes.available // []' "$f")
76
- if [[ "$modes" != "[]" ]] && [[ "$modes" != "null" ]]; then
77
- # Format as comma-separated list
78
- modes_col=$(yq eval '.workflow.modes.available | join(",")' "$f")
79
- else
80
- modes_col="-"
81
- fi
82
-
83
- echo "| $name | $type_col | $default_col | $steps_col | $modes_col | $desc |"
84
- done
85
-
86
- echo ""
87
- echo "**Legend:**"
88
- echo "- **phased**: Agent-driven workflow (SM → TEA → Dev → Reviewer)"
89
- echo "- **stepped**: Step-by-step guided workflow with progressive disclosure"
90
- echo ""
91
- echo "Use \`/workflow show <name>\` for workflow details."