@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.
Files changed (178) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/init.js +7 -6
  5. package/packages/core/dist/cli/commands/init.js.map +1 -1
  6. package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
  7. package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
  8. package/packages/core/dist/cli/utils/symlinks.js +25 -0
  9. package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
  10. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  11. package/pennyfarthing-dist/scripts/core/prime.sh +23 -0
  12. package/pennyfarthing-dist/scripts/core/run.sh +5 -5
  13. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
  14. package/pennyfarthing-dist/scripts/git/release.sh +2 -2
  15. package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
  16. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
  17. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
  18. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
  19. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
  20. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
  21. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
  22. package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
  23. package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
  24. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
  25. package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
  26. package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
  27. package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
  28. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
  29. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
  30. package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
  31. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
  32. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +1 -1
  33. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
  34. package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
  35. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
  36. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
  37. package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
  38. package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
  39. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
  40. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
  41. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
  42. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
  43. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
  44. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
  45. package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
  46. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
  47. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
  48. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
  49. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
  50. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
  51. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
  52. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
  53. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
  54. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
  55. package/pennyfarthing_scripts/__init__.py +17 -0
  56. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  57. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  58. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  59. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  60. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  61. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  62. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  63. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  64. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  65. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  66. package/pennyfarthing_scripts/bellmode_hook.py +154 -0
  67. package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
  68. package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
  69. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  70. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  71. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  72. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  73. package/pennyfarthing_scripts/brownfield/cli.py +131 -0
  74. package/pennyfarthing_scripts/brownfield/discover.py +753 -0
  75. package/pennyfarthing_scripts/common/__init__.py +49 -0
  76. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  77. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  78. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  79. package/pennyfarthing_scripts/common/config.py +65 -0
  80. package/pennyfarthing_scripts/common/output.py +180 -0
  81. package/pennyfarthing_scripts/config.py +21 -0
  82. package/pennyfarthing_scripts/git/__init__.py +29 -0
  83. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  84. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  85. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  86. package/pennyfarthing_scripts/git/create_branches.py +439 -0
  87. package/pennyfarthing_scripts/git/status_all.py +310 -0
  88. package/pennyfarthing_scripts/hooks.py +455 -0
  89. package/pennyfarthing_scripts/jira/__init__.py +93 -0
  90. package/pennyfarthing_scripts/jira/__main__.py +10 -0
  91. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  92. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  94. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  95. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  96. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  97. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  98. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  99. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  100. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  101. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  102. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  103. package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
  104. package/pennyfarthing_scripts/jira/claim.py +211 -0
  105. package/pennyfarthing_scripts/jira/cli.py +150 -0
  106. package/pennyfarthing_scripts/jira/client.py +613 -0
  107. package/pennyfarthing_scripts/jira/epic.py +176 -0
  108. package/pennyfarthing_scripts/jira/story.py +219 -0
  109. package/pennyfarthing_scripts/jira/sync.py +350 -0
  110. package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
  111. package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
  112. package/pennyfarthing_scripts/jira_sync.py +36 -0
  113. package/pennyfarthing_scripts/jira_sync_story.py +30 -0
  114. package/pennyfarthing_scripts/output.py +37 -0
  115. package/pennyfarthing_scripts/preflight/__init__.py +17 -0
  116. package/pennyfarthing_scripts/preflight/__main__.py +10 -0
  117. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/preflight/cli.py +141 -0
  122. package/pennyfarthing_scripts/preflight/finish.py +382 -0
  123. package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
  124. package/pennyfarthing_scripts/prime/__init__.py +38 -0
  125. package/pennyfarthing_scripts/prime/__main__.py +8 -0
  126. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/prime/cli.py +220 -0
  135. package/pennyfarthing_scripts/prime/loader.py +239 -0
  136. package/pennyfarthing_scripts/sprint/__init__.py +66 -0
  137. package/pennyfarthing_scripts/sprint/__main__.py +10 -0
  138. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  145. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  146. package/pennyfarthing_scripts/sprint/archive.py +108 -0
  147. package/pennyfarthing_scripts/sprint/cli.py +124 -0
  148. package/pennyfarthing_scripts/sprint/loader.py +193 -0
  149. package/pennyfarthing_scripts/sprint/status.py +122 -0
  150. package/pennyfarthing_scripts/sprint/validator.py +405 -0
  151. package/pennyfarthing_scripts/sprint/work.py +192 -0
  152. package/pennyfarthing_scripts/story/__init__.py +67 -0
  153. package/pennyfarthing_scripts/story/__main__.py +10 -0
  154. package/pennyfarthing_scripts/story/cli.py +105 -0
  155. package/pennyfarthing_scripts/story/create.py +167 -0
  156. package/pennyfarthing_scripts/story/size.py +113 -0
  157. package/pennyfarthing_scripts/story/template.py +151 -0
  158. package/pennyfarthing_scripts/swebench.py +216 -0
  159. package/pennyfarthing_scripts/tests/__init__.py +1 -0
  160. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  162. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  163. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  164. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  165. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  166. package/pennyfarthing_scripts/tests/conftest.py +106 -0
  167. package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
  168. package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
  169. package/pennyfarthing_scripts/tests/test_common.py +180 -0
  170. package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
  171. package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
  172. package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
  173. package/pennyfarthing_scripts/tests/test_prime.py +397 -0
  174. package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
  175. package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
  176. package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
  177. package/pennyfarthing_scripts/welcome_hook.py +157 -0
  178. 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())