@pennyfarthing/core 10.0.2 → 10.0.5
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 +287 -0
- package/package.json +29 -41
- package/{dist → packages/core/dist}/cli/commands/cyclist.d.ts +5 -1
- package/packages/core/dist/cli/commands/cyclist.d.ts.map +1 -0
- package/{dist → packages/core/dist}/cli/commands/cyclist.js +4 -4
- package/packages/core/dist/cli/commands/cyclist.js.map +1 -0
- package/{dist → packages/core/dist}/cli/commands/cyclist.test.js +2 -2
- package/packages/core/dist/cli/commands/cyclist.test.js.map +1 -0
- package/packages/core/dist/cli/commands/doctor-file-layout.test.d.ts +13 -0
- package/packages/core/dist/cli/commands/doctor-file-layout.test.d.ts.map +1 -0
- package/packages/core/dist/cli/commands/doctor-file-layout.test.js +234 -0
- package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -0
- package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.js +17 -16
- package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.js.map +1 -1
- package/{dist → packages/core/dist}/cli/commands/doctor.d.ts +8 -0
- package/{dist → packages/core/dist}/cli/commands/doctor.d.ts.map +1 -1
- package/{dist → packages/core/dist}/cli/commands/doctor.js +224 -3
- package/packages/core/dist/cli/commands/doctor.js.map +1 -0
- package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.js +1 -1
- package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.js.map +1 -1
- package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.js +1 -1
- package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.js.map +1 -1
- package/packages/core/dist/cli/commands/hooks-consolidation.test.d.ts +19 -0
- package/packages/core/dist/cli/commands/hooks-consolidation.test.d.ts.map +1 -0
- package/packages/core/dist/cli/commands/hooks-consolidation.test.js +358 -0
- package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -0
- package/{dist → packages/core/dist}/cli/commands/init.d.ts.map +1 -1
- package/{dist → packages/core/dist}/cli/commands/init.js +3 -0
- package/packages/core/dist/cli/commands/init.js.map +1 -0
- package/{dist → packages/core/dist}/cli/commands/update.d.ts.map +1 -1
- package/{dist → packages/core/dist}/cli/commands/update.js +53 -1
- package/{dist → packages/core/dist}/cli/commands/update.js.map +1 -1
- package/{dist → packages/core/dist}/cli/ocean-profiles.test.js +1 -1
- package/{dist → packages/core/dist}/cli/ocean-profiles.test.js.map +1 -1
- package/{dist → packages/core/dist}/cli/utils/files.d.ts +10 -0
- package/{dist → packages/core/dist}/cli/utils/files.d.ts.map +1 -1
- package/{dist → packages/core/dist}/cli/utils/files.js +35 -0
- package/{dist → packages/core/dist}/cli/utils/files.js.map +1 -1
- package/{dist → packages/core/dist}/cli/utils/settings.d.ts.map +1 -1
- package/{dist → packages/core/dist}/cli/utils/settings.js +24 -0
- package/packages/core/dist/cli/utils/settings.js.map +1 -0
- package/{dist → packages/core/dist}/cli/utils/themes.d.ts +1 -0
- package/packages/core/dist/cli/utils/themes.d.ts.map +1 -0
- package/{dist → packages/core/dist}/cli/utils/themes.js.map +1 -1
- package/{dist → packages/core/dist}/scripts/generate-report.d.ts.map +1 -1
- package/{dist → packages/core/dist}/scripts/generate-report.js +11 -7
- package/packages/core/dist/scripts/generate-report.js.map +1 -0
- package/{dist → packages/core/dist}/scripts/generate-spider-report.d.ts.map +1 -1
- package/{dist → packages/core/dist}/scripts/generate-spider-report.js +12 -8
- package/packages/core/dist/scripts/generate-spider-report.js.map +1 -0
- package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -0
- package/{dist → packages/core/dist}/scripts/generate-spider.js +6 -4
- package/packages/core/dist/scripts/generate-spider.js.map +1 -0
- package/{dist → packages/core/dist}/scripts/generate-spider.test.js +2 -2
- package/packages/core/dist/scripts/generate-spider.test.js.map +1 -0
- package/pennyfarthing-dist/agents/pm.md +1 -1
- package/pennyfarthing-dist/agents/sm-finish.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +6 -6
- package/pennyfarthing-dist/agents/sm.md +12 -6
- package/pennyfarthing-dist/agents/workflow-status-check.md +1 -1
- package/pennyfarthing-dist/commands/repo-status.md +2 -2
- package/pennyfarthing-dist/commands/sprint.md +8 -8
- package/pennyfarthing-dist/guides/bell-mode.md +65 -0
- package/pennyfarthing-dist/guides/benchmarks.md +62 -0
- package/pennyfarthing-dist/guides/bikelane.md +86 -0
- package/pennyfarthing-dist/guides/prime.md +72 -0
- package/pennyfarthing-dist/guides/reflector.md +59 -0
- package/pennyfarthing-dist/guides/relay-mode.md +53 -0
- package/pennyfarthing-dist/guides/skill-schema.md +25 -26
- package/pennyfarthing-dist/guides/tirepump.md +54 -0
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +59 -58
- package/pennyfarthing-dist/personas/themes/blade-runner.yaml +10 -10
- package/pennyfarthing-dist/personas/themes/doctor-who.yaml +10 -10
- package/pennyfarthing-dist/personas/themes/dune.yaml +64 -69
- package/pennyfarthing-dist/personas/themes/firefly.yaml +60 -73
- package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +60 -69
- package/pennyfarthing-dist/personas/themes/harry-potter.yaml +59 -73
- package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +45 -57
- package/pennyfarthing-dist/personas/themes/mad-max.yaml +5 -11
- package/pennyfarthing-dist/personas/themes/princess-bride.yaml +53 -63
- package/pennyfarthing-dist/personas/themes/sandman.yaml +59 -59
- package/pennyfarthing-dist/personas/themes/the-matrix.yaml +61 -62
- package/pennyfarthing-dist/personas/themes/west-wing.yaml +8 -9
- package/pennyfarthing-dist/scripts/README.md +2 -2
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +1 -1
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +3 -3
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +32 -0
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -7
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +12 -91
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +11 -86
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +11 -255
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +3 -3
- package/pennyfarthing-dist/scripts/sprint/README.md +32 -17
- package/pennyfarthing-dist/scripts/story/README.md +1 -1
- package/pennyfarthing-dist/scripts/test/test-setup.sh +1 -1
- package/pennyfarthing-dist/skills/jira/SKILL.md +107 -408
- package/pennyfarthing-dist/skills/skill-registry.yaml +21 -12
- package/pennyfarthing-dist/skills/sprint/skill.md +386 -68
- package/pennyfarthing-dist/skills/story/skill.md +14 -206
- package/pennyfarthing-dist/templates/settings.local.json.template +9 -1
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup.yaml +1 -1
- package/pennyfarthing-dist/workflows/project-setup/steps/step-10-complete.md +1 -1
- package/pennyfarthing_scripts/README.md +66 -0
- package/pennyfarthing_scripts/__init__.py +17 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bellmode_hook.py +154 -0
- package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
- package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/cli.py +131 -0
- package/pennyfarthing_scripts/brownfield/discover.py +753 -0
- package/pennyfarthing_scripts/cli.py +184 -0
- package/pennyfarthing_scripts/common/__init__.py +49 -0
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +92 -0
- package/pennyfarthing_scripts/common/output.py +180 -0
- package/pennyfarthing_scripts/common/themes.py +253 -0
- package/pennyfarthing_scripts/config.py +21 -0
- package/pennyfarthing_scripts/context.py +414 -0
- package/pennyfarthing_scripts/git/__init__.py +29 -0
- 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__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +439 -0
- package/pennyfarthing_scripts/git/status_all.py +310 -0
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +7 -0
- package/pennyfarthing_scripts/hooks.py +454 -0
- package/pennyfarthing_scripts/hotspots/__init__.py +31 -0
- package/pennyfarthing_scripts/hotspots/__main__.py +6 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/analyze.py +472 -0
- package/pennyfarthing_scripts/hotspots/cli.py +152 -0
- package/pennyfarthing_scripts/hotspots/formatters.py +109 -0
- package/pennyfarthing_scripts/hotspots/models.py +60 -0
- package/pennyfarthing_scripts/jira/__init__.py +99 -0
- package/pennyfarthing_scripts/jira/__main__.py +10 -0
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
- package/pennyfarthing_scripts/jira/claim.py +211 -0
- package/pennyfarthing_scripts/jira/cli.py +351 -0
- package/pennyfarthing_scripts/jira/client.py +762 -0
- package/pennyfarthing_scripts/jira/create.py +267 -0
- package/pennyfarthing_scripts/jira/epic.py +176 -0
- package/pennyfarthing_scripts/jira/operations.py +124 -0
- package/pennyfarthing_scripts/jira/reconcile.py +277 -0
- package/pennyfarthing_scripts/jira/story.py +219 -0
- package/pennyfarthing_scripts/jira/sync.py +350 -0
- package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
- package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
- package/pennyfarthing_scripts/jira_sync.py +36 -0
- package/pennyfarthing_scripts/jira_sync_story.py +30 -0
- package/pennyfarthing_scripts/migration/__init__.py +39 -0
- package/pennyfarthing_scripts/migration/__main__.py +10 -0
- package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/cli.py +304 -0
- package/pennyfarthing_scripts/migration/session.py +384 -0
- package/pennyfarthing_scripts/migration/skill.py +188 -0
- package/pennyfarthing_scripts/migration/step.py +229 -0
- package/pennyfarthing_scripts/migration/validate.py +282 -0
- package/pennyfarthing_scripts/output.py +37 -0
- package/pennyfarthing_scripts/patch_mode.py +449 -0
- package/pennyfarthing_scripts/preflight/__init__.py +17 -0
- package/pennyfarthing_scripts/preflight/__main__.py +10 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/cli.py +141 -0
- package/pennyfarthing_scripts/preflight/finish.py +382 -0
- package/pennyfarthing_scripts/pretooluse_hook.py +193 -0
- package/pennyfarthing_scripts/prime/__init__.py +125 -0
- package/pennyfarthing_scripts/prime/__main__.py +8 -0
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +645 -0
- package/pennyfarthing_scripts/prime/loader.py +239 -0
- package/pennyfarthing_scripts/prime/models.py +206 -0
- package/pennyfarthing_scripts/prime/persona.py +309 -0
- package/pennyfarthing_scripts/prime/session.py +183 -0
- package/pennyfarthing_scripts/prime/tiers.py +201 -0
- package/pennyfarthing_scripts/prime/workflow.py +277 -0
- package/pennyfarthing_scripts/schema_validation_hook.py +306 -0
- package/pennyfarthing_scripts/sprint/__init__.py +66 -0
- package/pennyfarthing_scripts/sprint/__main__.py +10 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +165 -0
- package/pennyfarthing_scripts/sprint/archive_epic.py +408 -0
- package/pennyfarthing_scripts/sprint/cli.py +1863 -0
- package/pennyfarthing_scripts/sprint/epic_add.py +173 -0
- package/pennyfarthing_scripts/sprint/import_epic.py +431 -0
- package/pennyfarthing_scripts/sprint/loader.py +237 -0
- package/pennyfarthing_scripts/sprint/status.py +122 -0
- package/pennyfarthing_scripts/sprint/story_add.py +187 -0
- package/pennyfarthing_scripts/sprint/story_update.py +181 -0
- package/pennyfarthing_scripts/sprint/validate_cmd.py +307 -0
- package/pennyfarthing_scripts/sprint/validator.py +580 -0
- package/pennyfarthing_scripts/sprint/work.py +208 -0
- package/pennyfarthing_scripts/sprint/yaml_io.py +367 -0
- package/pennyfarthing_scripts/story/__init__.py +67 -0
- package/pennyfarthing_scripts/story/__main__.py +10 -0
- package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/cli.py +105 -0
- package/pennyfarthing_scripts/story/create.py +167 -0
- package/pennyfarthing_scripts/story/size.py +113 -0
- package/pennyfarthing_scripts/story/template.py +151 -0
- package/pennyfarthing_scripts/swebench.py +216 -0
- package/pennyfarthing_scripts/tests/__init__.py +1 -0
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.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/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/conftest.py +106 -0
- package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
- package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
- package/pennyfarthing_scripts/tests/test_common.py +180 -0
- package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
- package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
- package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
- package/pennyfarthing_scripts/tests/test_patch_mode.py +830 -0
- package/pennyfarthing_scripts/tests/test_prime.py +1050 -0
- package/pennyfarthing_scripts/tests/test_sprint_package.py +402 -0
- package/pennyfarthing_scripts/tests/test_sprint_validator.py +731 -0
- package/pennyfarthing_scripts/tests/test_story_add.py +921 -0
- package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
- package/pennyfarthing_scripts/tests/test_story_update.py +769 -0
- package/pennyfarthing_scripts/tests/test_tiers.py +1090 -0
- package/pennyfarthing_scripts/tests/test_token_counting.py +559 -0
- package/pennyfarthing_scripts/tests/test_validate_cmd.py +500 -0
- package/pennyfarthing_scripts/tests/test_workflow_check.py +341 -0
- package/pennyfarthing_scripts/tests/test_yaml_io.py +815 -0
- package/pennyfarthing_scripts/welcome_hook.py +157 -0
- package/pennyfarthing_scripts/workflow.py +287 -0
- package/scripts/postinstall.cjs +34 -0
- package/dist/cli/commands/cyclist.d.ts.map +0 -1
- package/dist/cli/commands/cyclist.js.map +0 -1
- package/dist/cli/commands/cyclist.test.js.map +0 -1
- package/dist/cli/commands/doctor.js.map +0 -1
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/cli/utils/settings.js.map +0 -1
- package/dist/cli/utils/themes.d.ts.map +0 -1
- package/dist/scripts/generate-report.js.map +0 -1
- package/dist/scripts/generate-spider-report.js.map +0 -1
- package/dist/scripts/generate-spider.d.ts.map +0 -1
- package/dist/scripts/generate-spider.js.map +0 -1
- package/dist/scripts/generate-spider.test.js.map +0 -1
- package/pennyfarthing-dist/scripts/jira/jira-lib.sh +0 -464
- package/pennyfarthing-dist/scripts/jira/jira-sync.sh +0 -16
- package/pennyfarthing-dist/scripts/jira/sync-epic-to-jira.sh +0 -16
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +0 -133
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +0 -91
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +0 -158
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +0 -52
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +0 -63
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +0 -145
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +0 -110
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +0 -148
- package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +0 -415
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +0 -33
- package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +0 -230
- package/pennyfarthing-dist/scripts/sprint/sprint-status.sh +0 -134
- package/pennyfarthing-dist/scripts/sprint/validate-sprint-yaml.sh +0 -139
- package/pennyfarthing-dist/skills/sprint/scripts/archive-story.sh +0 -101
- package/pennyfarthing-dist/skills/sprint/scripts/available-stories.sh +0 -97
- package/pennyfarthing-dist/skills/sprint/scripts/check-story.sh +0 -164
- package/pennyfarthing-dist/skills/sprint/scripts/create-jira-epic.sh +0 -101
- package/pennyfarthing-dist/skills/sprint/scripts/new-sprint.sh +0 -116
- package/pennyfarthing-dist/skills/sprint/scripts/promote-epic.sh +0 -164
- package/pennyfarthing-dist/skills/sprint/scripts/sprint-info.sh +0 -39
- package/pennyfarthing-dist/skills/sprint/scripts/sprint-status.sh +0 -147
- package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +0 -93
- /package/{bin → packages/core/bin}/pennyfarthing.js +0 -0
- /package/{dist → packages/core/dist}/bmad/context-reader.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/context-reader.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/context-reader.js +0 -0
- /package/{dist → packages/core/dist}/bmad/context-reader.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/context-reader.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/context-reader.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/context-reader.test.js +0 -0
- /package/{dist → packages/core/dist}/bmad/context-reader.test.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/epics-parser.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/epics-parser.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/epics-parser.js +0 -0
- /package/{dist → packages/core/dist}/bmad/epics-parser.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/epics-parser.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/epics-parser.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/epics-parser.test.js +0 -0
- /package/{dist → packages/core/dist}/bmad/epics-parser.test.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/index.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/index.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/index.js +0 -0
- /package/{dist → packages/core/dist}/bmad/index.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/status-sync.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/status-sync.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/status-sync.js +0 -0
- /package/{dist → packages/core/dist}/bmad/status-sync.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/status-sync.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/status-sync.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/status-sync.test.js +0 -0
- /package/{dist → packages/core/dist}/bmad/status-sync.test.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/story-exporter.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/story-exporter.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/story-exporter.js +0 -0
- /package/{dist → packages/core/dist}/bmad/story-exporter.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/story-exporter.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/story-exporter.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/story-exporter.test.js +0 -0
- /package/{dist → packages/core/dist}/bmad/story-exporter.test.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/story-parser.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/story-parser.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/story-parser.js +0 -0
- /package/{dist → packages/core/dist}/bmad/story-parser.js.map +0 -0
- /package/{dist → packages/core/dist}/bmad/story-parser.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/bmad/story-parser.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/bmad/story-parser.test.js +0 -0
- /package/{dist → packages/core/dist}/bmad/story-parser.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/command.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/command.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/command.js +0 -0
- /package/{dist → packages/core/dist}/cli/commands/command.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/cyclist.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/cyclist.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/init.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/skill.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/skill.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/skill.js +0 -0
- /package/{dist → packages/core/dist}/cli/commands/skill.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/theme.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/theme.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/theme.js +0 -0
- /package/{dist → packages/core/dist}/cli/commands/theme.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/uninstall.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/uninstall.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/uninstall.js +0 -0
- /package/{dist → packages/core/dist}/cli/commands/uninstall.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/update.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/version.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/commands/version.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/commands/version.js +0 -0
- /package/{dist → packages/core/dist}/cli/commands/version.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/customization.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/customization.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/customization.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/customization.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/cyclist-migration.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/cyclist-migration.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/cyclist-migration.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/cyclist-migration.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/index.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/index.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/index.js +0 -0
- /package/{dist → packages/core/dist}/cli/index.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/ocean-profiles.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/ocean-profiles.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/theme-maker.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/theme-maker.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/theme-maker.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/theme-maker.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/constants.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/constants.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/constants.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/constants.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/logger.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/logger.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/logger.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/logger.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/manifest.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/manifest.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/manifest.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/manifest.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/node-modules.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/node-modules.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/node-modules.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/node-modules.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/prompts.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/prompts.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/prompts.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/prompts.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/settings.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/symlinks.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/symlinks.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/symlinks.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/symlinks.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/themes.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/themes.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/themes.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/themes.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/themes.test.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/version.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/utils/version.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/utils/version.js +0 -0
- /package/{dist → packages/core/dist}/cli/utils/version.js.map +0 -0
- /package/{dist → packages/core/dist}/cli/workspace.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/cli/workspace.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/cli/workspace.test.js +0 -0
- /package/{dist → packages/core/dist}/cli/workspace.test.js.map +0 -0
- /package/{dist → packages/core/dist}/index.d.ts +0 -0
- /package/{dist → packages/core/dist}/index.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/index.js +0 -0
- /package/{dist → packages/core/dist}/index.js.map +0 -0
- /package/{dist → packages/core/dist}/jira/jira-epic-creation.d.ts +0 -0
- /package/{dist → packages/core/dist}/jira/jira-epic-creation.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/jira/jira-epic-creation.js +0 -0
- /package/{dist → packages/core/dist}/jira/jira-epic-creation.js.map +0 -0
- /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.js +0 -0
- /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.js.map +0 -0
- /package/{dist → packages/core/dist}/jira/jira-sprint-sync.d.ts +0 -0
- /package/{dist → packages/core/dist}/jira/jira-sprint-sync.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/jira/jira-sprint-sync.js +0 -0
- /package/{dist → packages/core/dist}/jira/jira-sprint-sync.js.map +0 -0
- /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.js +0 -0
- /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.js.map +0 -0
- /package/{dist → packages/core/dist}/permissions/index.d.ts +0 -0
- /package/{dist → packages/core/dist}/permissions/index.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/permissions/index.js +0 -0
- /package/{dist → packages/core/dist}/permissions/index.js.map +0 -0
- /package/{dist → packages/core/dist}/permissions/permission-schema.d.ts +0 -0
- /package/{dist → packages/core/dist}/permissions/permission-schema.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/permissions/permission-schema.js +0 -0
- /package/{dist → packages/core/dist}/permissions/permission-schema.js.map +0 -0
- /package/{dist → packages/core/dist}/permissions/permission-schema.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/permissions/permission-schema.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/permissions/permission-schema.test.js +0 -0
- /package/{dist → packages/core/dist}/permissions/permission-schema.test.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.js +0 -0
- /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/benchmark-integration.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/benchmark-integration.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/benchmark-integration.js +0 -0
- /package/{dist → packages/core/dist}/scripts/benchmark-integration.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.js +0 -0
- /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.js +0 -0
- /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-all-spiders.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-all-spiders.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-all-spiders.js +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-all-spiders.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-report.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-report.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-report.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-report.test.js +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-report.test.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-spider-report.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.js +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-spider.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-spider.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/generate-spider.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.js +0 -0
- /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.js +0 -0
- /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/run-ci.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/run-ci.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/run-ci.test.js +0 -0
- /package/{dist → packages/core/dist}/scripts/run-ci.test.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/theme-detail.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/theme-detail.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/theme-detail.test.js +0 -0
- /package/{dist → packages/core/dist}/scripts/theme-detail.test.js.map +0 -0
- /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.d.ts +0 -0
- /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.js +0 -0
- /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/gate-handler.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/gate-handler.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/gate-handler.js +0 -0
- /package/{dist → packages/core/dist}/workflow/gate-handler.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/gate-handler.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/gate-handler.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/gate-handler.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/gate-handler.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/generic-sm-finish.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/generic-sm-finish.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/generic-sm-finish.js +0 -0
- /package/{dist → packages/core/dist}/workflow/generic-sm-finish.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/generic-sm-setup.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/generic-sm-setup.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/generic-sm-setup.js +0 -0
- /package/{dist → packages/core/dist}/workflow/generic-sm-setup.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/handoff.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/handoff.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/handoff.js +0 -0
- /package/{dist → packages/core/dist}/workflow/handoff.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/handoff.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/handoff.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/handoff.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/handoff.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/index.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/index.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/index.js +0 -0
- /package/{dist → packages/core/dist}/workflow/index.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/session-state.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/session-state.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/session-state.js +0 -0
- /package/{dist → packages/core/dist}/workflow/session-state.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/session-state.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/session-state.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/session-state.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/session-state.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/sm-subagents.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/sm-subagents.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/sm-subagents.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/sm-subagents.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/step-parser.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/step-parser.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/step-parser.js +0 -0
- /package/{dist → packages/core/dist}/workflow/step-parser.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/step-parser.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/step-parser.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/step-parser.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/step-parser.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/test-cache.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/test-cache.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/test-cache.js +0 -0
- /package/{dist → packages/core/dist}/workflow/test-cache.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/test-cache.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/test-cache.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/test-cache.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/test-cache.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/trimodal.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/trimodal.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/trimodal.js +0 -0
- /package/{dist → packages/core/dist}/workflow/trimodal.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/trimodal.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/trimodal.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/trimodal.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/trimodal.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/variable-resolver.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/variable-resolver.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/variable-resolver.js +0 -0
- /package/{dist → packages/core/dist}/workflow/variable-resolver.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/variable-resolver.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/variable-resolver.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/variable-resolver.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/variable-resolver.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-executor.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-executor.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-executor.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-executor.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-executor.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-executor.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-executor.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-executor.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-loader.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-loader.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-loader.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-loader.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-loader.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-loader.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-loader.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-loader.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-migration.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-migration.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-migration.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-migration.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-permissions.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-permissions.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-permissions.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-permissions.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-router.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-router.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-router.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-router.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-router.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-router.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-router.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-router.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-schema.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-schema.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-schema.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-schema.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-schema.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-schema.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-schema.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-schema.test.js.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.d.ts +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.d.ts.map +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.js +0 -0
- /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.js.map +0 -0
|
@@ -0,0 +1,1863 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint CLI - Click-based CLI for sprint operations.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pf sprint [COMMAND] [ARGS]...
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
status Show sprint status
|
|
9
|
+
backlog Show available stories
|
|
10
|
+
work Start work on a story
|
|
11
|
+
archive Archive a completed story
|
|
12
|
+
story Story subcommands (show, add, update, size, template, finish, claim)
|
|
13
|
+
epic Epic subcommands (show, add, promote, archive, cancel, import, remove)
|
|
14
|
+
initiative Initiative subcommands (show, cancel)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group()
|
|
21
|
+
def sprint():
|
|
22
|
+
"""Sprint status and story operations.
|
|
23
|
+
|
|
24
|
+
\b
|
|
25
|
+
Commands:
|
|
26
|
+
status - Show sprint status
|
|
27
|
+
backlog - Show available stories
|
|
28
|
+
story - Story operations (show, add, update, size, template, finish, claim)
|
|
29
|
+
epic - Epic operations (show, add, promote, archive, cancel, import, remove)
|
|
30
|
+
initiative - Initiative operations (show, cancel)
|
|
31
|
+
work - Start work on a story
|
|
32
|
+
archive - Archive a completed story
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@sprint.command()
|
|
38
|
+
@click.argument("filter", required=False, type=click.Choice(
|
|
39
|
+
["backlog", "todo", "in-progress", "review", "done"],
|
|
40
|
+
case_sensitive=False,
|
|
41
|
+
))
|
|
42
|
+
def status(filter: str | None):
|
|
43
|
+
"""Show sprint status.
|
|
44
|
+
|
|
45
|
+
\b
|
|
46
|
+
Arguments:
|
|
47
|
+
FILTER - Optional status filter (backlog, in-progress, done, etc.)
|
|
48
|
+
"""
|
|
49
|
+
# Lazy import to maintain startup performance
|
|
50
|
+
from pennyfarthing_scripts.sprint.status import format_status, get_sprint_status
|
|
51
|
+
|
|
52
|
+
sprint_status = get_sprint_status(filter)
|
|
53
|
+
click.echo(format_status(sprint_status))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@sprint.command()
|
|
57
|
+
def backlog():
|
|
58
|
+
"""Show available stories grouped by epic.
|
|
59
|
+
|
|
60
|
+
Shows stories with backlog, ready, or planning status.
|
|
61
|
+
Output is grouped by epic with a markdown table per epic.
|
|
62
|
+
"""
|
|
63
|
+
from pennyfarthing_scripts.sprint.loader import load_sprint
|
|
64
|
+
|
|
65
|
+
data = load_sprint()
|
|
66
|
+
if not data or "epics" not in data:
|
|
67
|
+
click.echo("No sprint data available")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
sprint_info = data.get("sprint", {})
|
|
71
|
+
click.echo(f"# Available Stories - {sprint_info.get('name', 'Unknown Sprint')}")
|
|
72
|
+
click.echo("")
|
|
73
|
+
|
|
74
|
+
available_statuses = {"backlog", "ready", "planning"}
|
|
75
|
+
total_count = 0
|
|
76
|
+
total_points = 0
|
|
77
|
+
|
|
78
|
+
for epic in data["epics"]:
|
|
79
|
+
if not isinstance(epic, dict):
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
stories = [
|
|
83
|
+
s for s in epic.get("stories", [])
|
|
84
|
+
if s.get("status") in available_statuses
|
|
85
|
+
]
|
|
86
|
+
if not stories:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
click.echo(f"### {epic.get('title', 'Unknown Epic')}")
|
|
90
|
+
if epic.get("description"):
|
|
91
|
+
desc = epic["description"].strip().split("\n")[0][:200]
|
|
92
|
+
click.echo(f"*{desc}*")
|
|
93
|
+
click.echo("")
|
|
94
|
+
click.echo("| ID | Title | Pts | Pri | Status | Workflow |")
|
|
95
|
+
click.echo("|----|-------|-----|-----|--------|----------|")
|
|
96
|
+
|
|
97
|
+
for s in stories:
|
|
98
|
+
title = s.get("title", "?")
|
|
99
|
+
if len(title) > 40:
|
|
100
|
+
title = title[:37] + "..."
|
|
101
|
+
sid = s.get("id", "?")
|
|
102
|
+
pts = s.get("points", "?")
|
|
103
|
+
pri = s.get("priority", "P2")
|
|
104
|
+
stat = s.get("status", "backlog")
|
|
105
|
+
wf = s.get("workflow", "tdd")
|
|
106
|
+
click.echo(f"| {sid} | {title} | {pts} | {pri} | {stat} | {wf} |")
|
|
107
|
+
total_count += 1
|
|
108
|
+
total_points += s.get("points", 0) or 0
|
|
109
|
+
|
|
110
|
+
click.echo("")
|
|
111
|
+
|
|
112
|
+
click.echo("---")
|
|
113
|
+
click.echo(f"**Total available:** {total_count} stories, {total_points} points")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@sprint.command()
|
|
117
|
+
@click.argument("story_id", required=False)
|
|
118
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
|
|
119
|
+
def work(story_id: str | None, dry_run: bool):
|
|
120
|
+
"""Start work on a story.
|
|
121
|
+
|
|
122
|
+
\b
|
|
123
|
+
Arguments:
|
|
124
|
+
STORY_ID - Story ID to work on, or 'next' for highest priority
|
|
125
|
+
"""
|
|
126
|
+
# Lazy import
|
|
127
|
+
from pennyfarthing_scripts.sprint.loader import get_stories_by_status
|
|
128
|
+
from pennyfarthing_scripts.sprint.work import check_story, get_next_story
|
|
129
|
+
|
|
130
|
+
if not story_id:
|
|
131
|
+
# Show backlog
|
|
132
|
+
stories = get_stories_by_status("backlog")
|
|
133
|
+
click.echo(f"Available stories: {len(stories)}")
|
|
134
|
+
for story in stories[:10]:
|
|
135
|
+
click.echo(f" {story.get('id')}: {story.get('title')} [{story.get('points', '?')}pts]")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if story_id == "next":
|
|
139
|
+
result = get_next_story()
|
|
140
|
+
else:
|
|
141
|
+
result = check_story(story_id)
|
|
142
|
+
|
|
143
|
+
if result.get("available"):
|
|
144
|
+
story = result.get("story", {})
|
|
145
|
+
click.echo(f"Story: {story.get('id')}")
|
|
146
|
+
click.echo(f"Title: {story.get('title')}")
|
|
147
|
+
click.echo(f"Points: {story.get('points')}")
|
|
148
|
+
click.echo(f"Status: Available")
|
|
149
|
+
else:
|
|
150
|
+
error_msg = result.get("error") or result.get("reason")
|
|
151
|
+
raise click.ClickException(f"Not available: {error_msg}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@sprint.command()
|
|
155
|
+
@click.argument("story_id")
|
|
156
|
+
@click.argument("pr_number", required=False)
|
|
157
|
+
@click.option("--apply", is_flag=True, help="Also remove from current-sprint.yaml")
|
|
158
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
|
|
159
|
+
def archive(story_id: str, pr_number: str | None, apply: bool, dry_run: bool):
|
|
160
|
+
"""Archive a completed story.
|
|
161
|
+
|
|
162
|
+
\b
|
|
163
|
+
Arguments:
|
|
164
|
+
STORY_ID - Story ID to archive
|
|
165
|
+
PR_NUMBER - Optional PR number if merged via PR
|
|
166
|
+
"""
|
|
167
|
+
# Lazy import
|
|
168
|
+
from pennyfarthing_scripts.sprint.archive import archive_story
|
|
169
|
+
|
|
170
|
+
result = archive_story(
|
|
171
|
+
story_id,
|
|
172
|
+
pr_number=pr_number,
|
|
173
|
+
dry_run=dry_run,
|
|
174
|
+
apply=apply,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if result.get("success"):
|
|
178
|
+
if result.get("dry_run"):
|
|
179
|
+
click.echo(f"[DRY-RUN] {result.get('message')}")
|
|
180
|
+
else:
|
|
181
|
+
click.echo(result.get("message"))
|
|
182
|
+
else:
|
|
183
|
+
raise click.ClickException(f"Failed: {result.get('error')}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --- Story subgroup ---
|
|
187
|
+
|
|
188
|
+
@sprint.group()
|
|
189
|
+
def story():
|
|
190
|
+
"""Story operations (show, add, update, size, template, finish, claim)."""
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@story.command("show")
|
|
195
|
+
@click.argument("story_id")
|
|
196
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
197
|
+
def story_show(story_id: str, output_json: bool):
|
|
198
|
+
"""Show details for a specific story.
|
|
199
|
+
|
|
200
|
+
\b
|
|
201
|
+
Arguments:
|
|
202
|
+
STORY_ID - Story ID (e.g., MSSCI-12664 or 67-1)
|
|
203
|
+
"""
|
|
204
|
+
# Lazy import
|
|
205
|
+
from pennyfarthing_scripts.sprint.loader import get_story_by_id
|
|
206
|
+
|
|
207
|
+
story_data = get_story_by_id(story_id)
|
|
208
|
+
|
|
209
|
+
if not story_data:
|
|
210
|
+
raise click.ClickException(f"Story not found: {story_id}")
|
|
211
|
+
|
|
212
|
+
if output_json:
|
|
213
|
+
import json
|
|
214
|
+
|
|
215
|
+
click.echo(json.dumps(story_data, indent=2))
|
|
216
|
+
else:
|
|
217
|
+
click.echo(f"Story: {story_data.get('id', story_id)}")
|
|
218
|
+
click.echo(f"Title: {story_data.get('title', 'N/A')}")
|
|
219
|
+
click.echo(f"Points: {story_data.get('points', 'N/A')}")
|
|
220
|
+
click.echo(f"Status: {story_data.get('status', 'N/A')}")
|
|
221
|
+
if story_data.get("priority"):
|
|
222
|
+
click.echo(f"Priority: {story_data.get('priority')}")
|
|
223
|
+
if story_data.get("workflow"):
|
|
224
|
+
click.echo(f"Workflow: {story_data.get('workflow')}")
|
|
225
|
+
if story_data.get("jira"):
|
|
226
|
+
click.echo(f"Jira: {story_data.get('jira')}")
|
|
227
|
+
if story_data.get("description"):
|
|
228
|
+
click.echo(f"Description: {story_data.get('description')}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@story.command("size")
|
|
232
|
+
@click.argument("points", required=False, type=int)
|
|
233
|
+
def story_size(points: int | None):
|
|
234
|
+
"""Display story sizing guidelines.
|
|
235
|
+
|
|
236
|
+
\b
|
|
237
|
+
Arguments:
|
|
238
|
+
POINTS - Optional specific point value to show guidance for
|
|
239
|
+
"""
|
|
240
|
+
from pennyfarthing_scripts.story.size import format_size_info, get_sizing_guidelines
|
|
241
|
+
|
|
242
|
+
guidelines = get_sizing_guidelines(points)
|
|
243
|
+
click.echo(format_size_info(guidelines))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@story.command("template")
|
|
247
|
+
@click.argument("template_type", required=False)
|
|
248
|
+
def story_template(template_type: str | None):
|
|
249
|
+
"""Display story templates by type.
|
|
250
|
+
|
|
251
|
+
\b
|
|
252
|
+
Arguments:
|
|
253
|
+
TYPE - Template type (feature, bug, refactor, chore)
|
|
254
|
+
"""
|
|
255
|
+
from pennyfarthing_scripts.story.template import get_all_templates, get_template
|
|
256
|
+
|
|
257
|
+
if template_type:
|
|
258
|
+
template = get_template(template_type)
|
|
259
|
+
if template:
|
|
260
|
+
click.echo(f"Type: {template['type']}")
|
|
261
|
+
click.echo(f"Description: {template['description']}")
|
|
262
|
+
click.echo("")
|
|
263
|
+
click.echo("Template:")
|
|
264
|
+
click.echo(template["template"])
|
|
265
|
+
else:
|
|
266
|
+
raise click.ClickException(f"Unknown template type: {template_type}")
|
|
267
|
+
else:
|
|
268
|
+
click.echo("Available templates:")
|
|
269
|
+
for name, template in get_all_templates().items():
|
|
270
|
+
click.echo(f" {name}: {template['description']}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@story.command("finish")
|
|
274
|
+
@click.argument("story_id")
|
|
275
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be done without executing")
|
|
276
|
+
def story_finish(story_id: str, dry_run: bool):
|
|
277
|
+
"""Complete a story: archive session, merge PR, transition Jira, update sprint YAML.
|
|
278
|
+
|
|
279
|
+
\b
|
|
280
|
+
Arguments:
|
|
281
|
+
STORY_ID - Story ID (e.g., MSSCI-12052)
|
|
282
|
+
"""
|
|
283
|
+
import subprocess as sp
|
|
284
|
+
|
|
285
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
286
|
+
|
|
287
|
+
script = get_project_root() / ".pennyfarthing" / "scripts" / "workflow" / "finish-story.sh"
|
|
288
|
+
if not script.exists():
|
|
289
|
+
raise click.ClickException(f"Script not found: {script}")
|
|
290
|
+
|
|
291
|
+
cmd = [str(script), story_id]
|
|
292
|
+
if dry_run:
|
|
293
|
+
cmd.append("--dry-run")
|
|
294
|
+
|
|
295
|
+
result = sp.run(cmd, capture_output=True, text=True, cwd=str(get_project_root()))
|
|
296
|
+
if result.stdout:
|
|
297
|
+
click.echo(result.stdout.rstrip())
|
|
298
|
+
if result.returncode != 0:
|
|
299
|
+
error = result.stderr.strip() if result.stderr else "Unknown error"
|
|
300
|
+
raise click.ClickException(error)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@story.command("claim")
|
|
304
|
+
@click.argument("story_id")
|
|
305
|
+
@click.option("--claim/--unclaim", default=True, help="Claim or unclaim the story")
|
|
306
|
+
def story_claim(story_id: str, claim: bool):
|
|
307
|
+
"""Claim or unclaim a story in Jira.
|
|
308
|
+
|
|
309
|
+
\b
|
|
310
|
+
Arguments:
|
|
311
|
+
STORY_ID - Story ID / Jira key to claim
|
|
312
|
+
"""
|
|
313
|
+
from pennyfarthing_scripts.jira.claim import claim_issue, unclaim_issue
|
|
314
|
+
|
|
315
|
+
if claim:
|
|
316
|
+
result = claim_issue(story_id)
|
|
317
|
+
else:
|
|
318
|
+
result = unclaim_issue(story_id)
|
|
319
|
+
|
|
320
|
+
if result.get("success"):
|
|
321
|
+
click.echo(result.get("message", f"{'Claimed' if claim else 'Unclaimed'} {story_id}"))
|
|
322
|
+
else:
|
|
323
|
+
raise click.ClickException(result.get("error", "Unknown error"))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# Register story-add as story.add
|
|
327
|
+
from pennyfarthing_scripts.sprint.story_add import story_add_command
|
|
328
|
+
|
|
329
|
+
story.add_command(story_add_command, "add")
|
|
330
|
+
|
|
331
|
+
# Register story-update as story.update
|
|
332
|
+
from pennyfarthing_scripts.sprint.story_update import story_update_command
|
|
333
|
+
|
|
334
|
+
story.add_command(story_update_command, "update")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# --- Epic subgroup ---
|
|
338
|
+
|
|
339
|
+
@sprint.group()
|
|
340
|
+
def epic():
|
|
341
|
+
"""Epic operations (show, add, promote, archive, cancel, import, remove)."""
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@epic.command("show")
|
|
346
|
+
@click.argument("epic_id")
|
|
347
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
348
|
+
def epic_show(epic_id: str, output_json: bool):
|
|
349
|
+
"""Show details for a specific epic.
|
|
350
|
+
|
|
351
|
+
Searches both the current sprint and future initiative shards.
|
|
352
|
+
|
|
353
|
+
\b
|
|
354
|
+
Arguments:
|
|
355
|
+
EPIC_ID - Epic ID (e.g., epic-42 or MSSCI-14298)
|
|
356
|
+
|
|
357
|
+
\b
|
|
358
|
+
Examples:
|
|
359
|
+
pf sprint epic show MSSCI-14298
|
|
360
|
+
pf sprint epic show epic-42
|
|
361
|
+
pf sprint epic show epic-42 --json
|
|
362
|
+
"""
|
|
363
|
+
import json as json_mod
|
|
364
|
+
|
|
365
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
366
|
+
from pennyfarthing_scripts.sprint.loader import load_sprint
|
|
367
|
+
|
|
368
|
+
root = get_project_root()
|
|
369
|
+
epic_data = None
|
|
370
|
+
source = None
|
|
371
|
+
|
|
372
|
+
# 1. Search current sprint
|
|
373
|
+
sprint_data = load_sprint(root)
|
|
374
|
+
if sprint_data and "epics" in sprint_data:
|
|
375
|
+
for e in sprint_data["epics"]:
|
|
376
|
+
if isinstance(e, dict):
|
|
377
|
+
eid = str(e.get("id", ""))
|
|
378
|
+
ejira = str(e.get("jira", ""))
|
|
379
|
+
if epic_id in (eid, ejira, eid.replace("epic-", ""), f"epic-{epic_id}"):
|
|
380
|
+
epic_data = e
|
|
381
|
+
source = "current sprint"
|
|
382
|
+
break
|
|
383
|
+
|
|
384
|
+
# 2. Search future initiative shards
|
|
385
|
+
if not epic_data:
|
|
386
|
+
epic_data, source = _find_epic_in_initiatives(epic_id, root)
|
|
387
|
+
|
|
388
|
+
if not epic_data:
|
|
389
|
+
raise click.ClickException(f"Epic not found: {epic_id}")
|
|
390
|
+
|
|
391
|
+
if output_json:
|
|
392
|
+
# Convert to plain dict for JSON serialization
|
|
393
|
+
click.echo(json_mod.dumps(dict(epic_data), indent=2, default=str))
|
|
394
|
+
else:
|
|
395
|
+
click.echo(f"Epic: {epic_data.get('id', epic_id)}")
|
|
396
|
+
click.echo(f"Title: {epic_data.get('title', 'N/A')}")
|
|
397
|
+
click.echo(f"Status: {epic_data.get('status', 'N/A')}")
|
|
398
|
+
click.echo(f"Points: {epic_data.get('points', 'N/A')}")
|
|
399
|
+
click.echo(f"Source: {source}")
|
|
400
|
+
if epic_data.get("priority"):
|
|
401
|
+
click.echo(f"Priority: {epic_data.get('priority')}")
|
|
402
|
+
if epic_data.get("jira"):
|
|
403
|
+
click.echo(f"Jira: {epic_data.get('jira')}")
|
|
404
|
+
if epic_data.get("repos"):
|
|
405
|
+
click.echo(f"Repos: {epic_data.get('repos')}")
|
|
406
|
+
if epic_data.get("description"):
|
|
407
|
+
click.echo(f"Description: {epic_data.get('description').rstrip()}")
|
|
408
|
+
|
|
409
|
+
stories = epic_data.get("stories", [])
|
|
410
|
+
if stories:
|
|
411
|
+
click.echo(f"\nStories ({len(stories)}):")
|
|
412
|
+
for s in stories:
|
|
413
|
+
sid = s.get("id", "?")
|
|
414
|
+
stitle = s.get("title", "?")
|
|
415
|
+
spts = s.get("points", "?")
|
|
416
|
+
sstat = s.get("status", "?")
|
|
417
|
+
click.echo(f" {sid}: {stitle} [{spts}pts] ({sstat})")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _epic_shard_path(sprint_dir, ref: str):
|
|
421
|
+
"""Resolve an epic shard file path from a ref string.
|
|
422
|
+
|
|
423
|
+
Handles both 'epic-42' and 'MSSCI-12792' style refs.
|
|
424
|
+
The file naming convention is epic-{ref}.yaml, but refs that
|
|
425
|
+
already start with 'epic-' should not be double-prefixed.
|
|
426
|
+
"""
|
|
427
|
+
if ref.startswith("epic-"):
|
|
428
|
+
return sprint_dir / f"{ref}.yaml"
|
|
429
|
+
return sprint_dir / f"epic-{ref}.yaml"
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _epic_ref_matches(ref: str, epic_id: str) -> bool:
|
|
433
|
+
"""Check if an initiative epic ref matches the requested epic_id."""
|
|
434
|
+
# Normalize both to compare without prefix
|
|
435
|
+
ref_bare = ref.replace("epic-", "") if ref.startswith("epic-") else ref
|
|
436
|
+
id_bare = epic_id.replace("epic-", "") if epic_id.startswith("epic-") else epic_id
|
|
437
|
+
return ref_bare == id_bare or ref == epic_id
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _find_epic_in_initiatives(epic_id: str, root):
|
|
441
|
+
"""Search initiative shard files for an epic by ID.
|
|
442
|
+
|
|
443
|
+
Returns (epic_dict, source_string) or (None, None).
|
|
444
|
+
"""
|
|
445
|
+
import yaml
|
|
446
|
+
|
|
447
|
+
sprint_dir = root / "sprint"
|
|
448
|
+
for init_file in sorted(sprint_dir.glob("initiative-*.yaml")):
|
|
449
|
+
with open(init_file) as f:
|
|
450
|
+
init_data = yaml.safe_load(f.read())
|
|
451
|
+
if not init_data:
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
init_name = init_data.get("name", init_file.stem)
|
|
455
|
+
epics = init_data.get("epics", [])
|
|
456
|
+
for e in epics:
|
|
457
|
+
if isinstance(e, str):
|
|
458
|
+
if _epic_ref_matches(e, epic_id):
|
|
459
|
+
shard = _epic_shard_path(sprint_dir, e)
|
|
460
|
+
if shard.exists():
|
|
461
|
+
with open(shard) as sf:
|
|
462
|
+
epic_data = yaml.safe_load(sf.read())
|
|
463
|
+
if epic_data:
|
|
464
|
+
return epic_data, f"initiative: {init_name}"
|
|
465
|
+
elif isinstance(e, dict):
|
|
466
|
+
eid = str(e.get("id", ""))
|
|
467
|
+
if _epic_ref_matches(eid, epic_id):
|
|
468
|
+
return e, f"initiative: {init_name}"
|
|
469
|
+
|
|
470
|
+
return None, None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@epic.command("cancel")
|
|
474
|
+
@click.argument("epic_id")
|
|
475
|
+
@click.option("--jira", is_flag=True, help="Also cancel the epic in Jira")
|
|
476
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
|
|
477
|
+
def epic_cancel(epic_id: str, jira: bool, dry_run: bool):
|
|
478
|
+
"""Cancel an epic and all its stories.
|
|
479
|
+
|
|
480
|
+
Sets the epic status to 'canceled' and all stories to 'canceled'.
|
|
481
|
+
Searches both the current sprint and future initiative shards.
|
|
482
|
+
|
|
483
|
+
\b
|
|
484
|
+
Arguments:
|
|
485
|
+
EPIC_ID - Epic ID (e.g., epic-42 or MSSCI-14298)
|
|
486
|
+
|
|
487
|
+
\b
|
|
488
|
+
Examples:
|
|
489
|
+
pf sprint epic cancel epic-42 --dry-run
|
|
490
|
+
pf sprint epic cancel epic-42
|
|
491
|
+
pf sprint epic cancel epic-42 --jira
|
|
492
|
+
"""
|
|
493
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
494
|
+
from pennyfarthing_scripts.sprint.loader import load_sprint
|
|
495
|
+
from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
|
|
496
|
+
|
|
497
|
+
root = get_project_root()
|
|
498
|
+
sprint_dir = root / "sprint"
|
|
499
|
+
sprint_path = sprint_dir / "current-sprint.yaml"
|
|
500
|
+
|
|
501
|
+
# 1. Try current sprint
|
|
502
|
+
sprint_data = read_sprint(sprint_path) if sprint_path.exists() else None
|
|
503
|
+
found_in_sprint = False
|
|
504
|
+
if sprint_data and "epics" in sprint_data:
|
|
505
|
+
for e in sprint_data["epics"]:
|
|
506
|
+
if not isinstance(e, dict):
|
|
507
|
+
continue
|
|
508
|
+
eid = str(e.get("id", ""))
|
|
509
|
+
ejira = str(e.get("jira", ""))
|
|
510
|
+
if epic_id in (eid, ejira, eid.replace("epic-", ""), f"epic-{epic_id}"):
|
|
511
|
+
found_in_sprint = True
|
|
512
|
+
jira_key = e.get("jira")
|
|
513
|
+
stories = e.get("stories", [])
|
|
514
|
+
story_count = len(stories)
|
|
515
|
+
|
|
516
|
+
click.echo(f"Epic: {eid}")
|
|
517
|
+
click.echo(f"Title: {e.get('title', 'N/A')}")
|
|
518
|
+
click.echo(f"Stories: {story_count}")
|
|
519
|
+
|
|
520
|
+
if jira_key and not jira:
|
|
521
|
+
click.echo(f"\nWarning: Epic has Jira key {jira_key} -- pass --jira to also cancel in Jira")
|
|
522
|
+
|
|
523
|
+
if dry_run:
|
|
524
|
+
click.echo(f"\n[DRY-RUN] Would cancel {eid} and {story_count} stories")
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
e["status"] = "canceled"
|
|
528
|
+
for s in stories:
|
|
529
|
+
s["status"] = "canceled"
|
|
530
|
+
|
|
531
|
+
write_sprint(sprint_path, sprint_data)
|
|
532
|
+
click.echo(f"\nCanceled {eid} and {story_count} stories in current sprint")
|
|
533
|
+
|
|
534
|
+
if jira and jira_key:
|
|
535
|
+
_transition_jira(jira_key, "Cancelled")
|
|
536
|
+
click.echo(f"Transitioned Jira {jira_key} to Cancelled")
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
# 2. Try initiative shards
|
|
540
|
+
if not found_in_sprint:
|
|
541
|
+
_cancel_epic_in_initiatives(epic_id, root, jira=jira, dry_run=dry_run)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _transition_jira(jira_key: str, status: str) -> bool:
|
|
545
|
+
"""Transition a Jira issue to the given status."""
|
|
546
|
+
import subprocess
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
result = subprocess.run(
|
|
550
|
+
["jira", "issue", "move", jira_key, status],
|
|
551
|
+
capture_output=True,
|
|
552
|
+
text=True,
|
|
553
|
+
timeout=30,
|
|
554
|
+
)
|
|
555
|
+
return result.returncode == 0
|
|
556
|
+
except Exception:
|
|
557
|
+
return False
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _cancel_epic_in_initiatives(epic_id: str, root, *, jira: bool, dry_run: bool):
|
|
561
|
+
"""Find and cancel an epic in initiative shard files."""
|
|
562
|
+
import yaml
|
|
563
|
+
|
|
564
|
+
sprint_dir = root / "sprint"
|
|
565
|
+
|
|
566
|
+
for init_file in sorted(sprint_dir.glob("initiative-*.yaml")):
|
|
567
|
+
with open(init_file) as f:
|
|
568
|
+
raw = f.read()
|
|
569
|
+
init_data = yaml.safe_load(raw)
|
|
570
|
+
if not init_data:
|
|
571
|
+
continue
|
|
572
|
+
|
|
573
|
+
init_name = init_data.get("name", init_file.stem)
|
|
574
|
+
epics = init_data.get("epics", [])
|
|
575
|
+
|
|
576
|
+
for i, e in enumerate(epics):
|
|
577
|
+
matched = False
|
|
578
|
+
epic_dict = None
|
|
579
|
+
|
|
580
|
+
if isinstance(e, str):
|
|
581
|
+
if _epic_ref_matches(e, epic_id):
|
|
582
|
+
shard = _epic_shard_path(sprint_dir, e)
|
|
583
|
+
if shard.exists():
|
|
584
|
+
with open(shard) as sf:
|
|
585
|
+
epic_dict = yaml.safe_load(sf.read())
|
|
586
|
+
matched = True
|
|
587
|
+
elif isinstance(e, dict):
|
|
588
|
+
eid = str(e.get("id", ""))
|
|
589
|
+
if _epic_ref_matches(eid, epic_id):
|
|
590
|
+
epic_dict = e
|
|
591
|
+
matched = True
|
|
592
|
+
|
|
593
|
+
if not matched or not epic_dict:
|
|
594
|
+
continue
|
|
595
|
+
|
|
596
|
+
jira_key = epic_dict.get("jira")
|
|
597
|
+
stories = epic_dict.get("stories", [])
|
|
598
|
+
story_count = len(stories)
|
|
599
|
+
|
|
600
|
+
click.echo(f"Epic: {epic_dict.get('id', epic_id)}")
|
|
601
|
+
click.echo(f"Title: {epic_dict.get('title', 'N/A')}")
|
|
602
|
+
click.echo(f"Initiative: {init_name}")
|
|
603
|
+
click.echo(f"Stories: {story_count}")
|
|
604
|
+
|
|
605
|
+
if jira_key and not jira:
|
|
606
|
+
click.echo(f"\nWarning: Epic has Jira key {jira_key} -- pass --jira to also cancel in Jira")
|
|
607
|
+
|
|
608
|
+
if dry_run:
|
|
609
|
+
click.echo(f"\n[DRY-RUN] Would cancel {epic_dict.get('id', epic_id)} and {story_count} stories")
|
|
610
|
+
return
|
|
611
|
+
|
|
612
|
+
epic_dict["status"] = "canceled"
|
|
613
|
+
for s in stories:
|
|
614
|
+
s["status"] = "canceled"
|
|
615
|
+
|
|
616
|
+
# Write back — either shard file or inline in initiative
|
|
617
|
+
if isinstance(e, str):
|
|
618
|
+
shard = _epic_shard_path(sprint_dir, e)
|
|
619
|
+
with open(shard, "w") as sf:
|
|
620
|
+
yaml.dump(dict(epic_dict), sf, default_flow_style=False, sort_keys=False)
|
|
621
|
+
else:
|
|
622
|
+
with open(init_file, "w") as f:
|
|
623
|
+
yaml.dump(init_data, f, default_flow_style=False, sort_keys=False)
|
|
624
|
+
|
|
625
|
+
click.echo(f"\nCanceled {epic_dict.get('id', epic_id)} and {story_count} stories")
|
|
626
|
+
|
|
627
|
+
if jira and jira_key:
|
|
628
|
+
_transition_jira(jira_key, "Cancelled")
|
|
629
|
+
click.echo(f"Transitioned Jira {jira_key} to Cancelled")
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
raise click.ClickException(f"Epic not found: {epic_id}")
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@epic.command("archive")
|
|
636
|
+
@click.argument("epic_id", required=False)
|
|
637
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
|
|
638
|
+
@click.option("--jira", is_flag=True, help="Also update Jira epic status to Done")
|
|
639
|
+
def epic_archive(epic_id: str | None, dry_run: bool, jira: bool):
|
|
640
|
+
"""Archive completed epics.
|
|
641
|
+
|
|
642
|
+
\b
|
|
643
|
+
Arguments:
|
|
644
|
+
EPIC_ID - Epic ID to archive (omit to scan all completed epics)
|
|
645
|
+
|
|
646
|
+
\b
|
|
647
|
+
Examples:
|
|
648
|
+
pf sprint epic archive # Scan and archive all completed
|
|
649
|
+
pf sprint epic archive --dry-run # Preview what would be archived
|
|
650
|
+
pf sprint epic archive epic-64 # Archive specific epic
|
|
651
|
+
pf sprint epic archive epic-64 --jira # Archive and update Jira
|
|
652
|
+
"""
|
|
653
|
+
# Lazy import
|
|
654
|
+
from pennyfarthing_scripts.sprint.archive_epic import (
|
|
655
|
+
archive_all_completed,
|
|
656
|
+
archive_epic as do_archive_epic,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
if epic_id:
|
|
660
|
+
result = do_archive_epic(epic_id, dry_run=dry_run, update_jira=jira)
|
|
661
|
+
else:
|
|
662
|
+
result = archive_all_completed(dry_run=dry_run, update_jira=jira)
|
|
663
|
+
|
|
664
|
+
if result.get("success"):
|
|
665
|
+
if dry_run:
|
|
666
|
+
click.echo(f"[DRY-RUN] {result.get('message')}")
|
|
667
|
+
if "archived" in result:
|
|
668
|
+
for r in result["archived"]:
|
|
669
|
+
e = r.get("epic", {})
|
|
670
|
+
eid = e.get("id") if e else r.get("epic_id")
|
|
671
|
+
stories = len(e.get("stories", [])) if e else r.get("stories_archived", 0)
|
|
672
|
+
click.echo(f" Would archive: {eid} ({stories} stories)")
|
|
673
|
+
else:
|
|
674
|
+
click.echo(result.get("message"))
|
|
675
|
+
if "archived" in result:
|
|
676
|
+
for r in result["archived"]:
|
|
677
|
+
click.echo(f" ✓ {r.get('epic_id')}: {r.get('stories_archived')} stories")
|
|
678
|
+
if result.get("stories_archived"):
|
|
679
|
+
click.echo(f" ✓ {result.get('epic_id')}: {result.get('stories_archived')} stories")
|
|
680
|
+
else:
|
|
681
|
+
error_msg = result.get("error", "Unknown error")
|
|
682
|
+
if result.get("incomplete_stories"):
|
|
683
|
+
error_msg += f"\n Incomplete: {', '.join(result['incomplete_stories'])}"
|
|
684
|
+
raise click.ClickException(error_msg)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@epic.command("import")
|
|
688
|
+
@click.argument("epics_file")
|
|
689
|
+
@click.argument("initiative_name", required=False)
|
|
690
|
+
@click.option("--marker", default="imported", help="Marker tag for stories (default: imported)")
|
|
691
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
|
|
692
|
+
def epic_import(epics_file: str, initiative_name: str | None, marker: str, dry_run: bool):
|
|
693
|
+
"""Import BMAD epics-and-stories output to future.yaml.
|
|
694
|
+
|
|
695
|
+
\b
|
|
696
|
+
Arguments:
|
|
697
|
+
EPICS_FILE - Path to markdown file from epics-and-stories workflow
|
|
698
|
+
INITIATIVE_NAME - Name for the initiative (optional, extracted from file)
|
|
699
|
+
|
|
700
|
+
\b
|
|
701
|
+
Examples:
|
|
702
|
+
pf sprint epic import docs/planning/my-feature-epics.md
|
|
703
|
+
pf sprint epic import docs/planning/my-feature-epics.md "My Feature" --marker my-feature
|
|
704
|
+
pf sprint epic import docs/planning/my-feature-epics.md --dry-run
|
|
705
|
+
"""
|
|
706
|
+
# Lazy import
|
|
707
|
+
from pennyfarthing_scripts.sprint.import_epic import import_epic as do_import
|
|
708
|
+
|
|
709
|
+
result = do_import(
|
|
710
|
+
epics_file,
|
|
711
|
+
initiative_name=initiative_name,
|
|
712
|
+
marker=marker,
|
|
713
|
+
dry_run=dry_run,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
if result.get("success"):
|
|
717
|
+
if dry_run:
|
|
718
|
+
click.echo(f"[DRY-RUN] {result.get('message')}")
|
|
719
|
+
click.echo(f" Epics: {result.get('epics_count')}")
|
|
720
|
+
click.echo(f" Stories: {result.get('stories_count')}")
|
|
721
|
+
click.echo(f" Points: {result.get('total_points')}")
|
|
722
|
+
click.echo(f" Epic numbers: epic-{result.get('start_epic_num')} to epic-{result.get('next_epic_num') - 1}")
|
|
723
|
+
click.echo("")
|
|
724
|
+
click.echo("YAML Preview:")
|
|
725
|
+
click.echo("-" * 60)
|
|
726
|
+
click.echo(result.get("yaml_preview"))
|
|
727
|
+
click.echo("-" * 60)
|
|
728
|
+
else:
|
|
729
|
+
click.echo(f"✓ {result.get('message')}")
|
|
730
|
+
click.echo(f" Next available epic number: {result.get('next_epic_num')}")
|
|
731
|
+
else:
|
|
732
|
+
raise click.ClickException(result.get("error", "Unknown error"))
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
@epic.command("remove")
|
|
736
|
+
@click.argument("epic_id")
|
|
737
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be removed without making changes")
|
|
738
|
+
def epic_remove(epic_id: str, dry_run: bool):
|
|
739
|
+
"""Remove an epic from future.yaml (for cancelled pre-Jira epics).
|
|
740
|
+
|
|
741
|
+
\b
|
|
742
|
+
Arguments:
|
|
743
|
+
EPIC_ID - Epic ID to remove (e.g., epic-41)
|
|
744
|
+
|
|
745
|
+
\b
|
|
746
|
+
Examples:
|
|
747
|
+
pf sprint epic remove epic-41
|
|
748
|
+
pf sprint epic remove epic-41 --dry-run
|
|
749
|
+
"""
|
|
750
|
+
from pathlib import Path
|
|
751
|
+
|
|
752
|
+
import yaml
|
|
753
|
+
|
|
754
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
755
|
+
|
|
756
|
+
future_path = get_project_root() / "sprint" / "future.yaml"
|
|
757
|
+
if not future_path.exists():
|
|
758
|
+
raise click.ClickException(f"File not found: {future_path}")
|
|
759
|
+
|
|
760
|
+
with open(future_path) as f:
|
|
761
|
+
data = yaml.safe_load(f.read())
|
|
762
|
+
|
|
763
|
+
if not data or "future" not in data or "initiatives" not in data["future"]:
|
|
764
|
+
raise click.ClickException("Invalid future.yaml structure")
|
|
765
|
+
|
|
766
|
+
# Find the epic
|
|
767
|
+
found = False
|
|
768
|
+
for init in data["future"]["initiatives"]:
|
|
769
|
+
epics = init.get("epics", [])
|
|
770
|
+
for e in epics:
|
|
771
|
+
if e.get("id") == epic_id:
|
|
772
|
+
found = True
|
|
773
|
+
story_count = len(e.get("stories", []))
|
|
774
|
+
click.echo(f"Found epic in initiative '{init.get('name', 'unknown')}':")
|
|
775
|
+
click.echo(f" ID: {epic_id}")
|
|
776
|
+
click.echo(f" Title: {e.get('title', 'unknown')}")
|
|
777
|
+
click.echo(f" Points: {e.get('points', '?')}")
|
|
778
|
+
click.echo(f" Stories: {story_count}")
|
|
779
|
+
|
|
780
|
+
if dry_run:
|
|
781
|
+
click.echo(f"\n[DRY-RUN] Would remove {epic_id} from future.yaml")
|
|
782
|
+
return
|
|
783
|
+
|
|
784
|
+
# Remove using yq to preserve comments and formatting
|
|
785
|
+
import subprocess as sp
|
|
786
|
+
|
|
787
|
+
result = sp.run(
|
|
788
|
+
[
|
|
789
|
+
"yq", "eval", "-i",
|
|
790
|
+
f'del(.future.initiatives[].epics[] | select(.id == "{epic_id}"))',
|
|
791
|
+
str(future_path),
|
|
792
|
+
],
|
|
793
|
+
capture_output=True,
|
|
794
|
+
text=True,
|
|
795
|
+
)
|
|
796
|
+
if result.returncode != 0:
|
|
797
|
+
raise click.ClickException(f"yq failed: {result.stderr}")
|
|
798
|
+
|
|
799
|
+
click.echo(f"\n✓ Removed {epic_id} from future.yaml")
|
|
800
|
+
return
|
|
801
|
+
|
|
802
|
+
if not found:
|
|
803
|
+
raise click.ClickException(
|
|
804
|
+
f"Epic {epic_id} not found in future.yaml"
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
@epic.command("promote")
|
|
809
|
+
@click.argument("epic_id")
|
|
810
|
+
def epic_promote(epic_id: str):
|
|
811
|
+
"""Move an epic from future initiatives to current-sprint.yaml.
|
|
812
|
+
|
|
813
|
+
Detects ID collisions and assigns new IDs if needed.
|
|
814
|
+
Automatically removes the epic from its initiative shard after promotion.
|
|
815
|
+
|
|
816
|
+
\b
|
|
817
|
+
Arguments:
|
|
818
|
+
EPIC_ID - Epic ID (e.g., epic-41 or 41)
|
|
819
|
+
|
|
820
|
+
\b
|
|
821
|
+
Examples:
|
|
822
|
+
pf sprint epic promote epic-41
|
|
823
|
+
pf sprint epic promote 41
|
|
824
|
+
"""
|
|
825
|
+
import copy
|
|
826
|
+
|
|
827
|
+
import yaml
|
|
828
|
+
|
|
829
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
830
|
+
|
|
831
|
+
root = get_project_root()
|
|
832
|
+
sprint_dir = root / "sprint"
|
|
833
|
+
sprint_file = sprint_dir / "current-sprint.yaml"
|
|
834
|
+
|
|
835
|
+
if not sprint_file.exists():
|
|
836
|
+
raise click.ClickException(f"Sprint file not found: {sprint_file}")
|
|
837
|
+
|
|
838
|
+
# Find the epic in initiative shards
|
|
839
|
+
epic_data = None
|
|
840
|
+
source_init_file = None
|
|
841
|
+
source_ref = None
|
|
842
|
+
|
|
843
|
+
for init_file in sorted(sprint_dir.glob("initiative-*.yaml")):
|
|
844
|
+
with open(init_file) as f:
|
|
845
|
+
init_data = yaml.safe_load(f.read())
|
|
846
|
+
if not init_data:
|
|
847
|
+
continue
|
|
848
|
+
for e in init_data.get("epics", []):
|
|
849
|
+
edata = _resolve_epic_ref(e, sprint_dir)
|
|
850
|
+
if not edata:
|
|
851
|
+
continue
|
|
852
|
+
eid = str(edata.get("id", ""))
|
|
853
|
+
if _epic_ref_matches(eid, epic_id):
|
|
854
|
+
epic_data = copy.deepcopy(edata)
|
|
855
|
+
source_init_file = init_file
|
|
856
|
+
source_ref = e
|
|
857
|
+
break
|
|
858
|
+
if epic_data:
|
|
859
|
+
break
|
|
860
|
+
|
|
861
|
+
if not epic_data:
|
|
862
|
+
raise click.ClickException(f"Epic {epic_id} not found in future initiatives")
|
|
863
|
+
|
|
864
|
+
# Load current sprint
|
|
865
|
+
with open(sprint_file) as f:
|
|
866
|
+
sprint_data = yaml.safe_load(f.read())
|
|
867
|
+
|
|
868
|
+
if not sprint_data:
|
|
869
|
+
raise click.ClickException(f"Invalid sprint file: {sprint_file}")
|
|
870
|
+
|
|
871
|
+
if "epics" not in sprint_data:
|
|
872
|
+
sprint_data["epics"] = []
|
|
873
|
+
|
|
874
|
+
# Check for ID collision
|
|
875
|
+
original_id = str(epic_data.get("id", epic_id))
|
|
876
|
+
new_epic_id = original_id
|
|
877
|
+
existing_ids = {str(e.get("id", "")) for e in sprint_data["epics"] if isinstance(e, dict)}
|
|
878
|
+
|
|
879
|
+
if new_epic_id in existing_ids:
|
|
880
|
+
max_num = 0
|
|
881
|
+
for eid in existing_ids:
|
|
882
|
+
if eid.startswith("epic-"):
|
|
883
|
+
try:
|
|
884
|
+
max_num = max(max_num, int(eid.replace("epic-", "")))
|
|
885
|
+
except ValueError:
|
|
886
|
+
pass
|
|
887
|
+
new_epic_id = f"epic-{max_num + 1}"
|
|
888
|
+
click.echo(f"Warning: Epic ID {original_id} already exists. Assigning new ID: {new_epic_id}")
|
|
889
|
+
|
|
890
|
+
# Transform epic for current sprint
|
|
891
|
+
old_id_num = original_id.replace("epic-", "")
|
|
892
|
+
new_id_num = new_epic_id.replace("epic-", "")
|
|
893
|
+
|
|
894
|
+
epic_data["id"] = new_epic_id
|
|
895
|
+
epic_data["status"] = "backlog"
|
|
896
|
+
if not epic_data.get("title", "").startswith("Epic:"):
|
|
897
|
+
epic_data["title"] = f"Epic: {epic_data.get('title', 'Unknown')}"
|
|
898
|
+
|
|
899
|
+
for s in epic_data.get("stories", []):
|
|
900
|
+
sid = str(s.get("id", ""))
|
|
901
|
+
if sid.startswith(f"{old_id_num}-"):
|
|
902
|
+
s["id"] = sid.replace(f"{old_id_num}-", f"{new_id_num}-", 1)
|
|
903
|
+
s["status"] = "backlog"
|
|
904
|
+
s.setdefault("repos", "pennyfarthing")
|
|
905
|
+
s.setdefault("workflow", "tdd")
|
|
906
|
+
s.setdefault("priority", "P2")
|
|
907
|
+
s.setdefault("acceptance_criteria", [])
|
|
908
|
+
|
|
909
|
+
story_count = len(epic_data.get("stories", []))
|
|
910
|
+
|
|
911
|
+
click.echo("")
|
|
912
|
+
click.echo("Promoting epic to current sprint:")
|
|
913
|
+
click.echo(f" Original ID: {original_id}")
|
|
914
|
+
if new_epic_id != original_id:
|
|
915
|
+
click.echo(f" New ID: {new_epic_id}")
|
|
916
|
+
click.echo(f" Title: {epic_data.get('title')}")
|
|
917
|
+
click.echo(f" Points: {epic_data.get('points', 0)}")
|
|
918
|
+
click.echo(f" Stories: {story_count}")
|
|
919
|
+
click.echo("")
|
|
920
|
+
|
|
921
|
+
# Append to sprint
|
|
922
|
+
sprint_data["epics"].append(epic_data)
|
|
923
|
+
|
|
924
|
+
from pennyfarthing_scripts.sprint.yaml_io import write_sprint
|
|
925
|
+
write_sprint(sprint_file, sprint_data)
|
|
926
|
+
click.echo(f"Added epic to {sprint_file}")
|
|
927
|
+
|
|
928
|
+
# Remove from initiative shard
|
|
929
|
+
with open(source_init_file) as f:
|
|
930
|
+
init_data = yaml.safe_load(f.read())
|
|
931
|
+
|
|
932
|
+
if isinstance(source_ref, str):
|
|
933
|
+
# String ref — remove from list and delete shard file
|
|
934
|
+
init_data["epics"] = [e for e in init_data.get("epics", []) if e != source_ref]
|
|
935
|
+
shard = _epic_shard_path(sprint_dir, source_ref)
|
|
936
|
+
if shard.exists():
|
|
937
|
+
shard.unlink()
|
|
938
|
+
else:
|
|
939
|
+
# Inline dict — remove matching entry
|
|
940
|
+
init_data["epics"] = [
|
|
941
|
+
e for e in init_data.get("epics", [])
|
|
942
|
+
if not (isinstance(e, dict) and _epic_ref_matches(str(e.get("id", "")), epic_id))
|
|
943
|
+
]
|
|
944
|
+
|
|
945
|
+
remaining_epics = init_data.get("epics", [])
|
|
946
|
+
if remaining_epics:
|
|
947
|
+
# Initiative still has epics — update shard in place
|
|
948
|
+
with open(source_init_file, "w") as f:
|
|
949
|
+
yaml.dump(init_data, f, default_flow_style=False, sort_keys=False)
|
|
950
|
+
click.echo(f"Removed {original_id} from {source_init_file.name}")
|
|
951
|
+
else:
|
|
952
|
+
# Initiative is empty — remove shard and future.yaml reference
|
|
953
|
+
init_name = init_data.get("name", "")
|
|
954
|
+
init_slug = source_init_file.stem.replace("initiative-", "")
|
|
955
|
+
source_init_file.unlink()
|
|
956
|
+
click.echo(f"Removed empty initiative shard: {source_init_file.name}")
|
|
957
|
+
|
|
958
|
+
# Remove from future.yaml
|
|
959
|
+
future_file = sprint_dir / "future.yaml"
|
|
960
|
+
if future_file.exists():
|
|
961
|
+
with open(future_file) as f:
|
|
962
|
+
future_data = yaml.safe_load(f.read()) or {}
|
|
963
|
+
future_inits = future_data.get("future", {}).get("initiatives", [])
|
|
964
|
+
if init_slug in future_inits:
|
|
965
|
+
future_inits.remove(init_slug)
|
|
966
|
+
with open(future_file, "w") as f:
|
|
967
|
+
yaml.dump(future_data, f, default_flow_style=False, sort_keys=False)
|
|
968
|
+
click.echo(f"Removed '{init_slug}' from future.yaml")
|
|
969
|
+
|
|
970
|
+
click.echo("")
|
|
971
|
+
click.echo("Promotion complete!")
|
|
972
|
+
click.echo("")
|
|
973
|
+
click.echo("Next steps:")
|
|
974
|
+
click.echo(f" 1. Review the epic: pf sprint epic show {new_epic_id}")
|
|
975
|
+
click.echo(f" 2. Create Jira epic: pf jira create epic {new_epic_id}")
|
|
976
|
+
click.echo(f" 3. Start work: /sprint work {new_id_num}-1")
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
# Register epic-add as epic.add
|
|
980
|
+
from pennyfarthing_scripts.sprint.epic_add import epic_add_command
|
|
981
|
+
|
|
982
|
+
epic.add_command(epic_add_command, "add")
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# --- Initiative subgroup ---
|
|
986
|
+
|
|
987
|
+
@sprint.group()
|
|
988
|
+
def initiative():
|
|
989
|
+
"""Initiative operations (show, cancel)."""
|
|
990
|
+
pass
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
@initiative.command("show")
|
|
994
|
+
@click.argument("name")
|
|
995
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
996
|
+
def initiative_show(name: str, output_json: bool):
|
|
997
|
+
"""Show details for a specific initiative.
|
|
998
|
+
|
|
999
|
+
\b
|
|
1000
|
+
Arguments:
|
|
1001
|
+
NAME - Initiative slug (e.g., benchmark-reliability, technical-debt)
|
|
1002
|
+
|
|
1003
|
+
\b
|
|
1004
|
+
Examples:
|
|
1005
|
+
pf sprint initiative show benchmark-reliability
|
|
1006
|
+
pf sprint initiative show technical-debt --json
|
|
1007
|
+
"""
|
|
1008
|
+
import json as json_mod
|
|
1009
|
+
|
|
1010
|
+
import yaml
|
|
1011
|
+
|
|
1012
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
1013
|
+
|
|
1014
|
+
root = get_project_root()
|
|
1015
|
+
init_file = root / "sprint" / f"initiative-{name}.yaml"
|
|
1016
|
+
|
|
1017
|
+
if not init_file.exists():
|
|
1018
|
+
raise click.ClickException(f"Initiative not found: {name}\n Expected: {init_file}")
|
|
1019
|
+
|
|
1020
|
+
with open(init_file) as f:
|
|
1021
|
+
init_data = yaml.safe_load(f.read())
|
|
1022
|
+
|
|
1023
|
+
if not init_data:
|
|
1024
|
+
raise click.ClickException(f"Empty initiative file: {init_file}")
|
|
1025
|
+
|
|
1026
|
+
if output_json:
|
|
1027
|
+
click.echo(json_mod.dumps(init_data, indent=2, default=str))
|
|
1028
|
+
return
|
|
1029
|
+
|
|
1030
|
+
click.echo(f"Initiative: {init_data.get('name', name)}")
|
|
1031
|
+
click.echo(f"Status: {init_data.get('status', 'N/A')}")
|
|
1032
|
+
if init_data.get("total_points"):
|
|
1033
|
+
click.echo(f"Total Points: {init_data.get('total_points')}")
|
|
1034
|
+
if init_data.get("blocked_by"):
|
|
1035
|
+
click.echo(f"Blocked By: {init_data.get('blocked_by')}")
|
|
1036
|
+
if init_data.get("description"):
|
|
1037
|
+
click.echo(f"Description: {init_data.get('description').rstrip()}")
|
|
1038
|
+
|
|
1039
|
+
epics = init_data.get("epics", [])
|
|
1040
|
+
if epics:
|
|
1041
|
+
click.echo(f"\nEpics ({len(epics)}):")
|
|
1042
|
+
sprint_dir = root / "sprint"
|
|
1043
|
+
for e in epics:
|
|
1044
|
+
if isinstance(e, str):
|
|
1045
|
+
# String ref — try to load shard for details
|
|
1046
|
+
shard = _epic_shard_path(sprint_dir, e)
|
|
1047
|
+
if shard.exists():
|
|
1048
|
+
with open(shard) as sf:
|
|
1049
|
+
edata = yaml.safe_load(sf.read())
|
|
1050
|
+
if edata:
|
|
1051
|
+
etitle = edata.get("title", "?")
|
|
1052
|
+
epts = edata.get("points", "?")
|
|
1053
|
+
estat = edata.get("status", "?")
|
|
1054
|
+
click.echo(f" {edata.get('id', e)}: {etitle} [{epts}pts] ({estat})")
|
|
1055
|
+
continue
|
|
1056
|
+
click.echo(f" {e} (shard not found)")
|
|
1057
|
+
elif isinstance(e, dict):
|
|
1058
|
+
eid = e.get("id", "?")
|
|
1059
|
+
etitle = e.get("title", "?")
|
|
1060
|
+
epts = e.get("points", "?")
|
|
1061
|
+
estat = e.get("status", "?")
|
|
1062
|
+
click.echo(f" {eid}: {etitle} [{epts}pts] ({estat})")
|
|
1063
|
+
|
|
1064
|
+
standalone_stories = init_data.get("standalone_stories", [])
|
|
1065
|
+
if standalone_stories:
|
|
1066
|
+
click.echo(f"\nStandalone Stories ({len(standalone_stories)}):")
|
|
1067
|
+
for s in standalone_stories:
|
|
1068
|
+
sid = s.get("id", "?")
|
|
1069
|
+
stitle = s.get("title", "?")
|
|
1070
|
+
spts = s.get("points", "?")
|
|
1071
|
+
sstat = s.get("status", "?")
|
|
1072
|
+
click.echo(f" {sid}: {stitle} [{spts}pts] ({sstat})")
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
@initiative.command("cancel")
|
|
1076
|
+
@click.argument("name")
|
|
1077
|
+
@click.option("--jira", is_flag=True, help="Also cancel epics in Jira")
|
|
1078
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
|
|
1079
|
+
def initiative_cancel(name: str, jira: bool, dry_run: bool):
|
|
1080
|
+
"""Cancel an initiative and all its epics/stories.
|
|
1081
|
+
|
|
1082
|
+
Sets the initiative status to 'canceled' and cancels all epics and stories
|
|
1083
|
+
within it.
|
|
1084
|
+
|
|
1085
|
+
\b
|
|
1086
|
+
Arguments:
|
|
1087
|
+
NAME - Initiative slug (e.g., benchmark-reliability, technical-debt)
|
|
1088
|
+
|
|
1089
|
+
\b
|
|
1090
|
+
Examples:
|
|
1091
|
+
pf sprint initiative cancel technical-debt --dry-run
|
|
1092
|
+
pf sprint initiative cancel technical-debt
|
|
1093
|
+
pf sprint initiative cancel technical-debt --jira
|
|
1094
|
+
"""
|
|
1095
|
+
import yaml
|
|
1096
|
+
|
|
1097
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
1098
|
+
|
|
1099
|
+
root = get_project_root()
|
|
1100
|
+
sprint_dir = root / "sprint"
|
|
1101
|
+
init_file = sprint_dir / f"initiative-{name}.yaml"
|
|
1102
|
+
|
|
1103
|
+
if not init_file.exists():
|
|
1104
|
+
raise click.ClickException(f"Initiative not found: {name}\n Expected: {init_file}")
|
|
1105
|
+
|
|
1106
|
+
with open(init_file) as f:
|
|
1107
|
+
init_data = yaml.safe_load(f.read())
|
|
1108
|
+
|
|
1109
|
+
if not init_data:
|
|
1110
|
+
raise click.ClickException(f"Empty initiative file: {init_file}")
|
|
1111
|
+
|
|
1112
|
+
init_name = init_data.get("name", name)
|
|
1113
|
+
epics = init_data.get("epics", [])
|
|
1114
|
+
standalone_stories = init_data.get("standalone_stories", [])
|
|
1115
|
+
|
|
1116
|
+
# Collect Jira keys for warning
|
|
1117
|
+
jira_keys = []
|
|
1118
|
+
epic_count = 0
|
|
1119
|
+
story_count = 0
|
|
1120
|
+
|
|
1121
|
+
for e in epics:
|
|
1122
|
+
if isinstance(e, str):
|
|
1123
|
+
shard = _epic_shard_path(sprint_dir, e)
|
|
1124
|
+
if shard.exists():
|
|
1125
|
+
with open(shard) as sf:
|
|
1126
|
+
edata = yaml.safe_load(sf.read())
|
|
1127
|
+
if edata:
|
|
1128
|
+
epic_count += 1
|
|
1129
|
+
if edata.get("jira"):
|
|
1130
|
+
jira_keys.append(edata["jira"])
|
|
1131
|
+
story_count += len(edata.get("stories", []))
|
|
1132
|
+
elif isinstance(e, dict):
|
|
1133
|
+
epic_count += 1
|
|
1134
|
+
if e.get("jira"):
|
|
1135
|
+
jira_keys.append(e["jira"])
|
|
1136
|
+
story_count += len(e.get("stories", []))
|
|
1137
|
+
|
|
1138
|
+
story_count += len(standalone_stories)
|
|
1139
|
+
|
|
1140
|
+
click.echo(f"Initiative: {init_name}")
|
|
1141
|
+
click.echo(f"Epics: {epic_count}")
|
|
1142
|
+
click.echo(f"Stories: {story_count}")
|
|
1143
|
+
|
|
1144
|
+
if jira_keys and not jira:
|
|
1145
|
+
click.echo(f"\nWarning: {len(jira_keys)} epic(s) have Jira keys -- pass --jira to also cancel in Jira")
|
|
1146
|
+
for k in jira_keys:
|
|
1147
|
+
click.echo(f" {k}")
|
|
1148
|
+
|
|
1149
|
+
if dry_run:
|
|
1150
|
+
click.echo(f"\n[DRY-RUN] Would cancel initiative '{init_name}' ({epic_count} epics, {story_count} stories)")
|
|
1151
|
+
return
|
|
1152
|
+
|
|
1153
|
+
# Cancel all epics
|
|
1154
|
+
for i, e in enumerate(epics):
|
|
1155
|
+
if isinstance(e, str):
|
|
1156
|
+
shard = _epic_shard_path(sprint_dir, e)
|
|
1157
|
+
if shard.exists():
|
|
1158
|
+
with open(shard) as sf:
|
|
1159
|
+
edata = yaml.safe_load(sf.read())
|
|
1160
|
+
if edata:
|
|
1161
|
+
edata["status"] = "canceled"
|
|
1162
|
+
for s in edata.get("stories", []):
|
|
1163
|
+
s["status"] = "canceled"
|
|
1164
|
+
with open(shard, "w") as sf:
|
|
1165
|
+
yaml.dump(edata, sf, default_flow_style=False, sort_keys=False)
|
|
1166
|
+
if jira and edata.get("jira"):
|
|
1167
|
+
_transition_jira(edata["jira"], "Cancelled")
|
|
1168
|
+
elif isinstance(e, dict):
|
|
1169
|
+
e["status"] = "canceled"
|
|
1170
|
+
for s in e.get("stories", []):
|
|
1171
|
+
s["status"] = "canceled"
|
|
1172
|
+
if jira and e.get("jira"):
|
|
1173
|
+
_transition_jira(e["jira"], "Cancelled")
|
|
1174
|
+
|
|
1175
|
+
# Cancel standalone stories
|
|
1176
|
+
for s in standalone_stories:
|
|
1177
|
+
s["status"] = "canceled"
|
|
1178
|
+
|
|
1179
|
+
# Update initiative status
|
|
1180
|
+
init_data["status"] = "canceled"
|
|
1181
|
+
|
|
1182
|
+
with open(init_file, "w") as f:
|
|
1183
|
+
yaml.dump(init_data, f, default_flow_style=False, sort_keys=False)
|
|
1184
|
+
|
|
1185
|
+
click.echo(f"\nCanceled initiative '{init_name}' ({epic_count} epics, {story_count} stories)")
|
|
1186
|
+
if jira and jira_keys:
|
|
1187
|
+
click.echo(f"Transitioned {len(jira_keys)} Jira epic(s) to Cancelled")
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
# --- Check command (replaces check-story.sh) ---
|
|
1191
|
+
|
|
1192
|
+
@sprint.command()
|
|
1193
|
+
@click.argument("id")
|
|
1194
|
+
def check(id: str):
|
|
1195
|
+
"""Check story/epic availability. Returns JSON.
|
|
1196
|
+
|
|
1197
|
+
\b
|
|
1198
|
+
Arguments:
|
|
1199
|
+
ID - Story ID, epic ID, or 'next' for highest priority
|
|
1200
|
+
|
|
1201
|
+
\b
|
|
1202
|
+
Returns JSON with type, details, and availability:
|
|
1203
|
+
type: "story" | "epic" | "next" | "not_found"
|
|
1204
|
+
"""
|
|
1205
|
+
import json
|
|
1206
|
+
|
|
1207
|
+
from pennyfarthing_scripts.sprint.loader import (
|
|
1208
|
+
find_epic,
|
|
1209
|
+
get_all_stories,
|
|
1210
|
+
load_sprint,
|
|
1211
|
+
)
|
|
1212
|
+
from pennyfarthing_scripts.sprint.work import check_story, get_next_story
|
|
1213
|
+
|
|
1214
|
+
data = load_sprint()
|
|
1215
|
+
|
|
1216
|
+
if id == "next":
|
|
1217
|
+
result = get_next_story()
|
|
1218
|
+
if result.get("available"):
|
|
1219
|
+
story = result["story"]
|
|
1220
|
+
# Find parent epic
|
|
1221
|
+
epic_id = _find_epic_for_story(data, story.get("id", ""))
|
|
1222
|
+
out = {
|
|
1223
|
+
"type": "next",
|
|
1224
|
+
"story": {
|
|
1225
|
+
"id": story.get("id"),
|
|
1226
|
+
"title": story.get("title"),
|
|
1227
|
+
"points": story.get("points", 0),
|
|
1228
|
+
"priority": story.get("priority", "P2"),
|
|
1229
|
+
"workflow": story.get("workflow", "tdd"),
|
|
1230
|
+
"repos": story.get("repos", "pennyfarthing"),
|
|
1231
|
+
"epic_id": epic_id,
|
|
1232
|
+
"acceptance_criteria": story.get("acceptance_criteria", []),
|
|
1233
|
+
},
|
|
1234
|
+
}
|
|
1235
|
+
else:
|
|
1236
|
+
out = {"type": "next", "story": None, "message": "No available stories in backlog"}
|
|
1237
|
+
click.echo(json.dumps(out, indent=2))
|
|
1238
|
+
return
|
|
1239
|
+
|
|
1240
|
+
# Check if it's an epic
|
|
1241
|
+
if data:
|
|
1242
|
+
epic = find_epic(data, id)
|
|
1243
|
+
if epic:
|
|
1244
|
+
available_statuses = {"backlog", "ready", "planning"}
|
|
1245
|
+
available = [
|
|
1246
|
+
s for s in epic.get("stories", [])
|
|
1247
|
+
if s.get("status") in available_statuses
|
|
1248
|
+
]
|
|
1249
|
+
# Sort by priority
|
|
1250
|
+
priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
|
|
1251
|
+
available.sort(key=lambda s: priority_order.get(s.get("priority", "P2"), 2))
|
|
1252
|
+
|
|
1253
|
+
first = available[0] if available else None
|
|
1254
|
+
out = {
|
|
1255
|
+
"type": "epic",
|
|
1256
|
+
"id": str(epic.get("id", id)),
|
|
1257
|
+
"title": epic.get("title", "Unknown"),
|
|
1258
|
+
"available_stories": len(available),
|
|
1259
|
+
}
|
|
1260
|
+
if first:
|
|
1261
|
+
out["first_story"] = {
|
|
1262
|
+
"id": first.get("id"),
|
|
1263
|
+
"title": first.get("title"),
|
|
1264
|
+
"points": first.get("points", 0),
|
|
1265
|
+
"workflow": first.get("workflow", "tdd"),
|
|
1266
|
+
"repos": first.get("repos", "pennyfarthing"),
|
|
1267
|
+
"acceptance_criteria": first.get("acceptance_criteria", []),
|
|
1268
|
+
}
|
|
1269
|
+
else:
|
|
1270
|
+
out["first_story"] = None
|
|
1271
|
+
out["message"] = "No available stories in this epic"
|
|
1272
|
+
click.echo(json.dumps(out, indent=2))
|
|
1273
|
+
return
|
|
1274
|
+
|
|
1275
|
+
# Check if it's a story
|
|
1276
|
+
result = check_story(id)
|
|
1277
|
+
story = result.get("story")
|
|
1278
|
+
if story:
|
|
1279
|
+
epic_id = _find_epic_for_story(data, story.get("id", ""))
|
|
1280
|
+
out = {
|
|
1281
|
+
"type": "story",
|
|
1282
|
+
"id": story.get("id", id),
|
|
1283
|
+
"title": story.get("title", "Unknown"),
|
|
1284
|
+
"points": story.get("points", 0),
|
|
1285
|
+
"workflow": story.get("workflow", "tdd"),
|
|
1286
|
+
"status": story.get("status", "backlog"),
|
|
1287
|
+
"assigned_to": story.get("assigned_to", ""),
|
|
1288
|
+
"epic_id": epic_id,
|
|
1289
|
+
"repos": story.get("repos", "pennyfarthing"),
|
|
1290
|
+
"available": result.get("available", False),
|
|
1291
|
+
"acceptance_criteria": story.get("acceptance_criteria", []),
|
|
1292
|
+
}
|
|
1293
|
+
click.echo(json.dumps(out, indent=2))
|
|
1294
|
+
return
|
|
1295
|
+
|
|
1296
|
+
# Not found
|
|
1297
|
+
click.echo(json.dumps({
|
|
1298
|
+
"type": "not_found",
|
|
1299
|
+
"id": id,
|
|
1300
|
+
"message": "Story or epic not found in current sprint",
|
|
1301
|
+
}, indent=2))
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def _find_epic_for_story(data: dict | None, story_id: str) -> str:
|
|
1305
|
+
"""Find the parent epic ID for a story."""
|
|
1306
|
+
if not data or "epics" not in data:
|
|
1307
|
+
return ""
|
|
1308
|
+
for epic in data["epics"]:
|
|
1309
|
+
if not isinstance(epic, dict):
|
|
1310
|
+
continue
|
|
1311
|
+
for s in epic.get("stories", []):
|
|
1312
|
+
if s.get("id") == story_id:
|
|
1313
|
+
return str(epic.get("id", ""))
|
|
1314
|
+
return ""
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
# --- Info command (replaces sprint-info.sh) ---
|
|
1318
|
+
|
|
1319
|
+
@sprint.command()
|
|
1320
|
+
def info():
|
|
1321
|
+
"""Output sprint info as JSON for Cyclist sidebar.
|
|
1322
|
+
|
|
1323
|
+
\b
|
|
1324
|
+
Returns: {"remaining": N, "inProgress": N, "endDate": "YYYY-MM-DD"}
|
|
1325
|
+
"""
|
|
1326
|
+
import json
|
|
1327
|
+
|
|
1328
|
+
from pennyfarthing_scripts.sprint.loader import get_all_stories, get_sprint_info
|
|
1329
|
+
|
|
1330
|
+
sprint_data = get_sprint_info()
|
|
1331
|
+
stories = get_all_stories()
|
|
1332
|
+
|
|
1333
|
+
end_date = sprint_data.get("end_date")
|
|
1334
|
+
|
|
1335
|
+
remaining = sum(
|
|
1336
|
+
s.get("points", 0) or 0
|
|
1337
|
+
for s in stories
|
|
1338
|
+
if s.get("status") in ("backlog", "planning", "ready", None)
|
|
1339
|
+
)
|
|
1340
|
+
in_progress = sum(
|
|
1341
|
+
s.get("points", 0) or 0
|
|
1342
|
+
for s in stories
|
|
1343
|
+
if s.get("status") == "in_progress"
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
click.echo(json.dumps({
|
|
1347
|
+
"remaining": remaining,
|
|
1348
|
+
"inProgress": in_progress,
|
|
1349
|
+
"endDate": str(end_date) if end_date else None,
|
|
1350
|
+
}))
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
# --- Metrics command (replaces sprint-metrics.sh) ---
|
|
1354
|
+
|
|
1355
|
+
@sprint.command()
|
|
1356
|
+
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
|
|
1357
|
+
def metrics(output_json: bool):
|
|
1358
|
+
"""Display sprint metrics and progress.
|
|
1359
|
+
|
|
1360
|
+
Shows points, stories, timeline, and velocity tracking.
|
|
1361
|
+
"""
|
|
1362
|
+
import json
|
|
1363
|
+
from datetime import date, datetime
|
|
1364
|
+
|
|
1365
|
+
from pennyfarthing_scripts.sprint.loader import get_all_stories, get_sprint_info
|
|
1366
|
+
|
|
1367
|
+
sprint_data = get_sprint_info()
|
|
1368
|
+
stories = get_all_stories()
|
|
1369
|
+
|
|
1370
|
+
if not sprint_data:
|
|
1371
|
+
click.echo("No sprint data available")
|
|
1372
|
+
return
|
|
1373
|
+
|
|
1374
|
+
sprint_name = sprint_data.get("name", "Unknown")
|
|
1375
|
+
goal = sprint_data.get("goal", "")
|
|
1376
|
+
start_date_str = sprint_data.get("start_date", "")
|
|
1377
|
+
end_date_str = sprint_data.get("end_date", "")
|
|
1378
|
+
|
|
1379
|
+
# Count stories/points by status
|
|
1380
|
+
done_stories = [s for s in stories if s.get("status") in ("done", "completed")]
|
|
1381
|
+
wip_stories = [s for s in stories if s.get("status") == "in_progress"]
|
|
1382
|
+
backlog_stories = [s for s in stories if s.get("status") in ("backlog", "planning", "ready", None)]
|
|
1383
|
+
|
|
1384
|
+
done_pts = sum(s.get("points", 0) or 0 for s in done_stories)
|
|
1385
|
+
wip_pts = sum(s.get("points", 0) or 0 for s in wip_stories)
|
|
1386
|
+
backlog_pts = sum(s.get("points", 0) or 0 for s in backlog_stories)
|
|
1387
|
+
total_pts = done_pts + wip_pts + backlog_pts
|
|
1388
|
+
|
|
1389
|
+
# Date calculations
|
|
1390
|
+
today = date.today()
|
|
1391
|
+
try:
|
|
1392
|
+
start_date = datetime.strptime(str(start_date_str), "%Y-%m-%d").date()
|
|
1393
|
+
end_date = datetime.strptime(str(end_date_str), "%Y-%m-%d").date()
|
|
1394
|
+
except (ValueError, TypeError):
|
|
1395
|
+
start_date = today
|
|
1396
|
+
end_date = today
|
|
1397
|
+
|
|
1398
|
+
total_days = (end_date - start_date).days or 1
|
|
1399
|
+
days_elapsed = max(0, (today - start_date).days)
|
|
1400
|
+
days_remaining = max(0, (end_date - today).days)
|
|
1401
|
+
|
|
1402
|
+
pct_complete = (done_pts * 100 // total_pts) if total_pts > 0 else 0
|
|
1403
|
+
pct_time = (days_elapsed * 100 // total_days) if total_days > 0 else 0
|
|
1404
|
+
|
|
1405
|
+
velocity_target = sprint_data.get("velocity_target", total_pts)
|
|
1406
|
+
expected_pts = (velocity_target * days_elapsed // total_days) if total_days > 0 else 0
|
|
1407
|
+
|
|
1408
|
+
if output_json:
|
|
1409
|
+
click.echo(json.dumps({
|
|
1410
|
+
"sprint": sprint_name,
|
|
1411
|
+
"dates": {
|
|
1412
|
+
"start": str(start_date_str),
|
|
1413
|
+
"end": str(end_date_str),
|
|
1414
|
+
"today": str(today),
|
|
1415
|
+
},
|
|
1416
|
+
"points": {
|
|
1417
|
+
"total": total_pts,
|
|
1418
|
+
"completed": done_pts,
|
|
1419
|
+
"in_progress": wip_pts,
|
|
1420
|
+
"backlog": backlog_pts,
|
|
1421
|
+
"velocity_target": velocity_target,
|
|
1422
|
+
},
|
|
1423
|
+
"stories": {
|
|
1424
|
+
"total": len(stories),
|
|
1425
|
+
"done": len(done_stories),
|
|
1426
|
+
"in_progress": len(wip_stories),
|
|
1427
|
+
"backlog": len(backlog_stories),
|
|
1428
|
+
},
|
|
1429
|
+
"progress": {
|
|
1430
|
+
"percent_complete": pct_complete,
|
|
1431
|
+
"percent_time": pct_time,
|
|
1432
|
+
"days_elapsed": days_elapsed,
|
|
1433
|
+
"days_remaining": days_remaining,
|
|
1434
|
+
"total_days": total_days,
|
|
1435
|
+
},
|
|
1436
|
+
"velocity": {
|
|
1437
|
+
"expected_points": expected_pts,
|
|
1438
|
+
"actual_points": done_pts,
|
|
1439
|
+
"on_track": done_pts >= expected_pts,
|
|
1440
|
+
},
|
|
1441
|
+
}, indent=2))
|
|
1442
|
+
return
|
|
1443
|
+
|
|
1444
|
+
# Human-readable output
|
|
1445
|
+
click.echo("")
|
|
1446
|
+
click.echo(f" Sprint: {sprint_name}")
|
|
1447
|
+
click.echo(f" Goal: {goal}")
|
|
1448
|
+
click.echo("")
|
|
1449
|
+
click.echo(f" Timeline: {start_date_str} to {end_date_str} (Day {days_elapsed}/{total_days}, {days_remaining} remaining)")
|
|
1450
|
+
click.echo("")
|
|
1451
|
+
click.echo(f" Points: {done_pts} done / {wip_pts} WIP / {backlog_pts} backlog = {total_pts} total ({pct_complete}%)")
|
|
1452
|
+
click.echo(f" Stories: {len(done_stories)} done / {len(wip_stories)} WIP / {len(backlog_stories)} backlog = {len(stories)} total")
|
|
1453
|
+
click.echo("")
|
|
1454
|
+
click.echo(f" Velocity: {done_pts}/{expected_pts} expected ({velocity_target} target)")
|
|
1455
|
+
if done_pts >= expected_pts:
|
|
1456
|
+
click.echo(" Status: On track")
|
|
1457
|
+
else:
|
|
1458
|
+
click.echo(" Status: Behind schedule")
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
# --- Story field command (replaces get-story-field.sh) ---
|
|
1462
|
+
|
|
1463
|
+
@story.command("field")
|
|
1464
|
+
@click.argument("story_id")
|
|
1465
|
+
@click.argument("field_name")
|
|
1466
|
+
def story_field(story_id: str, field_name: str):
|
|
1467
|
+
"""Get a field value from a story.
|
|
1468
|
+
|
|
1469
|
+
\b
|
|
1470
|
+
Arguments:
|
|
1471
|
+
STORY_ID - Story ID (e.g., 79-1 or MSSCI-12345)
|
|
1472
|
+
FIELD_NAME - Field to extract (e.g., workflow, status, points)
|
|
1473
|
+
|
|
1474
|
+
Returns the field value or "null" if not found.
|
|
1475
|
+
"""
|
|
1476
|
+
from pennyfarthing_scripts.sprint.loader import get_story_by_id, get_story_field, load_sprint
|
|
1477
|
+
|
|
1478
|
+
# Default values for common fields
|
|
1479
|
+
defaults = {
|
|
1480
|
+
"workflow": "tdd",
|
|
1481
|
+
"status": "backlog",
|
|
1482
|
+
"repos": "pennyfarthing",
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
# Try get_story_field first (works with epic-story format like "79-1")
|
|
1486
|
+
data = load_sprint()
|
|
1487
|
+
if data:
|
|
1488
|
+
value = get_story_field(data, story_id, field_name)
|
|
1489
|
+
if value is not None:
|
|
1490
|
+
click.echo(str(value))
|
|
1491
|
+
return
|
|
1492
|
+
|
|
1493
|
+
# Fallback: try direct story lookup (works with Jira keys)
|
|
1494
|
+
story = get_story_by_id(story_id)
|
|
1495
|
+
if story:
|
|
1496
|
+
value = story.get(field_name)
|
|
1497
|
+
if value is not None:
|
|
1498
|
+
click.echo(str(value))
|
|
1499
|
+
return
|
|
1500
|
+
|
|
1501
|
+
# Return default or null
|
|
1502
|
+
click.echo(defaults.get(field_name, "null"))
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
# --- Epic field command (replaces get-epic-field.sh) ---
|
|
1506
|
+
|
|
1507
|
+
@epic.command("field")
|
|
1508
|
+
@click.argument("epic_id")
|
|
1509
|
+
@click.argument("field_name")
|
|
1510
|
+
def epic_field(epic_id: str, field_name: str):
|
|
1511
|
+
"""Get a field value from an epic.
|
|
1512
|
+
|
|
1513
|
+
\b
|
|
1514
|
+
Arguments:
|
|
1515
|
+
EPIC_ID - Epic ID (e.g., epic-79 or 79)
|
|
1516
|
+
FIELD_NAME - Field to extract (e.g., jira, title, status)
|
|
1517
|
+
|
|
1518
|
+
Returns the field value or "null" if not found.
|
|
1519
|
+
"""
|
|
1520
|
+
from pennyfarthing_scripts.sprint.loader import find_epic, load_sprint
|
|
1521
|
+
|
|
1522
|
+
data = load_sprint()
|
|
1523
|
+
if not data:
|
|
1524
|
+
click.echo("null")
|
|
1525
|
+
return
|
|
1526
|
+
|
|
1527
|
+
epic = find_epic(data, epic_id)
|
|
1528
|
+
if not epic:
|
|
1529
|
+
click.echo("null")
|
|
1530
|
+
return
|
|
1531
|
+
|
|
1532
|
+
value = epic.get(field_name)
|
|
1533
|
+
if value is not None:
|
|
1534
|
+
click.echo(str(value).rstrip())
|
|
1535
|
+
else:
|
|
1536
|
+
click.echo("null")
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
# --- Future command (replaces list-future.sh) ---
|
|
1540
|
+
|
|
1541
|
+
@sprint.command()
|
|
1542
|
+
@click.argument("epic_id", required=False)
|
|
1543
|
+
def future(epic_id: str | None):
|
|
1544
|
+
"""Show future work initiatives and epics.
|
|
1545
|
+
|
|
1546
|
+
\b
|
|
1547
|
+
Arguments:
|
|
1548
|
+
EPIC_ID - Optional epic ID to show detailed stories (e.g., epic-55)
|
|
1549
|
+
|
|
1550
|
+
\b
|
|
1551
|
+
Examples:
|
|
1552
|
+
pf sprint future # Show all initiatives
|
|
1553
|
+
pf sprint future epic-55 # Show stories for specific epic
|
|
1554
|
+
"""
|
|
1555
|
+
import yaml
|
|
1556
|
+
|
|
1557
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
1558
|
+
|
|
1559
|
+
root = get_project_root()
|
|
1560
|
+
sprint_dir = root / "sprint"
|
|
1561
|
+
|
|
1562
|
+
init_files = sorted(sprint_dir.glob("initiative-*.yaml"))
|
|
1563
|
+
if not init_files:
|
|
1564
|
+
click.echo("No future initiatives found.")
|
|
1565
|
+
return
|
|
1566
|
+
|
|
1567
|
+
# If specific epic requested, show detailed view
|
|
1568
|
+
if epic_id:
|
|
1569
|
+
_show_future_epic_detail(epic_id, init_files, sprint_dir)
|
|
1570
|
+
return
|
|
1571
|
+
|
|
1572
|
+
# Default: show initiative summary
|
|
1573
|
+
click.echo("# Future Work - Available for Promotion")
|
|
1574
|
+
click.echo("")
|
|
1575
|
+
|
|
1576
|
+
total_epics = 0
|
|
1577
|
+
total_points = 0
|
|
1578
|
+
|
|
1579
|
+
for init_file in init_files:
|
|
1580
|
+
with open(init_file) as f:
|
|
1581
|
+
init_data = yaml.safe_load(f.read())
|
|
1582
|
+
if not init_data:
|
|
1583
|
+
continue
|
|
1584
|
+
|
|
1585
|
+
init_name = init_data.get("name", init_file.stem)
|
|
1586
|
+
init_status = init_data.get("status", "planning")
|
|
1587
|
+
blocked_by = init_data.get("blocked_by")
|
|
1588
|
+
init_points = init_data.get("total_points", 0)
|
|
1589
|
+
|
|
1590
|
+
if init_status == "ready":
|
|
1591
|
+
status_tag = "[READY]"
|
|
1592
|
+
elif blocked_by:
|
|
1593
|
+
status_tag = "[BLOCKED]"
|
|
1594
|
+
else:
|
|
1595
|
+
status_tag = f"[{init_status}]"
|
|
1596
|
+
|
|
1597
|
+
click.echo(f"## {init_name} {status_tag}")
|
|
1598
|
+
click.echo(f"**Total:** {init_points} points")
|
|
1599
|
+
if blocked_by:
|
|
1600
|
+
click.echo(f"**Blocked:** {blocked_by}")
|
|
1601
|
+
click.echo("")
|
|
1602
|
+
|
|
1603
|
+
click.echo("| Epic | Title | Pts | Pri | Status |")
|
|
1604
|
+
click.echo("|------|-------|-----|-----|--------|")
|
|
1605
|
+
|
|
1606
|
+
epics = init_data.get("epics", [])
|
|
1607
|
+
for e in epics:
|
|
1608
|
+
edata = _resolve_epic_ref(e, sprint_dir)
|
|
1609
|
+
if not edata:
|
|
1610
|
+
continue
|
|
1611
|
+
eid = edata.get("id", "?")
|
|
1612
|
+
etitle = edata.get("title", "?")
|
|
1613
|
+
if len(etitle) > 40:
|
|
1614
|
+
etitle = etitle[:37] + "..."
|
|
1615
|
+
epts = edata.get("points", "?")
|
|
1616
|
+
epri = edata.get("priority", "P2")
|
|
1617
|
+
estat = edata.get("status", "planning")
|
|
1618
|
+
click.echo(f"| {eid} | {etitle} | {epts} | {epri} | {estat} |")
|
|
1619
|
+
total_epics += 1
|
|
1620
|
+
total_points += edata.get("points", 0) or 0
|
|
1621
|
+
|
|
1622
|
+
click.echo("")
|
|
1623
|
+
|
|
1624
|
+
click.echo("---")
|
|
1625
|
+
click.echo(f"**Summary:** {total_epics} epics, {total_points} points total")
|
|
1626
|
+
click.echo("")
|
|
1627
|
+
click.echo("To see epic details: `pf sprint future epic-55`")
|
|
1628
|
+
click.echo("To promote an epic: `pf sprint epic promote epic-55`")
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
def _resolve_epic_ref(ref, sprint_dir) -> dict | None:
|
|
1632
|
+
"""Resolve an epic reference (string ref or inline dict) to a dict."""
|
|
1633
|
+
import yaml
|
|
1634
|
+
|
|
1635
|
+
if isinstance(ref, dict):
|
|
1636
|
+
return ref
|
|
1637
|
+
if isinstance(ref, str):
|
|
1638
|
+
shard = _epic_shard_path(sprint_dir, ref)
|
|
1639
|
+
if shard.exists():
|
|
1640
|
+
with open(shard) as f:
|
|
1641
|
+
return yaml.safe_load(f.read())
|
|
1642
|
+
return None
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
def _show_future_epic_detail(epic_id: str, init_files, sprint_dir):
|
|
1646
|
+
"""Show detailed view of a specific future epic."""
|
|
1647
|
+
import yaml
|
|
1648
|
+
|
|
1649
|
+
for init_file in init_files:
|
|
1650
|
+
with open(init_file) as f:
|
|
1651
|
+
init_data = yaml.safe_load(f.read())
|
|
1652
|
+
if not init_data:
|
|
1653
|
+
continue
|
|
1654
|
+
|
|
1655
|
+
for e in init_data.get("epics", []):
|
|
1656
|
+
edata = _resolve_epic_ref(e, sprint_dir)
|
|
1657
|
+
if not edata:
|
|
1658
|
+
continue
|
|
1659
|
+
eid = str(edata.get("id", ""))
|
|
1660
|
+
if epic_id not in (eid, eid.replace("epic-", ""), f"epic-{epic_id}"):
|
|
1661
|
+
continue
|
|
1662
|
+
|
|
1663
|
+
click.echo(f"# Epic Details: {eid}")
|
|
1664
|
+
click.echo("")
|
|
1665
|
+
click.echo(f"**Title:** {edata.get('title', '?')}")
|
|
1666
|
+
click.echo(f"**Points:** {edata.get('points', '?')} | **Priority:** {edata.get('priority', 'P2')} | **Status:** {edata.get('status', 'planning')}")
|
|
1667
|
+
click.echo("")
|
|
1668
|
+
desc = edata.get("description", "No description")
|
|
1669
|
+
if desc:
|
|
1670
|
+
click.echo("**Description:**")
|
|
1671
|
+
for line in str(desc).strip().split("\n")[:5]:
|
|
1672
|
+
click.echo(line)
|
|
1673
|
+
click.echo("")
|
|
1674
|
+
|
|
1675
|
+
stories = edata.get("stories", [])
|
|
1676
|
+
if stories:
|
|
1677
|
+
click.echo("## Stories")
|
|
1678
|
+
click.echo("")
|
|
1679
|
+
click.echo("| ID | Title | Pts | Pri | Status |")
|
|
1680
|
+
click.echo("|----|-------|-----|-----|--------|")
|
|
1681
|
+
for s in stories:
|
|
1682
|
+
stitle = s.get("title", "?")
|
|
1683
|
+
if len(stitle) > 45:
|
|
1684
|
+
stitle = stitle[:42] + "..."
|
|
1685
|
+
click.echo(f"| {s.get('id', '?')} | {stitle} | {s.get('points', '?')} | {s.get('priority', 'P1')} | {s.get('status', 'planning')} |")
|
|
1686
|
+
click.echo("")
|
|
1687
|
+
|
|
1688
|
+
click.echo("---")
|
|
1689
|
+
click.echo(f"To promote this epic: `pf sprint epic promote {eid}`")
|
|
1690
|
+
return
|
|
1691
|
+
|
|
1692
|
+
raise click.ClickException(f"Epic {epic_id} not found in future initiatives")
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
# --- New sprint command (replaces new-sprint.sh) ---
|
|
1696
|
+
|
|
1697
|
+
@sprint.command("new")
|
|
1698
|
+
@click.argument("sprint_yyww")
|
|
1699
|
+
@click.argument("jira_id", type=int)
|
|
1700
|
+
@click.argument("start_date")
|
|
1701
|
+
@click.argument("end_date")
|
|
1702
|
+
@click.argument("goal")
|
|
1703
|
+
def new_sprint(sprint_yyww: str, jira_id: int, start_date: str, end_date: str, goal: str):
|
|
1704
|
+
"""Initialize a new sprint.
|
|
1705
|
+
|
|
1706
|
+
\b
|
|
1707
|
+
Arguments:
|
|
1708
|
+
SPRINT_YYWW Sprint identifier in YYWW format (e.g., 2607)
|
|
1709
|
+
JIRA_ID Jira sprint ID number (e.g., 278)
|
|
1710
|
+
START_DATE Sprint start date YYYY-MM-DD
|
|
1711
|
+
END_DATE Sprint end date YYYY-MM-DD
|
|
1712
|
+
GOAL Sprint goal (quoted string)
|
|
1713
|
+
|
|
1714
|
+
\b
|
|
1715
|
+
Examples:
|
|
1716
|
+
pf sprint new 2607 278 2026-02-16 2026-03-01 "Performance and polish"
|
|
1717
|
+
"""
|
|
1718
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
1719
|
+
|
|
1720
|
+
root = get_project_root()
|
|
1721
|
+
sprint_file = root / "sprint" / "current-sprint.yaml"
|
|
1722
|
+
archive_file = root / "sprint" / "archive" / f"sprint-{sprint_yyww}-completed.yaml"
|
|
1723
|
+
|
|
1724
|
+
# Warn if current sprint is active
|
|
1725
|
+
if sprint_file.exists():
|
|
1726
|
+
import yaml
|
|
1727
|
+
|
|
1728
|
+
with open(sprint_file) as f:
|
|
1729
|
+
existing = yaml.safe_load(f.read())
|
|
1730
|
+
if existing and existing.get("sprint", {}).get("status") == "active":
|
|
1731
|
+
click.echo("Warning: Current sprint is still active!")
|
|
1732
|
+
click.echo("Current sprint file will be overwritten.")
|
|
1733
|
+
if not click.confirm("Continue?"):
|
|
1734
|
+
click.echo("Aborted.")
|
|
1735
|
+
return
|
|
1736
|
+
|
|
1737
|
+
# Create sprint file using write_sprint for consistency
|
|
1738
|
+
from pennyfarthing_scripts.sprint.yaml_io import write_sprint
|
|
1739
|
+
|
|
1740
|
+
sprint_data = {
|
|
1741
|
+
"sprint": {
|
|
1742
|
+
"name": f"TO Sprint {sprint_yyww}",
|
|
1743
|
+
"jira_sprint_id": jira_id,
|
|
1744
|
+
"jira_sprint_name": f"TO Sprint {sprint_yyww}",
|
|
1745
|
+
"goal": goal,
|
|
1746
|
+
"start_date": start_date,
|
|
1747
|
+
"end_date": end_date,
|
|
1748
|
+
"status": "active",
|
|
1749
|
+
},
|
|
1750
|
+
"epics": [],
|
|
1751
|
+
}
|
|
1752
|
+
write_sprint(sprint_file, sprint_data)
|
|
1753
|
+
click.echo(f"Created {sprint_file}")
|
|
1754
|
+
|
|
1755
|
+
# Create archive file
|
|
1756
|
+
from datetime import date
|
|
1757
|
+
|
|
1758
|
+
archive_content = f"""# Sprint TO Sprint {sprint_yyww} - Completed Stories
|
|
1759
|
+
# Jira Sprint ID: {jira_id}
|
|
1760
|
+
# Archived: {date.today()}
|
|
1761
|
+
|
|
1762
|
+
sprint:
|
|
1763
|
+
name: "TO Sprint {sprint_yyww}"
|
|
1764
|
+
jira_sprint_id: {jira_id}
|
|
1765
|
+
jira_sprint_name: "TO Sprint {sprint_yyww}"
|
|
1766
|
+
goal: {goal}
|
|
1767
|
+
|
|
1768
|
+
completed:
|
|
1769
|
+
# Completed stories will be appended here by pf sprint archive
|
|
1770
|
+
"""
|
|
1771
|
+
archive_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1772
|
+
archive_file.write_text(archive_content)
|
|
1773
|
+
click.echo(f"Created {archive_file}")
|
|
1774
|
+
|
|
1775
|
+
click.echo("")
|
|
1776
|
+
click.echo(f"New sprint initialized:")
|
|
1777
|
+
click.echo(f" Name: TO Sprint {sprint_yyww}")
|
|
1778
|
+
click.echo(f" Jira ID: {jira_id}")
|
|
1779
|
+
click.echo(f" Dates: {start_date} to {end_date}")
|
|
1780
|
+
click.echo(f" Goal: {goal}")
|
|
1781
|
+
click.echo("")
|
|
1782
|
+
click.echo("Next steps:")
|
|
1783
|
+
click.echo(" 1. Add epics: pf sprint epic promote <epic-id>")
|
|
1784
|
+
click.echo(" 2. Check status: pf sprint status")
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
# --- Standalone command ---
|
|
1788
|
+
|
|
1789
|
+
@sprint.command()
|
|
1790
|
+
@click.argument("title", required=False)
|
|
1791
|
+
@click.argument("points", required=False, type=int)
|
|
1792
|
+
def standalone(title: str | None, points: int | None):
|
|
1793
|
+
"""Wrap current changes into a standalone Jira story, branch, PR, and merge.
|
|
1794
|
+
|
|
1795
|
+
This is an agent-executed workflow. Use /standalone to run it interactively.
|
|
1796
|
+
"""
|
|
1797
|
+
click.echo("The standalone command is an agent-executed workflow.")
|
|
1798
|
+
click.echo("Use /standalone to run it interactively with full agent support.")
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
# --- Backwards compatibility aliases (hidden) ---
|
|
1802
|
+
|
|
1803
|
+
# Hidden alias: sprint story-add -> sprint story add
|
|
1804
|
+
sprint.add_command(story_add_command, "story-add")
|
|
1805
|
+
sprint.commands["story-add"].hidden = True
|
|
1806
|
+
|
|
1807
|
+
# Hidden alias: sprint story-update -> sprint story update
|
|
1808
|
+
sprint.add_command(story_update_command, "story-update")
|
|
1809
|
+
sprint.commands["story-update"].hidden = True
|
|
1810
|
+
|
|
1811
|
+
# Hidden alias: sprint archive-epic -> sprint epic archive
|
|
1812
|
+
@sprint.command("archive-epic", hidden=True)
|
|
1813
|
+
@click.argument("epic_id", required=False)
|
|
1814
|
+
@click.option("--dry-run", is_flag=True)
|
|
1815
|
+
@click.option("--jira", is_flag=True)
|
|
1816
|
+
def archive_epic_compat(epic_id, dry_run, jira):
|
|
1817
|
+
"""(Deprecated) Use 'sprint epic archive' instead."""
|
|
1818
|
+
ctx = click.get_current_context()
|
|
1819
|
+
ctx.invoke(epic_archive, epic_id=epic_id, dry_run=dry_run, jira=jira)
|
|
1820
|
+
|
|
1821
|
+
# Hidden alias: sprint import-epic -> sprint epic import
|
|
1822
|
+
@sprint.command("import-epic", hidden=True)
|
|
1823
|
+
@click.argument("epics_file")
|
|
1824
|
+
@click.argument("initiative_name", required=False)
|
|
1825
|
+
@click.option("--marker", default="imported")
|
|
1826
|
+
@click.option("--dry-run", is_flag=True)
|
|
1827
|
+
def import_epic_compat(epics_file, initiative_name, marker, dry_run):
|
|
1828
|
+
"""(Deprecated) Use 'sprint epic import' instead."""
|
|
1829
|
+
ctx = click.get_current_context()
|
|
1830
|
+
ctx.invoke(epic_import, epics_file=epics_file, initiative_name=initiative_name, marker=marker, dry_run=dry_run)
|
|
1831
|
+
|
|
1832
|
+
# Hidden alias: sprint remove-epic -> sprint epic remove
|
|
1833
|
+
@sprint.command("remove-epic", hidden=True)
|
|
1834
|
+
@click.argument("epic_id")
|
|
1835
|
+
@click.option("--dry-run", is_flag=True)
|
|
1836
|
+
def remove_epic_compat(epic_id, dry_run):
|
|
1837
|
+
"""(Deprecated) Use 'sprint epic remove' instead."""
|
|
1838
|
+
ctx = click.get_current_context()
|
|
1839
|
+
ctx.invoke(epic_remove, epic_id=epic_id, dry_run=dry_run)
|
|
1840
|
+
|
|
1841
|
+
# Hidden alias: sprint epic-add -> sprint epic add
|
|
1842
|
+
sprint.add_command(epic_add_command, "epic-add")
|
|
1843
|
+
sprint.commands["epic-add"].hidden = True
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
# Register validate command from validate_cmd module
|
|
1847
|
+
from pennyfarthing_scripts.sprint.validate_cmd import validate_command
|
|
1848
|
+
|
|
1849
|
+
sprint.add_command(validate_command)
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
# For backwards compatibility when running as module
|
|
1853
|
+
def main(args: list[str] | None = None) -> int:
|
|
1854
|
+
"""Entry point for backwards compatibility."""
|
|
1855
|
+
try:
|
|
1856
|
+
sprint(args)
|
|
1857
|
+
return 0
|
|
1858
|
+
except SystemExit as e:
|
|
1859
|
+
return e.code if isinstance(e.code, int) else 0
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
if __name__ == "__main__":
|
|
1863
|
+
sprint()
|