@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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Story 103-14: Subscribes to /ws/git, extracts dirtyFiles from all repos,
|
|
4
4
|
renders Rich table with file path, change type icon, and status.
|
|
5
|
+
|
|
6
|
+
Story 110-1: Selectable file list with arrow navigation and Enter to navigate.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
from __future__ import annotations
|
|
@@ -9,6 +11,7 @@ from __future__ import annotations
|
|
|
9
11
|
from typing import Any
|
|
10
12
|
|
|
11
13
|
from rich.text import Text
|
|
14
|
+
from textual.binding import Binding
|
|
12
15
|
|
|
13
16
|
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
14
17
|
|
|
@@ -55,15 +58,98 @@ class ChangedPanel(BasePanel):
|
|
|
55
58
|
|
|
56
59
|
Subscribes to the ``git`` WebSocket channel and renders
|
|
57
60
|
dirty files from all repos as a Rich table with file path,
|
|
58
|
-
change type icon, and status.
|
|
61
|
+
change type icon, and status. Supports arrow-key selection
|
|
62
|
+
and Enter to navigate to diffs.
|
|
59
63
|
"""
|
|
60
64
|
|
|
61
65
|
channel: str = "git"
|
|
62
66
|
panel_name: str = "Changed"
|
|
63
67
|
icon: str = PANEL_ICONS["changed"][0]
|
|
68
|
+
can_focus = True
|
|
69
|
+
|
|
70
|
+
BINDINGS = [
|
|
71
|
+
Binding("up", "select_prev_key", "Up"),
|
|
72
|
+
Binding("down", "select_next_key", "Down"),
|
|
73
|
+
Binding("enter", "select_file", "Select file"),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
def __init__(self, client=None, **kwargs):
|
|
77
|
+
super().__init__(client=client, **kwargs)
|
|
78
|
+
self._selected_index: int = 0
|
|
79
|
+
self._file_paths: list[str] = []
|
|
80
|
+
|
|
81
|
+
def handle_message(self, message: dict[str, Any] | None) -> None:
|
|
82
|
+
"""Handle incoming message — build file path index then render."""
|
|
83
|
+
if message is not None:
|
|
84
|
+
self._build_file_paths(message)
|
|
85
|
+
super().handle_message(message)
|
|
86
|
+
|
|
87
|
+
def _build_file_paths(self, payload: dict[str, Any]) -> None:
|
|
88
|
+
"""Extract flat list of file paths from repos payload."""
|
|
89
|
+
paths: list[str] = []
|
|
90
|
+
repos = payload.get("repos", [])
|
|
91
|
+
if isinstance(repos, list):
|
|
92
|
+
for repo in repos:
|
|
93
|
+
if not isinstance(repo, dict):
|
|
94
|
+
continue
|
|
95
|
+
dirty_files = repo.get("dirtyFiles", [])
|
|
96
|
+
if not isinstance(dirty_files, list):
|
|
97
|
+
continue
|
|
98
|
+
for f in dirty_files:
|
|
99
|
+
if isinstance(f, dict):
|
|
100
|
+
path = f.get("path", "")
|
|
101
|
+
if path:
|
|
102
|
+
paths.append(path)
|
|
103
|
+
self._file_paths = paths
|
|
104
|
+
if self._selected_index >= len(paths):
|
|
105
|
+
self._selected_index = max(0, len(paths) - 1)
|
|
106
|
+
|
|
107
|
+
def select_next(self) -> None:
|
|
108
|
+
"""Move selection to the next file."""
|
|
109
|
+
if self._file_paths and self._selected_index < len(self._file_paths) - 1:
|
|
110
|
+
self._selected_index += 1
|
|
111
|
+
self._rerender()
|
|
112
|
+
|
|
113
|
+
def select_prev(self) -> None:
|
|
114
|
+
"""Move selection to the previous file."""
|
|
115
|
+
if self._selected_index > 0:
|
|
116
|
+
self._selected_index -= 1
|
|
117
|
+
self._rerender()
|
|
118
|
+
|
|
119
|
+
def action_select_next_key(self) -> None:
|
|
120
|
+
"""Binding action: move selection down."""
|
|
121
|
+
self.select_next()
|
|
122
|
+
|
|
123
|
+
def action_select_prev_key(self) -> None:
|
|
124
|
+
"""Binding action: move selection up."""
|
|
125
|
+
self.select_prev()
|
|
126
|
+
|
|
127
|
+
def _rerender(self) -> None:
|
|
128
|
+
"""Re-render panel with current payload after selection change."""
|
|
129
|
+
if self._last_payload:
|
|
130
|
+
try:
|
|
131
|
+
self.update(self.render_panel(self._last_payload))
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def get_selected_path(self) -> str | None:
|
|
136
|
+
"""Return the currently selected file path, or None if empty."""
|
|
137
|
+
if not self._file_paths:
|
|
138
|
+
return None
|
|
139
|
+
if self._selected_index >= len(self._file_paths):
|
|
140
|
+
return None
|
|
141
|
+
return self._file_paths[self._selected_index]
|
|
142
|
+
|
|
143
|
+
def action_select_file(self) -> None:
|
|
144
|
+
"""Post NavigateToFile event for the selected file."""
|
|
145
|
+
path = self.get_selected_path()
|
|
146
|
+
if path is not None:
|
|
147
|
+
from pennyfarthing_scripts.bikerack.events import NavigateToFile
|
|
148
|
+
|
|
149
|
+
self.post_message(NavigateToFile(path=path))
|
|
64
150
|
|
|
65
151
|
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
66
|
-
"""Render changed files grouped by repository."""
|
|
152
|
+
"""Render changed files grouped by repository with selection highlight."""
|
|
67
153
|
repos = payload.get("repos", [])
|
|
68
154
|
if not isinstance(repos, list):
|
|
69
155
|
return Text("No changed files", style="dim italic")
|
|
@@ -85,6 +171,7 @@ class ChangedPanel(BasePanel):
|
|
|
85
171
|
from rich.console import Group as RichGroup
|
|
86
172
|
|
|
87
173
|
parts: list[Any] = []
|
|
174
|
+
flat_idx = 0
|
|
88
175
|
for repo_name, files in repo_files.items():
|
|
89
176
|
count = len(files)
|
|
90
177
|
label = "file" if count == 1 else "files"
|
|
@@ -97,12 +184,17 @@ class ChangedPanel(BasePanel):
|
|
|
97
184
|
status_code = f.get("status", " ")
|
|
98
185
|
path = f.get("path", "")
|
|
99
186
|
icon, label_text, style = _parse_status(status_code)
|
|
187
|
+
is_selected = flat_idx == self._selected_index
|
|
100
188
|
line = Text()
|
|
101
|
-
|
|
189
|
+
if is_selected:
|
|
190
|
+
line.append("› ", style="bold reverse")
|
|
191
|
+
else:
|
|
192
|
+
line.append(" ")
|
|
102
193
|
line.append(icon, style=f"bold {style}")
|
|
103
|
-
line.append(f" {path}", style="cyan")
|
|
194
|
+
line.append(f" {path}", style="bold cyan reverse" if is_selected else "cyan")
|
|
104
195
|
line.append(f" {label_text}", style=style)
|
|
105
196
|
parts.append(line)
|
|
197
|
+
flat_idx += 1
|
|
106
198
|
|
|
107
199
|
parts.append(Text("")) # spacer between repos
|
|
108
200
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""ContextMeterFooter — Persistent context usage footer bar for BikeRack TUI.
|
|
2
|
+
|
|
3
|
+
Story 110-5: Context meter footer bar. Displays context window usage
|
|
4
|
+
percentage with color-coded tier thresholds, always visible at the
|
|
5
|
+
bottom of the layout.
|
|
6
|
+
|
|
7
|
+
Subscribes to /ws/context WebSocket channel.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
from textual.message import Message
|
|
16
|
+
from textual.widgets import Static
|
|
17
|
+
|
|
18
|
+
from pennyfarthing_scripts.bikerack.base_panel import render_progress_bar
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContextMeterFooter(Static):
|
|
22
|
+
"""Persistent footer bar showing context window usage.
|
|
23
|
+
|
|
24
|
+
Not a Footer subclass — this is a Static widget mounted between
|
|
25
|
+
#main-content and BindingFooter in the app layout.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
class MeterUpdate(Message, bubble=False):
|
|
29
|
+
"""Context meter data received — routed through Textual message system."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, content: Any) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.content = content
|
|
34
|
+
|
|
35
|
+
#: WebSocket channel this footer subscribes to
|
|
36
|
+
channel: str = "context"
|
|
37
|
+
|
|
38
|
+
def __init__(self, client: Any = None, **kwargs: Any) -> None:
|
|
39
|
+
super().__init__(**kwargs)
|
|
40
|
+
self._client = client
|
|
41
|
+
self._context_data: dict[str, Any] | None = None
|
|
42
|
+
self._mounted = False
|
|
43
|
+
|
|
44
|
+
def on_mount(self) -> None:
|
|
45
|
+
"""Subscribe to context channel on mount."""
|
|
46
|
+
self._mounted = True
|
|
47
|
+
if self._client is not None:
|
|
48
|
+
self._client.subscribe("context", self.handle_context_message)
|
|
49
|
+
|
|
50
|
+
def on_unmount(self) -> None:
|
|
51
|
+
"""Mark as unmounted so further messages are ignored."""
|
|
52
|
+
self._mounted = False
|
|
53
|
+
|
|
54
|
+
def on_context_meter_footer_meter_update(self, event: MeterUpdate) -> None:
|
|
55
|
+
"""Process MeterUpdate in Textual message context — triggers repaint."""
|
|
56
|
+
self.update(event.content)
|
|
57
|
+
|
|
58
|
+
def handle_context_message(self, msg: dict[str, Any] | None) -> None:
|
|
59
|
+
"""Process incoming /ws/context message."""
|
|
60
|
+
if not self._mounted or msg is None:
|
|
61
|
+
return
|
|
62
|
+
ctx = msg.get("context")
|
|
63
|
+
if ctx is None:
|
|
64
|
+
return
|
|
65
|
+
self._context_data = ctx
|
|
66
|
+
try:
|
|
67
|
+
rendered = self.render_meter(ctx)
|
|
68
|
+
self.post_message(self.MeterUpdate(rendered))
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def render_meter(self, ctx: dict[str, Any]) -> Text:
|
|
73
|
+
"""Render a compact context usage bar with percentage and tier badge."""
|
|
74
|
+
percent = ctx.get("percent", 0)
|
|
75
|
+
tier = ctx.get("tier", "")
|
|
76
|
+
|
|
77
|
+
bar = render_progress_bar(percent, warn_high=True)
|
|
78
|
+
|
|
79
|
+
if tier:
|
|
80
|
+
if percent < 50:
|
|
81
|
+
tier_style = "green"
|
|
82
|
+
elif percent <= 80:
|
|
83
|
+
tier_style = "yellow"
|
|
84
|
+
else:
|
|
85
|
+
tier_style = "red"
|
|
86
|
+
bar.append(f" {tier}", style=f"bold {tier_style}")
|
|
87
|
+
|
|
88
|
+
return bar
|
|
@@ -103,7 +103,7 @@ class DebugPanel(BasePanel):
|
|
|
103
103
|
"""Re-render with the latest data from both channels."""
|
|
104
104
|
rendered = self.render_panel(self._context_data or {})
|
|
105
105
|
try:
|
|
106
|
-
self.
|
|
106
|
+
self._thread_safe_update(rendered)
|
|
107
107
|
except Exception:
|
|
108
108
|
pass
|
|
109
109
|
|
|
@@ -17,6 +17,7 @@ from typing import Any
|
|
|
17
17
|
from rich.console import Group
|
|
18
18
|
from rich.syntax import Syntax
|
|
19
19
|
from rich.text import Text
|
|
20
|
+
from textual.binding import Binding
|
|
20
21
|
|
|
21
22
|
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
22
23
|
|
|
@@ -46,6 +47,12 @@ class DiffsPanel(BasePanel):
|
|
|
46
47
|
channel: str = "diffs"
|
|
47
48
|
panel_name: str = "Diffs"
|
|
48
49
|
icon: str = PANEL_ICONS["diffs"][0]
|
|
50
|
+
can_focus = True
|
|
51
|
+
|
|
52
|
+
BINDINGS = [
|
|
53
|
+
Binding("n", "next_file_key", "Next file"),
|
|
54
|
+
Binding("p", "prev_file_key", "Prev file"),
|
|
55
|
+
]
|
|
49
56
|
|
|
50
57
|
def __init__(self, client=None, **kwargs):
|
|
51
58
|
super().__init__(client=client, **kwargs)
|
|
@@ -87,6 +94,29 @@ class DiffsPanel(BasePanel):
|
|
|
87
94
|
except Exception:
|
|
88
95
|
pass
|
|
89
96
|
|
|
97
|
+
def navigate_to_file(self, path: str) -> None:
|
|
98
|
+
"""Jump to a specific file by path. No-op if not found."""
|
|
99
|
+
if self._last_payload is None:
|
|
100
|
+
return
|
|
101
|
+
diffs = self._last_payload.get("diffs", [])
|
|
102
|
+
for i, d in enumerate(diffs):
|
|
103
|
+
if d.get("path") == path:
|
|
104
|
+
self._current_file_index = i
|
|
105
|
+
rendered = self.render_panel(self._last_payload)
|
|
106
|
+
try:
|
|
107
|
+
self.update(rendered)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
def action_next_file_key(self) -> None:
|
|
113
|
+
"""Binding action: advance to next file."""
|
|
114
|
+
self.next_file()
|
|
115
|
+
|
|
116
|
+
def action_prev_file_key(self) -> None:
|
|
117
|
+
"""Binding action: go to previous file."""
|
|
118
|
+
self.prev_file()
|
|
119
|
+
|
|
90
120
|
def handle_message(self, message: dict[str, Any] | None) -> None:
|
|
91
121
|
"""Handle incoming WebSocket message with pagination reset and temp management."""
|
|
92
122
|
if not self._mounted or message is None:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Cross-panel event bus for BikeRack TUI (Story 110-1).
|
|
2
|
+
|
|
3
|
+
Defines Textual Message subclasses for inter-panel communication.
|
|
4
|
+
First use case: NavigateToFile posted by ChangedPanel, handled by App
|
|
5
|
+
to switch to DiffsPanel filtered to that file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PanelEvent(Message):
|
|
14
|
+
"""Base message class for cross-panel communication."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NavigateToFile(PanelEvent):
|
|
20
|
+
"""Navigate to a specific file in the diffs panel.
|
|
21
|
+
|
|
22
|
+
Posted by ChangedPanel when user presses Enter on a selected file.
|
|
23
|
+
Handled by BikeRackApp to switch to DiffsPanel and navigate to that file.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, path: str) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.path = path
|
|
@@ -24,8 +24,8 @@ def is_process_alive(pid: int) -> bool:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def cleanup_files(project_dir: Path) -> None:
|
|
27
|
-
"""Clean up .
|
|
28
|
-
for name in (".
|
|
27
|
+
"""Clean up .bikerack-port, .wheelhub-pid, and .wheelhub-gui-pid files."""
|
|
28
|
+
for name in (".bikerack-port", ".wheelhub-pid", ".wheelhub-gui-pid"):
|
|
29
29
|
try:
|
|
30
30
|
(project_dir / name).unlink()
|
|
31
31
|
except FileNotFoundError:
|
|
@@ -33,9 +33,9 @@ def cleanup_files(project_dir: Path) -> None:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def read_port_file(project_dir: Path) -> int | None:
|
|
36
|
-
"""Read port from .
|
|
36
|
+
"""Read port from .bikerack-port file. Returns None if not found."""
|
|
37
37
|
try:
|
|
38
|
-
return int((project_dir / ".
|
|
38
|
+
return int((project_dir / ".bikerack-port").read_text().strip())
|
|
39
39
|
except (FileNotFoundError, ValueError):
|
|
40
40
|
return None
|
|
41
41
|
|
|
@@ -104,8 +104,8 @@ def start_wheelhub(project_dir: Path) -> subprocess.Popen:
|
|
|
104
104
|
def poll_for_port_file(
|
|
105
105
|
project_dir: Path, timeout: float = 5.0, interval: float = 0.1
|
|
106
106
|
) -> int:
|
|
107
|
-
"""Poll for .
|
|
108
|
-
port_file = project_dir / ".
|
|
107
|
+
"""Poll for .bikerack-port file, return port number."""
|
|
108
|
+
port_file = project_dir / ".bikerack-port"
|
|
109
109
|
deadline = time.monotonic() + timeout
|
|
110
110
|
|
|
111
111
|
while True:
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Portrait path resolution for BikeRack TUI.
|
|
2
|
+
|
|
3
|
+
Resolves persona portrait image paths using the canonical theme discovery
|
|
4
|
+
from ``pennyfarthing_scripts.common.themes``. Each theme directory that
|
|
5
|
+
contains ``themes/{name}.yaml`` has a sibling ``portraits/{name}/`` with
|
|
6
|
+
size-bucketed portrait images.
|
|
7
|
+
|
|
8
|
+
Story 110-3: Portrait image header with textual-image.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _to_slug(name: str) -> str:
|
|
20
|
+
"""Convert a name to URL-safe slug (lowercase kebab-case)."""
|
|
21
|
+
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _extract_agent_slug(theme_yaml: Path, agent: str) -> str | None:
|
|
25
|
+
"""Extract portrait slug (shortName-OCEAN) from a theme YAML file."""
|
|
26
|
+
if not theme_yaml.exists():
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
data = yaml.safe_load(theme_yaml.read_text())
|
|
30
|
+
agent_data = (data or {}).get("agents", {}).get(agent)
|
|
31
|
+
if not agent_data:
|
|
32
|
+
return None
|
|
33
|
+
short_name = agent_data.get("shortName") or (
|
|
34
|
+
agent_data.get("character", "").split()[0] if agent_data.get("character") else None
|
|
35
|
+
)
|
|
36
|
+
ocean = agent_data.get("ocean", {})
|
|
37
|
+
if short_name and all(k in ocean for k in "OCEAN"):
|
|
38
|
+
return f"{_to_slug(short_name)}-{ocean['O']}{ocean['C']}{ocean['E']}{ocean['A']}{ocean['N']}"
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _find_portrait(portraits_theme_dir: Path, slug: str) -> Path | None:
|
|
45
|
+
"""Find a portrait file matching the slug in a theme's portrait directory."""
|
|
46
|
+
if not portraits_theme_dir.is_dir():
|
|
47
|
+
return None
|
|
48
|
+
for size in ["medium", "large", "small", "original"]:
|
|
49
|
+
size_dir = portraits_theme_dir / size
|
|
50
|
+
if size_dir.is_dir():
|
|
51
|
+
for f in size_dir.iterdir():
|
|
52
|
+
if f.name.lower().startswith(slug.lower()) and f.suffix in (".png", ".jpg"):
|
|
53
|
+
return f
|
|
54
|
+
# Fallback to root of theme dir
|
|
55
|
+
for f in portraits_theme_dir.iterdir():
|
|
56
|
+
if f.is_file() and f.name.lower().startswith(slug.lower()) and f.suffix in (".png", ".jpg"):
|
|
57
|
+
return f
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_portrait_path(
|
|
62
|
+
theme: str, agent: str, project_root: Path | None = None
|
|
63
|
+
) -> Path | None:
|
|
64
|
+
"""Resolve the full path to a portrait image.
|
|
65
|
+
|
|
66
|
+
Uses ``discover_all_theme_dirs`` from ``common.themes`` to search core
|
|
67
|
+
themes, installed theme packages, monorepo workspace packages, and
|
|
68
|
+
custom themes — in canonical priority order.
|
|
69
|
+
|
|
70
|
+
For each theme directory the portrait sibling is derived:
|
|
71
|
+
- ``.pennyfarthing/personas/themes/`` → ``.pennyfarthing/personas/portraits/``
|
|
72
|
+
- ``themes-*/themes/`` → ``themes-*/portraits/``
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
theme: Theme name (e.g., 'hogans-heroes', 'monty-python')
|
|
76
|
+
agent: Agent role (e.g., 'sm', 'tea', 'dev')
|
|
77
|
+
project_root: Project root for path resolution. Defaults to cwd.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Path to portrait file, or None if not found.
|
|
81
|
+
"""
|
|
82
|
+
from pennyfarthing_scripts.common.themes import discover_all_theme_dirs
|
|
83
|
+
|
|
84
|
+
theme_dirs = discover_all_theme_dirs(project_root)
|
|
85
|
+
|
|
86
|
+
# Resolve slug from the first theme dir that has this theme's YAML
|
|
87
|
+
slug: str | None = None
|
|
88
|
+
for themes_dir in theme_dirs:
|
|
89
|
+
theme_yaml = themes_dir / f"{theme}.yaml"
|
|
90
|
+
slug = _extract_agent_slug(theme_yaml, agent)
|
|
91
|
+
if slug:
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if not slug:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Search portrait directories (sibling of each themes dir)
|
|
98
|
+
for themes_dir in theme_dirs:
|
|
99
|
+
portraits_dir = themes_dir.parent / "portraits" / theme
|
|
100
|
+
result = _find_portrait(portraits_dir, slug)
|
|
101
|
+
if result:
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def detect_image_protocol() -> str | None:
|
|
108
|
+
"""Detect the best available terminal image protocol.
|
|
109
|
+
|
|
110
|
+
Uses environment variables for reliable detection since subprocess
|
|
111
|
+
stdout may not be a TTY (e.g., when launched via Claude Code).
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Protocol name ('kitty', 'sixel', 'halfcell', None for unsupported).
|
|
115
|
+
"""
|
|
116
|
+
import os
|
|
117
|
+
|
|
118
|
+
# Kitty: TERM=xterm-kitty or KITTY_WINDOW_ID present
|
|
119
|
+
term = os.environ.get("TERM", "")
|
|
120
|
+
if "kitty" in term or os.environ.get("KITTY_WINDOW_ID"):
|
|
121
|
+
return "kitty"
|
|
122
|
+
|
|
123
|
+
# Sixel: some terminals advertise via TERM or COLORTERM
|
|
124
|
+
# WezTerm, foot, mlterm support sixel
|
|
125
|
+
term_program = os.environ.get("TERM_PROGRAM", "")
|
|
126
|
+
if term_program.lower() in ("wezterm", "foot", "mlterm"):
|
|
127
|
+
return "sixel"
|
|
128
|
+
|
|
129
|
+
# Fallback: try textual-image's cell size probe for halfcell baseline
|
|
130
|
+
try:
|
|
131
|
+
from textual_image._terminal import get_cell_size
|
|
132
|
+
|
|
133
|
+
cell_size = get_cell_size()
|
|
134
|
+
if cell_size and cell_size.width > 0:
|
|
135
|
+
return "halfcell"
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return None
|