@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
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for Pennyfarthing Claude Code hooks.
|
|
3
|
+
|
|
4
|
+
Provides common functionality for all hooks:
|
|
5
|
+
- Project root detection
|
|
6
|
+
- Port file discovery
|
|
7
|
+
- Settings loading (relay_mode, permission_mode)
|
|
8
|
+
- Context state checking
|
|
9
|
+
- HTTP communication with Cyclist
|
|
10
|
+
|
|
11
|
+
All hooks should import from this module for consistency.
|
|
12
|
+
|
|
13
|
+
Story: MSSCI-12409 - Hook consistency and relay mode compatibility
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# Port File Constants
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
# WheelHub port file - central coordination server for all communication
|
|
32
|
+
# Per ADR-0004: "the hub where all communication converges"
|
|
33
|
+
CYCLIST_PORT_FILE = ".bikerack-port"
|
|
34
|
+
|
|
35
|
+
# Default port if file not found
|
|
36
|
+
DEFAULT_CYCLIST_PORT = 7431
|
|
37
|
+
|
|
38
|
+
# HTTP timeout for Cyclist communication
|
|
39
|
+
HTTP_TIMEOUT_SECONDS = 120
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# Project Root Detection
|
|
44
|
+
# =============================================================================
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_project_root(start_dir: Path | None = None) -> Path | None:
|
|
48
|
+
"""Find the project root by looking for marker files.
|
|
49
|
+
|
|
50
|
+
Searches for (in order):
|
|
51
|
+
1. .bikerack-port (WheelHub is running)
|
|
52
|
+
2. .pennyfarthing directory
|
|
53
|
+
3. .claude directory
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
start_dir: Directory to start search from (defaults to cwd)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Path to project root, or None if not found
|
|
60
|
+
"""
|
|
61
|
+
current = Path(start_dir) if start_dir else Path.cwd()
|
|
62
|
+
current = current.resolve()
|
|
63
|
+
|
|
64
|
+
while current != current.parent:
|
|
65
|
+
# Check for Cyclist port files first (indicates Cyclist is running)
|
|
66
|
+
if (current / CYCLIST_PORT_FILE).exists():
|
|
67
|
+
return current
|
|
68
|
+
# Fall back to directory markers
|
|
69
|
+
if (current / ".pennyfarthing").is_dir():
|
|
70
|
+
return current
|
|
71
|
+
if (current / ".claude").is_dir():
|
|
72
|
+
return current
|
|
73
|
+
current = current.parent
|
|
74
|
+
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# =============================================================================
|
|
79
|
+
# Port File Reading
|
|
80
|
+
# =============================================================================
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def read_port_file(file_name: str, project_root: Path | None = None) -> int | None:
|
|
84
|
+
"""Read a port number from a Cyclist port file.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
file_name: Name of the port file (e.g. .bikerack-port)
|
|
88
|
+
project_root: Project root directory (auto-detected if not provided)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Port number, or None if file not found or invalid
|
|
92
|
+
"""
|
|
93
|
+
root = project_root or find_project_root()
|
|
94
|
+
if not root:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
port_file = root / file_name
|
|
98
|
+
if not port_file.exists():
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
content = port_file.read_text().strip()
|
|
103
|
+
port = int(content)
|
|
104
|
+
if 0 < port < 65536:
|
|
105
|
+
return port
|
|
106
|
+
except (ValueError, OSError):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_cyclist_port(project_root: Path | None = None) -> int:
|
|
113
|
+
"""Get the WheelHub server port.
|
|
114
|
+
|
|
115
|
+
WheelHub is the central coordination server for all Cyclist communication,
|
|
116
|
+
including hook requests, OTEL, REST APIs, and WebSocket.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
project_root: Project root directory (auto-detected if not provided)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Port number (default if file not found)
|
|
123
|
+
"""
|
|
124
|
+
port = read_port_file(CYCLIST_PORT_FILE, project_root)
|
|
125
|
+
if port:
|
|
126
|
+
return port
|
|
127
|
+
|
|
128
|
+
return DEFAULT_CYCLIST_PORT
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# =============================================================================
|
|
132
|
+
# Settings Loading
|
|
133
|
+
# =============================================================================
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class CyclistSettings:
|
|
138
|
+
"""Cyclist workflow settings from config.local.yaml."""
|
|
139
|
+
|
|
140
|
+
permission_mode: str = "manual" # plan, manual, accept
|
|
141
|
+
relay_mode: bool = False
|
|
142
|
+
bell_mode: bool = False
|
|
143
|
+
git_monitor: bool = False
|
|
144
|
+
theme: str | None = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_settings(project_root: Path | None = None) -> CyclistSettings:
|
|
148
|
+
"""Load Cyclist settings from .pennyfarthing/config.local.yaml.
|
|
149
|
+
|
|
150
|
+
Handles legacy setting migrations:
|
|
151
|
+
- permission_mode: 'turbo' -> 'accept' + relay_mode: True
|
|
152
|
+
- handoff_mode: 'auto' -> relay_mode: True
|
|
153
|
+
- auto_handoff: True -> relay_mode: True
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
project_root: Project root directory (auto-detected if not provided)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
CyclistSettings with current configuration
|
|
160
|
+
"""
|
|
161
|
+
settings = CyclistSettings()
|
|
162
|
+
|
|
163
|
+
root = project_root or find_project_root()
|
|
164
|
+
if not root:
|
|
165
|
+
return settings
|
|
166
|
+
|
|
167
|
+
config_path = root / ".pennyfarthing" / "config.local.yaml"
|
|
168
|
+
if not config_path.exists():
|
|
169
|
+
return settings
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
with open(config_path) as f:
|
|
173
|
+
config = yaml.safe_load(f) or {}
|
|
174
|
+
except (OSError, yaml.YAMLError):
|
|
175
|
+
return settings
|
|
176
|
+
|
|
177
|
+
# Extract theme
|
|
178
|
+
settings.theme = config.get("theme")
|
|
179
|
+
|
|
180
|
+
# Extract workflow settings
|
|
181
|
+
workflow = config.get("workflow", {})
|
|
182
|
+
if not isinstance(workflow, dict):
|
|
183
|
+
return settings
|
|
184
|
+
|
|
185
|
+
# Handle permission_mode
|
|
186
|
+
mode = workflow.get("permission_mode", "manual")
|
|
187
|
+
if mode == "turbo":
|
|
188
|
+
# Migrate turbo -> accept + relay_mode
|
|
189
|
+
settings.permission_mode = "accept"
|
|
190
|
+
settings.relay_mode = True
|
|
191
|
+
elif mode in ("plan", "manual", "accept"):
|
|
192
|
+
settings.permission_mode = mode
|
|
193
|
+
else:
|
|
194
|
+
settings.permission_mode = "manual"
|
|
195
|
+
|
|
196
|
+
# Handle explicit relay_mode (overrides migration)
|
|
197
|
+
if "relay_mode" in workflow and isinstance(workflow["relay_mode"], bool):
|
|
198
|
+
settings.relay_mode = workflow["relay_mode"]
|
|
199
|
+
elif not settings.relay_mode:
|
|
200
|
+
# Check legacy settings
|
|
201
|
+
if workflow.get("handoff_mode") == "auto":
|
|
202
|
+
settings.relay_mode = True
|
|
203
|
+
elif workflow.get("auto_handoff") is True:
|
|
204
|
+
settings.relay_mode = True
|
|
205
|
+
|
|
206
|
+
# Handle bell_mode
|
|
207
|
+
if "bell_mode" in workflow and isinstance(workflow["bell_mode"], bool):
|
|
208
|
+
settings.bell_mode = workflow["bell_mode"]
|
|
209
|
+
|
|
210
|
+
# Handle git_monitor
|
|
211
|
+
if "git_monitor" in workflow and isinstance(workflow["git_monitor"], bool):
|
|
212
|
+
settings.git_monitor = workflow["git_monitor"]
|
|
213
|
+
|
|
214
|
+
return settings
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def is_relay_mode_enabled(project_root: Path | None = None) -> bool:
|
|
218
|
+
"""Check if relay mode (auto-handoff) is enabled.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
project_root: Project root directory (auto-detected if not provided)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if relay mode is enabled
|
|
225
|
+
"""
|
|
226
|
+
return load_settings(project_root).relay_mode
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def is_bell_mode_enabled(project_root: Path | None = None) -> bool:
|
|
230
|
+
"""Check if bell mode is enabled.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
project_root: Project root directory (auto-detected if not provided)
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if bell mode is enabled
|
|
237
|
+
"""
|
|
238
|
+
return load_settings(project_root).bell_mode
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# =============================================================================
|
|
242
|
+
# Context State
|
|
243
|
+
# =============================================================================
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class ContextState:
|
|
248
|
+
"""Current context usage state."""
|
|
249
|
+
|
|
250
|
+
used_tokens: int = 0
|
|
251
|
+
max_tokens: int = 200000
|
|
252
|
+
percentage: float = 0.0
|
|
253
|
+
is_high: bool = False # > 60%
|
|
254
|
+
is_critical: bool = False # > 80%
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_context_state(project_root: Path | None = None) -> ContextState:
|
|
258
|
+
"""Get current context usage from Cyclist API.
|
|
259
|
+
|
|
260
|
+
Calls Cyclist's /api/context endpoint which runs check-context.sh.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
project_root: Project root directory (auto-detected if not provided)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
ContextState with current usage (defaults if Cyclist not running)
|
|
267
|
+
"""
|
|
268
|
+
state = ContextState()
|
|
269
|
+
|
|
270
|
+
port = get_cyclist_port(project_root)
|
|
271
|
+
url = f"http://127.0.0.1:{port}/api/context"
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
with urllib.request.urlopen(url, timeout=5) as response:
|
|
275
|
+
data = json.loads(response.read().decode())
|
|
276
|
+
state.used_tokens = data.get("used_tokens", 0)
|
|
277
|
+
state.max_tokens = data.get("max_tokens", 200000)
|
|
278
|
+
if state.max_tokens > 0:
|
|
279
|
+
state.percentage = (state.used_tokens / state.max_tokens) * 100
|
|
280
|
+
state.is_high = state.percentage > 60
|
|
281
|
+
state.is_critical = state.percentage > 80
|
|
282
|
+
except (urllib.error.URLError, json.JSONDecodeError, OSError):
|
|
283
|
+
# Cyclist not running or error - return defaults
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
return state
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# =============================================================================
|
|
290
|
+
# Cyclist HTTP Communication
|
|
291
|
+
# =============================================================================
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def send_to_cyclist(
|
|
295
|
+
endpoint: str,
|
|
296
|
+
data: dict[str, Any],
|
|
297
|
+
port: int | None = None,
|
|
298
|
+
project_root: Path | None = None,
|
|
299
|
+
timeout: int = HTTP_TIMEOUT_SECONDS,
|
|
300
|
+
) -> dict[str, Any] | None:
|
|
301
|
+
"""Send a POST request to WheelHub (Cyclist's central coordination server).
|
|
302
|
+
|
|
303
|
+
All endpoints go through WheelHub per ADR-0004.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
endpoint: API endpoint path (e.g., "/api/hook-request")
|
|
307
|
+
data: JSON data to send
|
|
308
|
+
port: Port to use (auto-detected if not provided)
|
|
309
|
+
project_root: Project root for port discovery
|
|
310
|
+
timeout: Request timeout in seconds
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Response JSON as dict, or None on error
|
|
314
|
+
"""
|
|
315
|
+
if port is None:
|
|
316
|
+
port = get_cyclist_port(project_root)
|
|
317
|
+
|
|
318
|
+
url = f"http://127.0.0.1:{port}{endpoint}"
|
|
319
|
+
json_data = json.dumps(data).encode("utf-8")
|
|
320
|
+
|
|
321
|
+
request = urllib.request.Request(
|
|
322
|
+
url,
|
|
323
|
+
data=json_data,
|
|
324
|
+
headers={"Content-Type": "application/json"},
|
|
325
|
+
method="POST",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
330
|
+
return json.loads(response.read().decode())
|
|
331
|
+
except urllib.error.URLError as e:
|
|
332
|
+
# Connection refused means Cyclist isn't running
|
|
333
|
+
if "Connection refused" in str(e):
|
|
334
|
+
return None
|
|
335
|
+
raise
|
|
336
|
+
except (json.JSONDecodeError, OSError):
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# =============================================================================
|
|
341
|
+
# Hook Response Formatting
|
|
342
|
+
# =============================================================================
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass
|
|
346
|
+
class HookResponse:
|
|
347
|
+
"""Standard hook response for Claude Code."""
|
|
348
|
+
|
|
349
|
+
event_name: str
|
|
350
|
+
decision: str | None = None # allow, deny, ask (for PreToolUse)
|
|
351
|
+
reason: str | None = None
|
|
352
|
+
updated_input: dict[str, Any] | None = None
|
|
353
|
+
additional_context: str | None = None # For PostToolUse context injection
|
|
354
|
+
|
|
355
|
+
def to_json(self) -> str:
|
|
356
|
+
"""Format as Claude Code hook JSON output."""
|
|
357
|
+
output: dict[str, Any] = {
|
|
358
|
+
"hookSpecificOutput": {
|
|
359
|
+
"hookEventName": self.event_name,
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
hook_output = output["hookSpecificOutput"]
|
|
364
|
+
|
|
365
|
+
if self.decision:
|
|
366
|
+
hook_output["permissionDecision"] = self.decision
|
|
367
|
+
if self.reason:
|
|
368
|
+
hook_output["permissionDecisionReason"] = self.reason
|
|
369
|
+
if self.updated_input:
|
|
370
|
+
hook_output["updatedInput"] = self.updated_input
|
|
371
|
+
if self.additional_context:
|
|
372
|
+
hook_output["additionalContext"] = self.additional_context
|
|
373
|
+
|
|
374
|
+
return json.dumps(output)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def output_hook_response(response: HookResponse) -> None:
|
|
378
|
+
"""Output hook response to stdout for Claude Code."""
|
|
379
|
+
print(response.to_json())
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def read_stdin_json() -> dict[str, Any]:
|
|
383
|
+
"""Read JSON from stdin (hook input from Claude Code).
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Parsed JSON as dict
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
ValueError: If input is not valid JSON
|
|
390
|
+
"""
|
|
391
|
+
data = sys.stdin.read()
|
|
392
|
+
try:
|
|
393
|
+
return json.loads(data)
|
|
394
|
+
except json.JSONDecodeError as e:
|
|
395
|
+
raise ValueError(f"Invalid JSON input: {e}") from e
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# =============================================================================
|
|
399
|
+
# Hook Execution Utilities
|
|
400
|
+
# =============================================================================
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def is_cyclist_running(project_root: Path | None = None) -> bool:
|
|
404
|
+
"""Check if Cyclist server is running.
|
|
405
|
+
|
|
406
|
+
Checks the CYCLIST environment variable set by ClaudeService when
|
|
407
|
+
spawning Claude inside Cyclist. No file I/O, no HTTP, no signals —
|
|
408
|
+
this runs on every tool invocation and must be instant.
|
|
409
|
+
|
|
410
|
+
The project_root parameter is kept for backward compatibility but
|
|
411
|
+
is no longer used.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
True if running inside a Cyclist-spawned Claude process
|
|
415
|
+
"""
|
|
416
|
+
return os.environ.get("CYCLIST") == "1"
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def should_auto_approve(settings: CyclistSettings) -> bool:
|
|
420
|
+
"""Check if requests should be auto-approved based on settings.
|
|
421
|
+
|
|
422
|
+
Auto-approve when permission_mode is 'accept' (formerly turbo).
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
settings: Current Cyclist settings
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
True if auto-approval is enabled
|
|
429
|
+
"""
|
|
430
|
+
return settings.permission_mode == "accept"
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def should_auto_handoff(settings: CyclistSettings) -> bool:
|
|
434
|
+
"""Check if handoffs should be automatic based on settings.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
settings: Current Cyclist settings
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
True if relay_mode is enabled
|
|
441
|
+
"""
|
|
442
|
+
return settings.relay_mode
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostToolUse Hook — Bell Mode + Tandem Injection.
|
|
3
|
+
|
|
4
|
+
Called by Claude Code after each tool execution. Handles two independent
|
|
5
|
+
injection systems:
|
|
6
|
+
|
|
7
|
+
1. Bell queue (Cyclist only) — injects queued user messages when Cyclist
|
|
8
|
+
is running and bell_mode is enabled. In CLI sessions this is a no-op.
|
|
9
|
+
2. Tandem observations (always active) — injects backseat agent observations
|
|
10
|
+
when tandem observation files exist. No configuration required.
|
|
11
|
+
|
|
12
|
+
Bell queue takes precedence: if a queued message exists, tandem is
|
|
13
|
+
deferred to the next hook invocation.
|
|
14
|
+
|
|
15
|
+
Consolidates bellmode_hook.py into the hooks subpackage.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from pennyfarthing_scripts.hooks import (
|
|
26
|
+
CYCLIST_PORT_FILE,
|
|
27
|
+
HookResponse,
|
|
28
|
+
find_project_root,
|
|
29
|
+
is_bell_mode_enabled,
|
|
30
|
+
output_hook_response,
|
|
31
|
+
read_port_file,
|
|
32
|
+
send_to_cyclist,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Bell Queue
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_bell_queue(project_root: Path) -> list[dict]:
|
|
41
|
+
queue_path = project_root / ".pennyfarthing" / "bell-queue.json"
|
|
42
|
+
if not queue_path.exists():
|
|
43
|
+
return []
|
|
44
|
+
try:
|
|
45
|
+
with open(queue_path) as f:
|
|
46
|
+
queue = json.load(f)
|
|
47
|
+
if isinstance(queue, list):
|
|
48
|
+
return queue
|
|
49
|
+
except (json.JSONDecodeError, OSError):
|
|
50
|
+
pass
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _dequeue_message(project_root: Path) -> None:
|
|
55
|
+
queue_path = project_root / ".pennyfarthing" / "bell-queue.json"
|
|
56
|
+
if not queue_path.exists():
|
|
57
|
+
return
|
|
58
|
+
try:
|
|
59
|
+
with open(queue_path) as f:
|
|
60
|
+
queue = json.load(f)
|
|
61
|
+
if isinstance(queue, list) and len(queue) > 0:
|
|
62
|
+
queue = queue[1:]
|
|
63
|
+
with open(queue_path, "w") as f:
|
|
64
|
+
json.dump(queue, f)
|
|
65
|
+
except (json.JSONDecodeError, OSError):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _notify_cyclist(project_root: Path, message_text: str) -> None:
|
|
70
|
+
try:
|
|
71
|
+
send_to_cyclist(
|
|
72
|
+
endpoint="/api/bell-consumed",
|
|
73
|
+
data={"text": message_text},
|
|
74
|
+
project_root=project_root,
|
|
75
|
+
timeout=5,
|
|
76
|
+
)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# =============================================================================
|
|
82
|
+
# Tandem Observations
|
|
83
|
+
# =============================================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _read_tandem_observations(project_root: Path) -> list[Path]:
|
|
87
|
+
session_dir = project_root / ".session"
|
|
88
|
+
if not session_dir.is_dir():
|
|
89
|
+
return []
|
|
90
|
+
return sorted(session_dir.glob("*-tandem-*.md"))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_latest_observation(file_content: str) -> dict | None:
|
|
94
|
+
persona_match = re.search(r"\*\*Observer:\*\*\s*\w+\s*\(([^)]+)\)", file_content)
|
|
95
|
+
persona = persona_match.group(1) if persona_match else "Unknown"
|
|
96
|
+
|
|
97
|
+
entries = re.split(r"## \[\d{1,2}:\d{2}\] Observation\n", file_content)
|
|
98
|
+
if len(entries) < 2:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
last_entry = entries[-1].strip()
|
|
102
|
+
last_entry = re.sub(r"\n---\s*$", "", last_entry).strip()
|
|
103
|
+
lines = last_entry.split("\n")
|
|
104
|
+
text_lines = [line for line in lines if not line.startswith("**Trigger:**")]
|
|
105
|
+
text = "\n".join(text_lines).strip()
|
|
106
|
+
|
|
107
|
+
if not text:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
return {"persona": persona, "text": text}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _get_tandem_mtime(project_root: Path, agent: str) -> float:
|
|
114
|
+
sidecar = project_root / ".session" / f".tandem-mtime-{agent}"
|
|
115
|
+
if not sidecar.exists():
|
|
116
|
+
return 0.0
|
|
117
|
+
try:
|
|
118
|
+
return float(sidecar.read_text().strip())
|
|
119
|
+
except (ValueError, OSError):
|
|
120
|
+
return 0.0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _save_tandem_mtime(project_root: Path, agent: str, mtime: float) -> None:
|
|
124
|
+
sidecar = project_root / ".session" / f".tandem-mtime-{agent}"
|
|
125
|
+
try:
|
|
126
|
+
sidecar.write_text(str(mtime))
|
|
127
|
+
except OSError:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _check_tandem_files(project_root: Path) -> list[dict]:
|
|
132
|
+
tandem_files = _read_tandem_observations(project_root)
|
|
133
|
+
if not tandem_files:
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
results = []
|
|
137
|
+
for obs_file in tandem_files:
|
|
138
|
+
agent_match = re.search(r"-tandem-(\w+)\.md$", obs_file.name)
|
|
139
|
+
if not agent_match:
|
|
140
|
+
continue
|
|
141
|
+
agent = agent_match.group(1)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
file_mtime = obs_file.stat().st_mtime
|
|
145
|
+
except OSError:
|
|
146
|
+
continue
|
|
147
|
+
saved_mtime = _get_tandem_mtime(project_root, agent)
|
|
148
|
+
if file_mtime == saved_mtime:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
content = obs_file.read_text()
|
|
153
|
+
except OSError:
|
|
154
|
+
continue
|
|
155
|
+
obs = _get_latest_observation(content)
|
|
156
|
+
if not obs:
|
|
157
|
+
_save_tandem_mtime(project_root, agent, file_mtime)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
message = f"[Tandem] {obs['persona']}: {obs['text']}"
|
|
161
|
+
results.append({"agent": agent, "message": message})
|
|
162
|
+
_save_tandem_mtime(project_root, agent, file_mtime)
|
|
163
|
+
|
|
164
|
+
return results
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# =============================================================================
|
|
168
|
+
# Entry Point
|
|
169
|
+
# =============================================================================
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main() -> None:
|
|
173
|
+
"""Main entry point for PostToolUse hook."""
|
|
174
|
+
try:
|
|
175
|
+
# Read and discard stdin (required by hook protocol)
|
|
176
|
+
sys.stdin.read()
|
|
177
|
+
|
|
178
|
+
project_root = find_project_root()
|
|
179
|
+
if not project_root:
|
|
180
|
+
sys.exit(0)
|
|
181
|
+
|
|
182
|
+
# --- Bell queue (Cyclist only, requires bell_mode: true) ---
|
|
183
|
+
is_cyclist = read_port_file(CYCLIST_PORT_FILE, project_root) is not None
|
|
184
|
+
if is_cyclist and is_bell_mode_enabled(project_root):
|
|
185
|
+
queue = _read_bell_queue(project_root)
|
|
186
|
+
if queue:
|
|
187
|
+
first_message = queue[0]
|
|
188
|
+
message_text = first_message.get("text", "")
|
|
189
|
+
if message_text:
|
|
190
|
+
output_hook_response(HookResponse(
|
|
191
|
+
event_name="PostToolUse",
|
|
192
|
+
additional_context=f"User feedback: {message_text}",
|
|
193
|
+
))
|
|
194
|
+
_dequeue_message(project_root)
|
|
195
|
+
_notify_cyclist(project_root, message_text)
|
|
196
|
+
sys.exit(0)
|
|
197
|
+
|
|
198
|
+
# --- Tandem observations (always active) ---
|
|
199
|
+
tandem_results = _check_tandem_files(project_root)
|
|
200
|
+
if tandem_results:
|
|
201
|
+
output_hook_response(HookResponse(
|
|
202
|
+
event_name="PostToolUse",
|
|
203
|
+
additional_context=tandem_results[0]["message"],
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
sys.exit(0)
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
print(f"[bellmode-hook] Error: {e}", file=sys.stderr)
|
|
210
|
+
sys.exit(0)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
main()
|