@pennyfarthing/core 7.4.1 → 7.6.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 (127) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/cli/commands/doctor-legacy.test.d.ts +13 -0
  3. package/packages/core/dist/cli/commands/doctor-legacy.test.d.ts.map +1 -0
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js +207 -0
  5. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -0
  6. package/packages/core/dist/cli/commands/doctor.d.ts +16 -0
  7. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  8. package/packages/core/dist/cli/commands/doctor.js +130 -2
  9. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  10. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  11. package/packages/core/dist/cli/commands/init.js +17 -27
  12. package/packages/core/dist/cli/commands/init.js.map +1 -1
  13. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  14. package/packages/core/dist/cli/commands/update.js +21 -52
  15. package/packages/core/dist/cli/commands/update.js.map +1 -1
  16. package/packages/core/dist/cli/utils/symlinks.d.ts +15 -0
  17. package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
  18. package/packages/core/dist/cli/utils/symlinks.js +148 -2
  19. package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
  20. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
  21. package/packages/core/dist/cli/utils/themes.js +9 -0
  22. package/packages/core/dist/cli/utils/themes.js.map +1 -1
  23. package/pennyfarthing-dist/agents/dev.md +29 -24
  24. package/pennyfarthing-dist/agents/handoff.md +42 -119
  25. package/pennyfarthing-dist/agents/reviewer.md +32 -37
  26. package/pennyfarthing-dist/agents/sm-handoff.md +43 -66
  27. package/pennyfarthing-dist/agents/sm.md +52 -35
  28. package/pennyfarthing-dist/agents/tea.md +25 -8
  29. package/pennyfarthing-dist/agents/testing-runner.md +4 -4
  30. package/pennyfarthing-dist/commands/architect.md +0 -55
  31. package/pennyfarthing-dist/commands/dev.md +1 -54
  32. package/pennyfarthing-dist/commands/devops.md +0 -52
  33. package/pennyfarthing-dist/commands/health-check.md +33 -0
  34. package/pennyfarthing-dist/commands/orchestrator.md +0 -49
  35. package/pennyfarthing-dist/commands/pm.md +0 -53
  36. package/pennyfarthing-dist/commands/reviewer.md +1 -58
  37. package/pennyfarthing-dist/commands/sm.md +1 -64
  38. package/pennyfarthing-dist/commands/sprint.md +133 -0
  39. package/pennyfarthing-dist/commands/standalone.md +194 -0
  40. package/pennyfarthing-dist/commands/tea.md +1 -57
  41. package/pennyfarthing-dist/commands/tech-writer.md +0 -46
  42. package/pennyfarthing-dist/commands/theme-maker.md +10 -5
  43. package/pennyfarthing-dist/commands/ux-designer.md +0 -55
  44. package/pennyfarthing-dist/guides/XML-TAGS.md +156 -0
  45. package/pennyfarthing-dist/guides/agent-behavior.md +64 -38
  46. package/pennyfarthing-dist/guides/measurement-framework.md +210 -0
  47. package/pennyfarthing-dist/personas/themes/a-team.yaml +130 -0
  48. package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +1 -1
  49. package/pennyfarthing-dist/personas/themes/ancient-strategists.yaml +1 -1
  50. package/pennyfarthing-dist/personas/themes/arcane.yaml +1 -1
  51. package/pennyfarthing-dist/personas/themes/better-call-saul.yaml +1 -1
  52. package/pennyfarthing-dist/personas/themes/big-lebowski.yaml +1 -1
  53. package/pennyfarthing-dist/personas/themes/black-sails.yaml +1 -1
  54. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +1 -1
  55. package/pennyfarthing-dist/personas/themes/bobiverse.yaml +1 -1
  56. package/pennyfarthing-dist/personas/themes/breaking-bad.yaml +1 -1
  57. package/pennyfarthing-dist/personas/themes/count-of-monte-cristo.yaml +1 -1
  58. package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +1 -1
  59. package/pennyfarthing-dist/personas/themes/deadwood.yaml +1 -1
  60. package/pennyfarthing-dist/personas/themes/dickens.yaml +1 -1
  61. package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
  62. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +1 -1
  63. package/pennyfarthing-dist/personas/themes/don-quixote.yaml +1 -1
  64. package/pennyfarthing-dist/personas/themes/dune.yaml +1 -1
  65. package/pennyfarthing-dist/personas/themes/enlightenment-thinkers.yaml +1 -1
  66. package/pennyfarthing-dist/personas/themes/expeditionary-force.yaml +1 -1
  67. package/pennyfarthing-dist/personas/themes/futurama.yaml +1 -1
  68. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +1 -1
  69. package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +131 -1
  70. package/pennyfarthing-dist/personas/themes/gothic-literature.yaml +1 -1
  71. package/pennyfarthing-dist/personas/themes/great-gatsby.yaml +1 -1
  72. package/pennyfarthing-dist/personas/themes/hannibal.yaml +1 -1
  73. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +1 -1
  74. package/pennyfarthing-dist/personas/themes/his-dark-materials.yaml +1 -1
  75. package/pennyfarthing-dist/personas/themes/inspector-morse.yaml +1 -1
  76. package/pennyfarthing-dist/personas/themes/jane-austen.yaml +1 -1
  77. package/pennyfarthing-dist/personas/themes/legion-of-doom.yaml +130 -0
  78. package/pennyfarthing-dist/personas/themes/mad-max.yaml +1 -1
  79. package/pennyfarthing-dist/personas/themes/moby-dick.yaml +1 -1
  80. package/pennyfarthing-dist/personas/themes/neuromancer.yaml +1 -1
  81. package/pennyfarthing-dist/personas/themes/parks-and-rec.yaml +130 -0
  82. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +130 -0
  83. package/pennyfarthing-dist/personas/themes/renaissance-masters.yaml +1 -1
  84. package/pennyfarthing-dist/personas/themes/russian-masters.yaml +1 -1
  85. package/pennyfarthing-dist/personas/themes/sandman.yaml +1 -1
  86. package/pennyfarthing-dist/personas/themes/scientific-revolutionaries.yaml +1 -1
  87. package/pennyfarthing-dist/personas/themes/shakespeare.yaml +1 -1
  88. package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +139 -3
  89. package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +124 -0
  90. package/pennyfarthing-dist/personas/themes/star-wars.yaml +1 -1
  91. package/pennyfarthing-dist/personas/themes/succession.yaml +1 -1
  92. package/pennyfarthing-dist/personas/themes/superfriends.yaml +131 -1
  93. package/pennyfarthing-dist/personas/themes/ted-lasso.yaml +131 -1
  94. package/pennyfarthing-dist/personas/themes/the-americans.yaml +1 -1
  95. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +131 -1
  96. package/pennyfarthing-dist/personas/themes/the-good-place.yaml +1 -1
  97. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +1 -1
  98. package/pennyfarthing-dist/personas/themes/the-sopranos.yaml +1 -1
  99. package/pennyfarthing-dist/personas/themes/west-wing.yaml +6 -6
  100. package/pennyfarthing-dist/personas/themes/world-explorers.yaml +1 -1
  101. package/pennyfarthing-dist/personas/themes/wwii-leaders.yaml +1 -1
  102. package/pennyfarthing-dist/scripts/core/check-context.sh +23 -6
  103. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +95 -0
  104. package/pennyfarthing-dist/scripts/git/release.sh +3 -2
  105. package/pennyfarthing-dist/scripts/health/drift-detection.sh +162 -0
  106. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +87 -0
  107. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
  108. package/pennyfarthing-dist/scripts/misc/deploy.sh +1 -1
  109. package/pennyfarthing-dist/scripts/misc/statusline.sh +25 -32
  110. package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +377 -0
  111. package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +9 -0
  112. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +492 -0
  113. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -200
  114. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +38 -5
  115. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +40 -0
  116. package/pennyfarthing-dist/skills/theme-creation/SKILL.md +12 -7
  117. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-04-final-validation.md +11 -3
  118. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +122 -0
  119. package/pennyfarthing-dist/workflows/epics-and-stories/workflow.yaml +3 -2
  120. package/packages/core/dist/workflow/generic-handoff.d.ts +0 -281
  121. package/packages/core/dist/workflow/generic-handoff.d.ts.map +0 -1
  122. package/packages/core/dist/workflow/generic-handoff.js +0 -411
  123. package/packages/core/dist/workflow/generic-handoff.js.map +0 -1
  124. package/packages/core/dist/workflow/generic-handoff.test.d.ts +0 -21
  125. package/packages/core/dist/workflow/generic-handoff.test.d.ts.map +0 -1
  126. package/packages/core/dist/workflow/generic-handoff.test.js +0 -499
  127. package/packages/core/dist/workflow/generic-handoff.test.js.map +0 -1
@@ -12,14 +12,14 @@ theme:
12
12
  default_emoji_use: none
13
13
  default_humor: subtle
14
14
  character_immersion: high
15
- user_title: Sir/Ma'am
16
- portrait_style: ", political drama, White House corridors, political cartoon style"
17
- tier: A
15
+ user_title: Staffer
16
+ portrait_style: ", 19th century political cartoon style, bold ink linework, ink on paper, muted colors"
17
+ tier: S
18
18
 
19
19
  agents:
20
20
  orchestrator:
21
21
  character: President Josiah Bartlet
22
- visual: "Distinguished older man with silver hair, piercing blue eyes, navy suit with Presidential seal pin, warm smile. Hands gesturing mid-lecture in Oval Office, Resolute desk and flag behind, golden afternoon light"
22
+ visual: "Distinguished older man with silver hair, piercing blue eyes, dark suit with Presidential seal pin, warm smile. Hands gesturing mid-lecture in Oval Office, Resolute desk and flag behind, golden afternoon light"
23
23
  ocean:
24
24
  O: 5
25
25
  C: 5
@@ -46,7 +46,7 @@ agents:
46
46
  shortName: Jed
47
47
  sm:
48
48
  character: Leo McGarry
49
- visual: "Weathered man in rumpled shirtsleeves, loosened tie, tired warm eyes, coffee cup in hand. Standing in cramped Chief of Staff office, papers stacked everywhere, republic's weight on shoulders, determination in jaw"
49
+ visual: "Older man with lined weathered face, disheveled silver hair, weary eyes behind reading glasses, rumpled white shirt with loosened tie, coffee cup in hand. Cramped West Wing office piled high with papers and briefing books"
50
50
  ocean:
51
51
  O: 3
52
52
  C: 5
@@ -129,7 +129,7 @@ agents:
129
129
  shortName: Toby
130
130
  reviewer:
131
131
  character: Josh Lyman
132
- visual: "Rumpled, energetic man with wild curly hair, intense brown eyes, sleeves rolled up, backpack over shoulder. Gesturing emphatically, striding through corridors at breakneck speed, Donna behind, brilliance and chaos in wake"
132
+ visual: "Rumpled, energetic man with wild curly hair, intense eyes, sleeves rolled up, backpack over shoulder. Gesturing emphatically, striding through corridors at breakneck speed"
133
133
  ocean:
134
134
  O: 4
135
135
  C: 4
@@ -13,7 +13,7 @@ theme:
13
13
  character_immersion: high
14
14
  user_title: Expedition Member
15
15
  portrait_style: ", Age of Exploration, antique map parchment, weathered adventurer, expedition documentary style"
16
- tier: A
16
+ tier: B
17
17
 
18
18
  agents:
19
19
  orchestrator:
@@ -13,7 +13,7 @@ theme:
13
13
  character_immersion: high
14
14
  user_title: Colleague
15
15
  portrait_style: ", WWII command, 1940s sepia photography, military uniform, Allied leadership newsreel aesthetic"
16
- tier: A
16
+ tier: B
17
17
 
18
18
  agents:
19
19
  orchestrator:
@@ -33,11 +33,12 @@ done
33
33
  PROJECT_DIR="${PROJECT_ROOT:-$(pwd)}"
34
34
  CLAUDE_PROJECT_PATH="$HOME/.claude/projects/$(echo "$PROJECT_DIR" | tr '/' '-')"
35
35
 
36
- # Default thresholds (can be overridden by settings.local.json)
36
+ # Default thresholds (can be overridden by settings.local.json or config.local.yaml)
37
37
  DEFAULT_IMMINENT_THRESHOLD=65
38
38
  DEFAULT_WARNING_THRESHOLD=60
39
39
  DEFAULT_CRITICAL_THRESHOLD=85
40
40
  DEFAULT_MAX_TOKENS=200000
41
+ DEFAULT_TIREPUMP_THRESHOLD=60
41
42
 
42
43
  # Load thresholds and permission_mode from .pennyfarthing/config.local.yaml (preferred) or settings.local.json (fallback)
43
44
  PENNYFARTHING_CONFIG="${CLAUDE_PROJECT_DIR:-$PROJECT_DIR}/.pennyfarthing/config.local.yaml"
@@ -50,6 +51,7 @@ imminent_threshold = $DEFAULT_IMMINENT_THRESHOLD
50
51
  warning_threshold = $DEFAULT_WARNING_THRESHOLD
51
52
  critical_threshold = $DEFAULT_CRITICAL_THRESHOLD
52
53
  max_tokens = $DEFAULT_MAX_TOKENS
54
+ tirepump_threshold = $DEFAULT_TIREPUMP_THRESHOLD
53
55
  permission_mode = 'manual' # Default to manual
54
56
 
55
57
  # First try .pennyfarthing/config.local.yaml (preferred location)
@@ -64,6 +66,7 @@ try:
64
66
  warning_threshold = cb.get('warning_threshold', warning_threshold)
65
67
  critical_threshold = cb.get('critical_threshold', critical_threshold)
66
68
  max_tokens = cb.get('max_tokens', max_tokens)
69
+ tirepump_threshold = cb.get('tirepump_threshold', tirepump_threshold)
67
70
  # Read permission_mode from workflow section
68
71
  if 'workflow' in config and 'permission_mode' in config['workflow']:
69
72
  permission_mode = config['workflow']['permission_mode']
@@ -78,6 +81,7 @@ except:
78
81
  warning_threshold = cb.get('warning_threshold', warning_threshold)
79
82
  critical_threshold = cb.get('critical_threshold', critical_threshold)
80
83
  max_tokens = cb.get('max_tokens', max_tokens)
84
+ tirepump_threshold = cb.get('tirepump_threshold', tirepump_threshold)
81
85
  if 'workflow' in settings and 'permission_mode' in settings['workflow']:
82
86
  permission_mode = settings['workflow']['permission_mode']
83
87
  except:
@@ -87,6 +91,7 @@ print(f'IMMINENT_THRESHOLD={imminent_threshold}')
87
91
  print(f'WARNING_THRESHOLD={warning_threshold}')
88
92
  print(f'CRITICAL_THRESHOLD={critical_threshold}')
89
93
  print(f'MAX_TOKENS={max_tokens}')
94
+ print(f'TIREPUMP_THRESHOLD={tirepump_threshold}')
90
95
  print(f'PERMISSION_MODE={permission_mode}')
91
96
  " 2>/dev/null)
92
97
 
@@ -96,6 +101,7 @@ eval "$CONFIG" 2>/dev/null || {
96
101
  WARNING_THRESHOLD=$DEFAULT_WARNING_THRESHOLD
97
102
  CRITICAL_THRESHOLD=$DEFAULT_CRITICAL_THRESHOLD
98
103
  MAX_TOKENS=$DEFAULT_MAX_TOKENS
104
+ TIREPUMP_THRESHOLD=$DEFAULT_TIREPUMP_THRESHOLD
99
105
  }
100
106
 
101
107
  # Find transcript - either specific session or most recent
@@ -134,7 +140,7 @@ import json
134
140
  warning_threshold = $WARNING_THRESHOLD
135
141
  max_tokens = $MAX_TOKENS
136
142
  permission_mode = '$PERMISSION_MODE'
137
- tirepump_threshold = 60 # Threshold for TirePump auto-handoff
143
+ tirepump_threshold = $TIREPUMP_THRESHOLD # Threshold for TirePump auto-handoff (configurable)
138
144
 
139
145
  with open('$TRANSCRIPT', 'r') as f:
140
146
  lines = f.readlines()
@@ -199,10 +205,21 @@ if last_total is not None:
199
205
  use_tirepump = permission_mode == 'turbo' and usable_pct > tirepump_threshold
200
206
  print(f'USE_TIREPUMP={str(use_tirepump).lower()}')
201
207
 
202
- # Cyclist detection: Check if CYCLIST env var is set to '1'
203
- # Markers should only be emitted when running inside Cyclist
208
+ # Cyclist detection: Multiple methods for robustness
209
+ # 1. CYCLIST env var set to '1' (Electron mode - Cyclist spawns Claude)
210
+ # 2. .cyclist-port file exists (Web mode - Claude connects to running Cyclist)
204
211
  import os
212
+ from pathlib import Path
205
213
  is_cyclist = os.environ.get('CYCLIST', '') == '1'
214
+ if not is_cyclist:
215
+ # Check for .cyclist-port file in packages/cyclist directory
216
+ # This indicates Cyclist is running in web mode
217
+ project_dir = os.environ.get('CYCLIST_PROJECT_DIR', os.getcwd())
218
+ port_file = Path(project_dir) / 'packages' / 'cyclist' / '.cyclist-port'
219
+ if not port_file.exists():
220
+ # Also check cwd in case we're already in cyclist dir
221
+ port_file = Path(os.getcwd()) / '.cyclist-port'
222
+ is_cyclist = port_file.exists()
206
223
  print(f'IS_CYCLIST={str(is_cyclist).lower()}')
207
224
  " 2>/dev/null)
208
225
 
@@ -234,10 +251,10 @@ else
234
251
  if [ -n "$CONTEXT_USABLE_PERCENT" ]; then
235
252
  if [ "$CONTEXT_USABLE_PERCENT" -ge "$CRITICAL_THRESHOLD" ] 2>/dev/null; then
236
253
  echo "CONTEXT_WARNING=Critical"
237
- echo "CONTEXT_RECOMMENDATION=checkpoint and handoff recommended"
254
+ echo "CONTEXT_RECOMMENDATION='checkpoint and handoff recommended'"
238
255
  elif [ "$CONTEXT_USABLE_PERCENT" -ge "$WARNING_THRESHOLD" ] 2>/dev/null; then
239
256
  echo "CONTEXT_WARNING=High"
240
- echo "CONTEXT_RECOMMENDATION=consider handoff soon"
257
+ echo "CONTEXT_RECOMMENDATION='consider handoff soon'"
241
258
  fi
242
259
  fi
243
260
  fi
@@ -0,0 +1,95 @@
1
+ #!/bin/bash
2
+ # Start an agent with phase check
3
+ # If the current story's phase belongs to a different agent, emit handoff marker instead
4
+ #
5
+ # Usage: phase-check-start.sh <agent>
6
+ # Example: phase-check-start.sh dev
7
+
8
+ set -euo pipefail
9
+
10
+ AGENT="${1:-}"
11
+
12
+ if [[ -z "$AGENT" ]]; then
13
+ echo "Usage: phase-check-start.sh <agent>" >&2
14
+ exit 1
15
+ fi
16
+
17
+ # Find project root
18
+ if [[ -z "${PROJECT_ROOT:-}" ]]; then
19
+ d="$PWD"
20
+ while [[ ! -d "$d/.claude" ]] && [[ "$d" != "/" ]]; do
21
+ d="$(dirname "$d")"
22
+ done
23
+ PROJECT_ROOT="$d"
24
+ fi
25
+
26
+ SCRIPT_DIR="$PROJECT_ROOT/.pennyfarthing/scripts/core"
27
+ SESSION_DIR="$PROJECT_ROOT/.session"
28
+
29
+ # Find active session file
30
+ SESSION_FILE=$(find "$SESSION_DIR" -maxdepth 1 -name "*-session.md" 2>/dev/null | head -1)
31
+
32
+ if [[ -z "$SESSION_FILE" || ! -f "$SESSION_FILE" ]]; then
33
+ # No session - just start the agent normally
34
+ exec "$SCRIPT_DIR/run.sh" core/agent-session.sh start "$AGENT"
35
+ fi
36
+
37
+ # Extract workflow and phase from session (handles multiple formats)
38
+ # Format 1: **Workflow:** value
39
+ # Format 2: - **Workflow**: value
40
+ WORKFLOW=$(grep -E "\*\*Workflow\*?\*?:" "$SESSION_FILE" 2>/dev/null | head -1 | sed 's/.*\*\*Workflow\*\*[:\*]* *//' | tr -d ' ' || echo "")
41
+ PHASE=$(grep -E "\*\*Phase\*?\*?:" "$SESSION_FILE" 2>/dev/null | head -1 | sed 's/.*\*\*Phase\*\*[:\*]* *//' | tr -d ' ' || echo "")
42
+
43
+ if [[ -z "$WORKFLOW" ]]; then
44
+ # Can't determine workflow - start normally
45
+ exec "$SCRIPT_DIR/run.sh" core/agent-session.sh start "$AGENT"
46
+ fi
47
+
48
+ # If no Phase field, try to infer from status patterns
49
+ if [[ -z "$PHASE" ]]; then
50
+ # Check for approval status (story is done, needs SM to close)
51
+ if grep -qE "\*\*Status\*\*:.*APPROVED" "$SESSION_FILE" 2>/dev/null; then
52
+ PHASE="approved"
53
+ # Check for review status
54
+ elif grep -qE "Reviewer Assessment|Review.*REJECTED\|Review.*APPROVED" "$SESSION_FILE" 2>/dev/null; then
55
+ PHASE="review"
56
+ # Check for green/implementation complete
57
+ elif grep -qE "Dev Assessment|\*\*Status\*\*:.*GREEN" "$SESSION_FILE" 2>/dev/null; then
58
+ # Has dev assessment - could be in review or approved
59
+ if grep -qE "Reviewer Assessment" "$SESSION_FILE" 2>/dev/null; then
60
+ PHASE="approved" # Reviewer already assessed
61
+ else
62
+ PHASE="review" # Needs reviewer
63
+ fi
64
+ # Check for red/test phase
65
+ elif grep -qE "TEA Assessment|\*\*Status\*\*:.*RED" "$SESSION_FILE" 2>/dev/null; then
66
+ PHASE="green" # TEA done, Dev's turn
67
+ else
68
+ # Can't determine phase - start normally
69
+ exec "$SCRIPT_DIR/run.sh" core/agent-session.sh start "$AGENT"
70
+ fi
71
+ fi
72
+
73
+ # Get the owner of this phase
74
+ OWNER=$("$SCRIPT_DIR/run.sh" workflow/phase-owner.sh "$WORKFLOW" "$PHASE" 2>/dev/null || echo "")
75
+
76
+ if [[ -z "$OWNER" ]]; then
77
+ # Phase owner lookup failed - start normally
78
+ exec "$SCRIPT_DIR/run.sh" core/agent-session.sh start "$AGENT"
79
+ fi
80
+
81
+ # Check if this agent owns the phase
82
+ if [[ "$OWNER" == "$AGENT" ]]; then
83
+ # Correct agent - start normally
84
+ exec "$SCRIPT_DIR/run.sh" core/agent-session.sh start "$AGENT"
85
+ fi
86
+
87
+ # Wrong agent! Output the handoff marker and info
88
+ STORY_ID=$(basename "$SESSION_FILE" -session.md)
89
+
90
+ echo "Phase check: Story $STORY_ID is in '$PHASE' phase (workflow: $WORKFLOW)"
91
+ echo "Phase owner: $OWNER (you requested: $AGENT)"
92
+ echo ""
93
+
94
+ # Generate and output the handoff marker
95
+ "$SCRIPT_DIR/handoff-marker.sh" "$OWNER"
@@ -94,10 +94,11 @@ if [[ -n "$BUMP_TYPE" ]]; then
94
94
  info "Version bump requested: $BUMP_TYPE"
95
95
 
96
96
  # Run deploy.sh which handles version bump + full release
97
+ DEPLOY_SCRIPT="$(dirname "$SCRIPT_DIR")/misc/deploy.sh"
97
98
  if $DRY_RUN; then
98
- exec "$SCRIPT_DIR/deploy.sh" --dry-run "$BUMP_TYPE"
99
+ exec "$DEPLOY_SCRIPT" --dry-run "$BUMP_TYPE"
99
100
  else
100
- exec "$SCRIPT_DIR/deploy.sh" "$BUMP_TYPE"
101
+ exec "$DEPLOY_SCRIPT" "$BUMP_TYPE"
101
102
  fi
102
103
  fi
103
104
 
@@ -0,0 +1,162 @@
1
+ #!/bin/bash
2
+ # Drift Detection - Analyze archived sessions for agent behavior drift
3
+ # Usage: .pennyfarthing/scripts/core/run.sh health/drift-detection.sh [--verbose] [--path /additional/path]
4
+ #
5
+ # Checks:
6
+ # 1. Reviewer: Substantive comments present (not just "LGTM")
7
+ # 2. Dev: Tests run before GREEN declaration
8
+ # 3. SM: Handoff markers present
9
+ # 4. TEA: Tests written before handoff to Dev
10
+
11
+ set -euo pipefail
12
+
13
+ VERBOSE=false
14
+ EXTRA_PATHS=()
15
+
16
+ while [[ $# -gt 0 ]]; do
17
+ case "$1" in
18
+ --verbose|-v) VERBOSE=true; shift ;;
19
+ --path) EXTRA_PATHS+=("$2"); shift 2 ;;
20
+ *) shift ;;
21
+ esac
22
+ done
23
+
24
+ # Find project root
25
+ if [[ -z "${PROJECT_ROOT:-}" ]]; then
26
+ d="$PWD"
27
+ while [[ ! -d "$d/.claude" ]] && [[ "$d" != "/" ]]; do
28
+ d="$(dirname "$d")"
29
+ done
30
+ PROJECT_ROOT="$d"
31
+ fi
32
+
33
+ ARCHIVE_DIR="$PROJECT_ROOT/sprint/archive"
34
+ SESSION_DIR="$PROJECT_ROOT/.session/archive"
35
+
36
+ # Build list of directories to search
37
+ SEARCH_DIRS=("$ARCHIVE_DIR" "$SESSION_DIR")
38
+ for extra in "${EXTRA_PATHS[@]}"; do
39
+ if [[ -d "$extra/sprint/archive" ]]; then
40
+ SEARCH_DIRS+=("$extra/sprint/archive")
41
+ fi
42
+ if [[ -d "$extra/.session/archive" ]]; then
43
+ SEARCH_DIRS+=("$extra/.session/archive")
44
+ fi
45
+ if [[ -d "$extra" ]] && [[ ! -d "$extra/sprint" ]]; then
46
+ # Direct path to archive directory
47
+ SEARCH_DIRS+=("$extra")
48
+ fi
49
+ done
50
+
51
+ echo "# Agent Behavior Drift Detection Report"
52
+ echo ""
53
+ echo "Generated: $(date '+%Y-%m-%d %H:%M')"
54
+ echo ""
55
+
56
+ # Counters
57
+ TOTAL_SESSIONS=0
58
+ REVIEWER_ISSUES=0
59
+ DEV_ISSUES=0
60
+ SM_ISSUES=0
61
+ TEA_ISSUES=0
62
+
63
+ # Collect session files from all locations
64
+ SESSION_FILES=$(find "${SEARCH_DIRS[@]}" -name "*-session.md" -type f 2>/dev/null | sort -u | head -100)
65
+
66
+ if [[ -z "$SESSION_FILES" ]]; then
67
+ echo "No session files found for analysis."
68
+ exit 0
69
+ fi
70
+
71
+ echo "## Analysis Summary"
72
+ echo ""
73
+
74
+ # Analyze each session
75
+ while IFS= read -r session_file; do
76
+ ((TOTAL_SESSIONS++))
77
+ filename=$(basename "$session_file")
78
+
79
+ # Check for Reviewer drift: approvals without substantive comments
80
+ if grep -qi "reviewer" "$session_file" 2>/dev/null; then
81
+ if grep -qi "approved" "$session_file" 2>/dev/null; then
82
+ # Look for substantive review content (more than just approval)
83
+ review_lines=$(grep -c -iE "(issue|concern|suggest|fix|change|improve|refactor|test|bug)" "$session_file" 2>/dev/null || echo "0")
84
+ if [[ "$review_lines" -lt 2 ]]; then
85
+ ((REVIEWER_ISSUES++))
86
+ if [[ "$VERBOSE" == "true" ]]; then
87
+ echo "⚠️ Reviewer drift: $filename - approval without substantive feedback"
88
+ fi
89
+ fi
90
+ fi
91
+ fi
92
+
93
+ # Check for Dev drift: GREEN without test evidence
94
+ if grep -qi "GREEN\|green phase\|tests pass" "$session_file" 2>/dev/null; then
95
+ if ! grep -qiE "(test.*pass|tests.*ran|npm test|vitest|jest|\d+ passed)" "$session_file" 2>/dev/null; then
96
+ ((DEV_ISSUES++))
97
+ if [[ "$VERBOSE" == "true" ]]; then
98
+ echo "⚠️ Dev drift: $filename - GREEN declared without test evidence"
99
+ fi
100
+ fi
101
+ fi
102
+
103
+ # Check for SM drift: handoff sections without proper structure
104
+ # Note: CYCLIST:HANDOFF markers are optional - we check for structured handoff content
105
+ if grep -qi "## Handoff\|Handoff to" "$session_file" 2>/dev/null; then
106
+ # Good handoff should mention target agent AND have some context
107
+ if ! grep -qiE "handoff.*(TEA|Dev|Reviewer)|→.*(TEA|Dev|Reviewer)" "$session_file" 2>/dev/null; then
108
+ ((SM_ISSUES++))
109
+ if [[ "$VERBOSE" == "true" ]]; then
110
+ echo "⚠️ SM drift: $filename - handoff section without target agent"
111
+ fi
112
+ fi
113
+ fi
114
+
115
+ # Check for TEA drift: handoff to Dev without test files mentioned
116
+ if grep -qi "TEA.*handoff\|handoff.*Dev" "$session_file" 2>/dev/null; then
117
+ if ! grep -qiE "\.test\.(ts|js|tsx)|spec\.(ts|js)|_test\.go|Test\.java" "$session_file" 2>/dev/null; then
118
+ ((TEA_ISSUES++))
119
+ if [[ "$VERBOSE" == "true" ]]; then
120
+ echo "⚠️ TEA drift: $filename - no test files referenced before Dev handoff"
121
+ fi
122
+ fi
123
+ fi
124
+
125
+ done <<< "$SESSION_FILES"
126
+
127
+ echo "| Agent | Issues | Total | Rate |"
128
+ echo "|-------|--------|-------|------|"
129
+ REVIEWER_PCT=$((REVIEWER_ISSUES * 100 / TOTAL_SESSIONS))
130
+ DEV_PCT=$((DEV_ISSUES * 100 / TOTAL_SESSIONS))
131
+ SM_PCT=$((SM_ISSUES * 100 / TOTAL_SESSIONS))
132
+ TEA_PCT=$((TEA_ISSUES * 100 / TOTAL_SESSIONS))
133
+ echo "| Reviewer | $REVIEWER_ISSUES | $TOTAL_SESSIONS | ${REVIEWER_PCT}% |"
134
+ echo "| Dev | $DEV_ISSUES | $TOTAL_SESSIONS | ${DEV_PCT}% |"
135
+ echo "| SM | $SM_ISSUES | $TOTAL_SESSIONS | ${SM_PCT}% |"
136
+ echo "| TEA | $TEA_ISSUES | $TOTAL_SESSIONS | ${TEA_PCT}% |"
137
+ echo ""
138
+
139
+ TOTAL_ISSUES=$((REVIEWER_ISSUES + DEV_ISSUES + SM_ISSUES + TEA_ISSUES))
140
+
141
+ if [[ "$TOTAL_ISSUES" -eq 0 ]]; then
142
+ echo "✅ **No drift detected.** All agents following expected behaviors."
143
+ else
144
+ echo "⚠️ **$TOTAL_ISSUES potential drift signals detected.**"
145
+ echo ""
146
+ echo "### Recommendations"
147
+ if [[ "$REVIEWER_ISSUES" -gt 0 ]]; then
148
+ echo "- **Reviewer:** Ensure substantive feedback on all reviews (not just LGTM)"
149
+ fi
150
+ if [[ "$DEV_ISSUES" -gt 0 ]]; then
151
+ echo "- **Dev:** Always include test run output when declaring GREEN"
152
+ fi
153
+ if [[ "$SM_ISSUES" -gt 0 ]]; then
154
+ echo "- **SM:** Use handoff markers (CYCLIST:HANDOFF) for Cyclist integration"
155
+ fi
156
+ if [[ "$TEA_ISSUES" -gt 0 ]]; then
157
+ echo "- **TEA:** Reference specific test files when handing off to Dev"
158
+ fi
159
+ fi
160
+
161
+ echo ""
162
+ echo "Run with --verbose to see individual file details."
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+ #
3
+ # Bell Mode PostToolUse Hook (Story MSSCI-12275)
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/bell-mode.json - { "enabled": 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
+ # Find project root (walk up to find .pennyfarthing)
25
+ PROJECT_ROOT="$PWD"
26
+ while [[ "$PROJECT_ROOT" != "/" ]]; do
27
+ if [[ -d "$PROJECT_ROOT/.pennyfarthing" ]]; then
28
+ break
29
+ fi
30
+ PROJECT_ROOT="$(dirname "$PROJECT_ROOT")"
31
+ done
32
+
33
+ if [[ ! -d "$PROJECT_ROOT/.pennyfarthing" ]]; then
34
+ # No .pennyfarthing directory found - exit silently
35
+ exit 0
36
+ fi
37
+
38
+ BELL_MODE_CONFIG="$PROJECT_ROOT/.pennyfarthing/bell-mode.json"
39
+ BELL_QUEUE_FILE="$PROJECT_ROOT/.pennyfarthing/bell-queue.json"
40
+
41
+ # Check if bell mode is enabled
42
+ if [[ ! -f "$BELL_MODE_CONFIG" ]]; then
43
+ exit 0
44
+ fi
45
+
46
+ ENABLED=$(cat "$BELL_MODE_CONFIG" 2>/dev/null | grep -o '"enabled"[[:space:]]*:[[:space:]]*true' || true)
47
+ if [[ -z "$ENABLED" ]]; then
48
+ exit 0
49
+ fi
50
+
51
+ # Check if queue file exists and has messages
52
+ if [[ ! -f "$BELL_QUEUE_FILE" ]]; then
53
+ exit 0
54
+ fi
55
+
56
+ # Read queue and check if non-empty
57
+ QUEUE_CONTENT=$(cat "$BELL_QUEUE_FILE" 2>/dev/null)
58
+ if [[ -z "$QUEUE_CONTENT" ]] || [[ "$QUEUE_CONTENT" == "[]" ]]; then
59
+ exit 0
60
+ fi
61
+
62
+ # Extract first message text using simple parsing
63
+ # The queue format is: [{"text":"...","images":[...]}, ...]
64
+ FIRST_MESSAGE_TEXT=$(echo "$QUEUE_CONTENT" | sed -n 's/.*"text"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
65
+
66
+ if [[ -z "$FIRST_MESSAGE_TEXT" ]]; then
67
+ exit 0
68
+ fi
69
+
70
+ # Output the hook response JSON
71
+ cat << EOF
72
+ {
73
+ "hookSpecificOutput": {
74
+ "hookEventName": "PostToolUse",
75
+ "additionalContext": "User feedback: $FIRST_MESSAGE_TEXT"
76
+ }
77
+ }
78
+ EOF
79
+
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
86
+
87
+ exit 0
@@ -92,7 +92,7 @@ else
92
92
  TITLE=$(yq -r "(.epics[] | select(.jira == \"$JIRA_EPIC_KEY\" or .id == \"$EPIC_ID\")).stories[] | select(.id == \"$STORY_ID\") | .title" "$SPRINT_FILE")
93
93
  echo "[DRY RUN] Would create: $STORY_ID - $TITLE"
94
94
  else
95
- "$SCRIPTS_DIR/create-jira-story.sh" "$JIRA_EPIC_KEY" "$STORY_ID"
95
+ "$SCRIPTS_DIR/jira/create-jira-story.sh" "$JIRA_EPIC_KEY" "$STORY_ID"
96
96
  fi
97
97
  done
98
98
  fi
@@ -194,7 +194,7 @@ if $DRY_RUN; then
194
194
  log_dry "git commit -m 'chore: bump version to $NEW_VERSION'"
195
195
  else
196
196
  git -C "$PROJECT_ROOT" add VERSION package.json package-lock.json README.md CHANGELOG.md 2>/dev/null || true
197
- git -C "$PROJECT_ROOT" commit -m "chore: bump version to $NEW_VERSION"
197
+ git -C "$PROJECT_ROOT" commit --no-verify -m "chore: bump version to $NEW_VERSION"
198
198
  log_info "Committed version bump"
199
199
  fi
200
200
 
@@ -13,6 +13,7 @@ fi
13
13
  # Extract fields - use cwd for display only, PROJECT_ROOT for file lookups
14
14
  cwd=$(echo "$input" | jq -r '.workspace.current_dir // empty' 2>/dev/null)
15
15
  dir_name=$(basename "$cwd" 2>/dev/null || echo "?")
16
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$cwd}"
16
17
  session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null)
17
18
 
18
19
  # Model name - clean up
@@ -89,7 +90,8 @@ elif [ -d "$PROJECT_ROOT/.session/agents" ]; then
89
90
  fi
90
91
  fi
91
92
 
92
- # Get character name from theme for current agent
93
+ # Get character name from config for current agent
94
+ # Config now includes theme_characters baked in (no symlink chasing needed)
93
95
  config_file=""
94
96
  if [ -f "$PROJECT_ROOT/.pennyfarthing/config.local.yaml" ]; then
95
97
  config_file="$PROJECT_ROOT/.pennyfarthing/config.local.yaml"
@@ -98,40 +100,31 @@ elif [ -f "$PROJECT_ROOT/.claude/persona-config.yaml" ]; then
98
100
  fi
99
101
 
100
102
  character_display=""
101
- if [ -n "$config_file" ]; then
102
- theme=$(yq '.theme' "$config_file" 2>/dev/null)
103
- if [ -n "$theme" ] && [ "$theme" != "null" ] && [ -n "$agent_name" ]; then
104
- # Find theme file (same resolution as agent-session.sh)
105
- theme_file=""
106
- if [ -f "$PROJECT_ROOT/.claude/pennyfarthing/themes/${theme}.yaml" ]; then
107
- theme_file="$PROJECT_ROOT/.claude/pennyfarthing/themes/${theme}.yaml"
108
- elif [ -f "$PROJECT_ROOT/.pennyfarthing/personas/themes/${theme}.yaml" ]; then
109
- theme_file="$PROJECT_ROOT/.pennyfarthing/personas/themes/${theme}.yaml"
110
- elif [ -f "$PROJECT_ROOT/personas/themes/${theme}.yaml" ]; then
111
- theme_file="$PROJECT_ROOT/personas/themes/${theme}.yaml"
112
- fi
113
-
114
- if [ -n "$theme_file" ]; then
115
- full_name=$(yq ".agents.${agent_name}.character" "$theme_file" 2>/dev/null)
116
- if [ -n "$full_name" ] && [ "$full_name" != "null" ]; then
117
- # Smart character name extraction:
118
- # 1. Remove parenthetical content: "Breq (Justice of Toren)" → "Breq"
119
- # 2. Strip common titles: "Captain Kirk" → "Kirk"
120
- # 3. If single word remains, use it; otherwise take last word
121
- clean_name=$(echo "$full_name" | sed 's/ *([^)]*)//g' | xargs)
122
- 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')
123
- word_count=$(echo "$clean_name" | wc -w | tr -d ' ')
124
- if [ "$word_count" -eq 1 ]; then
125
- character_display="$clean_name"
126
- else
127
- character_display=$(echo "$clean_name" | awk '{print $NF}')
128
- fi
129
- fi
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}')
130
119
  fi
131
120
  fi
121
+
132
122
  # Fallback to theme name if no character found
133
- if [ -z "$character_display" ] && [ -n "$theme" ] && [ "$theme" != "null" ]; then
134
- character_display="$(echo "${theme:0:1}" | tr '[:lower:]' '[:upper:]')${theme:1}"
123
+ if [ -z "$character_display" ]; then
124
+ theme=$(yq '.theme' "$config_file" 2>/dev/null)
125
+ if [ -n "$theme" ] && [ "$theme" != "null" ]; then
126
+ character_display="$(echo "${theme:0:1}" | tr '[:lower:]' '[:upper:]')${theme:1}"
127
+ fi
135
128
  fi
136
129
  fi
137
130
  theme_display="$character_display"