@pennyfarthing/core 7.8.1 → 7.8.4
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 +1 -1
- package/package.json +2 -1
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +7 -6
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
- package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +25 -0
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
- package/pennyfarthing-dist/scripts/core/prime.sh +23 -0
- package/pennyfarthing-dist/scripts/core/run.sh +5 -5
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
- package/pennyfarthing-dist/scripts/git/release.sh +2 -2
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
- package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
- package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
- package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
- 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__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.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__/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/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/config.py +65 -0
- package/pennyfarthing_scripts/common/output.py +180 -0
- package/pennyfarthing_scripts/config.py +21 -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.py +455 -0
- package/pennyfarthing_scripts/jira/__init__.py +93 -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__/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__/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 +150 -0
- package/pennyfarthing_scripts/jira/client.py +613 -0
- package/pennyfarthing_scripts/jira/epic.py +176 -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/output.py +37 -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 +142 -0
- package/pennyfarthing_scripts/prime/__init__.py +38 -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__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +220 -0
- package/pennyfarthing_scripts/prime/loader.py +239 -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__/cli.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__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +108 -0
- package/pennyfarthing_scripts/sprint/cli.py +124 -0
- package/pennyfarthing_scripts/sprint/loader.py +193 -0
- package/pennyfarthing_scripts/sprint/status.py +122 -0
- package/pennyfarthing_scripts/sprint/validator.py +405 -0
- package/pennyfarthing_scripts/sprint/work.py +192 -0
- package/pennyfarthing_scripts/story/__init__.py +67 -0
- package/pennyfarthing_scripts/story/__main__.py +10 -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_git_utils.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_validator.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_prime.py +397 -0
- package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
- package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
- package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
- package/pennyfarthing_scripts/welcome_hook.py +157 -0
- package/pennyfarthing_scripts/workflow.py +183 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint CLI - Fan-out CLI for sprint operations.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m pennyfarthing_scripts.sprint <subcommand> [args]
|
|
6
|
+
|
|
7
|
+
Subcommands:
|
|
8
|
+
status Show sprint status
|
|
9
|
+
backlog Show available stories
|
|
10
|
+
work Start work on a story
|
|
11
|
+
archive Archive a completed story
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def status(args: list[str]) -> int:
|
|
19
|
+
"""Show sprint status."""
|
|
20
|
+
from pennyfarthing_scripts.sprint.status import main as status_main
|
|
21
|
+
return status_main(args)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def backlog(args: list[str]) -> int:
|
|
25
|
+
"""Show available stories."""
|
|
26
|
+
from pennyfarthing_scripts.sprint.loader import get_stories_by_status
|
|
27
|
+
|
|
28
|
+
stories = get_stories_by_status("backlog")
|
|
29
|
+
print(f"Backlog: {len(stories)} stories")
|
|
30
|
+
print("")
|
|
31
|
+
for story in stories:
|
|
32
|
+
priority = story.get("priority", "P2")
|
|
33
|
+
points = story.get("points", "?")
|
|
34
|
+
print(f" [{priority}] {story.get('id')}: {story.get('title')} [{points}pts]")
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def work(args: list[str]) -> int:
|
|
39
|
+
"""Start work on a story."""
|
|
40
|
+
from pennyfarthing_scripts.sprint.work import main as work_main
|
|
41
|
+
return work_main(args)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def archive(args: list[str]) -> int:
|
|
45
|
+
"""Archive a completed story."""
|
|
46
|
+
from pennyfarthing_scripts.sprint.archive import main as archive_main
|
|
47
|
+
return archive_main(args)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Subcommand registry
|
|
51
|
+
SUBCOMMANDS = {
|
|
52
|
+
"status": status,
|
|
53
|
+
"backlog": backlog,
|
|
54
|
+
"work": work,
|
|
55
|
+
"archive": archive,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cli(args: list[str] | None = None) -> int:
|
|
60
|
+
"""Main CLI entry point.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
args: Command line arguments (defaults to sys.argv[1:])
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Exit code
|
|
67
|
+
"""
|
|
68
|
+
if args is None:
|
|
69
|
+
args = sys.argv[1:]
|
|
70
|
+
|
|
71
|
+
parser = argparse.ArgumentParser(
|
|
72
|
+
prog="sprint",
|
|
73
|
+
description="Sprint CLI for Pennyfarthing",
|
|
74
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
75
|
+
epilog="""
|
|
76
|
+
Subcommands:
|
|
77
|
+
status [filter] Show sprint status (optional filter: backlog, in-progress, done)
|
|
78
|
+
backlog Show available stories
|
|
79
|
+
work [story-id] Start work on a story (or show available)
|
|
80
|
+
archive <id> [pr] Archive a completed story
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
sprint status
|
|
84
|
+
sprint status in-progress
|
|
85
|
+
sprint backlog
|
|
86
|
+
sprint work MSSCI-12345
|
|
87
|
+
sprint work next
|
|
88
|
+
sprint archive 63-7 489
|
|
89
|
+
""",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"subcommand",
|
|
94
|
+
nargs="?",
|
|
95
|
+
choices=list(SUBCOMMANDS.keys()),
|
|
96
|
+
help="Subcommand to run",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"args",
|
|
100
|
+
nargs=argparse.REMAINDER,
|
|
101
|
+
help="Arguments for subcommand",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
parsed = parser.parse_args(args)
|
|
105
|
+
|
|
106
|
+
if not parsed.subcommand:
|
|
107
|
+
# Default to status
|
|
108
|
+
return status([])
|
|
109
|
+
|
|
110
|
+
handler = SUBCOMMANDS.get(parsed.subcommand)
|
|
111
|
+
if handler:
|
|
112
|
+
return handler(parsed.args)
|
|
113
|
+
else:
|
|
114
|
+
print(f"Unknown subcommand: {parsed.subcommand}", file=sys.stderr)
|
|
115
|
+
return 1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main(args: list[str] | None = None) -> int:
|
|
119
|
+
"""Alias for cli()."""
|
|
120
|
+
return cli(args)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
sys.exit(cli())
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint YAML parsing utilities for Pennyfarthing scripts.
|
|
3
|
+
|
|
4
|
+
Provides access to sprint/current-sprint.yaml data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pennyfarthing_scripts.common.config import get_project_root, load_yaml_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_sprint(project_root: Path | None = None) -> dict[str, Any] | None:
|
|
14
|
+
"""Load sprint data from project root.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
project_root: Project root path (defaults to auto-detect)
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Sprint data as dict, or None if not found
|
|
21
|
+
"""
|
|
22
|
+
root = project_root or get_project_root()
|
|
23
|
+
sprint_path = root / "sprint" / "current-sprint.yaml"
|
|
24
|
+
return load_yaml_config(sprint_path)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_epic(sprint_data: dict[str, Any], epic_num: str) -> dict[str, Any] | None:
|
|
28
|
+
"""Find epic in sprint data (handles various ID formats).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
sprint_data: Sprint YAML data
|
|
32
|
+
epic_num: Epic number (e.g., "63", "epic-63", or "63")
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Epic dict if found, None otherwise
|
|
36
|
+
"""
|
|
37
|
+
if not sprint_data or "epics" not in sprint_data:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
# Normalize the epic number
|
|
41
|
+
epic_num_clean = epic_num.replace("epic-", "")
|
|
42
|
+
|
|
43
|
+
for epic in sprint_data["epics"]:
|
|
44
|
+
epic_id = str(epic.get("id", ""))
|
|
45
|
+
# Match "63", "epic-63", or just the number
|
|
46
|
+
if epic_id == epic_num or epic_id == f"epic-{epic_num}" or epic_id == epic_num_clean:
|
|
47
|
+
return epic
|
|
48
|
+
# Also try without prefix
|
|
49
|
+
if epic_id.replace("epic-", "") == epic_num_clean:
|
|
50
|
+
return epic
|
|
51
|
+
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Alias for backwards compatibility
|
|
56
|
+
load_current_sprint = load_sprint
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_sprint_info() -> dict[str, Any]:
|
|
60
|
+
"""Get sprint metadata (number, dates, status, etc).
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Sprint info dict with number, status, goal, etc.
|
|
64
|
+
"""
|
|
65
|
+
data = load_sprint()
|
|
66
|
+
if data and "sprint" in data:
|
|
67
|
+
return data["sprint"]
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_all_stories() -> list[dict[str, Any]]:
|
|
72
|
+
"""Get all stories from all epics.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Flat list of all story dicts
|
|
76
|
+
"""
|
|
77
|
+
data = load_sprint()
|
|
78
|
+
if not data or "epics" not in data:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
stories = []
|
|
82
|
+
for epic in data["epics"]:
|
|
83
|
+
if "stories" in epic:
|
|
84
|
+
stories.extend(epic["stories"])
|
|
85
|
+
return stories
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_story_by_id(story_id: str) -> dict[str, Any] | None:
|
|
89
|
+
"""Find a story by its ID.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
story_id: The story ID (e.g., "63-4" or "MSSCI-12398")
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Story dict if found, None otherwise
|
|
96
|
+
"""
|
|
97
|
+
for story in get_all_stories():
|
|
98
|
+
if story.get("id") == story_id:
|
|
99
|
+
return story
|
|
100
|
+
# Also check jira field
|
|
101
|
+
if story.get("jira") == story_id:
|
|
102
|
+
return story
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_stories_by_status(status: str) -> list[dict[str, Any]]:
|
|
107
|
+
"""Get all stories with a given status.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
status: Status to filter by (e.g., "backlog", "in_progress", "done")
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of stories matching the status
|
|
114
|
+
"""
|
|
115
|
+
return [s for s in get_all_stories() if s.get("status") == status]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_epic_by_id(epic_id: str) -> dict[str, Any] | None:
|
|
119
|
+
"""Find an epic by its ID.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
epic_id: The epic ID
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Epic dict if found, None otherwise
|
|
126
|
+
"""
|
|
127
|
+
data = load_sprint()
|
|
128
|
+
if not data or "epics" not in data:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
for epic in data["epics"]:
|
|
132
|
+
if epic.get("id") == epic_id:
|
|
133
|
+
return epic
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def find_story(epic: dict[str, Any] | None, story_id: str) -> dict[str, Any] | None:
|
|
138
|
+
"""Find a story within an epic by its ID.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
epic: Epic dict containing stories
|
|
142
|
+
story_id: The story ID (e.g., "63-7")
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Story dict if found, None otherwise
|
|
146
|
+
"""
|
|
147
|
+
if not epic or "stories" not in epic:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
for story in epic["stories"]:
|
|
151
|
+
if story.get("id") == story_id:
|
|
152
|
+
return story
|
|
153
|
+
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_story_field(
|
|
158
|
+
sprint_data: dict[str, Any], story_id: str, field_name: str
|
|
159
|
+
) -> Any | None:
|
|
160
|
+
"""Get a specific field from a story in sprint data.
|
|
161
|
+
|
|
162
|
+
Extracts the epic number from the story ID (e.g., "63-7" -> epic 63)
|
|
163
|
+
and looks up the story within that epic.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
sprint_data: Sprint YAML data
|
|
167
|
+
story_id: The story ID (e.g., "63-7")
|
|
168
|
+
field_name: The field to extract (e.g., "status", "points", "workflow")
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Field value if found, None otherwise
|
|
172
|
+
"""
|
|
173
|
+
if not sprint_data or not story_id:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
# Extract epic number from story ID (e.g., "63-7" -> "63")
|
|
177
|
+
parts = story_id.split("-")
|
|
178
|
+
if len(parts) < 2:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
epic_num = parts[0]
|
|
182
|
+
|
|
183
|
+
# Find the epic
|
|
184
|
+
epic = find_epic(sprint_data, epic_num)
|
|
185
|
+
if not epic:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
# Find the story within the epic
|
|
189
|
+
story = find_story(epic, story_id)
|
|
190
|
+
if not story:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
return story.get(field_name)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint status operations.
|
|
3
|
+
|
|
4
|
+
Provides functions for getting and displaying sprint status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pennyfarthing_scripts.sprint.loader import get_all_stories, get_sprint_info, load_sprint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_sprint_status(filter_status: str | None = None) -> dict[str, Any]:
|
|
13
|
+
"""Get sprint status with story counts.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
filter_status: Optional status to filter by
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Dict with sprint status information
|
|
20
|
+
"""
|
|
21
|
+
sprint_info = get_sprint_info()
|
|
22
|
+
stories = get_all_stories()
|
|
23
|
+
|
|
24
|
+
if not stories:
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
# Count by status
|
|
28
|
+
status_counts: dict[str, int] = {}
|
|
29
|
+
total_points = 0
|
|
30
|
+
completed_points = 0
|
|
31
|
+
|
|
32
|
+
for story in stories:
|
|
33
|
+
status = story.get("status", "backlog")
|
|
34
|
+
points = story.get("points", 0) or 0
|
|
35
|
+
|
|
36
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
37
|
+
total_points += points
|
|
38
|
+
|
|
39
|
+
if status in ("done", "completed"):
|
|
40
|
+
completed_points += points
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"sprint": sprint_info,
|
|
44
|
+
"total_stories": len(stories),
|
|
45
|
+
"by_status": status_counts,
|
|
46
|
+
"backlog": status_counts.get("backlog", 0),
|
|
47
|
+
"in_progress": status_counts.get("in_progress", 0),
|
|
48
|
+
"review": status_counts.get("review", 0),
|
|
49
|
+
"completed": status_counts.get("done", 0) + status_counts.get("completed", 0),
|
|
50
|
+
"total_points": total_points,
|
|
51
|
+
"completed_points": completed_points,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def format_status(status: dict[str, Any]) -> str:
|
|
56
|
+
"""Format sprint status as human-readable string.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
status: Status dict from get_sprint_status
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Formatted status string
|
|
63
|
+
"""
|
|
64
|
+
if not status:
|
|
65
|
+
return "No sprint data available"
|
|
66
|
+
|
|
67
|
+
lines = []
|
|
68
|
+
|
|
69
|
+
# Sprint header
|
|
70
|
+
sprint = status.get("sprint", {})
|
|
71
|
+
if sprint:
|
|
72
|
+
lines.append(f"Sprint: {sprint.get('name', 'Unknown')}")
|
|
73
|
+
lines.append(f"Status: {sprint.get('status', 'unknown')}")
|
|
74
|
+
if sprint.get("goal"):
|
|
75
|
+
lines.append(f"Goal: {sprint['goal']}")
|
|
76
|
+
lines.append("")
|
|
77
|
+
|
|
78
|
+
# Story counts
|
|
79
|
+
lines.append(f"Total Stories: {status.get('total_stories', 0)}")
|
|
80
|
+
lines.append(f" Backlog: {status.get('backlog', 0)}")
|
|
81
|
+
lines.append(f" In Progress: {status.get('in_progress', 0)}")
|
|
82
|
+
lines.append(f" In Review: {status.get('review', 0)}")
|
|
83
|
+
lines.append(f" Completed: {status.get('completed', 0)}")
|
|
84
|
+
lines.append("")
|
|
85
|
+
|
|
86
|
+
# Points
|
|
87
|
+
lines.append(f"Points: {status.get('completed_points', 0)}/{status.get('total_points', 0)}")
|
|
88
|
+
|
|
89
|
+
return "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main(args: list[str] | None = None) -> int:
|
|
93
|
+
"""CLI entry point for sprint status.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
args: Command line arguments
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Exit code
|
|
100
|
+
"""
|
|
101
|
+
import argparse
|
|
102
|
+
import sys
|
|
103
|
+
|
|
104
|
+
parser = argparse.ArgumentParser(description="Show sprint status")
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"filter",
|
|
107
|
+
nargs="?",
|
|
108
|
+
choices=["backlog", "todo", "in-progress", "review", "done"],
|
|
109
|
+
help="Filter by status",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
parsed = parser.parse_args(args)
|
|
113
|
+
|
|
114
|
+
status = get_sprint_status(parsed.filter)
|
|
115
|
+
print(format_status(status))
|
|
116
|
+
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
import sys
|
|
122
|
+
sys.exit(main())
|