@pennyfarthing/core 7.8.2 → 7.9.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/README.md +1 -1
- package/package.json +1 -1
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +8 -7
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/cyclist-migration.test.js +16 -13
- package/packages/core/dist/cli/cyclist-migration.test.js.map +1 -1
- package/packages/core/dist/cli/utils/files.d.ts +5 -4
- package/packages/core/dist/cli/utils/files.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/files.js +8 -6
- package/packages/core/dist/cli/utils/files.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
- package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +25 -0
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/packages/core/dist/cli/utils/themes.d.ts +1 -1
- package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
- package/packages/core/dist/scripts/run-ci.test.js +1 -1
- package/packages/core/dist/scripts/run-ci.test.js.map +1 -1
- package/pennyfarthing-dist/agents/README.md +25 -17
- package/pennyfarthing-dist/agents/architect.md +3 -11
- package/pennyfarthing-dist/agents/dev.md +2 -2
- package/pennyfarthing-dist/agents/devops.md +3 -11
- package/pennyfarthing-dist/agents/handoff.md +4 -4
- package/pennyfarthing-dist/agents/orchestrator.md +2 -4
- package/pennyfarthing-dist/agents/pm.md +4 -11
- package/pennyfarthing-dist/agents/reviewer-preflight.md +4 -3
- package/pennyfarthing-dist/agents/reviewer.md +2 -8
- package/pennyfarthing-dist/agents/sm-handoff.md +3 -3
- package/pennyfarthing-dist/agents/sm-setup.md +1 -1
- package/pennyfarthing-dist/agents/sm.md +5 -29
- package/pennyfarthing-dist/agents/tea.md +2 -2
- package/pennyfarthing-dist/agents/tech-writer.md +3 -12
- package/pennyfarthing-dist/agents/testing-runner.md +8 -8
- package/pennyfarthing-dist/agents/ux-designer.md +3 -12
- package/pennyfarthing-dist/commands/git-cleanup.md +29 -53
- package/pennyfarthing-dist/commands/party-mode.md +20 -10
- package/pennyfarthing-dist/commands/work.md +6 -105
- package/pennyfarthing-dist/guides/agent-behavior.md +19 -7
- package/pennyfarthing-dist/personas/themes/1984.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/a-team.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/agatha-christie.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/all-stars.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/ancient-philosophers.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/ancient-strategists.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/arcane.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/arthurian-mythos.yaml +0 -13
- package/pennyfarthing-dist/personas/themes/avatar-the-last-airbender.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/babylon-5.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/better-call-saul.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/big-lebowski.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/black-sails.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/blade-runner.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/bobiverse.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/breaking-bad.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/catch-22.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/classical-composers.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/count-of-monte-cristo.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/deadwood.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/dickens.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/discworld.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/doctor-who.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/don-quixote.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/dune.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/enlightenment-thinkers.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/expeditionary-force.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/fargo.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/film-auteurs.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/firefly.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/foundation.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/futurama.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/gothic-literature.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/great-gatsby.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/greek-mythology.yaml +0 -13
- package/pennyfarthing-dist/personas/themes/hannibal.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/harry-potter.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/his-dark-materials.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/historical-figures.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/house-md.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/imperial-radch.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/inspector-morse.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/jane-austen.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/jazz-legends.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/justified.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/legion-of-doom.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/les-miserables.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/lord-of-the-rings.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/lovecraft-mythos.yaml +0 -13
- package/pennyfarthing-dist/personas/themes/mad-max.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/mad-men.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/marvel-mcu.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/mash.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/mass-effect.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/military-commanders.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/moby-dick.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/monty-python.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/neuromancer.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/norse-mythology.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/parks-and-rec.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/peaky-blinders.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/princess-bride.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/renaissance-masters.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/rome.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/russian-masters.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/sandman.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/scientific-revolutionaries.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/shakespeare.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/sherlock-holmes.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/snow-crash.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/software-pioneers.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +0 -11
- package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/star-wars.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/succession.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/superfriends.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/ted-lasso.yaml +0 -11
- package/pennyfarthing-dist/personas/themes/the-americans.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/the-crown.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/the-expanse.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/the-good-place.yaml +0 -11
- package/pennyfarthing-dist/personas/themes/the-matrix.yaml +0 -15
- package/pennyfarthing-dist/personas/themes/the-odyssey.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/the-office.yaml +0 -11
- package/pennyfarthing-dist/personas/themes/the-simpsons.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/the-sopranos.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/the-wire.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/the-witcher.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/twin-peaks.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/vorkosigan-saga.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/watchmen.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/west-wing.yaml +0 -10
- package/pennyfarthing-dist/personas/themes/world-explorers.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/wwii-leaders.yaml +0 -12
- package/pennyfarthing-dist/personas/themes/x-files.yaml +0 -10
- package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -14
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
- package/pennyfarthing-dist/scripts/core/prime.sh +17 -2
- package/pennyfarthing-dist/scripts/core/run.sh +5 -5
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
- package/pennyfarthing-dist/scripts/git/release.sh +2 -2
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
- package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
- package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +102 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
- package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +6 -2
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +3 -2
- package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +18 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +18 -4
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +13 -5
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/client.py +1 -1
- package/pennyfarthing_scripts/prime/__init__.py +98 -11
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +208 -53
- package/pennyfarthing_scripts/prime/models.py +169 -0
- package/pennyfarthing_scripts/prime/persona.py +288 -0
- package/pennyfarthing_scripts/prime/session.py +183 -0
- package/pennyfarthing_scripts/prime/workflow.py +275 -0
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_prime.py +653 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Persona loading for Prime v2.
|
|
3
|
+
|
|
4
|
+
Loads agent personas from theme YAML files for character-driven agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from pennyfarthing_scripts.common.config import get_project_root, load_yaml_config
|
|
15
|
+
from pennyfarthing_scripts.prime.models import CrewMember, Persona
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Standard agent roles for crew manifest
|
|
19
|
+
AGENT_ROLES = [
|
|
20
|
+
"sm", "tea", "dev", "reviewer", "architect",
|
|
21
|
+
"pm", "tech-writer", "ux-designer", "devops", "orchestrator",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_current_theme(project_root: Path | None = None) -> str | None:
|
|
26
|
+
"""Get the currently configured theme.
|
|
27
|
+
|
|
28
|
+
Checks config files in priority order:
|
|
29
|
+
1. .pennyfarthing/config.local.yaml
|
|
30
|
+
2. .claude/persona-config.yaml
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
project_root: Project root path (auto-detected if not provided)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Theme name, or None if not configured
|
|
37
|
+
"""
|
|
38
|
+
root = project_root or get_project_root()
|
|
39
|
+
|
|
40
|
+
# Check config files in priority order
|
|
41
|
+
config_paths = [
|
|
42
|
+
root / ".pennyfarthing" / "config.local.yaml",
|
|
43
|
+
root / ".claude" / "persona-config.yaml",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
for config_path in config_paths:
|
|
47
|
+
config = load_yaml_config(config_path)
|
|
48
|
+
if config and "theme" in config:
|
|
49
|
+
return config["theme"]
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_theme_path(theme: str, project_root: Path) -> Path | None:
|
|
55
|
+
"""Get the path to a theme YAML file.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
theme: Theme name
|
|
59
|
+
project_root: Project root path
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Path to theme file, or None if not found
|
|
63
|
+
"""
|
|
64
|
+
# Single source of truth: .pennyfarthing/personas/themes/
|
|
65
|
+
theme_path = project_root / ".pennyfarthing" / "personas" / "themes" / f"{theme}.yaml"
|
|
66
|
+
|
|
67
|
+
if theme_path.exists():
|
|
68
|
+
return theme_path
|
|
69
|
+
|
|
70
|
+
# Fallback to pennyfarthing-dist (for development)
|
|
71
|
+
theme_path = project_root / "pennyfarthing-dist" / "personas" / "themes" / f"{theme}.yaml"
|
|
72
|
+
|
|
73
|
+
if theme_path.exists():
|
|
74
|
+
return theme_path
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_theme(theme: str, project_root: Path | None = None) -> dict[str, Any] | None:
|
|
80
|
+
"""Load a theme YAML file.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
theme: Theme name
|
|
84
|
+
project_root: Project root path (auto-detected if not provided)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Theme data dict, or None if not found
|
|
88
|
+
"""
|
|
89
|
+
root = project_root or get_project_root()
|
|
90
|
+
theme_path = get_theme_path(theme, root)
|
|
91
|
+
|
|
92
|
+
if not theme_path:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
return yaml.safe_load(theme_path.read_text())
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_persona(agent_name: str, project_root: Path | None = None) -> tuple[Persona | None, str | None]:
|
|
102
|
+
"""Load persona for an agent from the current theme.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
agent_name: Agent name (sm, tea, dev, etc.)
|
|
106
|
+
project_root: Project root path (auto-detected if not provided)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Tuple of (Persona, theme_name) or (None, None) if not found
|
|
110
|
+
"""
|
|
111
|
+
root = project_root or get_project_root()
|
|
112
|
+
theme = get_current_theme(root)
|
|
113
|
+
|
|
114
|
+
if not theme:
|
|
115
|
+
return None, None
|
|
116
|
+
|
|
117
|
+
theme_data = load_theme(theme, root)
|
|
118
|
+
if not theme_data or "agents" not in theme_data:
|
|
119
|
+
return None, None
|
|
120
|
+
|
|
121
|
+
agent_data = theme_data["agents"].get(agent_name)
|
|
122
|
+
if not agent_data:
|
|
123
|
+
return None, None
|
|
124
|
+
|
|
125
|
+
# Extract helper info if present
|
|
126
|
+
helper = agent_data.get("helper", {})
|
|
127
|
+
|
|
128
|
+
persona = Persona(
|
|
129
|
+
character=agent_data.get("character", "Unknown"),
|
|
130
|
+
style=agent_data.get("style", ""),
|
|
131
|
+
role=agent_data.get("role", ""),
|
|
132
|
+
quote=agent_data.get("quote"),
|
|
133
|
+
trait=agent_data.get("trait"),
|
|
134
|
+
quirk=agent_data.get("quirk"),
|
|
135
|
+
motto=agent_data.get("motto"),
|
|
136
|
+
helper_name=helper.get("name") if helper else None,
|
|
137
|
+
helper_style=helper.get("style") if helper else None,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return persona, theme
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_crew_manifest(project_root: Path | None = None) -> list[CrewMember]:
|
|
144
|
+
"""Get all agent characters from the current theme.
|
|
145
|
+
|
|
146
|
+
Used for handoff reference so agents know other characters.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
project_root: Project root path (auto-detected if not provided)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of CrewMember objects for all 10 standard roles
|
|
153
|
+
"""
|
|
154
|
+
root = project_root or get_project_root()
|
|
155
|
+
theme = get_current_theme(root)
|
|
156
|
+
|
|
157
|
+
if not theme:
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
theme_data = load_theme(theme, root)
|
|
161
|
+
if not theme_data or "agents" not in theme_data:
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
crew = []
|
|
165
|
+
for role in AGENT_ROLES:
|
|
166
|
+
agent_data = theme_data["agents"].get(role)
|
|
167
|
+
if agent_data and "character" in agent_data:
|
|
168
|
+
crew.append(CrewMember(role=role, character=agent_data["character"]))
|
|
169
|
+
|
|
170
|
+
return crew
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_user_title(project_root: Path | None = None) -> str | None:
|
|
174
|
+
"""Get the user title from the current theme.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
project_root: Project root path (auto-detected if not provided)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
User title (e.g., "Bossmang"), or None if not set
|
|
181
|
+
"""
|
|
182
|
+
root = project_root or get_project_root()
|
|
183
|
+
theme = get_current_theme(root)
|
|
184
|
+
|
|
185
|
+
if not theme:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
theme_data = load_theme(theme, root)
|
|
189
|
+
if not theme_data or "theme" not in theme_data:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
return theme_data["theme"].get("user_title")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def format_persona_output(
|
|
196
|
+
persona: Persona,
|
|
197
|
+
theme: str,
|
|
198
|
+
agent_name: str,
|
|
199
|
+
crew: list[CrewMember] | None = None,
|
|
200
|
+
user_title: str | None = None,
|
|
201
|
+
) -> str:
|
|
202
|
+
"""Format persona as XML output for Claude.
|
|
203
|
+
|
|
204
|
+
Matches the format from agent-session.sh:
|
|
205
|
+
<persona agent="dev" theme="the-expanse">
|
|
206
|
+
Character: Naomi Nagata
|
|
207
|
+
Style: ...
|
|
208
|
+
</persona>
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
persona: Persona to format
|
|
212
|
+
theme: Theme name
|
|
213
|
+
agent_name: Agent name
|
|
214
|
+
crew: Optional crew manifest for handoff reference
|
|
215
|
+
user_title: Optional user title
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Formatted persona XML string
|
|
219
|
+
"""
|
|
220
|
+
lines = [
|
|
221
|
+
f'<persona agent="{agent_name}" theme="{theme}">',
|
|
222
|
+
f"Character: {persona.character}",
|
|
223
|
+
f"Style: {persona.style}",
|
|
224
|
+
f"Role: {persona.role}",
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
if persona.trait:
|
|
228
|
+
lines.append(f"Trait: {persona.trait}")
|
|
229
|
+
if persona.quirk:
|
|
230
|
+
lines.append(f"Quirk: {persona.quirk}")
|
|
231
|
+
if persona.motto:
|
|
232
|
+
lines.append(f"Motto: {persona.motto}")
|
|
233
|
+
if persona.quote:
|
|
234
|
+
lines.append(f"Quote: {persona.quote}")
|
|
235
|
+
if persona.helper_name:
|
|
236
|
+
helper_line = f"Helper: {persona.helper_name}"
|
|
237
|
+
if persona.helper_style:
|
|
238
|
+
helper_line += f" - {persona.helper_style}"
|
|
239
|
+
lines.append(helper_line)
|
|
240
|
+
|
|
241
|
+
lines.append("</persona>")
|
|
242
|
+
|
|
243
|
+
# Add user title if set
|
|
244
|
+
if user_title:
|
|
245
|
+
lines.append("")
|
|
246
|
+
lines.append(f"<user-title>Address the user as: {user_title}</user-title>")
|
|
247
|
+
|
|
248
|
+
# Add crew manifest for handoff reference
|
|
249
|
+
if crew:
|
|
250
|
+
lines.append("")
|
|
251
|
+
lines.append(f'<crew theme="{theme}">')
|
|
252
|
+
lines.append("When handing off to other agents, address them by character name:")
|
|
253
|
+
for member in crew:
|
|
254
|
+
lines.append(f" {member.role:12} {member.character}")
|
|
255
|
+
lines.append("</crew>")
|
|
256
|
+
|
|
257
|
+
return "\n".join(lines)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def is_character_voice_enabled(project_root: Path | None = None) -> bool:
|
|
261
|
+
"""Check if character voice is enabled in preferences.
|
|
262
|
+
|
|
263
|
+
Checks preferences files in order:
|
|
264
|
+
1. .claude/pennyfarthing/preferences.local.yaml
|
|
265
|
+
2. .claude/pennyfarthing/preferences.yaml
|
|
266
|
+
|
|
267
|
+
Defaults to True if no preference is set.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
project_root: Project root path (auto-detected if not provided)
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True if character voice is enabled
|
|
274
|
+
"""
|
|
275
|
+
root = project_root or get_project_root()
|
|
276
|
+
|
|
277
|
+
prefs_paths = [
|
|
278
|
+
root / ".claude" / "pennyfarthing" / "preferences.local.yaml",
|
|
279
|
+
root / ".claude" / "pennyfarthing" / "preferences.yaml",
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
for prefs_path in prefs_paths:
|
|
283
|
+
prefs = load_yaml_config(prefs_path)
|
|
284
|
+
if prefs and "character_voice" in prefs:
|
|
285
|
+
return prefs["character_voice"] is not False
|
|
286
|
+
|
|
287
|
+
# Default to enabled
|
|
288
|
+
return True
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session registration for Prime v2.
|
|
3
|
+
|
|
4
|
+
Manages agent session files in .session/agents/ for multi-session support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
15
|
+
from pennyfarthing_scripts.prime.models import SessionInfo
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_agents_dir(project_root: Path) -> Path:
|
|
19
|
+
"""Get the agents session directory.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
project_root: Project root path
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Path to .session/agents/
|
|
26
|
+
"""
|
|
27
|
+
return project_root / ".session" / "agents"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_session(
|
|
31
|
+
agent_name: str,
|
|
32
|
+
session_id: str | None = None,
|
|
33
|
+
project_root: Path | None = None,
|
|
34
|
+
) -> SessionInfo:
|
|
35
|
+
"""Register an agent session.
|
|
36
|
+
|
|
37
|
+
Creates a session file in .session/agents/{session_id} containing
|
|
38
|
+
the agent name. If no session_id is provided, generates a new UUID.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
agent_name: Name of the agent to register
|
|
42
|
+
session_id: Optional session ID (generated if not provided)
|
|
43
|
+
project_root: Project root path (auto-detected if not provided)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
SessionInfo with session details
|
|
47
|
+
"""
|
|
48
|
+
root = project_root or get_project_root()
|
|
49
|
+
agents_dir = get_agents_dir(root)
|
|
50
|
+
|
|
51
|
+
# Create agents directory if needed
|
|
52
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# Generate session ID if not provided
|
|
55
|
+
if not session_id:
|
|
56
|
+
# Check SESSION_ID environment variable first
|
|
57
|
+
session_id = os.environ.get("SESSION_ID")
|
|
58
|
+
if not session_id:
|
|
59
|
+
session_id = str(uuid.uuid4())
|
|
60
|
+
|
|
61
|
+
# Write agent file
|
|
62
|
+
session_file = agents_dir / session_id
|
|
63
|
+
session_file.write_text(agent_name)
|
|
64
|
+
|
|
65
|
+
return SessionInfo(
|
|
66
|
+
session_id=session_id,
|
|
67
|
+
agent_name=agent_name,
|
|
68
|
+
file_path=str(session_file),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cleanup_old_sessions(project_root: Path | None = None, max_age_days: int = 7) -> int:
|
|
73
|
+
"""Remove stale session files.
|
|
74
|
+
|
|
75
|
+
Deletes session files older than max_age_days.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
project_root: Project root path (auto-detected if not provided)
|
|
79
|
+
max_age_days: Maximum age in days before cleanup
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Number of files removed
|
|
83
|
+
"""
|
|
84
|
+
root = project_root or get_project_root()
|
|
85
|
+
agents_dir = get_agents_dir(root)
|
|
86
|
+
|
|
87
|
+
if not agents_dir.is_dir():
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
cutoff = time.time() - (max_age_days * 86400)
|
|
91
|
+
removed = 0
|
|
92
|
+
|
|
93
|
+
for session_file in agents_dir.iterdir():
|
|
94
|
+
if session_file.is_file():
|
|
95
|
+
try:
|
|
96
|
+
if session_file.stat().st_mtime < cutoff:
|
|
97
|
+
session_file.unlink()
|
|
98
|
+
removed += 1
|
|
99
|
+
except OSError:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
return removed
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_session_agent(
|
|
106
|
+
session_id: str,
|
|
107
|
+
project_root: Path | None = None,
|
|
108
|
+
) -> str | None:
|
|
109
|
+
"""Get the agent name for a session.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
session_id: Session ID to look up
|
|
113
|
+
project_root: Project root path (auto-detected if not provided)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Agent name, or None if session not found
|
|
117
|
+
"""
|
|
118
|
+
root = project_root or get_project_root()
|
|
119
|
+
session_file = get_agents_dir(root) / session_id
|
|
120
|
+
|
|
121
|
+
if not session_file.is_file():
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
return session_file.read_text().strip()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def unregister_session(
|
|
128
|
+
session_id: str,
|
|
129
|
+
project_root: Path | None = None,
|
|
130
|
+
) -> bool:
|
|
131
|
+
"""Unregister an agent session.
|
|
132
|
+
|
|
133
|
+
Removes the session file.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
session_id: Session ID to unregister
|
|
137
|
+
project_root: Project root path (auto-detected if not provided)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if session was removed, False if not found
|
|
141
|
+
"""
|
|
142
|
+
root = project_root or get_project_root()
|
|
143
|
+
session_file = get_agents_dir(root) / session_id
|
|
144
|
+
|
|
145
|
+
if not session_file.is_file():
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
session_file.unlink()
|
|
150
|
+
return True
|
|
151
|
+
except OSError:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def list_sessions(project_root: Path | None = None) -> list[SessionInfo]:
|
|
156
|
+
"""List all active sessions.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
project_root: Project root path (auto-detected if not provided)
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of SessionInfo for all active sessions
|
|
163
|
+
"""
|
|
164
|
+
root = project_root or get_project_root()
|
|
165
|
+
agents_dir = get_agents_dir(root)
|
|
166
|
+
|
|
167
|
+
if not agents_dir.is_dir():
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
sessions = []
|
|
171
|
+
for session_file in agents_dir.iterdir():
|
|
172
|
+
if session_file.is_file():
|
|
173
|
+
try:
|
|
174
|
+
agent_name = session_file.read_text().strip()
|
|
175
|
+
sessions.append(SessionInfo(
|
|
176
|
+
session_id=session_file.name,
|
|
177
|
+
agent_name=agent_name,
|
|
178
|
+
file_path=str(session_file),
|
|
179
|
+
))
|
|
180
|
+
except OSError:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
return sessions
|