@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,1099 @@
1
+ """Workflow CLI — phase management, stepped workflow control, and state queries.
2
+
3
+ Usage:
4
+ pf workflow check [--json]
5
+ pf workflow phase-check WORKFLOW PHASE
6
+ pf workflow handoff NEXT_AGENT
7
+ pf workflow type WORKFLOW
8
+ pf workflow list
9
+ pf workflow show [NAME]
10
+ pf workflow start NAME [--mode MODE]
11
+ pf workflow resume [NAME]
12
+ pf workflow status [NAME]
13
+ pf workflow fix-phase STORY_ID PHASE [--dry-run]
14
+ pf workflow complete-step [NAME] [--step N]
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import click
20
+
21
+
22
+ @click.group()
23
+ def workflow():
24
+ """Workflow state and phase management.
25
+
26
+ \b
27
+ Commands:
28
+ check - Check current workflow state
29
+ phase-check - Verify phase ownership
30
+ handoff - Emit handoff marker
31
+ type - Get workflow type (phased/stepped/procedural)
32
+ list - List all available workflows
33
+ show - Show workflow details
34
+ start - Start a stepped workflow
35
+ resume - Resume an interrupted workflow
36
+ status - Show stepped workflow progress
37
+ fix-phase - Repair session phase tracking
38
+ complete-step - Complete current step in stepped workflow
39
+ """
40
+ pass
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Existing commands (migrated from inline cli.py)
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ @workflow.command("check")
49
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
50
+ def workflow_check(output_json: bool):
51
+ """Check current workflow state.
52
+
53
+ Returns the current story ID, phase, and workflow state.
54
+ """
55
+ from pennyfarthing_scripts.workflow.state import get_workflow_state
56
+
57
+ state = get_workflow_state()
58
+
59
+ if output_json:
60
+ import json
61
+
62
+ click.echo(json.dumps(state, indent=2))
63
+ else:
64
+ click.echo(f"State: {state.get('state', 'unknown')}")
65
+ if state.get("story_id"):
66
+ click.echo(f"Story: {state['story_id']}")
67
+ if state.get("workflow"):
68
+ click.echo(f"Workflow: {state['workflow']}")
69
+ if state.get("phase"):
70
+ click.echo(f"Phase: {state['phase']}")
71
+
72
+
73
+ @workflow.command("phase-check")
74
+ @click.argument("workflow_name")
75
+ @click.argument("phase")
76
+ def workflow_phase_check(workflow_name: str, phase: str):
77
+ """Check which agent owns a workflow phase.
78
+
79
+ \b
80
+ Arguments:
81
+ WORKFLOW_NAME - The workflow type (tdd, trivial, etc.)
82
+ PHASE - The phase to check (red, implement, review, etc.)
83
+ """
84
+ from pennyfarthing_scripts.workflow.state import get_phase_owner
85
+
86
+ owner = get_phase_owner(workflow_name, phase)
87
+ click.echo(owner)
88
+
89
+
90
+ @workflow.command("handoff")
91
+ @click.argument("next_agent")
92
+ def workflow_handoff(next_agent: str):
93
+ """Emit a handoff marker for Cyclist.
94
+
95
+ \b
96
+ Arguments:
97
+ NEXT_AGENT - The agent to hand off to (tea, dev, reviewer, etc.)
98
+ """
99
+ click.echo("---")
100
+ click.echo("AGENT_COMMAND:")
101
+ click.echo(f' marker: "<!-- CYCLIST:HANDOFF:/{next_agent} -->"')
102
+ click.echo(f' fallback: "Run `/{next_agent}` to continue"')
103
+ click.echo("---")
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # New commands (migrated from bash scripts)
108
+ # ---------------------------------------------------------------------------
109
+
110
+
111
+ @workflow.command("type")
112
+ @click.argument("workflow_name")
113
+ def workflow_type_cmd(workflow_name: str):
114
+ """Get workflow type (phased, stepped, or procedural).
115
+
116
+ \b
117
+ Arguments:
118
+ WORKFLOW_NAME - Workflow name (e.g., tdd, architecture)
119
+ """
120
+ from pennyfarthing_scripts.workflow.helpers import (
121
+ find_workflow_file,
122
+ get_workflow_type,
123
+ get_workflows_dir,
124
+ load_workflow_data,
125
+ )
126
+
127
+ workflows_dir = get_workflows_dir()
128
+ wf_file = find_workflow_file(workflows_dir, workflow_name)
129
+ if not wf_file:
130
+ click.echo(f"Error: Workflow '{workflow_name}' not found", err=True)
131
+ raise SystemExit(1)
132
+
133
+ data = load_workflow_data(wf_file)
134
+ click.echo(get_workflow_type(data))
135
+
136
+
137
+ @workflow.command("list")
138
+ def workflow_list_cmd():
139
+ """List all available workflows.
140
+
141
+ Shows a markdown table with type, phases/steps, modes, and descriptions.
142
+ """
143
+ import yaml as yaml_mod
144
+
145
+ from pennyfarthing_scripts.workflow.helpers import (
146
+ count_steps,
147
+ get_workflows_dir,
148
+ load_workflow_data,
149
+ resolve_steps_path,
150
+ )
151
+
152
+ workflows_dir = get_workflows_dir()
153
+
154
+ if not workflows_dir.is_dir():
155
+ click.echo(f"Error: Workflows directory not found at {workflows_dir}", err=True)
156
+ raise SystemExit(1)
157
+
158
+ # Collect workflow files: top-level *.yaml and nested workflow.yaml
159
+ workflow_files = sorted(workflows_dir.glob("*.yaml"))
160
+ for subdir in sorted(workflows_dir.iterdir()):
161
+ if subdir.is_dir():
162
+ nested = subdir / "workflow.yaml"
163
+ if nested.exists():
164
+ workflow_files.append(nested)
165
+
166
+ if not workflow_files:
167
+ click.echo("No workflows found.")
168
+ return
169
+
170
+ click.echo("# Available Workflows")
171
+ click.echo("")
172
+ click.echo("| Workflow | Type | Default | Steps/Phases | Modes | Description |")
173
+ click.echo("|----------|------|---------|--------------|-------|-------------|")
174
+
175
+ from pennyfarthing_scripts.common.config import get_project_root
176
+
177
+ project_root = get_project_root()
178
+
179
+ for wf_file in workflow_files:
180
+ data = load_workflow_data(wf_file)
181
+ wf = data.get("workflow", {})
182
+
183
+ name = wf.get("name", wf_file.stem)
184
+ desc = (wf.get("description") or "-")
185
+ if isinstance(desc, str):
186
+ desc = desc.split("\n")[0][:80]
187
+ is_default = wf.get("triggers", {}).get("default", False)
188
+
189
+ # Detect type
190
+ wf_type_raw = wf.get("type", "phased")
191
+ has_steps = wf.get("steps") is not None
192
+
193
+ if has_steps or wf_type_raw == "stepped":
194
+ type_col = "stepped"
195
+ try:
196
+ steps_path = resolve_steps_path(data, wf_file.parent, None, project_root)
197
+ step_count = count_steps(steps_path)
198
+ steps_col = f"{step_count} steps" if step_count > 0 else "-"
199
+ except Exception:
200
+ steps_col = "-"
201
+ elif wf_type_raw == "procedural":
202
+ type_col = "procedural"
203
+ steps_col = "instructions"
204
+ else:
205
+ type_col = "phased"
206
+ phases = wf.get("phases", [])
207
+ steps_col = f"{len(phases)} phases"
208
+
209
+ default_col = "yes" if is_default else "no"
210
+
211
+ # Modes
212
+ modes_available = wf.get("modes", {}).get("available", [])
213
+ if modes_available:
214
+ modes_col = ",".join(modes_available)
215
+ else:
216
+ modes_col = "-"
217
+
218
+ click.echo(f"| {name} | {type_col} | {default_col} | {steps_col} | {modes_col} | {desc} |")
219
+
220
+ click.echo("")
221
+ click.echo("**Legend:**")
222
+ click.echo("- **phased**: Agent-driven workflow (SM > TEA > Dev > Reviewer)")
223
+ click.echo("- **stepped**: Step-by-step guided workflow with progressive disclosure")
224
+ click.echo("- **procedural**: BMAD reference workflow with instructions file")
225
+ click.echo("")
226
+ click.echo("Use `pf workflow show <name>` for workflow details.")
227
+
228
+
229
+ @workflow.command("show")
230
+ @click.argument("name", required=False, default=None)
231
+ def workflow_show_cmd(name: str | None):
232
+ """Show workflow details including phase flow, triggers, and gates.
233
+
234
+ \b
235
+ Arguments:
236
+ NAME - Workflow name (defaults to current session's workflow or tdd)
237
+ """
238
+ from pennyfarthing_scripts.common.config import get_project_root
239
+ from pennyfarthing_scripts.workflow.helpers import (
240
+ find_workflow_file,
241
+ get_session_dir,
242
+ get_workflows_dir,
243
+ load_workflow_data,
244
+ )
245
+
246
+ project_root = get_project_root()
247
+ workflows_dir = get_workflows_dir(project_root)
248
+ session_dir = get_session_dir(project_root)
249
+
250
+ workflow_name = name
251
+
252
+ if not workflow_name:
253
+ # Try to detect from current session
254
+ if session_dir.is_dir():
255
+ for sf in session_dir.glob("*-session.md"):
256
+ content = sf.read_text()
257
+ import re
258
+
259
+ match = re.search(r"\*\*Workflow:\*\*\s*(\S+)", content)
260
+ if match:
261
+ workflow_name = match.group(1)
262
+ break
263
+
264
+ if not workflow_name:
265
+ click.echo("# Current Workflow")
266
+ click.echo("")
267
+ click.echo("No active session found. Showing default workflow (tdd).")
268
+ click.echo("")
269
+ workflow_name = "tdd"
270
+ else:
271
+ click.echo(f"# Current Session Workflow: {workflow_name}")
272
+ click.echo("")
273
+ else:
274
+ click.echo(f"# Workflow: {workflow_name}")
275
+ click.echo("")
276
+
277
+ wf_file = find_workflow_file(workflows_dir, workflow_name)
278
+ if not wf_file:
279
+ click.echo(f"Error: Workflow '{workflow_name}' not found", err=True)
280
+ click.echo("", err=True)
281
+ click.echo("Available workflows:", err=True)
282
+ for f in sorted(workflows_dir.glob("*.yaml")):
283
+ click.echo(f" {f.stem}", err=True)
284
+ raise SystemExit(1)
285
+
286
+ data = load_workflow_data(wf_file)
287
+ wf = data.get("workflow", {})
288
+
289
+ desc = wf.get("description", "-")
290
+ version = wf.get("version", "-")
291
+
292
+ click.echo(f"**Description:** {desc}")
293
+ click.echo(f"**Version:** {version}")
294
+ click.echo("")
295
+
296
+ # Phase flow diagram
297
+ phases = wf.get("phases", [])
298
+ if phases:
299
+ click.echo("## Phase Flow")
300
+ click.echo("")
301
+ phase_names = [p.get("name", "?") for p in phases]
302
+ click.echo("```")
303
+ click.echo(" -> ".join(phase_names))
304
+ click.echo("```")
305
+ click.echo("")
306
+
307
+ # Phases table
308
+ click.echo("## Phases")
309
+ click.echo("")
310
+ click.echo("| Phase | Agent | Gate |")
311
+ click.echo("|-------|-------|------|")
312
+ for p in phases:
313
+ pname = p.get("name", "?")
314
+ pagent = p.get("agent", "?")
315
+ pgate = p.get("gate", {}).get("type", "none") if isinstance(p.get("gate"), dict) else "none"
316
+ click.echo(f"| {pname} | {pagent} | {pgate} |")
317
+ click.echo("")
318
+
319
+ # Triggers
320
+ triggers = wf.get("triggers", {})
321
+ if triggers:
322
+ click.echo("## Triggers")
323
+ click.echo("")
324
+
325
+ types = triggers.get("types", [])
326
+ if types:
327
+ click.echo(f"**Types:** {', '.join(types)}")
328
+
329
+ points = triggers.get("points", {})
330
+ if points.get("min") is not None:
331
+ click.echo(f"**Points Min:** {points['min']}")
332
+ if points.get("max") is not None:
333
+ click.echo(f"**Points Max:** {points['max']}")
334
+
335
+ if triggers.get("default"):
336
+ click.echo("**Default:** yes (used when no other workflow matches)")
337
+
338
+ tags = triggers.get("tags", [])
339
+ if tags:
340
+ click.echo(f"**Tags:** {', '.join(tags)}")
341
+
342
+
343
+ @workflow.command("start")
344
+ @click.argument("name")
345
+ @click.option("--mode", "-m", default=None, help="Mode: create, validate, or edit")
346
+ def workflow_start_cmd(name: str, mode: str | None):
347
+ """Start a stepped workflow from step 1.
348
+
349
+ Creates a new workflow session and loads the first step.
350
+
351
+ \b
352
+ Arguments:
353
+ NAME - Workflow name (e.g., architecture, release)
354
+ """
355
+ from datetime import datetime, timezone
356
+
357
+ from pennyfarthing_scripts.common.config import get_project_root
358
+ from pennyfarthing_scripts.workflow.helpers import (
359
+ count_steps,
360
+ find_step_file,
361
+ find_workflow_file,
362
+ get_session_dir,
363
+ get_workflows_dir,
364
+ load_workflow_data,
365
+ resolve_steps_path,
366
+ strip_frontmatter,
367
+ )
368
+
369
+ project_root = get_project_root()
370
+ workflows_dir = get_workflows_dir(project_root)
371
+ session_dir = get_session_dir(project_root)
372
+
373
+ # Find workflow file
374
+ wf_file = find_workflow_file(workflows_dir, name)
375
+ if not wf_file:
376
+ click.echo(f"Error: Workflow '{name}' not found", err=True)
377
+ raise SystemExit(1)
378
+
379
+ data = load_workflow_data(wf_file)
380
+ wf = data.get("workflow", {})
381
+
382
+ # Validate it's a stepped workflow
383
+ wf_type = wf.get("type", "phased")
384
+ has_steps = wf.get("steps") is not None
385
+ if wf_type != "stepped" and not has_steps:
386
+ click.echo(f"Error: '{name}' is a phased workflow, not stepped", err=True)
387
+ click.echo("Use TDD workflow commands (/sm, /tea, /dev, /reviewer) for phased workflows", err=True)
388
+ raise SystemExit(1)
389
+
390
+ # Validate mode
391
+ if mode:
392
+ valid_modes = {"create", "validate", "edit"}
393
+ if mode not in valid_modes:
394
+ click.echo(f"Error: Invalid mode '{mode}'. Must be one of: {', '.join(sorted(valid_modes))}", err=True)
395
+ raise SystemExit(1)
396
+
397
+ # Resolve mode
398
+ effective_mode = mode
399
+ if not effective_mode:
400
+ default_mode = wf.get("modes", {}).get("default")
401
+ if default_mode:
402
+ effective_mode = default_mode
403
+ else:
404
+ effective_mode = "create"
405
+
406
+ # If explicit mode, validate it exists for this workflow
407
+ if mode:
408
+ modes = wf.get("modes", {})
409
+ mode_path = modes.get(mode)
410
+ if not mode_path or mode_path == "null":
411
+ available = [k for k in modes if k not in ("default", "available")]
412
+ click.echo(f"Error: Mode '{mode}' not available for workflow '{name}'", err=True)
413
+ if available:
414
+ click.echo(f"Available modes: {', '.join(available)}", err=True)
415
+ raise SystemExit(1)
416
+
417
+ # Resolve steps path
418
+ steps_path = resolve_steps_path(data, wf_file.parent, effective_mode, project_root)
419
+ step_count = count_steps(steps_path)
420
+
421
+ if step_count == 0:
422
+ click.echo(f"Error: No step files found in {steps_path}", err=True)
423
+ raise SystemExit(1)
424
+
425
+ # Create session directory
426
+ session_dir.mkdir(parents=True, exist_ok=True)
427
+
428
+ # Check for existing session
429
+ session_file = session_dir / f"{name}-workflow-session.md"
430
+ if session_file.exists():
431
+ click.echo("**Warning:** Existing session found")
432
+ click.echo("")
433
+ click.echo(f"Session: {session_file}")
434
+ click.echo("")
435
+ click.echo("Options:")
436
+ click.echo(f"1. Use `pf workflow resume {name}` to continue")
437
+ click.echo("2. Delete the session file to start fresh")
438
+ click.echo("")
439
+ click.echo("To start fresh, run:")
440
+ click.echo("```bash")
441
+ click.echo(f'rm "{session_file}"')
442
+ click.echo("```")
443
+ return
444
+
445
+ # Create session file
446
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
447
+ wf_agent = wf.get("agent", "pm")
448
+ wf_desc = wf.get("description", "-")
449
+
450
+ session_content = f"""# Workflow Session: {name}
451
+
452
+ **Workflow:** {name}
453
+ **Type:** stepped
454
+ **Agent:** {wf_agent}
455
+ **Started:** {now}
456
+
457
+ ## Workflow State
458
+ - **Workflow Name:** {name}
459
+ - **Type:** stepped
460
+ - **Mode:** {effective_mode}
461
+ - **Started:** {now}
462
+ - **Last Updated:** {now}
463
+ - **Current Step:** 1
464
+ - **Steps Completed:** []
465
+ - **Status:** in_progress
466
+ - **Notes:** Session created via pf workflow start
467
+
468
+ ## Progress
469
+ - Total Steps: {step_count}
470
+ - Completion: 0%
471
+
472
+ ---
473
+
474
+ """
475
+ session_file.write_text(session_content)
476
+
477
+ # Find step 1
478
+ step_file = find_step_file(steps_path, 1)
479
+ if not step_file:
480
+ click.echo(f"Error: Could not find step 1 file in {steps_path}", err=True)
481
+ raise SystemExit(1)
482
+
483
+ # Output
484
+ click.echo(f"# Starting Workflow: {name}")
485
+ click.echo("")
486
+ click.echo(f"**Description:** {wf_desc}")
487
+ click.echo(f"**Mode:** {effective_mode}")
488
+ click.echo(f"**Steps:** {step_count}")
489
+ click.echo(f"**Agent:** {wf_agent}")
490
+ click.echo(f"**Session:** {session_file}")
491
+ click.echo("")
492
+ click.echo("---")
493
+ click.echo("")
494
+ click.echo(f"## Step 1 of {step_count}")
495
+ click.echo("")
496
+
497
+ step_content = step_file.read_text()
498
+ click.echo(strip_frontmatter(step_content))
499
+
500
+ click.echo("")
501
+ click.echo("---")
502
+ click.echo("")
503
+ click.echo("**Controls:**")
504
+ click.echo("- `C` - Continue to next step")
505
+ click.echo("- `pf workflow status` - Check progress")
506
+ click.echo("- `pf workflow resume` - Resume after break")
507
+
508
+
509
+ @workflow.command("resume")
510
+ @click.argument("name", required=False, default=None)
511
+ def workflow_resume_cmd(name: str | None):
512
+ """Resume a stepped workflow from the current step.
513
+
514
+ \b
515
+ Arguments:
516
+ NAME - Workflow name (auto-detects from active session if omitted)
517
+ """
518
+ from datetime import datetime, timezone
519
+
520
+ from pennyfarthing_scripts.common.config import get_project_root
521
+ from pennyfarthing_scripts.workflow.helpers import (
522
+ count_steps,
523
+ find_step_file,
524
+ find_workflow_file,
525
+ find_workflow_session,
526
+ get_session_dir,
527
+ get_workflows_dir,
528
+ load_workflow_data,
529
+ parse_session_field,
530
+ parse_steps_completed,
531
+ resolve_steps_path,
532
+ strip_frontmatter,
533
+ )
534
+
535
+ project_root = get_project_root()
536
+ workflows_dir = get_workflows_dir(project_root)
537
+ session_dir = get_session_dir(project_root)
538
+
539
+ if not session_dir.is_dir():
540
+ click.echo("# Resume Stepped Workflow")
541
+ click.echo("")
542
+ click.echo("No active workflow session found.")
543
+ click.echo("")
544
+ click.echo("Use `pf workflow start <name>` to begin a new workflow.")
545
+ raise SystemExit(1)
546
+
547
+ result = find_workflow_session(session_dir, name)
548
+ if not result:
549
+ if name:
550
+ click.echo(f"Error: No session found for workflow '{name}'", err=True)
551
+ click.echo(f"\nUse `pf workflow start {name}` to begin.", err=True)
552
+ else:
553
+ click.echo("# Resume Stepped Workflow")
554
+ click.echo("")
555
+ click.echo("No active workflow session found.")
556
+ click.echo("")
557
+ click.echo("Use `pf workflow start <name>` to begin a new workflow.")
558
+ raise SystemExit(1)
559
+
560
+ session_file, workflow_name = result
561
+ content = session_file.read_text()
562
+
563
+ # Parse session state
564
+ current_step_str = parse_session_field(content, "Current Step") or "1"
565
+ current_step = int(current_step_str)
566
+ mode_val = parse_session_field(content, "Mode") or "create"
567
+ status = parse_session_field(content, "Status") or "in_progress"
568
+ steps_completed_str = parse_session_field(content, "Steps Completed") or "[]"
569
+
570
+ # Check if complete
571
+ if status == "completed":
572
+ click.echo(f"# Workflow Complete: {workflow_name}")
573
+ click.echo("")
574
+ click.echo("This workflow has already been completed.")
575
+ click.echo("")
576
+ click.echo("To start a new session, delete the session file:")
577
+ click.echo("```bash")
578
+ click.echo(f'rm "{session_file}"')
579
+ click.echo("```")
580
+ click.echo("")
581
+ click.echo(f"Then run `pf workflow start {workflow_name}`")
582
+ return
583
+
584
+ # Find workflow definition
585
+ wf_file = find_workflow_file(workflows_dir, workflow_name)
586
+ if not wf_file:
587
+ click.echo(f"Error: Workflow definition '{workflow_name}' not found", err=True)
588
+ raise SystemExit(1)
589
+
590
+ data = load_workflow_data(wf_file)
591
+
592
+ # Resolve steps path
593
+ steps_path = resolve_steps_path(data, wf_file.parent, mode_val, project_root)
594
+ step_count = count_steps(steps_path)
595
+
596
+ # Find current step file
597
+ step_file = find_step_file(steps_path, current_step)
598
+ if not step_file:
599
+ click.echo(f"Error: Could not find step {current_step} file in {steps_path}", err=True)
600
+ raise SystemExit(1)
601
+
602
+ # Update last updated timestamp
603
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
604
+ import re
605
+
606
+ updated_content = re.sub(
607
+ r"^- \*\*Last Updated:\*\*.*$",
608
+ f"- **Last Updated:** {now}",
609
+ content, flags=re.MULTILINE
610
+ )
611
+ session_file.write_text(updated_content)
612
+
613
+ # Calculate completion
614
+ steps_completed = parse_steps_completed(steps_completed_str)
615
+ completed_count = len(steps_completed)
616
+ completion_pct = (completed_count * 100 // step_count) if step_count > 0 else 0
617
+
618
+ # Output
619
+ click.echo(f"# Resuming Workflow: {workflow_name}")
620
+ click.echo("")
621
+ click.echo(f"**Mode:** {mode_val}")
622
+ click.echo(f"**Progress:** Step {current_step} of {step_count} ({completion_pct}% complete)")
623
+ click.echo(f"**Steps Completed:** {steps_completed_str}")
624
+ click.echo(f"**Session:** {session_file}")
625
+ click.echo("")
626
+ click.echo("---")
627
+ click.echo("")
628
+ click.echo(f"## Step {current_step} of {step_count}")
629
+ click.echo("")
630
+
631
+ step_content = step_file.read_text()
632
+ click.echo(strip_frontmatter(step_content))
633
+
634
+ click.echo("")
635
+ click.echo("---")
636
+ click.echo("")
637
+ click.echo("**Controls:**")
638
+ click.echo("- `C` - Continue to next step")
639
+ click.echo("- `pf workflow status` - Check progress")
640
+
641
+
642
+ @workflow.command("status")
643
+ @click.argument("name", required=False, default=None)
644
+ def workflow_status_cmd(name: str | None):
645
+ """Show current stepped workflow progress.
646
+
647
+ \b
648
+ Arguments:
649
+ NAME - Workflow name (auto-detects from active session if omitted)
650
+ """
651
+ from pennyfarthing_scripts.common.config import get_project_root
652
+ from pennyfarthing_scripts.workflow.helpers import (
653
+ count_steps,
654
+ find_workflow_file,
655
+ find_workflow_session,
656
+ get_session_dir,
657
+ get_workflows_dir,
658
+ load_workflow_data,
659
+ parse_session_field,
660
+ parse_steps_completed,
661
+ resolve_steps_path,
662
+ )
663
+
664
+ project_root = get_project_root()
665
+ workflows_dir = get_workflows_dir(project_root)
666
+ session_dir = get_session_dir(project_root)
667
+
668
+ if not session_dir.is_dir():
669
+ click.echo("# Workflow Status")
670
+ click.echo("")
671
+ click.echo("No active workflow session found.")
672
+ click.echo("")
673
+ click.echo("Use `pf workflow start <name>` to begin a new workflow.")
674
+ return
675
+
676
+ result = find_workflow_session(session_dir, name)
677
+ if not result:
678
+ if name:
679
+ click.echo(f"# Workflow Status: {name}")
680
+ click.echo("")
681
+ click.echo(f"No session found for workflow '{name}'")
682
+ click.echo("")
683
+ click.echo(f"Use `pf workflow start {name}` to begin.")
684
+ else:
685
+ click.echo("# Workflow Status")
686
+ click.echo("")
687
+ click.echo("No active workflow session found.")
688
+ click.echo("")
689
+ click.echo("Use `pf workflow start <name>` to begin a new workflow.")
690
+ return
691
+
692
+ session_file, workflow_name = result
693
+ content = session_file.read_text()
694
+
695
+ # Parse session state
696
+ current_step_str = parse_session_field(content, "Current Step") or "1"
697
+ current_step = int(current_step_str)
698
+ mode_val = parse_session_field(content, "Mode") or "create"
699
+ status = parse_session_field(content, "Status") or "in_progress"
700
+ started = parse_session_field(content, "Started") or "-"
701
+ last_updated = parse_session_field(content, "Last Updated") or "-"
702
+ steps_completed_str = parse_session_field(content, "Steps Completed") or "[]"
703
+ notes = parse_session_field(content, "Notes") or "-"
704
+
705
+ # Get step count from workflow file
706
+ step_count_str = "?"
707
+ wf_desc = "-"
708
+ wf_file = find_workflow_file(workflows_dir, workflow_name)
709
+ if wf_file:
710
+ data = load_workflow_data(wf_file)
711
+ wf_desc = data.get("workflow", {}).get("description", "-")
712
+ if isinstance(wf_desc, str):
713
+ wf_desc = wf_desc.split("\n")[0]
714
+ try:
715
+ steps_path = resolve_steps_path(data, wf_file.parent, mode_val, project_root)
716
+ step_count = count_steps(steps_path)
717
+ step_count_str = str(step_count)
718
+ except Exception:
719
+ step_count = 0
720
+ else:
721
+ step_count = 0
722
+
723
+ # Calculate completion
724
+ steps_completed = parse_steps_completed(steps_completed_str)
725
+ completed_count = len(steps_completed)
726
+ if step_count > 0:
727
+ completion_pct = completed_count * 100 // step_count
728
+ else:
729
+ completion_pct = 0
730
+
731
+ # Progress bar
732
+ bar_width = 20
733
+ if step_count > 0:
734
+ filled = completion_pct * bar_width // 100
735
+ empty = bar_width - filled
736
+ progress_bar = "#" * filled + "-" * empty
737
+ else:
738
+ progress_bar = "?" * bar_width
739
+
740
+ # Status indicator
741
+ status_icons = {
742
+ "completed": "[COMPLETE]",
743
+ "paused": "[PAUSED]",
744
+ }
745
+ status_icon = status_icons.get(status, "[IN PROGRESS]")
746
+
747
+ # Output
748
+ click.echo(f"# Workflow Status: {workflow_name}")
749
+ click.echo("")
750
+ click.echo(f"**Description:** {wf_desc}")
751
+ click.echo("")
752
+ click.echo("## Progress")
753
+ click.echo("")
754
+ click.echo("```")
755
+ click.echo(f"[{progress_bar}] {completion_pct}%")
756
+ click.echo("```")
757
+ click.echo("")
758
+ click.echo("| Field | Value |")
759
+ click.echo("|-------|-------|")
760
+ click.echo(f"| Status | {status_icon} |")
761
+ click.echo(f"| Mode | {mode_val} |")
762
+ click.echo(f"| Current Step | {current_step} of {step_count_str} |")
763
+ click.echo(f"| Completed | {completed_count} steps |")
764
+ click.echo(f"| Steps Done | {steps_completed_str} |")
765
+ click.echo(f"| Started | {started} |")
766
+ click.echo(f"| Last Updated | {last_updated} |")
767
+ if notes != "-":
768
+ click.echo(f"| Notes | {notes} |")
769
+ click.echo("")
770
+ click.echo(f"**Session:** {session_file}")
771
+ click.echo("")
772
+
773
+ if status != "completed":
774
+ click.echo(f"**Next:** Use `pf workflow resume` to continue from step {current_step}")
775
+
776
+
777
+ @workflow.command("fix-phase")
778
+ @click.argument("story_id")
779
+ @click.argument("target_phase")
780
+ @click.option("--dry-run", is_flag=True, help="Preview without making changes")
781
+ def workflow_fix_phase_cmd(story_id: str, target_phase: str, dry_run: bool):
782
+ """Repair session phase tracking when handoffs didn't update properly.
783
+
784
+ \b
785
+ Arguments:
786
+ STORY_ID - Story ID (e.g., 56-1 or MSSCI-12190)
787
+ TARGET_PHASE - Target phase to set (e.g., review, approved, finish)
788
+ """
789
+ import re
790
+ from datetime import datetime, timezone
791
+
792
+ from pennyfarthing_scripts.common.config import get_project_root
793
+ from pennyfarthing_scripts.workflow.helpers import (
794
+ find_story_session,
795
+ get_session_dir,
796
+ )
797
+
798
+ project_root = get_project_root()
799
+ session_dir = get_session_dir(project_root)
800
+
801
+ if not session_dir.is_dir():
802
+ click.echo(f"Error: Session directory not found at {session_dir}", err=True)
803
+ raise SystemExit(1)
804
+
805
+ session_file = find_story_session(session_dir, story_id)
806
+ if not session_file:
807
+ click.echo(f"Error: Session file not found for story {story_id}", err=True)
808
+ click.echo(f"Searched in: {session_dir}", err=True)
809
+ raise SystemExit(1)
810
+
811
+ click.echo(f"Session file: {session_file}")
812
+
813
+ content = session_file.read_text()
814
+
815
+ # Extract current state
816
+ current_phase_match = re.search(r"\*\*Phase:\*\*\s*(\S+)", content)
817
+ current_phase = current_phase_match.group(1) if current_phase_match else "unknown"
818
+
819
+ workflow_match = re.search(r"\*\*Workflow:\*\*\s*(\S+)", content)
820
+ workflow_name = workflow_match.group(1) if workflow_match else "tdd"
821
+
822
+ click.echo(f"Current phase: {current_phase}")
823
+ click.echo(f"Target phase: {target_phase}")
824
+ click.echo(f"Workflow: {workflow_name}")
825
+
826
+ # Define valid phase sequences
827
+ phase_defs: dict[str, tuple[list[str], list[str], list[str]]] = {
828
+ "tdd": (
829
+ ["setup", "red", "green", "review", "approved", "finish"],
830
+ ["sm", "tea", "dev", "reviewer", "sm", "sm"],
831
+ ["manual", "tests_fail", "tests_pass", "approval", "complete", ""],
832
+ ),
833
+ "trivial": (
834
+ ["setup", "implement", "review", "approved", "finish"],
835
+ ["sm", "dev", "reviewer", "sm", "sm"],
836
+ ["manual", "tests_pass", "approval", "complete", ""],
837
+ ),
838
+ }
839
+
840
+ phases, agents, gates = phase_defs.get(
841
+ workflow_name,
842
+ phase_defs["tdd"], # Default to TDD
843
+ )
844
+
845
+ if workflow_name not in phase_defs:
846
+ click.echo(f"Warning: Unknown workflow '{workflow_name}', assuming TDD")
847
+
848
+ # Find indices
849
+ try:
850
+ current_idx = phases.index(current_phase)
851
+ except ValueError:
852
+ click.echo(f"Error: Current phase '{current_phase}' not found in {workflow_name} workflow", err=True)
853
+ click.echo(f"Valid phases: {', '.join(phases)}", err=True)
854
+ raise SystemExit(1)
855
+
856
+ try:
857
+ target_idx = phases.index(target_phase)
858
+ except ValueError:
859
+ click.echo(f"Error: Target phase '{target_phase}' not found in {workflow_name} workflow", err=True)
860
+ click.echo(f"Valid phases: {', '.join(phases)}", err=True)
861
+ raise SystemExit(1)
862
+
863
+ if target_idx <= current_idx:
864
+ click.echo(f"Error: Target phase '{target_phase}' is not ahead of current phase '{current_phase}'", err=True)
865
+ click.echo(f"Phase sequence: {', '.join(phases)}", err=True)
866
+ raise SystemExit(1)
867
+
868
+ # Calculate transitions
869
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
870
+ click.echo("")
871
+ click.echo("Transitions needed:")
872
+
873
+ transitions: list[tuple[str, str, str, str, str]] = []
874
+ for i in range(current_idx, target_idx):
875
+ from_phase = phases[i]
876
+ to_phase = phases[i + 1]
877
+ from_agent = agents[i]
878
+ to_agent = agents[i + 1]
879
+ gate = gates[i + 1]
880
+ click.echo(f" {from_phase} ({from_agent}) -> {to_phase} ({to_agent}) [gate: {gate}]")
881
+ transitions.append((from_phase, to_phase, from_agent, to_agent, gate))
882
+
883
+ if dry_run:
884
+ click.echo("")
885
+ click.echo("[DRY RUN] Would update session file with:")
886
+ click.echo(f" - **Phase:** {target_phase}")
887
+ click.echo(f" - **Phase Started:** {now}")
888
+ click.echo(f" - Phase History: close out {current_phase}, add intermediate phases")
889
+ click.echo(f" - Handoff History: add {len(transitions)} handoff(s)")
890
+ return
891
+
892
+ click.echo("")
893
+ click.echo("Updating session file...")
894
+
895
+ # Update Phase line
896
+ content = re.sub(
897
+ r"\*\*Phase:\*\*\s*\S+",
898
+ f"**Phase:** {target_phase}",
899
+ content,
900
+ )
901
+
902
+ # Update Phase Started line
903
+ content = re.sub(
904
+ r"\*\*Phase Started:\*\*\s*\S+",
905
+ f"**Phase Started:** {now}",
906
+ content,
907
+ )
908
+
909
+ # Build handoff history additions
910
+ handoff_lines = []
911
+ for from_phase, to_phase, from_agent, to_agent, gate in transitions:
912
+ handoff_lines.append(f"| {from_agent} | {to_agent} | {gate} | PASSED | {now} |")
913
+
914
+ # Insert handoff rows after the last PASSED/FAILED row
915
+ if handoff_lines:
916
+ lines = content.split("\n")
917
+ insert_idx = None
918
+ for i, line in enumerate(lines):
919
+ if ("PASSED" in line or "FAILED" in line) and line.strip().startswith("|"):
920
+ insert_idx = i
921
+
922
+ if insert_idx is not None:
923
+ for j, hl in enumerate(handoff_lines):
924
+ lines.insert(insert_idx + 1 + j, hl)
925
+ content = "\n".join(lines)
926
+
927
+ session_file.write_text(content)
928
+
929
+ click.echo("")
930
+ click.echo("Session file updated")
931
+ click.echo(f" Phase: {current_phase} -> {target_phase}")
932
+ click.echo(f" Handoffs added: {len(transitions)}")
933
+ click.echo("")
934
+ click.echo("Note: Phase History end times set to now. Review and adjust if needed.")
935
+
936
+
937
+ @workflow.command("complete-step")
938
+ @click.argument("name", required=False, default=None)
939
+ @click.option("--step", "step_override", type=int, default=None,
940
+ help="Complete a specific step number instead of current step")
941
+ def workflow_complete_step_cmd(name: str | None, step_override: int | None):
942
+ """Complete the current step of a stepped workflow.
943
+
944
+ Advances session state: increments current step, updates steps completed,
945
+ recalculates completion percentage. Marks workflow as completed when
946
+ all steps are done.
947
+
948
+ \b
949
+ Arguments:
950
+ NAME - Workflow name (auto-detects from session if omitted)
951
+ """
952
+ import re
953
+ from datetime import datetime, timezone
954
+
955
+ from pennyfarthing_scripts.common.config import get_project_root
956
+ from pennyfarthing_scripts.workflow.helpers import (
957
+ count_steps,
958
+ find_step_file,
959
+ find_workflow_file,
960
+ find_workflow_session,
961
+ format_steps_completed,
962
+ get_session_dir,
963
+ get_workflows_dir,
964
+ load_workflow_data,
965
+ parse_session_field,
966
+ parse_steps_completed,
967
+ resolve_steps_path,
968
+ strip_frontmatter,
969
+ )
970
+
971
+ project_root = get_project_root()
972
+ workflows_dir = get_workflows_dir(project_root)
973
+ session_dir = get_session_dir(project_root)
974
+
975
+ if not session_dir.is_dir():
976
+ click.echo("Error: No active workflow session found.", err=True)
977
+ raise SystemExit(1)
978
+
979
+ result = find_workflow_session(session_dir, name)
980
+ if not result:
981
+ if name:
982
+ click.echo(f"Error: No session found for workflow '{name}'", err=True)
983
+ click.echo(f"\nUse `pf workflow start {name}` to begin.", err=True)
984
+ else:
985
+ click.echo("Error: No active workflow session found.", err=True)
986
+ raise SystemExit(1)
987
+
988
+ session_file, workflow_name = result
989
+ content = session_file.read_text()
990
+
991
+ # Parse session state
992
+ current_step_str = parse_session_field(content, "Current Step") or "1"
993
+ current_step = int(current_step_str)
994
+ mode_val = parse_session_field(content, "Mode") or "create"
995
+ status = parse_session_field(content, "Status") or "in_progress"
996
+ steps_completed_str = parse_session_field(content, "Steps Completed") or "[]"
997
+
998
+ # Check if already completed
999
+ if status == "completed":
1000
+ click.echo(f"# Workflow Already Completed: {workflow_name}")
1001
+ click.echo("")
1002
+ click.echo("This workflow has already been completed.")
1003
+ click.echo("")
1004
+ click.echo("To start a new session, delete the session file:")
1005
+ click.echo("```bash")
1006
+ click.echo(f'rm "{session_file}"')
1007
+ click.echo("```")
1008
+ click.echo("")
1009
+ click.echo(f"Then run `pf workflow start {workflow_name}`")
1010
+ return
1011
+
1012
+ # Determine which step to complete
1013
+ completing_step = step_override if step_override is not None else current_step
1014
+
1015
+ # Find workflow file and resolve steps path
1016
+ wf_file = find_workflow_file(workflows_dir, workflow_name)
1017
+ if not wf_file:
1018
+ click.echo(f"Error: Workflow definition '{workflow_name}' not found", err=True)
1019
+ raise SystemExit(1)
1020
+
1021
+ data = load_workflow_data(wf_file)
1022
+ steps_path = resolve_steps_path(data, wf_file.parent, mode_val, project_root)
1023
+ step_count = count_steps(steps_path)
1024
+
1025
+ # Update steps completed
1026
+ steps_completed = parse_steps_completed(steps_completed_str)
1027
+ if completing_step not in steps_completed:
1028
+ steps_completed.append(completing_step)
1029
+ new_steps_completed = format_steps_completed(steps_completed)
1030
+
1031
+ # Calculate
1032
+ next_step = completing_step + 1
1033
+ completed_count = len(steps_completed)
1034
+ completion_pct = (completed_count * 100 // step_count) if step_count > 0 else 0
1035
+ new_status = "completed" if completed_count >= step_count else "in_progress"
1036
+
1037
+ # Update session file
1038
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
1039
+
1040
+ content = re.sub(
1041
+ r"^- \*\*Current Step:\*\*.*$",
1042
+ f"- **Current Step:** {next_step}",
1043
+ content, flags=re.MULTILINE
1044
+ )
1045
+ content = re.sub(
1046
+ r"^- \*\*Steps Completed:\*\*.*$",
1047
+ f"- **Steps Completed:** {new_steps_completed}",
1048
+ content, flags=re.MULTILINE
1049
+ )
1050
+ content = re.sub(
1051
+ r"^- \*\*Last Updated:\*\*.*$",
1052
+ f"- **Last Updated:** {now}",
1053
+ content, flags=re.MULTILINE
1054
+ )
1055
+ content = re.sub(
1056
+ r"^- \*\*Status:\*\*.*$",
1057
+ f"- **Status:** {new_status}",
1058
+ content, flags=re.MULTILINE
1059
+ )
1060
+ content = re.sub(
1061
+ r"^- Completion:.*$",
1062
+ f"- Completion: {completion_pct}%",
1063
+ content, flags=re.MULTILINE
1064
+ )
1065
+
1066
+ session_file.write_text(content)
1067
+
1068
+ # Output
1069
+ if new_status == "completed":
1070
+ click.echo(f"# Workflow Complete: {workflow_name}")
1071
+ click.echo("")
1072
+ click.echo(f"All {step_count} steps completed!")
1073
+ click.echo("")
1074
+ click.echo(f"**Final Progress:** {completion_pct}%")
1075
+ click.echo(f"**Steps Completed:** {new_steps_completed}")
1076
+ click.echo("")
1077
+ click.echo(f"Session updated: {session_file}")
1078
+ else:
1079
+ click.echo(f"# Step {completing_step} Complete")
1080
+ click.echo("")
1081
+ click.echo(f"**Progress:** Step {next_step} of {step_count} ({completion_pct}% complete)")
1082
+ click.echo(f"**Steps Completed:** {new_steps_completed}")
1083
+ click.echo("")
1084
+ click.echo("---")
1085
+ click.echo("")
1086
+ click.echo(f"## Step {next_step} of {step_count}")
1087
+ click.echo("")
1088
+
1089
+ next_step_file = find_step_file(steps_path, next_step)
1090
+ if next_step_file:
1091
+ step_content = next_step_file.read_text()
1092
+ click.echo(strip_frontmatter(step_content))
1093
+
1094
+ click.echo("")
1095
+ click.echo("---")
1096
+ click.echo("")
1097
+ click.echo("**Controls:**")
1098
+ click.echo("- `C` - Continue to next step")
1099
+ click.echo("- `pf workflow status` - Check progress")