@pennyfarthing/core 11.1.1 → 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 +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/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/check-context.sh +1 -1
- 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/worktree-manager.sh +5 -496
- 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/otel-auto-config.sh +9 -11
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
- 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-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/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.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/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/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/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
|
@@ -5,21 +5,30 @@ Story 103-4: Connection status indicator in TUI header.
|
|
|
5
5
|
Story 103-6: SprintPanel as default panel on launch.
|
|
6
6
|
Story 103-7: /bc TUI panel focus — subscribe to /ws/focus, switch panels.
|
|
7
7
|
Story 103-9: Panel header chrome — icon + name indicator for active panel.
|
|
8
|
+
Panel navigation: Mount all panels, tab bar, keyboard switching, command palette.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
13
|
+
from functools import partial
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from typing import Any
|
|
14
16
|
|
|
15
17
|
from textual.app import App, ComposeResult
|
|
16
18
|
from textual.binding import Binding
|
|
19
|
+
from textual.command import Hit, Hits, Provider
|
|
17
20
|
from textual.containers import VerticalScroll
|
|
18
21
|
from textual.reactive import reactive
|
|
19
22
|
from textual.widgets import Footer, Header, Static
|
|
20
23
|
|
|
21
24
|
from pennyfarthing_scripts.bc.focus import get_last_panel, save_last_panel
|
|
25
|
+
from pennyfarthing_scripts.bikerack.background_panel import BackgroundPanel
|
|
22
26
|
from pennyfarthing_scripts.bikerack.base_panel import get_panel_icon
|
|
27
|
+
from pennyfarthing_scripts.bikerack.changed_panel import ChangedPanel
|
|
28
|
+
from pennyfarthing_scripts.bikerack.debug_panel import DebugPanel
|
|
29
|
+
from pennyfarthing_scripts.bikerack.diffs_panel import DiffsPanel
|
|
30
|
+
from pennyfarthing_scripts.bikerack.git_panel import GitPanel
|
|
31
|
+
from pennyfarthing_scripts.bikerack.progress_panel import ProgressPanel
|
|
23
32
|
from pennyfarthing_scripts.bikerack.sprint_panel import SprintPanel
|
|
24
33
|
from pennyfarthing_scripts.bikerack.ws_client import ConnectionState, WheelHubClient
|
|
25
34
|
|
|
@@ -30,7 +39,48 @@ STATE_DISPLAY: dict[ConnectionState, str] = {
|
|
|
30
39
|
ConnectionState.CONNECTING: "[yellow]● Connecting…[/yellow]",
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
#
|
|
42
|
+
# Agent role colors for Rich markup (mapped from React AGENT_COLORS)
|
|
43
|
+
AGENT_ROLE_COLORS: dict[str, str] = {
|
|
44
|
+
"pm": "purple",
|
|
45
|
+
"sm": "blue",
|
|
46
|
+
"dev": "green",
|
|
47
|
+
"tea": "cyan",
|
|
48
|
+
"reviewer": "red",
|
|
49
|
+
"architect": "dark_orange",
|
|
50
|
+
"devops": "bright_cyan",
|
|
51
|
+
"ux-designer": "magenta",
|
|
52
|
+
"tech-writer": "white",
|
|
53
|
+
"orchestrator": "bright_magenta",
|
|
54
|
+
"ba": "bright_green",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
AGENT_ABBREV: dict[str, str] = {
|
|
58
|
+
"pm": "PM",
|
|
59
|
+
"sm": "SM",
|
|
60
|
+
"dev": "DEV",
|
|
61
|
+
"tea": "TEA",
|
|
62
|
+
"reviewer": "REV",
|
|
63
|
+
"architect": "ARC",
|
|
64
|
+
"devops": "OPS",
|
|
65
|
+
"ux-designer": "UX",
|
|
66
|
+
"tech-writer": "TW",
|
|
67
|
+
"orchestrator": "ORC",
|
|
68
|
+
"ba": "BA",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Ordered panel registry: (key, display_name, widget_class)
|
|
72
|
+
# Only panels with implemented widget classes are included.
|
|
73
|
+
PANEL_REGISTRY: list[tuple[str, str]] = [
|
|
74
|
+
("sprint", "Sprint"),
|
|
75
|
+
("git", "Git"),
|
|
76
|
+
("diffs", "Diffs"),
|
|
77
|
+
("changed", "Changed"),
|
|
78
|
+
("background", "Background"),
|
|
79
|
+
("debug", "Debug"),
|
|
80
|
+
("progress", "Progress"),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Human-readable display names for panels (full set for external focus messages)
|
|
34
84
|
PANEL_DISPLAY_NAMES: dict[str, str] = {
|
|
35
85
|
"sprint": "Sprint",
|
|
36
86
|
"git": "Git",
|
|
@@ -42,24 +92,106 @@ PANEL_DISPLAY_NAMES: dict[str, str] = {
|
|
|
42
92
|
"changed": "Changed",
|
|
43
93
|
"ac": "Acceptance Criteria",
|
|
44
94
|
"debug": "Debug",
|
|
95
|
+
"progress": "Progress",
|
|
45
96
|
"settings": "Settings",
|
|
46
97
|
"tty": "TTY",
|
|
47
98
|
}
|
|
48
99
|
|
|
100
|
+
# Keys from PANEL_REGISTRY for fast lookup
|
|
101
|
+
_PANEL_KEYS = [key for key, _ in PANEL_REGISTRY]
|
|
49
102
|
|
|
50
|
-
class PanelIndicator(Static):
|
|
51
|
-
"""Displays the active panel's Nerd Font icon and name."""
|
|
52
103
|
|
|
53
|
-
|
|
104
|
+
class PanelTabBar(Static):
|
|
105
|
+
"""Horizontal tab bar showing all available panels with active highlight."""
|
|
54
106
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
107
|
+
active: reactive[str] = reactive("sprint")
|
|
108
|
+
|
|
109
|
+
def watch_active(self, key: str) -> None:
|
|
110
|
+
"""Re-render tab bar when active panel changes."""
|
|
111
|
+
parts: list[str] = []
|
|
112
|
+
for panel_key, display_name in PANEL_REGISTRY:
|
|
113
|
+
icon = get_panel_icon(panel_key)
|
|
114
|
+
idx = _PANEL_KEYS.index(panel_key) + 1
|
|
115
|
+
prefix = f"{idx}:"
|
|
116
|
+
if panel_key == key:
|
|
117
|
+
if icon:
|
|
118
|
+
parts.append(f"[bold reverse] {prefix}{icon} {display_name} [/]")
|
|
119
|
+
else:
|
|
120
|
+
parts.append(f"[bold reverse] {prefix}{display_name} [/]")
|
|
121
|
+
else:
|
|
122
|
+
if icon:
|
|
123
|
+
parts.append(f"[dim]{prefix}{icon} {display_name}[/]")
|
|
124
|
+
else:
|
|
125
|
+
parts.append(f"[dim]{prefix}{display_name}[/]")
|
|
126
|
+
self.update(" ".join(parts))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class AgentHeader(Static):
|
|
130
|
+
"""Displays current agent persona from WheelHub /ws/persona channel."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
133
|
+
super().__init__(**kwargs)
|
|
134
|
+
self._is_streaming: bool = False
|
|
135
|
+
self._persona_data: dict[str, Any] = {}
|
|
136
|
+
|
|
137
|
+
def _apply_persona(self, data: dict[str, Any]) -> None:
|
|
138
|
+
"""Render persona data into the header."""
|
|
139
|
+
if data.get("type") == "streaming":
|
|
140
|
+
self._is_streaming = bool(data.get("isStreaming", False))
|
|
141
|
+
self._render_header()
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
self._persona_data = data
|
|
145
|
+
self._is_streaming = bool(data.get("isStreaming", False))
|
|
146
|
+
self._render_header()
|
|
147
|
+
|
|
148
|
+
def _render_header(self) -> None:
|
|
149
|
+
"""Re-render the header from stored state."""
|
|
150
|
+
data = self._persona_data
|
|
151
|
+
char = data.get("character", "")
|
|
152
|
+
role = data.get("role", "")
|
|
153
|
+
role_desc = data.get("roleDescription", "")
|
|
154
|
+
quote = data.get("quote", "")
|
|
155
|
+
style = data.get("style", "")
|
|
156
|
+
theme = data.get("theme", "")
|
|
157
|
+
|
|
158
|
+
if not char:
|
|
159
|
+
self.update("[dim]Waiting for agent...[/dim]")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
parts: list[str] = []
|
|
163
|
+
|
|
164
|
+
# Role badge
|
|
165
|
+
if role:
|
|
166
|
+
abbrev = AGENT_ABBREV.get(role, role.upper()[:3])
|
|
167
|
+
color = AGENT_ROLE_COLORS.get(role, "bright_magenta")
|
|
168
|
+
parts.append(f"[bold {color}][{abbrev}][/bold {color}]")
|
|
169
|
+
|
|
170
|
+
# Character name
|
|
171
|
+
parts.append(f"[bold]{char}[/bold]")
|
|
172
|
+
|
|
173
|
+
# Theme name
|
|
174
|
+
if theme:
|
|
175
|
+
from pennyfarthing_scripts.bikerack.base_panel import humanize_theme
|
|
176
|
+
parts.append(f"[dim]{humanize_theme(theme)}[/dim]")
|
|
177
|
+
|
|
178
|
+
# Streaming indicator
|
|
179
|
+
if self._is_streaming:
|
|
180
|
+
parts.append("[bold yellow]⚡[/bold yellow]")
|
|
181
|
+
|
|
182
|
+
line = " ".join(parts)
|
|
183
|
+
|
|
184
|
+
# Role description / style subtitle
|
|
185
|
+
if role_desc:
|
|
186
|
+
line += f"\n[dim]{role_desc}[/dim]"
|
|
187
|
+
elif style:
|
|
188
|
+
line += f"\n[dim]{style}[/dim]"
|
|
189
|
+
|
|
190
|
+
# Quote
|
|
191
|
+
if quote:
|
|
192
|
+
line += f"\n[italic dim]\"{quote}\"[/italic dim]"
|
|
193
|
+
|
|
194
|
+
self.update(line)
|
|
63
195
|
|
|
64
196
|
|
|
65
197
|
class ConnectionStatus(Static):
|
|
@@ -74,50 +206,212 @@ class ConnectionStatus(Static):
|
|
|
74
206
|
self.update(STATE_DISPLAY.get(state, "● Unknown"))
|
|
75
207
|
|
|
76
208
|
|
|
209
|
+
class PanelCommands(Provider):
|
|
210
|
+
"""Command palette provider for panel switching."""
|
|
211
|
+
|
|
212
|
+
async def search(self, query: str) -> Hits:
|
|
213
|
+
matcher = self.matcher(query)
|
|
214
|
+
for panel_key, display_name in PANEL_REGISTRY:
|
|
215
|
+
icon = get_panel_icon(panel_key)
|
|
216
|
+
label = f"{icon} {display_name}" if icon else display_name
|
|
217
|
+
score = matcher.match(display_name)
|
|
218
|
+
if score > 0:
|
|
219
|
+
yield Hit(
|
|
220
|
+
score,
|
|
221
|
+
matcher.highlight(label),
|
|
222
|
+
partial(self.app.action_switch_panel, panel_key),
|
|
223
|
+
help=f"Switch to {display_name} panel",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
77
227
|
class BikeRackApp(App):
|
|
78
228
|
"""BikeRack TUI application shell."""
|
|
79
229
|
|
|
80
230
|
TITLE = "BikeRack"
|
|
81
231
|
|
|
232
|
+
CSS = """
|
|
233
|
+
#agent-header {
|
|
234
|
+
height: auto;
|
|
235
|
+
max-height: 3;
|
|
236
|
+
padding: 0 1;
|
|
237
|
+
}
|
|
238
|
+
#tab-bar {
|
|
239
|
+
height: 1;
|
|
240
|
+
}
|
|
241
|
+
#connection-status {
|
|
242
|
+
height: 1;
|
|
243
|
+
}
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
COMMANDS = App.COMMANDS | {PanelCommands}
|
|
247
|
+
|
|
82
248
|
BINDINGS = [
|
|
83
249
|
Binding("q", "quit", "Quit"),
|
|
250
|
+
Binding("1", "switch_panel('sprint')", "Sprint", show=False),
|
|
251
|
+
Binding("2", "switch_panel('git')", "Git", show=False),
|
|
252
|
+
Binding("3", "switch_panel('diffs')", "Diffs", show=False),
|
|
253
|
+
Binding("4", "switch_panel('changed')", "Changed", show=False),
|
|
254
|
+
Binding("5", "switch_panel('background')", "Background", show=False),
|
|
255
|
+
Binding("6", "switch_panel('debug')", "Debug", show=False),
|
|
256
|
+
Binding("7", "switch_panel('progress')", "Progress", show=False),
|
|
257
|
+
Binding("bracketright", "next_panel", "]Next"),
|
|
258
|
+
Binding("bracketleft", "prev_panel", "[Prev"),
|
|
259
|
+
Binding("tab", "next_panel", show=False),
|
|
260
|
+
Binding("shift+tab", "prev_panel", show=False),
|
|
261
|
+
Binding("n", "next_diff_file", "Next file", show=False),
|
|
262
|
+
Binding("p", "prev_diff_file", "Prev file", show=False),
|
|
263
|
+
Binding("j", "next_epic", show=False),
|
|
264
|
+
Binding("k", "prev_epic", show=False),
|
|
265
|
+
Binding("e", "toggle_epic", show=False),
|
|
84
266
|
]
|
|
85
267
|
|
|
86
268
|
def __init__(self, client=None, **kwargs):
|
|
87
269
|
super().__init__(**kwargs)
|
|
88
270
|
self._client = client
|
|
89
|
-
self._focused_panel: str
|
|
271
|
+
self._focused_panel: str = "sprint"
|
|
90
272
|
self._previous_panel: str | None = None
|
|
91
273
|
|
|
92
274
|
def compose(self) -> ComposeResult:
|
|
93
275
|
yield Header()
|
|
94
|
-
yield
|
|
276
|
+
yield AgentHeader(id="agent-header")
|
|
277
|
+
yield PanelTabBar(id="tab-bar")
|
|
95
278
|
yield ConnectionStatus(
|
|
96
279
|
STATE_DISPLAY[ConnectionState.DISCONNECTED],
|
|
97
280
|
id="connection-status",
|
|
98
281
|
)
|
|
99
282
|
with VerticalScroll(id="main-content"):
|
|
100
|
-
yield SprintPanel(client=self._client, id="sprint
|
|
283
|
+
yield SprintPanel(client=self._client, id="panel-sprint")
|
|
284
|
+
yield GitPanel(client=self._client, id="panel-git")
|
|
285
|
+
yield DiffsPanel(client=self._client, id="panel-diffs")
|
|
286
|
+
yield ChangedPanel(client=self._client, id="panel-changed")
|
|
287
|
+
yield BackgroundPanel(client=self._client, id="panel-background")
|
|
288
|
+
yield DebugPanel(client=self._client, id="panel-debug")
|
|
289
|
+
yield ProgressPanel(client=self._client, id="panel-progress")
|
|
101
290
|
yield Footer()
|
|
102
291
|
|
|
103
292
|
async def on_mount(self) -> None:
|
|
293
|
+
# Restore last panel or default to sprint
|
|
104
294
|
result = get_last_panel()
|
|
295
|
+
initial = "sprint"
|
|
105
296
|
if result.get("success") and result.get("last_panel"):
|
|
106
|
-
|
|
297
|
+
last = result["last_panel"]
|
|
298
|
+
if last in _PANEL_KEYS:
|
|
299
|
+
initial = last
|
|
300
|
+
|
|
301
|
+
self._focused_panel = initial
|
|
107
302
|
|
|
108
|
-
#
|
|
109
|
-
|
|
303
|
+
# Hide all panels except the active one
|
|
304
|
+
for panel_key in _PANEL_KEYS:
|
|
305
|
+
widget_id = f"panel-{panel_key}"
|
|
306
|
+
try:
|
|
307
|
+
widget = self.query_one(f"#{widget_id}")
|
|
308
|
+
widget.display = (panel_key == initial)
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
# Set tab bar active state
|
|
313
|
+
self._update_tab_bar(initial)
|
|
110
314
|
|
|
111
315
|
if self._client is not None:
|
|
112
316
|
self._client.on_state_change(self._on_ws_state_change)
|
|
113
317
|
self._client.subscribe("focus", self._handle_focus_message)
|
|
318
|
+
self._client.subscribe("persona", self._handle_persona_message)
|
|
114
319
|
self.run_worker(self._client.connect(), exclusive=True, name="ws-client")
|
|
115
320
|
|
|
116
|
-
def
|
|
117
|
-
"""
|
|
321
|
+
def action_switch_panel(self, key: str) -> None:
|
|
322
|
+
"""Switch to a panel by key."""
|
|
323
|
+
if key not in _PANEL_KEYS:
|
|
324
|
+
return
|
|
325
|
+
if key == self._focused_panel:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
# Hide current panel
|
|
329
|
+
try:
|
|
330
|
+
current = self.query_one(f"#panel-{self._focused_panel}")
|
|
331
|
+
current.display = False
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
# Show target panel
|
|
336
|
+
try:
|
|
337
|
+
target = self.query_one(f"#panel-{key}")
|
|
338
|
+
target.display = True
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
self._previous_panel = self._focused_panel
|
|
343
|
+
self._focused_panel = key
|
|
344
|
+
save_last_panel(key, project_dir=None)
|
|
345
|
+
self._update_tab_bar(key)
|
|
346
|
+
|
|
347
|
+
def action_next_panel(self) -> None:
|
|
348
|
+
"""Cycle to the next panel."""
|
|
118
349
|
try:
|
|
119
|
-
|
|
120
|
-
|
|
350
|
+
idx = _PANEL_KEYS.index(self._focused_panel)
|
|
351
|
+
except ValueError:
|
|
352
|
+
idx = 0
|
|
353
|
+
next_idx = (idx + 1) % len(_PANEL_KEYS)
|
|
354
|
+
self.action_switch_panel(_PANEL_KEYS[next_idx])
|
|
355
|
+
|
|
356
|
+
def action_prev_panel(self) -> None:
|
|
357
|
+
"""Cycle to the previous panel."""
|
|
358
|
+
try:
|
|
359
|
+
idx = _PANEL_KEYS.index(self._focused_panel)
|
|
360
|
+
except ValueError:
|
|
361
|
+
idx = 0
|
|
362
|
+
prev_idx = (idx - 1) % len(_PANEL_KEYS)
|
|
363
|
+
self.action_switch_panel(_PANEL_KEYS[prev_idx])
|
|
364
|
+
|
|
365
|
+
def action_next_diff_file(self) -> None:
|
|
366
|
+
"""Advance to next file in diffs panel."""
|
|
367
|
+
if self._focused_panel == "diffs":
|
|
368
|
+
try:
|
|
369
|
+
panel = self.query_one("#panel-diffs", DiffsPanel)
|
|
370
|
+
panel.next_file()
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
def action_prev_diff_file(self) -> None:
|
|
375
|
+
"""Go to previous file in diffs panel."""
|
|
376
|
+
if self._focused_panel == "diffs":
|
|
377
|
+
try:
|
|
378
|
+
panel = self.query_one("#panel-diffs", DiffsPanel)
|
|
379
|
+
panel.prev_file()
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
def action_next_epic(self) -> None:
|
|
384
|
+
"""Move to next epic in sprint panel."""
|
|
385
|
+
if self._focused_panel == "sprint":
|
|
386
|
+
try:
|
|
387
|
+
panel = self.query_one("#panel-sprint", SprintPanel)
|
|
388
|
+
panel.next_epic()
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
def action_prev_epic(self) -> None:
|
|
393
|
+
"""Move to previous epic in sprint panel."""
|
|
394
|
+
if self._focused_panel == "sprint":
|
|
395
|
+
try:
|
|
396
|
+
panel = self.query_one("#panel-sprint", SprintPanel)
|
|
397
|
+
panel.prev_epic()
|
|
398
|
+
except Exception:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
def action_toggle_epic(self) -> None:
|
|
402
|
+
"""Toggle expand/collapse on selected epic in sprint panel."""
|
|
403
|
+
if self._focused_panel == "sprint":
|
|
404
|
+
try:
|
|
405
|
+
panel = self.query_one("#panel-sprint", SprintPanel)
|
|
406
|
+
panel.toggle_epic()
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
def _update_tab_bar(self, panel_key: str) -> None:
|
|
411
|
+
"""Update the tab bar widget with the given panel key."""
|
|
412
|
+
try:
|
|
413
|
+
tab_bar = self.query_one("#tab-bar", PanelTabBar)
|
|
414
|
+
tab_bar.active = panel_key
|
|
121
415
|
except Exception:
|
|
122
416
|
pass
|
|
123
417
|
|
|
@@ -135,14 +429,23 @@ class BikeRackApp(App):
|
|
|
135
429
|
return
|
|
136
430
|
|
|
137
431
|
focus = message["focus"]
|
|
138
|
-
if focus is not None:
|
|
432
|
+
if focus is not None and focus in _PANEL_KEYS:
|
|
433
|
+
self.action_switch_panel(focus)
|
|
434
|
+
elif focus is not None:
|
|
435
|
+
# Panel exists in display names but not implemented — just update state
|
|
139
436
|
self._previous_panel = self._focused_panel
|
|
140
437
|
self._focused_panel = focus
|
|
141
438
|
save_last_panel(focus, project_dir=None)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
439
|
+
|
|
440
|
+
def _handle_persona_message(self, message: dict[str, Any] | None) -> None:
|
|
441
|
+
"""Handle incoming persona channel messages."""
|
|
442
|
+
if message is None or not isinstance(message, dict):
|
|
443
|
+
return
|
|
444
|
+
try:
|
|
445
|
+
header = self.query_one("#agent-header", AgentHeader)
|
|
446
|
+
header._apply_persona(message)
|
|
447
|
+
except Exception:
|
|
448
|
+
pass
|
|
146
449
|
|
|
147
450
|
def _on_ws_state_change(self, state: ConnectionState) -> None:
|
|
148
451
|
"""Handle WheelHub connection state changes."""
|
|
@@ -156,16 +459,19 @@ class BikeRackApp(App):
|
|
|
156
459
|
DEFAULT_PORT = 2898
|
|
157
460
|
|
|
158
461
|
|
|
159
|
-
def main(
|
|
462
|
+
def main(
|
|
463
|
+
port: int | None = None,
|
|
464
|
+
project_dir: Path | None = None,
|
|
465
|
+
) -> None:
|
|
160
466
|
"""Launch BikeRack TUI as a standalone application.
|
|
161
467
|
|
|
162
468
|
Args:
|
|
163
|
-
port: Explicit WheelHub port. If None, reads from .
|
|
469
|
+
port: Explicit WheelHub port. If None, reads from .wheelhub-port file.
|
|
164
470
|
project_dir: Project directory for port file discovery. Defaults to cwd.
|
|
165
471
|
"""
|
|
166
472
|
if port is None:
|
|
167
473
|
if project_dir is not None:
|
|
168
|
-
port_file = project_dir / ".
|
|
474
|
+
port_file = project_dir / ".wheelhub-port"
|
|
169
475
|
if port_file.exists():
|
|
170
476
|
try:
|
|
171
477
|
port = int(port_file.read_text().strip())
|
|
@@ -75,14 +75,14 @@ class WheelHubClient:
|
|
|
75
75
|
cb(new_state)
|
|
76
76
|
|
|
77
77
|
def discover_port(self) -> int:
|
|
78
|
-
"""Read port from .
|
|
78
|
+
"""Read port from .wheelhub-port file, fallback to DEFAULT_PORT.
|
|
79
79
|
|
|
80
80
|
Priority: explicit port > port file > DEFAULT_PORT.
|
|
81
81
|
"""
|
|
82
82
|
if self._port is not None:
|
|
83
83
|
return self._port
|
|
84
84
|
if self._project_dir is not None:
|
|
85
|
-
port_file = self._project_dir / ".
|
|
85
|
+
port_file = self._project_dir / ".wheelhub-port"
|
|
86
86
|
if port_file.exists():
|
|
87
87
|
try:
|
|
88
88
|
return int(port_file.read_text().strip())
|
|
@@ -137,6 +137,11 @@ from pennyfarthing_scripts.epic.cli import epic # noqa: E402
|
|
|
137
137
|
|
|
138
138
|
cli.add_command(epic)
|
|
139
139
|
|
|
140
|
+
# Import and register consultation group
|
|
141
|
+
from pennyfarthing_scripts.consultation.cli import consultation # noqa: E402
|
|
142
|
+
|
|
143
|
+
cli.add_command(consultation)
|
|
144
|
+
|
|
140
145
|
|
|
141
146
|
@cli.group()
|
|
142
147
|
def agent():
|
|
@@ -144,7 +149,8 @@ def agent():
|
|
|
144
149
|
|
|
145
150
|
\b
|
|
146
151
|
Commands:
|
|
147
|
-
start
|
|
152
|
+
start - Start an agent session with context
|
|
153
|
+
heatmap - Visualize context distribution and attention
|
|
148
154
|
"""
|
|
149
155
|
pass
|
|
150
156
|
|
|
@@ -193,78 +199,44 @@ def agent_start(
|
|
|
193
199
|
raise SystemExit(exit_code)
|
|
194
200
|
|
|
195
201
|
|
|
196
|
-
@
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
@workflow.command("check")
|
|
210
|
-
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
211
|
-
def workflow_check(output_json: bool):
|
|
212
|
-
"""Check current workflow state.
|
|
213
|
-
|
|
214
|
-
Returns the current story ID, phase, and workflow state.
|
|
215
|
-
"""
|
|
216
|
-
# Lazy import - only load when command is actually invoked
|
|
217
|
-
from pennyfarthing_scripts.workflow import get_workflow_state
|
|
218
|
-
|
|
219
|
-
state = get_workflow_state()
|
|
220
|
-
|
|
221
|
-
if output_json:
|
|
222
|
-
import json
|
|
223
|
-
|
|
224
|
-
click.echo(json.dumps(state, indent=2))
|
|
225
|
-
else:
|
|
226
|
-
click.echo(f"State: {state.get('state', 'unknown')}")
|
|
227
|
-
if state.get("story_id"):
|
|
228
|
-
click.echo(f"Story: {state['story_id']}")
|
|
229
|
-
if state.get("workflow"):
|
|
230
|
-
click.echo(f"Workflow: {state['workflow']}")
|
|
231
|
-
if state.get("phase"):
|
|
232
|
-
click.echo(f"Phase: {state['phase']}")
|
|
233
|
-
|
|
202
|
+
@agent.command("heatmap")
|
|
203
|
+
@click.argument("name", required=False)
|
|
204
|
+
@click.option("--all", "show_all", is_flag=True, help="Show summary across all primary agents")
|
|
205
|
+
@click.option("--csv", "csv_output", is_flag=True, help="Output CSV for machine consumption")
|
|
206
|
+
@click.option("--json", "json_output", is_flag=True, help="Output JSON")
|
|
207
|
+
def agent_heatmap(
|
|
208
|
+
name: str | None,
|
|
209
|
+
show_all: bool,
|
|
210
|
+
csv_output: bool,
|
|
211
|
+
json_output: bool,
|
|
212
|
+
):
|
|
213
|
+
"""Visualize context distribution and attention for agent activation.
|
|
234
214
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def workflow_phase_check(workflow_name: str, phase: str):
|
|
239
|
-
"""Check which agent owns a workflow phase.
|
|
215
|
+
Shows a heat map of how tokens are distributed across sections of an
|
|
216
|
+
agent's activation context, with attention scores based on the
|
|
217
|
+
"Lost in the Middle" U-shaped attention model.
|
|
240
218
|
|
|
241
219
|
\b
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
220
|
+
Examples:
|
|
221
|
+
pf agent heatmap sm # Detailed view for SM
|
|
222
|
+
pf agent heatmap --all # Summary across all agents
|
|
223
|
+
pf agent heatmap dev --json # JSON output for tooling
|
|
245
224
|
"""
|
|
246
|
-
|
|
247
|
-
from pennyfarthing_scripts.workflow import get_phase_owner
|
|
225
|
+
from pennyfarthing_scripts.prime.heatmap import run_heatmap
|
|
248
226
|
|
|
249
|
-
|
|
250
|
-
|
|
227
|
+
exit_code = run_heatmap(
|
|
228
|
+
agent_name=name,
|
|
229
|
+
show_all=show_all,
|
|
230
|
+
csv_output=csv_output,
|
|
231
|
+
json_output=json_output,
|
|
232
|
+
)
|
|
233
|
+
raise SystemExit(exit_code)
|
|
251
234
|
|
|
252
235
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def workflow_handoff(next_agent: str):
|
|
256
|
-
"""Emit a handoff marker for Cyclist.
|
|
236
|
+
# Import and register workflow group
|
|
237
|
+
from pennyfarthing_scripts.workflow.cli import workflow # noqa: E402
|
|
257
238
|
|
|
258
|
-
|
|
259
|
-
Arguments:
|
|
260
|
-
NEXT_AGENT - The agent to hand off to (tea, dev, reviewer, etc.)
|
|
261
|
-
"""
|
|
262
|
-
# Output the marker format expected by Cyclist
|
|
263
|
-
click.echo("---")
|
|
264
|
-
click.echo("AGENT_COMMAND:")
|
|
265
|
-
click.echo(f' marker: "<!-- CYCLIST:HANDOFF:/{next_agent} -->"')
|
|
266
|
-
click.echo(f' fallback: "Run `/{next_agent}` to continue"')
|
|
267
|
-
click.echo("---")
|
|
239
|
+
cli.add_command(workflow)
|
|
268
240
|
|
|
269
241
|
|
|
270
242
|
@cli.command("help")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Consultation package — Dialogue file management for tandem agent consultation."""
|