@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,350 @@
1
+ """
2
+ Jira sync for Pennyfarthing epics.
3
+
4
+ Syncs epic stories to Jira with async parallel API calls.
5
+ Ported from jira-sync.mjs for Python parallelism.
6
+
7
+ Usage:
8
+ python -m pennyfarthing_scripts.jira sync <epic_number> [--dry-run] [--transition] [--points]
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import asyncio
15
+ import sys
16
+ from dataclasses import dataclass, field
17
+ from typing import Any
18
+
19
+ from pennyfarthing_scripts.common.config import get_project_root
20
+ from pennyfarthing_scripts.jira.client import (
21
+ JiraClient,
22
+ extract_jira_key,
23
+ get_jira_field,
24
+ map_status_to_jira,
25
+ )
26
+ from pennyfarthing_scripts.common.output import error, info, success, warn
27
+ from pennyfarthing_scripts.sprint.loader import find_epic, load_sprint
28
+
29
+
30
+ @dataclass
31
+ class SyncResult:
32
+ """Result of syncing a single story."""
33
+
34
+ story_id: str
35
+ success: bool
36
+ skipped: bool
37
+ error: str | None
38
+ actions: list[str] = field(default_factory=list)
39
+ dry_run: bool = False
40
+
41
+
42
+ # Module-level client for async operations
43
+ _client: JiraClient | None = None
44
+
45
+
46
+ def _get_client() -> JiraClient:
47
+ """Get or create the module's JiraClient instance."""
48
+ global _client
49
+ if _client is None:
50
+ _client = JiraClient()
51
+ return _client
52
+
53
+
54
+ def format_story_line(story: dict[str, Any]) -> str:
55
+ """Format a story for display.
56
+
57
+ Args:
58
+ story: Story dict from sprint YAML
59
+
60
+ Returns:
61
+ Formatted string for display
62
+ """
63
+ story_id = story.get("id", "?")
64
+ title = story.get("title", "Untitled")
65
+ status = story.get("status", "backlog")
66
+ return f"Story {story_id}: {title} [{status}]"
67
+
68
+
69
+ def format_summary(synced: int, skipped: int, errors: int) -> str:
70
+ """Format sync summary.
71
+
72
+ Args:
73
+ synced: Number of stories synced
74
+ skipped: Number of stories skipped
75
+ errors: Number of errors
76
+
77
+ Returns:
78
+ Formatted summary string
79
+ """
80
+ return f"Summary: {synced} synced, {skipped} skipped, {errors} errors"
81
+
82
+
83
+ async def sync_story(
84
+ story: dict[str, Any],
85
+ dry_run: bool = False,
86
+ do_transition: bool = False,
87
+ sync_points: bool = False,
88
+ ) -> SyncResult:
89
+ """Sync a single story to Jira.
90
+
91
+ Args:
92
+ story: Story dict from sprint YAML
93
+ dry_run: If True, don't make changes
94
+ do_transition: If True, transition status
95
+ sync_points: If True, sync story points
96
+
97
+ Returns:
98
+ SyncResult with outcome
99
+ """
100
+ story_id = story.get("id", "?")
101
+ jira_key = story.get("jira")
102
+
103
+ # Skip if no Jira key
104
+ if not jira_key or jira_key == "null":
105
+ return SyncResult(
106
+ story_id=story_id,
107
+ success=False,
108
+ skipped=True,
109
+ error=None,
110
+ actions=[],
111
+ dry_run=dry_run,
112
+ )
113
+
114
+ jira_key = extract_jira_key(jira_key)
115
+ actions: list[str] = []
116
+
117
+ # Dry run mode
118
+ if dry_run:
119
+ if do_transition:
120
+ actions.append("transition")
121
+ if sync_points:
122
+ actions.append("sync_points")
123
+ return SyncResult(
124
+ story_id=story_id,
125
+ success=True,
126
+ skipped=False,
127
+ error=None,
128
+ actions=actions,
129
+ dry_run=True,
130
+ )
131
+
132
+ # Get client and fetch current Jira state
133
+ client = _get_client()
134
+ issue_json = await client.get_issue_async(jira_key)
135
+ if not issue_json:
136
+ return SyncResult(
137
+ story_id=story_id,
138
+ success=False,
139
+ skipped=False,
140
+ error=f"Could not fetch {jira_key}",
141
+ actions=[],
142
+ )
143
+
144
+ current_status = get_jira_field(issue_json, "fields.status.name", "Unknown")
145
+ target_status = map_status_to_jira(story.get("status"))
146
+
147
+ # Transition if requested and needed
148
+ if do_transition and current_status != target_status:
149
+ result = await client.transition_async(jira_key, target_status)
150
+ if result.get("success"):
151
+ actions.append(f"transitioned: {current_status} -> {target_status}")
152
+ else:
153
+ actions.append(f"transition failed: {result.get('reason')}")
154
+
155
+ # Sync points if requested
156
+ story_points = story.get("points")
157
+ if sync_points and story_points:
158
+ current_points = get_jira_field(issue_json, "fields.customfield_10031")
159
+ current_points_int = int(current_points) if current_points else None
160
+
161
+ result = await client.sync_story_points_async(
162
+ jira_key, int(story_points), current_points_int
163
+ )
164
+ if result.get("success"):
165
+ if result.get("already_synced"):
166
+ actions.append(f"points already synced: {story_points}")
167
+ else:
168
+ actions.append(f"synced points: {story_points}")
169
+ else:
170
+ actions.append(f"points sync failed: {result.get('reason')}")
171
+
172
+ return SyncResult(
173
+ story_id=story_id,
174
+ success=True,
175
+ skipped=False,
176
+ error=None,
177
+ actions=actions,
178
+ )
179
+
180
+
181
+ async def sync_epic(
182
+ epic: dict[str, Any],
183
+ dry_run: bool = False,
184
+ do_transition: bool = False,
185
+ sync_points: bool = False,
186
+ ) -> dict[str, Any]:
187
+ """Sync all stories in an epic to Jira.
188
+
189
+ Args:
190
+ epic: Epic dict from sprint YAML
191
+ dry_run: If True, don't make changes
192
+ do_transition: If True, transition status
193
+ sync_points: If True, sync story points
194
+
195
+ Returns:
196
+ Summary dict with counts
197
+ """
198
+ stories = epic.get("stories", [])
199
+
200
+ # Process all stories in parallel
201
+ tasks = [
202
+ sync_story(story, dry_run=dry_run, do_transition=do_transition, sync_points=sync_points)
203
+ for story in stories
204
+ ]
205
+ results = await asyncio.gather(*tasks)
206
+
207
+ # Count results
208
+ synced = sum(1 for r in results if r.success and not r.skipped)
209
+ skipped = sum(1 for r in results if r.skipped)
210
+ errors = sum(1 for r in results if not r.success and not r.skipped)
211
+
212
+ return {
213
+ "total": len(stories),
214
+ "synced": synced,
215
+ "skipped": skipped,
216
+ "errors": errors,
217
+ "results": results,
218
+ }
219
+
220
+
221
+ def parse_args(args: list[str] | None = None) -> argparse.Namespace:
222
+ """Parse command line arguments.
223
+
224
+ Args:
225
+ args: Arguments to parse (defaults to sys.argv[1:])
226
+
227
+ Returns:
228
+ Parsed arguments namespace
229
+ """
230
+ parser = argparse.ArgumentParser(
231
+ description="Sync Pennyfarthing epic to Jira",
232
+ formatter_class=argparse.RawDescriptionHelpFormatter,
233
+ epilog="""
234
+ Examples:
235
+ jira sync 35 Show sync status for epic 35
236
+ jira sync 35 --dry-run Show what would be done
237
+ jira sync 35 --transition Sync status to Jira
238
+ jira sync 35 --transition --points Sync status and story points
239
+ """,
240
+ )
241
+ parser.add_argument("epic", help="Epic number (e.g., '35' or 'epic-35')")
242
+ parser.add_argument(
243
+ "--dry-run", action="store_true", help="Show what would be done without making changes"
244
+ )
245
+ parser.add_argument(
246
+ "--transition", action="store_true", help="Transition Jira issues to match status"
247
+ )
248
+ parser.add_argument(
249
+ "--points", action="store_true", help="Sync story points from Pennyfarthing to Jira"
250
+ )
251
+
252
+ return parser.parse_args(args)
253
+
254
+
255
+ async def async_main(args: argparse.Namespace) -> int:
256
+ """Async main entry point.
257
+
258
+ Args:
259
+ args: Parsed arguments
260
+
261
+ Returns:
262
+ Exit code
263
+ """
264
+ # Find project root and load sprint
265
+ try:
266
+ project_root = get_project_root()
267
+ except FileNotFoundError as e:
268
+ error(str(e))
269
+ return 1
270
+
271
+ sprint_data = load_sprint(project_root)
272
+ if not sprint_data:
273
+ error("Could not load sprint data")
274
+ return 1
275
+
276
+ # Find epic
277
+ epic = find_epic(sprint_data, args.epic)
278
+ if not epic:
279
+ error(f"Epic {args.epic} not found in sprint file")
280
+ return 1
281
+
282
+ # Display header
283
+ print("", file=sys.stderr)
284
+ info("==========================================")
285
+ info(f"Epic {args.epic}: {epic.get('title', 'Untitled')}")
286
+ if epic.get("jira"):
287
+ info(f"Jira: {epic.get('jira')}")
288
+ info("==========================================")
289
+ print("", file=sys.stderr)
290
+
291
+ if args.dry_run:
292
+ warn("[DRY-RUN MODE] No changes will be made")
293
+ print("", file=sys.stderr)
294
+
295
+ stories = epic.get("stories", [])
296
+ if not stories:
297
+ warn(f"No stories found in epic {args.epic}")
298
+ return 0
299
+
300
+ info(f"Found {len(stories)} stories to process")
301
+ print("", file=sys.stderr)
302
+
303
+ # Sync epic
304
+ result = await sync_epic(
305
+ epic,
306
+ dry_run=args.dry_run,
307
+ do_transition=args.transition,
308
+ sync_points=args.points,
309
+ )
310
+
311
+ # Display results
312
+ for sync_result in result["results"]:
313
+ story = next((s for s in stories if s.get("id") == sync_result.story_id), {})
314
+ print("---", file=sys.stderr)
315
+ info(format_story_line(story))
316
+ if sync_result.skipped:
317
+ warn(" Not synced to Jira - skipping")
318
+ elif sync_result.error:
319
+ error(f" {sync_result.error}")
320
+ elif sync_result.dry_run:
321
+ actions_str = ", ".join(sync_result.actions) if sync_result.actions else "view only"
322
+ warn(f" [DRY-RUN] Would sync: {actions_str}")
323
+ else:
324
+ for action in sync_result.actions:
325
+ success(f" {action}")
326
+
327
+ # Summary
328
+ print("", file=sys.stderr)
329
+ print("==========================================", file=sys.stderr)
330
+ success(format_summary(result["synced"], result["skipped"], result["errors"]))
331
+ print("==========================================", file=sys.stderr)
332
+
333
+ return 1 if result["errors"] > 0 else 0
334
+
335
+
336
+ def main(args: list[str] | None = None) -> int:
337
+ """Main entry point.
338
+
339
+ Args:
340
+ args: Command line arguments (defaults to sys.argv[1:])
341
+
342
+ Returns:
343
+ Exit code
344
+ """
345
+ parsed_args = parse_args(args)
346
+ return asyncio.run(async_main(parsed_args))
347
+
348
+
349
+ if __name__ == "__main__":
350
+ sys.exit(main())
@@ -0,0 +1,37 @@
1
+ """
2
+ Bidirectional sync between sprint YAML and Jira.
3
+
4
+ This module re-exports from pennyfarthing_scripts.jira.bidirectional for
5
+ backwards compatibility. New code should import from jira.bidirectional directly.
6
+
7
+ Usage: python -m pennyfarthing_scripts.jira_bidirectional_sync [options]
8
+ """
9
+
10
+ # Re-export everything from the new location
11
+ from pennyfarthing_scripts.jira.bidirectional import (
12
+ SyncChange,
13
+ SyncPlan,
14
+ SyncResult,
15
+ async_main,
16
+ execute_sync_plan,
17
+ format_sync_plan,
18
+ generate_sync_plan,
19
+ main,
20
+ parse_cli_args,
21
+ )
22
+
23
+ __all__ = [
24
+ "SyncChange",
25
+ "SyncPlan",
26
+ "SyncResult",
27
+ "async_main",
28
+ "execute_sync_plan",
29
+ "format_sync_plan",
30
+ "generate_sync_plan",
31
+ "main",
32
+ "parse_cli_args",
33
+ ]
34
+
35
+ if __name__ == "__main__":
36
+ import sys
37
+ sys.exit(main())
@@ -0,0 +1,30 @@
1
+ """
2
+ Create Jira epics from Pennyfarthing sprint YAML definitions.
3
+
4
+ This module re-exports from pennyfarthing_scripts.jira.epic for
5
+ backwards compatibility. New code should import from jira.epic directly.
6
+
7
+ Usage:
8
+ python -m pennyfarthing_scripts.jira_epic_creation <epic_id> [options]
9
+ """
10
+
11
+ # Re-export everything from the new location
12
+ from pennyfarthing_scripts.jira.epic import (
13
+ build_epic_payload,
14
+ create_epic,
15
+ create_epic_from_yaml,
16
+ main,
17
+ parse_args,
18
+ )
19
+
20
+ __all__ = [
21
+ "build_epic_payload",
22
+ "create_epic",
23
+ "create_epic_from_yaml",
24
+ "main",
25
+ "parse_args",
26
+ ]
27
+
28
+ if __name__ == "__main__":
29
+ import sys
30
+ sys.exit(main())
@@ -0,0 +1,36 @@
1
+ """
2
+ Jira sync for Pennyfarthing epics.
3
+
4
+ This module re-exports from pennyfarthing_scripts.jira.sync for
5
+ backwards compatibility. New code should import from jira.sync directly.
6
+
7
+ Usage:
8
+ python -m pennyfarthing_scripts.jira_sync <epic_number> [--dry-run] [--transition] [--points]
9
+ """
10
+
11
+ # Re-export everything from the new location
12
+ from pennyfarthing_scripts.jira.sync import (
13
+ SyncResult,
14
+ async_main,
15
+ format_story_line,
16
+ format_summary,
17
+ main,
18
+ parse_args,
19
+ sync_epic,
20
+ sync_story,
21
+ )
22
+
23
+ __all__ = [
24
+ "SyncResult",
25
+ "async_main",
26
+ "format_story_line",
27
+ "format_summary",
28
+ "main",
29
+ "parse_args",
30
+ "sync_epic",
31
+ "sync_story",
32
+ ]
33
+
34
+ if __name__ == "__main__":
35
+ import sys
36
+ sys.exit(main())
@@ -0,0 +1,30 @@
1
+ """
2
+ Sync a single story between Pennyfarthing sprint YAML and Jira.
3
+
4
+ This module re-exports from pennyfarthing_scripts.jira.story for
5
+ backwards compatibility. New code should import from jira.story directly.
6
+
7
+ Usage:
8
+ python -m pennyfarthing_scripts.jira_sync_story <story_key> [options]
9
+ """
10
+
11
+ # Re-export everything from the new location
12
+ from pennyfarthing_scripts.jira.story import (
13
+ fetch_jira_issue,
14
+ get_story_from_sprint,
15
+ main,
16
+ parse_args,
17
+ sync_story,
18
+ )
19
+
20
+ __all__ = [
21
+ "fetch_jira_issue",
22
+ "get_story_from_sprint",
23
+ "main",
24
+ "parse_args",
25
+ "sync_story",
26
+ ]
27
+
28
+ if __name__ == "__main__":
29
+ import sys
30
+ sys.exit(main())
@@ -0,0 +1,37 @@
1
+ """
2
+ Console output utilities for Pennyfarthing scripts.
3
+
4
+ This module re-exports from pennyfarthing_scripts.common.output for
5
+ backwards compatibility. New code should import from common.output directly.
6
+ """
7
+
8
+ # Re-export everything from the new location
9
+ from pennyfarthing_scripts.common.output import (
10
+ Colors,
11
+ _colorize,
12
+ _supports_color,
13
+ bold,
14
+ debug,
15
+ dim,
16
+ divider,
17
+ error,
18
+ header,
19
+ info,
20
+ success,
21
+ warn,
22
+ )
23
+
24
+ __all__ = [
25
+ "Colors",
26
+ "_colorize",
27
+ "_supports_color",
28
+ "bold",
29
+ "debug",
30
+ "dim",
31
+ "divider",
32
+ "error",
33
+ "header",
34
+ "info",
35
+ "success",
36
+ "warn",
37
+ ]
@@ -0,0 +1,17 @@
1
+ """Preflight checks for Pennyfarthing workflows.
2
+
3
+ This package provides async preflight checks that run in parallel
4
+ to validate story completion before finishing.
5
+ """
6
+
7
+ from pennyfarthing_scripts.preflight.finish import (
8
+ PreflightResult,
9
+ PreflightIssue,
10
+ run_finish_preflight,
11
+ )
12
+
13
+ __all__ = [
14
+ "PreflightResult",
15
+ "PreflightIssue",
16
+ "run_finish_preflight",
17
+ ]
@@ -0,0 +1,10 @@
1
+ """
2
+ Entry point for python -m pennyfarthing_scripts.preflight
3
+ """
4
+
5
+ import sys
6
+
7
+ from pennyfarthing_scripts.preflight.cli import cli
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(cli())
@@ -0,0 +1,141 @@
1
+ """
2
+ Preflight CLI - Fan-out CLI for preflight checks.
3
+
4
+ Usage:
5
+ python -m pennyfarthing_scripts.preflight <subcommand> [args]
6
+
7
+ Subcommands:
8
+ finish Run finish preflight checks
9
+ """
10
+
11
+ import argparse
12
+ import asyncio
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from pennyfarthing_scripts.preflight.finish import run_finish_preflight
18
+
19
+
20
+ def finish(args: list[str]) -> int:
21
+ """Run finish preflight checks."""
22
+ parser = argparse.ArgumentParser(
23
+ prog="preflight finish",
24
+ description="Run finish preflight checks in parallel",
25
+ )
26
+ parser.add_argument(
27
+ "story_id",
28
+ help="Story identifier (e.g., 31-10)",
29
+ )
30
+ parser.add_argument(
31
+ "--branch", "-b",
32
+ required=True,
33
+ help="Feature branch name",
34
+ )
35
+ parser.add_argument(
36
+ "--jira", "-j",
37
+ help="Jira issue key (optional, skips Jira checks if absent)",
38
+ )
39
+ parser.add_argument(
40
+ "--repo", "-r",
41
+ help="Repository name for PR lookup (optional)",
42
+ )
43
+ parser.add_argument(
44
+ "--project-root", "-p",
45
+ type=Path,
46
+ help="Project root path (defaults to cwd)",
47
+ )
48
+ parser.add_argument(
49
+ "--format", "-f",
50
+ choices=["json", "yaml"],
51
+ default="json",
52
+ help="Output format (default: json)",
53
+ )
54
+
55
+ parsed = parser.parse_args(args)
56
+
57
+ # Run async preflight
58
+ result = asyncio.run(run_finish_preflight(
59
+ story_id=parsed.story_id,
60
+ branch=parsed.branch,
61
+ jira_key=parsed.jira,
62
+ repo=parsed.repo,
63
+ project_root=parsed.project_root,
64
+ ))
65
+
66
+ # Output result
67
+ result_dict = result.to_dict()
68
+
69
+ if parsed.format == "yaml":
70
+ try:
71
+ import yaml
72
+ print(yaml.dump(result_dict, default_flow_style=False, sort_keys=False))
73
+ except ImportError:
74
+ print("YAML output requires PyYAML. Falling back to JSON.", file=sys.stderr)
75
+ print(json.dumps(result_dict, indent=2))
76
+ else:
77
+ print(json.dumps(result_dict, indent=2))
78
+
79
+ # Return non-zero if not ready to finish
80
+ return 0 if result.ready_to_finish else 1
81
+
82
+
83
+ # Subcommand registry
84
+ SUBCOMMANDS = {
85
+ "finish": finish,
86
+ }
87
+
88
+
89
+ def cli(args: list[str] | None = None) -> int:
90
+ """Main CLI entry point."""
91
+ if args is None:
92
+ args = sys.argv[1:]
93
+
94
+ parser = argparse.ArgumentParser(
95
+ prog="preflight",
96
+ description="Preflight checks for Pennyfarthing workflows",
97
+ formatter_class=argparse.RawDescriptionHelpFormatter,
98
+ epilog="""
99
+ Subcommands:
100
+ finish <story_id> --branch <branch> [--jira <key>]
101
+ Run finish preflight checks in parallel
102
+
103
+ Examples:
104
+ preflight finish 31-10 --branch feat/31-10-feature --jira MSSCI-12345
105
+ preflight finish 63-9 -b feat/63-9-fanout -j MSSCI-12413 -f yaml
106
+ """,
107
+ )
108
+
109
+ parser.add_argument(
110
+ "subcommand",
111
+ nargs="?",
112
+ choices=list(SUBCOMMANDS.keys()),
113
+ help="Subcommand to run",
114
+ )
115
+ parser.add_argument(
116
+ "args",
117
+ nargs=argparse.REMAINDER,
118
+ help="Arguments for subcommand",
119
+ )
120
+
121
+ parsed = parser.parse_args(args)
122
+
123
+ if not parsed.subcommand:
124
+ parser.print_help()
125
+ return 0
126
+
127
+ handler = SUBCOMMANDS.get(parsed.subcommand)
128
+ if handler:
129
+ return handler(parsed.args)
130
+ else:
131
+ print(f"Unknown subcommand: {parsed.subcommand}", file=sys.stderr)
132
+ return 1
133
+
134
+
135
+ def main(args: list[str] | None = None) -> int:
136
+ """Alias for cli()."""
137
+ return cli(args)
138
+
139
+
140
+ if __name__ == "__main__":
141
+ sys.exit(cli())