@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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Agent definition structural validator adapter.
|
|
2
|
+
|
|
3
|
+
Validates agent definition files in pennyfarthing-dist/agents/.
|
|
4
|
+
Checks required sections, model values, and subagent references.
|
|
5
|
+
|
|
6
|
+
Story: MSSCI-14710 (91-12)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from pennyfarthing_scripts.validate import ValidateReport
|
|
17
|
+
|
|
18
|
+
VALID_MODELS = {"haiku", "sonnet", "opus"}
|
|
19
|
+
BUILTIN_AGENTS = {"Explore", "Plan"}
|
|
20
|
+
|
|
21
|
+
# Regex to find XML-like tags in markdown: <tagname> or <tag-name>
|
|
22
|
+
_TAG_RE = re.compile(r"<([a-zA-Z][a-zA-Z0-9_-]*)(?:\s[^>]*)?>", re.MULTILINE)
|
|
23
|
+
|
|
24
|
+
# Regex to extract **Model:** value from helpers section
|
|
25
|
+
_MODEL_RE = re.compile(r"\*\*Model:\*\*\s+(\S+)", re.IGNORECASE)
|
|
26
|
+
|
|
27
|
+
# Regex to extract subagent names from markdown table rows: | `name` | purpose |
|
|
28
|
+
_HELPER_TABLE_RE = re.compile(r"^\s*\|\s*`?([^`|]+)`?\s*\|", re.MULTILINE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _has_frontmatter(content: str) -> bool:
|
|
32
|
+
"""Check if content starts with YAML frontmatter (--- delimited)."""
|
|
33
|
+
return content.startswith("---\n")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_frontmatter(content: str) -> dict:
|
|
37
|
+
"""Extract YAML frontmatter from content."""
|
|
38
|
+
if not _has_frontmatter(content):
|
|
39
|
+
return {}
|
|
40
|
+
end = content.find("\n---", 3)
|
|
41
|
+
if end == -1:
|
|
42
|
+
return {}
|
|
43
|
+
fm_text = content[4:end]
|
|
44
|
+
try:
|
|
45
|
+
return yaml.safe_load(fm_text) or {}
|
|
46
|
+
except yaml.YAMLError:
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _find_tags(content: str) -> set[str]:
|
|
51
|
+
"""Find all XML-like tag names in content."""
|
|
52
|
+
return {m.group(1) for m in _TAG_RE.finditer(content)}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _extract_section(content: str, tag: str) -> str | None:
|
|
56
|
+
"""Extract content between <tag> and </tag>."""
|
|
57
|
+
pattern = re.compile(rf"<{re.escape(tag)}[^>]*>(.*?)</{re.escape(tag)}>", re.DOTALL)
|
|
58
|
+
m = pattern.search(content)
|
|
59
|
+
return m.group(1) if m else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def classify_agent_files(
|
|
63
|
+
agents_dir: Path,
|
|
64
|
+
) -> tuple[list[Path], list[Path], list[Path]]:
|
|
65
|
+
"""Classify agent files into main agents, subagents, and skipped.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
(main_agents, subagents, skipped) — three lists of Path objects.
|
|
69
|
+
"""
|
|
70
|
+
main: list[Path] = []
|
|
71
|
+
sub: list[Path] = []
|
|
72
|
+
skipped: list[Path] = []
|
|
73
|
+
|
|
74
|
+
for f in sorted(agents_dir.glob("*.md")):
|
|
75
|
+
if f.name == "README.md":
|
|
76
|
+
skipped.append(f)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
content = f.read_text()
|
|
80
|
+
if _has_frontmatter(content):
|
|
81
|
+
sub.append(f)
|
|
82
|
+
else:
|
|
83
|
+
main.append(f)
|
|
84
|
+
|
|
85
|
+
return main, sub, skipped
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate_main_agent(
|
|
89
|
+
path: Path, agents_dir: Path
|
|
90
|
+
) -> tuple[list[str], list[str]]:
|
|
91
|
+
"""Validate a main agent definition file.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
(errors, warnings) — two lists of message strings.
|
|
95
|
+
"""
|
|
96
|
+
errors: list[str] = []
|
|
97
|
+
warnings: list[str] = []
|
|
98
|
+
content = path.read_text()
|
|
99
|
+
tags = _find_tags(content)
|
|
100
|
+
|
|
101
|
+
# Required sections
|
|
102
|
+
if "role" not in tags:
|
|
103
|
+
errors.append("Missing required <role> section")
|
|
104
|
+
if "critical" not in tags:
|
|
105
|
+
errors.append("Missing required <critical> section")
|
|
106
|
+
if "helpers" not in tags:
|
|
107
|
+
errors.append("Missing required <helpers> section")
|
|
108
|
+
if "skills" not in tags:
|
|
109
|
+
errors.append("Missing required <skills> section")
|
|
110
|
+
|
|
111
|
+
# Model validation (only if helpers exists)
|
|
112
|
+
if "helpers" in tags:
|
|
113
|
+
helpers_content = _extract_section(content, "helpers")
|
|
114
|
+
if helpers_content:
|
|
115
|
+
model_match = _MODEL_RE.search(helpers_content)
|
|
116
|
+
if model_match:
|
|
117
|
+
model_val = model_match.group(1).lower()
|
|
118
|
+
if model_val not in VALID_MODELS:
|
|
119
|
+
errors.append(
|
|
120
|
+
f"Invalid model '{model_match.group(1)}' in <helpers> "
|
|
121
|
+
f"(must be one of: {', '.join(sorted(VALID_MODELS))})"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Subagent reference validation
|
|
125
|
+
existing_files = {f.stem for f in agents_dir.glob("*.md") if f.name != "README.md"}
|
|
126
|
+
# Parse table rows — skip header/separator rows
|
|
127
|
+
for line in helpers_content.splitlines():
|
|
128
|
+
m = _HELPER_TABLE_RE.match(line)
|
|
129
|
+
if not m:
|
|
130
|
+
continue
|
|
131
|
+
name = m.group(1).strip().strip("`")
|
|
132
|
+
# Skip table header and separator
|
|
133
|
+
if name.lower() in ("subagent", "---", "") or name.startswith("-"):
|
|
134
|
+
continue
|
|
135
|
+
if name in BUILTIN_AGENTS:
|
|
136
|
+
continue
|
|
137
|
+
if name not in existing_files:
|
|
138
|
+
warnings.append(
|
|
139
|
+
f"Helper references '{name}' but no matching agent file not found in agents/"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Recommended sections (warnings)
|
|
143
|
+
if "on-activation" not in tags:
|
|
144
|
+
warnings.append("Missing recommended <on-activation> section")
|
|
145
|
+
if "exit" not in tags and "exit-sequence" not in tags:
|
|
146
|
+
warnings.append("Missing recommended <exit> or <exit-sequence> section")
|
|
147
|
+
|
|
148
|
+
return errors, warnings
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def validate_subagent(path: Path) -> tuple[list[str], list[str]]:
|
|
152
|
+
"""Validate a subagent definition file.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
(errors, warnings) — two lists of message strings.
|
|
156
|
+
"""
|
|
157
|
+
errors: list[str] = []
|
|
158
|
+
warnings: list[str] = []
|
|
159
|
+
content = path.read_text()
|
|
160
|
+
|
|
161
|
+
# Frontmatter validation
|
|
162
|
+
fm = _parse_frontmatter(content)
|
|
163
|
+
if not fm:
|
|
164
|
+
errors.append("Missing or invalid YAML frontmatter")
|
|
165
|
+
return errors, warnings
|
|
166
|
+
|
|
167
|
+
for field in ("name", "description", "tools", "model"):
|
|
168
|
+
if field not in fm:
|
|
169
|
+
errors.append(f"Missing required frontmatter field: {field}")
|
|
170
|
+
|
|
171
|
+
# Model must be haiku for subagents
|
|
172
|
+
if "model" in fm:
|
|
173
|
+
model_val = str(fm["model"]).lower()
|
|
174
|
+
if model_val != "haiku":
|
|
175
|
+
errors.append(
|
|
176
|
+
f"Subagent model must be 'haiku', got '{fm['model']}'"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Required tags
|
|
180
|
+
tags = _find_tags(content)
|
|
181
|
+
if "output" not in tags:
|
|
182
|
+
errors.append("Missing required <output> section")
|
|
183
|
+
|
|
184
|
+
# Recommended tags
|
|
185
|
+
if "arguments" not in tags:
|
|
186
|
+
warnings.append("Missing recommended <arguments> section")
|
|
187
|
+
|
|
188
|
+
return errors, warnings
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
|
|
192
|
+
"""Validate all agent definition files."""
|
|
193
|
+
report = ValidateReport(validator="agent")
|
|
194
|
+
agents_dir = root / "pennyfarthing-dist" / "agents"
|
|
195
|
+
|
|
196
|
+
if not agents_dir.is_dir():
|
|
197
|
+
report.details.append("[ERROR] agents directory not found")
|
|
198
|
+
report.errors += 1
|
|
199
|
+
return report
|
|
200
|
+
|
|
201
|
+
main_agents, subagents, _skipped = classify_agent_files(agents_dir)
|
|
202
|
+
|
|
203
|
+
for path in main_agents:
|
|
204
|
+
file_errors, file_warnings = validate_main_agent(path, agents_dir)
|
|
205
|
+
|
|
206
|
+
for e in file_errors:
|
|
207
|
+
report.errors += 1
|
|
208
|
+
report.details.append(f"[ERROR] {path.name}: {e}")
|
|
209
|
+
|
|
210
|
+
for w in file_warnings:
|
|
211
|
+
if strict:
|
|
212
|
+
report.errors += 1
|
|
213
|
+
report.details.append(f"[ERROR] {path.name}: {w}")
|
|
214
|
+
else:
|
|
215
|
+
report.warnings += 1
|
|
216
|
+
report.details.append(f"[WARN] {path.name}: {w}")
|
|
217
|
+
|
|
218
|
+
if not file_errors:
|
|
219
|
+
report.passed += 1
|
|
220
|
+
|
|
221
|
+
for path in subagents:
|
|
222
|
+
file_errors, file_warnings = validate_subagent(path)
|
|
223
|
+
|
|
224
|
+
for e in file_errors:
|
|
225
|
+
report.errors += 1
|
|
226
|
+
report.details.append(f"[ERROR] {path.name}: {e}")
|
|
227
|
+
|
|
228
|
+
for w in file_warnings:
|
|
229
|
+
if strict:
|
|
230
|
+
report.errors += 1
|
|
231
|
+
report.details.append(f"[ERROR] {path.name}: {w}")
|
|
232
|
+
else:
|
|
233
|
+
report.warnings += 1
|
|
234
|
+
report.details.append(f"[WARN] {path.name}: {w}")
|
|
235
|
+
|
|
236
|
+
if not file_errors:
|
|
237
|
+
report.passed += 1
|
|
238
|
+
|
|
239
|
+
return report
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""XML schema validator adapter.
|
|
2
|
+
|
|
3
|
+
Delegates to migration/validate.validate_all() for session, skill, and workflow step files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pennyfarthing_scripts.validate import ValidateReport
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
|
|
14
|
+
"""Validate XML schema files (sessions, skills, workflow steps)."""
|
|
15
|
+
from pennyfarthing_scripts.migration.validate import validate_all
|
|
16
|
+
|
|
17
|
+
summary = validate_all(root, file_type="all", strict=strict)
|
|
18
|
+
|
|
19
|
+
report = ValidateReport(validator="schema")
|
|
20
|
+
report.passed = summary.passed
|
|
21
|
+
report.warnings = summary.warnings
|
|
22
|
+
report.errors = summary.errors
|
|
23
|
+
|
|
24
|
+
for result in summary.results:
|
|
25
|
+
for e in result.errors:
|
|
26
|
+
report.details.append(f"[ERROR] {result.file_path.name}: {e}")
|
|
27
|
+
for w in result.warnings:
|
|
28
|
+
report.details.append(f"[WARN] {result.file_path.name}: {w}")
|
|
29
|
+
|
|
30
|
+
return report
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Skill registry and command file structural validator adapter.
|
|
2
|
+
|
|
3
|
+
Validates skill-registry.yaml against skill-registry.schema.json and
|
|
4
|
+
command file structure in pennyfarthing-dist/commands/.
|
|
5
|
+
|
|
6
|
+
Story: MSSCI-14711 (91-13)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from pennyfarthing_scripts.validate import ValidateReport
|
|
18
|
+
|
|
19
|
+
_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def discover_skill_registry(root: Path) -> Path | None:
|
|
23
|
+
"""Locate skill-registry.yaml in the project.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Path to skill-registry.yaml, or None if not found.
|
|
27
|
+
"""
|
|
28
|
+
path = root / "pennyfarthing-dist" / "skills" / "skill-registry.yaml"
|
|
29
|
+
return path if path.is_file() else None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def discover_command_files(commands_dir: Path) -> list[Path]:
|
|
33
|
+
"""Discover all command markdown files.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Sorted list of Path objects for command .md files.
|
|
37
|
+
"""
|
|
38
|
+
if not commands_dir.is_dir():
|
|
39
|
+
return []
|
|
40
|
+
return sorted(f for f in commands_dir.glob("*.md"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_schema(root: Path) -> dict | None:
|
|
44
|
+
"""Load the skill-registry.schema.json file."""
|
|
45
|
+
schema_path = root / "pennyfarthing-dist" / "skills" / "skill-registry.schema.json"
|
|
46
|
+
if not schema_path.is_file():
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(schema_path.read_text())
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _validate_against_schema(
|
|
55
|
+
data: dict, schema: dict
|
|
56
|
+
) -> list[str]:
|
|
57
|
+
"""Validate data against a JSON Schema (manual implementation).
|
|
58
|
+
|
|
59
|
+
Handles the subset of JSON Schema used by skill-registry.schema.json:
|
|
60
|
+
- type checking (object, string, array, boolean)
|
|
61
|
+
- required fields
|
|
62
|
+
- additionalProperties: false
|
|
63
|
+
- pattern (regex)
|
|
64
|
+
- enum
|
|
65
|
+
- $ref to #/definitions/*
|
|
66
|
+
- items (for arrays)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of error message strings.
|
|
70
|
+
"""
|
|
71
|
+
errors: list[str] = []
|
|
72
|
+
definitions = schema.get("definitions", {})
|
|
73
|
+
|
|
74
|
+
def _resolve_ref(ref: str) -> dict:
|
|
75
|
+
"""Resolve a $ref pointer like #/definitions/skill."""
|
|
76
|
+
parts = ref.lstrip("#/").split("/")
|
|
77
|
+
result = schema
|
|
78
|
+
for part in parts:
|
|
79
|
+
result = result.get(part, {})
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def _validate_value(value, prop_schema: dict, path: str) -> None:
|
|
83
|
+
"""Validate a single value against its schema."""
|
|
84
|
+
# Handle $ref
|
|
85
|
+
if "$ref" in prop_schema:
|
|
86
|
+
prop_schema = _resolve_ref(prop_schema["$ref"])
|
|
87
|
+
|
|
88
|
+
expected_type = prop_schema.get("type")
|
|
89
|
+
|
|
90
|
+
# Type checking
|
|
91
|
+
if expected_type == "object":
|
|
92
|
+
if not isinstance(value, dict):
|
|
93
|
+
errors.append(f"{path}: expected object, got {type(value).__name__}")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
props = prop_schema.get("properties", {})
|
|
97
|
+
additional = prop_schema.get("additionalProperties")
|
|
98
|
+
|
|
99
|
+
# Required fields
|
|
100
|
+
for req in prop_schema.get("required", []):
|
|
101
|
+
if req not in value:
|
|
102
|
+
errors.append(f"{path}: missing required field '{req}'")
|
|
103
|
+
|
|
104
|
+
# Validate known properties
|
|
105
|
+
for key, val in value.items():
|
|
106
|
+
if key in props:
|
|
107
|
+
_validate_value(val, props[key], f"{path}.{key}")
|
|
108
|
+
elif additional is False:
|
|
109
|
+
errors.append(
|
|
110
|
+
f"{path}: additional property '{key}' not allowed"
|
|
111
|
+
)
|
|
112
|
+
elif isinstance(additional, dict):
|
|
113
|
+
_validate_value(val, additional, f"{path}.{key}")
|
|
114
|
+
|
|
115
|
+
elif expected_type == "string":
|
|
116
|
+
if not isinstance(value, str):
|
|
117
|
+
errors.append(f"{path}: expected string, got {type(value).__name__}")
|
|
118
|
+
return
|
|
119
|
+
pattern = prop_schema.get("pattern")
|
|
120
|
+
if pattern and not re.match(pattern, value):
|
|
121
|
+
errors.append(f"{path}: value '{value}' does not match pattern '{pattern}'")
|
|
122
|
+
enum_vals = prop_schema.get("enum")
|
|
123
|
+
if enum_vals and value not in enum_vals:
|
|
124
|
+
errors.append(
|
|
125
|
+
f"{path}: value '{value}' not in allowed values: "
|
|
126
|
+
f"{', '.join(enum_vals)}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
elif expected_type == "array":
|
|
130
|
+
if not isinstance(value, list):
|
|
131
|
+
errors.append(f"{path}: expected array, got {type(value).__name__}")
|
|
132
|
+
return
|
|
133
|
+
items_schema = prop_schema.get("items")
|
|
134
|
+
if items_schema:
|
|
135
|
+
for i, item in enumerate(value):
|
|
136
|
+
_validate_value(item, items_schema, f"{path}[{i}]")
|
|
137
|
+
|
|
138
|
+
elif expected_type == "boolean":
|
|
139
|
+
if not isinstance(value, bool):
|
|
140
|
+
errors.append(f"{path}: expected boolean, got {type(value).__name__}")
|
|
141
|
+
|
|
142
|
+
_validate_value(data, schema, "")
|
|
143
|
+
return errors
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def validate_skill_registry(root: Path) -> tuple[list[str], list[str]]:
|
|
147
|
+
"""Validate skill-registry.yaml against its JSON schema.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
(errors, warnings) — two lists of message strings.
|
|
151
|
+
"""
|
|
152
|
+
errors: list[str] = []
|
|
153
|
+
warnings: list[str] = []
|
|
154
|
+
|
|
155
|
+
registry_path = discover_skill_registry(root)
|
|
156
|
+
if registry_path is None:
|
|
157
|
+
errors.append("skill-registry.yaml not found")
|
|
158
|
+
return errors, warnings
|
|
159
|
+
|
|
160
|
+
schema = _load_schema(root)
|
|
161
|
+
if schema is None:
|
|
162
|
+
errors.append("skill-registry.schema.json not found or invalid")
|
|
163
|
+
return errors, warnings
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
content = registry_path.read_text()
|
|
167
|
+
data = yaml.safe_load(content)
|
|
168
|
+
except yaml.YAMLError:
|
|
169
|
+
errors.append("skill-registry.yaml: YAML parse error")
|
|
170
|
+
return errors, warnings
|
|
171
|
+
|
|
172
|
+
if not isinstance(data, dict):
|
|
173
|
+
errors.append("skill-registry.yaml: expected a YAML mapping")
|
|
174
|
+
return errors, warnings
|
|
175
|
+
|
|
176
|
+
schema_errors = _validate_against_schema(data, schema)
|
|
177
|
+
errors.extend(schema_errors)
|
|
178
|
+
|
|
179
|
+
return errors, warnings
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _has_frontmatter(content: str) -> bool:
|
|
183
|
+
"""Check if content starts with YAML frontmatter (--- delimited)."""
|
|
184
|
+
return content.startswith("---\n")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _parse_frontmatter(content: str) -> dict | None:
|
|
188
|
+
"""Extract YAML frontmatter from content.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Parsed dict, or None if no valid frontmatter.
|
|
192
|
+
"""
|
|
193
|
+
if not _has_frontmatter(content):
|
|
194
|
+
return None
|
|
195
|
+
end = content.find("\n---", 3)
|
|
196
|
+
if end == -1:
|
|
197
|
+
return None
|
|
198
|
+
fm_text = content[4:end]
|
|
199
|
+
try:
|
|
200
|
+
return yaml.safe_load(fm_text) or {}
|
|
201
|
+
except yaml.YAMLError:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _get_body(content: str) -> str:
|
|
206
|
+
"""Extract body content after frontmatter."""
|
|
207
|
+
if not _has_frontmatter(content):
|
|
208
|
+
return content
|
|
209
|
+
end = content.find("\n---", 3)
|
|
210
|
+
if end == -1:
|
|
211
|
+
return ""
|
|
212
|
+
return content[end + 4:].strip()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def validate_command_file(path: Path) -> tuple[list[str], list[str]]:
|
|
216
|
+
"""Validate a command markdown file.
|
|
217
|
+
|
|
218
|
+
Checks:
|
|
219
|
+
- YAML frontmatter present
|
|
220
|
+
- description field in frontmatter (non-empty)
|
|
221
|
+
- Body content not empty (warning)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
(errors, warnings) — two lists of message strings.
|
|
225
|
+
"""
|
|
226
|
+
errors: list[str] = []
|
|
227
|
+
warnings: list[str] = []
|
|
228
|
+
content = path.read_text()
|
|
229
|
+
|
|
230
|
+
fm = _parse_frontmatter(content)
|
|
231
|
+
if fm is None:
|
|
232
|
+
errors.append("Missing YAML frontmatter")
|
|
233
|
+
return errors, warnings
|
|
234
|
+
|
|
235
|
+
desc = fm.get("description")
|
|
236
|
+
if desc is None:
|
|
237
|
+
errors.append("Missing required frontmatter field: description")
|
|
238
|
+
elif not isinstance(desc, str) or not desc.strip():
|
|
239
|
+
errors.append("Frontmatter description must be a non-empty string")
|
|
240
|
+
|
|
241
|
+
body = _get_body(content)
|
|
242
|
+
if not body:
|
|
243
|
+
warnings.append("Command body is empty — consider adding content")
|
|
244
|
+
|
|
245
|
+
return errors, warnings
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
|
|
249
|
+
"""Validate skill registry and command files."""
|
|
250
|
+
report = ValidateReport(validator="skill-command")
|
|
251
|
+
|
|
252
|
+
# --- Skill registry validation ---
|
|
253
|
+
registry_errors, registry_warnings = validate_skill_registry(root)
|
|
254
|
+
|
|
255
|
+
for e in registry_errors:
|
|
256
|
+
report.errors += 1
|
|
257
|
+
report.details.append(f"[ERROR] skill-registry.yaml: {e}")
|
|
258
|
+
|
|
259
|
+
for w in registry_warnings:
|
|
260
|
+
if strict:
|
|
261
|
+
report.errors += 1
|
|
262
|
+
report.details.append(f"[ERROR] skill-registry.yaml: {w}")
|
|
263
|
+
else:
|
|
264
|
+
report.warnings += 1
|
|
265
|
+
report.details.append(f"[WARN] skill-registry.yaml: {w}")
|
|
266
|
+
|
|
267
|
+
if not registry_errors:
|
|
268
|
+
report.passed += 1
|
|
269
|
+
|
|
270
|
+
# --- Command file validation ---
|
|
271
|
+
commands_dir = root / "pennyfarthing-dist" / "commands"
|
|
272
|
+
command_files = discover_command_files(commands_dir)
|
|
273
|
+
|
|
274
|
+
for path in command_files:
|
|
275
|
+
file_errors, file_warnings = validate_command_file(path)
|
|
276
|
+
|
|
277
|
+
for e in file_errors:
|
|
278
|
+
report.errors += 1
|
|
279
|
+
report.details.append(f"[ERROR] {path.name}: {e}")
|
|
280
|
+
|
|
281
|
+
for w in file_warnings:
|
|
282
|
+
if strict:
|
|
283
|
+
report.errors += 1
|
|
284
|
+
report.details.append(f"[ERROR] {path.name}: {w}")
|
|
285
|
+
else:
|
|
286
|
+
report.warnings += 1
|
|
287
|
+
report.details.append(f"[WARN] {path.name}: {w}")
|
|
288
|
+
|
|
289
|
+
if not file_errors:
|
|
290
|
+
report.passed += 1
|
|
291
|
+
|
|
292
|
+
return report
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Sprint YAML validator adapter.
|
|
2
|
+
|
|
3
|
+
Auto-discovers sprint files and delegates to sprint/validate_cmd.validate_sprint_yaml().
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pennyfarthing_scripts.validate import ValidateReport
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _discover_files(root: Path) -> list[Path]:
|
|
14
|
+
"""Find all validatable YAML in sprint/."""
|
|
15
|
+
sprint_dir = root / "sprint"
|
|
16
|
+
if not sprint_dir.is_dir():
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
files: list[Path] = []
|
|
20
|
+
|
|
21
|
+
cs = sprint_dir / "current-sprint.yaml"
|
|
22
|
+
if cs.exists():
|
|
23
|
+
files.append(cs)
|
|
24
|
+
|
|
25
|
+
files.extend(sorted(sprint_dir.glob("epic-*.yaml")))
|
|
26
|
+
files.extend(sorted(sprint_dir.glob("initiative-*.yaml")))
|
|
27
|
+
|
|
28
|
+
future = sprint_dir / "future.yaml"
|
|
29
|
+
if future.exists():
|
|
30
|
+
files.append(future)
|
|
31
|
+
|
|
32
|
+
return files
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run(root: Path, *, fix: bool = False, strict: bool = False) -> ValidateReport:
|
|
36
|
+
"""Validate all sprint YAML files."""
|
|
37
|
+
from pennyfarthing_scripts.sprint.validate_cmd import validate_sprint_yaml
|
|
38
|
+
|
|
39
|
+
report = ValidateReport(validator="sprint")
|
|
40
|
+
files = _discover_files(root)
|
|
41
|
+
|
|
42
|
+
for path in files:
|
|
43
|
+
result = validate_sprint_yaml(path, fix=fix)
|
|
44
|
+
|
|
45
|
+
if result.errors:
|
|
46
|
+
report.errors += len(result.errors)
|
|
47
|
+
for err in result.errors:
|
|
48
|
+
line_info = f" (line {err.line})" if err.line else ""
|
|
49
|
+
report.details.append(
|
|
50
|
+
f"[{err.category.upper()}] {path.name}: {err.message}{line_info}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if result.format_issues:
|
|
54
|
+
for issue in result.format_issues:
|
|
55
|
+
if strict:
|
|
56
|
+
report.errors += 1
|
|
57
|
+
else:
|
|
58
|
+
report.warnings += 1
|
|
59
|
+
report.details.append(
|
|
60
|
+
f"[FORMAT] {path.name}: {issue.message}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if result.valid and not result.errors:
|
|
64
|
+
report.passed += 1
|
|
65
|
+
|
|
66
|
+
if result.fixed:
|
|
67
|
+
report.fixed = True
|
|
68
|
+
|
|
69
|
+
return report
|