@pennyfarthing/core 10.0.5 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -22
- package/package.json +17 -11
- package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor-legacy.test.js +24 -0
- package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +346 -13
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/e2e-fresh-install.test.js +1 -1
- package/packages/core/dist/cli/commands/e2e-fresh-install.test.js.map +1 -1
- package/packages/core/dist/cli/commands/e2e-upgrade.test.js +1 -1
- package/packages/core/dist/cli/commands/e2e-upgrade.test.js.map +1 -1
- package/packages/core/dist/cli/commands/hooks-consolidation.test.js +2 -2
- package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -1
- package/packages/core/dist/cli/commands/init-consolidation.test.js.map +1 -1
- package/packages/core/dist/cli/commands/init.d.ts +7 -0
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +41 -8
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/uninstall.js +24 -13
- package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
- package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
- package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +26 -0
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/index.js +1 -1
- package/packages/core/dist/cli/index.js.map +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
- package/packages/core/dist/cli/theme-maker.test.js +64 -115
- package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
- package/packages/core/dist/cli/utils/python.d.ts +22 -0
- package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/python.js +102 -0
- package/packages/core/dist/cli/utils/python.js.map +1 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +10 -0
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/index.d.ts +1 -1
- package/packages/core/dist/index.d.ts.map +1 -1
- package/packages/core/dist/index.js +2 -2
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
- package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
- package/packages/core/dist/plugins/plugin-discovery.js +165 -0
- package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
- package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
- package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
- package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
- package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
- package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
- package/packages/core/dist/workflow/context-watch.d.ts +80 -0
- package/packages/core/dist/workflow/context-watch.d.ts.map +1 -0
- package/packages/core/dist/workflow/context-watch.js +235 -0
- package/packages/core/dist/workflow/context-watch.js.map +1 -0
- package/packages/core/dist/workflow/context-watch.test.d.ts +1 -0
- package/packages/core/dist/workflow/context-watch.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/context-watch.test.js +746 -0
- package/packages/core/dist/workflow/context-watch.test.js.map +1 -0
- package/packages/core/dist/workflow/file-watch.d.ts +82 -0
- package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
- package/packages/core/dist/workflow/file-watch.js +198 -0
- package/packages/core/dist/workflow/file-watch.js.map +1 -0
- package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
- package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/file-watch.test.js +469 -0
- package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
- package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
- package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
- package/packages/core/dist/workflow/observation-writer.js +97 -0
- package/packages/core/dist/workflow/observation-writer.js.map +1 -0
- package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
- package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/observation-writer.test.js +424 -0
- package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
- package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
- package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
- package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
- package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
- package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
- package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
- package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
- package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
- package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
- package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
- package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
- package/packages/core/dist/workflow/tool-watch.js +166 -0
- package/packages/core/dist/workflow/tool-watch.js.map +1 -0
- package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
- package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/tool-watch.test.js +718 -0
- package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
- package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
- package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
- package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.js +44 -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 +192 -0
- package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
- package/pennyfarthing-dist/agents/README.md +1 -3
- package/pennyfarthing-dist/agents/architect.md +0 -6
- package/pennyfarthing-dist/agents/devops.md +0 -6
- package/pennyfarthing-dist/agents/handoff.md +18 -3
- package/pennyfarthing-dist/agents/orchestrator.md +0 -6
- package/pennyfarthing-dist/agents/pm.md +0 -6
- package/pennyfarthing-dist/agents/sm-finish.md +1 -1
- package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
- package/pennyfarthing-dist/agents/sm.md +11 -11
- package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
- package/pennyfarthing-dist/commands/architect.md +11 -3
- package/pennyfarthing-dist/commands/close-epic.md +24 -131
- package/pennyfarthing-dist/commands/create-theme.md +14 -24
- package/pennyfarthing-dist/commands/dev.md +11 -3
- package/pennyfarthing-dist/commands/devops.md +11 -3
- package/pennyfarthing-dist/commands/health-check.md +1 -3
- package/pennyfarthing-dist/commands/help.md +8 -12
- package/pennyfarthing-dist/commands/list-themes.md +14 -16
- package/pennyfarthing-dist/commands/orchestrator.md +11 -3
- package/pennyfarthing-dist/commands/parallel-work.md +1 -3
- package/pennyfarthing-dist/commands/pm.md +11 -3
- package/pennyfarthing-dist/commands/prime.md +6 -6
- package/pennyfarthing-dist/commands/reviewer.md +11 -3
- package/pennyfarthing-dist/commands/run-ci.md +1 -1
- package/pennyfarthing-dist/commands/set-theme.md +14 -51
- package/pennyfarthing-dist/commands/setup.md +5 -1
- package/pennyfarthing-dist/commands/show-theme.md +14 -16
- package/pennyfarthing-dist/commands/sm.md +11 -3
- package/pennyfarthing-dist/commands/tea.md +11 -3
- package/pennyfarthing-dist/commands/tech-writer.md +11 -3
- package/pennyfarthing-dist/commands/theme-maker.md +14 -671
- package/pennyfarthing-dist/commands/theme.md +95 -0
- package/pennyfarthing-dist/commands/ux-designer.md +11 -3
- package/pennyfarthing-dist/commands/work.md +3 -5
- package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
- package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
- package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
- package/pennyfarthing-dist/guides/bikelane.md +3 -2
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
- package/pennyfarthing-dist/guides/hooks.md +5 -5
- package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
- package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
- package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
- package/pennyfarthing-dist/guides/prime.md +2 -2
- package/pennyfarthing-dist/guides/scale-levels.md +4 -6
- package/pennyfarthing-dist/guides/skill-schema.md +4 -4
- package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
- package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/fifth-element.yaml +295 -0
- package/pennyfarthing-dist/scripts/README.md +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
- package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
- package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
- package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
- package/pennyfarthing-dist/scripts/git/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +32 -15
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/README.md +1 -1
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/README.md +1 -1
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
- package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -144
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
- package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
- package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +21 -17
- package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
- package/pennyfarthing-dist/skills/story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/story/scripts/story-template.sh +0 -0
- package/pennyfarthing-dist/skills/theme/skill.md +290 -75
- package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
- package/pennyfarthing-dist/skills/workflow/scripts/list-workflows.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/scripts/resume-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/scripts/show-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/scripts/start-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/scripts/workflow-status.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/skill.md +27 -4
- package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
- package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
- package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
- package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
- package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +41 -24
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
- 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__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bellmode_hook.py +202 -47
- package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
- package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/cli.py +0 -1
- package/pennyfarthing_scripts/brownfield/discover.py +1 -2
- package/pennyfarthing_scripts/cli.py +23 -3
- package/pennyfarthing_scripts/codemarkers/__init__.py +23 -0
- package/pennyfarthing_scripts/codemarkers/__main__.py +6 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/analyze.py +501 -0
- package/pennyfarthing_scripts/codemarkers/cli.py +179 -0
- package/pennyfarthing_scripts/codemarkers/formatters.py +88 -0
- package/pennyfarthing_scripts/codemarkers/models.py +60 -0
- package/pennyfarthing_scripts/common/__init__.py +8 -9
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +1 -1
- package/pennyfarthing_scripts/complexity/__init__.py +15 -0
- package/pennyfarthing_scripts/complexity/__main__.py +6 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/analyze.py +207 -0
- package/pennyfarthing_scripts/complexity/cli.py +82 -0
- package/pennyfarthing_scripts/complexity/formatters.py +64 -0
- package/pennyfarthing_scripts/complexity/models.py +32 -0
- package/pennyfarthing_scripts/context.py +14 -15
- package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__main__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/analyze.py +322 -0
- package/pennyfarthing_scripts/deadcode/cli.py +163 -0
- package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
- package/pennyfarthing_scripts/deadcode/models.py +54 -0
- package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
- package/pennyfarthing_scripts/dependencies/__main__.py +5 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/analyze.py +155 -0
- package/pennyfarthing_scripts/dependencies/cli.py +76 -0
- package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
- package/pennyfarthing_scripts/dependencies/models.py +39 -0
- package/pennyfarthing_scripts/git/__init__.py +5 -5
- 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__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +3 -2
- package/pennyfarthing_scripts/git/status_all.py +1 -1
- package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
- package/pennyfarthing_scripts/healthscore/__main__.py +14 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/analyze.py +591 -0
- package/pennyfarthing_scripts/healthscore/cli.py +80 -0
- package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
- package/pennyfarthing_scripts/healthscore/models.py +43 -0
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/hooks.py +8 -11
- package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
- package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/analyze.py +155 -14
- package/pennyfarthing_scripts/hotspots/cli.py +12 -10
- package/pennyfarthing_scripts/hotspots/models.py +0 -1
- package/pennyfarthing_scripts/jira/__init__.py +15 -17
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +44 -18
- package/pennyfarthing_scripts/jira/claim.py +21 -0
- package/pennyfarthing_scripts/jira/cli.py +6 -3
- package/pennyfarthing_scripts/jira/client.py +32 -4
- package/pennyfarthing_scripts/jira/create.py +45 -1
- package/pennyfarthing_scripts/jira/epic.py +3 -2
- package/pennyfarthing_scripts/jira/reconcile.py +0 -1
- package/pennyfarthing_scripts/jira/story.py +2 -0
- package/pennyfarthing_scripts/jira/sync.py +1 -1
- package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/skill.py +0 -1
- package/pennyfarthing_scripts/migration/step.py +0 -1
- package/pennyfarthing_scripts/migration/validate.py +8 -5
- package/pennyfarthing_scripts/patch_mode.py +2 -2
- package/pennyfarthing_scripts/preflight/__init__.py +1 -1
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/finish.py +0 -1
- package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +5 -1
- package/pennyfarthing_scripts/prime/loader.py +2 -3
- package/pennyfarthing_scripts/prime/persona.py +2 -1
- package/pennyfarthing_scripts/prime/tiers.py +4 -4
- package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
- package/pennyfarthing_scripts/sprint/__init__.py +10 -12
- package/pennyfarthing_scripts/sprint/__main__.py +2 -2
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_add.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/__pycache__/validate_cmd.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +0 -1
- package/pennyfarthing_scripts/sprint/archive_epic.py +198 -97
- package/pennyfarthing_scripts/sprint/cli.py +62 -46
- package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
- package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
- package/pennyfarthing_scripts/sprint/loader.py +6 -0
- package/pennyfarthing_scripts/sprint/status.py +1 -2
- package/pennyfarthing_scripts/sprint/story_add.py +202 -27
- package/pennyfarthing_scripts/sprint/story_finish.py +209 -0
- package/pennyfarthing_scripts/sprint/story_update.py +11 -3
- package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
- package/pennyfarthing_scripts/sprint/validator.py +120 -6
- package/pennyfarthing_scripts/sprint/work.py +28 -7
- package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
- package/pennyfarthing_scripts/story/__init__.py +14 -16
- package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/size.py +0 -1
- package/pennyfarthing_scripts/story/template.py +0 -1
- package/pennyfarthing_scripts/swebench.py +1 -2
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/conftest.py +1 -2
- package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
- package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
- package/pennyfarthing_scripts/tests/test_codemarkers.py +687 -0
- package/pennyfarthing_scripts/tests/test_common.py +9 -4
- package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
- package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
- package/pennyfarthing_scripts/tests/test_healthscore.py +516 -0
- package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
- package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
- package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
- package/pennyfarthing_scripts/tests/test_prime.py +39 -21
- package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
- package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
- package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
- package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
- package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
- package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
- package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
- package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
- package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
- package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
- package/pennyfarthing_scripts/theme/__init__.py +5 -0
- package/pennyfarthing_scripts/theme/__main__.py +6 -0
- package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/cli.py +287 -0
- package/pennyfarthing_scripts/validate/__init__.py +21 -0
- package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
- package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
- package/pennyfarthing_scripts/validate/adapters/skill_command.py +292 -0
- package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
- package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
- package/pennyfarthing_scripts/validate/cli.py +141 -0
- package/pennyfarthing_scripts/welcome_hook.py +2 -3
- package/pennyfarthing_scripts/workflow.py +3 -3
- package/scripts/README.md +41 -0
- package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
- package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
- package/pennyfarthing-dist/commands/benchmark.md +0 -485
- package/pennyfarthing-dist/commands/job-fair.md +0 -102
- package/pennyfarthing-dist/commands/solo.md +0 -447
- package/pennyfarthing-dist/guides/benchmarks.md +0 -62
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
- package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
- package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
- package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
- package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
- package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
- package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
- package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
- package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
- package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
- package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
- package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
- package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
- package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
- package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
- package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
- package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
- package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core health score analysis engine.
|
|
3
|
+
|
|
4
|
+
Aggregates lightweight dimension scores into a composite 0-100 score.
|
|
5
|
+
Supports caching with a configurable TTL (default 5 minutes).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from datetime import UTC
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from pennyfarthing_scripts.healthscore.models import (
|
|
19
|
+
DEFAULT_WEIGHTS,
|
|
20
|
+
DimensionScore,
|
|
21
|
+
HealthscoreResult,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("healthscore")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def analyze_healthscore(
|
|
28
|
+
target_path: Path,
|
|
29
|
+
weights: dict[str, float] | None = None,
|
|
30
|
+
cache_ttl: int = 300,
|
|
31
|
+
) -> HealthscoreResult:
|
|
32
|
+
"""Analyze codebase health across all dimensions.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
target_path: Directory to analyze.
|
|
36
|
+
weights: Custom dimension weights (must sum to 1.0). Uses defaults if None.
|
|
37
|
+
cache_ttl: Cache time-to-live in seconds (default 300 = 5 minutes).
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
HealthscoreResult with composite score and per-dimension breakdown.
|
|
41
|
+
"""
|
|
42
|
+
w = weights if weights is not None else DEFAULT_WEIGHTS
|
|
43
|
+
resolved = target_path.resolve()
|
|
44
|
+
logger.info("[healthscore] Starting analysis for %s", resolved)
|
|
45
|
+
logger.info("[healthscore] Dimensions: %s", list(w.keys()))
|
|
46
|
+
|
|
47
|
+
cache_dir = get_cache_path(resolved)
|
|
48
|
+
any_cached = False
|
|
49
|
+
raw_scores: dict[str, float | None] = {}
|
|
50
|
+
dimensions: list[DimensionScore] = []
|
|
51
|
+
|
|
52
|
+
# Separate cached vs uncached dimensions
|
|
53
|
+
uncached_dims: list[str] = []
|
|
54
|
+
for dim_name in w:
|
|
55
|
+
if cache_ttl > 0:
|
|
56
|
+
cached = read_cached_score(cache_dir, dim_name, cache_ttl)
|
|
57
|
+
if cached is not None:
|
|
58
|
+
logger.info("[healthscore] %s: cached score = %.1f", dim_name, cached)
|
|
59
|
+
raw_scores[dim_name] = cached
|
|
60
|
+
any_cached = True
|
|
61
|
+
continue
|
|
62
|
+
uncached_dims.append(dim_name)
|
|
63
|
+
|
|
64
|
+
logger.info("[healthscore] Uncached dimensions to probe: %s", uncached_dims)
|
|
65
|
+
|
|
66
|
+
# Run all uncached probes concurrently
|
|
67
|
+
if uncached_dims:
|
|
68
|
+
probe_results = await asyncio.gather(
|
|
69
|
+
*(_probe_dimension(name, resolved) for name in uncached_dims)
|
|
70
|
+
)
|
|
71
|
+
for dim_name, score in zip(uncached_dims, probe_results, strict=False):
|
|
72
|
+
raw_scores[dim_name] = score
|
|
73
|
+
logger.info("[healthscore] %s: probed score = %s", dim_name, score)
|
|
74
|
+
if score is not None and cache_ttl > 0:
|
|
75
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
write_cached_score(cache_dir, dim_name, score)
|
|
77
|
+
|
|
78
|
+
# Build dimension list in original weight order
|
|
79
|
+
for dim_name, dim_weight in w.items():
|
|
80
|
+
score = raw_scores.get(dim_name)
|
|
81
|
+
error = f"{dim_name} not available" if score is None else None
|
|
82
|
+
dimensions.append(DimensionScore(
|
|
83
|
+
name=dim_name,
|
|
84
|
+
score=score,
|
|
85
|
+
weight=dim_weight,
|
|
86
|
+
error=error,
|
|
87
|
+
))
|
|
88
|
+
|
|
89
|
+
composite = compute_composite_score(raw_scores, w)
|
|
90
|
+
logger.info("[healthscore] Composite score: %.1f", composite)
|
|
91
|
+
|
|
92
|
+
return HealthscoreResult(
|
|
93
|
+
success=True,
|
|
94
|
+
composite_score=composite,
|
|
95
|
+
target_path=str(resolved),
|
|
96
|
+
dimensions=dimensions,
|
|
97
|
+
cached=any_cached,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def _probe_dimension(name: str, target_path: Path) -> float | None:
|
|
102
|
+
"""Run a lightweight probe for a single dimension.
|
|
103
|
+
|
|
104
|
+
Returns a score 0-100 or None if the dimension cannot be assessed.
|
|
105
|
+
Wires into existing analyzer modules where available.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
probes = {
|
|
109
|
+
"churn": _probe_churn,
|
|
110
|
+
"todo_density": _probe_todo_density,
|
|
111
|
+
"complexity": _probe_complexity,
|
|
112
|
+
"dead_code": _probe_dead_code,
|
|
113
|
+
"dependency_freshness": _probe_dependency_freshness,
|
|
114
|
+
"deprecation_debt": _probe_deprecation_debt,
|
|
115
|
+
"test_gaps": _probe_test_gaps,
|
|
116
|
+
"agent_context_efficiency": _probe_agent_context_efficiency,
|
|
117
|
+
}
|
|
118
|
+
probe_fn = probes.get(name)
|
|
119
|
+
if probe_fn is None:
|
|
120
|
+
logger.warning("[healthscore] No probe registered for dimension: %s", name)
|
|
121
|
+
return None
|
|
122
|
+
logger.info("[healthscore] Running probe: %s", name)
|
|
123
|
+
result = await probe_fn(target_path)
|
|
124
|
+
logger.info("[healthscore] Probe %s returned: %s", name, result)
|
|
125
|
+
return result
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
logger.error("[healthscore] Probe %s failed: %s", name, exc, exc_info=True)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def _probe_churn(target_path: Path) -> float | None:
|
|
132
|
+
"""Score based on code churn — uses PyDriller for smart file filtering.
|
|
133
|
+
|
|
134
|
+
Falls back to existing hotspots analyzer if PyDriller is unavailable.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
return await _probe_churn_pydriller(target_path)
|
|
138
|
+
except ImportError:
|
|
139
|
+
logger.info("[healthscore:churn] PyDriller not available, falling back to hotspots")
|
|
140
|
+
return await _probe_churn_fallback(target_path)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def _probe_churn_pydriller(target_path: Path) -> float | None:
|
|
144
|
+
"""PyDriller-based churn: counts changes per file with noise filtering."""
|
|
145
|
+
from datetime import datetime, timedelta
|
|
146
|
+
|
|
147
|
+
from pydriller import Repository
|
|
148
|
+
|
|
149
|
+
# Files that churn naturally but aren't code quality signals
|
|
150
|
+
noise_patterns = {
|
|
151
|
+
"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
|
|
152
|
+
"tsconfig.json", "pyproject.toml", ".gitignore",
|
|
153
|
+
}
|
|
154
|
+
noise_exts = {
|
|
155
|
+
".md", ".yaml", ".yml", ".json", ".lock", ".toml",
|
|
156
|
+
".png", ".jpg", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot",
|
|
157
|
+
".d.ts", ".snap", ".map",
|
|
158
|
+
}
|
|
159
|
+
noise_dirs = {
|
|
160
|
+
"node_modules", "dist", "build", ".git", "sprint", ".session",
|
|
161
|
+
"docs", ".github", "coverage", "__pycache__",
|
|
162
|
+
}
|
|
163
|
+
code_exts = {".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".rb"}
|
|
164
|
+
|
|
165
|
+
since = datetime.now(UTC) - timedelta(days=90)
|
|
166
|
+
file_changes: dict[str, int] = {}
|
|
167
|
+
|
|
168
|
+
# PyDriller is sync — run in executor to avoid blocking
|
|
169
|
+
def _collect():
|
|
170
|
+
repo = Repository(str(target_path), since=since)
|
|
171
|
+
for commit in repo.traverse_commits():
|
|
172
|
+
for mod in commit.modified_files:
|
|
173
|
+
fpath = mod.new_path or mod.old_path
|
|
174
|
+
if not fpath:
|
|
175
|
+
continue
|
|
176
|
+
# Skip noise files
|
|
177
|
+
fname = fpath.split("/")[-1]
|
|
178
|
+
if fname in noise_patterns:
|
|
179
|
+
continue
|
|
180
|
+
ext = "." + fname.rsplit(".", 1)[-1] if "." in fname else ""
|
|
181
|
+
if ext.lower() in noise_exts:
|
|
182
|
+
continue
|
|
183
|
+
# Skip noise directories
|
|
184
|
+
if any(d in fpath.split("/") for d in noise_dirs):
|
|
185
|
+
continue
|
|
186
|
+
# Only count source code files
|
|
187
|
+
if ext.lower() not in code_exts:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
file_changes[fpath] = file_changes.get(fpath, 0) + 1
|
|
191
|
+
return file_changes
|
|
192
|
+
|
|
193
|
+
loop = asyncio.get_event_loop()
|
|
194
|
+
await loop.run_in_executor(None, _collect)
|
|
195
|
+
|
|
196
|
+
if not file_changes:
|
|
197
|
+
logger.info("[healthscore:churn] No code file changes in 90 days")
|
|
198
|
+
return 100.0 # No churn = perfect score
|
|
199
|
+
|
|
200
|
+
# Score based on top-20 most-churned files
|
|
201
|
+
sorted_files = sorted(file_changes.items(), key=lambda x: x[1], reverse=True)
|
|
202
|
+
top20 = sorted_files[:20]
|
|
203
|
+
max_changes = top20[0][1] if top20 else 1
|
|
204
|
+
|
|
205
|
+
# Normalize: files with many changes score higher (worse churn)
|
|
206
|
+
# Score each file 0-100 based on its change count relative to max
|
|
207
|
+
churn_scores = [(changes / max_changes) * 100.0 for _, changes in top20]
|
|
208
|
+
avg_churn = sum(churn_scores) / len(churn_scores)
|
|
209
|
+
|
|
210
|
+
# Invert: high churn = low health score
|
|
211
|
+
score = max(0.0, min(100.0, 100.0 - avg_churn))
|
|
212
|
+
|
|
213
|
+
logger.info("[healthscore:churn] PyDriller: %d code files changed, top=%s(%d), avg_churn=%.1f, score=%.1f",
|
|
214
|
+
len(file_changes), top20[0][0] if top20 else "?", max_changes, avg_churn, score)
|
|
215
|
+
for f, c in top20[:5]:
|
|
216
|
+
logger.info("[healthscore:churn] %s: %d changes", f, c)
|
|
217
|
+
return score
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def _probe_churn_fallback(target_path: Path) -> float | None:
|
|
221
|
+
"""Fallback churn probe using existing hotspots analyzer."""
|
|
222
|
+
from pennyfarthing_scripts.hotspots.analyze import analyze_repo
|
|
223
|
+
|
|
224
|
+
result = await analyze_repo("project", target_path, days=90)
|
|
225
|
+
if not result.success or not result.file_hotspots:
|
|
226
|
+
logger.info("[healthscore:churn] No hotspot data (success=%s, count=%s)",
|
|
227
|
+
result.success, len(result.file_hotspots) if result.file_hotspots else 0)
|
|
228
|
+
return None
|
|
229
|
+
top = sorted(result.file_hotspots, key=lambda h: h.hotspot_score, reverse=True)[:20]
|
|
230
|
+
avg_hotspot = sum(h.hotspot_score for h in top) / len(top)
|
|
231
|
+
score = max(0.0, min(100.0, 100.0 - avg_hotspot))
|
|
232
|
+
logger.info("[healthscore:churn] fallback: top20 avg=%.1f, score=%.1f", avg_hotspot, score)
|
|
233
|
+
return score
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def _probe_todo_density(target_path: Path) -> float | None:
|
|
237
|
+
"""Score based on TODO/FIXME marker count."""
|
|
238
|
+
from pennyfarthing_scripts.codemarkers.analyze import analyze_repo
|
|
239
|
+
|
|
240
|
+
result = await analyze_repo("project", target_path)
|
|
241
|
+
if not result.success or not result.summary:
|
|
242
|
+
logger.info("[healthscore:todo_density] No marker data (success=%s)", result.success)
|
|
243
|
+
return None
|
|
244
|
+
total = result.summary.total_markers
|
|
245
|
+
logger.info("[healthscore:todo_density] Found %d markers", total)
|
|
246
|
+
if total <= 10:
|
|
247
|
+
score = 95.0
|
|
248
|
+
elif total <= 50:
|
|
249
|
+
score = 90.0 - (total - 10) * (30.0 / 40.0)
|
|
250
|
+
elif total <= 200:
|
|
251
|
+
score = 60.0 - (total - 50) * (30.0 / 150.0)
|
|
252
|
+
elif total <= 1000:
|
|
253
|
+
score = 30.0 - (total - 200) * (25.0 / 800.0)
|
|
254
|
+
else:
|
|
255
|
+
score = max(0.0, 5.0 - (total - 1000) * 0.005)
|
|
256
|
+
logger.info("[healthscore:todo_density] total=%d, score=%.1f", total, score)
|
|
257
|
+
return score
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
async def _probe_complexity(target_path: Path) -> float | None:
|
|
261
|
+
"""Score based on average cyclomatic complexity."""
|
|
262
|
+
from pennyfarthing_scripts.complexity.analyze import analyze_complexity
|
|
263
|
+
|
|
264
|
+
result = await analyze_complexity(target_path)
|
|
265
|
+
if not result.success or not result.files:
|
|
266
|
+
logger.info("[healthscore:complexity] No complexity data (success=%s)", result.success)
|
|
267
|
+
return None
|
|
268
|
+
files_with_fns = [f for f in result.files if f.function_count > 0]
|
|
269
|
+
if not files_with_fns:
|
|
270
|
+
logger.info("[healthscore:complexity] No files with functions found")
|
|
271
|
+
return None
|
|
272
|
+
avg = sum(f.avg_cyclomatic_complexity for f in files_with_fns) / len(files_with_fns)
|
|
273
|
+
if avg <= 2.0:
|
|
274
|
+
score = 95.0
|
|
275
|
+
elif avg <= 5.0:
|
|
276
|
+
score = 90.0 - (avg - 2.0) * (20.0 / 3.0)
|
|
277
|
+
elif avg <= 10.0:
|
|
278
|
+
score = 70.0 - (avg - 5.0) * (30.0 / 5.0)
|
|
279
|
+
else:
|
|
280
|
+
score = max(0.0, 40.0 - (avg - 10.0) * 4.0)
|
|
281
|
+
logger.info("[healthscore:complexity] avg=%.2f, files=%d, score=%.1f", avg, len(files_with_fns), score)
|
|
282
|
+
return score
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
async def _probe_dead_code(target_path: Path) -> float | None:
|
|
286
|
+
"""Score based on unused export count."""
|
|
287
|
+
from pennyfarthing_scripts.deadcode.analyze import find_unused_exports
|
|
288
|
+
|
|
289
|
+
result = await find_unused_exports(target_path)
|
|
290
|
+
if not result.success:
|
|
291
|
+
logger.info("[healthscore:dead_code] Analysis failed")
|
|
292
|
+
return None
|
|
293
|
+
count = len(result.unused_exports)
|
|
294
|
+
score = max(0.0, 100.0 - count * 2.0)
|
|
295
|
+
logger.info("[healthscore:dead_code] unused_exports=%d, score=%.1f", count, score)
|
|
296
|
+
return score
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def _probe_dependency_freshness(target_path: Path) -> float | None:
|
|
300
|
+
"""Score based on outdated dependency count."""
|
|
301
|
+
from pennyfarthing_scripts.dependencies.analyze import analyze_dependencies
|
|
302
|
+
|
|
303
|
+
result = await analyze_dependencies(target_path)
|
|
304
|
+
if not result.success:
|
|
305
|
+
logger.info("[healthscore:dependency_freshness] Analysis failed")
|
|
306
|
+
return None
|
|
307
|
+
outdated = len(result.outdated)
|
|
308
|
+
advisories = len(result.advisories)
|
|
309
|
+
score = max(0.0, 100.0 - outdated * 5.0 - advisories * 15.0)
|
|
310
|
+
logger.info("[healthscore:dependency_freshness] outdated=%d, advisories=%d, score=%.1f",
|
|
311
|
+
outdated, advisories, score)
|
|
312
|
+
return score
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
async def _probe_deprecation_debt(target_path: Path) -> float | None:
|
|
316
|
+
"""Score based on @deprecated symbol count and active callers."""
|
|
317
|
+
from pennyfarthing_scripts.codemarkers.analyze import analyze_deprecations
|
|
318
|
+
|
|
319
|
+
result = await analyze_deprecations(target_path)
|
|
320
|
+
if not result.get("success"):
|
|
321
|
+
logger.info("[healthscore:deprecation_debt] Analysis failed: %s", result.get("error"))
|
|
322
|
+
return None
|
|
323
|
+
summary = result.get("summary", {})
|
|
324
|
+
total = summary.get("total_deprecations", 0)
|
|
325
|
+
with_callers = summary.get("deprecations_with_callers", 0)
|
|
326
|
+
# Heuristic: each deprecated symbol deducts 5 points,
|
|
327
|
+
# each one still actively called deducts an extra 10
|
|
328
|
+
score = max(0.0, 100.0 - total * 5.0 - with_callers * 10.0)
|
|
329
|
+
logger.info("[healthscore:deprecation_debt] total=%d, with_callers=%d, score=%.1f",
|
|
330
|
+
total, with_callers, score)
|
|
331
|
+
return score
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def _probe_test_gaps(target_path: Path) -> float | None:
|
|
335
|
+
"""Score based on ratio of testable source files with corresponding test files.
|
|
336
|
+
|
|
337
|
+
Uses directory-aware matching: for a source file like src/api/health-score.ts,
|
|
338
|
+
checks for tests/api/health-score.test.ts, src/api/__tests__/health-score.test.ts,
|
|
339
|
+
test_health_score.py, etc. Also filters out non-testable files (configs, types, index
|
|
340
|
+
re-exports) to avoid inflating the denominator.
|
|
341
|
+
"""
|
|
342
|
+
logger.info("[healthscore:test_gaps] Scanning %s", target_path)
|
|
343
|
+
|
|
344
|
+
exclude_dirs = {"node_modules", "dist", "build", ".git", "__pycache__", ".cache",
|
|
345
|
+
".pennyfarthing", "coverage", ".next", ".venv", "venv", ".session",
|
|
346
|
+
"sprint", "docs"}
|
|
347
|
+
source_exts = {".ts", ".tsx", ".js", ".jsx", ".py"}
|
|
348
|
+
# Files that don't need dedicated tests
|
|
349
|
+
non_testable_stems = {"index", "types", "constants", "config", "__init__",
|
|
350
|
+
"cli", "__main__", "main", "preload", "vite-env"}
|
|
351
|
+
non_testable_patterns = {".d.ts", ".config.ts", ".config.js", "vite.config",
|
|
352
|
+
"tailwind.config", "postcss.config", "jest.config",
|
|
353
|
+
"vitest.config", "tsconfig"}
|
|
354
|
+
|
|
355
|
+
# Collect source files as (stem_lower, rel_path) and test files as set of stem variants
|
|
356
|
+
source_files: list[tuple[str, str]] = []
|
|
357
|
+
# test_stems: set of lowered stems stripped of test prefixes/suffixes
|
|
358
|
+
test_stems: set[str] = set()
|
|
359
|
+
# test_relpaths: full relative paths of test files for directory matching
|
|
360
|
+
test_relpaths: set[str] = set()
|
|
361
|
+
|
|
362
|
+
for file_path in target_path.rglob("*"):
|
|
363
|
+
if not file_path.is_file():
|
|
364
|
+
continue
|
|
365
|
+
if file_path.suffix.lower() not in source_exts:
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
parts = file_path.relative_to(target_path).parts
|
|
369
|
+
if any(p in exclude_dirs for p in parts):
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
rel = str(file_path.relative_to(target_path))
|
|
373
|
+
fname = file_path.name.lower()
|
|
374
|
+
stem = file_path.stem.lower()
|
|
375
|
+
# Strip double extensions: foo.test.ts -> stem is "foo.test"
|
|
376
|
+
if "." in stem:
|
|
377
|
+
base_stem = stem.split(".")[0]
|
|
378
|
+
else:
|
|
379
|
+
base_stem = stem
|
|
380
|
+
|
|
381
|
+
is_test = (
|
|
382
|
+
fname.startswith("test_")
|
|
383
|
+
or ".test." in fname
|
|
384
|
+
or ".spec." in fname
|
|
385
|
+
or fname.endswith("_test.py")
|
|
386
|
+
or "__tests__" in rel
|
|
387
|
+
or "/tests/" in rel
|
|
388
|
+
or "/test/" in rel
|
|
389
|
+
or rel.startswith("tests/")
|
|
390
|
+
or rel.startswith("test/")
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if is_test:
|
|
394
|
+
# Extract the tested module stem from test file name
|
|
395
|
+
# test_foo.py -> foo, foo.test.ts -> foo, foo.spec.tsx -> foo, foo_test.py -> foo
|
|
396
|
+
tested = base_stem
|
|
397
|
+
if tested.startswith("test_"):
|
|
398
|
+
tested = tested[5:]
|
|
399
|
+
elif tested.startswith("test"):
|
|
400
|
+
tested = tested[4:]
|
|
401
|
+
if tested.endswith("_test"):
|
|
402
|
+
tested = tested[:-5]
|
|
403
|
+
if tested:
|
|
404
|
+
test_stems.add(tested)
|
|
405
|
+
test_relpaths.add(rel.lower())
|
|
406
|
+
else:
|
|
407
|
+
# Filter out non-testable files
|
|
408
|
+
if base_stem in non_testable_stems:
|
|
409
|
+
continue
|
|
410
|
+
if any(p in fname for p in non_testable_patterns):
|
|
411
|
+
continue
|
|
412
|
+
source_files.append((base_stem, rel))
|
|
413
|
+
|
|
414
|
+
if not source_files:
|
|
415
|
+
logger.info("[healthscore:test_gaps] No testable source files found")
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
covered = 0
|
|
419
|
+
uncovered_samples: list[str] = []
|
|
420
|
+
for src_stem, src_rel in source_files:
|
|
421
|
+
# Strategy 1: Direct stem match in test_stems set
|
|
422
|
+
if src_stem in test_stems:
|
|
423
|
+
covered += 1
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
# Strategy 2: Hyphenated/underscored variants (health-score -> health_score)
|
|
427
|
+
normalized = src_stem.replace("-", "_")
|
|
428
|
+
if normalized in test_stems or normalized.replace("_", "-") in test_stems:
|
|
429
|
+
covered += 1
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
# Strategy 3: Directory-aware — check if test file exists at parallel path
|
|
433
|
+
# src/api/health-score.ts -> tests/api/health-score.test.ts
|
|
434
|
+
src_lower = src_rel.lower()
|
|
435
|
+
src_dir = "/".join(src_lower.split("/")[:-1])
|
|
436
|
+
matched = False
|
|
437
|
+
for variant in [
|
|
438
|
+
f"{src_dir}/{src_stem}.test.",
|
|
439
|
+
f"{src_dir}/{src_stem}.spec.",
|
|
440
|
+
f"{src_dir}/__tests__/{src_stem}.",
|
|
441
|
+
]:
|
|
442
|
+
if any(variant in tp for tp in test_relpaths):
|
|
443
|
+
matched = True
|
|
444
|
+
break
|
|
445
|
+
# Also check tests/ mirror: src/api/foo.ts -> tests/api/foo.test.ts
|
|
446
|
+
if not matched and src_dir:
|
|
447
|
+
for prefix in ["tests/", "test/"]:
|
|
448
|
+
for variant in [
|
|
449
|
+
f"{prefix}{src_dir}/{src_stem}.test.",
|
|
450
|
+
f"{prefix}{src_dir}/{src_stem}.spec.",
|
|
451
|
+
f"{prefix}{src_dir}/test_{src_stem}.",
|
|
452
|
+
]:
|
|
453
|
+
if any(variant in tp for tp in test_relpaths):
|
|
454
|
+
matched = True
|
|
455
|
+
break
|
|
456
|
+
if matched:
|
|
457
|
+
break
|
|
458
|
+
|
|
459
|
+
if matched:
|
|
460
|
+
covered += 1
|
|
461
|
+
else:
|
|
462
|
+
if len(uncovered_samples) < 10:
|
|
463
|
+
uncovered_samples.append(src_rel)
|
|
464
|
+
|
|
465
|
+
ratio = covered / len(source_files)
|
|
466
|
+
if ratio >= 0.8:
|
|
467
|
+
score = 90.0 + (ratio - 0.8) * 50.0
|
|
468
|
+
elif ratio >= 0.5:
|
|
469
|
+
score = 60.0 + (ratio - 0.5) * 100.0
|
|
470
|
+
elif ratio >= 0.2:
|
|
471
|
+
score = 30.0 + (ratio - 0.2) * 100.0
|
|
472
|
+
else:
|
|
473
|
+
score = max(5.0, ratio * 150.0)
|
|
474
|
+
|
|
475
|
+
score = max(0.0, min(100.0, score))
|
|
476
|
+
logger.info("[healthscore:test_gaps] source=%d, covered=%d, ratio=%.2f, score=%.1f",
|
|
477
|
+
len(source_files), covered, ratio, score)
|
|
478
|
+
if uncovered_samples:
|
|
479
|
+
logger.info("[healthscore:test_gaps] Sample uncovered: %s", uncovered_samples[:5])
|
|
480
|
+
return score
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
async def _probe_agent_context_efficiency(target_path: Path) -> float | None:
|
|
484
|
+
"""Score based on agent context token budgets.
|
|
485
|
+
|
|
486
|
+
Uses the Prime tier system to load FULL context for each agent
|
|
487
|
+
and scores based on how well agents stay within token budget.
|
|
488
|
+
Target: ~4000 tokens per agent for FULL tier.
|
|
489
|
+
"""
|
|
490
|
+
from pennyfarthing_scripts.prime.tiers import ContextTier, load_tier_components
|
|
491
|
+
|
|
492
|
+
agents = ["sm", "tea", "dev", "reviewer", "architect",
|
|
493
|
+
"pm", "tech-writer", "ux-designer", "devops", "orchestrator"]
|
|
494
|
+
|
|
495
|
+
target_budget = 4000
|
|
496
|
+
scores: list[float] = []
|
|
497
|
+
|
|
498
|
+
for agent in agents:
|
|
499
|
+
try:
|
|
500
|
+
components = load_tier_components(ContextTier.FULL, agent, target_path)
|
|
501
|
+
total = components.get("total_tokens", 0)
|
|
502
|
+
if total <= 0:
|
|
503
|
+
logger.info("[healthscore:agent_context] %s: no tokens loaded", agent)
|
|
504
|
+
continue
|
|
505
|
+
# Score per agent: at or under budget = 100, over budget degrades linearly
|
|
506
|
+
# 2x budget = 0
|
|
507
|
+
ratio = total / target_budget
|
|
508
|
+
if ratio <= 1.0:
|
|
509
|
+
agent_score = 100.0
|
|
510
|
+
else:
|
|
511
|
+
agent_score = max(0.0, 100.0 - (ratio - 1.0) * 100.0)
|
|
512
|
+
logger.info("[healthscore:agent_context] %s: %d tokens (%.1f%% of budget), score=%.1f",
|
|
513
|
+
agent, total, ratio * 100, agent_score)
|
|
514
|
+
scores.append(agent_score)
|
|
515
|
+
except Exception as exc:
|
|
516
|
+
logger.warning("[healthscore:agent_context] %s failed: %s", agent, exc)
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
if not scores:
|
|
520
|
+
logger.info("[healthscore:agent_context] No agent scores collected")
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
avg = sum(scores) / len(scores)
|
|
524
|
+
logger.info("[healthscore:agent_context] %d agents scored, avg=%.1f", len(scores), avg)
|
|
525
|
+
return avg
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def compute_composite_score(
|
|
529
|
+
dimension_scores: dict[str, float | None],
|
|
530
|
+
weights: dict[str, float],
|
|
531
|
+
) -> float:
|
|
532
|
+
"""Compute weighted average from dimension scores.
|
|
533
|
+
|
|
534
|
+
Dimensions with None scores are excluded and remaining weights
|
|
535
|
+
are renormalized.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
dimension_scores: Map of dimension name to score (0-100 or None).
|
|
539
|
+
weights: Map of dimension name to weight.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
Composite score 0-100.
|
|
543
|
+
"""
|
|
544
|
+
total_weight = 0.0
|
|
545
|
+
weighted_sum = 0.0
|
|
546
|
+
|
|
547
|
+
for name, score in dimension_scores.items():
|
|
548
|
+
if score is not None:
|
|
549
|
+
w = weights.get(name, 0.0)
|
|
550
|
+
weighted_sum += score * w
|
|
551
|
+
total_weight += w
|
|
552
|
+
|
|
553
|
+
if total_weight == 0.0:
|
|
554
|
+
return 0.0
|
|
555
|
+
|
|
556
|
+
return weighted_sum / total_weight
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def get_cache_path(target_path: Path) -> Path:
|
|
560
|
+
"""Return the cache directory for a given target path."""
|
|
561
|
+
path_hash = hashlib.md5(str(target_path).encode()).hexdigest()[:12]
|
|
562
|
+
return target_path / ".pennyfarthing" / ".cache" / "healthscore" / path_hash
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def read_cached_score(cache_dir: Path, dimension: str, ttl: int) -> float | None:
|
|
566
|
+
"""Read a cached dimension score if still valid.
|
|
567
|
+
|
|
568
|
+
Returns None if cache miss or expired.
|
|
569
|
+
"""
|
|
570
|
+
cache_file = cache_dir / f"{dimension}.json"
|
|
571
|
+
if not cache_file.exists():
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
data = json.loads(cache_file.read_text())
|
|
576
|
+
except (json.JSONDecodeError, OSError):
|
|
577
|
+
return None
|
|
578
|
+
|
|
579
|
+
ts = data.get("timestamp", 0)
|
|
580
|
+
if ttl <= 0 or (time.time() - ts) > ttl:
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
return data.get("score")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def write_cached_score(cache_dir: Path, dimension: str, score: float) -> None:
|
|
587
|
+
"""Write a dimension score to cache."""
|
|
588
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
589
|
+
cache_file = cache_dir / f"{dimension}.json"
|
|
590
|
+
data = {"score": score, "timestamp": time.time()}
|
|
591
|
+
cache_file.write_text(json.dumps(data))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for health score analysis.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pf healthscore analyze [OPTIONS]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pennyfarthing_scripts.healthscore.models import HealthscoreResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group()
|
|
21
|
+
def healthscore():
|
|
22
|
+
"""Composite codebase health score.
|
|
23
|
+
|
|
24
|
+
\b
|
|
25
|
+
Commands:
|
|
26
|
+
analyze - Compute health score across all dimensions
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _common_options(fn):
|
|
32
|
+
"""Shared options for healthscore commands."""
|
|
33
|
+
fn = click.option("--path", "target_path", type=click.Path(exists=True),
|
|
34
|
+
help="Directory to analyze")(fn)
|
|
35
|
+
fn = click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]),
|
|
36
|
+
default="table", show_default=True)(fn)
|
|
37
|
+
fn = click.option("--output", "output_file", type=click.Path(),
|
|
38
|
+
help="Write output to file")(fn)
|
|
39
|
+
fn = click.option("--no-cache", is_flag=True,
|
|
40
|
+
help="Bypass cache, force fresh analysis")(fn)
|
|
41
|
+
return fn
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _run_analysis(target_path: str | None, no_cache: bool) -> HealthscoreResult:
|
|
45
|
+
"""Run analysis and return result."""
|
|
46
|
+
from pennyfarthing_scripts.healthscore.analyze import analyze_healthscore
|
|
47
|
+
|
|
48
|
+
p = Path(target_path).resolve() if target_path else Path(".").resolve()
|
|
49
|
+
cache_ttl = 0 if no_cache else 300
|
|
50
|
+
return asyncio.run(analyze_healthscore(p, cache_ttl=cache_ttl))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _output_result(result, fmt: str, output_file: str | None):
|
|
54
|
+
"""Format and output the analysis result."""
|
|
55
|
+
from pennyfarthing_scripts.healthscore.formatters import (
|
|
56
|
+
export_csv,
|
|
57
|
+
export_json,
|
|
58
|
+
format_table,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if fmt == "json":
|
|
62
|
+
text = export_json(result)
|
|
63
|
+
elif fmt == "csv":
|
|
64
|
+
text = export_csv(result)
|
|
65
|
+
else:
|
|
66
|
+
text = format_table(result)
|
|
67
|
+
|
|
68
|
+
if output_file:
|
|
69
|
+
Path(output_file).write_text(text)
|
|
70
|
+
click.echo(f"Output written to {output_file}", err=True)
|
|
71
|
+
else:
|
|
72
|
+
click.echo(text)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@healthscore.command()
|
|
76
|
+
@_common_options
|
|
77
|
+
def analyze(target_path, fmt, output_file, no_cache):
|
|
78
|
+
"""Compute composite health score."""
|
|
79
|
+
result = _run_analysis(target_path, no_cache)
|
|
80
|
+
_output_result(result, fmt, output_file)
|