@pennyfarthing/core 11.2.0 → 11.2.2
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 +100 -40
- package/package.json +2 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +474 -66
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.js +4 -4
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +4 -5
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts +3 -8
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +3 -4
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/cli/utils/settings.d.ts +7 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +70 -29
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +16 -16
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.js +1 -1
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -1
- package/packages/core/dist/consultation/tandem-metrics.d.ts +91 -0
- package/packages/core/dist/consultation/tandem-metrics.d.ts.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.js +131 -0
- package/packages/core/dist/consultation/tandem-metrics.js.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.test.d.ts +18 -0
- package/packages/core/dist/consultation/tandem-metrics.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.test.js +457 -0
- package/packages/core/dist/consultation/tandem-metrics.test.js.map +1 -0
- package/packages/core/dist/public/css/react.css +1 -1
- package/packages/core/dist/public/js/react/react.js +14 -14
- package/packages/core/dist/server/api/agent-load.js +1 -1
- package/packages/core/dist/server/api/agent-load.js.map +1 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +0 -1
- package/packages/core/dist/server/api/git.js.map +1 -1
- package/packages/core/dist/server/api/index.d.ts +2 -0
- package/packages/core/dist/server/api/index.d.ts.map +1 -1
- package/packages/core/dist/server/api/index.js +2 -0
- package/packages/core/dist/server/api/index.js.map +1 -1
- package/packages/core/dist/server/api/project-info.d.ts +11 -0
- package/packages/core/dist/server/api/project-info.d.ts.map +1 -0
- package/packages/core/dist/server/api/project-info.js +18 -0
- package/packages/core/dist/server/api/project-info.js.map +1 -0
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +18 -1
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.test.js +1 -1
- package/packages/core/dist/server/otlp-receiver.test.js.map +1 -1
- package/packages/core/dist/server/server.d.ts +0 -3
- package/packages/core/dist/server/server.d.ts.map +1 -1
- package/packages/core/dist/server/server.js +5 -38
- package/packages/core/dist/server/server.js.map +1 -1
- package/packages/core/dist/server/server.test.d.ts +1 -1
- package/packages/core/dist/server/server.test.js +12 -23
- package/packages/core/dist/server/server.test.js.map +1 -1
- package/packages/core/dist/server/settings.d.ts +1 -0
- package/packages/core/dist/server/settings.d.ts.map +1 -1
- package/packages/core/dist/server/settings.js +13 -0
- package/packages/core/dist/server/settings.js.map +1 -1
- package/packages/core/dist/shared/capabilities.d.ts +88 -0
- package/packages/core/dist/shared/capabilities.d.ts.map +1 -0
- package/packages/core/dist/shared/capabilities.js +133 -0
- package/packages/core/dist/shared/capabilities.js.map +1 -0
- package/packages/core/dist/shared/capabilities.test.d.ts +2 -0
- package/packages/core/dist/shared/capabilities.test.d.ts.map +1 -0
- package/packages/core/dist/shared/capabilities.test.js +217 -0
- package/packages/core/dist/shared/capabilities.test.js.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.d.ts +47 -0
- package/packages/core/dist/shared/spawn-prompt.d.ts.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.js +82 -0
- package/packages/core/dist/shared/spawn-prompt.js.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.test.d.ts +2 -0
- package/packages/core/dist/shared/spawn-prompt.test.d.ts.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.test.js +251 -0
- package/packages/core/dist/shared/spawn-prompt.test.js.map +1 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts +18 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.js +434 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -0
- package/packages/core/dist/workflow/team-lifecycle.d.ts +169 -0
- package/packages/core/dist/workflow/team-lifecycle.d.ts.map +1 -0
- package/packages/core/dist/workflow/team-lifecycle.js +217 -0
- package/packages/core/dist/workflow/team-lifecycle.js.map +1 -0
- package/packages/core/dist/workflow/team-lifecycle.test.d.ts +20 -0
- package/packages/core/dist/workflow/team-lifecycle.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/team-lifecycle.test.js +966 -0
- package/packages/core/dist/workflow/team-lifecycle.test.js.map +1 -0
- package/packages/core/dist/workflow/workflow-schema.d.ts +32 -0
- package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.js +120 -0
- package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.test.js +570 -1
- package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
- package/packages/core/dist/workflow/workflow-team-templates.test.d.ts +17 -0
- package/packages/core/dist/workflow/workflow-team-templates.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/workflow-team-templates.test.js +275 -0
- package/packages/core/dist/workflow/workflow-team-templates.test.js.map +1 -0
- package/pennyfarthing-dist/agents/dev.md +21 -12
- package/pennyfarthing-dist/agents/reviewer.md +23 -4
- package/pennyfarthing-dist/agents/sm-finish.md +19 -2
- package/pennyfarthing-dist/agents/sm-setup.md +7 -7
- package/pennyfarthing-dist/agents/sm.md +12 -12
- package/pennyfarthing-dist/agents/tea.md +2 -2
- package/pennyfarthing-dist/agents/testing-runner.md +1 -1
- package/pennyfarthing-dist/commands/pf-architect.md +1 -1
- package/pennyfarthing-dist/commands/pf-ba.md +1 -1
- package/pennyfarthing-dist/commands/pf-chore.md +2 -2
- package/pennyfarthing-dist/commands/pf-dev.md +1 -1
- package/pennyfarthing-dist/commands/pf-devops.md +1 -1
- package/pennyfarthing-dist/commands/pf-epic.md +6 -6
- package/pennyfarthing-dist/commands/pf-git.md +12 -10
- package/pennyfarthing-dist/commands/pf-health-check.md +1 -1
- package/pennyfarthing-dist/commands/pf-help.md +12 -12
- package/pennyfarthing-dist/commands/pf-orchestrator.md +1 -1
- package/pennyfarthing-dist/commands/pf-pm.md +1 -1
- package/pennyfarthing-dist/commands/pf-prime.md +8 -8
- package/pennyfarthing-dist/commands/pf-reviewer.md +1 -1
- package/pennyfarthing-dist/commands/pf-session.md +7 -7
- package/pennyfarthing-dist/commands/pf-sm.md +1 -1
- package/pennyfarthing-dist/commands/pf-sprint.md +7 -7
- package/pennyfarthing-dist/commands/pf-tea.md +1 -1
- package/pennyfarthing-dist/commands/pf-tech-writer.md +1 -1
- package/pennyfarthing-dist/commands/pf-theme.md +9 -9
- package/pennyfarthing-dist/commands/pf-ux-designer.md +1 -1
- package/pennyfarthing-dist/commands/pf-work.md +1 -1
- package/pennyfarthing-dist/gates/approval.md +63 -0
- package/pennyfarthing-dist/gates/confidence-sm.md +71 -0
- package/pennyfarthing-dist/gates/context-ok.md +56 -0
- package/pennyfarthing-dist/gates/evaluations/confidence-sm.md +54 -0
- package/pennyfarthing-dist/gates/quality-pass.md +67 -0
- package/pennyfarthing-dist/gates/tests-fail.md +84 -0
- package/pennyfarthing-dist/gates/tests-pass.md +79 -0
- package/pennyfarthing-dist/guides/agent-behavior.md +84 -29
- package/pennyfarthing-dist/guides/agent-coordination.md +10 -10
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -6
- package/pennyfarthing-dist/guides/agent-template-tactical.md +1 -1
- package/pennyfarthing-dist/guides/bell-mode.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +10 -10
- package/pennyfarthing-dist/guides/brownfield-tools.md +24 -24
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/gate-schema.md +2 -2
- package/pennyfarthing-dist/guides/gates.md +3 -3
- package/pennyfarthing-dist/guides/handoff-cli.md +8 -8
- package/pennyfarthing-dist/guides/hooks.md +29 -29
- package/pennyfarthing-dist/guides/prime.md +2 -2
- package/pennyfarthing-dist/guides/reflector.md +1 -1
- package/pennyfarthing-dist/guides/skill-schema.md +6 -6
- package/pennyfarthing-dist/guides/tandem-protocol.md +3 -3
- package/pennyfarthing-dist/guides/workflow-schema.md +1 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +8 -8
- package/pennyfarthing-dist/scripts/README.md +4 -4
- package/pennyfarthing-dist/scripts/core/agent-session.sh +2 -5
- package/pennyfarthing-dist/scripts/core/check-context.sh +3 -1
- package/pennyfarthing-dist/scripts/core/pf.sh +5 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +4 -89
- package/pennyfarthing-dist/scripts/core/prime.sh +2 -25
- package/pennyfarthing-dist/scripts/git/README.md +14 -14
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +2 -3
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +2 -3
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -3
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +2 -4
- package/pennyfarthing-dist/scripts/hooks/README.md +6 -6
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +4 -183
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +4 -95
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +4 -65
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +3 -31
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +5 -4
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +29 -34
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +4 -71
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +3 -19
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +4 -30
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +3 -32
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +4 -65
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +4 -78
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +3 -93
- package/pennyfarthing-dist/scripts/lib/env.sh +34 -0
- package/pennyfarthing-dist/scripts/lib/run-pf.sh +39 -0
- package/pennyfarthing-dist/scripts/misc/README.md +1 -1
- package/pennyfarthing-dist/scripts/misc/statusline.sh +4 -301
- package/pennyfarthing-dist/scripts/sprint/README.md +21 -21
- package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +2 -16
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +3 -3
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +3 -3
- package/pennyfarthing-dist/skills/pf-bc/examples.md +23 -23
- package/pennyfarthing-dist/skills/pf-bc/skill.md +17 -17
- package/pennyfarthing-dist/skills/pf-bc/usage.md +8 -8
- package/pennyfarthing-dist/skills/pf-jira/SKILL.md +15 -15
- package/pennyfarthing-dist/skills/pf-jira/examples.md +48 -48
- package/pennyfarthing-dist/skills/pf-jira/usage.md +15 -15
- package/pennyfarthing-dist/skills/pf-sprint/examples.md +80 -80
- package/pennyfarthing-dist/skills/pf-sprint/skill.md +35 -35
- package/pennyfarthing-dist/skills/pf-sprint/usage.md +30 -30
- package/pennyfarthing-dist/skills/pf-theme/examples.md +15 -15
- package/pennyfarthing-dist/skills/pf-theme/skill.md +6 -6
- package/pennyfarthing-dist/skills/pf-theme/usage.md +5 -5
- package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -27
- package/pennyfarthing-dist/skills/pf-workflow/skill.md +11 -11
- package/pennyfarthing-dist/skills/pf-workflow/usage.md +11 -11
- package/pennyfarthing-dist/skills/skill-registry.yaml +19 -19
- package/pennyfarthing-dist/templates/settings.local.json.template +19 -10
- package/pennyfarthing-dist/workflows/bdd-team.yaml +89 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +1 -1
- 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/project-setup/steps/step-01-discover.md +47 -0
- package/pennyfarthing-dist/workflows/tdd-team.yaml +80 -0
- package/pennyfarthing-dist/workflows/tdd.yaml +11 -2
- package/pennyfarthing_scripts/CLAUDE.md +19 -10
- package/pennyfarthing_scripts/__init__.py +1 -1
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/context.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__/schema_validation_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/__pycache__/focus.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/split.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +2 -2
- package/pennyfarthing_scripts/bellmode_hook.py +9 -296
- package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
- 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__/context_meter_footer.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__/events.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_resolver.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__/story_detail_data.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_screen.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/audit_log_panel.py +161 -0
- package/pennyfarthing_scripts/bikerack/base_panel.py +27 -4
- package/pennyfarthing_scripts/bikerack/changed_panel.py +96 -4
- package/pennyfarthing_scripts/bikerack/context_meter_footer.py +88 -0
- package/pennyfarthing_scripts/bikerack/debug_panel.py +1 -1
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +30 -0
- package/pennyfarthing_scripts/bikerack/events.py +28 -0
- package/pennyfarthing_scripts/bikerack/launcher.py +6 -6
- package/pennyfarthing_scripts/bikerack/portrait_resolver.py +139 -0
- package/pennyfarthing_scripts/bikerack/progress_panel.py +0 -1
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +373 -142
- package/pennyfarthing_scripts/bikerack/story_detail_data.py +247 -0
- package/pennyfarthing_scripts/bikerack/story_detail_screen.py +177 -0
- package/pennyfarthing_scripts/bikerack/tui.py +304 -62
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/cli.py +5 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +29 -2
- package/pennyfarthing_scripts/common/pr_config.py +38 -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 +3 -3
- package/pennyfarthing_scripts/context.py +3 -3
- 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__/repos.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/hooks_installer.py +2 -3
- package/pennyfarthing_scripts/git/status_all.py +1 -1
- package/pennyfarthing_scripts/git/worktree.py +2 -2
- package/pennyfarthing_scripts/git_group/__pycache__/cli.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__/marker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/phase_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/cli.py +33 -1
- package/pennyfarthing_scripts/handoff/complete_phase.py +28 -0
- package/pennyfarthing_scripts/handoff/marker.py +15 -15
- package/pennyfarthing_scripts/handoff/phase_check.py +96 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +13 -1
- package/pennyfarthing_scripts/hooks/__init__.py +442 -0
- package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/bell_mode.py +214 -0
- package/pennyfarthing_scripts/hooks/cli.py +96 -0
- package/pennyfarthing_scripts/hooks/context_breaker.py +104 -0
- package/pennyfarthing_scripts/hooks/context_warning.py +66 -0
- package/pennyfarthing_scripts/hooks/cyclist_pretooluse.py +129 -0
- package/pennyfarthing_scripts/hooks/pre_edit_check.py +77 -0
- package/pennyfarthing_scripts/hooks/reflector_check.py +270 -0
- package/pennyfarthing_scripts/hooks/schema_validation.py +202 -0
- package/pennyfarthing_scripts/hooks/session_start.py +294 -0
- package/pennyfarthing_scripts/hooks/session_stop.py +111 -0
- package/pennyfarthing_scripts/hooks/sprint_yaml_validation.py +97 -0
- package/pennyfarthing_scripts/hooks/statusline.py +429 -0
- package/pennyfarthing_scripts/hooks.py +27 -432
- package/pennyfarthing_scripts/pretooluse_hook.py +3 -185
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/heatmap.py +3 -15
- package/pennyfarthing_scripts/prime/workflow.py +2 -1
- package/pennyfarthing_scripts/schema_validation_hook.py +3 -298
- package/pennyfarthing_scripts/session_start_hook.py +4 -186
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/cli.py +121 -0
- package/pennyfarthing_scripts/sprint/loader.py +154 -3
- package/pennyfarthing_scripts/sprint/story_update.py +26 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_list_team.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_bikerack.py +26 -26
- package/pennyfarthing_scripts/tests/test_dialogue_manager.py +0 -1
- package/pennyfarthing_scripts/tests/test_sprint_panel.py +344 -265
- package/pennyfarthing_scripts/tests/test_workflow_list_team.py +147 -0
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/tandem_awareness.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/team_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/team_mode.py +323 -0
- package/pennyfarthing_scripts/validate/adapters/workflow.py +19 -0
- package/pennyfarthing_scripts/welcome_hook.py +3 -149
- 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/__pycache__/team_lifecycle.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/cli.py +22 -20
- package/pennyfarthing_scripts/workflow/state.py +0 -1
- package/pennyfarthing_scripts/workflow/team_lifecycle.py +256 -0
- package/packages/core/dist/cli/cyclist-migration.test.d.ts +0 -16
- package/packages/core/dist/cli/cyclist-migration.test.d.ts.map +0 -1
- package/packages/core/dist/cli/cyclist-migration.test.js +0 -229
- package/packages/core/dist/cli/cyclist-migration.test.js.map +0 -1
- package/packages/core/dist/scripts/theme-detail.test.d.ts +0 -10
- package/packages/core/dist/scripts/theme-detail.test.d.ts.map +0 -1
- package/packages/core/dist/scripts/theme-detail.test.js +0 -199
- package/packages/core/dist/scripts/theme-detail.test.js.map +0 -1
|
@@ -1,204 +1,435 @@
|
|
|
1
1
|
"""SprintPanel — Sprint status panel for BikeRack TUI.
|
|
2
2
|
|
|
3
3
|
Story 103-6: First panel implementation proving the BasePanel vertical slice.
|
|
4
|
-
|
|
4
|
+
Story 110-2: Added per-story cursor navigation and drill-through.
|
|
5
|
+
Migrated to Textual Tree widget for native hierarchy navigation.
|
|
6
|
+
Subscribes to /ws/sprint, renders sprint status with epic/story tree.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
from __future__ import annotations
|
|
8
10
|
|
|
9
11
|
from typing import Any
|
|
10
12
|
|
|
11
|
-
from rich.console import Group
|
|
12
|
-
from rich.padding import Padding
|
|
13
13
|
from rich.text import Text
|
|
14
|
+
from textual.app import ComposeResult
|
|
15
|
+
from textual.message import Message
|
|
16
|
+
from textual.widget import Widget
|
|
17
|
+
from textual.widgets import Static, Tree
|
|
14
18
|
|
|
15
|
-
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS,
|
|
19
|
+
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, render_progress_bar
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_status(status: str) -> str:
|
|
23
|
+
"""Normalize status string: lowercase, strip, hyphens for separators."""
|
|
24
|
+
s = status.lower().strip().replace("_", "-") if status else ""
|
|
25
|
+
# Normalize both British and American spelling
|
|
26
|
+
if s == "cancelled":
|
|
27
|
+
s = "canceled"
|
|
28
|
+
return s
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_terminal(status: str) -> bool:
|
|
32
|
+
"""Return True if the normalized status is done or canceled."""
|
|
33
|
+
return _normalize_status(status) in {"done", "canceled"}
|
|
16
34
|
|
|
17
35
|
|
|
18
36
|
def _status_badge(status: str) -> Text:
|
|
19
|
-
"""Convert status string to styled Rich Text badge."""
|
|
20
|
-
s = status
|
|
37
|
+
"""Convert status string to styled Rich Text badge (symbol only, no text)."""
|
|
38
|
+
s = _normalize_status(status)
|
|
21
39
|
if s == "done":
|
|
22
|
-
return Text("\u2713
|
|
40
|
+
return Text("\u2713", style="dim green")
|
|
41
|
+
if s == "canceled":
|
|
42
|
+
return Text("\u2715", style="dim")
|
|
23
43
|
if s == "in-progress":
|
|
24
|
-
return Text("\u27f3
|
|
44
|
+
return Text("\u27f3", style="bold yellow")
|
|
25
45
|
if s == "backlog":
|
|
26
|
-
return Text("\u25ef
|
|
46
|
+
return Text("\u25ef", style="dim")
|
|
27
47
|
if s == "blocked":
|
|
28
|
-
return Text("!
|
|
48
|
+
return Text("!", style="bold red")
|
|
29
49
|
if s == "review":
|
|
30
|
-
return Text("\u25ce
|
|
31
|
-
return Text(
|
|
50
|
+
return Text("\u25ce", style="cyan")
|
|
51
|
+
return Text("\u2014", style="dim")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_assignee(email: str | None) -> str:
|
|
55
|
+
"""Format an email address into a short display name.
|
|
56
|
+
|
|
57
|
+
``"keith.avery@1898andco.io"`` → ``"K. Avery"``
|
|
58
|
+
``None`` → ``""``
|
|
59
|
+
"""
|
|
60
|
+
if not email:
|
|
61
|
+
return ""
|
|
62
|
+
local = email.split("@")[0]
|
|
63
|
+
parts = local.replace("_", ".").split(".")
|
|
64
|
+
if len(parts) < 2:
|
|
65
|
+
return parts[0].capitalize()
|
|
66
|
+
first_initial = parts[0][0].upper() if parts[0] else ""
|
|
67
|
+
last = parts[-1].capitalize()
|
|
68
|
+
return f"{first_initial}. {last}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _should_expand(epic: dict[str, Any]) -> bool:
|
|
72
|
+
"""Check if an epic should be expanded by default (has incomplete work).
|
|
73
|
+
|
|
74
|
+
Epics that are canceled, or whose stories are all done/canceled, collapse.
|
|
75
|
+
"""
|
|
76
|
+
if _is_terminal(epic.get("status", "")):
|
|
77
|
+
return False
|
|
78
|
+
stories = epic.get("stories", [])
|
|
79
|
+
total_pts = 0
|
|
80
|
+
done_pts = 0
|
|
81
|
+
has_in_progress = False
|
|
82
|
+
for story in stories:
|
|
83
|
+
pts = story.get("points", 0)
|
|
84
|
+
if isinstance(pts, (int, float)):
|
|
85
|
+
total_pts += pts
|
|
86
|
+
if _is_terminal(story.get("status", "")):
|
|
87
|
+
done_pts += pts
|
|
88
|
+
if _normalize_status(story.get("status", "")) == "in-progress":
|
|
89
|
+
has_in_progress = True
|
|
90
|
+
return has_in_progress or done_pts < total_pts
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_EPIC_ID_WIDTH = 11 # "MSSCI-NNNNN" = 11 chars
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_epic_label(
|
|
97
|
+
epic_id: str, title: str, done_pts: int, total_pts: int, jira_key: str = ""
|
|
98
|
+
) -> Text:
|
|
99
|
+
"""Build Rich Text label for an epic tree node."""
|
|
100
|
+
label = Text(no_wrap=True, overflow="ellipsis")
|
|
101
|
+
display_id = jira_key if jira_key else epic_id
|
|
102
|
+
if len(display_id) > _EPIC_ID_WIDTH:
|
|
103
|
+
display_id = display_id[: _EPIC_ID_WIDTH - 1] + "\u2026"
|
|
104
|
+
display_id = f"{display_id:<{_EPIC_ID_WIDTH}}"
|
|
105
|
+
label.append(display_id, style="bold cyan")
|
|
106
|
+
label.append(" ")
|
|
107
|
+
if total_pts > 0:
|
|
108
|
+
pct = int(done_pts / total_pts * 100)
|
|
109
|
+
label.append_text(render_progress_bar(pct, width=10, fill_style="dim green"))
|
|
110
|
+
label.append(f" {done_pts}/{total_pts} pts", style="dim")
|
|
111
|
+
else:
|
|
112
|
+
label.append("0 pts", style="dim")
|
|
113
|
+
label.append(f" {title}", style="bold")
|
|
114
|
+
return label
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _build_story_label(story: dict[str, Any], current_story_id: str) -> Text:
|
|
118
|
+
"""Build Rich Text label for a story tree leaf.
|
|
119
|
+
|
|
120
|
+
Layout: ``✓ MSSCI-14952 2 Story title``
|
|
121
|
+
In-progress adds owner: ``⟳ MSSCI-15186 5 Story title [K. Avery]``
|
|
122
|
+
Done stories are rendered entirely dim.
|
|
123
|
+
"""
|
|
124
|
+
story_id = story.get("id", "")
|
|
125
|
+
title = story.get("title", "")
|
|
126
|
+
pts = story.get("points", "")
|
|
127
|
+
jira = story.get("jiraKey") or "\u2014"
|
|
128
|
+
status = _normalize_status(story.get("status", ""))
|
|
129
|
+
badge = _status_badge(story.get("status", ""))
|
|
130
|
+
|
|
131
|
+
is_done = _is_terminal(story.get("status", ""))
|
|
132
|
+
is_in_progress = status == "in-progress"
|
|
133
|
+
is_current = story_id == current_story_id
|
|
134
|
+
|
|
135
|
+
label = Text(no_wrap=True, overflow="ellipsis")
|
|
136
|
+
label.append_text(badge)
|
|
137
|
+
|
|
138
|
+
# MSSCI key flush left, fixed-width (14 chars — fits "MSSCI-NNNNN" + padding)
|
|
139
|
+
jira_padded = f"{jira:<14}"
|
|
140
|
+
label.append(f" {jira_padded}", style="dim" if is_done else ("bold cyan" if is_current else "cyan"))
|
|
32
141
|
|
|
142
|
+
# Points right-aligned (2 chars)
|
|
143
|
+
pts_str = f"{pts:>2}" if isinstance(pts, int) else f"{pts!s:>2}"
|
|
144
|
+
label.append(f" {pts_str}", style="dim")
|
|
33
145
|
|
|
34
|
-
|
|
35
|
-
"""
|
|
146
|
+
# Title
|
|
147
|
+
label.append(f" {title}", style="dim" if is_done else "")
|
|
148
|
+
|
|
149
|
+
# Owner for in-progress stories
|
|
150
|
+
if is_in_progress:
|
|
151
|
+
owner = _format_assignee(story.get("assignee"))
|
|
152
|
+
if owner:
|
|
153
|
+
label.append(f" [{owner}]", style="dim yellow")
|
|
154
|
+
|
|
155
|
+
if is_current:
|
|
156
|
+
label.stylize("bold")
|
|
157
|
+
|
|
158
|
+
if is_done:
|
|
159
|
+
label.stylize("dim")
|
|
160
|
+
|
|
161
|
+
return label
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class SprintPanel(Widget):
|
|
165
|
+
"""Sprint panel using native Textual Tree widget.
|
|
36
166
|
|
|
37
167
|
Subscribes to the ``sprint`` WebSocket channel and renders
|
|
38
|
-
sprint status as a
|
|
168
|
+
sprint status as a Tree with epic/story hierarchy.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
DEFAULT_CSS = """
|
|
172
|
+
SprintPanel {
|
|
173
|
+
height: 1fr;
|
|
174
|
+
layout: vertical;
|
|
175
|
+
}
|
|
176
|
+
#sprint-header {
|
|
177
|
+
height: auto;
|
|
178
|
+
max-height: 3;
|
|
179
|
+
padding: 0 1;
|
|
180
|
+
}
|
|
181
|
+
#sprint-hints {
|
|
182
|
+
height: 1;
|
|
183
|
+
padding: 0 1;
|
|
184
|
+
}
|
|
185
|
+
#sprint-tree {
|
|
186
|
+
height: 1fr;
|
|
187
|
+
}
|
|
39
188
|
"""
|
|
40
189
|
|
|
41
190
|
channel: str = "sprint"
|
|
42
191
|
panel_name: str = "Sprint"
|
|
43
192
|
icon: str = PANEL_ICONS["sprint"][0]
|
|
193
|
+
can_focus = True
|
|
44
194
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self._selected_epic: int = 0
|
|
48
|
-
self._toggled: dict[str, bool] = {} # epic_id -> user override
|
|
195
|
+
class DataReceived(Message, bubble=False):
|
|
196
|
+
"""WebSocket data received — triggers tree rebuild."""
|
|
49
197
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if epic_count == 0:
|
|
54
|
-
return
|
|
55
|
-
self._selected_epic = (self._selected_epic + 1) % epic_count
|
|
56
|
-
self._rerender()
|
|
198
|
+
def __init__(self, payload: dict[str, Any]) -> None:
|
|
199
|
+
super().__init__()
|
|
200
|
+
self.payload = payload
|
|
57
201
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
202
|
+
def __init__(self, client: Any = None, **kwargs: Any) -> None:
|
|
203
|
+
super().__init__(**kwargs)
|
|
204
|
+
self._client = client
|
|
205
|
+
self._last_payload: dict[str, Any] | None = None
|
|
206
|
+
self._mounted = False
|
|
207
|
+
|
|
208
|
+
def compose(self) -> ComposeResult:
|
|
209
|
+
yield Static(
|
|
210
|
+
"[dim]Waiting for sprint data...[/dim]", id="sprint-header"
|
|
211
|
+
)
|
|
212
|
+
yield Static(
|
|
213
|
+
"[dim]\u2191/\u2193:navigate space:expand/collapse Enter:open j/k/e:vim nav[/dim]",
|
|
214
|
+
id="sprint-hints",
|
|
215
|
+
)
|
|
216
|
+
tree: Tree[dict[str, Any]] = Tree("Sprint", id="sprint-tree")
|
|
217
|
+
tree.show_root = False
|
|
218
|
+
tree.guide_depth = 3
|
|
219
|
+
yield tree
|
|
220
|
+
|
|
221
|
+
def on_mount(self) -> None:
|
|
222
|
+
"""Subscribe to sprint channel via WS client."""
|
|
223
|
+
self._mounted = True
|
|
224
|
+
if self._client is not None and self.channel:
|
|
225
|
+
self._client.subscribe(self.channel, self._handle_ws_message)
|
|
226
|
+
|
|
227
|
+
def on_unmount(self) -> None:
|
|
228
|
+
"""Mark unmounted so WS callbacks are ignored."""
|
|
229
|
+
self._mounted = False
|
|
230
|
+
|
|
231
|
+
def focus(self, scroll_visible: bool = True) -> SprintPanel:
|
|
232
|
+
"""Delegate focus to the tree widget."""
|
|
233
|
+
try:
|
|
234
|
+
tree = self.query_one("#sprint-tree", Tree)
|
|
235
|
+
tree.focus(scroll_visible)
|
|
236
|
+
except Exception:
|
|
237
|
+
super().focus(scroll_visible)
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
# --- WebSocket message handling ---
|
|
241
|
+
|
|
242
|
+
def _handle_ws_message(self, message: dict[str, Any] | None) -> None:
|
|
243
|
+
"""Handle incoming WebSocket message (called from async WS task)."""
|
|
244
|
+
if not self._mounted or message is None:
|
|
69
245
|
return
|
|
70
|
-
|
|
71
|
-
|
|
246
|
+
self._last_payload = message
|
|
247
|
+
try:
|
|
248
|
+
self.post_message(self.DataReceived(message))
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
def on_sprint_panel_data_received(self, event: DataReceived) -> None:
|
|
253
|
+
"""Process data in Textual message context — rebuilds tree."""
|
|
254
|
+
self._rebuild_tree(event.payload)
|
|
255
|
+
|
|
256
|
+
# --- Tree rebuild ---
|
|
257
|
+
|
|
258
|
+
def _rebuild_tree(self, payload: dict[str, Any]) -> None:
|
|
259
|
+
"""Rebuild tree nodes from sprint payload, preserving expand/cursor state."""
|
|
260
|
+
try:
|
|
261
|
+
tree = self.query_one("#sprint-tree", Tree)
|
|
262
|
+
except Exception:
|
|
72
263
|
return
|
|
73
|
-
epic_id = epics[self._selected_epic].get("id", "")
|
|
74
|
-
if epic_id:
|
|
75
|
-
self._toggled[epic_id] = not self._is_expanded(epics[self._selected_epic])
|
|
76
|
-
self._rerender()
|
|
77
|
-
|
|
78
|
-
def _rerender(self) -> None:
|
|
79
|
-
if self._last_payload is not None:
|
|
80
|
-
rendered = self.render_panel(self._last_payload)
|
|
81
|
-
try:
|
|
82
|
-
self.update(rendered)
|
|
83
|
-
except Exception:
|
|
84
|
-
pass
|
|
85
264
|
|
|
86
|
-
def _epic_count(self) -> int:
|
|
87
|
-
if self._last_payload is None:
|
|
88
|
-
return 0
|
|
89
|
-
return len(self._last_payload.get("epics", []))
|
|
90
|
-
|
|
91
|
-
def _is_expanded(self, epic: dict[str, Any]) -> bool:
|
|
92
|
-
"""Check if an epic should be expanded."""
|
|
93
|
-
epic_id = epic.get("id", "")
|
|
94
|
-
if epic_id in self._toggled:
|
|
95
|
-
return self._toggled[epic_id]
|
|
96
|
-
# Default: expand if has incomplete work
|
|
97
|
-
stories = epic.get("stories", [])
|
|
98
|
-
total_pts = 0
|
|
99
|
-
done_pts = 0
|
|
100
|
-
has_in_progress = False
|
|
101
|
-
for story in stories:
|
|
102
|
-
pts = story.get("points", 0)
|
|
103
|
-
if isinstance(pts, (int, float)):
|
|
104
|
-
total_pts += pts
|
|
105
|
-
status = (story.get("status") or "").lower().strip()
|
|
106
|
-
if status == "done":
|
|
107
|
-
done_pts += pts
|
|
108
|
-
if status == "in-progress":
|
|
109
|
-
has_in_progress = True
|
|
110
|
-
return has_in_progress or done_pts < total_pts
|
|
111
|
-
|
|
112
|
-
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
113
|
-
"""Render sprint data with epic grouping and progress bars."""
|
|
114
265
|
sprint = payload.get("sprint", {})
|
|
115
266
|
metrics = payload.get("metrics", {})
|
|
116
267
|
epics = payload.get("epics", [])
|
|
117
268
|
current_story_id = sprint.get("currentStory", "")
|
|
118
269
|
|
|
119
|
-
#
|
|
120
|
-
if epics and self._selected_epic >= len(epics):
|
|
121
|
-
self._selected_epic = len(epics) - 1
|
|
122
|
-
|
|
123
|
-
# Sprint metrics header
|
|
270
|
+
# Update header
|
|
124
271
|
sprint_num = sprint.get("number", "")
|
|
125
272
|
done = sprint.get("done", 0)
|
|
126
273
|
remaining = sprint.get("remaining", 0)
|
|
127
274
|
in_progress = sprint.get("inProgress", 0)
|
|
128
275
|
velocity = metrics.get("velocity", 0)
|
|
129
|
-
|
|
130
|
-
header = Text.from_markup(
|
|
276
|
+
header_text = Text.from_markup(
|
|
131
277
|
f"Sprint {sprint_num} "
|
|
132
278
|
f"[green]Done: {done}[/green] | "
|
|
133
279
|
f"Remaining: {remaining} | "
|
|
134
280
|
f"In Progress: {in_progress} | "
|
|
135
281
|
f"Velocity: {velocity}"
|
|
136
282
|
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
283
|
+
try:
|
|
284
|
+
self.query_one("#sprint-header", Static).update(header_text)
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
# Save expand state from current tree nodes
|
|
289
|
+
saved_expanded: dict[str, bool] = {}
|
|
290
|
+
for node in tree.root.children:
|
|
291
|
+
data = node.data
|
|
292
|
+
if data and data.get("type") == "epic":
|
|
293
|
+
saved_expanded[data["id"]] = node.is_expanded
|
|
294
|
+
|
|
295
|
+
# Save cursor position by node identity
|
|
296
|
+
cursor_node_key: str | None = None
|
|
297
|
+
try:
|
|
298
|
+
highlighted = tree.get_node_at_line(tree.cursor_line)
|
|
299
|
+
if highlighted and highlighted.data:
|
|
300
|
+
data = highlighted.data
|
|
301
|
+
if data.get("type") == "epic":
|
|
302
|
+
cursor_node_key = f"epic:{data['id']}"
|
|
303
|
+
elif data.get("type") == "story":
|
|
304
|
+
cursor_node_key = f"story:{data['story'].get('id', '')}"
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
# Rebuild tree
|
|
309
|
+
tree.clear()
|
|
310
|
+
cursor_target = None
|
|
311
|
+
|
|
312
|
+
for epic in epics:
|
|
142
313
|
epic_id = epic.get("id", "")
|
|
143
314
|
epic_title = epic.get("title", "")
|
|
144
315
|
stories = epic.get("stories", [])
|
|
145
316
|
|
|
146
|
-
# Calculate epic progress
|
|
317
|
+
# Calculate epic progress (canceled counts as done)
|
|
147
318
|
total_pts = 0
|
|
148
319
|
done_pts = 0
|
|
149
320
|
for story in stories:
|
|
150
321
|
pts = story.get("points", 0)
|
|
151
322
|
if isinstance(pts, (int, float)):
|
|
152
323
|
total_pts += pts
|
|
153
|
-
|
|
154
|
-
if status == "done":
|
|
324
|
+
if _is_terminal(story.get("status", "")):
|
|
155
325
|
done_pts += pts
|
|
156
326
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
327
|
+
label = _build_epic_label(
|
|
328
|
+
epic_id, epic_title, done_pts, total_pts,
|
|
329
|
+
jira_key=epic.get("jiraKey", ""),
|
|
330
|
+
)
|
|
331
|
+
epic_data: dict[str, Any] = {
|
|
332
|
+
"type": "epic",
|
|
333
|
+
"id": epic_id,
|
|
334
|
+
"title": epic_title,
|
|
335
|
+
}
|
|
336
|
+
epic_node = tree.root.add(label, data=epic_data)
|
|
337
|
+
|
|
338
|
+
# Add story leaves
|
|
339
|
+
for story in stories:
|
|
340
|
+
story_label = _build_story_label(story, current_story_id)
|
|
341
|
+
story_data: dict[str, Any] = {"type": "story", "story": story}
|
|
342
|
+
story_node = epic_node.add_leaf(story_label, data=story_data)
|
|
343
|
+
|
|
344
|
+
if cursor_node_key == f"story:{story.get('id', '')}":
|
|
345
|
+
cursor_target = story_node
|
|
346
|
+
|
|
347
|
+
# Restore expand state or apply default
|
|
348
|
+
if epic_id in saved_expanded:
|
|
349
|
+
if saved_expanded[epic_id]:
|
|
350
|
+
epic_node.expand()
|
|
351
|
+
else:
|
|
352
|
+
epic_node.collapse()
|
|
353
|
+
elif _should_expand(epic):
|
|
354
|
+
epic_node.expand()
|
|
355
|
+
|
|
356
|
+
if cursor_node_key == f"epic:{epic_id}":
|
|
357
|
+
cursor_target = epic_node
|
|
358
|
+
|
|
359
|
+
# Restore cursor position
|
|
360
|
+
if cursor_target is not None:
|
|
361
|
+
try:
|
|
362
|
+
tree.move_cursor(cursor_target)
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
# --- Tree event handling ---
|
|
367
|
+
|
|
368
|
+
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
|
|
369
|
+
"""Handle enter on a tree node: drill into story or toggle epic."""
|
|
370
|
+
data = event.node.data
|
|
371
|
+
if not data:
|
|
372
|
+
return
|
|
373
|
+
if data.get("type") == "story":
|
|
374
|
+
story = data["story"]
|
|
375
|
+
from pennyfarthing_scripts.bikerack.story_detail_screen import (
|
|
376
|
+
StoryDetailScreen,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
self.app.push_screen(StoryDetailScreen(story_data=story))
|
|
381
|
+
except Exception:
|
|
382
|
+
pass
|
|
383
|
+
elif data.get("type") == "epic":
|
|
384
|
+
event.node.toggle()
|
|
385
|
+
|
|
386
|
+
# --- Compatibility methods for app-level bindings ---
|
|
387
|
+
|
|
388
|
+
def next_epic(self) -> None:
|
|
389
|
+
"""Move cursor down — compat wrapper for app j binding."""
|
|
390
|
+
try:
|
|
391
|
+
tree = self.query_one("#sprint-tree", Tree)
|
|
392
|
+
tree.action_cursor_down()
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
def prev_epic(self) -> None:
|
|
397
|
+
"""Move cursor up — compat wrapper for app k binding."""
|
|
398
|
+
try:
|
|
399
|
+
tree = self.query_one("#sprint-tree", Tree)
|
|
400
|
+
tree.action_cursor_up()
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
def toggle_epic(self) -> None:
|
|
405
|
+
"""Toggle current node — compat wrapper for app e binding."""
|
|
406
|
+
try:
|
|
407
|
+
tree = self.query_one("#sprint-tree", Tree)
|
|
408
|
+
tree.action_toggle_node()
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
def get_selected_story(self) -> dict[str, Any] | None:
|
|
413
|
+
"""Return the currently highlighted story data, or None."""
|
|
414
|
+
try:
|
|
415
|
+
tree = self.query_one("#sprint-tree", Tree)
|
|
416
|
+
node = tree.get_node_at_line(tree.cursor_line)
|
|
417
|
+
if node and node.data and node.data.get("type") == "story":
|
|
418
|
+
return node.data["story"]
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
def drill_into_story(self) -> None:
|
|
424
|
+
"""Push StoryDetailScreen for the highlighted story."""
|
|
425
|
+
story = self.get_selected_story()
|
|
426
|
+
if story is None:
|
|
427
|
+
return
|
|
428
|
+
from pennyfarthing_scripts.bikerack.story_detail_screen import (
|
|
429
|
+
StoryDetailScreen,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
self.app.push_screen(StoryDetailScreen(story_data=story))
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|