@pennyfarthing/core 11.1.0 β†’ 11.2.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 (263) hide show
  1. package/README.md +8 -8
  2. package/package.json +16 -14
  3. package/packages/core/dist/cli/utils/constants.d.ts +1 -1
  4. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  5. package/packages/core/dist/cli/utils/constants.js +2 -1
  6. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  7. package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
  8. package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
  9. package/packages/core/dist/consultation/dialogue-manager.js +334 -0
  10. package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
  11. package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
  12. package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
  13. package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
  14. package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
  15. package/packages/core/dist/server/api/git.d.ts +13 -1
  16. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  17. package/packages/core/dist/server/api/git.js +53 -34
  18. package/packages/core/dist/server/api/git.js.map +1 -1
  19. package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
  20. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  21. package/packages/core/dist/server/otlp-receiver.js +185 -24
  22. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  23. package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
  24. package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
  25. package/packages/core/dist/server/otlp-receiver.test.js +446 -0
  26. package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
  27. package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
  28. package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
  29. package/packages/core/dist/shared/portrait-resolver.js +27 -0
  30. package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
  31. package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
  32. package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
  33. package/packages/core/dist/shared/skill-search.test.js +2 -2
  34. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
  35. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
  36. package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
  37. package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
  38. package/pennyfarthing-dist/agents/dev.md +1 -1
  39. package/pennyfarthing-dist/agents/reviewer.md +1 -1
  40. package/pennyfarthing-dist/agents/sm-setup.md +1 -1
  41. package/pennyfarthing-dist/agents/sm.md +2 -2
  42. package/pennyfarthing-dist/agents/tea.md +1 -1
  43. package/pennyfarthing-dist/agents/testing-runner.md +2 -1
  44. package/pennyfarthing-dist/commands/pf-chore.md +2 -2
  45. package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
  46. package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
  47. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
  48. package/pennyfarthing-dist/guides/bikerack.md +3 -3
  49. package/pennyfarthing-dist/guides/hooks.md +1 -1
  50. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  51. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  52. package/pennyfarthing-dist/scripts/README.md +1 -1
  53. package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
  54. package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
  55. package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
  56. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
  57. package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
  58. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  59. package/pennyfarthing-dist/scripts/git/README.md +24 -14
  60. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
  61. package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
  62. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
  63. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  64. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
  65. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  66. package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
  67. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
  68. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  69. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  70. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  71. package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
  72. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
  73. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
  74. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
  75. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  76. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
  77. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  78. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  79. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  80. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  81. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  82. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  83. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
  84. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  85. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  86. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  87. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  88. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  89. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  90. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  91. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  92. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  93. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  94. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  95. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  96. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  97. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  98. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  99. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  100. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  101. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  102. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  103. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  104. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  105. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  106. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
  107. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  108. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  109. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  110. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  111. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  112. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  113. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  114. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  115. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
  116. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
  117. package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
  118. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  119. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  120. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  121. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  122. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  123. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  124. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
  125. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  126. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  127. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  128. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  129. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  130. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
  131. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  132. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  133. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  134. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  135. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  136. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
  137. package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
  138. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  139. package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
  140. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
  141. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
  142. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  143. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
  144. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
  145. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
  146. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
  147. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
  148. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
  149. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
  150. package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
  151. package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
  152. package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
  153. package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
  154. package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
  155. package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
  156. package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
  157. package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
  158. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
  159. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
  160. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
  161. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  162. package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
  163. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
  164. package/pennyfarthing_scripts/CLAUDE.md +26 -4
  165. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  167. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/bc/cli.py +3 -5
  171. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  172. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/bikerack/__pycache__/portrait.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  183. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  184. package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
  185. package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
  186. package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
  187. package/pennyfarthing_scripts/bikerack/cli.py +10 -11
  188. package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
  189. package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
  190. package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
  191. package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
  192. package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
  193. package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
  194. package/pennyfarthing_scripts/bikerack/tui.py +336 -30
  195. package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
  196. package/pennyfarthing_scripts/cli.py +37 -65
  197. package/pennyfarthing_scripts/consultation/__init__.py +1 -0
  198. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/consultation/cli.py +149 -0
  201. package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
  202. package/pennyfarthing_scripts/context.py +3 -3
  203. package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/git/__init__.py +12 -1
  206. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/git/__pycache__/hooks_installer.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  211. package/pennyfarthing_scripts/git/__pycache__/worktree.cpython-314.pyc +0 -0
  212. package/pennyfarthing_scripts/git/create_branches.py +3 -4
  213. package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
  214. package/pennyfarthing_scripts/git/repos.py +196 -0
  215. package/pennyfarthing_scripts/git/status_all.py +27 -11
  216. package/pennyfarthing_scripts/git/worktree.py +302 -0
  217. package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/git_group/cli.py +143 -40
  220. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  222. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
  225. package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
  226. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  227. package/pennyfarthing_scripts/hooks.py +3 -17
  228. package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
  229. package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/prime/heatmap.py +655 -0
  232. package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/session_start_hook.py +1 -1
  235. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/sprint/loader.py +15 -1
  237. package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
  238. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  239. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  240. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  241. package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
  242. package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
  243. package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
  244. package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
  245. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
  247. package/pennyfarthing_scripts/validate/cli.py +17 -5
  248. package/pennyfarthing_scripts/workflow/__init__.py +40 -0
  249. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/workflow/cli.py +1099 -0
  255. package/pennyfarthing_scripts/workflow/helpers.py +241 -0
  256. package/pennyfarthing_scripts/{workflow.py β†’ workflow/scale.py} +0 -104
  257. package/pennyfarthing_scripts/workflow/state.py +112 -0
  258. package/scripts/README.md +41 -0
  259. package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
  260. package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
  261. package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
  262. package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
  263. package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -167
@@ -0,0 +1,655 @@
1
+ """
2
+ Agent activation heatmap β€” visualize context distribution and attention.
3
+
4
+ Parses raw prime output into sections, applies a U-shaped attention model
5
+ (Liu et al. "Lost in the Middle"), and renders a terminal heat map showing
6
+ where the LLM's attention falls across each agent's activation context.
7
+
8
+ Usage:
9
+ pf agent heatmap sm # Single agent detail view
10
+ pf agent heatmap --all # Summary across all agents
11
+ pf agent heatmap --all --csv # Machine-readable output
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import math
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+
24
+
25
+ # ── Section Categories ──────────────────────────────────────────────
26
+
27
+ CATEGORY_ICONS = {
28
+ "routing": "πŸ”€",
29
+ "identity": "πŸ†”",
30
+ "guardrail": "πŸ›‘",
31
+ "procedure": "πŸ“",
32
+ "reference": "πŸ“‹",
33
+ "persona": "🎭",
34
+ "shared": "πŸ“¦",
35
+ "learned": "🧠",
36
+ }
37
+
38
+ # Tags that always map to a specific category regardless of context
39
+ TAG_CATEGORIES: dict[str, str] = {
40
+ # Identity / discipline
41
+ "role": "identity",
42
+ "minimalist-discipline": "identity",
43
+ "coordination-discipline": "identity",
44
+ "systems-thinking": "identity",
45
+ # Guardrails
46
+ "critical": "guardrail",
47
+ "merge-gate": "guardrail",
48
+ "gate": "guardrail",
49
+ "self-review": "guardrail",
50
+ # Routing
51
+ "on-activation": "routing",
52
+ "phase-check": "routing",
53
+ # Procedures
54
+ "finish-flow": "procedure",
55
+ "session-new-flow": "procedure",
56
+ "empty-backlog-flow": "procedure",
57
+ "exit": "procedure",
58
+ "workflow": "procedure",
59
+ # Reference
60
+ "helpers": "reference",
61
+ "parameters": "reference",
62
+ "workflow-routing": "reference",
63
+ "skills": "reference",
64
+ "delegation": "reference",
65
+ "assessment-template": "reference",
66
+ "tandem-consultation": "reference",
67
+ "handoffs": "reference",
68
+ "workflows": "reference",
69
+ "coordination": "reference",
70
+ "workflow-participation": "reference",
71
+ # Persona
72
+ "persona": "persona",
73
+ "user-title": "persona",
74
+ "crew": "persona",
75
+ # Shared (behavior guide)
76
+ "tandem-protocol": "shared",
77
+ "agent-exit-protocol": "shared",
78
+ "wrong-phase-detection": "shared",
79
+ "info": "shared",
80
+ }
81
+
82
+ # Top-level header β†’ default category (for content directly under the header)
83
+ HEADER_CATEGORIES: dict[str, str] = {
84
+ "Workflow State": "routing",
85
+ "Sprint Context": "shared",
86
+ "Repos Topology": "shared",
87
+ }
88
+
89
+ # Primary agents (ordered by typical workflow position)
90
+ PRIMARY_AGENTS = [
91
+ "sm", "tea", "dev", "reviewer", "architect",
92
+ "pm", "tech-writer", "ux-designer", "devops",
93
+ "orchestrator", "ba",
94
+ ]
95
+
96
+ SUBAGENTS = [
97
+ "sm-setup", "sm-finish", "sm-file-summary",
98
+ "reviewer-preflight", "testing-runner", "tandem-backseat",
99
+ ]
100
+
101
+
102
+ # ── Data Model ──────────────────────────────────────────────────────
103
+
104
+ @dataclass
105
+ class Section:
106
+ """A parsed section from prime output."""
107
+
108
+ name: str
109
+ start_line: int
110
+ end_line: int
111
+ chars: int
112
+ tokens: int
113
+ category: str
114
+ component: str # which prime component this belongs to
115
+
116
+
117
+ @dataclass
118
+ class AgentHeatmap:
119
+ """Complete heatmap data for one agent."""
120
+
121
+ agent: str
122
+ sections: list[Section] = field(default_factory=list)
123
+ total_tokens: int = 0
124
+ total_chars: int = 0
125
+
126
+
127
+ # ── Attention Model ─────────────────────────────────────────────────
128
+
129
+ def attention_score(position_pct: float) -> float:
130
+ """U-shaped attention model based on "Lost in the Middle" (Liu et al. 2023).
131
+
132
+ Returns a score 0.0-1.0 where higher = more likely to be attended to.
133
+ Peaks at start (primacy) and end (recency), valley around 55-65%.
134
+ """
135
+ # Primacy: strong at start, decays linearly
136
+ primacy = max(0.0, 1.0 - position_pct * 1.8)
137
+ # Recency: rises in final third
138
+ recency = max(0.0, (position_pct - 0.5) * 2.0) ** 1.5
139
+ return min(1.0, max(0.15, primacy + recency * 0.7))
140
+
141
+
142
+ def heat_blocks(score: float) -> str:
143
+ """Score β†’ colored heat blocks."""
144
+ if score >= 0.85:
145
+ return "πŸŸ₯πŸŸ₯πŸŸ₯"
146
+ if score >= 0.65:
147
+ return "🟧🟧🟧"
148
+ if score >= 0.45:
149
+ return "🟨🟨🟨"
150
+ if score >= 0.30:
151
+ return "🟩🟩🟩"
152
+ return "🟦🟦🟦"
153
+
154
+
155
+ def size_bar(tokens: int, max_tokens: int, width: int = 15) -> str:
156
+ """Token count β†’ fixed-width bar."""
157
+ if max_tokens == 0:
158
+ return "β–‘" * width
159
+ filled = int((tokens / max_tokens) * width)
160
+ return "β–ˆ" * filled + "β–‘" * (width - filled)
161
+
162
+
163
+ # ── Parser ──────────────────────────────────────────────────────────
164
+
165
+ def _estimate_tokens(text: str) -> int:
166
+ """Estimate tokens using ~4 characters per token."""
167
+ if not text:
168
+ return 0
169
+ return max(1, len(text) // 4)
170
+
171
+
172
+ def parse_sections(raw_output: str) -> list[Section]:
173
+ """Parse raw prime output into categorized sections.
174
+
175
+ Detection strategy:
176
+ 1. Only KNOWN H1 headers create component boundaries
177
+ 2. XML opening tags define sections within components
178
+ 3. Unknown H1 headers fold into the current section
179
+ 4. Category is determined by tag name β†’ TAG_CATEGORIES mapping
180
+ """
181
+ lines = raw_output.split("\n")
182
+ sections: list[Section] = []
183
+
184
+ # Known component-boundary H1 prefixes
185
+ COMPONENT_HEADERS = [
186
+ "Workflow State",
187
+ "Agent Definition",
188
+ "Persona:",
189
+ "Agent Behavior Guide",
190
+ "Sprint Context",
191
+ "Repos Topology",
192
+ "Agent Sidecar:",
193
+ "Active Session:",
194
+ ]
195
+
196
+ def _is_component_header(text: str) -> bool:
197
+ return any(text.startswith(prefix) for prefix in COMPONENT_HEADERS)
198
+
199
+ # State
200
+ current_component = "preamble"
201
+ current_section_name = "preamble"
202
+ current_section_start = 1
203
+ current_section_category = "routing"
204
+ current_section_lines: list[str] = []
205
+ in_agent_def = False
206
+ in_behavior_guide = False
207
+ in_sidecar = False
208
+
209
+ def _flush_section(end_line: int) -> None:
210
+ """Emit the current section."""
211
+ text = "\n".join(current_section_lines)
212
+ chars = len(text)
213
+ tokens = _estimate_tokens(text)
214
+ if tokens > 0 and current_section_name != "preamble":
215
+ sections.append(Section(
216
+ name=current_section_name,
217
+ start_line=current_section_start,
218
+ end_line=end_line,
219
+ chars=chars,
220
+ tokens=tokens,
221
+ category=current_section_category,
222
+ component=current_component,
223
+ ))
224
+
225
+ tag_open_re = re.compile(r"^<([a-z][-a-z0-9]*)(?:\s[^>]*)?>$")
226
+
227
+ for i, line in enumerate(lines, start=1):
228
+ stripped = line.strip()
229
+
230
+ # ── H1 header detection (only known component boundaries) ──
231
+ if stripped.startswith("# "):
232
+ header_text = stripped[2:].strip()
233
+
234
+ if not _is_component_header(header_text):
235
+ # Not a component boundary β€” fold into current section
236
+ current_section_lines.append(line)
237
+ continue
238
+
239
+ _flush_section(i - 1)
240
+
241
+ # Determine component and default category
242
+ if header_text.startswith("Workflow State"):
243
+ current_component = "workflow_state"
244
+ current_section_category = "routing"
245
+ current_section_name = "Workflow State"
246
+ in_agent_def = False
247
+ in_behavior_guide = False
248
+ in_sidecar = False
249
+ elif header_text.startswith("Agent Definition"):
250
+ current_component = "agent_definition"
251
+ current_section_category = "identity"
252
+ current_section_name = "Agent Definition"
253
+ in_agent_def = True
254
+ in_behavior_guide = False
255
+ in_sidecar = False
256
+ elif header_text.startswith("Persona:"):
257
+ current_component = "persona"
258
+ current_section_category = "persona"
259
+ current_section_name = header_text
260
+ in_agent_def = False
261
+ in_behavior_guide = False
262
+ in_sidecar = False
263
+ elif header_text.startswith("Agent Behavior Guide"):
264
+ current_component = "behavior_guide"
265
+ current_section_category = "shared"
266
+ current_section_name = "BG: Preamble"
267
+ in_agent_def = False
268
+ in_behavior_guide = True
269
+ in_sidecar = False
270
+ elif header_text.startswith("Sprint Context"):
271
+ current_component = "sprint_context"
272
+ current_section_category = "shared"
273
+ current_section_name = "Sprint Context"
274
+ in_agent_def = False
275
+ in_behavior_guide = False
276
+ in_sidecar = False
277
+ elif header_text.startswith("Repos Topology"):
278
+ current_component = "repos_topology"
279
+ current_section_category = "shared"
280
+ current_section_name = "Repos Topology"
281
+ in_agent_def = False
282
+ in_behavior_guide = False
283
+ in_sidecar = False
284
+ elif header_text.startswith("Agent Sidecar:"):
285
+ current_component = "sidecars"
286
+ current_section_category = "learned"
287
+ sidecar_file = header_text.split(":", 1)[1].strip()
288
+ current_section_name = f"Sidecar: {sidecar_file.replace('.md', '').title()}"
289
+ in_agent_def = False
290
+ in_behavior_guide = False
291
+ in_sidecar = True
292
+ elif header_text.startswith("Active Session:"):
293
+ current_component = "session"
294
+ current_section_category = "routing"
295
+ current_section_name = "Active Session"
296
+ in_agent_def = False
297
+ in_behavior_guide = False
298
+ in_sidecar = False
299
+
300
+ current_section_start = i
301
+ current_section_lines = [line]
302
+ continue
303
+
304
+ # ── XML tag detection (within agent def, behavior guide, or persona) ──
305
+ if in_agent_def or in_behavior_guide or current_component == "persona":
306
+ m = tag_open_re.match(stripped)
307
+ if m:
308
+ tag_name = m.group(1)
309
+ _flush_section(i - 1)
310
+
311
+ # Look up category
312
+ if tag_name in TAG_CATEGORIES:
313
+ current_section_category = TAG_CATEGORIES[tag_name]
314
+ elif in_behavior_guide:
315
+ current_section_category = "shared"
316
+ elif in_agent_def:
317
+ current_section_category = "reference"
318
+
319
+ # Build section name
320
+ if in_behavior_guide:
321
+ current_section_name = f"BG: {_pretty_tag(tag_name)}"
322
+ elif current_component == "persona":
323
+ current_section_name = f"Persona: {_pretty_tag(tag_name)}"
324
+ else:
325
+ current_section_name = _pretty_tag(tag_name)
326
+
327
+ current_section_start = i
328
+ current_section_lines = [line]
329
+ continue
330
+
331
+ # ── Default: accumulate into current section ────────────
332
+ current_section_lines.append(line)
333
+
334
+ # Flush final section
335
+ _flush_section(len(lines))
336
+
337
+ return sections
338
+
339
+
340
+ def _pretty_tag(tag: str) -> str:
341
+ """Convert XML tag name to display name."""
342
+ return tag.replace("-", " ").title()
343
+
344
+
345
+ # ── Runner ──────────────────────────────────────────────────────────
346
+
347
+ def capture_agent_output(agent_name: str) -> str:
348
+ """Run pf agent start and capture raw output."""
349
+ result = subprocess.run(
350
+ ["pf", "agent", "start", agent_name],
351
+ capture_output=True,
352
+ text=True,
353
+ timeout=30,
354
+ )
355
+ return result.stdout
356
+
357
+
358
+ def build_heatmap(agent_name: str) -> AgentHeatmap:
359
+ """Build complete heatmap for one agent."""
360
+ raw = capture_agent_output(agent_name)
361
+ sections = parse_sections(raw)
362
+ total_tokens = sum(s.tokens for s in sections)
363
+ total_chars = sum(s.chars for s in sections)
364
+ return AgentHeatmap(
365
+ agent=agent_name,
366
+ sections=sections,
367
+ total_tokens=total_tokens,
368
+ total_chars=total_chars,
369
+ )
370
+
371
+
372
+ # ── Renderers ───────────────────────────────────────────────────────
373
+
374
+ def render_detail(hm: AgentHeatmap) -> str:
375
+ """Render detailed section-by-section heat map for one agent."""
376
+ out: list[str] = []
377
+ w = 100
378
+
379
+ out.append("=" * w)
380
+ out.append(f" {hm.agent.upper()} AGENT HEAT MAP β€” Section-by-Section with Attention Model")
381
+ out.append(f" Total: ~{hm.total_tokens:,} tokens across {hm.sections[-1].end_line if hm.sections else 0} lines")
382
+ out.append("=" * w)
383
+ out.append("")
384
+ out.append(' Attention model: U-shaped ("Lost in the Middle" β€” Liu et al. 2023)')
385
+ out.append(" Start of context = HIGH attention | Middle = LOW | End = MODERATE")
386
+ out.append("")
387
+
388
+ max_tok = max((s.tokens for s in hm.sections), default=1)
389
+
390
+ header = f" {'Pos':>4} {'Section':<34} {'Cat':>2} {'Tokens':>5} {'%Tot':>5} {'Size':<15} {'Attn':>5} {'Heat'}"
391
+ out.append(header)
392
+ out.append(" " + "─" * (w - 2))
393
+
394
+ running = 0
395
+ for s in hm.sections:
396
+ mid = running + s.tokens / 2
397
+ pct_pos = mid / hm.total_tokens if hm.total_tokens else 0
398
+ attn = attention_score(pct_pos)
399
+ pct_tot = s.tokens / hm.total_tokens * 100 if hm.total_tokens else 0
400
+ icon = CATEGORY_ICONS.get(s.category, " ")
401
+ bar = size_bar(s.tokens, max_tok)
402
+ heat = heat_blocks(attn)
403
+
404
+ out.append(
405
+ f" {running:>4} {s.name:<34} {icon} {s.tokens:>5} {pct_tot:>4.1f}% {bar} {attn:>5.2f} {heat}"
406
+ )
407
+ running += s.tokens
408
+
409
+ out.append(" " + "─" * (w - 2))
410
+ out.append("")
411
+
412
+ # ── Category summary ────────────────────────────────────────
413
+ cats: dict[str, dict] = {}
414
+ running = 0
415
+ for s in hm.sections:
416
+ cat = s.category
417
+ if cat not in cats:
418
+ cats[cat] = {"tokens": 0, "sections": 0, "attn_sum": 0.0}
419
+ cats[cat]["tokens"] += s.tokens
420
+ cats[cat]["sections"] += 1
421
+ mid = running + s.tokens / 2
422
+ pct_pos = mid / hm.total_tokens if hm.total_tokens else 0
423
+ cats[cat]["attn_sum"] += attention_score(pct_pos)
424
+ running += s.tokens
425
+
426
+ out.append(" CATEGORY SUMMARY:")
427
+ out.append(" " + "─" * 75)
428
+ out.append(f" {'Category':<14} {'Icon':>4} {'Tokens':>6} {'% Total':>7} {'Sections':>8} {'Avg Attn':>8} {'Verdict'}")
429
+
430
+ cat_order = ["routing", "identity", "guardrail", "procedure", "reference", "persona", "shared", "learned"]
431
+ for cat in cat_order:
432
+ if cat not in cats:
433
+ continue
434
+ d = cats[cat]
435
+ avg_attn = d["attn_sum"] / d["sections"] if d["sections"] else 0
436
+ pct = d["tokens"] / hm.total_tokens * 100 if hm.total_tokens else 0
437
+ icon = CATEGORY_ICONS.get(cat, " ")
438
+ if avg_attn >= 0.6:
439
+ verdict = "WELL-PLACED βœ“"
440
+ elif avg_attn >= 0.35:
441
+ verdict = "attention dip ⚠️"
442
+ else:
443
+ verdict = "LOST IN MIDDLE ❌"
444
+ out.append(f" {cat:<14} {icon:>4} {d['tokens']:>6} {pct:>6.1f}% {d['sections']:>8} {avg_attn:>8.2f} {verdict}")
445
+
446
+ out.append("")
447
+
448
+ # ── Duplication detection ───────────────────────────────────
449
+ agent_def_sections = {s.name.lower() for s in hm.sections if s.component == "agent_definition"}
450
+ bg_sections = {s.name.lower() for s in hm.sections if s.component == "behavior_guide"}
451
+
452
+ dups = []
453
+ # Check for exit protocol duplication
454
+ if any("exit" in n for n in agent_def_sections) and any("exit" in n for n in bg_sections):
455
+ dups.append(("Exit protocol (agent def)", "BG: Agent Exit Protocol"))
456
+ if any("phase" in n for n in agent_def_sections) and any("phase" in n or "wrong" in n for n in bg_sections):
457
+ dups.append(("Phase Check (agent def)", "BG: Wrong Phase Detection"))
458
+
459
+ if dups:
460
+ out.append(" DUPLICATION DETECTED:")
461
+ out.append(" β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”")
462
+ out.append(" β”‚ Agent Def (high attn) β”‚ Behavior Guide (low attn) β”‚")
463
+ out.append(" β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€")
464
+ for ad, bg in dups:
465
+ out.append(f" β”‚ {ad:<30} β”‚ {bg:<34} β”‚")
466
+ out.append(" β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜")
467
+ out.append("")
468
+
469
+ return "\n".join(out)
470
+
471
+
472
+ def render_summary(heatmaps: list[AgentHeatmap]) -> str:
473
+ """Render summary heat map across all agents."""
474
+ out: list[str] = []
475
+ w = 100
476
+
477
+ out.append("=" * w)
478
+ out.append(" AGENT ACTIVATION HEAT MAP β€” All Agents Summary")
479
+ out.append("=" * w)
480
+ out.append("")
481
+
482
+ # Gather per-agent category totals
483
+ components = ["routing", "identity", "guardrail", "procedure", "reference", "persona", "shared", "learned"]
484
+ comp_labels = ["Route", "Ident", "Guard", "Proced", "Refer", "Perso", "Shared", "Learn"]
485
+
486
+ # Find max per category across all agents
487
+ cat_maxes: dict[str, int] = {c: 0 for c in components}
488
+ agent_cats: dict[str, dict[str, int]] = {}
489
+ for hm in heatmaps:
490
+ agent_cats[hm.agent] = {c: 0 for c in components}
491
+ for s in hm.sections:
492
+ agent_cats[hm.agent][s.category] = agent_cats[hm.agent].get(s.category, 0) + s.tokens
493
+ for c in components:
494
+ cat_maxes[c] = max(cat_maxes[c], agent_cats[hm.agent].get(c, 0))
495
+
496
+ # Header
497
+ header = f" {'Agent':<14}"
498
+ for label in comp_labels:
499
+ header += f" {label:>7}"
500
+ header += f" {'TOTAL':>6} {'Unique%':>7} {'Bar'}"
501
+ out.append(header)
502
+ out.append(" " + "─" * (w - 2))
503
+
504
+ max_total = max((hm.total_tokens for hm in heatmaps), default=1)
505
+ shared_cats = {"shared"}
506
+
507
+ for hm in sorted(heatmaps, key=lambda h: h.total_tokens, reverse=True):
508
+ row = f" {hm.agent:<14}"
509
+ unique = 0
510
+ for c in components:
511
+ val = agent_cats[hm.agent].get(c, 0)
512
+ mx = cat_maxes[c]
513
+ if val == 0:
514
+ row += " Β· "
515
+ else:
516
+ # Heat relative to max in that category
517
+ ratio = val / mx if mx else 0
518
+ if ratio > 0.8:
519
+ h = "πŸŸ₯"
520
+ elif ratio > 0.6:
521
+ h = "🟧"
522
+ elif ratio > 0.4:
523
+ h = "🟨"
524
+ elif ratio > 0.2:
525
+ h = "🟩"
526
+ else:
527
+ h = "🟦"
528
+ row += f" {h}{val:>5}"
529
+ if c not in shared_cats:
530
+ unique += val
531
+ pct = unique / hm.total_tokens * 100 if hm.total_tokens else 0
532
+ bar = size_bar(hm.total_tokens, max_total, width=20)
533
+ row += f" {hm.total_tokens:>6} {pct:>5.1f}% {bar}"
534
+ out.append(row)
535
+
536
+ out.append("")
537
+
538
+ # Efficiency ranking
539
+ out.append(" ATTENTION EFFICIENCY (unique content / total):")
540
+ for hm in sorted(heatmaps, key=lambda h: h.total_tokens, reverse=True):
541
+ unique = sum(v for c, v in agent_cats[hm.agent].items() if c not in shared_cats)
542
+ total = hm.total_tokens
543
+ pct = unique / total * 100 if total else 0
544
+ if pct >= 65:
545
+ emoji = "🟒"
546
+ elif pct >= 55:
547
+ emoji = "🟑"
548
+ else:
549
+ emoji = "πŸ”΄"
550
+ ad = agent_cats[hm.agent].get("identity", 0) + agent_cats[hm.agent].get("guardrail", 0) + agent_cats[hm.agent].get("procedure", 0) + agent_cats[hm.agent].get("reference", 0) + agent_cats[hm.agent].get("routing", 0)
551
+ sc = agent_cats[hm.agent].get("learned", 0)
552
+ out.append(f" {emoji} {hm.agent:<14} {unique:>4}/{total:>4} = {pct:>5.1f}% (agent_defβ‰ˆ{ad}, sidecars={sc})")
553
+
554
+ out.append("")
555
+ out.append(" KEY: 🟒 >65% unique 🟑 55-65% πŸ”΄ <55% (diluted by boilerplate)")
556
+ out.append("")
557
+
558
+ # Shared boilerplate analysis
559
+ shared_tokens = [agent_cats[hm.agent].get("shared", 0) for hm in heatmaps]
560
+ if shared_tokens:
561
+ avg_shared = sum(shared_tokens) // len(shared_tokens)
562
+ min_total = min(hm.total_tokens for hm in heatmaps)
563
+ out.append(f" BOILERPLATE: ~{avg_shared} shared tokens per agent = {avg_shared/min_total*100:.0f}% of smallest ({min_total} tok)")
564
+
565
+ return "\n".join(out)
566
+
567
+
568
+ def render_csv(heatmaps: list[AgentHeatmap]) -> str:
569
+ """Render CSV output for machine consumption."""
570
+ rows = ["agent,section,component,category,tokens,start_line,end_line"]
571
+ for hm in heatmaps:
572
+ for s in hm.sections:
573
+ rows.append(f"{hm.agent},{s.name},{s.component},{s.category},{s.tokens},{s.start_line},{s.end_line}")
574
+ return "\n".join(rows)
575
+
576
+
577
+ # ── CLI Entry Point ─────────────────────────────────────────────────
578
+
579
+ def run_heatmap(
580
+ agent_name: str | None = None,
581
+ show_all: bool = False,
582
+ csv_output: bool = False,
583
+ json_output: bool = False,
584
+ ) -> int:
585
+ """Main entry point for heatmap command."""
586
+ try:
587
+ if show_all:
588
+ agents = PRIMARY_AGENTS
589
+ heatmaps = []
590
+ for a in agents:
591
+ sys.stderr.write(f" Scanning {a}...\n")
592
+ heatmaps.append(build_heatmap(a))
593
+ sys.stderr.write("\n")
594
+
595
+ if csv_output:
596
+ print(render_csv(heatmaps))
597
+ elif json_output:
598
+ data = []
599
+ for hm in heatmaps:
600
+ data.append({
601
+ "agent": hm.agent,
602
+ "total_tokens": hm.total_tokens,
603
+ "sections": [
604
+ {
605
+ "name": s.name,
606
+ "component": s.component,
607
+ "category": s.category,
608
+ "tokens": s.tokens,
609
+ "start_line": s.start_line,
610
+ "end_line": s.end_line,
611
+ }
612
+ for s in hm.sections
613
+ ],
614
+ })
615
+ print(json.dumps(data, indent=2))
616
+ else:
617
+ print(render_summary(heatmaps))
618
+ elif agent_name:
619
+ hm = build_heatmap(agent_name)
620
+ if json_output:
621
+ data = {
622
+ "agent": hm.agent,
623
+ "total_tokens": hm.total_tokens,
624
+ "sections": [
625
+ {
626
+ "name": s.name,
627
+ "component": s.component,
628
+ "category": s.category,
629
+ "tokens": s.tokens,
630
+ "start_line": s.start_line,
631
+ "end_line": s.end_line,
632
+ "attention": round(
633
+ attention_score(
634
+ (sum(ss.tokens for ss in hm.sections[:j]) + s.tokens / 2) / hm.total_tokens
635
+ ),
636
+ 3,
637
+ ),
638
+ }
639
+ for j, s in enumerate(hm.sections)
640
+ ],
641
+ }
642
+ print(json.dumps(data, indent=2))
643
+ else:
644
+ print(render_detail(hm))
645
+ else:
646
+ sys.stderr.write("Usage: pf agent heatmap <AGENT> | pf agent heatmap --all\n")
647
+ return 1
648
+ except subprocess.TimeoutExpired:
649
+ sys.stderr.write("Error: agent start timed out\n")
650
+ return 1
651
+ except Exception as e:
652
+ sys.stderr.write(f"Error: {e}\n")
653
+ return 1
654
+
655
+ return 0
@@ -123,7 +123,7 @@ def _ensure_wheelhub(project_dir: Path) -> int | None:
123
123
  )
124
124
 
125
125
  # Skip if full Cyclist is running
126
- cyclist_port_file = project_dir / ".cyclist-port"
126
+ cyclist_port_file = project_dir / ".wheelhub-port"
127
127
  if cyclist_port_file.exists():
128
128
  try:
129
129
  return int(cyclist_port_file.read_text().strip())