@pennyfarthing/core 7.8.1 → 7.8.2

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 (126) hide show
  1. package/package.json +2 -1
  2. package/pennyfarthing-dist/scripts/core/prime.sh +8 -0
  3. package/pennyfarthing_scripts/__init__.py +17 -0
  4. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  6. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  7. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  8. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  9. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  10. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  11. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  12. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  13. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  14. package/pennyfarthing_scripts/bellmode_hook.py +154 -0
  15. package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
  16. package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
  17. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  18. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  19. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  20. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  21. package/pennyfarthing_scripts/brownfield/cli.py +131 -0
  22. package/pennyfarthing_scripts/brownfield/discover.py +753 -0
  23. package/pennyfarthing_scripts/common/__init__.py +49 -0
  24. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  25. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  26. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  27. package/pennyfarthing_scripts/common/config.py +65 -0
  28. package/pennyfarthing_scripts/common/output.py +180 -0
  29. package/pennyfarthing_scripts/config.py +21 -0
  30. package/pennyfarthing_scripts/git/__init__.py +29 -0
  31. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  32. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  33. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  34. package/pennyfarthing_scripts/git/create_branches.py +439 -0
  35. package/pennyfarthing_scripts/git/status_all.py +310 -0
  36. package/pennyfarthing_scripts/hooks.py +455 -0
  37. package/pennyfarthing_scripts/jira/__init__.py +93 -0
  38. package/pennyfarthing_scripts/jira/__main__.py +10 -0
  39. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  40. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  41. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  42. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  43. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  44. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  45. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  46. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  47. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  48. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  49. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  50. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  51. package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
  52. package/pennyfarthing_scripts/jira/claim.py +211 -0
  53. package/pennyfarthing_scripts/jira/cli.py +150 -0
  54. package/pennyfarthing_scripts/jira/client.py +613 -0
  55. package/pennyfarthing_scripts/jira/epic.py +176 -0
  56. package/pennyfarthing_scripts/jira/story.py +219 -0
  57. package/pennyfarthing_scripts/jira/sync.py +350 -0
  58. package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
  59. package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
  60. package/pennyfarthing_scripts/jira_sync.py +36 -0
  61. package/pennyfarthing_scripts/jira_sync_story.py +30 -0
  62. package/pennyfarthing_scripts/output.py +37 -0
  63. package/pennyfarthing_scripts/preflight/__init__.py +17 -0
  64. package/pennyfarthing_scripts/preflight/__main__.py +10 -0
  65. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  66. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  67. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  68. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  69. package/pennyfarthing_scripts/preflight/cli.py +141 -0
  70. package/pennyfarthing_scripts/preflight/finish.py +382 -0
  71. package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
  72. package/pennyfarthing_scripts/prime/__init__.py +38 -0
  73. package/pennyfarthing_scripts/prime/__main__.py +8 -0
  74. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  75. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  76. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  77. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  78. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  79. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  80. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  81. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  82. package/pennyfarthing_scripts/prime/cli.py +220 -0
  83. package/pennyfarthing_scripts/prime/loader.py +239 -0
  84. package/pennyfarthing_scripts/sprint/__init__.py +66 -0
  85. package/pennyfarthing_scripts/sprint/__main__.py +10 -0
  86. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  87. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  88. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  89. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  90. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  91. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  92. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  94. package/pennyfarthing_scripts/sprint/archive.py +108 -0
  95. package/pennyfarthing_scripts/sprint/cli.py +124 -0
  96. package/pennyfarthing_scripts/sprint/loader.py +193 -0
  97. package/pennyfarthing_scripts/sprint/status.py +122 -0
  98. package/pennyfarthing_scripts/sprint/validator.py +405 -0
  99. package/pennyfarthing_scripts/sprint/work.py +192 -0
  100. package/pennyfarthing_scripts/story/__init__.py +67 -0
  101. package/pennyfarthing_scripts/story/__main__.py +10 -0
  102. package/pennyfarthing_scripts/story/cli.py +105 -0
  103. package/pennyfarthing_scripts/story/create.py +167 -0
  104. package/pennyfarthing_scripts/story/size.py +113 -0
  105. package/pennyfarthing_scripts/story/template.py +151 -0
  106. package/pennyfarthing_scripts/swebench.py +216 -0
  107. package/pennyfarthing_scripts/tests/__init__.py +1 -0
  108. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  109. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  110. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  111. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  112. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  113. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  114. package/pennyfarthing_scripts/tests/conftest.py +106 -0
  115. package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
  116. package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
  117. package/pennyfarthing_scripts/tests/test_common.py +180 -0
  118. package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
  119. package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
  120. package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
  121. package/pennyfarthing_scripts/tests/test_prime.py +397 -0
  122. package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
  123. package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
  124. package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
  125. package/pennyfarthing_scripts/welcome_hook.py +157 -0
  126. package/pennyfarthing_scripts/workflow.py +183 -0
@@ -0,0 +1,211 @@
1
+ """
2
+ Claim Jira stories for work.
3
+
4
+ Usage:
5
+ python -m pennyfarthing_scripts.jira claim <issue-key> [--claim]
6
+
7
+ Options:
8
+ --claim Actually claim (assign + move to In Progress)
9
+
10
+ Exit codes:
11
+ 0 - Available or successfully claimed
12
+ 1 - Assigned to someone else
13
+ 2 - Not found or not synced
14
+ 3 - Error (CLI not installed, etc.)
15
+ """
16
+
17
+ import argparse
18
+ import sys
19
+ from typing import Any
20
+
21
+ from pennyfarthing_scripts.jira.client import (
22
+ get_issue,
23
+ get_jira_field,
24
+ is_jira_cli_available,
25
+ update_issue_status,
26
+ )
27
+
28
+
29
+ def parse_args(args: list[str] | None = None) -> argparse.Namespace:
30
+ """Parse command line arguments.
31
+
32
+ Args:
33
+ args: Command line arguments (defaults to sys.argv[1:])
34
+
35
+ Returns:
36
+ Parsed arguments namespace
37
+ """
38
+ parser = argparse.ArgumentParser(
39
+ description="Check availability and claim a Jira story"
40
+ )
41
+ parser.add_argument("issue_key", help="Jira issue key (e.g., MSSCI-12345)")
42
+ parser.add_argument(
43
+ "--claim",
44
+ action="store_true",
45
+ help="Actually claim (assign to self + move to In Progress)",
46
+ )
47
+
48
+ return parser.parse_args(args)
49
+
50
+
51
+ def check_availability(issue_key: str) -> dict[str, Any]:
52
+ """Check if a story is available for claiming.
53
+
54
+ Args:
55
+ issue_key: Jira issue key
56
+
57
+ Returns:
58
+ Dict with available status and details
59
+ """
60
+ if not is_jira_cli_available():
61
+ return {
62
+ "available": False,
63
+ "error": "Jira CLI not installed",
64
+ "exit_code": 3,
65
+ }
66
+
67
+ issue = get_issue(issue_key)
68
+ if not issue:
69
+ return {
70
+ "available": False,
71
+ "error": f"Issue {issue_key} not found",
72
+ "exit_code": 2,
73
+ }
74
+
75
+ assignee = get_jira_field(issue, "fields.assignee.displayName")
76
+ status = get_jira_field(issue, "fields.status.name", "Unknown")
77
+ summary = get_jira_field(issue, "fields.summary", "")
78
+
79
+ if assignee:
80
+ return {
81
+ "available": False,
82
+ "assigned_to": assignee,
83
+ "status": status,
84
+ "summary": summary,
85
+ "exit_code": 1,
86
+ }
87
+
88
+ return {
89
+ "available": True,
90
+ "status": status,
91
+ "summary": summary,
92
+ "exit_code": 0,
93
+ }
94
+
95
+
96
+ def claim_story(issue_key: str) -> dict[str, Any]:
97
+ """Claim a story by assigning to self and moving to In Progress.
98
+
99
+ Args:
100
+ issue_key: Jira issue key
101
+
102
+ Returns:
103
+ Dict with success status and details
104
+ """
105
+ import subprocess
106
+
107
+ if not is_jira_cli_available():
108
+ return {
109
+ "success": False,
110
+ "error": "Jira CLI not installed",
111
+ "exit_code": 3,
112
+ }
113
+
114
+ # Check availability first
115
+ availability = check_availability(issue_key)
116
+ if not availability["available"]:
117
+ return {
118
+ "success": False,
119
+ "error": f"Story not available: assigned to {availability.get('assigned_to', 'unknown')}",
120
+ "exit_code": 1,
121
+ }
122
+
123
+ actions = []
124
+ errors = []
125
+
126
+ # Get current user
127
+ result = subprocess.run(
128
+ ["jira", "me"],
129
+ capture_output=True,
130
+ text=True,
131
+ )
132
+ if result.returncode != 0:
133
+ return {
134
+ "success": False,
135
+ "error": "Could not get current Jira user",
136
+ "exit_code": 3,
137
+ }
138
+ current_user = result.stdout.strip()
139
+
140
+ # Assign to self
141
+ result = subprocess.run(
142
+ ["jira", "issue", "assign", issue_key, current_user, "--project", "MSSCI"],
143
+ capture_output=True,
144
+ text=True,
145
+ )
146
+ if result.returncode == 0:
147
+ actions.append(f"Assigned to {current_user}")
148
+ else:
149
+ errors.append(f"Failed to assign: {result.stderr}")
150
+
151
+ # Move to In Progress
152
+ if update_issue_status(issue_key, "In Progress"):
153
+ actions.append("Moved to In Progress")
154
+ else:
155
+ errors.append("Failed to move to In Progress")
156
+
157
+ if errors:
158
+ return {
159
+ "success": False,
160
+ "actions": actions,
161
+ "errors": errors,
162
+ "exit_code": 3,
163
+ }
164
+
165
+ return {
166
+ "success": True,
167
+ "actions": actions,
168
+ "exit_code": 0,
169
+ }
170
+
171
+
172
+ def main(args: list[str] | None = None) -> int:
173
+ """CLI entry point.
174
+
175
+ Args:
176
+ args: Command line arguments (defaults to sys.argv[1:])
177
+
178
+ Returns:
179
+ Exit code
180
+ """
181
+ parsed_args = parse_args(args)
182
+
183
+ if parsed_args.claim:
184
+ result = claim_story(parsed_args.issue_key)
185
+ if result["success"]:
186
+ print(f"Claimed {parsed_args.issue_key}")
187
+ for action in result.get("actions", []):
188
+ print(f" {action}")
189
+ return 0
190
+ else:
191
+ print(f"Failed to claim: {result.get('error', 'Unknown error')}", file=sys.stderr)
192
+ for err in result.get("errors", []):
193
+ print(f" {err}", file=sys.stderr)
194
+ return result.get("exit_code", 1)
195
+ else:
196
+ result = check_availability(parsed_args.issue_key)
197
+ if result["available"]:
198
+ print(f"{parsed_args.issue_key}: Available")
199
+ print(f" Status: {result.get('status')}")
200
+ print(f" Summary: {result.get('summary')}")
201
+ return 0
202
+ else:
203
+ if result.get("assigned_to"):
204
+ print(f"{parsed_args.issue_key}: Assigned to {result['assigned_to']}")
205
+ else:
206
+ print(f"{parsed_args.issue_key}: {result.get('error', 'Not available')}")
207
+ return result.get("exit_code", 1)
208
+
209
+
210
+ if __name__ == "__main__":
211
+ sys.exit(main())
@@ -0,0 +1,150 @@
1
+ """
2
+ Jira CLI - Fan-out CLI for Jira operations.
3
+
4
+ Usage:
5
+ python -m pennyfarthing_scripts.jira <subcommand> [args]
6
+
7
+ Subcommands:
8
+ view View issue details
9
+ claim Claim a story
10
+ sync Sync epic to Jira
11
+ bidirectional Bidirectional sync
12
+ create Create epic or story
13
+ """
14
+
15
+ import argparse
16
+ import sys
17
+ from typing import Any
18
+
19
+
20
+ def view(args: list[str]) -> int:
21
+ """View issue details."""
22
+ import subprocess
23
+
24
+ if not args:
25
+ print("Usage: jira view <issue-key>", file=sys.stderr)
26
+ return 1
27
+
28
+ result = subprocess.run(
29
+ ["jira", "issue", "view", args[0]],
30
+ capture_output=False,
31
+ )
32
+ return result.returncode
33
+
34
+
35
+ def claim(args: list[str]) -> int:
36
+ """Claim a story."""
37
+ from pennyfarthing_scripts.jira.claim import main as claim_main
38
+ return claim_main(args)
39
+
40
+
41
+ def sync(args: list[str]) -> int:
42
+ """Sync epic to Jira."""
43
+ from pennyfarthing_scripts.jira.sync import main as sync_main
44
+ return sync_main(args)
45
+
46
+
47
+ def bidirectional(args: list[str]) -> int:
48
+ """Bidirectional sync."""
49
+ from pennyfarthing_scripts.jira.bidirectional import main as bidirectional_main
50
+ return bidirectional_main(args)
51
+
52
+
53
+ def create(args: list[str]) -> int:
54
+ """Create epic or story."""
55
+ if not args:
56
+ print("Usage: jira create <epic|story> [args]", file=sys.stderr)
57
+ return 1
58
+
59
+ subcommand = args[0]
60
+ remaining = args[1:]
61
+
62
+ if subcommand == "epic":
63
+ from pennyfarthing_scripts.jira.epic import main as epic_main
64
+ return epic_main(remaining)
65
+ elif subcommand == "story":
66
+ from pennyfarthing_scripts.jira.story import main as story_main
67
+ return story_main(remaining)
68
+ else:
69
+ print(f"Unknown create subcommand: {subcommand}", file=sys.stderr)
70
+ print("Usage: jira create <epic|story> [args]", file=sys.stderr)
71
+ return 1
72
+
73
+
74
+ # Subcommand registry
75
+ SUBCOMMANDS = {
76
+ "view": view,
77
+ "claim": claim,
78
+ "sync": sync,
79
+ "bidirectional": bidirectional,
80
+ "create": create,
81
+ }
82
+
83
+
84
+ def cli(args: list[str] | None = None) -> int:
85
+ """Main CLI entry point.
86
+
87
+ Args:
88
+ args: Command line arguments (defaults to sys.argv[1:])
89
+
90
+ Returns:
91
+ Exit code
92
+ """
93
+ if args is None:
94
+ args = sys.argv[1:]
95
+
96
+ parser = argparse.ArgumentParser(
97
+ prog="jira",
98
+ description="Jira CLI for Pennyfarthing",
99
+ formatter_class=argparse.RawDescriptionHelpFormatter,
100
+ epilog="""
101
+ Subcommands:
102
+ view <key> View issue details
103
+ claim <key> [--claim] Check/claim a story
104
+ sync <epic> [opts] Sync epic to Jira
105
+ bidirectional [opts] Bidirectional sync
106
+ create epic <id> Create epic from YAML
107
+ create story <key> Sync single story
108
+
109
+ Examples:
110
+ jira view MSSCI-12345
111
+ jira claim MSSCI-12345 --claim
112
+ jira sync 63 --transition --points
113
+ jira bidirectional --all --dry-run
114
+ jira create epic epic-63
115
+ """,
116
+ )
117
+
118
+ parser.add_argument(
119
+ "subcommand",
120
+ nargs="?",
121
+ choices=list(SUBCOMMANDS.keys()),
122
+ help="Subcommand to run",
123
+ )
124
+ parser.add_argument(
125
+ "args",
126
+ nargs=argparse.REMAINDER,
127
+ help="Arguments for subcommand",
128
+ )
129
+
130
+ parsed = parser.parse_args(args)
131
+
132
+ if not parsed.subcommand:
133
+ parser.print_help()
134
+ return 0
135
+
136
+ handler = SUBCOMMANDS.get(parsed.subcommand)
137
+ if handler:
138
+ return handler(parsed.args)
139
+ else:
140
+ print(f"Unknown subcommand: {parsed.subcommand}", file=sys.stderr)
141
+ return 1
142
+
143
+
144
+ def main(args: list[str] | None = None) -> int:
145
+ """Alias for cli()."""
146
+ return cli(args)
147
+
148
+
149
+ if __name__ == "__main__":
150
+ sys.exit(cli())