@pennyfarthing/core 7.6.0 → 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 (70) 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 +37 -2
  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 +25 -8
  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 +393 -0
  48. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +20 -0
  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/tests/question-reflector.test.mjs +545 -0
  52. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +94 -0
  53. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +10 -152
  54. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +14 -4
  55. package/pennyfarthing-dist/scripts/jira/jira-sync.sh +12 -4
  56. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +11 -99
  57. package/pennyfarthing-dist/scripts/lib/common.sh +55 -0
  58. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +97 -0
  59. package/pennyfarthing-dist/scripts/misc/deploy.sh +13 -1
  60. package/pennyfarthing-dist/scripts/misc/statusline.sh +27 -22
  61. package/pennyfarthing-dist/scripts/story/create-story.sh +14 -154
  62. package/pennyfarthing-dist/scripts/story/size-story.sh +12 -192
  63. package/pennyfarthing-dist/scripts/story/story-template.sh +12 -156
  64. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +24 -93
  65. package/pennyfarthing-dist/scripts/test/swebench-judge.py +33 -59
  66. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +575 -0
  67. package/pennyfarthing-dist/scripts/workflow/check.py +502 -0
  68. package/pennyfarthing-dist/skills/skill-registry.yaml +52 -16
  69. package/pennyfarthing-dist/skills/sprint/skill.md +1 -1
  70. package/pennyfarthing-dist/templates/settings.local.json.template +11 -0
@@ -53,6 +53,7 @@ critical_threshold = $DEFAULT_CRITICAL_THRESHOLD
53
53
  max_tokens = $DEFAULT_MAX_TOKENS
54
54
  tirepump_threshold = $DEFAULT_TIREPUMP_THRESHOLD
55
55
  permission_mode = 'manual' # Default to manual
56
+ relay_mode = False # MSSCI-12395: Independent auto-handoff toggle
56
57
 
57
58
  # First try .pennyfarthing/config.local.yaml (preferred location)
58
59
  try:
@@ -67,9 +68,13 @@ try:
67
68
  critical_threshold = cb.get('critical_threshold', critical_threshold)
68
69
  max_tokens = cb.get('max_tokens', max_tokens)
69
70
  tirepump_threshold = cb.get('tirepump_threshold', tirepump_threshold)
70
- # Read permission_mode from workflow section
71
- if 'workflow' in config and 'permission_mode' in config['workflow']:
72
- permission_mode = config['workflow']['permission_mode']
71
+ # Read permission_mode and relay_mode from workflow section
72
+ if 'workflow' in config:
73
+ if 'permission_mode' in config['workflow']:
74
+ permission_mode = config['workflow']['permission_mode']
75
+ # MSSCI-12395: relay_mode for auto-handoff (independent of permission_mode)
76
+ if 'relay_mode' in config['workflow']:
77
+ relay_mode = config['workflow']['relay_mode'] == True
73
78
  except:
74
79
  # Fallback to settings.local.json (legacy location)
75
80
  try:
@@ -82,8 +87,12 @@ except:
82
87
  critical_threshold = cb.get('critical_threshold', critical_threshold)
83
88
  max_tokens = cb.get('max_tokens', max_tokens)
84
89
  tirepump_threshold = cb.get('tirepump_threshold', tirepump_threshold)
85
- if 'workflow' in settings and 'permission_mode' in settings['workflow']:
86
- permission_mode = settings['workflow']['permission_mode']
90
+ if 'workflow' in settings:
91
+ if 'permission_mode' in settings['workflow']:
92
+ permission_mode = settings['workflow']['permission_mode']
93
+ # MSSCI-12395: relay_mode for auto-handoff (independent of permission_mode)
94
+ if 'relay_mode' in settings['workflow']:
95
+ relay_mode = settings['workflow']['relay_mode'] == True
87
96
  except:
88
97
  pass
89
98
 
@@ -93,6 +102,7 @@ print(f'CRITICAL_THRESHOLD={critical_threshold}')
93
102
  print(f'MAX_TOKENS={max_tokens}')
94
103
  print(f'TIREPUMP_THRESHOLD={tirepump_threshold}')
95
104
  print(f'PERMISSION_MODE={permission_mode}')
105
+ print(f'RELAY_MODE={str(relay_mode).lower()}')
96
106
  " 2>/dev/null)
97
107
 
98
108
  # Apply config or use defaults
@@ -140,6 +150,7 @@ import json
140
150
  warning_threshold = $WARNING_THRESHOLD
141
151
  max_tokens = $MAX_TOKENS
142
152
  permission_mode = '$PERMISSION_MODE'
153
+ relay_mode = '$RELAY_MODE' == 'true' # MSSCI-12395: Independent auto-handoff toggle
143
154
  tirepump_threshold = $TIREPUMP_THRESHOLD # Threshold for TirePump auto-handoff (configurable)
144
155
 
145
156
  with open('$TRANSCRIPT', 'r') as f:
@@ -193,16 +204,22 @@ if last_total is not None:
193
204
  # Use usable percent for status decisions (more accurate for user)
194
205
  if usable_pct > warning_threshold:
195
206
  print('CONTEXT_STATUS=HIGH')
196
- print('HANDOFF_MODE=auto')
197
207
  else:
198
208
  print('CONTEXT_STATUS=OK')
209
+
210
+ # HANDOFF_MODE: 'auto' if relay_mode enabled, 'ask' otherwise
211
+ # MSSCI-12395: relay_mode controls autohandoff independent of context level
212
+ if relay_mode:
213
+ print('HANDOFF_MODE=auto')
214
+ else:
199
215
  print('HANDOFF_MODE=ask')
200
216
 
201
217
  # TirePump: Use CONTEXT_CLEAR (clear + load next agent) when:
202
- # 1. permission_mode is 'turbo' (auto-handoff enabled)
218
+ # 1. relay_mode is true (auto-handoff enabled) - MSSCI-12395
203
219
  # 2. context > 60% (tirepump_threshold)
204
220
  # This enables continuous autonomous runs without manual intervention
205
- use_tirepump = permission_mode == 'turbo' and usable_pct > tirepump_threshold
221
+ # Legacy: also support permission_mode == 'turbo' for backwards compatibility
222
+ use_tirepump = (relay_mode or permission_mode == 'turbo') and usable_pct > tirepump_threshold
206
223
  print(f'USE_TIREPUMP={str(use_tirepump).lower()}')
207
224
 
208
225
  # Cyclist detection: Multiple methods for robustness
@@ -2,12 +2,13 @@
2
2
  # prime.sh - Load essential project context at agent activation
3
3
  # Usage: prime.sh [--minimal] [--full] [--quiet] [--agent <name>]
4
4
  #
5
- # Loads context in priority order (CLAUDE.md skipped - already in system prompt):
6
- # 1. Sprint summary (current-sprint.yaml key fields)
7
- # 2. Active session (.session/*-session.md)
8
- # 3. Agent sidecar (if --agent provided)
9
- # 4. Agent behavior guide (combined protocols for all agents)
10
- # 5. Domain docs (--full only)
5
+ # Loads context in priority order (optimized for attention):
6
+ # 1. CLAUDE.md (already in system prompt - skipped)
7
+ # 2. Agent definition + behavior guide (HIGHEST PRIORITY - load first!)
8
+ # 3. Persona (already output by agent-session.sh before this runs)
9
+ # 4. Session summary (active work context)
10
+ # 5. Sidecars (patterns, gotchas, decisions - lowest priority)
11
+ # 6. Domain docs (--full only)
11
12
 
12
13
  set -euo pipefail
13
14
 
@@ -33,7 +34,7 @@ while [[ $# -gt 0 ]]; do
33
34
  echo " --minimal Skip all context (fastest)"
34
35
  echo " --full Include domain docs from .claude/project/"
35
36
  echo " --quiet Suppress section headers"
36
- echo " --agent <name> Load agent's sidecar patterns"
37
+ echo " --agent <name> Load agent definition and sidecar"
37
38
  exit 0
38
39
  ;;
39
40
  *) shift ;;
@@ -48,33 +49,61 @@ print_header() {
48
49
  fi
49
50
  }
50
51
 
51
- # CLAUDE.md is already loaded by Claude Code system prompt - skip it
52
-
53
52
  # Stop here for minimal mode
54
53
  if [[ "$MINIMAL" == "true" ]]; then
55
54
  exit 0
56
55
  fi
57
56
 
58
- # 1. Sprint summary (uses shared functions from sprint-common.sh)
57
+ # =============================================================================
58
+ # PRIORITY 1: Agent definition (HIGHEST ATTENTION ZONE)
59
+ # =============================================================================
60
+ # This is the most critical content - load it FIRST while attention is highest
61
+
62
+ if [[ -n "$AGENT_NAME" ]]; then
63
+ AGENT_FILE="$PROJECT_ROOT/.pennyfarthing/agents/${AGENT_NAME}.md"
64
+ if [[ -f "$AGENT_FILE" ]]; then
65
+ print_header "Agent Definition: ${AGENT_NAME}"
66
+ cat "$AGENT_FILE"
67
+ fi
68
+ fi
69
+
70
+ # =============================================================================
71
+ # PRIORITY 2: Agent behavior guide (shared protocols)
72
+ # =============================================================================
73
+
74
+ if [[ -n "$AGENT_NAME" ]]; then
75
+ BEHAVIOR_GUIDE="$PROJECT_ROOT/.pennyfarthing/guides/agent-behavior.md"
76
+ if [[ -f "$BEHAVIOR_GUIDE" ]]; then
77
+ print_header "Agent Behavior Guide"
78
+ cat "$BEHAVIOR_GUIDE"
79
+ fi
80
+ fi
81
+
82
+ # =============================================================================
83
+ # PRIORITY 3: Persona already loaded by agent-session.sh (before prime.sh runs)
84
+ # =============================================================================
85
+ # Nothing to do here - persona is output by agent-session.sh start
86
+
87
+ # =============================================================================
88
+ # PRIORITY 4: Session summary (active work context)
89
+ # =============================================================================
90
+
91
+ # Sprint summary (brief - just name and progress)
59
92
  if [[ -f "$(get_sprint_file)" ]]; then
60
93
  print_header "Sprint Context"
61
-
62
- # Use shared functions for consistent output
63
94
  summary=$(get_sprint_summary)
64
95
  if [[ -n "$summary" ]]; then
65
96
  echo "$summary"
66
97
  fi
67
-
68
98
  progress=$(get_sprint_progress)
69
99
  if [[ -n "$progress" ]]; then
70
100
  echo "$progress"
71
101
  fi
72
102
  fi
73
103
 
74
- # 2. Active session (if exists) - extract header metadata + current assessment
104
+ # Active session (if exists) - extract header metadata + current assessment
75
105
  SESSION_FILE=""
76
106
  if [[ -d "$PROJECT_ROOT/.session" ]]; then
77
- # Find a session file (typically only one active at a time)
78
107
  SESSION_FILE=$(find "$PROJECT_ROOT/.session" -maxdepth 1 -name "*-session.md" -type f 2>/dev/null | head -1)
79
108
  fi
80
109
 
@@ -82,18 +111,14 @@ if [[ -n "$SESSION_FILE" && -f "$SESSION_FILE" ]]; then
82
111
  print_header "Active Session: $(basename "$SESSION_FILE")"
83
112
 
84
113
  # Extract header (everything before first ## heading)
85
- # This includes: title, metadata fields (Phase, Workflow, Repos, Branch, etc.)
86
114
  awk '/^## / {exit} {print}' "$SESSION_FILE"
87
115
 
88
- # Find the most recent assessment section (workflow-agnostic)
89
- # Assessment sections are named: "## {Agent} Assessment" (TEA, Dev, Reviewer, SM, etc.)
90
- # Show the LAST one in the file as it represents current state
116
+ # Find the most recent assessment section
91
117
  last_assessment=$(grep -n '^## .*Assessment' "$SESSION_FILE" | tail -1 | cut -d: -f1)
92
118
 
93
119
  if [[ -n "$last_assessment" ]]; then
94
120
  echo ""
95
121
  echo "---"
96
- # Extract from that line to next ## or EOF
97
122
  awk -v start="$last_assessment" '
98
123
  NR >= start {
99
124
  if (NR > start && /^## /) exit
@@ -103,29 +128,29 @@ if [[ -n "$SESSION_FILE" && -f "$SESSION_FILE" ]]; then
103
128
  fi
104
129
  fi
105
130
 
106
- # 3. Agent sidecar (if --agent provided)
131
+ # =============================================================================
132
+ # PRIORITY 5: Sidecars (patterns, gotchas, decisions - LOWEST PRIORITY)
133
+ # =============================================================================
134
+ # These are supplementary - loaded last when attention is lower
135
+
107
136
  if [[ -n "$AGENT_NAME" ]]; then
108
137
  SIDECAR_DIR="$PROJECT_ROOT/.pennyfarthing/sidecars/${AGENT_NAME}"
109
138
  if [[ -d "$SIDECAR_DIR" ]]; then
110
- for pattern_file in "$SIDECAR_DIR"/*.md; do
139
+ # Load in specific order: patterns first (most useful), then gotchas, then decisions
140
+ for filename in patterns.md gotchas.md decisions.md; do
141
+ pattern_file="$SIDECAR_DIR/$filename"
111
142
  if [[ -f "$pattern_file" ]]; then
112
- print_header "Agent Sidecar: $(basename "$pattern_file")"
143
+ print_header "Agent Sidecar: $filename"
113
144
  cat "$pattern_file"
114
145
  fi
115
146
  done
116
147
  fi
117
148
  fi
118
149
 
119
- # 4. Agent behavior guide (combined protocols for all agents)
120
- if [[ -n "$AGENT_NAME" ]]; then
121
- BEHAVIOR_GUIDE="$PROJECT_ROOT/.pennyfarthing/guides/agent-behavior.md"
122
- if [[ -f "$BEHAVIOR_GUIDE" ]]; then
123
- print_header "Agent Behavior Guide"
124
- cat "$BEHAVIOR_GUIDE"
125
- fi
126
- fi
150
+ # =============================================================================
151
+ # PRIORITY 6: Domain docs (--full only, rarely used)
152
+ # =============================================================================
127
153
 
128
- # 5. Domain docs (--full only)
129
154
  if [[ "$FULL" == "true" ]]; then
130
155
  for doc in "$PROJECT_ROOT/.claude/project"/CLAUDE-*.md; do
131
156
  if [[ -f "$doc" ]]; then
@@ -143,6 +143,7 @@ echo " Branch: $BRANCH_NAME"
143
143
  echo " Repos: $REPOS"
144
144
 
145
145
  # Process repos based on selection
146
+ # When processing multiple repos, run in parallel for faster network I/O
146
147
  case "$REPOS" in
147
148
  api)
148
149
  create_or_checkout_branch "$REPO_BASE/Pennyfarthing-api" "Pennyfarthing-api"
@@ -151,8 +152,42 @@ case "$REPOS" in
151
152
  create_or_checkout_branch "$REPO_BASE/Pennyfarthing-ui" "Pennyfarthing-ui"
152
153
  ;;
153
154
  all)
154
- create_or_checkout_branch "$REPO_BASE/Pennyfarthing-api" "Pennyfarthing-api"
155
- create_or_checkout_branch "$REPO_BASE/Pennyfarthing-ui" "Pennyfarthing-ui"
155
+ # Parallel execution for both repos
156
+ tmpdir=$(mktemp -d)
157
+ trap "rm -rf '$tmpdir'" EXIT
158
+ HAD_ERRORS=false
159
+
160
+ # Run both in parallel, capturing output
161
+ # Write to separate files to avoid race condition on shared file
162
+ (
163
+ create_or_checkout_branch "$REPO_BASE/Pennyfarthing-api" "Pennyfarthing-api"
164
+ echo "$REPO_BASE/Pennyfarthing-api:Pennyfarthing-api" > "$tmpdir/api.processed"
165
+ ) > "$tmpdir/api.out" 2>&1 &
166
+ pid_api=$!
167
+
168
+ (
169
+ create_or_checkout_branch "$REPO_BASE/Pennyfarthing-ui" "Pennyfarthing-ui"
170
+ echo "$REPO_BASE/Pennyfarthing-ui:Pennyfarthing-ui" > "$tmpdir/ui.processed"
171
+ ) > "$tmpdir/ui.out" 2>&1 &
172
+ pid_ui=$!
173
+
174
+ # Wait for both and capture exit codes
175
+ wait $pid_api; rc_api=$?
176
+ wait $pid_ui; rc_ui=$?
177
+
178
+ # Show output in order
179
+ [ -f "$tmpdir/api.out" ] && cat "$tmpdir/api.out"
180
+ [ -f "$tmpdir/ui.out" ] && cat "$tmpdir/ui.out"
181
+
182
+ # Rebuild PROCESSED_REPOS from separate files (avoids race condition)
183
+ [ -f "$tmpdir/api.processed" ] && PROCESSED_REPOS+=("$(cat "$tmpdir/api.processed")")
184
+ [ -f "$tmpdir/ui.processed" ] && PROCESSED_REPOS+=("$(cat "$tmpdir/ui.processed")")
185
+
186
+ # Check for failures
187
+ if [ $rc_api -ne 0 ] || [ $rc_ui -ne 0 ]; then
188
+ echo "⚠️ Some repos had errors"
189
+ HAD_ERRORS=true
190
+ fi
156
191
  ;;
157
192
  esac
158
193
 
@@ -222,5 +257,11 @@ done
222
257
 
223
258
  echo ""
224
259
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
225
- echo " Done! All branches verified and ready."
226
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
260
+ if [ "$HAD_ERRORS" = true ]; then
261
+ echo "⚠️ Done with errors. Check output above."
262
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
263
+ exit 1
264
+ else
265
+ echo "✅ Done! All branches verified and ready."
266
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
267
+ fi
@@ -82,15 +82,26 @@ if ! $BRIEF; then
82
82
  echo ""
83
83
  fi
84
84
 
85
- # Check each repo from configuration
85
+ # Check each repo from configuration (parallelized for performance)
86
86
  repo_count=$(get_repo_count)
87
87
  if [[ "$repo_count" -eq 0 ]]; then
88
88
  # No repos configured, just show current directory
89
89
  show_repo_status "Project" "$PROJECT_ROOT"
90
90
  else
91
+ # Create temp dir for parallel output capture
92
+ tmpdir=$(mktemp -d)
93
+ trap "rm -rf '$tmpdir'" EXIT
94
+
95
+ # Launch status checks in parallel
91
96
  for repo in $(get_repos); do
92
97
  repo_path=$(get_repo_full_path "$repo")
93
- show_repo_status "$repo" "$repo_path"
98
+ (show_repo_status "$repo" "$repo_path" > "$tmpdir/$repo.out" 2>&1) &
99
+ done
100
+ wait
101
+
102
+ # Output results in order
103
+ for repo in $(get_repos); do
104
+ [ -f "$tmpdir/$repo.out" ] && cat "$tmpdir/$repo.out"
94
105
  done
95
106
  fi
96
107
 
@@ -108,14 +119,28 @@ if ! $BRIEF; then
108
119
  unpushed=$(git -C "$PROJECT_ROOT" log origin/develop..HEAD --oneline 2>/dev/null | wc -l | tr -d ' ')
109
120
  total_unpushed=$((total_unpushed + unpushed))
110
121
  else
122
+ # Parallelize summary collection
123
+ summary_tmp=$(mktemp -d)
111
124
  for repo in $(get_repos); do
112
125
  repo_path=$(get_repo_full_path "$repo")
113
- [ -d "$repo_path/.git" ] || [ -d "$repo_path" ] || continue
114
- count=$(git -C "$repo_path" status --short 2>/dev/null | wc -l | tr -d ' ')
115
- total_changes=$((total_changes + count))
116
- unpushed=$(git -C "$repo_path" log origin/develop..HEAD --oneline 2>/dev/null | wc -l | tr -d ' ')
117
- total_unpushed=$((total_unpushed + unpushed))
126
+ (
127
+ [ -d "$repo_path/.git" ] || [ -d "$repo_path" ] || exit 0
128
+ count=$(git -C "$repo_path" status --short 2>/dev/null | wc -l | tr -d ' ')
129
+ unpushed=$(git -C "$repo_path" log origin/develop..HEAD --oneline 2>/dev/null | wc -l | tr -d ' ')
130
+ echo "$count $unpushed" > "$summary_tmp/$repo.count"
131
+ ) &
132
+ done
133
+ wait
134
+
135
+ # Aggregate results
136
+ for repo in $(get_repos); do
137
+ if [ -f "$summary_tmp/$repo.count" ]; then
138
+ read count unpushed < "$summary_tmp/$repo.count"
139
+ total_changes=$((total_changes + count))
140
+ total_unpushed=$((total_unpushed + unpushed))
141
+ fi
118
142
  done
143
+ rm -rf "$summary_tmp"
119
144
  fi
120
145
 
121
146
  if [ "$total_changes" -eq 0 ] && [ "$total_unpushed" -eq 0 ]; then
@@ -8,7 +8,7 @@
8
8
  # Claude's next API call.
9
9
  #
10
10
  # Configuration files:
11
- # .pennyfarthing/bell-mode.json - { "enabled": true/false }
11
+ # .pennyfarthing/config.local.yaml - workflow.bell_mode: true/false
12
12
  # .pennyfarthing/bell-queue.json - [{ "text": "...", "images": [...] }, ...]
13
13
  #
14
14
  # Output format (when injecting):
@@ -35,15 +35,17 @@ if [[ ! -d "$PROJECT_ROOT/.pennyfarthing" ]]; then
35
35
  exit 0
36
36
  fi
37
37
 
38
- BELL_MODE_CONFIG="$PROJECT_ROOT/.pennyfarthing/bell-mode.json"
38
+ CONFIG_LOCAL_YAML="$PROJECT_ROOT/.pennyfarthing/config.local.yaml"
39
39
  BELL_QUEUE_FILE="$PROJECT_ROOT/.pennyfarthing/bell-queue.json"
40
40
 
41
- # Check if bell mode is enabled
42
- if [[ ! -f "$BELL_MODE_CONFIG" ]]; then
41
+ # Check if bell mode is enabled in config.local.yaml
42
+ if [[ ! -f "$CONFIG_LOCAL_YAML" ]]; then
43
43
  exit 0
44
44
  fi
45
45
 
46
- ENABLED=$(cat "$BELL_MODE_CONFIG" 2>/dev/null | grep -o '"enabled"[[:space:]]*:[[:space:]]*true' || true)
46
+ # Parse YAML to check workflow.bell_mode - look for "bell_mode: true"
47
+ # This handles both "bell_mode: true" and " bell_mode: true" (indented under workflow)
48
+ ENABLED=$(grep -E '^\s*bell_mode:\s*true' "$CONFIG_LOCAL_YAML" 2>/dev/null || true)
47
49
  if [[ -z "$ENABLED" ]]; then
48
50
  exit 0
49
51
  fi
@@ -67,6 +69,13 @@ if [[ -z "$FIRST_MESSAGE_TEXT" ]]; then
67
69
  exit 0
68
70
  fi
69
71
 
72
+ # Get Cyclist port (if running)
73
+ CYCLIST_PORT=""
74
+ PORT_FILE="$PROJECT_ROOT/.cyclist-port"
75
+ if [[ -f "$PORT_FILE" ]]; then
76
+ CYCLIST_PORT=$(cat "$PORT_FILE" 2>/dev/null)
77
+ fi
78
+
70
79
  # Output the hook response JSON
71
80
  cat << EOF
72
81
  {
@@ -77,11 +86,21 @@ cat << EOF
77
86
  }
78
87
  EOF
79
88
 
80
- # Remove the first message from the queue (for next invocation)
81
- # Use jq if available, otherwise leave queue management to the TypeScript side
82
- # Run in background and ignore errors to avoid blocking hook response
83
- if command -v jq &> /dev/null; then
84
- (jq 'if length > 0 then .[1:] else [] end' "$BELL_QUEUE_FILE" > "$BELL_QUEUE_FILE.tmp" 2>/dev/null && mv "$BELL_QUEUE_FILE.tmp" "$BELL_QUEUE_FILE") &
85
- fi
89
+ # Remove the first message from the queue and notify Cyclist
90
+ # Run in background to avoid blocking hook response
91
+ (
92
+ # Dequeue using jq if available
93
+ if command -v jq &> /dev/null; then
94
+ jq 'if length > 0 then .[1:] else [] end' "$BELL_QUEUE_FILE" > "$BELL_QUEUE_FILE.tmp" 2>/dev/null && mv "$BELL_QUEUE_FILE.tmp" "$BELL_QUEUE_FILE"
95
+ fi
96
+
97
+ # Notify Cyclist browser to dequeue and display the message
98
+ if [[ -n "$CYCLIST_PORT" ]] && [[ "$CYCLIST_PORT" =~ ^[0-9]+$ ]]; then
99
+ curl -s -X POST "http://localhost:$CYCLIST_PORT/api/bell-consumed" \
100
+ -H "Content-Type: application/json" \
101
+ -d "{\"text\": \"$FIRST_MESSAGE_TEXT\"}" \
102
+ >/dev/null 2>&1 || true
103
+ fi
104
+ ) &
86
105
 
87
106
  exit 0
@@ -1,8 +1,10 @@
1
1
  #!/bin/bash
2
- # pre-commit.sh - Git hook to enforce branch protection
2
+ # pre-commit.sh - Git hook to enforce branch protection and agent validation
3
3
  #
4
- # Prevents direct commits to protected branches (main, develop).
5
- # Exception: sprint/ folder commits allowed on develop for administrative tracking.
4
+ # Checks:
5
+ # 1. Prevents direct commits to protected branches (main, develop)
6
+ # Exception: sprint/ folder commits allowed on develop
7
+ # 2. Validates agent files when pennyfarthing-dist/agents/*.md is modified
6
8
  #
7
9
  # Installation:
8
10
  # Installed to .git/hooks/pre-commit by pennyfarthing init or doctor --fix
@@ -10,6 +12,19 @@
10
12
 
11
13
  set -uo pipefail
12
14
 
15
+ # Find project root
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ # Handle both direct execution and symlink from .git/hooks
18
+ if [[ "$SCRIPT_DIR" == *".git/hooks"* ]]; then
19
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
20
+ else
21
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
22
+ fi
23
+
24
+ # =============================================================================
25
+ # Check 1: Branch Protection
26
+ # =============================================================================
27
+
13
28
  BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
14
29
  PROTECTED_BRANCHES="^(main|develop)$"
15
30
 
@@ -21,30 +36,72 @@ if [[ $BRANCH =~ $PROTECTED_BRANCHES ]]; then
21
36
 
22
37
  # If all staged files are in sprint/, allow the commit
23
38
  if [ -z "$NON_SPRINT_FILES" ] && [ -n "$STAGED_FILES" ]; then
24
- exit 0
39
+ # Continue to agent validation check
40
+ :
41
+ else
42
+ echo ""
43
+ echo "COMMIT BLOCKED"
44
+ echo ""
45
+ echo "You are trying to commit directly to: $BRANCH"
46
+ echo "This violates the git workflow rules."
47
+ echo ""
48
+ echo "Protected branches: main, develop"
49
+ echo ""
50
+ echo "What to do:"
51
+ echo "1. Create a feature branch:"
52
+ echo " git checkout -b <type>/<epic-story>-<description>"
53
+ echo ""
54
+ echo "2. Example:"
55
+ echo " git checkout -b feat/8-2-add-authentication"
56
+ echo ""
57
+ echo "3. Then commit your changes on the feature branch"
58
+ echo ""
59
+ echo "Exception: Sprint tracking files (sprint/*) can be committed to develop"
60
+ echo ""
61
+ exit 1
25
62
  fi
63
+ else
64
+ echo ""
65
+ echo "COMMIT BLOCKED - Cannot commit directly to $BRANCH"
66
+ echo ""
67
+ exit 1
26
68
  fi
69
+ fi
27
70
 
71
+ # =============================================================================
72
+ # Check 2: Agent File Validation
73
+ # =============================================================================
74
+
75
+ STAGED_FILES=$(git diff --cached --name-only 2>/dev/null || true)
76
+ AGENT_FILES=$(echo "$STAGED_FILES" | grep "^pennyfarthing-dist/agents/.*\.md$" || true)
77
+
78
+ if [[ -n "$AGENT_FILES" ]]; then
79
+ echo "Agent files staged for commit:"
80
+ echo "$AGENT_FILES" | sed 's/^/ /'
28
81
  echo ""
29
- echo "COMMIT BLOCKED"
30
- echo ""
31
- echo "You are trying to commit directly to: $BRANCH"
32
- echo "This violates the git workflow rules."
33
- echo ""
34
- echo "Protected branches: main, develop"
35
- echo ""
36
- echo "What to do:"
37
- echo "1. Create a feature branch:"
38
- echo " git checkout -b <type>/<epic-story>-<description>"
39
- echo ""
40
- echo "2. Example:"
41
- echo " git checkout -b feat/8-2-add-authentication"
42
- echo ""
43
- echo "3. Then commit your changes on the feature branch"
44
- echo ""
45
- echo "Exception: Sprint tracking files (sprint/*) can be committed to develop"
46
- echo ""
47
- exit 1
82
+
83
+ VALIDATOR="$PROJECT_ROOT/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh"
84
+
85
+ if [[ -x "$VALIDATOR" ]]; then
86
+ echo "Running agent schema validation..."
87
+ echo ""
88
+
89
+ if ! "$VALIDATOR"; then
90
+ echo ""
91
+ echo "COMMIT BLOCKED - Agent validation failed"
92
+ echo ""
93
+ echo "Fix the validation errors above and try again."
94
+ echo "Run 'just validate-agents -v' for detailed output."
95
+ echo ""
96
+ exit 1
97
+ fi
98
+
99
+ echo ""
100
+ echo "✓ Agent validation passed"
101
+ else
102
+ echo "Warning: Agent validator not found at $VALIDATOR"
103
+ echo "Skipping agent validation."
104
+ fi
48
105
  fi
49
106
 
50
107
  exit 0