@pennyfarthing/core 11.0.0 → 11.1.1
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 +81 -23
- package/package.json +1 -1
- package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts +20 -0
- package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js +278 -0
- package/packages/core/dist/cli/utils/010-detect-remove-old-packages.test.js.map +1 -0
- package/packages/core/dist/cli/utils/constants.d.ts +8 -2
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +4 -1
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/cli/utils/constants.test.d.ts +10 -0
- package/packages/core/dist/cli/utils/constants.test.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/constants.test.js +38 -0
- package/packages/core/dist/cli/utils/constants.test.js.map +1 -0
- package/packages/core/dist/consultation/consultation-protocol.d.ts +139 -0
- package/packages/core/dist/consultation/consultation-protocol.d.ts.map +1 -0
- package/packages/core/dist/consultation/consultation-protocol.js +178 -0
- package/packages/core/dist/consultation/consultation-protocol.js.map +1 -0
- package/packages/core/dist/consultation/consultation-protocol.test.d.ts +20 -0
- package/packages/core/dist/consultation/consultation-protocol.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/consultation-protocol.test.js +474 -0
- package/packages/core/dist/consultation/consultation-protocol.test.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.js +334 -0
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
- package/packages/core/dist/public/js/react/react.js +3 -3
- package/packages/core/dist/scripts/theme-detail.test.d.ts +10 -0
- package/packages/core/dist/scripts/theme-detail.test.js +199 -0
- package/packages/core/dist/server/api/git.d.ts +13 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +53 -34
- package/packages/core/dist/server/api/git.js.map +1 -1
- package/packages/core/dist/server/api/health-score.d.ts.map +1 -1
- package/packages/core/dist/server/api/health-score.js +25 -1
- package/packages/core/dist/server/api/health-score.js.map +1 -1
- package/packages/core/dist/server/api/settings.d.ts.map +1 -1
- package/packages/core/dist/server/api/settings.js +63 -1
- package/packages/core/dist/server/api/settings.js.map +1 -1
- package/packages/core/dist/server/api/theme-agents.d.ts.map +1 -1
- package/packages/core/dist/server/api/theme-agents.js +61 -0
- package/packages/core/dist/server/api/theme-agents.js.map +1 -1
- package/packages/core/dist/server/server.d.ts.map +1 -1
- package/packages/core/dist/server/server.js +17 -12
- package/packages/core/dist/server/server.js.map +1 -1
- package/packages/core/dist/shared/skill-search.test.js +2 -2
- package/packages/core/dist/workflow/gate-file-validation.d.ts +49 -0
- package/packages/core/dist/workflow/gate-file-validation.d.ts.map +1 -0
- package/packages/core/dist/workflow/gate-file-validation.js +157 -0
- package/packages/core/dist/workflow/gate-file-validation.js.map +1 -0
- package/packages/core/dist/workflow/gate-file-validation.test.d.ts +19 -0
- package/packages/core/dist/workflow/gate-file-validation.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/gate-file-validation.test.js +536 -0
- package/packages/core/dist/workflow/gate-file-validation.test.js.map +1 -0
- package/packages/core/dist/workflow/gate-schema-validation.test.d.ts +14 -0
- package/packages/core/dist/workflow/gate-schema-validation.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/gate-schema-validation.test.js +339 -0
- package/packages/core/dist/workflow/gate-schema-validation.test.js.map +1 -0
- package/packages/core/dist/workflow/handoff.js +2 -2
- package/packages/core/dist/workflow/handoff.js.map +1 -1
- package/packages/core/dist/workflow/handoff.test.js +16 -0
- package/packages/core/dist/workflow/handoff.test.js.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.d.ts +4 -2
- package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.js +43 -8
- package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
- package/pennyfarthing-dist/agents/README.md +6 -14
- package/pennyfarthing-dist/agents/architect.md +43 -29
- package/pennyfarthing-dist/agents/ba.md +30 -29
- package/pennyfarthing-dist/agents/dev.md +32 -43
- package/pennyfarthing-dist/agents/devops.md +57 -21
- package/pennyfarthing-dist/agents/orchestrator.md +3 -10
- package/pennyfarthing-dist/agents/pm.md +45 -31
- package/pennyfarthing-dist/agents/reviewer.md +20 -66
- package/pennyfarthing-dist/agents/sm-setup.md +2 -2
- package/pennyfarthing-dist/agents/sm.md +8 -30
- package/pennyfarthing-dist/agents/tea.md +25 -41
- package/pennyfarthing-dist/agents/tech-writer.md +33 -90
- package/pennyfarthing-dist/agents/ux-designer.md +39 -39
- package/pennyfarthing-dist/commands/benchmark-control.md +8 -64
- package/pennyfarthing-dist/commands/benchmark.md +8 -480
- package/pennyfarthing-dist/commands/job-fair.md +8 -97
- package/pennyfarthing-dist/commands/pf-benchmark-control.md +70 -0
- package/pennyfarthing-dist/commands/pf-benchmark.md +486 -0
- package/pennyfarthing-dist/commands/pf-chore.md +4 -4
- package/pennyfarthing-dist/commands/pf-ci.md +40 -0
- package/pennyfarthing-dist/commands/pf-close-epic.md +9 -27
- package/pennyfarthing-dist/commands/pf-continue-session.md +9 -213
- package/pennyfarthing-dist/commands/pf-create-branches-from-story.md +11 -353
- package/pennyfarthing-dist/commands/pf-docs.md +28 -0
- package/pennyfarthing-dist/commands/pf-epic.md +67 -0
- package/pennyfarthing-dist/commands/pf-git-cleanup.md +11 -52
- package/pennyfarthing-dist/commands/pf-git.md +75 -0
- package/pennyfarthing-dist/commands/pf-help.md +110 -128
- package/pennyfarthing-dist/commands/pf-job-fair.md +102 -0
- package/pennyfarthing-dist/commands/pf-new-work.md +9 -18
- package/pennyfarthing-dist/commands/pf-parallel-work.md +6 -66
- package/pennyfarthing-dist/commands/pf-release.md +11 -76
- package/pennyfarthing-dist/commands/pf-repo-status.md +11 -44
- package/pennyfarthing-dist/commands/pf-run-ci.md +8 -111
- package/pennyfarthing-dist/commands/pf-session.md +51 -0
- package/pennyfarthing-dist/commands/pf-solo.md +447 -0
- package/pennyfarthing-dist/commands/pf-sprint-planning.md +8 -104
- package/pennyfarthing-dist/commands/pf-standalone.md +1 -1
- package/pennyfarthing-dist/commands/pf-start-epic.md +9 -163
- package/pennyfarthing-dist/commands/pf-sync-epic-to-jira.md +8 -179
- package/pennyfarthing-dist/commands/pf-sync-work-with-sprint.md +8 -368
- package/pennyfarthing-dist/commands/pf-update-domain-docs.md +8 -78
- package/pennyfarthing-dist/commands/solo.md +8 -442
- package/pennyfarthing-dist/guides/agent-behavior.md +13 -13
- package/pennyfarthing-dist/guides/agent-coordination.md +7 -7
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -5
- package/pennyfarthing-dist/guides/bikerack.md +128 -0
- package/pennyfarthing-dist/guides/brownfield-tools.md +133 -0
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +2 -2
- package/pennyfarthing-dist/guides/gate-schema.md +227 -0
- package/pennyfarthing-dist/guides/gates.md +120 -0
- package/pennyfarthing-dist/guides/handoff-cli.md +116 -0
- package/pennyfarthing-dist/guides/hooks.md +86 -4
- package/pennyfarthing-dist/guides/output-styles.md +65 -0
- package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +5 -5
- package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -4
- package/pennyfarthing-dist/guides/prompt-patterns.md +5 -5
- package/pennyfarthing-dist/guides/reflector.md +4 -4
- package/pennyfarthing-dist/guides/session-artifacts.md +1 -1
- package/pennyfarthing-dist/guides/skill-schema.md +1 -1
- package/pennyfarthing-dist/guides/tandem-protocol.md +13 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +5 -4
- package/pennyfarthing-dist/personas/themes/hogans-heroes.yaml +11 -22
- package/pennyfarthing-dist/personas/themes/stephen-king.yaml +13 -24
- package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +19 -14
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +191 -57
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +26 -10
- package/pennyfarthing-dist/skills/pf-changelog/SKILL.md +4 -4
- package/pennyfarthing-dist/skills/pf-sprint/skill.md +1 -1
- package/pennyfarthing-dist/skills/skill-registry.schema.json +4 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +5 -0
- package/pennyfarthing-dist/workflows/2party-tdd.yaml +11 -0
- package/pennyfarthing-dist/workflows/agent-docs.yaml +2 -0
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +4 -0
- package/pennyfarthing-dist/workflows/bdd.yaml +4 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +3 -0
- package/pennyfarthing-dist/workflows/tdd.yaml +3 -0
- package/pennyfarthing-dist/workflows/trivial.yaml +2 -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__/context.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__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/cli.py +10 -11
- package/pennyfarthing_scripts/bikerack/debug_panel.py +218 -0
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +203 -27
- 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/cli.py +114 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/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/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/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/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/epic/__init__.py +0 -0
- package/pennyfarthing_scripts/epic/cli.py +64 -0
- package/pennyfarthing_scripts/gate/__init__.py +1 -0
- package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/gate/cli.py +56 -0
- package/pennyfarthing_scripts/gate/validate.py +266 -0
- 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_group/__init__.py +0 -0
- package/pennyfarthing_scripts/git_group/cli.py +100 -0
- package/pennyfarthing_scripts/handoff/__init__.py +1 -0
- package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/cli.py +120 -0
- package/pennyfarthing_scripts/handoff/complete_phase.py +155 -0
- package/pennyfarthing_scripts/handoff/gate_file.py +105 -0
- package/pennyfarthing_scripts/handoff/gate_runner.py +152 -0
- package/pennyfarthing_scripts/handoff/marker.py +109 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +152 -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/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/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/launch/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
- 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/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/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__/version_sentinel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/workflow.py +39 -0
- package/pennyfarthing_scripts/session/__init__.py +0 -0
- package/pennyfarthing_scripts/session/cli.py +87 -0
- 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__/epic_update.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/story_finish.py +14 -0
- 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/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_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.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_cli_normalization.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_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.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_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.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_resolve_gate_file_field.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_panel.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_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.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_version_sentinel.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/test_108_1_gate_migration.py +540 -0
- package/pennyfarthing_scripts/tests/test_108_2_remove_handoff_fallback.py +339 -0
- package/pennyfarthing_scripts/tests/test_confidence_sm_evaluation.py +253 -0
- package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +315 -0
- package/pennyfarthing_scripts/tests/test_gate_file_resolution.py +341 -0
- package/pennyfarthing_scripts/tests/test_gate_runner.py +620 -0
- package/pennyfarthing_scripts/tests/test_handoff_cli.py +929 -0
- package/pennyfarthing_scripts/tests/test_handoff_e2e.py +454 -0
- package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +464 -0
- package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +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/skill_command.py +200 -0
- package/pennyfarthing_scripts/validate/adapters/workflow.py +64 -0
- package/pennyfarthing_scripts/validate/cli.py +15 -4
- package/packages/core/dist/scripts/benchmark-integration.d.ts +0 -182
- package/packages/core/dist/scripts/benchmark-integration.d.ts.map +0 -1
- package/packages/core/dist/scripts/benchmark-integration.js +0 -691
- package/packages/core/dist/scripts/benchmark-integration.js.map +0 -1
- package/packages/core/dist/scripts/job-fair-aggregator.d.ts +0 -150
- package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +0 -1
- package/packages/core/dist/scripts/job-fair-aggregator.js +0 -547
- package/packages/core/dist/scripts/job-fair-aggregator.js.map +0 -1
- package/pennyfarthing-dist/agents/handoff.md +0 -250
- package/pennyfarthing-dist/agents/sm-handoff.md +0 -152
- package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -112
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.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,620 @@
|
|
|
1
|
+
"""Tests for gate subagent runner — Story 106-2.
|
|
2
|
+
|
|
3
|
+
Epic: 106 (Gate Files & First Migration)
|
|
4
|
+
Story: 106-2 — Gate subagent runner with GATE_RESULT contract
|
|
5
|
+
|
|
6
|
+
Tests parse_gate_file() and extract_gate_result() — the parsing layer
|
|
7
|
+
around gate file evaluation. The actual subagent spawning is handled by
|
|
8
|
+
the Claude agent; these functions parse inputs and outputs.
|
|
9
|
+
|
|
10
|
+
Acceptance Criteria:
|
|
11
|
+
- [AC1] Gate runner function accepts a gate file path and spawns it as a haiku Task subagent
|
|
12
|
+
- [AC2] Returns structured GATE_RESULT: {status: pass|fail, message, checks}
|
|
13
|
+
- [AC3] Supports model attribute (default: haiku, overridable per gate)
|
|
14
|
+
- [AC4] Default-deny: missing GATE_RESULT = fail
|
|
15
|
+
- [AC5] GATE_RESULT extraction uses regex/grep, not full YAML parser
|
|
16
|
+
- [AC6] Gate files are read-only at runtime (never written to)
|
|
17
|
+
- [AC7] Handles subagent timeouts and crashes gracefully (default to fail)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
from pennyfarthing_scripts.handoff.gate_runner import (
|
|
28
|
+
extract_gate_result,
|
|
29
|
+
parse_gate_file,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Fixtures: Gate file content
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
GATE_WITH_MODEL = """\
|
|
37
|
+
<gate name="tests-pass" model="haiku">
|
|
38
|
+
|
|
39
|
+
<purpose>
|
|
40
|
+
Verify that all tests pass and working tree is clean.
|
|
41
|
+
</purpose>
|
|
42
|
+
|
|
43
|
+
<pass>
|
|
44
|
+
Run tests and report results.
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
GATE_RESULT:
|
|
48
|
+
status: pass
|
|
49
|
+
message: "All tests passing"
|
|
50
|
+
```
|
|
51
|
+
</pass>
|
|
52
|
+
|
|
53
|
+
<fail>
|
|
54
|
+
Report failures.
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
GATE_RESULT:
|
|
58
|
+
status: fail
|
|
59
|
+
message: "Tests failing"
|
|
60
|
+
```
|
|
61
|
+
</fail>
|
|
62
|
+
|
|
63
|
+
</gate>
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
GATE_WITH_CUSTOM_MODEL = """\
|
|
67
|
+
<gate name="deep-review" model="sonnet">
|
|
68
|
+
|
|
69
|
+
<purpose>
|
|
70
|
+
Deep code review requiring more capable model.
|
|
71
|
+
</purpose>
|
|
72
|
+
|
|
73
|
+
<pass>
|
|
74
|
+
Review passes.
|
|
75
|
+
</pass>
|
|
76
|
+
|
|
77
|
+
<fail>
|
|
78
|
+
Review fails.
|
|
79
|
+
</fail>
|
|
80
|
+
|
|
81
|
+
</gate>
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
GATE_WITHOUT_MODEL = """\
|
|
85
|
+
<gate name="simple-check">
|
|
86
|
+
|
|
87
|
+
<purpose>
|
|
88
|
+
A gate with no model attribute — should default to haiku.
|
|
89
|
+
</purpose>
|
|
90
|
+
|
|
91
|
+
<pass>
|
|
92
|
+
Check passes.
|
|
93
|
+
</pass>
|
|
94
|
+
|
|
95
|
+
<fail>
|
|
96
|
+
Check fails.
|
|
97
|
+
</fail>
|
|
98
|
+
|
|
99
|
+
</gate>
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
GATE_MINIMAL = """\
|
|
103
|
+
<gate name="minimal" model="haiku">
|
|
104
|
+
<pass>Pass</pass>
|
|
105
|
+
<fail>Fail</fail>
|
|
106
|
+
</gate>
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.fixture
|
|
111
|
+
def gate_file(tmp_path: Path) -> Path:
|
|
112
|
+
"""Create a standard gate file with model="haiku"."""
|
|
113
|
+
p = tmp_path / "tests-pass.md"
|
|
114
|
+
p.write_text(GATE_WITH_MODEL)
|
|
115
|
+
return p
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.fixture
|
|
119
|
+
def gate_file_custom_model(tmp_path: Path) -> Path:
|
|
120
|
+
"""Create a gate file with model="sonnet"."""
|
|
121
|
+
p = tmp_path / "deep-review.md"
|
|
122
|
+
p.write_text(GATE_WITH_CUSTOM_MODEL)
|
|
123
|
+
return p
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.fixture
|
|
127
|
+
def gate_file_no_model(tmp_path: Path) -> Path:
|
|
128
|
+
"""Create a gate file with no model attribute."""
|
|
129
|
+
p = tmp_path / "simple-check.md"
|
|
130
|
+
p.write_text(GATE_WITHOUT_MODEL)
|
|
131
|
+
return p
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.fixture
|
|
135
|
+
def gate_file_minimal(tmp_path: Path) -> Path:
|
|
136
|
+
"""Create a minimal gate file."""
|
|
137
|
+
p = tmp_path / "minimal.md"
|
|
138
|
+
p.write_text(GATE_MINIMAL)
|
|
139
|
+
return p
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ===========================================================================
|
|
143
|
+
# AC1: Gate runner accepts a gate file path and parses it
|
|
144
|
+
# ===========================================================================
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestParseGateFileBasic:
|
|
148
|
+
"""AC1: parse_gate_file reads gate file and returns structured metadata."""
|
|
149
|
+
|
|
150
|
+
def test_returns_ok_status(self, gate_file: Path) -> None:
|
|
151
|
+
"""AC1: Successful parse returns status 'ok'."""
|
|
152
|
+
result = parse_gate_file(gate_file)
|
|
153
|
+
assert result["status"] == "ok"
|
|
154
|
+
|
|
155
|
+
def test_returns_gate_name(self, gate_file: Path) -> None:
|
|
156
|
+
"""AC1: Extracts gate name from <gate name="...">."""
|
|
157
|
+
result = parse_gate_file(gate_file)
|
|
158
|
+
assert result["name"] == "tests-pass"
|
|
159
|
+
|
|
160
|
+
def test_returns_gate_content(self, gate_file: Path) -> None:
|
|
161
|
+
"""AC1: Returns full gate file content for subagent prompt."""
|
|
162
|
+
result = parse_gate_file(gate_file)
|
|
163
|
+
assert result["content"] is not None
|
|
164
|
+
assert len(result["content"]) > 0
|
|
165
|
+
assert "<gate" in result["content"]
|
|
166
|
+
|
|
167
|
+
def test_content_matches_file(self, gate_file: Path) -> None:
|
|
168
|
+
"""AC1: Content matches what was written to file."""
|
|
169
|
+
result = parse_gate_file(gate_file)
|
|
170
|
+
assert result["content"] == GATE_WITH_MODEL
|
|
171
|
+
|
|
172
|
+
def test_returns_model(self, gate_file: Path) -> None:
|
|
173
|
+
"""AC1: Extracts model from <gate model="...">."""
|
|
174
|
+
result = parse_gate_file(gate_file)
|
|
175
|
+
assert result["model"] == "haiku"
|
|
176
|
+
|
|
177
|
+
def test_error_is_none_on_success(self, gate_file: Path) -> None:
|
|
178
|
+
"""AC1: Error field is None when parse succeeds."""
|
|
179
|
+
result = parse_gate_file(gate_file)
|
|
180
|
+
assert result["error"] is None
|
|
181
|
+
|
|
182
|
+
def test_result_has_required_keys(self, gate_file: Path) -> None:
|
|
183
|
+
"""AC1: Result dict has all required keys."""
|
|
184
|
+
result = parse_gate_file(gate_file)
|
|
185
|
+
assert "status" in result
|
|
186
|
+
assert "name" in result
|
|
187
|
+
assert "model" in result
|
|
188
|
+
assert "content" in result
|
|
189
|
+
assert "error" in result
|
|
190
|
+
|
|
191
|
+
def test_accepts_string_path(self, gate_file: Path) -> None:
|
|
192
|
+
"""AC1: Accepts both str and Path objects."""
|
|
193
|
+
result = parse_gate_file(str(gate_file))
|
|
194
|
+
assert result["status"] == "ok"
|
|
195
|
+
|
|
196
|
+
def test_nonexistent_file_returns_error(self, tmp_path: Path) -> None:
|
|
197
|
+
"""AC1: Missing file returns error status."""
|
|
198
|
+
result = parse_gate_file(tmp_path / "nonexistent.md")
|
|
199
|
+
assert result["status"] == "error"
|
|
200
|
+
assert result["error"] is not None
|
|
201
|
+
|
|
202
|
+
def test_empty_file_returns_error(self, tmp_path: Path) -> None:
|
|
203
|
+
"""AC1: Empty file with no <gate> tag returns error."""
|
|
204
|
+
p = tmp_path / "empty.md"
|
|
205
|
+
p.write_text("")
|
|
206
|
+
result = parse_gate_file(p)
|
|
207
|
+
assert result["status"] == "error"
|
|
208
|
+
|
|
209
|
+
def test_file_without_gate_tag_returns_error(self, tmp_path: Path) -> None:
|
|
210
|
+
"""AC1: File without <gate> tag returns error."""
|
|
211
|
+
p = tmp_path / "bad.md"
|
|
212
|
+
p.write_text("# Just a regular markdown file\nNo gate here.\n")
|
|
213
|
+
result = parse_gate_file(p)
|
|
214
|
+
assert result["status"] == "error"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ===========================================================================
|
|
218
|
+
# AC3: Model attribute — default haiku, overridable per gate
|
|
219
|
+
# ===========================================================================
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestParseGateFileModel:
|
|
223
|
+
"""AC3: Model attribute extraction with default fallback."""
|
|
224
|
+
|
|
225
|
+
def test_extracts_haiku_model(self, gate_file: Path) -> None:
|
|
226
|
+
"""AC3: model="haiku" extracted correctly."""
|
|
227
|
+
result = parse_gate_file(gate_file)
|
|
228
|
+
assert result["model"] == "haiku"
|
|
229
|
+
|
|
230
|
+
def test_extracts_custom_model(self, gate_file_custom_model: Path) -> None:
|
|
231
|
+
"""AC3: model="sonnet" extracted correctly."""
|
|
232
|
+
result = parse_gate_file(gate_file_custom_model)
|
|
233
|
+
assert result["model"] == "sonnet"
|
|
234
|
+
|
|
235
|
+
def test_defaults_to_haiku_when_no_model(
|
|
236
|
+
self, gate_file_no_model: Path
|
|
237
|
+
) -> None:
|
|
238
|
+
"""AC3: Missing model attribute defaults to 'haiku'."""
|
|
239
|
+
result = parse_gate_file(gate_file_no_model)
|
|
240
|
+
assert result["model"] == "haiku"
|
|
241
|
+
|
|
242
|
+
def test_extracts_name_when_no_model(
|
|
243
|
+
self, gate_file_no_model: Path
|
|
244
|
+
) -> None:
|
|
245
|
+
"""AC3: Gate name still extracted when model is missing."""
|
|
246
|
+
result = parse_gate_file(gate_file_no_model)
|
|
247
|
+
assert result["name"] == "simple-check"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ===========================================================================
|
|
251
|
+
# AC6: Gate files are read-only at runtime
|
|
252
|
+
# ===========================================================================
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestParseGateFileReadOnly:
|
|
256
|
+
"""AC6: Gate files must not be modified during parsing."""
|
|
257
|
+
|
|
258
|
+
def test_file_not_modified(self, gate_file: Path) -> None:
|
|
259
|
+
"""AC6: File mtime unchanged after parsing."""
|
|
260
|
+
mtime_before = os.path.getmtime(gate_file)
|
|
261
|
+
content_before = gate_file.read_text()
|
|
262
|
+
parse_gate_file(gate_file)
|
|
263
|
+
mtime_after = os.path.getmtime(gate_file)
|
|
264
|
+
content_after = gate_file.read_text()
|
|
265
|
+
assert mtime_before == mtime_after
|
|
266
|
+
assert content_before == content_after
|
|
267
|
+
|
|
268
|
+
def test_file_size_unchanged(self, gate_file: Path) -> None:
|
|
269
|
+
"""AC6: File size unchanged after parsing."""
|
|
270
|
+
size_before = gate_file.stat().st_size
|
|
271
|
+
parse_gate_file(gate_file)
|
|
272
|
+
size_after = gate_file.stat().st_size
|
|
273
|
+
assert size_before == size_after
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ===========================================================================
|
|
277
|
+
# AC2: Returns structured GATE_RESULT: {status, message, checks}
|
|
278
|
+
# ===========================================================================
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Subagent output samples for extract_gate_result tests
|
|
282
|
+
|
|
283
|
+
PASSING_OUTPUT = """\
|
|
284
|
+
I've run all the checks. Here are the results:
|
|
285
|
+
|
|
286
|
+
GATE_RESULT:
|
|
287
|
+
status: pass
|
|
288
|
+
gate: tests-pass
|
|
289
|
+
message: "All 47 tests passing. Working tree clean."
|
|
290
|
+
checks:
|
|
291
|
+
- name: test-suite
|
|
292
|
+
status: pass
|
|
293
|
+
detail: "47/47 tests passing (0 skipped)"
|
|
294
|
+
- name: working-tree
|
|
295
|
+
status: pass
|
|
296
|
+
detail: "No uncommitted changes"
|
|
297
|
+
- name: branch-status
|
|
298
|
+
status: pass
|
|
299
|
+
detail: "On branch feature/106-2, HEAD at abc1234"
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
FAILING_OUTPUT = """\
|
|
303
|
+
Some checks failed:
|
|
304
|
+
|
|
305
|
+
GATE_RESULT:
|
|
306
|
+
status: fail
|
|
307
|
+
gate: tests-pass
|
|
308
|
+
message: "3 tests failing, dirty working tree"
|
|
309
|
+
checks:
|
|
310
|
+
- name: test-suite
|
|
311
|
+
status: fail
|
|
312
|
+
detail: "44/47 tests passing, 3 failing"
|
|
313
|
+
- name: working-tree
|
|
314
|
+
status: fail
|
|
315
|
+
detail: "2 uncommitted files"
|
|
316
|
+
- name: branch-status
|
|
317
|
+
status: pass
|
|
318
|
+
detail: "On branch feature/106-2, HEAD at abc1234"
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
MINIMAL_PASS_OUTPUT = """\
|
|
322
|
+
GATE_RESULT:
|
|
323
|
+
status: pass
|
|
324
|
+
message: "All good"
|
|
325
|
+
checks: []
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
MINIMAL_FAIL_OUTPUT = """\
|
|
329
|
+
GATE_RESULT:
|
|
330
|
+
status: fail
|
|
331
|
+
message: "Something wrong"
|
|
332
|
+
checks: []
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class TestExtractGateResultPass:
|
|
337
|
+
"""AC2: Correctly extracts passing GATE_RESULT."""
|
|
338
|
+
|
|
339
|
+
def test_status_is_pass(self) -> None:
|
|
340
|
+
"""AC2: Passing output yields status 'pass'."""
|
|
341
|
+
result = extract_gate_result(PASSING_OUTPUT)
|
|
342
|
+
assert result["status"] == "pass"
|
|
343
|
+
|
|
344
|
+
def test_message_extracted(self) -> None:
|
|
345
|
+
"""AC2: Message field extracted from output."""
|
|
346
|
+
result = extract_gate_result(PASSING_OUTPUT)
|
|
347
|
+
assert "47 tests passing" in result["message"]
|
|
348
|
+
|
|
349
|
+
def test_checks_is_list(self) -> None:
|
|
350
|
+
"""AC2: Checks field is a list."""
|
|
351
|
+
result = extract_gate_result(PASSING_OUTPUT)
|
|
352
|
+
assert isinstance(result["checks"], list)
|
|
353
|
+
|
|
354
|
+
def test_checks_have_required_fields(self) -> None:
|
|
355
|
+
"""AC2: Each check has name, status, detail."""
|
|
356
|
+
result = extract_gate_result(PASSING_OUTPUT)
|
|
357
|
+
for check in result["checks"]:
|
|
358
|
+
assert "name" in check
|
|
359
|
+
assert "status" in check
|
|
360
|
+
assert "detail" in check
|
|
361
|
+
|
|
362
|
+
def test_check_count(self) -> None:
|
|
363
|
+
"""AC2: Correct number of checks extracted."""
|
|
364
|
+
result = extract_gate_result(PASSING_OUTPUT)
|
|
365
|
+
assert len(result["checks"]) == 3
|
|
366
|
+
|
|
367
|
+
def test_check_names(self) -> None:
|
|
368
|
+
"""AC2: Check names match expected values."""
|
|
369
|
+
result = extract_gate_result(PASSING_OUTPUT)
|
|
370
|
+
names = [c["name"] for c in result["checks"]]
|
|
371
|
+
assert "test-suite" in names
|
|
372
|
+
assert "working-tree" in names
|
|
373
|
+
assert "branch-status" in names
|
|
374
|
+
|
|
375
|
+
def test_all_checks_pass(self) -> None:
|
|
376
|
+
"""AC2: All checks have status 'pass' in passing output."""
|
|
377
|
+
result = extract_gate_result(PASSING_OUTPUT)
|
|
378
|
+
for check in result["checks"]:
|
|
379
|
+
assert check["status"] == "pass"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class TestExtractGateResultFail:
|
|
383
|
+
"""AC2: Correctly extracts failing GATE_RESULT."""
|
|
384
|
+
|
|
385
|
+
def test_status_is_fail(self) -> None:
|
|
386
|
+
"""AC2: Failing output yields status 'fail'."""
|
|
387
|
+
result = extract_gate_result(FAILING_OUTPUT)
|
|
388
|
+
assert result["status"] == "fail"
|
|
389
|
+
|
|
390
|
+
def test_message_extracted(self) -> None:
|
|
391
|
+
"""AC2: Failure message extracted."""
|
|
392
|
+
result = extract_gate_result(FAILING_OUTPUT)
|
|
393
|
+
assert "failing" in result["message"].lower()
|
|
394
|
+
|
|
395
|
+
def test_mixed_check_statuses(self) -> None:
|
|
396
|
+
"""AC2: Individual checks can have different statuses."""
|
|
397
|
+
result = extract_gate_result(FAILING_OUTPUT)
|
|
398
|
+
statuses = {c["name"]: c["status"] for c in result["checks"]}
|
|
399
|
+
assert statuses["test-suite"] == "fail"
|
|
400
|
+
assert statuses["working-tree"] == "fail"
|
|
401
|
+
assert statuses["branch-status"] == "pass"
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class TestExtractGateResultMinimal:
|
|
405
|
+
"""AC2: Handles minimal GATE_RESULT (no checks)."""
|
|
406
|
+
|
|
407
|
+
def test_minimal_pass(self) -> None:
|
|
408
|
+
"""AC2: Minimal pass output with empty checks."""
|
|
409
|
+
result = extract_gate_result(MINIMAL_PASS_OUTPUT)
|
|
410
|
+
assert result["status"] == "pass"
|
|
411
|
+
assert isinstance(result["checks"], list)
|
|
412
|
+
|
|
413
|
+
def test_minimal_fail(self) -> None:
|
|
414
|
+
"""AC2: Minimal fail output with empty checks."""
|
|
415
|
+
result = extract_gate_result(MINIMAL_FAIL_OUTPUT)
|
|
416
|
+
assert result["status"] == "fail"
|
|
417
|
+
|
|
418
|
+
def test_result_has_required_keys(self) -> None:
|
|
419
|
+
"""AC2: Result always has status, message, checks."""
|
|
420
|
+
result = extract_gate_result(MINIMAL_PASS_OUTPUT)
|
|
421
|
+
assert "status" in result
|
|
422
|
+
assert "message" in result
|
|
423
|
+
assert "checks" in result
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ===========================================================================
|
|
427
|
+
# AC4: Default-deny — missing GATE_RESULT = fail
|
|
428
|
+
# ===========================================================================
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class TestExtractGateResultDefaultDeny:
|
|
432
|
+
"""AC4: Missing or unparseable GATE_RESULT always returns fail."""
|
|
433
|
+
|
|
434
|
+
def test_none_output_returns_fail(self) -> None:
|
|
435
|
+
"""AC4: None input (crash/timeout) → fail."""
|
|
436
|
+
result = extract_gate_result(None)
|
|
437
|
+
assert result["status"] == "fail"
|
|
438
|
+
|
|
439
|
+
def test_empty_string_returns_fail(self) -> None:
|
|
440
|
+
"""AC4: Empty string output → fail."""
|
|
441
|
+
result = extract_gate_result("")
|
|
442
|
+
assert result["status"] == "fail"
|
|
443
|
+
|
|
444
|
+
def test_no_gate_result_block_returns_fail(self) -> None:
|
|
445
|
+
"""AC4: Output without GATE_RESULT → fail."""
|
|
446
|
+
result = extract_gate_result("I ran the tests and everything looks good!")
|
|
447
|
+
assert result["status"] == "fail"
|
|
448
|
+
|
|
449
|
+
def test_partial_gate_result_no_status_returns_fail(self) -> None:
|
|
450
|
+
"""AC4: GATE_RESULT without status field → fail."""
|
|
451
|
+
output = "GATE_RESULT:\n message: 'partial result'\n checks: []\n"
|
|
452
|
+
result = extract_gate_result(output)
|
|
453
|
+
assert result["status"] == "fail"
|
|
454
|
+
|
|
455
|
+
def test_malformed_yaml_returns_fail(self) -> None:
|
|
456
|
+
"""AC4: Malformed GATE_RESULT block → fail."""
|
|
457
|
+
output = "GATE_RESULT:\n status: [[[invalid yaml\n"
|
|
458
|
+
result = extract_gate_result(output)
|
|
459
|
+
assert result["status"] == "fail"
|
|
460
|
+
|
|
461
|
+
def test_default_deny_has_message(self) -> None:
|
|
462
|
+
"""AC4: Default-deny result includes explanatory message."""
|
|
463
|
+
result = extract_gate_result(None)
|
|
464
|
+
assert result["message"] is not None
|
|
465
|
+
assert len(result["message"]) > 0
|
|
466
|
+
|
|
467
|
+
def test_default_deny_has_empty_checks(self) -> None:
|
|
468
|
+
"""AC4: Default-deny result has empty checks list."""
|
|
469
|
+
result = extract_gate_result(None)
|
|
470
|
+
assert result["checks"] == []
|
|
471
|
+
|
|
472
|
+
def test_status_typo_returns_fail(self) -> None:
|
|
473
|
+
"""AC4: status: 'passed' (not 'pass') → fail (strict enum)."""
|
|
474
|
+
output = "GATE_RESULT:\n status: passed\n message: 'oops'\n checks: []\n"
|
|
475
|
+
result = extract_gate_result(output)
|
|
476
|
+
assert result["status"] == "fail"
|
|
477
|
+
|
|
478
|
+
def test_status_uppercase_returns_fail(self) -> None:
|
|
479
|
+
"""AC4: status: PASS (uppercase) → fail (strict matching)."""
|
|
480
|
+
output = "GATE_RESULT:\n status: PASS\n message: 'oops'\n checks: []\n"
|
|
481
|
+
result = extract_gate_result(output)
|
|
482
|
+
assert result["status"] == "fail"
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# ===========================================================================
|
|
486
|
+
# AC5: GATE_RESULT extraction uses regex, not full YAML parser
|
|
487
|
+
# ===========================================================================
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class TestExtractGateResultRegex:
|
|
491
|
+
"""AC5: Extraction uses regex patterns, handles various formats."""
|
|
492
|
+
|
|
493
|
+
def test_gate_result_embedded_in_prose(self) -> None:
|
|
494
|
+
"""AC5: GATE_RESULT found even when surrounded by other text."""
|
|
495
|
+
output = (
|
|
496
|
+
"Here is my analysis of the codebase.\n"
|
|
497
|
+
"I found several issues.\n\n"
|
|
498
|
+
"GATE_RESULT:\n"
|
|
499
|
+
" status: fail\n"
|
|
500
|
+
' message: "Found issues"\n'
|
|
501
|
+
" checks:\n"
|
|
502
|
+
" - name: lint\n"
|
|
503
|
+
" status: fail\n"
|
|
504
|
+
' detail: "3 lint errors"\n'
|
|
505
|
+
"\nLet me know if you need more details."
|
|
506
|
+
)
|
|
507
|
+
result = extract_gate_result(output)
|
|
508
|
+
assert result["status"] == "fail"
|
|
509
|
+
|
|
510
|
+
def test_gate_result_in_code_block(self) -> None:
|
|
511
|
+
"""AC5: GATE_RESULT inside a code block still extracted."""
|
|
512
|
+
output = (
|
|
513
|
+
"```yaml\n"
|
|
514
|
+
"GATE_RESULT:\n"
|
|
515
|
+
" status: pass\n"
|
|
516
|
+
' message: "All clear"\n'
|
|
517
|
+
" checks: []\n"
|
|
518
|
+
"```\n"
|
|
519
|
+
)
|
|
520
|
+
result = extract_gate_result(output)
|
|
521
|
+
assert result["status"] == "pass"
|
|
522
|
+
|
|
523
|
+
def test_handles_extra_whitespace(self) -> None:
|
|
524
|
+
"""AC5: Extra whitespace in GATE_RESULT still parses."""
|
|
525
|
+
output = (
|
|
526
|
+
"GATE_RESULT:\n"
|
|
527
|
+
" status: pass\n"
|
|
528
|
+
' message: "Tests OK"\n'
|
|
529
|
+
" checks: []\n"
|
|
530
|
+
)
|
|
531
|
+
result = extract_gate_result(output)
|
|
532
|
+
assert result["status"] == "pass"
|
|
533
|
+
|
|
534
|
+
def test_handles_unquoted_message(self) -> None:
|
|
535
|
+
"""AC5: Message without quotes still extracted."""
|
|
536
|
+
output = (
|
|
537
|
+
"GATE_RESULT:\n"
|
|
538
|
+
" status: pass\n"
|
|
539
|
+
" message: All tests passing\n"
|
|
540
|
+
" checks: []\n"
|
|
541
|
+
)
|
|
542
|
+
result = extract_gate_result(output)
|
|
543
|
+
assert result["status"] == "pass"
|
|
544
|
+
assert len(result["message"]) > 0
|
|
545
|
+
|
|
546
|
+
def test_handles_single_quoted_message(self) -> None:
|
|
547
|
+
"""AC5: Single-quoted message extracted."""
|
|
548
|
+
output = (
|
|
549
|
+
"GATE_RESULT:\n"
|
|
550
|
+
" status: pass\n"
|
|
551
|
+
" message: 'All tests passing'\n"
|
|
552
|
+
" checks: []\n"
|
|
553
|
+
)
|
|
554
|
+
result = extract_gate_result(output)
|
|
555
|
+
assert result["status"] == "pass"
|
|
556
|
+
assert "All tests passing" in result["message"]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# ===========================================================================
|
|
560
|
+
# AC7: Handles subagent timeouts and crashes gracefully
|
|
561
|
+
# ===========================================================================
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class TestExtractGateResultEdgeCases:
|
|
565
|
+
"""AC7: Graceful handling of timeouts, crashes, and edge cases."""
|
|
566
|
+
|
|
567
|
+
def test_timeout_marker_returns_fail(self) -> None:
|
|
568
|
+
"""AC7: Output indicating timeout → fail."""
|
|
569
|
+
output = "Error: Task timed out after 120 seconds"
|
|
570
|
+
result = extract_gate_result(output)
|
|
571
|
+
assert result["status"] == "fail"
|
|
572
|
+
|
|
573
|
+
def test_very_long_output_still_extracts(self) -> None:
|
|
574
|
+
"""AC7: Large output doesn't break extraction."""
|
|
575
|
+
padding = "x" * 10000
|
|
576
|
+
output = (
|
|
577
|
+
f"{padding}\n"
|
|
578
|
+
"GATE_RESULT:\n"
|
|
579
|
+
" status: pass\n"
|
|
580
|
+
' message: "Found after long output"\n'
|
|
581
|
+
" checks: []\n"
|
|
582
|
+
f"\n{padding}"
|
|
583
|
+
)
|
|
584
|
+
result = extract_gate_result(output)
|
|
585
|
+
assert result["status"] == "pass"
|
|
586
|
+
|
|
587
|
+
def test_multiple_gate_results_takes_last(self) -> None:
|
|
588
|
+
"""AC7: If multiple GATE_RESULT blocks, take the last one."""
|
|
589
|
+
output = (
|
|
590
|
+
"GATE_RESULT:\n"
|
|
591
|
+
" status: fail\n"
|
|
592
|
+
' message: "First attempt failed"\n'
|
|
593
|
+
" checks: []\n"
|
|
594
|
+
"\nRetrying...\n\n"
|
|
595
|
+
"GATE_RESULT:\n"
|
|
596
|
+
" status: pass\n"
|
|
597
|
+
' message: "Second attempt passed"\n'
|
|
598
|
+
" checks: []\n"
|
|
599
|
+
)
|
|
600
|
+
result = extract_gate_result(output)
|
|
601
|
+
assert result["status"] == "pass"
|
|
602
|
+
assert "Second" in result["message"] or "passed" in result["message"]
|
|
603
|
+
|
|
604
|
+
def test_only_whitespace_returns_fail(self) -> None:
|
|
605
|
+
"""AC7: Whitespace-only output → fail."""
|
|
606
|
+
result = extract_gate_result(" \n\n\t \n")
|
|
607
|
+
assert result["status"] == "fail"
|
|
608
|
+
|
|
609
|
+
def test_gate_result_with_extra_fields_ignored(self) -> None:
|
|
610
|
+
"""AC7: Extra fields in GATE_RESULT don't break extraction."""
|
|
611
|
+
output = (
|
|
612
|
+
"GATE_RESULT:\n"
|
|
613
|
+
" status: pass\n"
|
|
614
|
+
" gate: tests-pass\n"
|
|
615
|
+
' message: "All good"\n'
|
|
616
|
+
" extra_field: ignored\n"
|
|
617
|
+
" checks: []\n"
|
|
618
|
+
)
|
|
619
|
+
result = extract_gate_result(output)
|
|
620
|
+
assert result["status"] == "pass"
|