@pennyfarthing/core 10.1.0 → 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 +13 -18
- package/package.json +3 -1
- 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 +101 -15
- 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/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.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/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/handoff.md +18 -3
- 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 -5
- package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
- package/pennyfarthing-dist/commands/setup.md +4 -0
- package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
- package/pennyfarthing-dist/guides/bikelane.md +3 -2
- package/pennyfarthing-dist/guides/scale-levels.md +4 -6
- 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/hooks/bell-mode-hook.sh +131 -54
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +20 -10
- package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
- package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -189
- package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +1 -1
- package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
- package/pennyfarthing-dist/skills/workflow/skill.md +24 -1
- package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
- 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 +11 -6
- package/pennyfarthing_scripts/codemarkers/__init__.py +5 -1
- 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 +177 -2
- package/pennyfarthing_scripts/codemarkers/cli.py +50 -0
- package/pennyfarthing_scripts/codemarkers/formatters.py +0 -1
- package/pennyfarthing_scripts/codemarkers/models.py +15 -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 +1 -1
- 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 +1 -1
- package/pennyfarthing_scripts/complexity/cli.py +5 -1
- package/pennyfarthing_scripts/complexity/formatters.py +1 -1
- package/pennyfarthing_scripts/context.py +14 -15
- 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 +3 -4
- package/pennyfarthing_scripts/deadcode/cli.py +2 -2
- package/pennyfarthing_scripts/dependencies/__init__.py +2 -2
- 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 +1 -1
- package/pennyfarthing_scripts/dependencies/cli.py +8 -4
- package/pennyfarthing_scripts/dependencies/formatters.py +1 -1
- 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 +2 -2
- package/pennyfarthing_scripts/healthscore/__main__.py +8 -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 +451 -21
- package/pennyfarthing_scripts/healthscore/cli.py +5 -1
- package/pennyfarthing_scripts/healthscore/models.py +0 -1
- 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 +128 -14
- package/pennyfarthing_scripts/hotspots/cli.py +2 -2
- 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 +2 -3
- package/pennyfarthing_scripts/jira/claim.py +21 -0
- package/pennyfarthing_scripts/jira/cli.py +2 -2
- package/pennyfarthing_scripts/jira/client.py +4 -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 +1 -4
- package/pennyfarthing_scripts/sprint/cli.py +34 -28
- 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 +2 -2
- package/pennyfarthing_scripts/sprint/story_finish.py +3 -5
- 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 +1 -4
- 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 +13 -8
- 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 +17 -25
- 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/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/cli.py +3 -2
- 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 +3 -15
- 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
|
@@ -7,9 +7,12 @@ Supports caching with a configurable TTL (default 5 minutes).
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import asyncio
|
|
10
11
|
import hashlib
|
|
11
12
|
import json
|
|
13
|
+
import logging
|
|
12
14
|
import time
|
|
15
|
+
from datetime import UTC
|
|
13
16
|
from pathlib import Path
|
|
14
17
|
|
|
15
18
|
from pennyfarthing_scripts.healthscore.models import (
|
|
@@ -18,6 +21,8 @@ from pennyfarthing_scripts.healthscore.models import (
|
|
|
18
21
|
HealthscoreResult,
|
|
19
22
|
)
|
|
20
23
|
|
|
24
|
+
logger = logging.getLogger("healthscore")
|
|
25
|
+
|
|
21
26
|
|
|
22
27
|
async def analyze_healthscore(
|
|
23
28
|
target_path: Path,
|
|
@@ -36,35 +41,44 @@ async def analyze_healthscore(
|
|
|
36
41
|
"""
|
|
37
42
|
w = weights if weights is not None else DEFAULT_WEIGHTS
|
|
38
43
|
resolved = target_path.resolve()
|
|
44
|
+
logger.info("[healthscore] Starting analysis for %s", resolved)
|
|
45
|
+
logger.info("[healthscore] Dimensions: %s", list(w.keys()))
|
|
39
46
|
|
|
40
47
|
cache_dir = get_cache_path(resolved)
|
|
41
48
|
any_cached = False
|
|
42
49
|
raw_scores: dict[str, float | None] = {}
|
|
43
50
|
dimensions: list[DimensionScore] = []
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Try cache if ttl > 0
|
|
52
|
+
# Separate cached vs uncached dimensions
|
|
53
|
+
uncached_dims: list[str] = []
|
|
54
|
+
for dim_name in w:
|
|
50
55
|
if cache_ttl > 0:
|
|
51
56
|
cached = read_cached_score(cache_dir, dim_name, cache_ttl)
|
|
52
57
|
if cached is not None:
|
|
53
|
-
score = cached
|
|
58
|
+
logger.info("[healthscore] %s: cached score = %.1f", dim_name, cached)
|
|
59
|
+
raw_scores[dim_name] = cached
|
|
54
60
|
any_cached = True
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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)
|
|
60
74
|
if score is not None and cache_ttl > 0:
|
|
61
75
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
62
76
|
write_cached_score(cache_dir, dim_name, score)
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
68
82
|
dimensions.append(DimensionScore(
|
|
69
83
|
name=dim_name,
|
|
70
84
|
score=score,
|
|
@@ -73,6 +87,7 @@ async def analyze_healthscore(
|
|
|
73
87
|
))
|
|
74
88
|
|
|
75
89
|
composite = compute_composite_score(raw_scores, w)
|
|
90
|
+
logger.info("[healthscore] Composite score: %.1f", composite)
|
|
76
91
|
|
|
77
92
|
return HealthscoreResult(
|
|
78
93
|
success=True,
|
|
@@ -83,16 +98,431 @@ async def analyze_healthscore(
|
|
|
83
98
|
)
|
|
84
99
|
|
|
85
100
|
|
|
86
|
-
def _probe_dimension(name: str, target_path: Path) -> float | None:
|
|
101
|
+
async def _probe_dimension(name: str, target_path: Path) -> float | None:
|
|
87
102
|
"""Run a lightweight probe for a single dimension.
|
|
88
103
|
|
|
89
104
|
Returns a score 0-100 or None if the dimension cannot be assessed.
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
92
135
|
"""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
96
526
|
|
|
97
527
|
|
|
98
528
|
def compute_composite_score(
|
|
@@ -9,9 +9,13 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
12
13
|
|
|
13
14
|
import click
|
|
14
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pennyfarthing_scripts.healthscore.models import HealthscoreResult
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
@click.group()
|
|
17
21
|
def healthscore():
|
|
@@ -37,7 +41,7 @@ def _common_options(fn):
|
|
|
37
41
|
return fn
|
|
38
42
|
|
|
39
43
|
|
|
40
|
-
def _run_analysis(target_path: str | None, no_cache: bool) ->
|
|
44
|
+
def _run_analysis(target_path: str | None, no_cache: bool) -> HealthscoreResult:
|
|
41
45
|
"""Run analysis and return result."""
|
|
42
46
|
from pennyfarthing_scripts.healthscore.analyze import analyze_healthscore
|
|
43
47
|
|
|
@@ -16,15 +16,14 @@ Story: MSSCI-12409 - Hook consistency and relay mode compatibility
|
|
|
16
16
|
import json
|
|
17
17
|
import os
|
|
18
18
|
import sys
|
|
19
|
-
import urllib.request
|
|
20
19
|
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
21
|
from dataclasses import dataclass
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
25
|
import yaml
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
# =============================================================================
|
|
29
28
|
# Port File Constants
|
|
30
29
|
# =============================================================================
|
|
@@ -413,19 +412,17 @@ def read_stdin_json() -> dict[str, Any]:
|
|
|
413
412
|
def is_cyclist_running(project_root: Path | None = None) -> bool:
|
|
414
413
|
"""Check if Cyclist server is running.
|
|
415
414
|
|
|
416
|
-
Checks
|
|
417
|
-
|
|
415
|
+
Checks the CYCLIST environment variable set by ClaudeService when
|
|
416
|
+
spawning Claude inside Cyclist. No file I/O, no HTTP, no signals —
|
|
417
|
+
this runs on every tool invocation and must be instant.
|
|
418
418
|
|
|
419
|
-
|
|
420
|
-
|
|
419
|
+
The project_root parameter is kept for backward compatibility but
|
|
420
|
+
is no longer used.
|
|
421
421
|
|
|
422
422
|
Returns:
|
|
423
|
-
True if
|
|
423
|
+
True if running inside a Cyclist-spawned Claude process
|
|
424
424
|
"""
|
|
425
|
-
|
|
426
|
-
if not root:
|
|
427
|
-
return False
|
|
428
|
-
return (root / CYCLIST_PORT_FILE).exists()
|
|
425
|
+
return os.environ.get("CYCLIST") == "1"
|
|
429
426
|
|
|
430
427
|
|
|
431
428
|
def should_auto_approve(settings: CyclistSettings) -> bool:
|
|
@@ -6,18 +6,18 @@ bug fix concentration, and multi-author churn — indicators of code hotspots th
|
|
|
6
6
|
may benefit from refactoring attention.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from pennyfarthing_scripts.hotspots.analyze import (
|
|
10
|
+
analyze_all_repos,
|
|
11
|
+
analyze_repo,
|
|
12
|
+
calculate_hotspot_score,
|
|
13
|
+
is_bug_fix_commit,
|
|
14
|
+
)
|
|
9
15
|
from pennyfarthing_scripts.hotspots.models import (
|
|
10
16
|
DirectoryHotspot,
|
|
11
17
|
FileHotspot,
|
|
12
18
|
HotspotResult,
|
|
13
19
|
MultiRepoHotspotResult,
|
|
14
20
|
)
|
|
15
|
-
from pennyfarthing_scripts.hotspots.analyze import (
|
|
16
|
-
analyze_repo,
|
|
17
|
-
analyze_all_repos,
|
|
18
|
-
calculate_hotspot_score,
|
|
19
|
-
is_bug_fix_commit,
|
|
20
|
-
)
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"DirectoryHotspot",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|