@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.
- package/README.md +8 -8
- package/package.json +16 -14
- package/packages/core/dist/cli/utils/constants.d.ts +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +2 -1
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.js +334 -0
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
- package/packages/core/dist/server/api/git.d.ts +13 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +53 -34
- package/packages/core/dist/server/api/git.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +185 -24
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
- package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
- package/packages/core/dist/server/otlp-receiver.test.js +446 -0
- package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.js +27 -0
- package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
- package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
- package/packages/core/dist/shared/skill-search.test.js +2 -2
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
- package/pennyfarthing-dist/agents/dev.md +1 -1
- package/pennyfarthing-dist/agents/reviewer.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +1 -1
- package/pennyfarthing-dist/agents/sm.md +2 -2
- package/pennyfarthing-dist/agents/tea.md +1 -1
- package/pennyfarthing-dist/agents/testing-runner.md +2 -1
- package/pennyfarthing-dist/commands/pf-chore.md +2 -2
- package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
- package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +3 -3
- package/pennyfarthing-dist/guides/hooks.md +1 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
- package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
- package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
- package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- package/pennyfarthing-dist/scripts/git/README.md +24 -14
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
- package/pennyfarthing-dist/scripts/git/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
- package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
- package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
- package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
- package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
- package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
- package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
- package/pennyfarthing_scripts/CLAUDE.md +26 -4
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +3 -5
- package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/portrait.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
- package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
- package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
- package/pennyfarthing_scripts/bikerack/cli.py +10 -11
- package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
- package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
- package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
- package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
- package/pennyfarthing_scripts/bikerack/tui.py +336 -30
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/cli.py +37 -65
- package/pennyfarthing_scripts/consultation/__init__.py +1 -0
- package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/cli.py +149 -0
- package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
- package/pennyfarthing_scripts/context.py +3 -3
- package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__init__.py +12 -1
- 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__/hooks_installer.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/worktree.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +3 -4
- package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
- package/pennyfarthing_scripts/git/repos.py +196 -0
- package/pennyfarthing_scripts/git/status_all.py +27 -11
- package/pennyfarthing_scripts/git/worktree.py +302 -0
- package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/cli.py +143 -40
- package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/hooks.py +3 -17
- package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
- package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/heatmap.py +655 -0
- package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session_start_hook.py +1 -1
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/loader.py +15 -1
- package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
- package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
- package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
- package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
- package/pennyfarthing_scripts/validate/cli.py +17 -5
- package/pennyfarthing_scripts/workflow/__init__.py +40 -0
- package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/cli.py +1099 -0
- package/pennyfarthing_scripts/workflow/helpers.py +241 -0
- package/pennyfarthing_scripts/{workflow.py β workflow/scale.py} +0 -104
- package/pennyfarthing_scripts/workflow/state.py +112 -0
- package/scripts/README.md +41 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
- package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
- package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
- package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
- 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
|
|
Binary file
|
|
@@ -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 / ".
|
|
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())
|
|
Binary file
|