@pennyfarthing/core 8.0.4 → 9.0.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 +3 -3
- package/package.json +3 -3
- package/pennyfarthing-dist/agents/README.md +1 -1
- package/pennyfarthing-dist/agents/dev.md +1 -1
- package/pennyfarthing-dist/agents/handoff.md +1 -1
- package/pennyfarthing-dist/agents/reviewer-preflight.md +1 -1
- package/pennyfarthing-dist/agents/reviewer.md +21 -4
- package/pennyfarthing-dist/agents/sm-setup.md +3 -3
- package/pennyfarthing-dist/agents/sm.md +11 -1
- package/pennyfarthing-dist/agents/tea.md +1 -1
- package/pennyfarthing-dist/agents/testing-runner.md +3 -3
- package/pennyfarthing-dist/commands/architect.md +3 -1
- package/pennyfarthing-dist/commands/continue-session.md +2 -2
- package/pennyfarthing-dist/commands/dev.md +3 -1
- package/pennyfarthing-dist/commands/devops.md +3 -1
- package/pennyfarthing-dist/commands/health-check.md +3 -1
- package/pennyfarthing-dist/commands/new-work.md +23 -0
- package/pennyfarthing-dist/commands/orchestrator.md +3 -1
- package/pennyfarthing-dist/commands/parallel-work.md +6 -4
- package/pennyfarthing-dist/commands/pm.md +3 -1
- package/pennyfarthing-dist/commands/prime.md +18 -22
- package/pennyfarthing-dist/commands/reviewer.md +3 -1
- package/pennyfarthing-dist/commands/set-theme.md +1 -1
- package/pennyfarthing-dist/commands/sm.md +3 -1
- package/pennyfarthing-dist/commands/sprint.md +13 -4
- package/pennyfarthing-dist/commands/tea.md +3 -1
- package/pennyfarthing-dist/commands/tech-writer.md +3 -1
- package/pennyfarthing-dist/commands/ux-designer.md +3 -1
- package/pennyfarthing-dist/commands/work.md +4 -2
- package/pennyfarthing-dist/guides/agent-behavior.md +36 -257
- package/pennyfarthing-dist/personas/themes/rome.yaml +11 -11
- package/pennyfarthing-dist/scripts/core/agent-session.sh +7 -0
- package/pennyfarthing-dist/scripts/core/check-context.sh +140 -226
- package/pennyfarthing-dist/scripts/core/handoff-marker.sh +13 -2
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +4 -1
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -7
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +4 -11
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +3 -8
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +3 -3
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -7
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +2 -8
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +2 -8
- package/pennyfarthing-dist/scripts/lib/find-root.sh +17 -45
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -7
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +2 -8
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +2 -8
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +2 -8
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +2 -8
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +2 -8
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +2 -8
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +2 -8
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +2 -8
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +2 -8
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +2 -1
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +4 -9
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +2 -8
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +2 -8
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -7
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +2 -8
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +2 -8
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +2 -8
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +2 -8
- package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +1 -1
- package/pennyfarthing-dist/skills/jira/SKILL.md +48 -24
- package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +7 -0
- package/pennyfarthing-dist/skills/sprint/skill.md +30 -30
- package/pennyfarthing-dist/workflows/patch.yaml +68 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/cli.py +168 -0
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/context.py +414 -0
- package/pennyfarthing_scripts/patch_mode.py +449 -0
- 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__/loader.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__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +209 -1
- package/pennyfarthing_scripts/prime/models.py +9 -0
- package/pennyfarthing_scripts/prime/persona.py +41 -0
- package/pennyfarthing_scripts/prime/tiers.py +201 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/cli.py +144 -84
- package/pennyfarthing_scripts/tests/test_patch_mode.py +830 -0
- package/pennyfarthing_scripts/tests/test_tiers.py +1090 -0
- package/pennyfarthing_scripts/tests/test_token_counting.py +559 -0
- package/pennyfarthing_scripts/tests/test_workflow_check.py +341 -0
- package/pennyfarthing_scripts/workflow.py +104 -0
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -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_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.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/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"""Tests for component-level token counting.
|
|
2
|
+
|
|
3
|
+
Story: MSSCI-12800 - Component-level token tracking
|
|
4
|
+
Epic: MSSCI-12793 - Tiered Context Injection System
|
|
5
|
+
|
|
6
|
+
This story adds granular token counting per injected component, allowing users
|
|
7
|
+
to see exactly where context tokens are being spent.
|
|
8
|
+
|
|
9
|
+
Acceptance Criteria:
|
|
10
|
+
- AC1: Each component in the context injection has an approximate token count
|
|
11
|
+
- AC2: Token breakdown is passed from Python prime script to TypeScript/UI
|
|
12
|
+
- AC3: DebugPanel displays a collapsible list of components with their token counts
|
|
13
|
+
- AC4: Token counts are approximate but reasonably accurate (~10% tolerance)
|
|
14
|
+
|
|
15
|
+
This file tests the Python side (AC1, AC2, AC4).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from unittest.mock import patch
|
|
21
|
+
import yaml
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# AC1: Each component has an approximate token count
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestComponentTokenCounting:
|
|
31
|
+
"""Tests for per-component token counting (AC1)."""
|
|
32
|
+
|
|
33
|
+
def test_load_tier_components_returns_token_counts(self, tmp_path: Path) -> None:
|
|
34
|
+
"""Test load_tier_components returns token counts for each component."""
|
|
35
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier
|
|
36
|
+
|
|
37
|
+
self._setup_complete_project(tmp_path)
|
|
38
|
+
|
|
39
|
+
result = load_tier_components(
|
|
40
|
+
tier=ContextTier.FULL,
|
|
41
|
+
agent_name="dev",
|
|
42
|
+
project_root=tmp_path,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Result should include token_counts dict
|
|
46
|
+
assert "token_counts" in result
|
|
47
|
+
assert isinstance(result["token_counts"], dict)
|
|
48
|
+
|
|
49
|
+
def test_token_counts_include_all_full_tier_components(self, tmp_path: Path) -> None:
|
|
50
|
+
"""Test FULL tier returns token counts for all components."""
|
|
51
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier
|
|
52
|
+
|
|
53
|
+
self._setup_complete_project(tmp_path)
|
|
54
|
+
|
|
55
|
+
result = load_tier_components(
|
|
56
|
+
tier=ContextTier.FULL,
|
|
57
|
+
agent_name="dev",
|
|
58
|
+
project_root=tmp_path,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
token_counts = result.get("token_counts", {})
|
|
62
|
+
|
|
63
|
+
# FULL tier should have counts for all these components
|
|
64
|
+
expected_components = [
|
|
65
|
+
"workflow_state",
|
|
66
|
+
"agent_definition",
|
|
67
|
+
"persona",
|
|
68
|
+
"behavior_guide",
|
|
69
|
+
"sprint_context",
|
|
70
|
+
"session_header",
|
|
71
|
+
"sidecars",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
for component in expected_components:
|
|
75
|
+
assert component in token_counts, f"Missing token count for {component}"
|
|
76
|
+
assert isinstance(token_counts[component], int), f"Token count for {component} should be int"
|
|
77
|
+
assert token_counts[component] >= 0, f"Token count for {component} should be non-negative"
|
|
78
|
+
|
|
79
|
+
def test_token_counts_are_positive_for_loaded_components(self, tmp_path: Path) -> None:
|
|
80
|
+
"""Test that loaded components have positive token counts."""
|
|
81
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier
|
|
82
|
+
|
|
83
|
+
self._setup_complete_project(tmp_path)
|
|
84
|
+
|
|
85
|
+
result = load_tier_components(
|
|
86
|
+
tier=ContextTier.FULL,
|
|
87
|
+
agent_name="dev",
|
|
88
|
+
project_root=tmp_path,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
token_counts = result.get("token_counts", {})
|
|
92
|
+
|
|
93
|
+
# Components with content should have positive counts
|
|
94
|
+
assert token_counts.get("agent_definition", 0) > 0
|
|
95
|
+
assert token_counts.get("behavior_guide", 0) > 0
|
|
96
|
+
assert token_counts.get("sidecars", 0) > 0
|
|
97
|
+
|
|
98
|
+
def test_token_counts_zero_for_missing_components(self, tmp_path: Path) -> None:
|
|
99
|
+
"""Test that missing optional components have zero token counts."""
|
|
100
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier
|
|
101
|
+
|
|
102
|
+
# Minimal setup - only agent definition
|
|
103
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
104
|
+
pf_dir.mkdir()
|
|
105
|
+
agents_dir = pf_dir / "agents"
|
|
106
|
+
agents_dir.mkdir()
|
|
107
|
+
(agents_dir / "dev.md").write_text("# Dev Agent")
|
|
108
|
+
|
|
109
|
+
result = load_tier_components(
|
|
110
|
+
tier=ContextTier.FULL,
|
|
111
|
+
agent_name="dev",
|
|
112
|
+
project_root=tmp_path,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
token_counts = result.get("token_counts", {})
|
|
116
|
+
|
|
117
|
+
# Missing components should have 0 count
|
|
118
|
+
assert token_counts.get("sidecars", 0) == 0
|
|
119
|
+
assert token_counts.get("behavior_guide", 0) == 0
|
|
120
|
+
|
|
121
|
+
def test_refresh_tier_only_counts_included_components(self, tmp_path: Path) -> None:
|
|
122
|
+
"""Test REFRESH tier only includes counts for its components."""
|
|
123
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier
|
|
124
|
+
|
|
125
|
+
self._setup_complete_project(tmp_path)
|
|
126
|
+
|
|
127
|
+
result = load_tier_components(
|
|
128
|
+
tier=ContextTier.REFRESH,
|
|
129
|
+
agent_name="dev",
|
|
130
|
+
project_root=tmp_path,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
token_counts = result.get("token_counts", {})
|
|
134
|
+
|
|
135
|
+
# REFRESH should have counts for workflow_state, sprint_context, session_header
|
|
136
|
+
assert "workflow_state" in token_counts
|
|
137
|
+
assert "sprint_context" in token_counts
|
|
138
|
+
assert "session_header" in token_counts
|
|
139
|
+
|
|
140
|
+
# REFRESH should NOT have counts for excluded components
|
|
141
|
+
# (or they should be 0)
|
|
142
|
+
assert token_counts.get("agent_definition", 0) == 0
|
|
143
|
+
assert token_counts.get("behavior_guide", 0) == 0
|
|
144
|
+
assert token_counts.get("sidecars", 0) == 0
|
|
145
|
+
|
|
146
|
+
def test_handoff_tier_only_counts_included_components(self, tmp_path: Path) -> None:
|
|
147
|
+
"""Test HANDOFF tier only includes counts for its components."""
|
|
148
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier
|
|
149
|
+
|
|
150
|
+
self._setup_complete_project(tmp_path)
|
|
151
|
+
|
|
152
|
+
result = load_tier_components(
|
|
153
|
+
tier=ContextTier.HANDOFF,
|
|
154
|
+
agent_name="dev",
|
|
155
|
+
project_root=tmp_path,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
token_counts = result.get("token_counts", {})
|
|
159
|
+
|
|
160
|
+
# HANDOFF should have counts for workflow_state, agent_definition, persona_compressed
|
|
161
|
+
assert "workflow_state" in token_counts
|
|
162
|
+
assert "agent_definition" in token_counts
|
|
163
|
+
assert "persona_compressed" in token_counts or "persona" in token_counts
|
|
164
|
+
|
|
165
|
+
def test_minimal_tier_only_counts_workflow_state(self, tmp_path: Path) -> None:
|
|
166
|
+
"""Test MINIMAL tier only counts workflow state."""
|
|
167
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier
|
|
168
|
+
|
|
169
|
+
self._setup_complete_project(tmp_path)
|
|
170
|
+
|
|
171
|
+
result = load_tier_components(
|
|
172
|
+
tier=ContextTier.MINIMAL,
|
|
173
|
+
agent_name="dev",
|
|
174
|
+
project_root=tmp_path,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
token_counts = result.get("token_counts", {})
|
|
178
|
+
|
|
179
|
+
# MINIMAL should only have workflow_state
|
|
180
|
+
assert "workflow_state" in token_counts
|
|
181
|
+
assert token_counts.get("workflow_state", 0) > 0
|
|
182
|
+
|
|
183
|
+
# All other components should be 0
|
|
184
|
+
assert token_counts.get("agent_definition", 0) == 0
|
|
185
|
+
assert token_counts.get("persona", 0) == 0
|
|
186
|
+
assert token_counts.get("behavior_guide", 0) == 0
|
|
187
|
+
|
|
188
|
+
def test_total_tokens_is_sum_of_components(self, tmp_path: Path) -> None:
|
|
189
|
+
"""Test that total_tokens equals sum of component counts."""
|
|
190
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier
|
|
191
|
+
|
|
192
|
+
self._setup_complete_project(tmp_path)
|
|
193
|
+
|
|
194
|
+
result = load_tier_components(
|
|
195
|
+
tier=ContextTier.FULL,
|
|
196
|
+
agent_name="dev",
|
|
197
|
+
project_root=tmp_path,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
token_counts = result.get("token_counts", {})
|
|
201
|
+
total_tokens = result.get("total_tokens", 0)
|
|
202
|
+
|
|
203
|
+
# Total should equal sum of components
|
|
204
|
+
component_sum = sum(token_counts.values())
|
|
205
|
+
assert total_tokens == component_sum
|
|
206
|
+
|
|
207
|
+
def _setup_complete_project(self, tmp_path: Path) -> None:
|
|
208
|
+
"""Set up a complete project for component testing."""
|
|
209
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
210
|
+
pf_dir.mkdir()
|
|
211
|
+
|
|
212
|
+
# Agent definition
|
|
213
|
+
agents_dir = pf_dir / "agents"
|
|
214
|
+
agents_dir.mkdir()
|
|
215
|
+
(agents_dir / "dev.md").write_text("# Dev Agent\n\nDeveloper agent with implementation focus.")
|
|
216
|
+
|
|
217
|
+
# Behavior guide
|
|
218
|
+
guides_dir = pf_dir / "guides"
|
|
219
|
+
guides_dir.mkdir()
|
|
220
|
+
(guides_dir / "agent-behavior.md").write_text("# Agent Behavior Guide\n\nShared protocols for all agents.")
|
|
221
|
+
|
|
222
|
+
# Sidecars
|
|
223
|
+
sidecar_dir = pf_dir / "sidecars" / "dev"
|
|
224
|
+
sidecar_dir.mkdir(parents=True)
|
|
225
|
+
(sidecar_dir / "patterns.md").write_text("# Dev Patterns\n\nDevelopment patterns documentation.")
|
|
226
|
+
(sidecar_dir / "gotchas.md").write_text("# Dev Gotchas\n\nCommon pitfalls to avoid.")
|
|
227
|
+
|
|
228
|
+
# Theme
|
|
229
|
+
(pf_dir / "config.local.yaml").write_text(yaml.dump({"theme": "test-theme"}))
|
|
230
|
+
themes_dir = pf_dir / "personas" / "themes"
|
|
231
|
+
themes_dir.mkdir(parents=True)
|
|
232
|
+
(themes_dir / "test-theme.yaml").write_text(yaml.dump({
|
|
233
|
+
"theme": {"name": "Test Theme", "user_title": "Developer"},
|
|
234
|
+
"agents": {
|
|
235
|
+
"dev": {
|
|
236
|
+
"character": "Test Developer",
|
|
237
|
+
"style": "Practical and efficient",
|
|
238
|
+
"role": "Implementation specialist",
|
|
239
|
+
"quote": "Ship it!",
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}))
|
|
243
|
+
|
|
244
|
+
# Sprint
|
|
245
|
+
sprint_dir = tmp_path / "sprint"
|
|
246
|
+
sprint_dir.mkdir()
|
|
247
|
+
(sprint_dir / "current-sprint.yaml").write_text(yaml.dump({
|
|
248
|
+
"sprint": {"number": 12, "goal": "Test sprint"},
|
|
249
|
+
"epics": []
|
|
250
|
+
}))
|
|
251
|
+
|
|
252
|
+
# Session
|
|
253
|
+
session_dir = tmp_path / ".session"
|
|
254
|
+
session_dir.mkdir()
|
|
255
|
+
(session_dir / "test-session.md").write_text("# Test Session\n\n- **Phase:** green")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# =============================================================================
|
|
259
|
+
# AC2: Token breakdown passed from Python to TypeScript/UI
|
|
260
|
+
# =============================================================================
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TestTokenBreakdownOutput:
|
|
264
|
+
"""Tests for token breakdown in output (AC2)."""
|
|
265
|
+
|
|
266
|
+
def test_json_output_includes_token_counts(self, tmp_path: Path, capsys) -> None:
|
|
267
|
+
"""Test JSON output includes token_counts object."""
|
|
268
|
+
from pennyfarthing_scripts.prime.cli import prime
|
|
269
|
+
|
|
270
|
+
self._setup_project(tmp_path)
|
|
271
|
+
|
|
272
|
+
with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
|
|
273
|
+
result = prime(
|
|
274
|
+
agent_name="dev",
|
|
275
|
+
tier="FULL",
|
|
276
|
+
json_output=True,
|
|
277
|
+
no_workflow=True,
|
|
278
|
+
no_register=True,
|
|
279
|
+
project_root=tmp_path,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
assert result == 0
|
|
283
|
+
captured = capsys.readouterr()
|
|
284
|
+
data = json.loads(captured.out)
|
|
285
|
+
|
|
286
|
+
# JSON should include token_counts
|
|
287
|
+
assert "token_counts" in data
|
|
288
|
+
assert isinstance(data["token_counts"], dict)
|
|
289
|
+
|
|
290
|
+
def test_json_output_includes_total_tokens(self, tmp_path: Path, capsys) -> None:
|
|
291
|
+
"""Test JSON output includes total_tokens field."""
|
|
292
|
+
from pennyfarthing_scripts.prime.cli import prime
|
|
293
|
+
|
|
294
|
+
self._setup_project(tmp_path)
|
|
295
|
+
|
|
296
|
+
with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
|
|
297
|
+
result = prime(
|
|
298
|
+
agent_name="dev",
|
|
299
|
+
tier="FULL",
|
|
300
|
+
json_output=True,
|
|
301
|
+
no_workflow=True,
|
|
302
|
+
no_register=True,
|
|
303
|
+
project_root=tmp_path,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
assert result == 0
|
|
307
|
+
captured = capsys.readouterr()
|
|
308
|
+
data = json.loads(captured.out)
|
|
309
|
+
|
|
310
|
+
# JSON should include total_tokens
|
|
311
|
+
assert "total_tokens" in data
|
|
312
|
+
assert isinstance(data["total_tokens"], int)
|
|
313
|
+
assert data["total_tokens"] > 0
|
|
314
|
+
|
|
315
|
+
def test_json_token_counts_match_tier(self, tmp_path: Path, capsys) -> None:
|
|
316
|
+
"""Test JSON token counts reflect the tier's components."""
|
|
317
|
+
from pennyfarthing_scripts.prime.cli import prime
|
|
318
|
+
|
|
319
|
+
self._setup_project(tmp_path)
|
|
320
|
+
|
|
321
|
+
with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
|
|
322
|
+
# MINIMAL tier
|
|
323
|
+
result = prime(
|
|
324
|
+
agent_name="dev",
|
|
325
|
+
tier="MINIMAL",
|
|
326
|
+
json_output=True,
|
|
327
|
+
no_workflow=True,
|
|
328
|
+
no_register=True,
|
|
329
|
+
project_root=tmp_path,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
assert result == 0
|
|
333
|
+
captured = capsys.readouterr()
|
|
334
|
+
data = json.loads(captured.out)
|
|
335
|
+
|
|
336
|
+
token_counts = data.get("token_counts", {})
|
|
337
|
+
|
|
338
|
+
# MINIMAL should only have workflow_state with tokens
|
|
339
|
+
assert token_counts.get("workflow_state", 0) > 0
|
|
340
|
+
# Other components should be 0 or missing
|
|
341
|
+
assert token_counts.get("agent_definition", 0) == 0
|
|
342
|
+
assert token_counts.get("behavior_guide", 0) == 0
|
|
343
|
+
|
|
344
|
+
def test_json_output_token_counts_per_component(self, tmp_path: Path, capsys) -> None:
|
|
345
|
+
"""Test JSON output has individual component counts."""
|
|
346
|
+
from pennyfarthing_scripts.prime.cli import prime
|
|
347
|
+
|
|
348
|
+
self._setup_project(tmp_path)
|
|
349
|
+
|
|
350
|
+
with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
|
|
351
|
+
with patch("pennyfarthing_scripts.prime.loader.get_project_root", return_value=tmp_path):
|
|
352
|
+
result = prime(
|
|
353
|
+
agent_name="dev",
|
|
354
|
+
tier="FULL",
|
|
355
|
+
json_output=True,
|
|
356
|
+
no_register=True,
|
|
357
|
+
project_root=tmp_path,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
assert result == 0
|
|
361
|
+
captured = capsys.readouterr()
|
|
362
|
+
data = json.loads(captured.out)
|
|
363
|
+
|
|
364
|
+
token_counts = data.get("token_counts", {})
|
|
365
|
+
|
|
366
|
+
# Should have individual component entries
|
|
367
|
+
expected_keys = ["workflow_state", "agent_definition", "persona", "behavior_guide"]
|
|
368
|
+
for key in expected_keys:
|
|
369
|
+
assert key in token_counts, f"Missing {key} in token_counts"
|
|
370
|
+
|
|
371
|
+
def _setup_project(self, tmp_path: Path) -> None:
|
|
372
|
+
"""Set up project for output testing."""
|
|
373
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
374
|
+
pf_dir.mkdir()
|
|
375
|
+
|
|
376
|
+
agents_dir = pf_dir / "agents"
|
|
377
|
+
agents_dir.mkdir()
|
|
378
|
+
(agents_dir / "dev.md").write_text("# Dev Agent\n\nA developer agent.")
|
|
379
|
+
|
|
380
|
+
guides_dir = pf_dir / "guides"
|
|
381
|
+
guides_dir.mkdir()
|
|
382
|
+
(guides_dir / "agent-behavior.md").write_text("# Behavior Guide")
|
|
383
|
+
|
|
384
|
+
(pf_dir / "config.local.yaml").write_text(yaml.dump({"theme": "test"}))
|
|
385
|
+
themes_dir = pf_dir / "personas" / "themes"
|
|
386
|
+
themes_dir.mkdir(parents=True)
|
|
387
|
+
(themes_dir / "test.yaml").write_text(yaml.dump({
|
|
388
|
+
"agents": {"dev": {"character": "Dev", "style": "s", "role": "r"}}
|
|
389
|
+
}))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# =============================================================================
|
|
393
|
+
# AC4: Token counts are accurate within 10% tolerance
|
|
394
|
+
# =============================================================================
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class TestTokenCountAccuracy:
|
|
398
|
+
"""Tests for token count accuracy (AC4)."""
|
|
399
|
+
|
|
400
|
+
def test_token_count_uses_tiktoken_or_approximation(self) -> None:
|
|
401
|
+
"""Test token counting uses tiktoken or reasonable approximation."""
|
|
402
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
403
|
+
|
|
404
|
+
# Test string with known token characteristics
|
|
405
|
+
# "Hello, world!" is typically 4 tokens in cl100k_base
|
|
406
|
+
text = "Hello, world!"
|
|
407
|
+
count = estimate_tokens(text)
|
|
408
|
+
|
|
409
|
+
# Should be close to 4 tokens (allow 10% tolerance = 1 token)
|
|
410
|
+
assert 3 <= count <= 5, f"Token count {count} not within expected range for 'Hello, world!'"
|
|
411
|
+
|
|
412
|
+
def test_token_count_scales_with_text_length(self) -> None:
|
|
413
|
+
"""Test token count increases proportionally with text length."""
|
|
414
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
415
|
+
|
|
416
|
+
short_text = "Hello"
|
|
417
|
+
medium_text = "Hello " * 10
|
|
418
|
+
long_text = "Hello " * 100
|
|
419
|
+
|
|
420
|
+
short_count = estimate_tokens(short_text)
|
|
421
|
+
medium_count = estimate_tokens(medium_text)
|
|
422
|
+
long_count = estimate_tokens(long_text)
|
|
423
|
+
|
|
424
|
+
# Counts should increase with length
|
|
425
|
+
assert short_count < medium_count < long_count
|
|
426
|
+
|
|
427
|
+
def test_token_count_handles_empty_string(self) -> None:
|
|
428
|
+
"""Test token counting handles empty string gracefully."""
|
|
429
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
430
|
+
|
|
431
|
+
count = estimate_tokens("")
|
|
432
|
+
assert count == 0
|
|
433
|
+
|
|
434
|
+
def test_token_count_handles_unicode(self) -> None:
|
|
435
|
+
"""Test token counting handles unicode text."""
|
|
436
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
437
|
+
|
|
438
|
+
# Unicode text (emojis typically use multiple tokens)
|
|
439
|
+
text = "Hello 👋 World 🌍"
|
|
440
|
+
count = estimate_tokens(text)
|
|
441
|
+
|
|
442
|
+
assert count > 0
|
|
443
|
+
# Should handle without error
|
|
444
|
+
|
|
445
|
+
def test_token_count_handles_markdown(self) -> None:
|
|
446
|
+
"""Test token counting handles markdown formatting."""
|
|
447
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
448
|
+
|
|
449
|
+
markdown = """# Heading
|
|
450
|
+
|
|
451
|
+
This is a **bold** statement with `code` and:
|
|
452
|
+
- List item 1
|
|
453
|
+
- List item 2
|
|
454
|
+
|
|
455
|
+
```python
|
|
456
|
+
def hello():
|
|
457
|
+
print("world")
|
|
458
|
+
```
|
|
459
|
+
"""
|
|
460
|
+
count = estimate_tokens(markdown)
|
|
461
|
+
|
|
462
|
+
# Markdown should count all characters including formatting
|
|
463
|
+
assert count > 20 # Reasonable minimum for this content
|
|
464
|
+
|
|
465
|
+
def test_component_token_count_within_10_percent_of_actual(self, tmp_path: Path) -> None:
|
|
466
|
+
"""Test component token counts are within 10% of actual tiktoken count."""
|
|
467
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier, estimate_tokens
|
|
468
|
+
|
|
469
|
+
# Create a known-content project
|
|
470
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
471
|
+
pf_dir.mkdir()
|
|
472
|
+
agents_dir = pf_dir / "agents"
|
|
473
|
+
agents_dir.mkdir()
|
|
474
|
+
|
|
475
|
+
# Write a file with known content
|
|
476
|
+
agent_content = "# Developer Agent\n\n" + ("Test content. " * 50)
|
|
477
|
+
(agents_dir / "dev.md").write_text(agent_content)
|
|
478
|
+
|
|
479
|
+
# Get token count from load_tier_components
|
|
480
|
+
result = load_tier_components(
|
|
481
|
+
tier=ContextTier.FULL,
|
|
482
|
+
agent_name="dev",
|
|
483
|
+
project_root=tmp_path,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
reported_count = result.get("token_counts", {}).get("agent_definition", 0)
|
|
487
|
+
|
|
488
|
+
# Get actual token count
|
|
489
|
+
actual_count = estimate_tokens(agent_content)
|
|
490
|
+
|
|
491
|
+
# Should be within 10%
|
|
492
|
+
tolerance = actual_count * 0.1
|
|
493
|
+
assert abs(reported_count - actual_count) <= tolerance, \
|
|
494
|
+
f"Reported {reported_count} vs actual {actual_count}, tolerance {tolerance}"
|
|
495
|
+
|
|
496
|
+
def test_total_tokens_within_10_percent_of_sum(self, tmp_path: Path) -> None:
|
|
497
|
+
"""Test total_tokens is within 10% of manually summed content."""
|
|
498
|
+
from pennyfarthing_scripts.prime.tiers import load_tier_components, ContextTier, estimate_tokens
|
|
499
|
+
|
|
500
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
501
|
+
pf_dir.mkdir()
|
|
502
|
+
agents_dir = pf_dir / "agents"
|
|
503
|
+
agents_dir.mkdir()
|
|
504
|
+
(agents_dir / "dev.md").write_text("# Dev Agent")
|
|
505
|
+
|
|
506
|
+
guides_dir = pf_dir / "guides"
|
|
507
|
+
guides_dir.mkdir()
|
|
508
|
+
(guides_dir / "agent-behavior.md").write_text("# Behavior Guide")
|
|
509
|
+
|
|
510
|
+
result = load_tier_components(
|
|
511
|
+
tier=ContextTier.FULL,
|
|
512
|
+
agent_name="dev",
|
|
513
|
+
project_root=tmp_path,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
total_tokens = result.get("total_tokens", 0)
|
|
517
|
+
token_counts = result.get("token_counts", {})
|
|
518
|
+
component_sum = sum(token_counts.values())
|
|
519
|
+
|
|
520
|
+
# Total should match component sum
|
|
521
|
+
assert total_tokens == component_sum
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# =============================================================================
|
|
525
|
+
# Utility function tests
|
|
526
|
+
# =============================================================================
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class TestEstimateTokensFunction:
|
|
530
|
+
"""Tests for the estimate_tokens utility function."""
|
|
531
|
+
|
|
532
|
+
def test_estimate_tokens_exists(self) -> None:
|
|
533
|
+
"""Test estimate_tokens function is exported from tiers module."""
|
|
534
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
535
|
+
assert callable(estimate_tokens)
|
|
536
|
+
|
|
537
|
+
def test_estimate_tokens_returns_int(self) -> None:
|
|
538
|
+
"""Test estimate_tokens returns an integer."""
|
|
539
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
540
|
+
|
|
541
|
+
result = estimate_tokens("Hello, world!")
|
|
542
|
+
assert isinstance(result, int)
|
|
543
|
+
|
|
544
|
+
def test_estimate_tokens_positive_for_content(self) -> None:
|
|
545
|
+
"""Test estimate_tokens returns positive value for non-empty content."""
|
|
546
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
547
|
+
|
|
548
|
+
result = estimate_tokens("Some content here")
|
|
549
|
+
assert result > 0
|
|
550
|
+
|
|
551
|
+
def test_estimate_tokens_deterministic(self) -> None:
|
|
552
|
+
"""Test estimate_tokens returns same result for same input."""
|
|
553
|
+
from pennyfarthing_scripts.prime.tiers import estimate_tokens
|
|
554
|
+
|
|
555
|
+
text = "Consistent input text"
|
|
556
|
+
result1 = estimate_tokens(text)
|
|
557
|
+
result2 = estimate_tokens(text)
|
|
558
|
+
|
|
559
|
+
assert result1 == result2
|