@pennyfarthing/core 7.6.1 → 7.7.0

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 (68) hide show
  1. package/README.md +109 -201
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor.js +91 -0
  5. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  6. package/packages/core/dist/cli/commands/init.js +31 -0
  7. package/packages/core/dist/cli/commands/init.js.map +1 -1
  8. package/packages/core/dist/cli/commands/update.js +31 -0
  9. package/packages/core/dist/cli/commands/update.js.map +1 -1
  10. package/pennyfarthing-dist/agents/architect.md +48 -53
  11. package/pennyfarthing-dist/agents/dev.md +74 -164
  12. package/pennyfarthing-dist/agents/devops.md +44 -39
  13. package/pennyfarthing-dist/agents/handoff.md +46 -23
  14. package/pennyfarthing-dist/agents/orchestrator.md +84 -255
  15. package/pennyfarthing-dist/agents/pm.md +40 -50
  16. package/pennyfarthing-dist/agents/reviewer-preflight.md +58 -26
  17. package/pennyfarthing-dist/agents/reviewer.md +107 -298
  18. package/pennyfarthing-dist/agents/sm-file-summary.md +51 -30
  19. package/pennyfarthing-dist/agents/sm-finish.md +59 -38
  20. package/pennyfarthing-dist/agents/sm-handoff.md +40 -33
  21. package/pennyfarthing-dist/agents/sm-setup.md +89 -47
  22. package/pennyfarthing-dist/agents/sm.md +171 -558
  23. package/pennyfarthing-dist/agents/tea.md +77 -146
  24. package/pennyfarthing-dist/agents/tech-writer.md +43 -24
  25. package/pennyfarthing-dist/agents/testing-runner.md +73 -30
  26. package/pennyfarthing-dist/agents/ux-designer.md +39 -25
  27. package/pennyfarthing-dist/agents/workflow-status-check.md +34 -16
  28. package/pennyfarthing-dist/commands/benchmark.md +19 -1
  29. package/pennyfarthing-dist/commands/continue-session.md +1 -1
  30. package/pennyfarthing-dist/commands/solo.md +5 -0
  31. package/pennyfarthing-dist/commands/theme-maker.md +5 -5
  32. package/pennyfarthing-dist/commands/work.md +1 -1
  33. package/pennyfarthing-dist/guides/XML-TAGS.md +179 -0
  34. package/pennyfarthing-dist/guides/agent-behavior.md +22 -9
  35. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +432 -0
  36. package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +27 -7
  37. package/pennyfarthing-dist/guides/scale-levels.md +114 -0
  38. package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +2 -2
  39. package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +1 -1
  40. package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -7
  41. package/pennyfarthing-dist/scripts/core/check-context.sh +6 -1
  42. package/pennyfarthing-dist/scripts/core/prime.sh +57 -32
  43. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +45 -4
  44. package/pennyfarthing-dist/scripts/git/git-status-all.sh +32 -7
  45. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +30 -11
  46. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +80 -23
  47. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +66 -53
  48. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +4 -4
  49. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +402 -0
  50. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +7 -0
  51. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +94 -0
  52. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +10 -152
  53. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +14 -4
  54. package/pennyfarthing-dist/scripts/jira/jira-sync.sh +12 -4
  55. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +11 -99
  56. package/pennyfarthing-dist/scripts/lib/common.sh +55 -0
  57. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +97 -0
  58. package/pennyfarthing-dist/scripts/misc/statusline.sh +27 -22
  59. package/pennyfarthing-dist/scripts/story/create-story.sh +14 -154
  60. package/pennyfarthing-dist/scripts/story/size-story.sh +12 -192
  61. package/pennyfarthing-dist/scripts/story/story-template.sh +12 -156
  62. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +24 -93
  63. package/pennyfarthing-dist/scripts/test/swebench-judge.py +33 -59
  64. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +575 -0
  65. package/pennyfarthing-dist/scripts/workflow/check.py +502 -0
  66. package/pennyfarthing-dist/skills/skill-registry.yaml +52 -16
  67. package/pennyfarthing-dist/skills/sprint/skill.md +1 -1
  68. package/pennyfarthing-dist/templates/settings.local.json.template +11 -0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # Welcome Hook: Display a friendly welcome message on session start
3
+ #
4
+ # For CLI: Displays ASCII art of a penny-farthing bicycle
5
+ # For Cyclist: Sends WebSocket message to display logo and welcome
6
+ #
7
+ # Called by Claude Code SessionStart hook
8
+
9
+ set -euo pipefail
10
+
11
+ # Read and discard stdin (required by hook protocol)
12
+ cat > /dev/null
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$SCRIPT_DIR/../../.." && pwd)}"
16
+
17
+ # Once-per-session guard: only show welcome on first invocation
18
+ # Use session ID from environment or generate a unique one
19
+ SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
20
+ WELCOME_LOCK="$PROJECT_ROOT/.session/.welcome-shown-$SESSION_ID"
21
+
22
+ # Ensure .session directory exists
23
+ mkdir -p "$PROJECT_ROOT/.session"
24
+
25
+ # Check if welcome was already shown for this session
26
+ if [[ -f "$WELCOME_LOCK" ]]; then
27
+ exit 0
28
+ fi
29
+
30
+ # Mark welcome as shown for this session
31
+ touch "$WELCOME_LOCK"
32
+
33
+ # Check if running in Cyclist (port file exists)
34
+ PORT_FILE="$PROJECT_ROOT/.cyclist-port"
35
+ IN_CYCLIST=false
36
+ if [[ -f "$PORT_FILE" ]]; then
37
+ CYCLIST_PORT=$(cat "$PORT_FILE" 2>/dev/null)
38
+ if [[ "$CYCLIST_PORT" =~ ^[0-9]+$ ]]; then
39
+ IN_CYCLIST=true
40
+ fi
41
+ fi
42
+
43
+ # Get project name from package.json or directory name
44
+ PROJECT_NAME=""
45
+ if [[ -f "$PROJECT_ROOT/package.json" ]]; then
46
+ PROJECT_NAME=$(jq -r '.name // empty' "$PROJECT_ROOT/package.json" 2>/dev/null || echo "")
47
+ fi
48
+ if [[ -z "$PROJECT_NAME" ]]; then
49
+ PROJECT_NAME=$(basename "$PROJECT_ROOT")
50
+ fi
51
+
52
+ # Get current theme from config
53
+ THEME=""
54
+ if [[ -f "$PROJECT_ROOT/.pennyfarthing/config.local.yaml" ]]; then
55
+ THEME=$(grep -E '^theme:' "$PROJECT_ROOT/.pennyfarthing/config.local.yaml" 2>/dev/null | sed 's/theme:[[:space:]]*//' | tr -d '"' || echo "")
56
+ fi
57
+
58
+ if [[ "$IN_CYCLIST" == "true" ]]; then
59
+ # Send welcome message via WebSocket API for Cyclist to display
60
+ # Cyclist will show the pennyfarthing logo image and welcome text
61
+ curl -s -X POST "http://localhost:$CYCLIST_PORT/api/welcome" \
62
+ -H "Content-Type: application/json" \
63
+ -d "{\"project\": \"$PROJECT_NAME\", \"theme\": \"$THEME\"}" \
64
+ >/dev/null 2>&1 || true
65
+ else
66
+ # CLI mode: Display ASCII art welcome
67
+ cat << 'EOF'
68
+
69
+ ___
70
+ / \
71
+ | | Welcome to
72
+ | | ╔═══════════════════════════════════╗
73
+ \___/ ║ ╔═╗╔═╗╔╗╔╔╗╔╦═╗╔═╗╔═╗╔═╗╦╔═╗ ║
74
+ ║ ║ ╠═╝║╣ ║║║║║║ ╠╣ ╠═╣╠╦╝ ║ ╠═╣ ║
75
+ ║ ║ ╩ ╚═╝╝╚╝╝╚╝╩ ╩ ╩╩╚═ ╩ ╩ ╩ ║
76
+ ╔═╩═╗ ╚═══════════════════════════════════╝
77
+ / \
78
+ │ O │ Agent-powered development with style
79
+ \ /
80
+ ╚═══╝
81
+
82
+ EOF
83
+
84
+ # Add project-specific info
85
+ if [[ -n "$PROJECT_NAME" ]]; then
86
+ echo " Project: $PROJECT_NAME"
87
+ fi
88
+ if [[ -n "$THEME" ]]; then
89
+ echo " Theme: $THEME"
90
+ fi
91
+ echo ""
92
+ fi
93
+
94
+ exit 0
@@ -1,164 +1,22 @@
1
1
  #!/usr/bin/env zsh
2
- # Check and claim a story in Jira for multi-developer coordination
3
- # Usage: ./scripts/jira-claim-story.sh <story_key_or_jira_key> [--claim]
2
+ # Check and claim a story in Jira
3
+ # Usage: jira-claim-story.sh <issue-key> [--claim]
4
4
  #
5
- # This script uses the `jira` CLI tool (https://github.com/ankitpokhrel/jira)
6
- #
7
- # Parameters:
8
- # <story_key_or_jira_key> Either:
9
- # - Story key format: 35-7-feedback-rule-delete-404
10
- # - Jira issue key format: MSSCI-10991
11
- #
12
- # Actions:
13
- # (default) Check if story is assigned and show status
14
- # --claim Assign story to current user and move to "In Progress"
5
+ # Thin wrapper that delegates to Python CLI:
6
+ # python -m pennyfarthing_scripts.jira claim <issue-key> [--claim]
15
7
  #
16
8
  # Exit codes:
17
- # 0 - Story is available (unassigned) or successfully claimed
9
+ # 0 - Story is available or successfully claimed
18
10
  # 1 - Story is assigned to someone else
19
- # 2 - Story not found or not synced to Jira
20
- # 3 - Error (jira CLI not installed, etc.)
11
+ # 2 - Story not found or not synced
12
+ # 3 - Error (CLI not installed, Python not found, etc.)
21
13
 
22
14
  set -e
23
15
 
24
- # Source common functions
16
+ # Source common functions for Python discovery
25
17
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
26
18
  PARENT_DIR="$(dirname "$SCRIPT_DIR")"
27
19
  source "${PARENT_DIR}/lib/common.sh"
28
- source "${PARENT_DIR}/sprint/sprint-common.sh"
29
- source "${SCRIPT_DIR}/jira-lib.sh"
30
-
31
- # Check dependencies (using jira-lib's robust check)
32
- check_jira_cli
33
-
34
- # Check arguments
35
- if [ -z "$1" ]; then
36
- error "Error: Story key or Jira issue key required"
37
- echo "Usage: $0 <story_key_or_jira_key> [--claim]"
38
- echo ""
39
- echo "Examples:"
40
- echo " $0 35-7-feedback-rule-delete-404 # Using story key"
41
- echo " $0 35-7-feedback-rule-delete-404 --claim # Claim using story key"
42
- echo " $0 MSSCI-10991 # Using Jira issue key"
43
- echo " $0 MSSCI-10991 --claim # Claim using Jira key"
44
- exit 3
45
- fi
46
-
47
- INPUT_KEY="$1"
48
- CLAIM_MODE=false
49
- [ "$2" = "--claim" ] && CLAIM_MODE=true
50
-
51
- # Detect input format: Jira key (MSSCI-12345) vs Story key (35-7-name)
52
- if [[ "$INPUT_KEY" =~ ^${JIRA_PROJECT}-[0-9]+$ ]]; then
53
- # Input is Jira issue key format (e.g., MSSCI-10991)
54
- JIRA_ISSUE_KEY="$INPUT_KEY"
55
- info "🔍 Using Jira issue key: ${JIRA_ISSUE_KEY}"
56
-
57
- # Skip story file lookup - we'll work directly with Jira
58
- STORY_KEY=""
59
-
60
- else
61
- # Input is story key format (e.g., 35-7-feedback-rule-delete-404)
62
- STORY_KEY="$INPUT_KEY"
63
-
64
- # Extract epic number from story key
65
- EPIC_NUM=$(echo "$STORY_KEY" | cut -d'-' -f1)
66
-
67
- info "🔍 Checking story ${STORY_KEY} in Jira..."
68
-
69
- # Find the story
70
- STORY_FILE=$(find_story_file "$STORY_KEY")
71
- if [ -z "$STORY_FILE" ]; then
72
- error "Error: Story ${STORY_KEY} not found in sprint files"
73
- exit 2
74
- fi
75
-
76
- # Get Jira link
77
- STORY_JIRA_LINK=$(get_story_field "$STORY_KEY" "jira")
78
- if [ "$STORY_JIRA_LINK" = "null" ] || [ -z "$STORY_JIRA_LINK" ]; then
79
- warn "⚠️ Story ${STORY_KEY} not synced to Jira yet"
80
- echo ""
81
- echo "Proceeding without Jira sync. Story will be synced later."
82
- echo "To sync now, run: ./scripts/sync-epic-to-jira.sh ${EPIC_NUM}"
83
- exit 0
84
- fi
85
-
86
- JIRA_ISSUE_KEY=$(extract_jira_key "$STORY_JIRA_LINK")
87
- success "✅ Found Jira issue: ${JIRA_ISSUE_KEY}"
88
- fi
89
-
90
- # Get current user
91
- CURRENT_USER=$(jira me 2>/dev/null || echo "")
92
- if [ -z "$CURRENT_USER" ]; then
93
- error "Error: Could not get current Jira user. Run 'jira init' to configure."
94
- exit 3
95
- fi
96
-
97
- info "👤 Current user: ${CURRENT_USER}"
98
-
99
- # Get issue details (using --raw for JSON parsing)
100
- info "📋 Fetching issue details..."
101
- ISSUE_JSON=$(jira issue view "$JIRA_ISSUE_KEY" --raw 2>/dev/null || echo "{}")
102
-
103
- if [ "$ISSUE_JSON" = "{}" ]; then
104
- error "Error: Could not fetch issue ${JIRA_ISSUE_KEY}"
105
- exit 3
106
- fi
107
-
108
- # Parse assignee (use printf to avoid zsh echo interpreting \n in JSON)
109
- ASSIGNEE=$(printf '%s' "$ISSUE_JSON" | jq -r '.fields.assignee.displayName // "Unassigned"')
110
- ASSIGNEE_EMAIL=$(printf '%s' "$ISSUE_JSON" | jq -r '.fields.assignee.emailAddress // ""')
111
- STATUS=$(printf '%s' "$ISSUE_JSON" | jq -r '.fields.status.name // "Unknown"')
112
- SUMMARY=$(printf '%s' "$ISSUE_JSON" | jq -r '.fields.summary // "No summary"')
113
-
114
- echo ""
115
- echo "📋 Issue: ${JIRA_ISSUE_KEY}"
116
- echo " Summary: ${SUMMARY}"
117
- echo " Status: ${STATUS}"
118
- echo " Assignee: ${ASSIGNEE}"
119
-
120
- # Check assignment status
121
- if [ "$ASSIGNEE" = "Unassigned" ]; then
122
- success "✅ Story is UNASSIGNED - available to claim"
123
-
124
- if [ "$CLAIM_MODE" = true ]; then
125
- info "🎯 Claiming story..."
126
-
127
- # Assign to self
128
- if jira issue assign "$JIRA_ISSUE_KEY" "$(jira me)" --project "$JIRA_PROJECT" 2>/dev/null; then
129
- success "✅ Assigned to you"
130
- else
131
- error "Failed to assign issue"
132
- exit 3
133
- fi
134
20
 
135
- # Move to In Progress (if not already)
136
- if [ "$STATUS" != "In Progress" ]; then
137
- info "📊 Moving to 'In Progress'..."
138
- if jira issue move "$JIRA_ISSUE_KEY" "In Progress" --project "$JIRA_PROJECT" 2>/dev/null; then
139
- success "✅ Moved to In Progress"
140
- else
141
- warn "⚠️ Could not transition to In Progress (may already be there or transition not available)"
142
- fi
143
- fi
144
-
145
- echo ""
146
- success "🎉 Story ${JIRA_ISSUE_KEY} claimed successfully!"
147
- else
148
- echo ""
149
- info "To claim this story, run:"
150
- echo " $0 $STORY_KEY --claim"
151
- fi
152
- exit 0
153
-
154
- elif [ "$ASSIGNEE_EMAIL" = "$CURRENT_USER" ] || [ "$ASSIGNEE" = "$(jira me 2>/dev/null)" ]; then
155
- success "✅ Story is assigned to YOU - proceed with work"
156
- exit 0
157
-
158
- else
159
- warn "⚠️ Story is assigned to: ${ASSIGNEE}"
160
- echo ""
161
- echo "This story is already being worked on by someone else."
162
- echo "Please choose a different story or coordinate with ${ASSIGNEE}."
163
- exit 1
164
- fi
21
+ # Delegate to Python CLI
22
+ run_python_module jira claim "$@"
@@ -1,8 +1,18 @@
1
1
  #!/usr/bin/env zsh
2
- # Thin wrapper for jira-sync-story.mjs
3
- # Usage: ./scripts/jira-sync-story.sh <story_key> [--transition] [--points] [--comment "message"]
2
+ # Sync a single story between sprint YAML and Jira
3
+ # Usage: jira-sync-story.sh <story_key> [--transition] [--points] [--comment "message"]
4
4
  #
5
- # Delegates to Node script for cleaner, more maintainable implementation.
5
+ # Thin wrapper that delegates to Python CLI:
6
+ # python -m pennyfarthing_scripts.jira create story <story_key> [options]
7
+ #
8
+ # Note: The subcommand is 'create story' but it handles sync, not creation.
9
+
10
+ set -e
6
11
 
12
+ # Source common functions for Python discovery
7
13
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
- exec node "${SCRIPT_DIR}/jira/jira-sync-story.mjs" "$@"
14
+ PARENT_DIR="$(dirname "$SCRIPT_DIR")"
15
+ source "${PARENT_DIR}/lib/common.sh"
16
+
17
+ # Delegate to Python CLI
18
+ run_python_module jira create story "$@"
@@ -1,8 +1,16 @@
1
1
  #!/usr/bin/env zsh
2
- # Thin wrapper for jira-sync.mjs
3
- # Usage: ./scripts/jira-sync.sh <epic_number> [--dry-run] [--transition] [--points]
2
+ # Sync an epic and its stories to Jira
3
+ # Usage: jira-sync.sh <epic_number> [--dry-run] [--transition] [--points]
4
4
  #
5
- # Delegates to Node script for cleaner, more maintainable implementation.
5
+ # Thin wrapper that delegates to Python CLI:
6
+ # python -m pennyfarthing_scripts.jira sync <epic_number> [options]
6
7
 
8
+ set -e
9
+
10
+ # Source common functions for Python discovery
7
11
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
- exec node "${SCRIPT_DIR}/jira/jira-sync.mjs" "$@"
12
+ PARENT_DIR="$(dirname "$SCRIPT_DIR")"
13
+ source "${PARENT_DIR}/lib/common.sh"
14
+
15
+ # Delegate to Python CLI
16
+ run_python_module jira sync "$@"
@@ -1,104 +1,16 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env zsh
2
2
  # Sync an epic and its stories to Jira
3
- # Usage: .pennyfarthing/scripts/core/run.sh sync-epic-jira.sh <epic-id> [--dry-run] [--transition] [--points] [--all]
3
+ # Usage: sync-epic-jira.sh <epic-id> [--dry-run] [--transition] [--points] [--all]
4
4
  #
5
- # Example: .pennyfarthing/scripts/core/run.sh sync-epic-jira.sh MSSCI-11952
6
- # Example: .pennyfarthing/scripts/core/run.sh sync-epic-jira.sh MSSCI-11952 --all
7
- #
8
- # This script syncs status and story points from sprint YAML to Jira.
9
- # It wraps the existing jira-sync.mjs for better integration with /sprint skill.
10
-
11
- set -euo pipefail
12
-
13
- EPIC_ID="${1:-}"
14
-
15
- if [[ -z "$EPIC_ID" ]]; then
16
- echo "Usage: sync-epic-jira.sh <epic-id> [options]"
17
- echo ""
18
- echo "Syncs an epic and its stories from sprint YAML to Jira."
19
- echo ""
20
- echo "Options:"
21
- echo " --dry-run Show what would be done without making changes"
22
- echo " --transition Transition Jira issues to match sprint YAML status"
23
- echo " --points Sync story points from sprint YAML to Jira"
24
- echo " --all Equivalent to --transition --points"
25
- echo ""
26
- echo "Examples:"
27
- echo " sync-epic-jira.sh MSSCI-11952 # Show sync status"
28
- echo " sync-epic-jira.sh MSSCI-11952 --dry-run # Preview changes"
29
- echo " sync-epic-jira.sh MSSCI-11952 --all # Sync status and points"
30
- exit 1
31
- fi
32
-
33
- # PROJECT_ROOT should be set by run.sh, but find it if not
34
- if [[ -z "${PROJECT_ROOT:-}" ]]; then
35
- d="$PWD"
36
- while [[ ! -d "$d/.claude" ]] && [[ "$d" != "/" ]]; do
37
- d="$(dirname "$d")"
38
- done
39
- PROJECT_ROOT="$d"
40
- fi
41
-
42
- SPRINT_FILE="$PROJECT_ROOT/sprint/current-sprint.yaml"
43
-
44
- if [[ ! -f "$SPRINT_FILE" ]]; then
45
- echo "Error: Sprint file not found at $SPRINT_FILE"
46
- exit 1
47
- fi
48
-
49
- if ! command -v yq &> /dev/null; then
50
- echo "Error: yq is required but not installed"
51
- echo "Install with: brew install yq"
52
- exit 1
53
- fi
54
-
55
- # Check if epic exists in sprint YAML
56
- EPIC_EXISTS=$(yq ".epics[] | select(.id == \"$EPIC_ID\") | .id" "$SPRINT_FILE" 2>/dev/null || echo "")
57
-
58
- if [[ -z "$EPIC_EXISTS" ]]; then
59
- echo "Error: Epic $EPIC_ID not found in $SPRINT_FILE"
60
- echo ""
61
- echo "Available epics:"
62
- yq '.epics[].id' "$SPRINT_FILE" 2>/dev/null || echo " None found"
63
- exit 1
64
- fi
65
-
66
- # Extract epic number for jira-sync.mjs compatibility
67
- # jira-sync.mjs expects epic number (e.g., 35) not full ID
68
- # But it also accepts Jira keys directly
69
- # Let's check if this is a Jira key (MSSCI-XXXXX) or local ID
70
-
71
- # Pass through to jira-sync.mjs with remaining arguments
72
- JIRA_SYNC_SCRIPT="$PROJECT_ROOT/.pennyfarthing/scripts/utils/jira/jira-sync.mjs"
73
-
74
- if [[ ! -f "$JIRA_SYNC_SCRIPT" ]]; then
75
- # Try alternate location
76
- JIRA_SYNC_SCRIPT="$PROJECT_ROOT/pennyfarthing-dist/scripts/utils/jira/jira-sync.mjs"
77
- fi
78
-
79
- if [[ ! -f "$JIRA_SYNC_SCRIPT" ]]; then
80
- echo "Error: jira-sync.mjs not found"
81
- exit 1
82
- fi
83
-
84
- # Build arguments for jira-sync.mjs
85
- shift # Remove epic-id from arguments
86
- SYNC_ARGS=("$EPIC_ID")
5
+ # Thin wrapper that delegates to Python CLI:
6
+ # python -m pennyfarthing_scripts.jira sync <epic-id> [options]
87
7
 
88
- # Handle --all flag
89
- for arg in "$@"; do
90
- case "$arg" in
91
- --all)
92
- SYNC_ARGS+=("--transition" "--points")
93
- ;;
94
- *)
95
- SYNC_ARGS+=("$arg")
96
- ;;
97
- esac
98
- done
8
+ set -e
99
9
 
100
- echo "Syncing epic $EPIC_ID to Jira..."
101
- echo ""
10
+ # Source common functions for Python discovery
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ PARENT_DIR="$(dirname "$SCRIPT_DIR")"
13
+ source "${PARENT_DIR}/lib/common.sh"
102
14
 
103
- # Run jira-sync.mjs
104
- exec node "$JIRA_SYNC_SCRIPT" "${SYNC_ARGS[@]}"
15
+ # Delegate to Python CLI
16
+ run_python_module jira sync "$@"
@@ -155,3 +155,58 @@ check_dependencies() {
155
155
  fi
156
156
  return 0
157
157
  }
158
+
159
+ #############################################
160
+ # Python Execution
161
+ #############################################
162
+
163
+ # get_python
164
+ # Find the best Python interpreter (venv preferred, fallback to system)
165
+ # Sets PYTHON_CMD variable
166
+ #
167
+ # Priority:
168
+ # 1. PROJECT_ROOT/.venv/bin/python (project venv)
169
+ # 2. python3 (system)
170
+ # 3. python (legacy fallback)
171
+ #
172
+ # Usage:
173
+ # get_python
174
+ # $PYTHON_CMD -m pennyfarthing_scripts.jira view MSSCI-12345
175
+ #
176
+ get_python() {
177
+ # Find project root if not set
178
+ if [[ -z "${PROJECT_ROOT:-}" ]]; then
179
+ local d="$PWD"
180
+ while [[ ! -d "$d/.claude" ]] && [[ "$d" != "/" ]]; do
181
+ d="$(dirname "$d")"
182
+ done
183
+ PROJECT_ROOT="$d"
184
+ fi
185
+
186
+ # Check for project venv first
187
+ if [[ -x "${PROJECT_ROOT}/.venv/bin/python" ]]; then
188
+ PYTHON_CMD="${PROJECT_ROOT}/.venv/bin/python"
189
+ elif command -v python3 &> /dev/null; then
190
+ PYTHON_CMD="python3"
191
+ elif command -v python &> /dev/null; then
192
+ PYTHON_CMD="python"
193
+ else
194
+ error "Python not found. Install Python 3.11+ or create .venv"
195
+ return 1
196
+ fi
197
+ }
198
+
199
+ # run_python_module MODULE [ARGS...]
200
+ # Run a pennyfarthing_scripts Python module with proper venv handling
201
+ #
202
+ # Usage:
203
+ # run_python_module jira view MSSCI-12345
204
+ # run_python_module sprint status
205
+ # run_python_module story size 3
206
+ #
207
+ run_python_module() {
208
+ get_python || return 1
209
+ local module="$1"
210
+ shift
211
+ exec $PYTHON_CMD -m "pennyfarthing_scripts.${module}" "$@"
212
+ }
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ # sidecar-health.sh - Check sidecar files for bloat and staleness
3
+ #
4
+ # Usage: sidecar-health.sh [--fix]
5
+ # --fix Offer to archive bloated files
6
+ #
7
+ # Thresholds:
8
+ # gotchas.md: 50 lines max
9
+ # patterns.md: 50 lines max
10
+ # decisions.md: 40 lines max
11
+
12
+ set -euo pipefail
13
+
14
+ # Find project root
15
+ if [[ -z "${PROJECT_ROOT:-}" ]]; then
16
+ d="$PWD"
17
+ while [[ ! -d "$d/.claude" ]] && [[ "$d" != "/" ]]; do
18
+ d="$(dirname "$d")"
19
+ done
20
+ PROJECT_ROOT="$d"
21
+ fi
22
+
23
+ SIDECAR_DIR="$PROJECT_ROOT/.pennyfarthing/sidecars"
24
+ FIX_MODE="${1:-}"
25
+
26
+ # Thresholds
27
+ GOTCHAS_MAX=50
28
+ PATTERNS_MAX=50
29
+ DECISIONS_MAX=40
30
+
31
+ # Colors
32
+ RED='\033[0;31m'
33
+ YELLOW='\033[1;33m'
34
+ GREEN='\033[0;32m'
35
+ NC='\033[0m' # No Color
36
+
37
+ echo "Sidecar Health Check"
38
+ echo "===================="
39
+ echo ""
40
+
41
+ issues_found=0
42
+
43
+ check_file() {
44
+ local file="$1"
45
+ local max_lines="$2"
46
+ local agent=$(basename "$(dirname "$file")")
47
+ local filename=$(basename "$file")
48
+
49
+ if [[ ! -f "$file" ]]; then
50
+ return
51
+ fi
52
+
53
+ local lines=$(wc -l < "$file" | tr -d ' ')
54
+
55
+ if [[ $lines -gt $max_lines ]]; then
56
+ echo -e "${RED}BLOATED${NC}: $agent/$filename ($lines lines, max $max_lines)"
57
+ issues_found=$((issues_found + 1))
58
+
59
+ if [[ "$FIX_MODE" == "--fix" ]]; then
60
+ local archive_dir="$PROJECT_ROOT/.pennyfarthing/sidecars/.archive"
61
+ local timestamp=$(date +%Y%m%d)
62
+ mkdir -p "$archive_dir"
63
+
64
+ echo " → Archiving to .archive/${agent}-${filename%.md}-${timestamp}.md"
65
+ cp "$file" "$archive_dir/${agent}-${filename%.md}-${timestamp}.md"
66
+ echo " → Original preserved. Manually prune $file to <$max_lines lines."
67
+ fi
68
+ elif [[ $lines -gt $((max_lines * 80 / 100)) ]]; then
69
+ echo -e "${YELLOW}WARNING${NC}: $agent/$filename ($lines lines, approaching $max_lines limit)"
70
+ else
71
+ echo -e "${GREEN}OK${NC}: $agent/$filename ($lines lines)"
72
+ fi
73
+ }
74
+
75
+ # Check all agent sidecars
76
+ for agent_dir in "$SIDECAR_DIR"/*/; do
77
+ if [[ -d "$agent_dir" ]]; then
78
+ agent=$(basename "$agent_dir")
79
+ [[ "$agent" == ".archive" ]] && continue
80
+
81
+ check_file "$agent_dir/gotchas.md" $GOTCHAS_MAX
82
+ check_file "$agent_dir/patterns.md" $PATTERNS_MAX
83
+ check_file "$agent_dir/decisions.md" $DECISIONS_MAX
84
+ fi
85
+ done
86
+
87
+ echo ""
88
+ if [[ $issues_found -gt 0 ]]; then
89
+ echo "Found $issues_found bloated file(s)."
90
+ if [[ "$FIX_MODE" != "--fix" ]]; then
91
+ echo "Run with --fix to archive and prepare for pruning."
92
+ fi
93
+ exit 1
94
+ else
95
+ echo "All sidecar files within limits."
96
+ exit 0
97
+ fi
@@ -90,8 +90,8 @@ elif [ -d "$PROJECT_ROOT/.session/agents" ]; then
90
90
  fi
91
91
  fi
92
92
 
93
- # Get character name from config for current agent
94
- # Config now includes theme_characters baked in (no symlink chasing needed)
93
+ # Get character name from theme file (single source of truth)
94
+ # Priority: .pennyfarthing/config.local.yaml > .claude/persona-config.yaml for theme name
95
95
  config_file=""
96
96
  if [ -f "$PROJECT_ROOT/.pennyfarthing/config.local.yaml" ]; then
97
97
  config_file="$PROJECT_ROOT/.pennyfarthing/config.local.yaml"
@@ -101,28 +101,33 @@ fi
101
101
 
102
102
  character_display=""
103
103
  if [ -n "$config_file" ] && [ -n "$agent_name" ]; then
104
- # Try baked theme_characters first (self-contained, no symlinks)
105
- full_name=$(yq ".theme_characters.${agent_name}" "$config_file" 2>/dev/null)
106
-
107
- if [ -n "$full_name" ] && [ "$full_name" != "null" ]; then
108
- # Smart character name extraction:
109
- # 1. Remove parenthetical content: "Breq (Justice of Toren)" → "Breq"
110
- # 2. Strip common titles: "Captain Kirk" "Kirk"
111
- # 3. If single word remains, use it; otherwise take last word
112
- clean_name=$(echo "$full_name" | sed 's/ *([^)]*)//g' | xargs)
113
- clean_name=$(echo "$clean_name" | sed -E 's/^(Captain|Lieutenant|Dr\.|Doc|Mr\.|Mrs\.|Ms\.|Admiral|Commander|Chief|Ensign|Translator|Agent|Colonel|Major|Sergeant|Professor|Lord|Lady|Sir|The) +//i')
114
- word_count=$(echo "$clean_name" | wc -w | tr -d ' ')
115
- if [ "$word_count" -eq 1 ]; then
116
- character_display="$clean_name"
117
- else
118
- character_display=$(echo "$clean_name" | awk '{print $NF}')
104
+ # Get theme name from config
105
+ theme=$(yq '.theme' "$config_file" 2>/dev/null)
106
+
107
+ if [ -n "$theme" ] && [ "$theme" != "null" ]; then
108
+ # Read character directly from theme file (matches agent-session.sh behavior)
109
+ theme_file="$PROJECT_ROOT/.pennyfarthing/personas/themes/${theme}.yaml"
110
+ if [ -f "$theme_file" ]; then
111
+ full_name=$(yq ".agents.${agent_name}.character" "$theme_file" 2>/dev/null)
112
+
113
+ if [ -n "$full_name" ] && [ "$full_name" != "null" ]; then
114
+ # Smart character name extraction:
115
+ # 1. Remove parenthetical content: "Breq (Justice of Toren)" → "Breq"
116
+ # 2. Strip common titles: "Captain Kirk" → "Kirk"
117
+ # 3. If single word remains, use it; otherwise take last word
118
+ clean_name=$(echo "$full_name" | sed 's/ *([^)]*)//g' | xargs)
119
+ clean_name=$(echo "$clean_name" | sed -E 's/^(Captain|Lieutenant|Dr\.|Doc|Mr\.|Mrs\.|Ms\.|Admiral|Commander|Chief|Ensign|Translator|Agent|Colonel|Major|Sergeant|Professor|Lord|Lady|Sir|The) +//i')
120
+ word_count=$(echo "$clean_name" | wc -w | tr -d ' ')
121
+ if [ "$word_count" -eq 1 ]; then
122
+ character_display="$clean_name"
123
+ else
124
+ character_display=$(echo "$clean_name" | awk '{print $NF}')
125
+ fi
126
+ fi
119
127
  fi
120
- fi
121
128
 
122
- # Fallback to theme name if no character found
123
- if [ -z "$character_display" ]; then
124
- theme=$(yq '.theme' "$config_file" 2>/dev/null)
125
- if [ -n "$theme" ] && [ "$theme" != "null" ]; then
129
+ # Fallback to theme name if no character found
130
+ if [ -z "$character_display" ]; then
126
131
  character_display="$(echo "${theme:0:1}" | tr '[:lower:]' '[:upper:]')${theme:1}"
127
132
  fi
128
133
  fi