@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.
- package/README.md +8 -8
- package/package.json +16 -14
- package/packages/core/dist/cli/utils/constants.d.ts +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +2 -1
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.js +334 -0
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
- package/packages/core/dist/server/api/git.d.ts +13 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +53 -34
- package/packages/core/dist/server/api/git.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +185 -24
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
- package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
- package/packages/core/dist/server/otlp-receiver.test.js +446 -0
- package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.js +27 -0
- package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
- package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
- package/packages/core/dist/shared/skill-search.test.js +2 -2
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
- package/pennyfarthing-dist/agents/dev.md +1 -1
- package/pennyfarthing-dist/agents/reviewer.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +1 -1
- package/pennyfarthing-dist/agents/sm.md +2 -2
- package/pennyfarthing-dist/agents/tea.md +1 -1
- package/pennyfarthing-dist/agents/testing-runner.md +2 -1
- package/pennyfarthing-dist/commands/pf-chore.md +2 -2
- package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
- package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +3 -3
- package/pennyfarthing-dist/guides/hooks.md +1 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
- package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
- package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
- package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- package/pennyfarthing-dist/scripts/git/README.md +24 -14
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
- package/pennyfarthing-dist/scripts/git/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
- package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
- package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
- package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
- package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
- package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
- package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
- package/pennyfarthing_scripts/CLAUDE.md +26 -4
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +3 -5
- package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/portrait.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
- package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
- package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
- package/pennyfarthing_scripts/bikerack/cli.py +10 -11
- package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
- package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
- package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
- package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
- package/pennyfarthing_scripts/bikerack/tui.py +336 -30
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/cli.py +37 -65
- package/pennyfarthing_scripts/consultation/__init__.py +1 -0
- package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/cli.py +149 -0
- package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
- package/pennyfarthing_scripts/context.py +3 -3
- package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__init__.py +12 -1
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/hooks_installer.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/worktree.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +3 -4
- package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
- package/pennyfarthing_scripts/git/repos.py +196 -0
- package/pennyfarthing_scripts/git/status_all.py +27 -11
- package/pennyfarthing_scripts/git/worktree.py +302 -0
- package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/cli.py +143 -40
- package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/hooks.py +3 -17
- package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
- package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/heatmap.py +655 -0
- package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session_start_hook.py +1 -1
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/loader.py +15 -1
- package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
- package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
- package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
- package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
- package/pennyfarthing_scripts/validate/cli.py +17 -5
- package/pennyfarthing_scripts/workflow/__init__.py +40 -0
- package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/cli.py +1099 -0
- package/pennyfarthing_scripts/workflow/helpers.py +241 -0
- package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
- package/pennyfarthing_scripts/workflow/state.py +112 -0
- package/scripts/README.md +41 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
- package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
- package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
- package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
- 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")
|