@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.
- package/package.json +1 -1
- package/packages/core/dist/cli/commands/doctor-legacy.test.d.ts +13 -0
- package/packages/core/dist/cli/commands/doctor-legacy.test.d.ts.map +1 -0
- package/packages/core/dist/cli/commands/doctor-legacy.test.js +207 -0
- package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -0
- package/packages/core/dist/cli/commands/doctor.d.ts +16 -0
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +130 -2
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +17 -27
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +21 -52
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.d.ts +15 -0
- package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +148 -2
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/themes.js +9 -0
- package/packages/core/dist/cli/utils/themes.js.map +1 -1
- package/pennyfarthing-dist/agents/dev.md +29 -24
- package/pennyfarthing-dist/agents/handoff.md +42 -119
- package/pennyfarthing-dist/agents/reviewer.md +32 -37
- package/pennyfarthing-dist/agents/sm-handoff.md +43 -66
- package/pennyfarthing-dist/agents/sm.md +52 -35
- package/pennyfarthing-dist/agents/tea.md +25 -8
- package/pennyfarthing-dist/agents/testing-runner.md +4 -4
- package/pennyfarthing-dist/commands/architect.md +0 -55
- package/pennyfarthing-dist/commands/dev.md +1 -54
- package/pennyfarthing-dist/commands/devops.md +0 -52
- package/pennyfarthing-dist/commands/health-check.md +33 -0
- package/pennyfarthing-dist/commands/orchestrator.md +0 -49
- package/pennyfarthing-dist/commands/pm.md +0 -53
- package/pennyfarthing-dist/commands/reviewer.md +1 -58
- package/pennyfarthing-dist/commands/sm.md +1 -64
- package/pennyfarthing-dist/commands/sprint.md +133 -0
- package/pennyfarthing-dist/commands/standalone.md +194 -0
- package/pennyfarthing-dist/commands/tea.md +1 -57
- package/pennyfarthing-dist/commands/tech-writer.md +0 -46
- package/pennyfarthing-dist/commands/theme-maker.md +10 -5
- package/pennyfarthing-dist/commands/ux-designer.md +0 -55
- package/pennyfarthing-dist/guides/XML-TAGS.md +156 -0
- package/pennyfarthing-dist/guides/agent-behavior.md +64 -38
- package/pennyfarthing-dist/guides/measurement-framework.md +210 -0
- package/pennyfarthing-dist/personas/themes/a-team.yaml +130 -0
- package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/ancient-strategists.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/arcane.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/better-call-saul.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/big-lebowski.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/black-sails.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/blade-runner.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/bobiverse.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/breaking-bad.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/count-of-monte-cristo.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/deadwood.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/dickens.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/doctor-who.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/don-quixote.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/dune.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/enlightenment-thinkers.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/expeditionary-force.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/futurama.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +131 -1
- package/pennyfarthing-dist/personas/themes/gothic-literature.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/great-gatsby.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/hannibal.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/harry-potter.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/his-dark-materials.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/inspector-morse.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/jane-austen.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/legion-of-doom.yaml +130 -0
- package/pennyfarthing-dist/personas/themes/mad-max.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/moby-dick.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/neuromancer.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/parks-and-rec.yaml +130 -0
- package/pennyfarthing-dist/personas/themes/princess-bride.yaml +130 -0
- package/pennyfarthing-dist/personas/themes/renaissance-masters.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/russian-masters.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/sandman.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/scientific-revolutionaries.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/shakespeare.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +139 -3
- package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +124 -0
- package/pennyfarthing-dist/personas/themes/star-wars.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/succession.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/superfriends.yaml +131 -1
- package/pennyfarthing-dist/personas/themes/ted-lasso.yaml +131 -1
- package/pennyfarthing-dist/personas/themes/the-americans.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/the-expanse.yaml +131 -1
- package/pennyfarthing-dist/personas/themes/the-good-place.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/the-matrix.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/the-sopranos.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/west-wing.yaml +6 -6
- package/pennyfarthing-dist/personas/themes/world-explorers.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/wwii-leaders.yaml +1 -1
- package/pennyfarthing-dist/scripts/core/check-context.sh +23 -6
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +95 -0
- package/pennyfarthing-dist/scripts/git/release.sh +3 -2
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +162 -0
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +87 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/deploy.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/statusline.sh +25 -32
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +377 -0
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +9 -0
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +492 -0
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -200
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +38 -5
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +40 -0
- package/pennyfarthing-dist/skills/theme-creation/SKILL.md +12 -7
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-04-final-validation.md +11 -3
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +122 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/workflow.yaml +3 -2
- package/packages/core/dist/workflow/generic-handoff.d.ts +0 -281
- package/packages/core/dist/workflow/generic-handoff.d.ts.map +0 -1
- package/packages/core/dist/workflow/generic-handoff.js +0 -411
- package/packages/core/dist/workflow/generic-handoff.js.map +0 -1
- package/packages/core/dist/workflow/generic-handoff.test.d.ts +0 -21
- package/packages/core/dist/workflow/generic-handoff.test.d.ts.map +0 -1
- package/packages/core/dist/workflow/generic-handoff.test.js +0 -499
- 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:
|
|
16
|
-
portrait_style: ", political
|
|
17
|
-
tier:
|
|
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,
|
|
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: "
|
|
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
|
|
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
|
|
@@ -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 =
|
|
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:
|
|
203
|
-
#
|
|
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 "$
|
|
99
|
+
exec "$DEPLOY_SCRIPT" --dry-run "$BUMP_TYPE"
|
|
99
100
|
else
|
|
100
|
-
exec "$
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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" ]
|
|
134
|
-
|
|
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"
|