@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pennyfarthing/core",
3
- "version": "7.8.1",
3
+ "version": "7.8.2",
4
4
  "description": "Claude Code agent framework with TDD workflow and persona system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "pennyfarthing-dist/skills/",
19
19
  "pennyfarthing-dist/templates/",
20
20
  "pennyfarthing-dist/workflows/",
21
+ "pennyfarthing_scripts/",
21
22
  "README.md"
22
23
  ],
23
24
  "repository": {
@@ -4,4 +4,12 @@
4
4
  #
5
5
  # Thin wrapper around python -m pennyfarthing_scripts.prime
6
6
 
7
+ # Find the package root (where pennyfarthing_scripts lives)
8
+ # Works for both npm installs (node_modules/@pennyfarthing/core) and local dev
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ PACKAGE_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
11
+
12
+ # Set PYTHONPATH so Python can find pennyfarthing_scripts
13
+ export PYTHONPATH="${PACKAGE_ROOT}:${PYTHONPATH:-}"
14
+
7
15
  exec python3 -m pennyfarthing_scripts.prime "$@"
@@ -0,0 +1,17 @@
1
+ """
2
+ Pennyfarthing Scripts - Python utilities for agent orchestration.
3
+
4
+ Modules:
5
+ config: Project root detection, YAML configuration loading
6
+ output: Colored console output utilities (success, info, warn, error)
7
+ sprint: Sprint YAML parsing and story access
8
+ jira: Jira CLI wrapper and JiraClient for REST API operations
9
+ jira_sync: Async epic sync to Jira
10
+ jira_sync_story: Single story sync to Jira
11
+ jira_epic_creation: Create Jira epics from sprint YAML
12
+ jira_bidirectional_sync: Bidirectional sync between sprint YAML and Jira
13
+ preflight: Async preflight checks for workflow completion
14
+ swebench: SWE-bench patch parsing and scenario utilities
15
+ """
16
+
17
+ __version__ = "7.6.1"
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bell Mode PostToolUse Hook (Python)
4
+
5
+ This hook is called by Claude Code after each tool execution.
6
+ When bell mode is enabled and there are queued messages, it returns
7
+ the first queued message as additionalContext to be injected into
8
+ Claude's next API call.
9
+
10
+ Configuration files:
11
+ .pennyfarthing/config.local.yaml - workflow.bell_mode: true/false
12
+ .pennyfarthing/bell-queue.json - [{"text": "...", "images": [...]}, ...]
13
+
14
+ Output format (when injecting):
15
+ {
16
+ "hookSpecificOutput": {
17
+ "hookEventName": "PostToolUse",
18
+ "additionalContext": "User feedback: <message>"
19
+ }
20
+ }
21
+
22
+ Output when disabled or queue empty: (nothing - exit 0)
23
+
24
+ Story: MSSCI-12409 - Hook consistency and WheelHub consolidation
25
+ """
26
+
27
+ import json
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ # Add parent directory to path for imports
32
+ sys.path.insert(0, str(Path(__file__).parent))
33
+
34
+ from hooks import (
35
+ find_project_root,
36
+ get_cyclist_port,
37
+ send_to_cyclist,
38
+ output_hook_response,
39
+ HookResponse,
40
+ is_bell_mode_enabled,
41
+ )
42
+
43
+
44
+ def read_bell_queue(project_root: Path) -> list[dict]:
45
+ """Read the bell message queue.
46
+
47
+ Args:
48
+ project_root: Project root directory
49
+
50
+ Returns:
51
+ List of queued messages, or empty list if none
52
+ """
53
+ queue_path = project_root / ".pennyfarthing" / "bell-queue.json"
54
+ if not queue_path.exists():
55
+ return []
56
+
57
+ try:
58
+ with open(queue_path) as f:
59
+ queue = json.load(f)
60
+ if isinstance(queue, list):
61
+ return queue
62
+ except (json.JSONDecodeError, OSError):
63
+ pass
64
+
65
+ return []
66
+
67
+
68
+ def dequeue_message(project_root: Path) -> None:
69
+ """Remove the first message from the queue.
70
+
71
+ Args:
72
+ project_root: Project root directory
73
+ """
74
+ queue_path = project_root / ".pennyfarthing" / "bell-queue.json"
75
+ if not queue_path.exists():
76
+ return
77
+
78
+ try:
79
+ with open(queue_path) as f:
80
+ queue = json.load(f)
81
+
82
+ if isinstance(queue, list) and len(queue) > 0:
83
+ queue = queue[1:] # Remove first item
84
+ with open(queue_path, "w") as f:
85
+ json.dump(queue, f)
86
+ except (json.JSONDecodeError, OSError):
87
+ pass
88
+
89
+
90
+ def notify_cyclist(project_root: Path, message_text: str) -> None:
91
+ """Notify Cyclist browser that a queued message was consumed.
92
+
93
+ Args:
94
+ project_root: Project root directory
95
+ message_text: The message text that was consumed
96
+ """
97
+ try:
98
+ send_to_cyclist(
99
+ endpoint="/api/bell-consumed",
100
+ data={"text": message_text},
101
+ project_root=project_root,
102
+ timeout=5,
103
+ )
104
+ except Exception:
105
+ # Ignore errors - don't block hook
106
+ pass
107
+
108
+
109
+ def main() -> None:
110
+ """Main entry point for PostToolUse bell mode hook."""
111
+ try:
112
+ # Read and discard stdin (required by hook protocol)
113
+ sys.stdin.read()
114
+
115
+ # Find project root
116
+ project_root = find_project_root()
117
+ if not project_root:
118
+ sys.exit(0)
119
+
120
+ # Check if bell mode is enabled
121
+ if not is_bell_mode_enabled(project_root):
122
+ sys.exit(0)
123
+
124
+ # Read queue
125
+ queue = read_bell_queue(project_root)
126
+ if not queue:
127
+ sys.exit(0)
128
+
129
+ # Get first message
130
+ first_message = queue[0]
131
+ message_text = first_message.get("text", "")
132
+ if not message_text:
133
+ sys.exit(0)
134
+
135
+ # Output hook response with additionalContext
136
+ output_hook_response(HookResponse(
137
+ event_name="PostToolUse",
138
+ additional_context=f"User feedback: {message_text}",
139
+ ))
140
+
141
+ # Dequeue message and notify Cyclist (in background-ish - after output)
142
+ dequeue_message(project_root)
143
+ notify_cyclist(project_root, message_text)
144
+
145
+ sys.exit(0)
146
+
147
+ except Exception as e:
148
+ # On error, exit silently
149
+ print(f"[bellmode-hook] Error: {e}", file=sys.stderr)
150
+ sys.exit(0)
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()
@@ -0,0 +1,35 @@
1
+ """
2
+ Brownfield discovery module for Pennyfarthing.
3
+
4
+ Analyzes existing codebases and generates AI-ready documentation.
5
+ """
6
+
7
+ from pennyfarthing_scripts.brownfield.discover import (
8
+ DepthLevel,
9
+ ProjectType,
10
+ DiscoveryResult,
11
+ detect_project_type,
12
+ detect_tech_stack,
13
+ scan_directory_structure,
14
+ detect_architecture_patterns,
15
+ generate_project_overview,
16
+ generate_tech_stack_doc,
17
+ generate_source_tree_doc,
18
+ generate_ai_guidance_doc,
19
+ discover,
20
+ )
21
+
22
+ __all__ = [
23
+ "DepthLevel",
24
+ "ProjectType",
25
+ "DiscoveryResult",
26
+ "detect_project_type",
27
+ "detect_tech_stack",
28
+ "scan_directory_structure",
29
+ "detect_architecture_patterns",
30
+ "generate_project_overview",
31
+ "generate_tech_stack_doc",
32
+ "generate_source_tree_doc",
33
+ "generate_ai_guidance_doc",
34
+ "discover",
35
+ ]
@@ -0,0 +1,7 @@
1
+ """Entry point for python -m pennyfarthing_scripts.brownfield."""
2
+
3
+ import sys
4
+ from pennyfarthing_scripts.brownfield.cli import cli
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(cli())
@@ -0,0 +1,131 @@
1
+ """
2
+ Brownfield CLI - Analyze existing codebases.
3
+
4
+ Usage:
5
+ python -m pennyfarthing_scripts.brownfield <subcommand> [args]
6
+
7
+ Subcommands:
8
+ scan Scan codebase and generate documentation
9
+ """
10
+
11
+ import argparse
12
+ import asyncio
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from pennyfarthing_scripts.brownfield.discover import (
18
+ DepthLevel,
19
+ discover,
20
+ )
21
+
22
+
23
+ def scan(args: list[str]) -> int:
24
+ """Scan codebase and generate documentation."""
25
+ parser = argparse.ArgumentParser(
26
+ prog="brownfield scan",
27
+ description="Scan codebase and generate AI-ready documentation",
28
+ )
29
+ parser.add_argument(
30
+ "path",
31
+ nargs="?",
32
+ default=".",
33
+ help="Path to scan (default: current directory)",
34
+ )
35
+ parser.add_argument(
36
+ "--depth",
37
+ choices=["quick", "standard", "deep"],
38
+ default="standard",
39
+ help="Scan depth level",
40
+ )
41
+ parser.add_argument(
42
+ "--output", "-o",
43
+ help="Output directory for generated docs",
44
+ )
45
+
46
+ parsed = parser.parse_args(args)
47
+
48
+ path = Path(parsed.path).resolve()
49
+ depth = DepthLevel(parsed.depth)
50
+ output_dir = Path(parsed.output) if parsed.output else None
51
+
52
+ result = asyncio.run(discover(path, depth, output_dir))
53
+
54
+ if not result.success:
55
+ print(f"Error: {result.error}", file=sys.stderr)
56
+ return 1
57
+
58
+ print(f"Discovery complete: {result.project_name}")
59
+ print(f"Type: {result.project_type.value}")
60
+ print(f"Tech stack: {len(result.tech_stack)} items detected")
61
+
62
+ return 0
63
+
64
+
65
+ # Subcommand registry
66
+ SUBCOMMANDS = {
67
+ "scan": scan,
68
+ }
69
+
70
+
71
+ def cli(args: list[str] | None = None) -> int:
72
+ """Main CLI entry point.
73
+
74
+ Args:
75
+ args: Command line arguments (defaults to sys.argv[1:])
76
+
77
+ Returns:
78
+ Exit code
79
+ """
80
+ if args is None:
81
+ args = sys.argv[1:]
82
+
83
+ parser = argparse.ArgumentParser(
84
+ prog="brownfield",
85
+ description="Brownfield codebase discovery for Pennyfarthing",
86
+ formatter_class=argparse.RawDescriptionHelpFormatter,
87
+ epilog="""
88
+ Subcommands:
89
+ scan [path] [opts] Scan codebase and generate docs
90
+
91
+ Examples:
92
+ brownfield scan
93
+ brownfield scan /path/to/project
94
+ brownfield scan --depth deep
95
+ brownfield scan . --output docs/brownfield
96
+ """,
97
+ )
98
+
99
+ parser.add_argument(
100
+ "subcommand",
101
+ nargs="?",
102
+ choices=list(SUBCOMMANDS.keys()),
103
+ help="Subcommand to run",
104
+ )
105
+ parser.add_argument(
106
+ "args",
107
+ nargs=argparse.REMAINDER,
108
+ help="Arguments for subcommand",
109
+ )
110
+
111
+ parsed = parser.parse_args(args)
112
+
113
+ if not parsed.subcommand:
114
+ parser.print_help()
115
+ return 0
116
+
117
+ handler = SUBCOMMANDS.get(parsed.subcommand)
118
+ if handler:
119
+ return handler(parsed.args)
120
+ else:
121
+ print(f"Unknown subcommand: {parsed.subcommand}", file=sys.stderr)
122
+ return 1
123
+
124
+
125
+ def main(args: list[str] | None = None) -> int:
126
+ """Alias for cli()."""
127
+ return cli(args)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ sys.exit(cli())