@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.
Files changed (210) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/init.js +8 -7
  5. package/packages/core/dist/cli/commands/init.js.map +1 -1
  6. package/packages/core/dist/cli/cyclist-migration.test.js +16 -13
  7. package/packages/core/dist/cli/cyclist-migration.test.js.map +1 -1
  8. package/packages/core/dist/cli/utils/files.d.ts +5 -4
  9. package/packages/core/dist/cli/utils/files.d.ts.map +1 -1
  10. package/packages/core/dist/cli/utils/files.js +8 -6
  11. package/packages/core/dist/cli/utils/files.js.map +1 -1
  12. package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
  13. package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
  14. package/packages/core/dist/cli/utils/symlinks.js +25 -0
  15. package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
  16. package/packages/core/dist/cli/utils/themes.d.ts +1 -1
  17. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
  18. package/packages/core/dist/scripts/run-ci.test.js +1 -1
  19. package/packages/core/dist/scripts/run-ci.test.js.map +1 -1
  20. package/pennyfarthing-dist/agents/README.md +25 -17
  21. package/pennyfarthing-dist/agents/architect.md +3 -11
  22. package/pennyfarthing-dist/agents/dev.md +2 -2
  23. package/pennyfarthing-dist/agents/devops.md +3 -11
  24. package/pennyfarthing-dist/agents/handoff.md +4 -4
  25. package/pennyfarthing-dist/agents/orchestrator.md +2 -4
  26. package/pennyfarthing-dist/agents/pm.md +4 -11
  27. package/pennyfarthing-dist/agents/reviewer-preflight.md +4 -3
  28. package/pennyfarthing-dist/agents/reviewer.md +2 -8
  29. package/pennyfarthing-dist/agents/sm-handoff.md +3 -3
  30. package/pennyfarthing-dist/agents/sm-setup.md +1 -1
  31. package/pennyfarthing-dist/agents/sm.md +5 -29
  32. package/pennyfarthing-dist/agents/tea.md +2 -2
  33. package/pennyfarthing-dist/agents/tech-writer.md +3 -12
  34. package/pennyfarthing-dist/agents/testing-runner.md +8 -8
  35. package/pennyfarthing-dist/agents/ux-designer.md +3 -12
  36. package/pennyfarthing-dist/commands/git-cleanup.md +29 -53
  37. package/pennyfarthing-dist/commands/party-mode.md +20 -10
  38. package/pennyfarthing-dist/commands/work.md +6 -105
  39. package/pennyfarthing-dist/guides/agent-behavior.md +19 -7
  40. package/pennyfarthing-dist/personas/themes/1984.yaml +0 -12
  41. package/pennyfarthing-dist/personas/themes/a-team.yaml +0 -10
  42. package/pennyfarthing-dist/personas/themes/agatha-christie.yaml +0 -10
  43. package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +0 -10
  44. package/pennyfarthing-dist/personas/themes/all-stars.yaml +0 -10
  45. package/pennyfarthing-dist/personas/themes/ancient-philosophers.yaml +0 -12
  46. package/pennyfarthing-dist/personas/themes/ancient-strategists.yaml +0 -12
  47. package/pennyfarthing-dist/personas/themes/arcane.yaml +0 -10
  48. package/pennyfarthing-dist/personas/themes/arthurian-mythos.yaml +0 -13
  49. package/pennyfarthing-dist/personas/themes/avatar-the-last-airbender.yaml +0 -10
  50. package/pennyfarthing-dist/personas/themes/babylon-5.yaml +0 -10
  51. package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +0 -10
  52. package/pennyfarthing-dist/personas/themes/better-call-saul.yaml +0 -10
  53. package/pennyfarthing-dist/personas/themes/big-lebowski.yaml +0 -10
  54. package/pennyfarthing-dist/personas/themes/black-sails.yaml +0 -10
  55. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +0 -10
  56. package/pennyfarthing-dist/personas/themes/bobiverse.yaml +0 -10
  57. package/pennyfarthing-dist/personas/themes/breaking-bad.yaml +0 -12
  58. package/pennyfarthing-dist/personas/themes/catch-22.yaml +0 -12
  59. package/pennyfarthing-dist/personas/themes/classical-composers.yaml +0 -12
  60. package/pennyfarthing-dist/personas/themes/count-of-monte-cristo.yaml +0 -12
  61. package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +0 -12
  62. package/pennyfarthing-dist/personas/themes/deadwood.yaml +0 -10
  63. package/pennyfarthing-dist/personas/themes/dickens.yaml +0 -12
  64. package/pennyfarthing-dist/personas/themes/discworld.yaml +0 -10
  65. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +0 -10
  66. package/pennyfarthing-dist/personas/themes/don-quixote.yaml +0 -12
  67. package/pennyfarthing-dist/personas/themes/dune.yaml +0 -10
  68. package/pennyfarthing-dist/personas/themes/enlightenment-thinkers.yaml +0 -12
  69. package/pennyfarthing-dist/personas/themes/expeditionary-force.yaml +0 -10
  70. package/pennyfarthing-dist/personas/themes/fargo.yaml +0 -12
  71. package/pennyfarthing-dist/personas/themes/film-auteurs.yaml +0 -12
  72. package/pennyfarthing-dist/personas/themes/firefly.yaml +0 -12
  73. package/pennyfarthing-dist/personas/themes/foundation.yaml +0 -10
  74. package/pennyfarthing-dist/personas/themes/futurama.yaml +0 -12
  75. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +0 -10
  76. package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +0 -12
  77. package/pennyfarthing-dist/personas/themes/gothic-literature.yaml +0 -12
  78. package/pennyfarthing-dist/personas/themes/great-gatsby.yaml +0 -12
  79. package/pennyfarthing-dist/personas/themes/greek-mythology.yaml +0 -13
  80. package/pennyfarthing-dist/personas/themes/hannibal.yaml +0 -10
  81. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +0 -12
  82. package/pennyfarthing-dist/personas/themes/his-dark-materials.yaml +0 -10
  83. package/pennyfarthing-dist/personas/themes/historical-figures.yaml +0 -10
  84. package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +0 -12
  85. package/pennyfarthing-dist/personas/themes/house-md.yaml +0 -12
  86. package/pennyfarthing-dist/personas/themes/imperial-radch.yaml +0 -10
  87. package/pennyfarthing-dist/personas/themes/inspector-morse.yaml +0 -10
  88. package/pennyfarthing-dist/personas/themes/jane-austen.yaml +0 -10
  89. package/pennyfarthing-dist/personas/themes/jazz-legends.yaml +0 -12
  90. package/pennyfarthing-dist/personas/themes/justified.yaml +0 -10
  91. package/pennyfarthing-dist/personas/themes/legion-of-doom.yaml +0 -10
  92. package/pennyfarthing-dist/personas/themes/les-miserables.yaml +0 -10
  93. package/pennyfarthing-dist/personas/themes/lord-of-the-rings.yaml +0 -12
  94. package/pennyfarthing-dist/personas/themes/lovecraft-mythos.yaml +0 -13
  95. package/pennyfarthing-dist/personas/themes/mad-max.yaml +0 -10
  96. package/pennyfarthing-dist/personas/themes/mad-men.yaml +0 -10
  97. package/pennyfarthing-dist/personas/themes/marvel-mcu.yaml +0 -10
  98. package/pennyfarthing-dist/personas/themes/mash.yaml +0 -12
  99. package/pennyfarthing-dist/personas/themes/mass-effect.yaml +0 -10
  100. package/pennyfarthing-dist/personas/themes/military-commanders.yaml +0 -12
  101. package/pennyfarthing-dist/personas/themes/moby-dick.yaml +0 -12
  102. package/pennyfarthing-dist/personas/themes/monty-python.yaml +0 -10
  103. package/pennyfarthing-dist/personas/themes/neuromancer.yaml +0 -10
  104. package/pennyfarthing-dist/personas/themes/norse-mythology.yaml +0 -12
  105. package/pennyfarthing-dist/personas/themes/parks-and-rec.yaml +0 -12
  106. package/pennyfarthing-dist/personas/themes/peaky-blinders.yaml +0 -10
  107. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +0 -10
  108. package/pennyfarthing-dist/personas/themes/renaissance-masters.yaml +0 -12
  109. package/pennyfarthing-dist/personas/themes/rome.yaml +0 -10
  110. package/pennyfarthing-dist/personas/themes/russian-masters.yaml +0 -12
  111. package/pennyfarthing-dist/personas/themes/sandman.yaml +0 -10
  112. package/pennyfarthing-dist/personas/themes/scientific-revolutionaries.yaml +0 -12
  113. package/pennyfarthing-dist/personas/themes/shakespeare.yaml +0 -10
  114. package/pennyfarthing-dist/personas/themes/sherlock-holmes.yaml +0 -10
  115. package/pennyfarthing-dist/personas/themes/snow-crash.yaml +0 -10
  116. package/pennyfarthing-dist/personas/themes/software-pioneers.yaml +0 -10
  117. package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +0 -11
  118. package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +0 -10
  119. package/pennyfarthing-dist/personas/themes/star-wars.yaml +0 -10
  120. package/pennyfarthing-dist/personas/themes/succession.yaml +0 -10
  121. package/pennyfarthing-dist/personas/themes/superfriends.yaml +0 -10
  122. package/pennyfarthing-dist/personas/themes/ted-lasso.yaml +0 -11
  123. package/pennyfarthing-dist/personas/themes/the-americans.yaml +0 -10
  124. package/pennyfarthing-dist/personas/themes/the-crown.yaml +0 -10
  125. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +0 -10
  126. package/pennyfarthing-dist/personas/themes/the-good-place.yaml +0 -11
  127. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +0 -15
  128. package/pennyfarthing-dist/personas/themes/the-odyssey.yaml +0 -10
  129. package/pennyfarthing-dist/personas/themes/the-office.yaml +0 -11
  130. package/pennyfarthing-dist/personas/themes/the-simpsons.yaml +0 -12
  131. package/pennyfarthing-dist/personas/themes/the-sopranos.yaml +0 -10
  132. package/pennyfarthing-dist/personas/themes/the-wire.yaml +0 -12
  133. package/pennyfarthing-dist/personas/themes/the-witcher.yaml +0 -10
  134. package/pennyfarthing-dist/personas/themes/twin-peaks.yaml +0 -10
  135. package/pennyfarthing-dist/personas/themes/vorkosigan-saga.yaml +0 -10
  136. package/pennyfarthing-dist/personas/themes/watchmen.yaml +0 -10
  137. package/pennyfarthing-dist/personas/themes/west-wing.yaml +0 -10
  138. package/pennyfarthing-dist/personas/themes/world-explorers.yaml +0 -12
  139. package/pennyfarthing-dist/personas/themes/wwii-leaders.yaml +0 -12
  140. package/pennyfarthing-dist/personas/themes/x-files.yaml +0 -10
  141. package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -14
  142. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  143. package/pennyfarthing-dist/scripts/core/prime.sh +17 -2
  144. package/pennyfarthing-dist/scripts/core/run.sh +5 -5
  145. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
  146. package/pennyfarthing-dist/scripts/git/release.sh +2 -2
  147. package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
  148. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
  149. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
  150. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
  151. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
  152. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
  153. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
  154. package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
  155. package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
  156. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +102 -0
  157. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
  158. package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
  159. package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
  160. package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
  161. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
  162. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
  163. package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
  164. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
  165. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +6 -2
  166. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
  167. package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
  168. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
  169. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
  170. package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
  171. package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
  172. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
  173. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
  174. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
  175. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
  176. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
  177. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
  178. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +3 -2
  179. package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
  180. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
  181. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
  182. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
  183. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
  184. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
  185. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
  186. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
  187. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
  188. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
  189. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +18 -0
  190. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +18 -4
  191. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +13 -5
  192. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/jira/client.py +1 -1
  195. package/pennyfarthing_scripts/prime/__init__.py +98 -11
  196. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/prime/cli.py +208 -53
  203. package/pennyfarthing_scripts/prime/models.py +169 -0
  204. package/pennyfarthing_scripts/prime/persona.py +288 -0
  205. package/pennyfarthing_scripts/prime/session.py +183 -0
  206. package/pennyfarthing_scripts/prime/workflow.py +275 -0
  207. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  209. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  210. 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